md-semantic-search 1.0.1 → 1.0.2
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/bin/cli.mjs +23 -3
- package/dist/core.js +3 -0
- package/dist/serve.js +27 -6
- package/dist/watcher.js +11 -3
- package/package.json +1 -1
- package/src/core.ts +3 -0
- package/src/serve.ts +28 -7
- package/src/watcher.ts +11 -3
package/bin/cli.mjs
CHANGED
|
@@ -1136,9 +1136,29 @@ async function main() {
|
|
|
1136
1136
|
}
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1139
|
+
/**
|
|
1140
|
+
* True only when this module is the process entry script (`node bin/cli.mjs`,
|
|
1141
|
+
* global `mdss`), never when imported by tests or library consumers (issue #29).
|
|
1142
|
+
*
|
|
1143
|
+
* The comparison goes through fs.realpathSync on BOTH sides of the identity:
|
|
1144
|
+
* on Windows a global install is an NTFS junction, so Node resolves this module
|
|
1145
|
+
* to its real path (D:\Repo\...\bin\cli.mjs) while process.argv[1] keeps the
|
|
1146
|
+
* junction path (C:\...\npm\node_modules\md-semantic-search\bin\cli.mjs). A
|
|
1147
|
+
* plain string comparison then never matches and the CLI silently does nothing.
|
|
1148
|
+
*/
|
|
1149
|
+
function isDirectRun() {
|
|
1150
|
+
const entry = process.argv[1];
|
|
1151
|
+
if (!entry) return false;
|
|
1152
|
+
try {
|
|
1153
|
+
return fs.realpathSync(entry) === fileURLToPath(import.meta.url);
|
|
1154
|
+
} catch {
|
|
1155
|
+
// argv[1] vanished between spawn and here (or is not a real file) —
|
|
1156
|
+
// fall back to lexical resolution so behavior matches the old guard.
|
|
1157
|
+
return pathToFileURL(path.resolve(entry)).href === import.meta.url;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
|
|
1161
|
+
if (isDirectRun()) {
|
|
1142
1162
|
main().catch(e => die(e.message));
|
|
1143
1163
|
}
|
|
1144
1164
|
|
package/dist/core.js
CHANGED
|
@@ -375,6 +375,9 @@ export function assertSafePath(targetPath, allowedRoots) {
|
|
|
375
375
|
if (targetPath.startsWith('\\\\') || targetPath.startsWith('//')) {
|
|
376
376
|
throw new Error('Forbidden path: UNC network paths not allowed');
|
|
377
377
|
}
|
|
378
|
+
if (/^[a-zA-Z]:[\\/]/.test(targetPath) && process.platform !== 'win32') {
|
|
379
|
+
throw new Error('Forbidden path: Windows drive paths not allowed on non-Windows systems');
|
|
380
|
+
}
|
|
378
381
|
const resolved = path.resolve(targetPath);
|
|
379
382
|
let canonical = resolved;
|
|
380
383
|
try {
|
package/dist/serve.js
CHANGED
|
@@ -40,13 +40,19 @@ export async function createServe(opts) {
|
|
|
40
40
|
apiKey,
|
|
41
41
|
healthPublic,
|
|
42
42
|
};
|
|
43
|
+
const sockets = new Set();
|
|
43
44
|
const server = http.createServer((req, res) => {
|
|
44
45
|
handleRequest(req, res, state).catch((e) => {
|
|
45
46
|
json(res, 500, { error: e.message });
|
|
46
47
|
});
|
|
47
48
|
});
|
|
49
|
+
server.on('connection', (socket) => {
|
|
50
|
+
sockets.add(socket);
|
|
51
|
+
socket.on('close', () => sockets.delete(socket));
|
|
52
|
+
});
|
|
48
53
|
let stopped = false;
|
|
49
54
|
let timer = null;
|
|
55
|
+
let resolveTimer = null;
|
|
50
56
|
const reload = async () => {
|
|
51
57
|
if (!db)
|
|
52
58
|
return false;
|
|
@@ -149,7 +155,13 @@ export async function createServe(opts) {
|
|
|
149
155
|
let pending = false;
|
|
150
156
|
while (!stopped) {
|
|
151
157
|
await new Promise((r) => {
|
|
152
|
-
|
|
158
|
+
resolveTimer = r;
|
|
159
|
+
timer = setTimeout(() => {
|
|
160
|
+
timer = null;
|
|
161
|
+
resolveTimer = null;
|
|
162
|
+
r();
|
|
163
|
+
}, watchInterval);
|
|
164
|
+
timer.unref?.();
|
|
153
165
|
});
|
|
154
166
|
if (stopped)
|
|
155
167
|
return;
|
|
@@ -200,15 +212,24 @@ export async function createServe(opts) {
|
|
|
200
212
|
}
|
|
201
213
|
const close = async () => {
|
|
202
214
|
stopped = true;
|
|
203
|
-
if (timer)
|
|
215
|
+
if (timer) {
|
|
204
216
|
clearTimeout(timer);
|
|
217
|
+
timer = null;
|
|
218
|
+
}
|
|
219
|
+
if (resolveTimer) {
|
|
220
|
+
resolveTimer();
|
|
221
|
+
resolveTimer = null;
|
|
222
|
+
}
|
|
205
223
|
if (nativeWatcher)
|
|
206
224
|
nativeWatcher.close();
|
|
207
|
-
// closeAllConnections() forcibly closes keep-alive sockets so server.close()
|
|
208
|
-
// doesn't hang indefinitely waiting for them — critical on Node 18 where the
|
|
209
|
-
// HTTP server does NOT auto-drain idle connections before calling back.
|
|
210
|
-
// The method was back-ported to Node 18.2.0 and is always present in CI.
|
|
211
225
|
server.closeAllConnections?.();
|
|
226
|
+
for (const socket of sockets) {
|
|
227
|
+
try {
|
|
228
|
+
socket.destroy();
|
|
229
|
+
}
|
|
230
|
+
catch { }
|
|
231
|
+
}
|
|
232
|
+
sockets.clear();
|
|
212
233
|
await new Promise((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
|
|
213
234
|
};
|
|
214
235
|
return { server, state, close };
|
package/dist/watcher.js
CHANGED
|
@@ -8,7 +8,7 @@ import fs from 'node:fs';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { globToRegExp } from './core.js';
|
|
10
10
|
export function createFileWatcher(dbPath, onChange, options = {}) {
|
|
11
|
-
const { debounceMs = 250, ignore = [], log = () => { }, fallbackIntervalMs =
|
|
11
|
+
const { debounceMs = 250, ignore = [], log = () => { }, fallbackIntervalMs = 50, } = options;
|
|
12
12
|
let closed = false;
|
|
13
13
|
let debounceTimer = null;
|
|
14
14
|
const ignoreRegexes = [
|
|
@@ -47,6 +47,7 @@ export function createFileWatcher(dbPath, onChange, options = {}) {
|
|
|
47
47
|
log(`watcher onChange error: ${err.message}`);
|
|
48
48
|
}
|
|
49
49
|
}, debounceMs);
|
|
50
|
+
debounceTimer.unref?.();
|
|
50
51
|
};
|
|
51
52
|
// Attempt recursive native fs.watch first
|
|
52
53
|
try {
|
|
@@ -61,6 +62,11 @@ export function createFileWatcher(dbPath, onChange, options = {}) {
|
|
|
61
62
|
fsWatcher.on('error', (err) => {
|
|
62
63
|
log(`Native watcher error (${err.message}), continuing.`);
|
|
63
64
|
});
|
|
65
|
+
// unref() lets the event loop exit even if the watcher is still active.
|
|
66
|
+
// Without this, on Node 18/Linux the inotify handle keeps the process
|
|
67
|
+
// alive indefinitely after all tests finish (or after close() is called
|
|
68
|
+
// but the handle hasn't been fully released by the kernel yet).
|
|
69
|
+
fsWatcher.unref?.();
|
|
64
70
|
return {
|
|
65
71
|
isNative: true,
|
|
66
72
|
close: () => {
|
|
@@ -80,13 +86,14 @@ export function createFileWatcher(dbPath, onChange, options = {}) {
|
|
|
80
86
|
let lastMtime = 0;
|
|
81
87
|
const getDirMtime = (dir) => {
|
|
82
88
|
try {
|
|
83
|
-
|
|
84
|
-
let max = stat.mtimeMs;
|
|
89
|
+
let max = 0;
|
|
85
90
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
86
91
|
for (const entry of entries) {
|
|
87
92
|
if (entry.name.startsWith('.') || entry.name === '.mdss')
|
|
88
93
|
continue;
|
|
89
94
|
const full = path.join(dir, entry.name);
|
|
95
|
+
if (shouldIgnore(entry.name))
|
|
96
|
+
continue;
|
|
90
97
|
if (entry.isDirectory()) {
|
|
91
98
|
const sub = getDirMtime(full);
|
|
92
99
|
if (sub > max)
|
|
@@ -117,6 +124,7 @@ export function createFileWatcher(dbPath, onChange, options = {}) {
|
|
|
117
124
|
scheduleChange();
|
|
118
125
|
}
|
|
119
126
|
}, fallbackIntervalMs);
|
|
127
|
+
pollInterval.unref?.();
|
|
120
128
|
return {
|
|
121
129
|
isNative: false,
|
|
122
130
|
close: () => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "md-semantic-search",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Local, private semantic (vector) search over any folder of Markdown files. Finds passages by meaning — cross-lingual — using transformers.js. No API keys, no cloud, no vector database.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
package/src/core.ts
CHANGED
|
@@ -436,6 +436,9 @@ export function assertSafePath(targetPath: string, allowedRoots?: string[]): str
|
|
|
436
436
|
if (targetPath.startsWith('\\\\') || targetPath.startsWith('//')) {
|
|
437
437
|
throw new Error('Forbidden path: UNC network paths not allowed');
|
|
438
438
|
}
|
|
439
|
+
if (/^[a-zA-Z]:[\\/]/.test(targetPath) && process.platform !== 'win32') {
|
|
440
|
+
throw new Error('Forbidden path: Windows drive paths not allowed on non-Windows systems');
|
|
441
|
+
}
|
|
439
442
|
const resolved = path.resolve(targetPath);
|
|
440
443
|
let canonical = resolved;
|
|
441
444
|
try {
|
package/src/serve.ts
CHANGED
|
@@ -92,14 +92,20 @@ export async function createServe(opts: CreateServeOptions): Promise<{
|
|
|
92
92
|
healthPublic,
|
|
93
93
|
};
|
|
94
94
|
|
|
95
|
+
const sockets = new Set<import('node:net').Socket>();
|
|
95
96
|
const server = http.createServer((req, res) => {
|
|
96
97
|
handleRequest(req, res, state).catch((e) => {
|
|
97
98
|
json(res, 500, { error: e.message });
|
|
98
99
|
});
|
|
99
100
|
});
|
|
101
|
+
server.on('connection', (socket) => {
|
|
102
|
+
sockets.add(socket);
|
|
103
|
+
socket.on('close', () => sockets.delete(socket));
|
|
104
|
+
});
|
|
100
105
|
|
|
101
106
|
let stopped = false;
|
|
102
107
|
let timer: NodeJS.Timeout | null = null;
|
|
108
|
+
let resolveTimer: (() => void) | null = null;
|
|
103
109
|
|
|
104
110
|
const reload = async () => {
|
|
105
111
|
if (!db) return false;
|
|
@@ -199,8 +205,14 @@ export async function createServe(opts: CreateServeOptions): Promise<{
|
|
|
199
205
|
let lastChangeAt = 0;
|
|
200
206
|
let pending = false;
|
|
201
207
|
while (!stopped) {
|
|
202
|
-
await new Promise((r) => {
|
|
203
|
-
|
|
208
|
+
await new Promise<void>((r) => {
|
|
209
|
+
resolveTimer = r;
|
|
210
|
+
timer = setTimeout(() => {
|
|
211
|
+
timer = null;
|
|
212
|
+
resolveTimer = null;
|
|
213
|
+
r();
|
|
214
|
+
}, watchInterval);
|
|
215
|
+
timer.unref?.();
|
|
204
216
|
});
|
|
205
217
|
if (stopped) return;
|
|
206
218
|
const tree = scanTree();
|
|
@@ -248,13 +260,22 @@ export async function createServe(opts: CreateServeOptions): Promise<{
|
|
|
248
260
|
|
|
249
261
|
const close = async (): Promise<void> => {
|
|
250
262
|
stopped = true;
|
|
251
|
-
if (timer)
|
|
263
|
+
if (timer) {
|
|
264
|
+
clearTimeout(timer);
|
|
265
|
+
timer = null;
|
|
266
|
+
}
|
|
267
|
+
if (resolveTimer) {
|
|
268
|
+
resolveTimer();
|
|
269
|
+
resolveTimer = null;
|
|
270
|
+
}
|
|
252
271
|
if (nativeWatcher) nativeWatcher.close();
|
|
253
|
-
// closeAllConnections() forcibly closes keep-alive sockets so server.close()
|
|
254
|
-
// doesn't hang indefinitely waiting for them — critical on Node 18 where the
|
|
255
|
-
// HTTP server does NOT auto-drain idle connections before calling back.
|
|
256
|
-
// The method was back-ported to Node 18.2.0 and is always present in CI.
|
|
257
272
|
server.closeAllConnections?.();
|
|
273
|
+
for (const socket of sockets) {
|
|
274
|
+
try {
|
|
275
|
+
socket.destroy();
|
|
276
|
+
} catch {}
|
|
277
|
+
}
|
|
278
|
+
sockets.clear();
|
|
258
279
|
await new Promise<void>((resolve, reject) => server.close((err) => (err ? reject(err) : resolve())));
|
|
259
280
|
};
|
|
260
281
|
|
package/src/watcher.ts
CHANGED
|
@@ -29,7 +29,7 @@ export function createFileWatcher(
|
|
|
29
29
|
debounceMs = 250,
|
|
30
30
|
ignore = [],
|
|
31
31
|
log = () => {},
|
|
32
|
-
fallbackIntervalMs =
|
|
32
|
+
fallbackIntervalMs = 50,
|
|
33
33
|
} = options;
|
|
34
34
|
|
|
35
35
|
let closed = false;
|
|
@@ -68,6 +68,7 @@ export function createFileWatcher(
|
|
|
68
68
|
log(`watcher onChange error: ${err.message}`);
|
|
69
69
|
}
|
|
70
70
|
}, debounceMs);
|
|
71
|
+
debounceTimer.unref?.();
|
|
71
72
|
};
|
|
72
73
|
|
|
73
74
|
// Attempt recursive native fs.watch first
|
|
@@ -88,6 +89,12 @@ export function createFileWatcher(
|
|
|
88
89
|
log(`Native watcher error (${err.message}), continuing.`);
|
|
89
90
|
});
|
|
90
91
|
|
|
92
|
+
// unref() lets the event loop exit even if the watcher is still active.
|
|
93
|
+
// Without this, on Node 18/Linux the inotify handle keeps the process
|
|
94
|
+
// alive indefinitely after all tests finish (or after close() is called
|
|
95
|
+
// but the handle hasn't been fully released by the kernel yet).
|
|
96
|
+
fsWatcher.unref?.();
|
|
97
|
+
|
|
91
98
|
return {
|
|
92
99
|
isNative: true,
|
|
93
100
|
close: () => {
|
|
@@ -105,12 +112,12 @@ export function createFileWatcher(
|
|
|
105
112
|
let lastMtime = 0;
|
|
106
113
|
const getDirMtime = (dir: string): number => {
|
|
107
114
|
try {
|
|
108
|
-
|
|
109
|
-
let max = stat.mtimeMs;
|
|
115
|
+
let max = 0;
|
|
110
116
|
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
111
117
|
for (const entry of entries) {
|
|
112
118
|
if (entry.name.startsWith('.') || entry.name === '.mdss') continue;
|
|
113
119
|
const full = path.join(dir, entry.name);
|
|
120
|
+
if (shouldIgnore(entry.name)) continue;
|
|
114
121
|
if (entry.isDirectory()) {
|
|
115
122
|
const sub = getDirMtime(full);
|
|
116
123
|
if (sub > max) max = sub;
|
|
@@ -136,6 +143,7 @@ export function createFileWatcher(
|
|
|
136
143
|
scheduleChange();
|
|
137
144
|
}
|
|
138
145
|
}, fallbackIntervalMs);
|
|
146
|
+
pollInterval.unref?.();
|
|
139
147
|
|
|
140
148
|
return {
|
|
141
149
|
isNative: false,
|