@chainlesschain/personal-data-hub 0.2.4 → 0.3.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.
Files changed (55) hide show
  1. package/__tests__/adapters/browser-history-chrome.test.js +377 -0
  2. package/__tests__/adapters/browser-history-edge.test.js +159 -0
  3. package/__tests__/adapters/git-activity.test.js +216 -0
  4. package/__tests__/adapters/local-files.test.js +264 -0
  5. package/__tests__/adapters/shell-history.test.js +180 -0
  6. package/__tests__/adapters/system-data-android.test.js +104 -3
  7. package/__tests__/adapters/vscode.test.js +299 -0
  8. package/__tests__/adapters/win-recent.test.js +192 -0
  9. package/__tests__/analysis.test.js +840 -1
  10. package/__tests__/categories.test.js +92 -0
  11. package/__tests__/e2e/local-data-adapters-cli.e2e.test.js +146 -0
  12. package/__tests__/entity-resolver-vault.test.js +5 -2
  13. package/__tests__/integration/local-data-adapters-pipeline.test.js +373 -0
  14. package/__tests__/query-parser.test.js +66 -0
  15. package/__tests__/registry.test.js +114 -0
  16. package/__tests__/sidecar-contacts-cross-validate.test.js +24 -1
  17. package/__tests__/sidecar-supervisor.test.js +9 -1
  18. package/__tests__/social-kuaishou-snapshot.test.js +55 -2
  19. package/__tests__/social-toutiao-snapshot.test.js +54 -2
  20. package/__tests__/vault-search-helpers.test.js +104 -0
  21. package/__tests__/vault-search.test.js +423 -0
  22. package/__tests__/vault.test.js +77 -3
  23. package/lib/adapters/browser-history-chrome/adapter.js +247 -0
  24. package/lib/adapters/browser-history-chrome/bookmarks-reader.js +79 -0
  25. package/lib/adapters/browser-history-chrome/chrome-db-reader.js +223 -0
  26. package/lib/adapters/browser-history-chrome/index.js +23 -0
  27. package/lib/adapters/browser-history-edge/adapter.js +34 -0
  28. package/lib/adapters/browser-history-edge/index.js +13 -0
  29. package/lib/adapters/git-activity/adapter.js +155 -0
  30. package/lib/adapters/git-activity/git-reader.js +125 -0
  31. package/lib/adapters/git-activity/index.js +17 -0
  32. package/lib/adapters/local-files/adapter.js +149 -0
  33. package/lib/adapters/local-files/file-walker.js +125 -0
  34. package/lib/adapters/local-files/index.js +18 -0
  35. package/lib/adapters/shell-history/adapter.js +137 -0
  36. package/lib/adapters/shell-history/index.js +17 -0
  37. package/lib/adapters/shell-history/shell-reader.js +100 -0
  38. package/lib/adapters/social-kuaishou/index.js +57 -1
  39. package/lib/adapters/social-toutiao/index.js +59 -1
  40. package/lib/adapters/system-data-android/adapter.js +220 -3
  41. package/lib/adapters/vscode/adapter.js +285 -0
  42. package/lib/adapters/vscode/index.js +18 -0
  43. package/lib/adapters/vscode/vscode-reader.js +191 -0
  44. package/lib/adapters/win-recent/adapter.js +150 -0
  45. package/lib/adapters/win-recent/index.js +16 -0
  46. package/lib/adapters/win-recent/win-recent-reader.js +72 -0
  47. package/lib/analysis.js +227 -9
  48. package/lib/categories.js +101 -0
  49. package/lib/index.js +61 -0
  50. package/lib/migrations.js +146 -0
  51. package/lib/query-parser.js +74 -0
  52. package/lib/registry.js +162 -0
  53. package/lib/vault.js +363 -2
  54. package/package.json +2 -1
  55. package/scripts/run-native-tests-sandbox.sh +53 -0
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+
3
+ // GitActivityAdapter — yields recent commits from every .git repo found
4
+ // under the user's code roots as a developer-activity timeline.
5
+ //
6
+ // Pure local: shells out `git log` per repo, no remote fetches.
7
+
8
+ const path = require("node:path");
9
+
10
+ const {
11
+ ENTITY_TYPES,
12
+ EVENT_SUBTYPES,
13
+ CAPTURED_BY,
14
+ } = require("../../constants");
15
+
16
+ const {
17
+ defaultCodeRoots,
18
+ findGitRepos,
19
+ listCommits,
20
+ } = require("./git-reader");
21
+
22
+ const NAME = "git-activity";
23
+ const VERSION = "0.1.0";
24
+
25
+ class GitActivityAdapter {
26
+ constructor(opts = {}) {
27
+ this.name = NAME;
28
+ this.version = VERSION;
29
+ this.capabilities = ["sync:git-log-local"];
30
+ this.extractMode = "file-import";
31
+ this.rateLimits = { perDay: 48 };
32
+ this.dataDisclosure = {
33
+ fields: ["commits:sha,authoredAtMs,authorName,authorEmail,subject,repoName"],
34
+ sensitivity: "high",
35
+ legalGate: false,
36
+ defaultInclude: { commits: true },
37
+ };
38
+ this._deps = {
39
+ defaultRoots: defaultCodeRoots,
40
+ findRepos: findGitRepos,
41
+ listCommits,
42
+ };
43
+ this._rootsOverride = Array.isArray(opts.codeRoots) ? opts.codeRoots : null;
44
+ }
45
+
46
+ _resolveRoots(opts) {
47
+ if (Array.isArray(opts?.codeRoots) && opts.codeRoots.length > 0) {
48
+ return opts.codeRoots;
49
+ }
50
+ if (this._rootsOverride && this._rootsOverride.length > 0) return this._rootsOverride;
51
+ return this._deps.defaultRoots();
52
+ }
53
+
54
+ async authenticate(ctx = {}) {
55
+ const roots = this._resolveRoots(ctx);
56
+ if (!roots || roots.length === 0) {
57
+ return {
58
+ ok: false,
59
+ reason: "NO_CODE_ROOTS",
60
+ message: "no default code roots found (tried C:\\code on Win, ~/code ~/projects on Unix); pass opts.codeRoots",
61
+ };
62
+ }
63
+ const repos = this._deps.findRepos(roots);
64
+ if (repos.length === 0) {
65
+ return {
66
+ ok: false,
67
+ reason: "NO_GIT_REPOS",
68
+ message: `no .git directories under any of: ${roots.join(", ")}`,
69
+ };
70
+ }
71
+ return { ok: true, mode: "file-import", codeRoots: roots, repoCount: repos.length };
72
+ }
73
+
74
+ async healthCheck() {
75
+ const roots = this._resolveRoots({});
76
+ const ok = roots && roots.length > 0;
77
+ return { ok, lastChecked: Date.now() };
78
+ }
79
+
80
+ async *sync(opts = {}) {
81
+ const roots = this._resolveRoots(opts);
82
+ if (!roots || roots.length === 0) {
83
+ throw new Error("git-activity.sync: no code roots — pass opts.codeRoots");
84
+ }
85
+ const repos = this._deps.findRepos(roots);
86
+ const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
87
+ const maxPerRepo =
88
+ Number.isInteger(opts.maxPerRepo) && opts.maxPerRepo > 0 ? opts.maxPerRepo : 500;
89
+ const capturedAt = Date.now();
90
+ let emitted = 0;
91
+ for (const repoDir of repos) {
92
+ if (emitted >= limit) return;
93
+ const commits = this._deps.listCommits(repoDir, { since: opts.since, maxPerRepo });
94
+ for (const c of commits) {
95
+ if (emitted >= limit) return;
96
+ yield {
97
+ kind: "commit",
98
+ // sha is globally unique within git's universe — repo path keeps
99
+ // multi-clone scenarios distinct (same sha in two clones = two rows).
100
+ originalId: `git-commit:${repoDir}:${c.sha}`,
101
+ capturedAt,
102
+ payload: c,
103
+ };
104
+ emitted += 1;
105
+ }
106
+ }
107
+ }
108
+
109
+ normalize(raw) {
110
+ const ingestedAt = Date.now();
111
+ const source = (originalId) => ({
112
+ adapter: NAME,
113
+ adapterVersion: VERSION,
114
+ capturedAt: raw.capturedAt,
115
+ capturedBy: CAPTURED_BY.SQLITE,
116
+ originalId,
117
+ });
118
+
119
+ if (raw.kind === "commit") {
120
+ const p = raw.payload || {};
121
+ const subj = typeof p.subject === "string" && p.subject ? p.subject : "(no subject)";
122
+ const event = {
123
+ id: `event-git-commit-${p.shortSha || p.sha}`,
124
+ type: ENTITY_TYPES.EVENT,
125
+ subtype: EVENT_SUBTYPES.OTHER,
126
+ occurredAt: Number.isInteger(p.authoredAtMs) ? p.authoredAtMs : raw.capturedAt,
127
+ ingestedAt,
128
+ source: source(raw.originalId),
129
+ actor: p.authorName || p.authorEmail || "self",
130
+ content: {
131
+ title: subj.length > 100 ? subj.substring(0, 100) + "…" : subj,
132
+ text: subj,
133
+ },
134
+ extra: {
135
+ kind: "git-commit",
136
+ sha: p.sha || null,
137
+ shortSha: p.shortSha || null,
138
+ repoName: p.repoName || null,
139
+ repoDir: p.repoDir || null,
140
+ authorName: p.authorName || null,
141
+ authorEmail: p.authorEmail || null,
142
+ },
143
+ };
144
+ return { events: [event], persons: [], places: [], items: [], topics: [] };
145
+ }
146
+
147
+ throw new Error(`git-activity.normalize: unknown raw.kind=${raw.kind}`);
148
+ }
149
+ }
150
+
151
+ module.exports = {
152
+ GitActivityAdapter,
153
+ GIT_ACTIVITY_NAME: NAME,
154
+ GIT_ACTIVITY_VERSION: VERSION,
155
+ };
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+
3
+ // git-reader — enumerates `.git` directories under configured code roots
4
+ // and shells out `git log` to extract recent commits. No clone-time
5
+ // metadata, no remote network calls; pure local-filesystem walk.
6
+
7
+ const fs = require("node:fs");
8
+ const path = require("node:path");
9
+ const os = require("node:os");
10
+ const { execFileSync } = require("node:child_process");
11
+
12
+ function defaultCodeRoots() {
13
+ const home = os.homedir();
14
+ if (process.platform === "win32") {
15
+ // Most devs on Windows use C:\code\ or ~/code/.
16
+ const candidates = ["C:\\code", path.join(home, "code"), path.join(home, "projects")];
17
+ return candidates.filter((d) => {
18
+ try {
19
+ return fs.statSync(d).isDirectory();
20
+ } catch {
21
+ return false;
22
+ }
23
+ });
24
+ }
25
+ return [path.join(home, "code"), path.join(home, "projects"), path.join(home, "src")].filter(
26
+ (d) => {
27
+ try {
28
+ return fs.statSync(d).isDirectory();
29
+ } catch {
30
+ return false;
31
+ }
32
+ },
33
+ );
34
+ }
35
+
36
+ // Find every `.git` directory one level under each root. Skips bare /
37
+ // nested repos for v0.1 — keeps the surface area predictable.
38
+ function findGitRepos(roots, opts = {}) {
39
+ const fsMod = opts.fs || fs;
40
+ const out = [];
41
+ for (const root of roots) {
42
+ let entries;
43
+ try {
44
+ entries = fsMod.readdirSync(root, { withFileTypes: true });
45
+ } catch {
46
+ continue;
47
+ }
48
+ for (const e of entries) {
49
+ if (!e.isDirectory()) continue;
50
+ const repoDir = path.join(root, e.name);
51
+ const dotGit = path.join(repoDir, ".git");
52
+ if (!fsMod.existsSync(dotGit)) continue;
53
+ out.push(repoDir);
54
+ }
55
+ }
56
+ return out;
57
+ }
58
+
59
+ // Run git log against one repo. The pipe delimiter is safer than a single
60
+ // char because commit messages may contain tabs or pipes — we add a
61
+ // recognizable sentinel between fields. Bail to [] on any spawn error so
62
+ // one corrupt repo doesn't sink the whole sync.
63
+ const FIELD_SEP = ""; // SOH — guaranteed absent from commit metadata
64
+ const ROW_SEP = ""; // RS
65
+
66
+ function listCommits(repoDir, opts = {}) {
67
+ const sinceMs = Number.isInteger(opts.since) && opts.since > 0 ? opts.since : 0;
68
+ // git wants ISO-ish dates or relative; use unix-seconds for precision.
69
+ const sinceArg = sinceMs > 0 ? `--since=@${Math.floor(sinceMs / 1000)}` : "--since=180.days";
70
+ const maxN = Number.isInteger(opts.maxPerRepo) && opts.maxPerRepo > 0 ? opts.maxPerRepo : 500;
71
+ const fmt = ["%H", "%aI", "%an", "%ae", "%s"].join(FIELD_SEP) + ROW_SEP;
72
+ let stdout = "";
73
+ try {
74
+ stdout = execFileSync(
75
+ "git",
76
+ [
77
+ "-C",
78
+ repoDir,
79
+ "log",
80
+ sinceArg,
81
+ `-n${maxN}`,
82
+ `--pretty=format:${fmt}`,
83
+ "--no-merges",
84
+ ],
85
+ {
86
+ encoding: "utf-8",
87
+ timeout: 30_000,
88
+ windowsHide: true,
89
+ stdio: ["ignore", "pipe", "ignore"],
90
+ maxBuffer: 32 * 1024 * 1024, // 32 MB — handles repos with many commits
91
+ },
92
+ );
93
+ } catch {
94
+ return [];
95
+ }
96
+ const repoName = path.basename(repoDir);
97
+ const out = [];
98
+ for (const raw of stdout.split(ROW_SEP)) {
99
+ const line = raw.replace(/^[\r\n]+/, "").replace(/[\r\n]+$/, "");
100
+ if (!line) continue;
101
+ const parts = line.split(FIELD_SEP);
102
+ if (parts.length < 5) continue;
103
+ const [sha, isoDate, authorName, authorEmail, subject] = parts;
104
+ const d = new Date(isoDate);
105
+ const ts = Number.isFinite(d.getTime()) ? d.getTime() : 0;
106
+ if (ts === 0) continue;
107
+ out.push({
108
+ sha,
109
+ shortSha: sha.substring(0, 8),
110
+ authoredAtMs: ts,
111
+ authorName: authorName || "",
112
+ authorEmail: authorEmail || "",
113
+ subject: subject || "",
114
+ repoDir,
115
+ repoName,
116
+ });
117
+ }
118
+ return out;
119
+ }
120
+
121
+ module.exports = {
122
+ defaultCodeRoots,
123
+ findGitRepos,
124
+ listCommits,
125
+ };
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ const {
4
+ GitActivityAdapter,
5
+ GIT_ACTIVITY_NAME,
6
+ GIT_ACTIVITY_VERSION,
7
+ } = require("./adapter");
8
+ const reader = require("./git-reader");
9
+
10
+ module.exports = {
11
+ GitActivityAdapter,
12
+ GIT_ACTIVITY_NAME,
13
+ GIT_ACTIVITY_VERSION,
14
+ defaultCodeRoots: reader.defaultCodeRoots,
15
+ findGitRepos: reader.findGitRepos,
16
+ listCommits: reader.listCommits,
17
+ };
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+
3
+ // LocalFilesAdapter — walks user-data roots (Documents / Desktop / Downloads
4
+ // / Pictures / Videos / Music) and surfaces one Event(OTHER) per file. Same
5
+ // "what did I touch and when" shape as win-recent, but rooted in real on-disk
6
+ // files instead of .lnk shortcuts — so it captures files the user created /
7
+ // saved / downloaded even if they never opened them via Explorer.
8
+ //
9
+ // Excludes app cache dirs (xwechat_files / WXWork / node_modules / .git) by
10
+ // default to keep the vault signal-rich.
11
+
12
+ const {
13
+ ENTITY_TYPES,
14
+ EVENT_SUBTYPES,
15
+ CAPTURED_BY,
16
+ } = require("../../constants");
17
+
18
+ const {
19
+ defaultRoots,
20
+ walkRoots,
21
+ DEFAULT_EXCLUDES,
22
+ } = require("./file-walker");
23
+
24
+ const NAME = "local-files";
25
+ const VERSION = "0.1.0";
26
+
27
+ class LocalFilesAdapter {
28
+ constructor(opts = {}) {
29
+ this.name = NAME;
30
+ this.version = VERSION;
31
+ this.capabilities = ["sync:local-file-walk"];
32
+ this.extractMode = "file-import";
33
+ this.rateLimits = { perDay: 24 };
34
+ this.dataDisclosure = {
35
+ fields: ["files:path,name,ext,size,mtimeMs,root"],
36
+ sensitivity: "high",
37
+ legalGate: false,
38
+ defaultInclude: { files: true },
39
+ };
40
+ this._deps = {
41
+ defaultRoots,
42
+ walkRoots,
43
+ };
44
+ this._rootsOverride = Array.isArray(opts.roots) ? opts.roots : null;
45
+ this._excludesOverride = Array.isArray(opts.excludes) ? opts.excludes : null;
46
+ }
47
+
48
+ _resolveRoots(opts) {
49
+ if (Array.isArray(opts?.roots) && opts.roots.length > 0) return opts.roots;
50
+ if (this._rootsOverride) return this._rootsOverride;
51
+ return this._deps.defaultRoots();
52
+ }
53
+
54
+ _resolveExcludes(opts) {
55
+ if (Array.isArray(opts?.excludes) && opts.excludes.length > 0) return opts.excludes;
56
+ if (this._excludesOverride) return this._excludesOverride;
57
+ return DEFAULT_EXCLUDES;
58
+ }
59
+
60
+ async authenticate(ctx = {}) {
61
+ const roots = this._resolveRoots(ctx);
62
+ if (!roots || roots.length === 0) {
63
+ return {
64
+ ok: false,
65
+ reason: "NO_DATA_ROOTS",
66
+ message: "no default user-data dirs available; pass opts.roots",
67
+ };
68
+ }
69
+ return { ok: true, mode: "file-import", roots };
70
+ }
71
+
72
+ async healthCheck() {
73
+ return { ok: true, lastChecked: Date.now() };
74
+ }
75
+
76
+ async *sync(opts = {}) {
77
+ const roots = this._resolveRoots(opts);
78
+ const excludes = this._resolveExcludes(opts);
79
+ const limit = Number.isInteger(opts.limit) && opts.limit > 0 ? opts.limit : Infinity;
80
+ const capturedAt = Date.now();
81
+ let emitted = 0;
82
+ for (const row of this._deps.walkRoots(roots, { ...opts, excludes })) {
83
+ if (emitted >= limit) return;
84
+ yield {
85
+ kind: "local-file",
86
+ originalId: `local-file:${row.path}:${row.mtimeMs}`,
87
+ capturedAt,
88
+ payload: row,
89
+ };
90
+ emitted += 1;
91
+ }
92
+ }
93
+
94
+ normalize(raw) {
95
+ const ingestedAt = Date.now();
96
+ const source = (originalId) => ({
97
+ adapter: NAME,
98
+ adapterVersion: VERSION,
99
+ capturedAt: raw.capturedAt,
100
+ capturedBy: CAPTURED_BY.SQLITE,
101
+ originalId,
102
+ });
103
+
104
+ if (raw.kind === "local-file") {
105
+ const p = raw.payload || {};
106
+ const name = typeof p.name === "string" && p.name.length > 0 ? p.name : "(无名)";
107
+ const titleText = `[file] ${name}`;
108
+ const event = {
109
+ id: `event-local-file-${shortHash(raw.originalId)}`,
110
+ type: ENTITY_TYPES.EVENT,
111
+ subtype: EVENT_SUBTYPES.OTHER,
112
+ occurredAt: Number.isInteger(p.mtimeMs) ? p.mtimeMs : raw.capturedAt,
113
+ ingestedAt,
114
+ source: source(raw.originalId),
115
+ actor: "self",
116
+ content: {
117
+ title: titleText.length > 100 ? titleText.substring(0, 100) + "…" : titleText,
118
+ text: typeof p.path === "string" ? p.path : name,
119
+ },
120
+ extra: {
121
+ kind: "local-file",
122
+ path: typeof p.path === "string" ? p.path : null,
123
+ name,
124
+ ext: typeof p.ext === "string" ? p.ext : "",
125
+ size: Number.isFinite(p.size) ? p.size : null,
126
+ root: typeof p.root === "string" ? p.root : null,
127
+ },
128
+ };
129
+ return { events: [event], persons: [], places: [], items: [], topics: [] };
130
+ }
131
+
132
+ throw new Error(`local-files.normalize: unknown raw.kind=${raw.kind}`);
133
+ }
134
+ }
135
+
136
+ function shortHash(s) {
137
+ let h = 5381;
138
+ const str = typeof s === "string" ? s : "";
139
+ for (let i = 0; i < str.length; i++) {
140
+ h = ((h << 5) + h + str.charCodeAt(i)) >>> 0;
141
+ }
142
+ return h.toString(36).substring(0, 10);
143
+ }
144
+
145
+ module.exports = {
146
+ LocalFilesAdapter,
147
+ LOCAL_FILES_NAME: NAME,
148
+ LOCAL_FILES_VERSION: VERSION,
149
+ };
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+
3
+ // file-walker — recursive directory walker for user-data roots
4
+ // (Documents / Desktop / Downloads / Pictures / Videos / Music).
5
+ //
6
+ // Emits one row per file with absolute path + mtime + size. Skips:
7
+ // - any path component matching a default exclude (xwechat_files,
8
+ // WXWork, node_modules, .git) — these are noisy app caches that
9
+ // would flood the vault without user value
10
+ // - hidden files / dirs (leading '.')
11
+ // - symlinks (avoid loops + permission errors)
12
+ //
13
+ // max depth + max files per root are bounded to keep first-time sync
14
+ // from walking a Downloads dir with 500k files.
15
+
16
+ const fs = require("node:fs");
17
+ const path = require("node:path");
18
+ const os = require("node:os");
19
+
20
+ const DEFAULT_EXCLUDES = Object.freeze([
21
+ "xwechat_files",
22
+ "WXWork",
23
+ "node_modules",
24
+ ".git",
25
+ ]);
26
+
27
+ const DEFAULT_MAX_DEPTH = 6;
28
+ const DEFAULT_MAX_FILES_PER_ROOT = 5000;
29
+
30
+ function defaultRoots() {
31
+ const home = os.homedir();
32
+ if (!home) return [];
33
+ const candidates = [
34
+ path.join(home, "Documents"),
35
+ path.join(home, "Desktop"),
36
+ path.join(home, "Downloads"),
37
+ path.join(home, "Pictures"),
38
+ path.join(home, "Videos"),
39
+ path.join(home, "Music"),
40
+ ];
41
+ return candidates;
42
+ }
43
+
44
+ function shouldSkip(name, excludes) {
45
+ if (!name) return true;
46
+ if (name.startsWith(".")) return true;
47
+ for (const ex of excludes) {
48
+ if (name === ex) return true;
49
+ }
50
+ return false;
51
+ }
52
+
53
+ // Walks one root; yields { path, name, ext, size, mtimeMs, root } per file.
54
+ // Uses iterative DFS to bound stack depth and skip whole subtrees on
55
+ // permission errors without aborting the whole walk.
56
+ function* walkRoot(root, opts = {}) {
57
+ const fsMod = opts.fs || fs;
58
+ const excludes = Array.isArray(opts.excludes) ? opts.excludes : DEFAULT_EXCLUDES;
59
+ const maxDepth = Number.isInteger(opts.maxDepth) && opts.maxDepth > 0 ? opts.maxDepth : DEFAULT_MAX_DEPTH;
60
+ const maxFiles = Number.isInteger(opts.maxFilesPerRoot) && opts.maxFilesPerRoot > 0
61
+ ? opts.maxFilesPerRoot
62
+ : DEFAULT_MAX_FILES_PER_ROOT;
63
+ if (!fsMod.existsSync(root)) return;
64
+ let count = 0;
65
+ const stack = [{ dir: root, depth: 0 }];
66
+ while (stack.length > 0) {
67
+ const { dir, depth } = stack.pop();
68
+ if (depth > maxDepth) continue;
69
+ let entries;
70
+ try {
71
+ entries = fsMod.readdirSync(dir, { withFileTypes: true });
72
+ } catch {
73
+ continue;
74
+ }
75
+ for (const entry of entries) {
76
+ if (shouldSkip(entry.name, excludes)) continue;
77
+ const full = path.join(dir, entry.name);
78
+ // dirent does not always carry symlink info reliably; skip symlinks
79
+ // explicitly via lstat to avoid following them.
80
+ let st;
81
+ try {
82
+ st = fsMod.lstatSync(full);
83
+ } catch {
84
+ continue;
85
+ }
86
+ if (st.isSymbolicLink()) continue;
87
+ if (st.isDirectory()) {
88
+ stack.push({ dir: full, depth: depth + 1 });
89
+ continue;
90
+ }
91
+ if (!st.isFile()) continue;
92
+ if (count >= maxFiles) return;
93
+ count += 1;
94
+ yield {
95
+ path: full,
96
+ name: entry.name,
97
+ ext: path.extname(entry.name).toLowerCase().replace(/^\./, ""),
98
+ size: Number.isFinite(st.size) ? st.size : 0,
99
+ mtimeMs: Math.floor(st.mtimeMs),
100
+ root,
101
+ };
102
+ }
103
+ }
104
+ }
105
+
106
+ // Yields across every configured root in stable order. `since` filters
107
+ // by file mtime so re-syncs only surface recently changed files.
108
+ function* walkRoots(roots, opts = {}) {
109
+ const sinceMs = Number.isInteger(opts.since) && opts.since > 0 ? opts.since : 0;
110
+ for (const root of roots) {
111
+ for (const row of walkRoot(root, opts)) {
112
+ if (sinceMs > 0 && row.mtimeMs < sinceMs) continue;
113
+ yield row;
114
+ }
115
+ }
116
+ }
117
+
118
+ module.exports = {
119
+ DEFAULT_EXCLUDES,
120
+ DEFAULT_MAX_DEPTH,
121
+ DEFAULT_MAX_FILES_PER_ROOT,
122
+ defaultRoots,
123
+ walkRoot,
124
+ walkRoots,
125
+ };
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+
3
+ const {
4
+ LocalFilesAdapter,
5
+ LOCAL_FILES_NAME,
6
+ LOCAL_FILES_VERSION,
7
+ } = require("./adapter");
8
+ const walker = require("./file-walker");
9
+
10
+ module.exports = {
11
+ LocalFilesAdapter,
12
+ LOCAL_FILES_NAME,
13
+ LOCAL_FILES_VERSION,
14
+ defaultRoots: walker.defaultRoots,
15
+ walkRoot: walker.walkRoot,
16
+ walkRoots: walker.walkRoots,
17
+ DEFAULT_EXCLUDES: walker.DEFAULT_EXCLUDES,
18
+ };