grepmax 0.12.10 → 0.12.12

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.
@@ -44,7 +44,6 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
44
44
  Object.defineProperty(exports, "__esModule", { value: true });
45
45
  exports.watch = void 0;
46
46
  const node_child_process_1 = require("node:child_process");
47
- const fs = __importStar(require("node:fs"));
48
47
  const path = __importStar(require("node:path"));
49
48
  const commander_1 = require("commander");
50
49
  const config_1 = require("../config");
@@ -80,16 +79,6 @@ exports.watch = new commander_1.Command("watch")
80
79
  if (options.background) {
81
80
  // Skip spawn if daemon already running — prevents process accumulation
82
81
  // when SessionStart hook fires on every session/clear/resume
83
- const pidFile = config_1.PATHS.daemonPidFile;
84
- try {
85
- const existingPid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
86
- if (existingPid) {
87
- process.kill(existingPid, 0); // throws if dead
88
- process.exit(0); // alive — skip
89
- }
90
- }
91
- catch (_c) { }
92
- // Also check socket as fallback
93
82
  const { isDaemonRunning } = yield Promise.resolve().then(() => __importStar(require("../lib/utils/daemon-client")));
94
83
  if (yield isDaemonRunning()) {
95
84
  process.exit(0);
package/dist/config.js CHANGED
@@ -96,6 +96,7 @@ exports.PATHS = {
96
96
  logsDir: path.join(GLOBAL_ROOT, "logs"),
97
97
  daemonSocket: path.join(GLOBAL_ROOT, "daemon.sock"),
98
98
  daemonPidFile: path.join(GLOBAL_ROOT, "daemon.pid"),
99
+ daemonLockFile: path.join(GLOBAL_ROOT, "daemon.lock"),
99
100
  // Centralized index storage — one database for all indexed directories
100
101
  lancedbDir: path.join(GLOBAL_ROOT, "lancedb"),
101
102
  cacheDir: path.join(GLOBAL_ROOT, "cache"),
@@ -41,12 +41,16 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
41
41
  step((generator = generator.apply(thisArg, _arguments || [])).next());
42
42
  });
43
43
  };
44
+ var __importDefault = (this && this.__importDefault) || function (mod) {
45
+ return (mod && mod.__esModule) ? mod : { "default": mod };
46
+ };
44
47
  Object.defineProperty(exports, "__esModule", { value: true });
45
48
  exports.Daemon = void 0;
46
49
  const fs = __importStar(require("node:fs"));
47
50
  const net = __importStar(require("node:net"));
48
51
  const path = __importStar(require("node:path"));
49
52
  const watcher = __importStar(require("@parcel/watcher"));
53
+ const proper_lockfile_1 = __importDefault(require("proper-lockfile"));
50
54
  const config_1 = require("../../config");
51
55
  const batch_processor_1 = require("../index/batch-processor");
52
56
  const watcher_1 = require("../index/watcher");
@@ -65,6 +69,7 @@ class Daemon {
65
69
  this.vectorDb = null;
66
70
  this.metaCache = null;
67
71
  this.server = null;
72
+ this.releaseLock = null;
68
73
  this.lastActivity = Date.now();
69
74
  this.startTime = Date.now();
70
75
  this.heartbeatInterval = null;
@@ -75,35 +80,37 @@ class Daemon {
75
80
  start() {
76
81
  return __awaiter(this, void 0, void 0, function* () {
77
82
  process.title = "gmax-daemon";
78
- // 1. Kill existing per-project watchers
83
+ // 1. Acquire exclusive lock — kernel-enforced, atomic, auto-released on death
84
+ fs.mkdirSync(path.dirname(config_1.PATHS.daemonLockFile), { recursive: true });
85
+ fs.writeFileSync(config_1.PATHS.daemonLockFile, "", { flag: "a" }); // ensure file exists
86
+ try {
87
+ this.releaseLock = yield proper_lockfile_1.default.lock(config_1.PATHS.daemonLockFile, {
88
+ retries: 0,
89
+ stale: 30000,
90
+ });
91
+ }
92
+ catch (err) {
93
+ if (err.code === "ELOCKED") {
94
+ console.error("[daemon] Another daemon is already running");
95
+ process.exit(0);
96
+ }
97
+ throw err;
98
+ }
99
+ // 2. Kill existing per-project watchers
79
100
  const existing = (0, watcher_store_1.listWatchers)();
80
101
  for (const w of existing) {
81
102
  console.log(`[daemon] Taking over from per-project watcher (PID: ${w.pid}, ${path.basename(w.projectRoot)})`);
82
103
  yield (0, process_1.killProcess)(w.pid);
83
104
  (0, watcher_store_1.unregisterWatcher)(w.pid);
84
105
  }
85
- // 2. PID file — atomic dedup guard
86
- const pidFile = config_1.PATHS.daemonPidFile;
87
- try {
88
- // Check if another daemon is alive
89
- const existingPid = Number.parseInt(fs.readFileSync(pidFile, "utf-8").trim(), 10);
90
- if (existingPid && existingPid !== process.pid) {
91
- try {
92
- process.kill(existingPid, 0); // throws if dead
93
- console.error("[daemon] Another daemon is already running (PID:", existingPid + ")");
94
- process.exit(0);
95
- }
96
- catch (_a) { }
97
- }
98
- }
99
- catch (_b) { }
100
- fs.writeFileSync(pidFile, String(process.pid));
101
- // 3. Stale socket cleanup
106
+ // 3. Write PID file (informational only lock is the real guard)
107
+ fs.writeFileSync(config_1.PATHS.daemonPidFile, String(process.pid));
108
+ // 4. Stale socket cleanup
102
109
  try {
103
110
  fs.unlinkSync(config_1.PATHS.daemonSocket);
104
111
  }
105
- catch (_c) { }
106
- // 3. Open shared resources
112
+ catch (_a) { }
113
+ // 5. Open shared resources
107
114
  try {
108
115
  fs.mkdirSync(config_1.PATHS.cacheDir, { recursive: true });
109
116
  fs.mkdirSync(config_1.PATHS.lancedbDir, { recursive: true });
@@ -114,9 +121,9 @@ class Daemon {
114
121
  console.error("[daemon] Failed to open shared resources:", err);
115
122
  throw err;
116
123
  }
117
- // 4. Register daemon (only after resources are open)
124
+ // 6. Register daemon (only after resources are open)
118
125
  (0, watcher_store_1.registerDaemon)(process.pid);
119
- // 5. Subscribe to all registered projects (skip missing directories)
126
+ // 7. Subscribe to all registered projects (skip missing directories)
120
127
  const projects = (0, project_registry_1.listProjects)().filter((p) => p.status === "indexed");
121
128
  for (const p of projects) {
122
129
  if (!fs.existsSync(p.root)) {
@@ -130,18 +137,18 @@ class Daemon {
130
137
  console.error(`[daemon] Failed to watch ${path.basename(p.root)}:`, err);
131
138
  }
132
139
  }
133
- // 6. Heartbeat
140
+ // 8. Heartbeat
134
141
  this.heartbeatInterval = setInterval(() => {
135
142
  (0, watcher_store_1.heartbeat)(process.pid);
136
143
  }, HEARTBEAT_INTERVAL_MS);
137
- // 7. Idle timeout
144
+ // 9. Idle timeout
138
145
  this.idleInterval = setInterval(() => {
139
146
  if (Date.now() - this.lastActivity > IDLE_TIMEOUT_MS) {
140
147
  console.log("[daemon] Idle for 30 minutes, shutting down");
141
148
  this.shutdown();
142
149
  }
143
150
  }, HEARTBEAT_INTERVAL_MS);
144
- // 8. Socket server
151
+ // 10. Socket server
145
152
  this.server = net.createServer((conn) => {
146
153
  let buf = "";
147
154
  conn.on("data", (chunk) => {
@@ -171,7 +178,7 @@ class Daemon {
171
178
  this.server.on("error", (err) => {
172
179
  const code = err.code;
173
180
  if (code === "EADDRINUSE") {
174
- console.error("[daemon] Another daemon is already running");
181
+ console.error("[daemon] Socket already in use");
175
182
  reject(err);
176
183
  }
177
184
  else if (code === "EOPNOTSUPP") {
@@ -301,7 +308,7 @@ class Daemon {
301
308
  catch (_d) { }
302
309
  }
303
310
  this.subscriptions.clear();
304
- // Close server + socket + PID file
311
+ // Close server + socket + PID file + lock
305
312
  (_a = this.server) === null || _a === void 0 ? void 0 : _a.close();
306
313
  try {
307
314
  fs.unlinkSync(config_1.PATHS.daemonSocket);
@@ -311,6 +318,13 @@ class Daemon {
311
318
  fs.unlinkSync(config_1.PATHS.daemonPidFile);
312
319
  }
313
320
  catch (_f) { }
321
+ if (this.releaseLock) {
322
+ try {
323
+ yield this.releaseLock();
324
+ }
325
+ catch (_g) { }
326
+ this.releaseLock = null;
327
+ }
314
328
  // Unregister all
315
329
  for (const root of this.processors.keys()) {
316
330
  (0, watcher_store_1.unregisterWatcherByRoot)(root);
@@ -321,11 +335,11 @@ class Daemon {
321
335
  try {
322
336
  yield ((_b = this.metaCache) === null || _b === void 0 ? void 0 : _b.close());
323
337
  }
324
- catch (_g) { }
338
+ catch (_h) { }
325
339
  try {
326
340
  yield ((_c = this.vectorDb) === null || _c === void 0 ? void 0 : _c.close());
327
341
  }
328
- catch (_h) { }
342
+ catch (_j) { }
329
343
  console.log("[daemon] Shutdown complete");
330
344
  });
331
345
  }
@@ -488,8 +488,12 @@ class VectorDB {
488
488
  (_a = this.unregisterCleanup) === null || _a === void 0 ? void 0 : _a.call(this);
489
489
  this.unregisterCleanup = undefined;
490
490
  if (this.db) {
491
- if (this.db.close)
492
- yield this.db.close();
491
+ if (this.db.close) {
492
+ yield Promise.race([
493
+ this.db.close(),
494
+ new Promise((resolve) => setTimeout(resolve, 5000)),
495
+ ]);
496
+ }
493
497
  }
494
498
  this.db = null;
495
499
  });
@@ -12,6 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.gracefulExit = gracefulExit;
13
13
  const pool_1 = require("../workers/pool");
14
14
  const cleanup_1 = require("./cleanup");
15
+ const EXIT_TIMEOUT_MS = 8000;
15
16
  function gracefulExit(code) {
16
17
  return __awaiter(this, void 0, void 0, function* () {
17
18
  const finalCode = typeof code === "number"
@@ -19,6 +20,12 @@ function gracefulExit(code) {
19
20
  : typeof process.exitCode === "number"
20
21
  ? process.exitCode
21
22
  : 0;
23
+ // Safety net: force-exit if cleanup hangs
24
+ const forceTimer = !process.env.VITEST && process.env.NODE_ENV !== "test"
25
+ ? setTimeout(() => process.exit(finalCode), EXIT_TIMEOUT_MS)
26
+ : undefined;
27
+ if (forceTimer)
28
+ forceTimer.unref();
22
29
  try {
23
30
  if ((0, pool_1.isWorkerPoolInitialized)()) {
24
31
  yield (0, pool_1.destroyWorkerPool)();
@@ -28,6 +35,8 @@ function gracefulExit(code) {
28
35
  console.error("[exit] Failed to destroy worker pool:", err);
29
36
  }
30
37
  yield (0, cleanup_1.runCleanup)();
38
+ if (forceTimer)
39
+ clearTimeout(forceTimer);
31
40
  // Avoid exiting the process during test runs so Vitest can report results.
32
41
  if (process.env.VITEST || process.env.NODE_ENV === "test") {
33
42
  process.exitCode = finalCode;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.12.10",
3
+ "version": "0.12.12",
4
4
  "author": "Robert Owens <robowens@me.com>",
5
5
  "homepage": "https://github.com/reowens/grepmax",
6
6
  "bugs": {
@@ -48,6 +48,7 @@
48
48
  "onnxruntime-node": "1.24.3",
49
49
  "ora": "^9.3.0",
50
50
  "piscina": "^5.1.4",
51
+ "proper-lockfile": "^4.1.2",
51
52
  "simsimd": "^6.5.5",
52
53
  "uuid": "^13.0.0",
53
54
  "web-tree-sitter": "^0.26.7",
@@ -57,6 +58,7 @@
57
58
  "@anthropic-ai/claude-agent-sdk": "^0.2.87",
58
59
  "@biomejs/biome": "2.4.10",
59
60
  "@types/node": "^25.5.0",
61
+ "@types/proper-lockfile": "^4.1.4",
60
62
  "node-gyp": "^12.1.0",
61
63
  "ts-node": "^10.9.2",
62
64
  "typescript": "^6.0.2",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "grepmax",
3
- "version": "0.12.10",
3
+ "version": "0.12.12",
4
4
  "description": "Semantic code search for Claude Code. Automatically indexes your project and provides intelligent search capabilities.",
5
5
  "author": {
6
6
  "name": "Robert Owens",