@maxgfr/codeindex 2.12.0 → 2.14.0

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/src/walk.ts CHANGED
@@ -1,15 +1,18 @@
1
- import { readdirSync, statSync, lstatSync, readFileSync, realpathSync } from "node:fs";
1
+ import { readdirSync, statSync, lstatSync, readFileSync, realpathSync, type Dirent } from "node:fs";
2
2
  import { join, relative, sep, extname } from "node:path";
3
3
  import { parseGitignore, isIgnored, type IgnoreRule } from "./ignore.js";
4
4
 
5
5
  // Directories that never carry signal for a documentation/code question and
6
6
  // would bloat the index (dependencies, build output, VCS internals, caches).
7
+ // .codeindex is the engine's OWN output (index artifacts, pulled models, MCP
8
+ // memories) — indexing it would feed memories into search and churn the scan
9
+ // fingerprint on every write_memory (issue #12).
7
10
  // Exported so grep.ts can align ripgrep's universe with the walker's.
8
11
  export const IGNORE_DIRS = new Set([
9
12
  ".git", "node_modules", ".pnpm", "bower_components", "vendor", "dist", "build", "out",
10
13
  "target", ".next", ".nuxt", ".svelte-kit", ".turbo", "coverage", "__pycache__", ".venv",
11
14
  "venv", ".tox", ".mypy_cache", ".pytest_cache", ".gradle", ".idea", ".vscode", ".cache",
12
- "tmp", ".ultraindex", "Pods", "DerivedData", ".terraform", "elm-stuff", ".dart_tool",
15
+ "tmp", ".ultraindex", ".codeindex", "Pods", "DerivedData", ".terraform", "elm-stuff", ".dart_tool",
13
16
  ]);
14
17
 
15
18
  // Lockfiles: huge, machine-generated, and pure noise for a code/docs question —
@@ -36,6 +39,13 @@ export interface WalkOptions {
36
39
  // semantics — see ignore.ts). Default TRUE: an ignored file is noise for
37
40
  // every consumer; pass false to index generated/ignored trees deliberately.
38
41
  gitignore?: boolean;
42
+ // Directory names to skip, REPLACING the default set entirely (not merging
43
+ // with it). IGNORE_DIRS is a public export, so consumers compose
44
+ // `[...IGNORE_DIRS, "extra"]` — or filter it — themselves; replace is the
45
+ // simplest contract. Deliberate scope boundary: grep.ts (the ripgrep
46
+ // universe) and the MCP server keep the DEFAULT set — recall consumers
47
+ // (e.g. ultrasec) consume scan/extract, not grep.
48
+ ignoreDirs?: string[];
39
49
  }
40
50
 
41
51
  export interface WalkedFile {
@@ -65,6 +75,9 @@ export function walk(root: string, opts: WalkOptions = {}): WalkResult {
65
75
  const maxFileBytes = opts.maxFileBytes ?? 1024 * 1024;
66
76
  const maxFiles = opts.maxFiles ?? DEFAULT_MAX_FILES;
67
77
  const useGitignore = opts.gitignore !== false;
78
+ // Effective ignored-directory set, built once: the caller's replacement when
79
+ // given (see WalkOptions.ignoreDirs — replace, never merge), else the default.
80
+ const ignoreDirs = opts.ignoreDirs ? new Set(opts.ignoreDirs) : IGNORE_DIRS;
68
81
  const out: WalkedFile[] = [];
69
82
  let capped = false;
70
83
  let excluded = 0;
@@ -100,32 +113,49 @@ export function walk(root: string, opts: WalkOptions = {}): WalkResult {
100
113
  if (seenDirs.has(real)) continue;
101
114
  seenDirs.add(real);
102
115
  if (!contained(real)) continue; // dir symlink escaping the repo
103
- let entries: string[];
116
+ let entries: Dirent[];
104
117
  try {
105
118
  // Sorted so the walk order — and therefore WHICH files survive a
106
- // maxFiles cap — is identical across filesystems and machines.
107
- entries = readdirSync(frame.dir).sort();
119
+ // maxFiles cap — is identical across filesystems and machines. Dirents
120
+ // sort by .name under the same code-unit comparison the bare-string
121
+ // sort used, so the order is byte-identical to the previous readdir.
122
+ entries = readdirSync(frame.dir, { withFileTypes: true }).sort((a, b) =>
123
+ a.name < b.name ? -1 : a.name > b.name ? 1 : 0,
124
+ );
108
125
  } catch {
109
126
  continue;
110
127
  }
111
128
  let rules = frame.rules;
112
- if (useGitignore && entries.includes(".gitignore")) {
129
+ if (useGitignore && entries.some((e) => e.name === ".gitignore")) {
113
130
  const parsed = parseGitignore(readText(join(frame.dir, ".gitignore")), frame.rel);
114
131
  if (parsed.length) rules = [...rules, ...parsed];
115
132
  }
116
- for (const name of entries) {
133
+ for (const entry of entries) {
134
+ const name = entry.name;
117
135
  const abs = join(frame.dir, name);
118
136
  const rel = frame.rel ? `${frame.rel}/${name}` : name;
137
+ // The dirent type IS the lstat type (Node lstats internally when the
138
+ // filesystem can't supply it), so no lstat is needed to detect links.
139
+ const isLink = entry.isSymbolicLink();
140
+ // Ignored-directory boundary (node_modules, .git…): skip on the dirent
141
+ // type alone — ZERO stat syscalls. A symlink reports isDirectory()
142
+ // false on its dirent and falls through to the stat-based
143
+ // classification below, so a link named node_modules still classifies
144
+ // by its target exactly as before.
145
+ if (entry.isDirectory() && ignoreDirs.has(name)) continue;
119
146
  let st;
120
- let isLink: boolean;
121
147
  try {
122
- st = statSync(abs);
123
- isLink = lstatSync(abs).isSymbolicLink();
148
+ // Non-links: a single lstatSync supplies isDirectory/isFile/size/
149
+ // mtimeMs — field-for-field what the previous statSync returned,
150
+ // since with no link to follow the two calls are identical. Links:
151
+ // keep the following statSync so the entry classifies by its TARGET;
152
+ // a broken link throws here and is skipped, same as before.
153
+ st = isLink ? statSync(abs) : lstatSync(abs);
124
154
  } catch {
125
155
  continue;
126
156
  }
127
157
  if (st.isDirectory()) {
128
- if (IGNORE_DIRS.has(name)) continue;
158
+ if (ignoreDirs.has(name)) continue;
129
159
  // An in-repo DIRECTORY symlink is skipped entirely: its target is (or
130
160
  // will be) walked under its canonical name, and letting both paths race
131
161
  // through the cycle guard would keep whichever readdir served first —