dsh-gitnexus-plus 0.1.3 → 0.1.5

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.
Files changed (84) hide show
  1. package/README-CN.md +34 -10
  2. package/README.md +79 -14
  3. package/lib/bin/graph-helper.d.ts +21 -0
  4. package/lib/bin/graph-helper.d.ts.map +1 -0
  5. package/lib/bin/graph-helper.js +71 -0
  6. package/lib/bin/graph-helper.js.map +1 -0
  7. package/lib/client.js +4402 -2371
  8. package/lib/gitnexus/analyze.d.ts +54 -8
  9. package/lib/gitnexus/analyze.d.ts.map +1 -1
  10. package/lib/gitnexus/analyze.js +355 -84
  11. package/lib/gitnexus/analyze.js.map +1 -1
  12. package/lib/gitnexus/commands.d.ts +19 -2
  13. package/lib/gitnexus/commands.d.ts.map +1 -1
  14. package/lib/gitnexus/commands.js +42 -4
  15. package/lib/gitnexus/commands.js.map +1 -1
  16. package/lib/gitnexus/coverage.d.ts +8 -0
  17. package/lib/gitnexus/coverage.d.ts.map +1 -1
  18. package/lib/gitnexus/coverage.js +14 -2
  19. package/lib/gitnexus/coverage.js.map +1 -1
  20. package/lib/gitnexus/delta.d.ts +144 -0
  21. package/lib/gitnexus/delta.d.ts.map +1 -0
  22. package/lib/gitnexus/delta.js +347 -0
  23. package/lib/gitnexus/delta.js.map +1 -0
  24. package/lib/gitnexus/discover.d.ts +91 -0
  25. package/lib/gitnexus/discover.d.ts.map +1 -0
  26. package/lib/gitnexus/discover.js +214 -0
  27. package/lib/gitnexus/discover.js.map +1 -0
  28. package/lib/gitnexus/git-activity.d.ts +13 -21
  29. package/lib/gitnexus/git-activity.d.ts.map +1 -1
  30. package/lib/gitnexus/git-activity.js +60 -26
  31. package/lib/gitnexus/git-activity.js.map +1 -1
  32. package/lib/gitnexus/graph.d.ts +135 -0
  33. package/lib/gitnexus/graph.d.ts.map +1 -0
  34. package/lib/gitnexus/graph.js +474 -0
  35. package/lib/gitnexus/graph.js.map +1 -0
  36. package/lib/gitnexus/index-meta.d.ts +34 -6
  37. package/lib/gitnexus/index-meta.d.ts.map +1 -1
  38. package/lib/gitnexus/index-meta.js +47 -7
  39. package/lib/gitnexus/index-meta.js.map +1 -1
  40. package/lib/gitnexus/install.d.ts +11 -0
  41. package/lib/gitnexus/install.d.ts.map +1 -1
  42. package/lib/gitnexus/install.js +65 -9
  43. package/lib/gitnexus/install.js.map +1 -1
  44. package/lib/gitnexus/root.d.ts +0 -3
  45. package/lib/gitnexus/root.d.ts.map +1 -1
  46. package/lib/gitnexus/root.js +64 -5
  47. package/lib/gitnexus/root.js.map +1 -1
  48. package/lib/gitnexus/search.d.ts +34 -8
  49. package/lib/gitnexus/search.d.ts.map +1 -1
  50. package/lib/gitnexus/search.js +89 -13
  51. package/lib/gitnexus/search.js.map +1 -1
  52. package/lib/index.d.ts +13 -0
  53. package/lib/index.d.ts.map +1 -1
  54. package/lib/index.js +189 -7
  55. package/lib/index.js.map +1 -1
  56. package/lib/routes/analyze.d.ts +8 -6
  57. package/lib/routes/analyze.d.ts.map +1 -1
  58. package/lib/routes/analyze.js +25 -6
  59. package/lib/routes/analyze.js.map +1 -1
  60. package/lib/routes/coverage.d.ts +16 -10
  61. package/lib/routes/coverage.d.ts.map +1 -1
  62. package/lib/routes/coverage.js +31 -22
  63. package/lib/routes/coverage.js.map +1 -1
  64. package/lib/routes/delta.d.ts +22 -0
  65. package/lib/routes/delta.d.ts.map +1 -0
  66. package/lib/routes/delta.js +50 -0
  67. package/lib/routes/delta.js.map +1 -0
  68. package/lib/routes/file-diff.d.ts +19 -0
  69. package/lib/routes/file-diff.d.ts.map +1 -0
  70. package/lib/routes/file-diff.js +69 -0
  71. package/lib/routes/file-diff.js.map +1 -0
  72. package/lib/routes/graph.d.ts +22 -0
  73. package/lib/routes/graph.d.ts.map +1 -0
  74. package/lib/routes/graph.js +71 -0
  75. package/lib/routes/graph.js.map +1 -0
  76. package/lib/routes/guard.d.ts +3 -6
  77. package/lib/routes/guard.d.ts.map +1 -1
  78. package/lib/routes/guard.js +3 -6
  79. package/lib/routes/guard.js.map +1 -1
  80. package/lib/routes/projects.d.ts +21 -0
  81. package/lib/routes/projects.d.ts.map +1 -0
  82. package/lib/routes/projects.js +61 -0
  83. package/lib/routes/projects.js.map +1 -0
  84. package/package.json +2 -2
@@ -0,0 +1,347 @@
1
+ /**
2
+ * Tier 0 staleness: is a refresh worth running at all?
3
+ *
4
+ * WHY THIS EXISTS
5
+ * ---------------
6
+ * The panel's only maintenance action used to be "run `gitnexus analyze`", and it ran
7
+ * the analyzer every time — on a repository where nothing had changed since the index
8
+ * was written. That is not a correctness bug, it is a cost bug with a bad shape: the
9
+ * work is measured in seconds on a small repository and in minutes on a large one, and
10
+ * the user has no way to know beforehand whether they are about to spend it.
11
+ *
12
+ * The index already records what it saw: `.gitnexus/gitnexus.json` carries
13
+ * `fileHashes`, a map of repo-relative path to sha256. Comparing the working tree
14
+ * against those hashes answers "would a re-analysis change anything?" exactly, and it
15
+ * needs nothing but git and a hash function.
16
+ *
17
+ * WHAT IS COMPARED, AND AGAINST WHAT
18
+ * ----------------------------------
19
+ * The baseline is the **index snapshot**, not `HEAD`. Those differ whenever the index
20
+ * was built from a dirty tree, and the manifest records that case in
21
+ * `indexCoverage.dirtyPaths`. Comparing against `HEAD` would call a file unchanged
22
+ * merely because it was committed, even though the index remembered different content
23
+ * — which is the direction that produces a stale graph the panel says is current.
24
+ *
25
+ * The candidate set is therefore everything that differs from the *indexed commit*:
26
+ *
27
+ * `git diff --no-renames --name-only <lastCommit>` tracked files, working tree vs
28
+ * that commit (staged and unstaged)
29
+ * `git ls-files --others --exclude-standard` untracked files git would include
30
+ *
31
+ * `--no-renames` on purpose: with rename detection on, git reports only the new path,
32
+ * so a rename looks like a pure addition and the index's old path silently disappears
33
+ * from the comparison. Forcing add+delete pairs lets each side be classified on its
34
+ * own evidence.
35
+ *
36
+ * Hash comparison is per candidate, not per indexed file, which is what keeps this
37
+ * cheap: a clean working tree produces an empty candidate set and no file is read at
38
+ * all. `mtime` is *not* used as a prefilter even though the plan called for one — the
39
+ * manifest stores a hash and no mtime, so "the mtime did not move" cannot prove the
40
+ * content matches, and using it would trade an exact answer for a fast wrong one.
41
+ *
42
+ * WHAT IT DOES NOT DO
43
+ * -------------------
44
+ * It never runs the analyzer, never writes, and never reads the graph. It also does not
45
+ * decide *which* tier of refresh to run — that needs the analyzer's own judgement about
46
+ * schema and FTS state. It answers one question: has indexed content changed?
47
+ */
48
+ import { createHash } from 'node:crypto';
49
+ import { createReadStream } from 'node:fs';
50
+ import fs from 'node:fs/promises';
51
+ import path from 'node:path';
52
+ import { execFile } from 'node:child_process';
53
+ import { promisify } from 'node:util';
54
+ import { formatBytes } from './coverage.js';
55
+ import { readIndexMeta } from './index-meta.js';
56
+ /**
57
+ * `execFile`, promisified, resolved as decoded text.
58
+ *
59
+ * No shell: the repository path is caller-supplied and only ever travels as `cwd`.
60
+ * `-z` output is NUL-separated rather than newline-separated precisely because a path
61
+ * may contain a newline, and `git status` would otherwise quote it into an unparseable
62
+ * form.
63
+ */
64
+ const run = promisify(execFile);
65
+ const DEFAULT_MAX_FILES = 4000;
66
+ const DEFAULT_HASH_CONCURRENCY = 8;
67
+ const DEFAULT_GIT_TIMEOUT_MS = 15_000;
68
+ /** Run one git command, or report that git could not answer. */
69
+ async function git(repoPath, args, timeoutMs) {
70
+ try {
71
+ const { stdout } = await run('git', args, {
72
+ cwd: repoPath,
73
+ timeout: timeoutMs,
74
+ windowsHide: true,
75
+ // Name lists on a very large repository can exceed the default 1 MB cap. The
76
+ // cap exists so a runaway command cannot exhaust memory; 32 MB of paths is
77
+ // already an unreasonable candidate set, and `maxFiles` truncates it anyway.
78
+ maxBuffer: 32 * 1024 * 1024,
79
+ });
80
+ return stdout;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ /** Split `-z` output on NUL and drop the trailing empty record. */
87
+ function splitNul(text) {
88
+ return text.split('\0').filter((part) => part !== '');
89
+ }
90
+ /** The key form `fileHashes` uses: forward slashes, no leading `./`. */
91
+ function toKey(relativePath) {
92
+ return relativePath.replace(/\\/g, '/').replace(/^\.\//, '');
93
+ }
94
+ /** Stream one file through sha256. Returns null when it cannot be read. */
95
+ async function hashFile(absolutePath) {
96
+ try {
97
+ const hash = createHash('sha256');
98
+ await new Promise((resolvePromise, reject) => {
99
+ const stream = createReadStream(absolutePath);
100
+ stream.on('error', reject);
101
+ stream.on('data', (chunk) => hash.update(chunk));
102
+ stream.on('end', () => resolvePromise());
103
+ });
104
+ return hash.digest('hex');
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ /** The counts of an empty report, so every early return carries the same shape. */
111
+ const ZERO_COUNTS = {
112
+ checked: 0,
113
+ hashed: 0,
114
+ unchanged: 0,
115
+ modified: 0,
116
+ added: 0,
117
+ deleted: 0,
118
+ excluded: 0,
119
+ };
120
+ /**
121
+ * Compare the working tree against the content the index recorded.
122
+ *
123
+ * @param repoPath absolute repository root
124
+ */
125
+ export async function readDelta(repoPath, options = {}) {
126
+ const now = options.now ?? (() => Date.now());
127
+ const startedAt = now();
128
+ const absRepo = path.resolve(repoPath);
129
+ const maxFiles = options.maxFiles ?? DEFAULT_MAX_FILES;
130
+ const concurrency = Math.max(1, options.hashConcurrency ?? DEFAULT_HASH_CONCURRENCY);
131
+ const gitTimeoutMs = options.gitTimeoutMs ?? DEFAULT_GIT_TIMEOUT_MS;
132
+ const empty = (patch) => ({
133
+ repoPath: absRepo,
134
+ indexed: false,
135
+ comparable: false,
136
+ basis: null,
137
+ indexedCommit: null,
138
+ headCommit: null,
139
+ entries: [],
140
+ counts: { ...ZERO_COUNTS },
141
+ needsRefresh: false,
142
+ certain: false,
143
+ truncated: false,
144
+ notes: [],
145
+ durationMs: now() - startedAt,
146
+ readAt: new Date().toISOString(),
147
+ ...patch,
148
+ });
149
+ const meta = await readIndexMeta(absRepo);
150
+ if (meta === null) {
151
+ return empty({ notes: ['该项目还没有 GitNexus 索引,先初始化再说'] });
152
+ }
153
+ const indexedCommit = meta.lastCommit === '' ? null : meta.lastCommit;
154
+ const base = { indexed: true, indexedCommit };
155
+ // The map is normalized-key -> original key, and the hash is read through the
156
+ // original: `fileHashes` is written by GitNexus, and a manifest whose keys use
157
+ // backslashes (Windows builds did) must still match the forward-slash key git
158
+ // reports. Rewriting the manifest's keys instead would mutate what we only read.
159
+ const hashByKey = new Map();
160
+ for (const [rawPath, hash] of Object.entries(meta.fileHashes)) {
161
+ hashByKey.set(toKey(rawPath), hash.toLowerCase());
162
+ }
163
+ if (hashByKey.size === 0) {
164
+ return empty({
165
+ ...base,
166
+ notes: ['该索引没有记录文件哈希(旧格式 manifest),无法判断内容是否变化 —— 直接刷新'],
167
+ });
168
+ }
169
+ const headCommit = (await git(absRepo, ['rev-parse', 'HEAD'], gitTimeoutMs))?.trim() ?? null;
170
+ let basis = null;
171
+ let candidates = [];
172
+ if (indexedCommit !== null && headCommit !== null) {
173
+ const diff = await git(absRepo, ['diff', '--no-renames', '--name-only', '-z', indexedCommit, '--'], gitTimeoutMs);
174
+ if (diff !== null) {
175
+ basis = 'index';
176
+ candidates = splitNul(diff);
177
+ }
178
+ }
179
+ if (basis === null && headCommit !== null) {
180
+ // The recorded commit is gone (rebase, branch rewrite, shallow clone) or the
181
+ // manifest never had one. Working tree vs HEAD is the honest fallback: it finds
182
+ // uncommitted and newly committed files, and it cannot see a difference that was
183
+ // committed before the index was written. Say so rather than implying otherwise.
184
+ const againstHead = await git(absRepo, ['diff', '--no-renames', '--name-only', '-z', 'HEAD', '--'], gitTimeoutMs);
185
+ if (againstHead !== null) {
186
+ basis = 'worktree';
187
+ candidates = splitNul(againstHead);
188
+ }
189
+ }
190
+ if (basis === null) {
191
+ return empty({
192
+ ...base,
193
+ headCommit,
194
+ notes: [
195
+ indexedCommit === null
196
+ ? '索引没有记录提交基线,且 git 也无法读取 HEAD —— 无法比较,直接刷新'
197
+ : '索引记录的提交在本地 git 历史中已不可用,且无法读取 HEAD —— 无法比较,直接刷新',
198
+ ],
199
+ });
200
+ }
201
+ const untracked = await git(absRepo, ['ls-files', '--others', '--exclude-standard', '-z'], gitTimeoutMs);
202
+ if (untracked !== null) {
203
+ candidates.push(...splitNul(untracked));
204
+ }
205
+ const seen = new Set();
206
+ const unique = [];
207
+ for (const candidate of candidates) {
208
+ const key = toKey(candidate);
209
+ if (key === '' || seen.has(key))
210
+ continue;
211
+ // The index's own storage is never indexed content. `.gitnexus/` self-ignores in
212
+ // most repositories (GitNexus writes a `*` .gitignore there), so this only matters
213
+ // where it does not — but in exactly that case every read would report the index
214
+ // manifest itself as a new file and ask for a refresh forever.
215
+ if (key.startsWith('.gitnexus/') || key.startsWith('.git/'))
216
+ continue;
217
+ seen.add(key);
218
+ unique.push(key);
219
+ }
220
+ unique.sort();
221
+ const truncated = unique.length > maxFiles;
222
+ const examined = truncated ? unique.slice(0, maxFiles) : unique;
223
+ const entries = [];
224
+ const counts = { ...ZERO_COUNTS };
225
+ counts.checked = examined.length;
226
+ // Bounded parallelism rather than one read at a time: a 4000-file candidate set read
227
+ // sequentially is dominated by syscall latency, and these are local files.
228
+ for (let offset = 0; offset < examined.length; offset += concurrency) {
229
+ const batch = examined.slice(offset, offset + concurrency);
230
+ const settled = await Promise.all(batch.map(async (key) => {
231
+ const absolute = path.join(absRepo, key);
232
+ let size;
233
+ try {
234
+ const stat = await fs.stat(absolute);
235
+ if (!stat.isFile())
236
+ return null;
237
+ size = stat.size;
238
+ }
239
+ catch {
240
+ // Gone from the working tree. Only worth reporting when the index had it;
241
+ // an untracked file deleted between `ls-files` and this stat is not news.
242
+ return hashByKey.has(key) ? { path: key, kind: 'deleted', reason: null } : null;
243
+ }
244
+ const recorded = hashByKey.get(key);
245
+ if (recorded === undefined) {
246
+ const cap = meta.coverageMaxFileSizeBytes;
247
+ if (cap !== null && size > cap) {
248
+ return {
249
+ path: key,
250
+ kind: 'excluded',
251
+ reason: `超过索引记录的大小上限(${formatBytes(cap)}),刷新也会被跳过`,
252
+ };
253
+ }
254
+ return { path: key, kind: 'added', reason: null };
255
+ }
256
+ counts.hashed += 1;
257
+ const current = await hashFile(absolute);
258
+ if (current === null) {
259
+ // Unreadable is not unchanged. Treating it as modified makes the panel ask
260
+ // for a refresh; treating it as unchanged would report a stale index as
261
+ // current, which is the one error this module exists to prevent.
262
+ return { path: key, kind: 'modified', reason: '无法读取文件内容,按已变更处理' };
263
+ }
264
+ if (current === recorded) {
265
+ counts.unchanged += 1;
266
+ return null;
267
+ }
268
+ return { path: key, kind: 'modified', reason: null };
269
+ }));
270
+ for (const entry of settled) {
271
+ if (entry !== null)
272
+ entries.push(entry);
273
+ }
274
+ }
275
+ entries.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
276
+ for (const entry of entries)
277
+ counts[entry.kind] += 1;
278
+ const actionable = counts.modified + counts.added + counts.deleted;
279
+ const certain = basis === 'index';
280
+ const notes = [];
281
+ if (basis === 'worktree') {
282
+ notes.push('索引没有记录可用的提交基线:只比较了工作区相对 HEAD 的改动,索引建立之后已提交的改动看不到');
283
+ }
284
+ if (truncated) {
285
+ notes.push(`候选文件超过 ${maxFiles} 个,只检查了前 ${maxFiles} 个 —— 建议直接刷新`);
286
+ }
287
+ if (actionable === 0) {
288
+ // A clean worktree is not the same claim as a clean index. Saying "一致" on the
289
+ // worktree basis is how a stale index gets reported as current: the next commit
290
+ // makes it stale and this comparison will still be empty.
291
+ notes.push(certain
292
+ ? counts.checked === 0
293
+ ? '工作区与索引记录一致:没有任何文件相对索引提交发生改动'
294
+ : `工作区与索引记录一致:${counts.checked} 个候选文件中 ${counts.unchanged} 个内容哈希匹配` +
295
+ (counts.excluded > 0 ? `,${counts.excluded} 个属索引策略排除` : '')
296
+ : '工作区相对 HEAD 没有改动,但无法确认索引已包含当前提交 —— 需要刷新才能确定');
297
+ if (counts.excluded > 0) {
298
+ notes.push('被排除的文件刷新也会再次跳过,因此不需要为它们刷新索引');
299
+ }
300
+ }
301
+ else {
302
+ const parts = [];
303
+ if (counts.modified > 0)
304
+ parts.push(`${counts.modified} 个修改`);
305
+ if (counts.added > 0)
306
+ parts.push(`${counts.added} 个新增`);
307
+ if (counts.deleted > 0)
308
+ parts.push(`${counts.deleted} 个删除`);
309
+ notes.unshift(`索引内容已落后:${parts.join('、')}`);
310
+ }
311
+ return {
312
+ repoPath: absRepo,
313
+ indexed: true,
314
+ comparable: true,
315
+ basis,
316
+ indexedCommit,
317
+ headCommit,
318
+ entries,
319
+ counts,
320
+ needsRefresh: actionable > 0,
321
+ certain,
322
+ truncated,
323
+ notes,
324
+ durationMs: now() - startedAt,
325
+ readAt: new Date().toISOString(),
326
+ };
327
+ }
328
+ /** One-line verdict for a tool result or a log line. */
329
+ export function describeDelta(report) {
330
+ if (!report.comparable) {
331
+ return `无法差量判断:${report.notes[0] ?? '原因未知'}`;
332
+ }
333
+ if (!report.needsRefresh) {
334
+ return report.certain
335
+ ? `无需刷新(${report.counts.checked} 个候选,${report.counts.hashed} 次哈希,${report.durationMs}ms)`
336
+ : `无法确认(${report.counts.checked} 个候选,${report.durationMs}ms):${report.notes[0] ?? '索引没有记录提交基线'}`;
337
+ }
338
+ const changed = report.entries.filter((entry) => entry.kind !== 'excluded');
339
+ const preview = changed
340
+ .slice(0, 10)
341
+ .map((entry) => `${entry.kind === 'modified' ? 'M' : entry.kind === 'added' ? 'A' : 'D'} ${entry.path}`)
342
+ .join('\n');
343
+ const more = changed.length > 10 ? `\n…还有 ${changed.length - 10} 个` : '';
344
+ return (`需要刷新:${report.counts.modified} 改 / ${report.counts.added} 增 / ${report.counts.deleted} 删` +
345
+ `(${report.durationMs}ms)\n${preview}${more}`);
346
+ }
347
+ //# sourceMappingURL=delta.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"delta.js","sourceRoot":"","sources":["../../src/gitnexus/delta.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,91 @@
1
+ /**
2
+ * Find the GitNexus indexes under a workspace.
3
+ *
4
+ * WHY SEARCH BELOW THE WORKSPACE
5
+ * ------------------------------
6
+ * The panel reports the project the operator is *in*, and on this machine that is
7
+ * often a container rather than a repository: `D:\Project\geo` holds `admin/`,
8
+ * `backend/`, `frontend/` and others, any one of which may be the tree GitNexus
9
+ * indexed. Requiring the workspace itself to carry `.gitnexus` made the panel answer
10
+ * a workspace-shaped path with a 404 and no numbers at all.
11
+ *
12
+ * So the search starts at the workspace and walks down breadth-first. Every directory
13
+ * carrying a readable manifest becomes a candidate; an indexed directory is a project
14
+ * boundary, so the walk does not descend into its source tree (the project's own
15
+ * subdirectories are not separate projects). The caller decides which candidate is
16
+ * active; this module only reports them, ordered so the nearest complete index is
17
+ * first.
18
+ *
19
+ * The walk is bounded (depth, directory count, wall clock) because a workspace can be
20
+ * a home directory or a drive. Dependency and build directories are skipped by name,
21
+ * hidden directories are skipped wholesale, and symlinks are not followed, so the
22
+ * walk cannot leave the workspace or loop.
23
+ */
24
+ /** One project found under the workspace. */
25
+ export interface ProjectCandidate {
26
+ /** Absolute directory carrying `.gitnexus` or `.git`. */
27
+ path: string;
28
+ /** Basename, for a compact switcher label. */
29
+ name: string;
30
+ /** Path relative to the workspace; empty for the workspace itself. */
31
+ relPath: string;
32
+ /**
33
+ * `index` — a readable GitNexus manifest;
34
+ * `git` — a git repository without an index;
35
+ * `workspace` — the workspace itself, with neither.
36
+ */
37
+ kind: 'index' | 'git' | 'workspace';
38
+ /** Whether the directory carries a `.git` marker. */
39
+ hasGit: boolean;
40
+ /** Whether this is the workspace itself or a descendant. */
41
+ direction: 'self' | 'descendant';
42
+ /** Distance below the workspace; 0 for the workspace itself. */
43
+ depth: number;
44
+ indexedAt: string | null;
45
+ lastCommit: string | null;
46
+ branch: string | null;
47
+ /** The manifest's counters, when it recorded them. */
48
+ stats: {
49
+ files: number;
50
+ nodes: number;
51
+ edges: number;
52
+ communities: number;
53
+ processes: number;
54
+ embeddings: number;
55
+ } | null;
56
+ /** Whether the graph store is present, not just the manifest. */
57
+ complete: boolean;
58
+ /** Which manifest the values came from. */
59
+ metaFile: string | null;
60
+ }
61
+ /** The result of one workspace search. */
62
+ export interface ProjectSearchResult {
63
+ root: string;
64
+ projects: ProjectCandidate[];
65
+ /** Directories examined, including the root. */
66
+ visited: number;
67
+ /** Whether a depth/count/time bound stopped the walk early. */
68
+ truncated: boolean;
69
+ }
70
+ export interface ProjectSearchOptions {
71
+ /** How many directory levels below the root to inspect. */
72
+ maxDepth?: number;
73
+ /** Hard cap on directories examined. */
74
+ maxDirectories?: number;
75
+ /** Wall-clock budget for the whole search. */
76
+ timeoutMs?: number;
77
+ /** Injectable clock, so a test can prove the budget is enforced. */
78
+ now?: () => number;
79
+ }
80
+ /**
81
+ * Find every project at or below `root`: indexed directories, git repositories without
82
+ * an index, and the workspace itself when it is only a container.
83
+ *
84
+ * Never throws: an unreadable directory is skipped. The caller decides what each kind
85
+ * means; the panel shows unindexed git projects so they can be initialized instead of
86
+ * reporting a bare "no index" for a workspace that clearly has subprojects.
87
+ *
88
+ * @param root absolute working directory to search from
89
+ */
90
+ export declare function findIndexProjects(root: string, options?: ProjectSearchOptions): Promise<ProjectSearchResult>;
91
+ //# sourceMappingURL=discover.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"discover.d.ts","sourceRoot":"","sources":["../../src/gitnexus/discover.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAOH,6CAA6C;AAC7C,MAAM,WAAW,gBAAgB;IAC/B,yDAAyD;IACzD,IAAI,EAAE,MAAM,CAAC;IACb,8CAA8C;IAC9C,IAAI,EAAE,MAAM,CAAC;IACb,sEAAsE;IACtE,OAAO,EAAE,MAAM,CAAC;IAChB;;;;OAIG;IACH,IAAI,EAAE,OAAO,GAAG,KAAK,GAAG,WAAW,CAAC;IACpC,qDAAqD;IACrD,MAAM,EAAE,OAAO,CAAC;IAChB,4DAA4D;IAC5D,SAAS,EAAE,MAAM,GAAG,YAAY,CAAC;IACjC,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,sDAAsD;IACtD,KAAK,EAAE;QACL,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,KAAK,EAAE,MAAM,CAAC;QACd,WAAW,EAAE,MAAM,CAAC;QACpB,SAAS,EAAE,MAAM,CAAC;QAClB,UAAU,EAAE,MAAM,CAAC;KACpB,GAAG,IAAI,CAAC;IACT,iEAAiE;IACjE,QAAQ,EAAE,OAAO,CAAC;IAClB,2CAA2C;IAC3C,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB;AAED,0CAA0C;AAC1C,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,gBAAgB,EAAE,CAAC;IAC7B,gDAAgD;IAChD,OAAO,EAAE,MAAM,CAAC;IAChB,+DAA+D;IAC/D,SAAS,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,2DAA2D;IAC3D,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,wCAAwC;IACxC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,8CAA8C;IAC9C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAoED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,mBAAmB,CAAC,CAiH9B"}
@@ -0,0 +1,214 @@
1
+ /**
2
+ * Find the GitNexus indexes under a workspace.
3
+ *
4
+ * WHY SEARCH BELOW THE WORKSPACE
5
+ * ------------------------------
6
+ * The panel reports the project the operator is *in*, and on this machine that is
7
+ * often a container rather than a repository: `D:\Project\geo` holds `admin/`,
8
+ * `backend/`, `frontend/` and others, any one of which may be the tree GitNexus
9
+ * indexed. Requiring the workspace itself to carry `.gitnexus` made the panel answer
10
+ * a workspace-shaped path with a 404 and no numbers at all.
11
+ *
12
+ * So the search starts at the workspace and walks down breadth-first. Every directory
13
+ * carrying a readable manifest becomes a candidate; an indexed directory is a project
14
+ * boundary, so the walk does not descend into its source tree (the project's own
15
+ * subdirectories are not separate projects). The caller decides which candidate is
16
+ * active; this module only reports them, ordered so the nearest complete index is
17
+ * first.
18
+ *
19
+ * The walk is bounded (depth, directory count, wall clock) because a workspace can be
20
+ * a home directory or a drive. Dependency and build directories are skipped by name,
21
+ * hidden directories are skipped wholesale, and symlinks are not followed, so the
22
+ * walk cannot leave the workspace or loop.
23
+ */
24
+ import fs from 'node:fs/promises';
25
+ import { existsSync } from 'node:fs';
26
+ import path from 'node:path';
27
+ import { readIndexMeta } from './index-meta.js';
28
+ const DEFAULT_MAX_DEPTH = 12;
29
+ const DEFAULT_MAX_DIRECTORIES = 4000;
30
+ const DEFAULT_TIMEOUT_MS = 2000;
31
+ /**
32
+ * Directories that never hold the project the operator opened.
33
+ *
34
+ * Lower-cased because Windows paths are case-insensitive and a dependency tree can be
35
+ * named `Node_Modules` on a case-sensitive filesystem mounted there.
36
+ */
37
+ const SKIP_DIRECTORIES = new Set([
38
+ 'node_modules',
39
+ 'bower_components',
40
+ 'vendor',
41
+ 'dist',
42
+ 'build',
43
+ 'out',
44
+ 'output',
45
+ 'target',
46
+ 'bin',
47
+ 'obj',
48
+ 'coverage',
49
+ 'tmp',
50
+ 'temp',
51
+ 'logs',
52
+ 'venv',
53
+ 'env',
54
+ '__pycache__',
55
+ ]);
56
+ /** A directory the walk may enter: real directory, not hidden, not a dependency tree. */
57
+ function isSearchableDirectory(entry) {
58
+ if (!entry.isDirectory())
59
+ return false;
60
+ if (entry.name.startsWith('.'))
61
+ return false;
62
+ return !SKIP_DIRECTORIES.has(entry.name.toLowerCase());
63
+ }
64
+ /** Whether the graph store exists beside the manifest. */
65
+ function hasGraphStore(dir) {
66
+ try {
67
+ return existsSync(path.join(dir, '.gitnexus', 'lbug'));
68
+ }
69
+ catch {
70
+ return false;
71
+ }
72
+ }
73
+ /**
74
+ * Rank candidates for display and for a default selection.
75
+ *
76
+ * The workspace itself comes first; indexed projects before unindexed git repositories;
77
+ * a complete index before one whose graph store is missing; the more recently indexed
78
+ * project before an older one; the path is the final deterministic tie-break.
79
+ */
80
+ function rankProjects(a, b) {
81
+ if (a.direction !== b.direction)
82
+ return a.direction === 'self' ? -1 : 1;
83
+ const kindRank = (project) => project.kind === 'index' ? 0 : project.kind === 'git' ? 1 : 2;
84
+ if (kindRank(a) !== kindRank(b))
85
+ return kindRank(a) - kindRank(b);
86
+ if (a.depth !== b.depth)
87
+ return a.depth - b.depth;
88
+ if (a.complete !== b.complete)
89
+ return a.complete ? -1 : 1;
90
+ const aAt = a.indexedAt ?? '';
91
+ const bAt = b.indexedAt ?? '';
92
+ if (aAt !== bAt)
93
+ return aAt < bAt ? 1 : -1;
94
+ return a.relPath < b.relPath ? -1 : a.relPath > b.relPath ? 1 : 0;
95
+ }
96
+ /**
97
+ * Find every project at or below `root`: indexed directories, git repositories without
98
+ * an index, and the workspace itself when it is only a container.
99
+ *
100
+ * Never throws: an unreadable directory is skipped. The caller decides what each kind
101
+ * means; the panel shows unindexed git projects so they can be initialized instead of
102
+ * reporting a bare "no index" for a workspace that clearly has subprojects.
103
+ *
104
+ * @param root absolute working directory to search from
105
+ */
106
+ export async function findIndexProjects(root, options = {}) {
107
+ const absolute = path.resolve(root);
108
+ const maxDepth = options.maxDepth ?? DEFAULT_MAX_DEPTH;
109
+ const maxDirectories = options.maxDirectories ?? DEFAULT_MAX_DIRECTORIES;
110
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
111
+ const now = options.now ?? Date.now;
112
+ const startedAt = now();
113
+ const queue = [{ dir: absolute, depth: 0 }];
114
+ let head = 0;
115
+ let visited = 0;
116
+ let truncated = false;
117
+ const projects = [];
118
+ while (head < queue.length) {
119
+ if (now() - startedAt > timeoutMs) {
120
+ truncated = true;
121
+ break;
122
+ }
123
+ const { dir, depth } = queue[head];
124
+ head += 1;
125
+ visited += 1;
126
+ if (visited > maxDirectories) {
127
+ truncated = true;
128
+ break;
129
+ }
130
+ const meta = await readIndexMeta(dir);
131
+ const hasGit = existsSync(path.join(dir, '.git'));
132
+ if (meta !== null) {
133
+ projects.push({
134
+ path: dir,
135
+ name: path.basename(dir),
136
+ relPath: path.relative(absolute, dir).replace(/\\/g, '/'),
137
+ kind: 'index',
138
+ hasGit,
139
+ direction: dir === absolute ? 'self' : 'descendant',
140
+ depth,
141
+ indexedAt: meta.indexedAt === '' ? null : meta.indexedAt,
142
+ lastCommit: meta.lastCommit === '' ? null : meta.lastCommit,
143
+ branch: meta.branch ?? null,
144
+ stats: meta.stats ?? null,
145
+ complete: hasGraphStore(dir),
146
+ metaFile: meta.metaFile,
147
+ });
148
+ // A project boundary: its own subdirectories are source, not sibling projects.
149
+ continue;
150
+ }
151
+ if (hasGit) {
152
+ projects.push({
153
+ path: dir,
154
+ name: path.basename(dir),
155
+ relPath: path.relative(absolute, dir).replace(/\\/g, '/'),
156
+ kind: 'git',
157
+ hasGit: true,
158
+ direction: dir === absolute ? 'self' : 'descendant',
159
+ depth,
160
+ indexedAt: null,
161
+ lastCommit: null,
162
+ branch: null,
163
+ stats: null,
164
+ complete: false,
165
+ metaFile: null,
166
+ });
167
+ // A git project without an index is still a project: its source is not a set of
168
+ // sibling projects. It appears in the switcher so the operator can initialize it.
169
+ continue;
170
+ }
171
+ if (dir === absolute) {
172
+ projects.push({
173
+ path: dir,
174
+ name: path.basename(dir),
175
+ relPath: '',
176
+ kind: 'workspace',
177
+ hasGit: false,
178
+ direction: 'self',
179
+ depth: 0,
180
+ indexedAt: null,
181
+ lastCommit: null,
182
+ branch: null,
183
+ stats: null,
184
+ complete: false,
185
+ metaFile: null,
186
+ });
187
+ // The workspace is a container; keep walking for its indexed/git children.
188
+ }
189
+ let entries;
190
+ try {
191
+ entries = await fs.readdir(dir, { withFileTypes: true });
192
+ }
193
+ catch {
194
+ continue;
195
+ }
196
+ if (depth >= maxDepth) {
197
+ // Nothing is enqueued at the cap; say so only when there was something to skip.
198
+ if (entries.some(isSearchableDirectory))
199
+ truncated = true;
200
+ continue;
201
+ }
202
+ // Sorted for a deterministic answer when several siblings carry an index; the
203
+ // order is only a tie-break, because breadth-first already prefers the nearest.
204
+ entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
205
+ for (const entry of entries) {
206
+ if (!isSearchableDirectory(entry))
207
+ continue;
208
+ queue.push({ dir: path.join(dir, entry.name), depth: depth + 1 });
209
+ }
210
+ }
211
+ projects.sort(rankProjects);
212
+ return { root: absolute, projects, visited, truncated };
213
+ }
214
+ //# sourceMappingURL=discover.js.map