@mnemonik/scanner 5.151.0 → 5.151.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/dist/client.d.ts +99 -0
- package/dist/client.js +140 -0
- package/dist/client.js.map +1 -0
- package/dist/daemon.d.ts +51 -0
- package/dist/daemon.js +550 -0
- package/dist/daemon.js.map +1 -0
- package/dist/discovery.d.ts +21 -0
- package/dist/discovery.js +107 -0
- package/dist/discovery.js.map +1 -0
- package/dist/doctor.d.ts +1 -0
- package/dist/doctor.js +233 -0
- package/dist/doctor.js.map +1 -0
- package/dist/fileLog.d.ts +17 -0
- package/dist/fileLog.js +70 -0
- package/dist/fileLog.js.map +1 -0
- package/dist/git.d.ts +31 -0
- package/dist/git.js +111 -0
- package/dist/git.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +399 -0
- package/dist/index.js.map +1 -0
- package/dist/pid.d.ts +1 -0
- package/dist/pid.js +37 -0
- package/dist/pid.js.map +1 -0
- package/dist/watcher.d.ts +30 -0
- package/dist/watcher.js +214 -0
- package/dist/watcher.js.map +1 -0
- package/package.json +7 -3
- package/src/client.ts +0 -216
- package/src/daemon.ts +0 -679
- package/src/discovery.ts +0 -124
- package/src/doctor.ts +0 -239
- package/src/fileLog.ts +0 -67
- package/src/git.ts +0 -122
- package/src/index.ts +0 -446
- package/src/pid.ts +0 -37
- package/src/watcher.ts +0 -219
- package/tests/validateServerUrl.test.ts +0 -41
- package/tsconfig.json +0 -18
- package/vitest.config.ts +0 -13
package/dist/watcher.js
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
import { watch } from 'fs';
|
|
2
|
+
import { join, relative } from 'path';
|
|
3
|
+
import { readdir, stat } from 'fs/promises';
|
|
4
|
+
import { isGitBoundary } from '@mnemonik/shared';
|
|
5
|
+
const SKIP_DIRS = new Set([
|
|
6
|
+
'node_modules',
|
|
7
|
+
'.git',
|
|
8
|
+
'dist',
|
|
9
|
+
'build',
|
|
10
|
+
'.next',
|
|
11
|
+
'.nuxt',
|
|
12
|
+
'.output',
|
|
13
|
+
'__pycache__',
|
|
14
|
+
'.venv',
|
|
15
|
+
'venv',
|
|
16
|
+
'.tox',
|
|
17
|
+
'target',
|
|
18
|
+
'.cache',
|
|
19
|
+
'coverage',
|
|
20
|
+
'.turbo',
|
|
21
|
+
'.vercel',
|
|
22
|
+
'.svelte-kit',
|
|
23
|
+
]);
|
|
24
|
+
export class FileWatcher {
|
|
25
|
+
rootPath;
|
|
26
|
+
onChange;
|
|
27
|
+
// Keyed by directory so a watcher can be closed and re-attached when the
|
|
28
|
+
// directory is deleted and recreated at the same path.
|
|
29
|
+
watchers = new Map();
|
|
30
|
+
watchedDirs = new Set();
|
|
31
|
+
// Inode per watched directory: a delete+recreate faster than the delete
|
|
32
|
+
// event's stat() presents as "directory exists and is already watched",
|
|
33
|
+
// but the live watcher is bound to the OLD inode and is inert. Comparing
|
|
34
|
+
// inodes at event time detects the swap so the subtree can re-attach.
|
|
35
|
+
watchedDirInodes = new Map();
|
|
36
|
+
pendingFiles = new Set();
|
|
37
|
+
flushTimer = null;
|
|
38
|
+
debounceMs;
|
|
39
|
+
onError;
|
|
40
|
+
constructor(rootPath, onChange, debounceMs = 500, onError) {
|
|
41
|
+
this.rootPath = rootPath;
|
|
42
|
+
this.onChange = onChange;
|
|
43
|
+
this.debounceMs = debounceMs;
|
|
44
|
+
this.onError = onError;
|
|
45
|
+
}
|
|
46
|
+
async start() {
|
|
47
|
+
await this.watchDir(this.rootPath);
|
|
48
|
+
console.log(`[scanner] Watching ${this.rootPath} for changes`);
|
|
49
|
+
}
|
|
50
|
+
stop() {
|
|
51
|
+
for (const w of this.watchers.values()) {
|
|
52
|
+
w.close();
|
|
53
|
+
}
|
|
54
|
+
this.watchers.clear();
|
|
55
|
+
this.watchedDirs.clear();
|
|
56
|
+
this.watchedDirInodes.clear();
|
|
57
|
+
if (this.flushTimer) {
|
|
58
|
+
clearTimeout(this.flushTimer);
|
|
59
|
+
this.flushTimer = null;
|
|
60
|
+
}
|
|
61
|
+
this.pendingFiles.clear();
|
|
62
|
+
}
|
|
63
|
+
scheduleFlush() {
|
|
64
|
+
if (this.flushTimer)
|
|
65
|
+
clearTimeout(this.flushTimer);
|
|
66
|
+
this.flushTimer = setTimeout(() => {
|
|
67
|
+
this.flushTimer = null;
|
|
68
|
+
if (this.pendingFiles.size === 0)
|
|
69
|
+
return;
|
|
70
|
+
const batch = [...this.pendingFiles];
|
|
71
|
+
this.pendingFiles.clear();
|
|
72
|
+
this.onChange(batch);
|
|
73
|
+
}, this.debounceMs);
|
|
74
|
+
}
|
|
75
|
+
async watchDir(dir) {
|
|
76
|
+
const dirName = dir.split('/').pop() ?? '';
|
|
77
|
+
if (SKIP_DIRS.has(dirName))
|
|
78
|
+
return;
|
|
79
|
+
// Dedup guard — the change callback re-enters watchDir for new
|
|
80
|
+
// subdirectories, and a rapid create/rename burst can resolve the same
|
|
81
|
+
// path twice before the first watch is registered.
|
|
82
|
+
if (this.watchedDirs.has(dir))
|
|
83
|
+
return;
|
|
84
|
+
this.watchedDirs.add(dir);
|
|
85
|
+
try {
|
|
86
|
+
// Record the inode BEFORE attaching so watchNewDir can distinguish a
|
|
87
|
+
// benign event on this directory from a recreate-at-same-path.
|
|
88
|
+
this.watchedDirInodes.set(dir, (await stat(dir)).ino);
|
|
89
|
+
const watcher = watch(dir, { persistent: true }, (event, filename) => {
|
|
90
|
+
if (!filename)
|
|
91
|
+
return;
|
|
92
|
+
const fullPath = join(dir, filename);
|
|
93
|
+
const relPath = relative(this.rootPath, fullPath);
|
|
94
|
+
this.pendingFiles.add(relPath);
|
|
95
|
+
this.scheduleFlush();
|
|
96
|
+
// fs.watch is non-recursive: the initial recursion below only covers
|
|
97
|
+
// directories that existed at start(). When this event is a newly
|
|
98
|
+
// created directory, attach a watcher to it too — otherwise the
|
|
99
|
+
// subtree is a permanent blind spot until restart. Fire-and-forget;
|
|
100
|
+
// watchNewDir no-ops for plain files, skip dirs, git boundaries, and
|
|
101
|
+
// already-watched paths.
|
|
102
|
+
void this.watchNewDir(fullPath, event);
|
|
103
|
+
});
|
|
104
|
+
watcher.on('error', (err) => {
|
|
105
|
+
console.warn(`[scanner] Watcher error for ${dir}:`, err.message);
|
|
106
|
+
if (dir === this.rootPath && this.onError) {
|
|
107
|
+
this.onError(err);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
this.watchers.set(dir, watcher);
|
|
111
|
+
const entries = await readdir(dir, { withFileTypes: true });
|
|
112
|
+
for (const entry of entries) {
|
|
113
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
114
|
+
const child = join(dir, entry.name);
|
|
115
|
+
// Nested-git-boundary rule (see @mnemonik/shared isGitBoundary):
|
|
116
|
+
// linked worktrees / nested clones are other repositories — don't
|
|
117
|
+
// watch their subtrees as part of this project.
|
|
118
|
+
if (await isGitBoundary(child))
|
|
119
|
+
continue;
|
|
120
|
+
await this.watchDir(child);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
// Watch registration failed. Never fatal for the subtree's parent, but
|
|
126
|
+
// the *reason* matters: inotify/fd limit exhaustion means silently
|
|
127
|
+
// growing blind spots, while permission-denied is a benign property of
|
|
128
|
+
// the directory itself. Drop the bookkeeping (and any watcher that did
|
|
129
|
+
// attach before the failure) so a later retry can re-enter cleanly.
|
|
130
|
+
this.watchedDirs.delete(dir);
|
|
131
|
+
this.watchedDirInodes.delete(dir);
|
|
132
|
+
const stale = this.watchers.get(dir);
|
|
133
|
+
if (stale) {
|
|
134
|
+
stale.close();
|
|
135
|
+
this.watchers.delete(dir);
|
|
136
|
+
}
|
|
137
|
+
const code = err.code;
|
|
138
|
+
if (code === 'ENOSPC' || code === 'EMFILE' || code === 'ENFILE') {
|
|
139
|
+
console.warn(`[scanner] Watch limit reached (${code}) — subtree unwatched: ${dir}. ` +
|
|
140
|
+
'Raise fs.inotify.max_user_watches or trim scanner roots.');
|
|
141
|
+
// Surface to the daemon only for the root — losing the root means the
|
|
142
|
+
// whole project is blind; a subtree gap must not tear the project down
|
|
143
|
+
// (the daemon's onError removes the project entirely).
|
|
144
|
+
if (dir === this.rootPath && this.onError) {
|
|
145
|
+
this.onError(err);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
else {
|
|
149
|
+
// Permission denied or inaccessible directory
|
|
150
|
+
console.warn(`[scanner] Cannot watch ${dir}: ${err.message}`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Attach a watcher to a directory created after start(). Called from the
|
|
156
|
+
* per-directory change callback with every event path; stats the path and
|
|
157
|
+
* only recurses when it is a genuinely new, watchable directory.
|
|
158
|
+
*/
|
|
159
|
+
async watchNewDir(fullPath, eventType) {
|
|
160
|
+
try {
|
|
161
|
+
// Stat before the dedup check: a delete event arrives with the same
|
|
162
|
+
// path as the original create, and the stale watchedDirs entry must
|
|
163
|
+
// not short-circuit the vanish detection below.
|
|
164
|
+
const s = await stat(fullPath);
|
|
165
|
+
if (!s.isDirectory())
|
|
166
|
+
return;
|
|
167
|
+
if (this.watchedDirs.has(fullPath)) {
|
|
168
|
+
// Already watched — but a delete+recreate faster than this stat
|
|
169
|
+
// presents exactly like this, with the live watcher bound to the
|
|
170
|
+
// OLD (dead) inode and inert. A 'rename' event means the entry's
|
|
171
|
+
// identity changed (created / deleted / moved), so re-attach
|
|
172
|
+
// unconditionally — comparing inodes is NOT sufficient there
|
|
173
|
+
// because ext4 routinely hands the freed inode straight back to
|
|
174
|
+
// the recreated directory. 'change' events are attrib noise (a
|
|
175
|
+
// write inside the child bumps its mtime, which fires on the
|
|
176
|
+
// parent), so the cheap inode check keeps those churn-free.
|
|
177
|
+
if (eventType !== 'rename' && this.watchedDirInodes.get(fullPath) === s.ino)
|
|
178
|
+
return;
|
|
179
|
+
this.unwatchSubtree(fullPath);
|
|
180
|
+
}
|
|
181
|
+
if (await isGitBoundary(fullPath))
|
|
182
|
+
return;
|
|
183
|
+
await this.watchDir(fullPath);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
// Path vanished between event and stat. If it (or anything under it)
|
|
187
|
+
// was a watched directory, drop the bookkeeping and close the dead
|
|
188
|
+
// watchers — otherwise the dedup guards in watchDir/watchNewDir block
|
|
189
|
+
// re-attachment forever when the path is recreated (build-output
|
|
190
|
+
// wipes, codegen, rm -rf && mkdir).
|
|
191
|
+
this.unwatchSubtree(fullPath);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Forget a deleted directory and everything watched beneath it. fs.watch
|
|
196
|
+
* emits no per-descendant events on a recursive delete, so the whole
|
|
197
|
+
* prefix must be purged here for a recreate to re-watch the full subtree.
|
|
198
|
+
*/
|
|
199
|
+
unwatchSubtree(root) {
|
|
200
|
+
const prefix = root + '/';
|
|
201
|
+
for (const dir of this.watchedDirs) {
|
|
202
|
+
if (dir !== root && !dir.startsWith(prefix))
|
|
203
|
+
continue;
|
|
204
|
+
this.watchedDirs.delete(dir);
|
|
205
|
+
this.watchedDirInodes.delete(dir);
|
|
206
|
+
const w = this.watchers.get(dir);
|
|
207
|
+
if (w) {
|
|
208
|
+
w.close();
|
|
209
|
+
this.watchers.delete(dir);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
//# sourceMappingURL=watcher.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher.js","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mnemonik/scanner",
|
|
3
|
-
"version": "5.151.
|
|
3
|
+
"version": "5.151.3",
|
|
4
4
|
"description": "Automatic codebase indexing daemon for Mnemonik",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
7
7
|
"mnemonik-scanner": "dist/index.js"
|
|
8
8
|
},
|
|
9
9
|
"main": "dist/index.js",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist"
|
|
12
|
+
],
|
|
10
13
|
"scripts": {
|
|
11
14
|
"build": "tsc",
|
|
12
15
|
"typecheck": "tsc --noEmit",
|
|
13
16
|
"test": "vitest run",
|
|
14
17
|
"test:watch": "vitest",
|
|
15
|
-
"start": "node dist/index.js"
|
|
18
|
+
"start": "node dist/index.js",
|
|
19
|
+
"prepublishOnly": "npm run build"
|
|
16
20
|
},
|
|
17
21
|
"dependencies": {
|
|
18
|
-
"@mnemonik/shared": "^5.
|
|
22
|
+
"@mnemonik/shared": "^5.151.0"
|
|
19
23
|
},
|
|
20
24
|
"devDependencies": {
|
|
21
25
|
"typescript": "^5.3.3",
|
package/src/client.ts
DELETED
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
export interface ScanPushFile {
|
|
2
|
-
path: string;
|
|
3
|
-
hash: string;
|
|
4
|
-
chunks: Array<{
|
|
5
|
-
content: string;
|
|
6
|
-
startLine: number;
|
|
7
|
-
endLine: number;
|
|
8
|
-
chunkType: string;
|
|
9
|
-
language: string;
|
|
10
|
-
contentHash: string;
|
|
11
|
-
metadata: {
|
|
12
|
-
fileName: string;
|
|
13
|
-
extension: string;
|
|
14
|
-
size: number;
|
|
15
|
-
};
|
|
16
|
-
}>;
|
|
17
|
-
/**
|
|
18
|
-
* Optional raw file content. Sent by the scanner daemon for ALL files
|
|
19
|
-
* (code and authority/manifest alike) so the doc-truth worker can run
|
|
20
|
-
* claim extraction against whole documents without filesystem access.
|
|
21
|
-
* Omitted when the file exceeds the server's 5 MB schema cap or when
|
|
22
|
-
* the raw content could not be read (chunk-based fallback path).
|
|
23
|
-
*/
|
|
24
|
-
content?: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface ScanPushCommit {
|
|
28
|
-
sha: string;
|
|
29
|
-
author: string;
|
|
30
|
-
date: string;
|
|
31
|
-
message: string;
|
|
32
|
-
files: string[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface ScanPushPayload {
|
|
36
|
-
projectId: string;
|
|
37
|
-
files: ScanPushFile[];
|
|
38
|
-
commits?: ScanPushCommit[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface ScanStatusResponse {
|
|
42
|
-
files: Array<{ path: string; hash: string }>;
|
|
43
|
-
gitMining?: {
|
|
44
|
-
enabled: boolean;
|
|
45
|
-
lastMinedCommit: string | null;
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export interface GitMiningMeta {
|
|
50
|
-
enabled: boolean;
|
|
51
|
-
lastMinedCommit: string | null;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const SCAN_PUSH_BATCH_DELAY_MS = 500;
|
|
55
|
-
|
|
56
|
-
/**
|
|
57
|
-
* Exponential backoff (base 2000ms × attempt) with ±25% jitter, to avoid a
|
|
58
|
-
* fleet of scanners retrying in lockstep after a shared 5xx/network blip
|
|
59
|
-
* (thundering herd). Base values unchanged from the pre-jitter constants.
|
|
60
|
-
*/
|
|
61
|
-
function backoffWithJitter(attempt: number): number {
|
|
62
|
-
const base = 2000 * (attempt + 1);
|
|
63
|
-
return Math.round(base * (0.75 + Math.random() * 0.5));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export class MnemonikClient {
|
|
67
|
-
constructor(
|
|
68
|
-
private serverUrl: string,
|
|
69
|
-
private apiKey: string
|
|
70
|
-
) {}
|
|
71
|
-
|
|
72
|
-
private async request<T>(path: string, body: unknown, retries = 3): Promise<T> {
|
|
73
|
-
const url = `${this.serverUrl}${path}`;
|
|
74
|
-
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
75
|
-
try {
|
|
76
|
-
const res = await fetch(url, {
|
|
77
|
-
method: 'POST',
|
|
78
|
-
headers: {
|
|
79
|
-
'Content-Type': 'application/json',
|
|
80
|
-
Authorization: `Bearer ${this.apiKey}`,
|
|
81
|
-
},
|
|
82
|
-
body: JSON.stringify(body),
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
if (!res.ok) {
|
|
86
|
-
const text = await res.text().catch(() => 'Unknown error');
|
|
87
|
-
if (res.status === 401 || res.status === 403) {
|
|
88
|
-
throw new Error(`Auth failed (${res.status}). Check your API key.`);
|
|
89
|
-
}
|
|
90
|
-
if (res.status === 503 && attempt < retries) {
|
|
91
|
-
const retryAfter = parseInt(res.headers.get('Retry-After') || '5', 10);
|
|
92
|
-
await new Promise((r) => setTimeout(r, retryAfter * 1000));
|
|
93
|
-
continue;
|
|
94
|
-
}
|
|
95
|
-
if (res.status >= 500 && attempt < retries) {
|
|
96
|
-
await new Promise((r) => setTimeout(r, backoffWithJitter(attempt)));
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
throw new Error(`${res.status} ${res.statusText}: ${text}`);
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
return res.json() as Promise<T>;
|
|
103
|
-
} catch (err) {
|
|
104
|
-
if (attempt < retries && (err as Error).message?.includes('fetch failed')) {
|
|
105
|
-
await new Promise((r) => setTimeout(r, backoffWithJitter(attempt)));
|
|
106
|
-
continue;
|
|
107
|
-
}
|
|
108
|
-
throw err;
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
throw new Error(`Request to ${path} failed after ${retries} retries`);
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
/**
|
|
115
|
-
* Fetch per-file hashes for dedup, plus git-mining metadata.
|
|
116
|
-
* The daemon uses `gitMining.enabled` to decide whether to collect commits
|
|
117
|
-
* this cycle, and `lastMinedCommit` as the lower bound of `git log`.
|
|
118
|
-
*/
|
|
119
|
-
async getStatus(
|
|
120
|
-
projectId: string
|
|
121
|
-
): Promise<{ fileHashes: Map<string, string>; gitMining: GitMiningMeta }> {
|
|
122
|
-
const result = await this.request<ScanStatusResponse>('/api/v1/scan/status', { projectId });
|
|
123
|
-
return {
|
|
124
|
-
fileHashes: new Map(result.files.map((f) => [f.path, f.hash])),
|
|
125
|
-
gitMining: result.gitMining ?? { enabled: false, lastMinedCommit: null },
|
|
126
|
-
};
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Push file chunks in batches. When `commits` is supplied (non-empty) it is
|
|
131
|
-
* attached to the first batch only — BullMQ's idempotent jobId means a
|
|
132
|
-
* duplicate would collapse anyway, but one payload saves bandwidth.
|
|
133
|
-
*
|
|
134
|
-
* When `files` is empty and `commits` is non-empty, a single commit-only
|
|
135
|
-
* push is made — the server accepts `files=[]` since If both are
|
|
136
|
-
* empty, no request is sent.
|
|
137
|
-
*/
|
|
138
|
-
async pushFiles(
|
|
139
|
-
projectId: string,
|
|
140
|
-
files: ScanPushFile[],
|
|
141
|
-
commits?: ScanPushCommit[]
|
|
142
|
-
): Promise<{ success: boolean }> {
|
|
143
|
-
const hasCommits = !!commits && commits.length > 0;
|
|
144
|
-
if (files.length === 0) {
|
|
145
|
-
if (hasCommits) {
|
|
146
|
-
await this.request('/api/v1/scan/push', { projectId, files: [], commits });
|
|
147
|
-
}
|
|
148
|
-
return { success: true };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const batchSize = 25;
|
|
152
|
-
let attachedCommits = false;
|
|
153
|
-
for (let i = 0; i < files.length; i += batchSize) {
|
|
154
|
-
const batch = files.slice(i, i + batchSize);
|
|
155
|
-
const body: ScanPushPayload = { projectId, files: batch };
|
|
156
|
-
if (!attachedCommits && hasCommits) {
|
|
157
|
-
body.commits = commits;
|
|
158
|
-
attachedCommits = true;
|
|
159
|
-
}
|
|
160
|
-
await this.request('/api/v1/scan/push', body);
|
|
161
|
-
if (i + batchSize < files.length) {
|
|
162
|
-
await sleep(SCAN_PUSH_BATCH_DELAY_MS);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return { success: true };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
async sendHeartbeat(
|
|
169
|
-
projectId: string,
|
|
170
|
-
scanner: { scope: 'global'; version?: string }
|
|
171
|
-
): Promise<void> {
|
|
172
|
-
await this.request('/api/v1/scan/heartbeat', { projectId, scanner });
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Notify the server of files that have been removed since the daemon's
|
|
177
|
-
* previous scan of this project. Server deprecates exactly those code
|
|
178
|
-
* memories. Empty arrays are accepted as no-ops so the daemon can call
|
|
179
|
-
* this every tick regardless of whether anything was removed.
|
|
180
|
-
*
|
|
181
|
-
* The caller is responsible for computing the removal set locally — the
|
|
182
|
-
* old inventory-diff shape that asked the server to derive removals from
|
|
183
|
-
* a "known files" list has been removed because a small/malformed list
|
|
184
|
-
* would mass-deprecate. The narrow `removedFiles` shape cannot exhibit
|
|
185
|
-
* that failure mode by construction.
|
|
186
|
-
*/
|
|
187
|
-
async reportRemovedFiles(
|
|
188
|
-
projectId: string,
|
|
189
|
-
removedFiles: string[]
|
|
190
|
-
): Promise<{ deprecated: number; couplingsRemoved: number }> {
|
|
191
|
-
const result = await this.request<{
|
|
192
|
-
success: boolean;
|
|
193
|
-
deprecated: number;
|
|
194
|
-
couplingsRemoved: number;
|
|
195
|
-
}>('/api/v1/scan/reconcile', { projectId, removedFiles });
|
|
196
|
-
return {
|
|
197
|
-
deprecated: result.deprecated ?? 0,
|
|
198
|
-
couplingsRemoved: result.couplingsRemoved ?? 0,
|
|
199
|
-
};
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
async healthCheck(): Promise<boolean> {
|
|
203
|
-
try {
|
|
204
|
-
const res = await fetch(`${this.serverUrl}/api/v1/health`, {
|
|
205
|
-
headers: { Authorization: `Bearer ${this.apiKey}` },
|
|
206
|
-
});
|
|
207
|
-
return res.ok;
|
|
208
|
-
} catch {
|
|
209
|
-
return false;
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
function sleep(ms: number): Promise<void> {
|
|
215
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
216
|
-
}
|