@aroman22/codegraph-vba-darwin-arm64 1.15.0 → 1.17.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 (230) hide show
  1. package/lib/dist/bin/codegraph.js +13 -7
  2. package/lib/dist/bin/codegraph.js.map +1 -1
  3. package/lib/dist/bin/daemon-release.d.ts +7 -0
  4. package/lib/dist/bin/daemon-release.d.ts.map +1 -0
  5. package/lib/dist/bin/daemon-release.js +18 -0
  6. package/lib/dist/bin/daemon-release.js.map +1 -0
  7. package/lib/dist/db/queries.d.ts +41 -0
  8. package/lib/dist/db/queries.d.ts.map +1 -1
  9. package/lib/dist/db/queries.js +78 -1
  10. package/lib/dist/db/queries.js.map +1 -1
  11. package/lib/dist/extraction/access-erd-extractor.d.ts +57 -0
  12. package/lib/dist/extraction/access-erd-extractor.d.ts.map +1 -0
  13. package/lib/dist/extraction/access-erd-extractor.js +389 -0
  14. package/lib/dist/extraction/access-erd-extractor.js.map +1 -0
  15. package/lib/dist/extraction/extraction-version.d.ts +1 -1
  16. package/lib/dist/extraction/extraction-version.js +1 -1
  17. package/lib/dist/extraction/grammars.d.ts +20 -0
  18. package/lib/dist/extraction/grammars.d.ts.map +1 -1
  19. package/lib/dist/extraction/grammars.js +34 -0
  20. package/lib/dist/extraction/grammars.js.map +1 -1
  21. package/lib/dist/extraction/index.d.ts +1 -0
  22. package/lib/dist/extraction/index.d.ts.map +1 -1
  23. package/lib/dist/extraction/index.js +371 -351
  24. package/lib/dist/extraction/index.js.map +1 -1
  25. package/lib/dist/extraction/parse-pool.d.ts +8 -3
  26. package/lib/dist/extraction/parse-pool.d.ts.map +1 -1
  27. package/lib/dist/extraction/parse-pool.js +1 -2
  28. package/lib/dist/extraction/parse-pool.js.map +1 -1
  29. package/lib/dist/extraction/parse-worker.js +7 -6
  30. package/lib/dist/extraction/parse-worker.js.map +1 -1
  31. package/lib/dist/extraction/sql-query-extractor.d.ts +16 -14
  32. package/lib/dist/extraction/sql-query-extractor.d.ts.map +1 -1
  33. package/lib/dist/extraction/sql-query-extractor.js +43 -31
  34. package/lib/dist/extraction/sql-query-extractor.js.map +1 -1
  35. package/lib/dist/extraction/sql-table-scan.d.ts +184 -0
  36. package/lib/dist/extraction/sql-table-scan.d.ts.map +1 -0
  37. package/lib/dist/extraction/sql-table-scan.js +288 -0
  38. package/lib/dist/extraction/sql-table-scan.js.map +1 -0
  39. package/lib/dist/extraction/tree-sitter.d.ts +10 -1
  40. package/lib/dist/extraction/tree-sitter.d.ts.map +1 -1
  41. package/lib/dist/extraction/tree-sitter.js +34 -3
  42. package/lib/dist/extraction/tree-sitter.js.map +1 -1
  43. package/lib/dist/extraction/vba/call-sweep.d.ts +2 -7
  44. package/lib/dist/extraction/vba/call-sweep.d.ts.map +1 -1
  45. package/lib/dist/extraction/vba/call-sweep.js +144 -90
  46. package/lib/dist/extraction/vba/call-sweep.js.map +1 -1
  47. package/lib/dist/extraction/vba/calls.d.ts +24 -4
  48. package/lib/dist/extraction/vba/calls.d.ts.map +1 -1
  49. package/lib/dist/extraction/vba/calls.js +42 -12
  50. package/lib/dist/extraction/vba/calls.js.map +1 -1
  51. package/lib/dist/extraction/vba/constants.d.ts +7 -14
  52. package/lib/dist/extraction/vba/constants.d.ts.map +1 -1
  53. package/lib/dist/extraction/vba/constants.js +8 -36
  54. package/lib/dist/extraction/vba/constants.js.map +1 -1
  55. package/lib/dist/extraction/vba/context.d.ts +443 -7
  56. package/lib/dist/extraction/vba/context.d.ts.map +1 -1
  57. package/lib/dist/extraction/vba/context.js +325 -29
  58. package/lib/dist/extraction/vba/context.js.map +1 -1
  59. package/lib/dist/extraction/vba/controls.d.ts +18 -1
  60. package/lib/dist/extraction/vba/controls.d.ts.map +1 -1
  61. package/lib/dist/extraction/vba/controls.js +99 -27
  62. package/lib/dist/extraction/vba/controls.js.map +1 -1
  63. package/lib/dist/extraction/vba/declarations.d.ts +1 -6
  64. package/lib/dist/extraction/vba/declarations.d.ts.map +1 -1
  65. package/lib/dist/extraction/vba/declarations.js +5 -39
  66. package/lib/dist/extraction/vba/declarations.js.map +1 -1
  67. package/lib/dist/extraction/vba/dims.d.ts +11 -7
  68. package/lib/dist/extraction/vba/dims.d.ts.map +1 -1
  69. package/lib/dist/extraction/vba/dims.js +265 -29
  70. package/lib/dist/extraction/vba/dims.js.map +1 -1
  71. package/lib/dist/extraction/vba/docmd.d.ts +29 -2
  72. package/lib/dist/extraction/vba/docmd.d.ts.map +1 -1
  73. package/lib/dist/extraction/vba/docmd.js +297 -18
  74. package/lib/dist/extraction/vba/docmd.js.map +1 -1
  75. package/lib/dist/extraction/vba/enums-consts.d.ts +7 -14
  76. package/lib/dist/extraction/vba/enums-consts.d.ts.map +1 -1
  77. package/lib/dist/extraction/vba/enums-consts.js +11 -45
  78. package/lib/dist/extraction/vba/enums-consts.js.map +1 -1
  79. package/lib/dist/extraction/vba/error-channel.d.ts +57 -0
  80. package/lib/dist/extraction/vba/error-channel.d.ts.map +1 -0
  81. package/lib/dist/extraction/vba/error-channel.js +128 -0
  82. package/lib/dist/extraction/vba/error-channel.js.map +1 -0
  83. package/lib/dist/extraction/vba/errors.d.ts +64 -0
  84. package/lib/dist/extraction/vba/errors.d.ts.map +1 -0
  85. package/lib/dist/extraction/vba/errors.js +775 -0
  86. package/lib/dist/extraction/vba/errors.js.map +1 -0
  87. package/lib/dist/extraction/vba/filesystem-statements.d.ts +23 -0
  88. package/lib/dist/extraction/vba/filesystem-statements.d.ts.map +1 -0
  89. package/lib/dist/extraction/vba/filesystem-statements.js +70 -0
  90. package/lib/dist/extraction/vba/filesystem-statements.js.map +1 -0
  91. package/lib/dist/extraction/vba/implements.d.ts +1 -6
  92. package/lib/dist/extraction/vba/implements.d.ts.map +1 -1
  93. package/lib/dist/extraction/vba/implements.js +1 -22
  94. package/lib/dist/extraction/vba/implements.js.map +1 -1
  95. package/lib/dist/extraction/vba/labels.d.ts +26 -0
  96. package/lib/dist/extraction/vba/labels.d.ts.map +1 -0
  97. package/lib/dist/extraction/vba/labels.js +180 -0
  98. package/lib/dist/extraction/vba/labels.js.map +1 -0
  99. package/lib/dist/extraction/vba/module-vars.d.ts +36 -0
  100. package/lib/dist/extraction/vba/module-vars.d.ts.map +1 -0
  101. package/lib/dist/extraction/vba/module-vars.js +145 -0
  102. package/lib/dist/extraction/vba/module-vars.js.map +1 -0
  103. package/lib/dist/extraction/vba/options.d.ts +88 -0
  104. package/lib/dist/extraction/vba/options.d.ts.map +1 -0
  105. package/lib/dist/extraction/vba/options.js +39 -0
  106. package/lib/dist/extraction/vba/options.js.map +1 -0
  107. package/lib/dist/extraction/vba/parameters.d.ts +35 -0
  108. package/lib/dist/extraction/vba/parameters.d.ts.map +1 -0
  109. package/lib/dist/extraction/vba/parameters.js +110 -0
  110. package/lib/dist/extraction/vba/parameters.js.map +1 -0
  111. package/lib/dist/extraction/vba/procedures.d.ts +1 -9
  112. package/lib/dist/extraction/vba/procedures.d.ts.map +1 -1
  113. package/lib/dist/extraction/vba/procedures.js +174 -112
  114. package/lib/dist/extraction/vba/procedures.js.map +1 -1
  115. package/lib/dist/extraction/vba/rules.d.ts +10 -5
  116. package/lib/dist/extraction/vba/rules.d.ts.map +1 -1
  117. package/lib/dist/extraction/vba/rules.js +19 -0
  118. package/lib/dist/extraction/vba/rules.js.map +1 -1
  119. package/lib/dist/extraction/vba/runtime-objects.d.ts +59 -0
  120. package/lib/dist/extraction/vba/runtime-objects.d.ts.map +1 -0
  121. package/lib/dist/extraction/vba/runtime-objects.js +106 -0
  122. package/lib/dist/extraction/vba/runtime-objects.js.map +1 -0
  123. package/lib/dist/extraction/vba/signature.d.ts +57 -0
  124. package/lib/dist/extraction/vba/signature.d.ts.map +1 -0
  125. package/lib/dist/extraction/vba/signature.js +164 -0
  126. package/lib/dist/extraction/vba/signature.js.map +1 -0
  127. package/lib/dist/extraction/vba/sql-wrapper.d.ts +76 -3
  128. package/lib/dist/extraction/vba/sql-wrapper.d.ts.map +1 -1
  129. package/lib/dist/extraction/vba/sql-wrapper.js +725 -151
  130. package/lib/dist/extraction/vba/sql-wrapper.js.map +1 -1
  131. package/lib/dist/extraction/vba/tempvars.d.ts.map +1 -1
  132. package/lib/dist/extraction/vba/tempvars.js +4 -0
  133. package/lib/dist/extraction/vba/tempvars.js.map +1 -1
  134. package/lib/dist/extraction/vba/text-utils.d.ts +62 -2
  135. package/lib/dist/extraction/vba/text-utils.d.ts.map +1 -1
  136. package/lib/dist/extraction/vba/text-utils.js +84 -6
  137. package/lib/dist/extraction/vba/text-utils.js.map +1 -1
  138. package/lib/dist/extraction/vba-extractor.d.ts +18 -1
  139. package/lib/dist/extraction/vba-extractor.d.ts.map +1 -1
  140. package/lib/dist/extraction/vba-extractor.js +111 -16
  141. package/lib/dist/extraction/vba-extractor.js.map +1 -1
  142. package/lib/dist/extraction/vba-form-extractor.d.ts +28 -14
  143. package/lib/dist/extraction/vba-form-extractor.d.ts.map +1 -1
  144. package/lib/dist/extraction/vba-form-extractor.js +56 -21
  145. package/lib/dist/extraction/vba-form-extractor.js.map +1 -1
  146. package/lib/dist/extraction/vba-preprocess.d.ts +89 -3
  147. package/lib/dist/extraction/vba-preprocess.d.ts.map +1 -1
  148. package/lib/dist/extraction/vba-preprocess.js +221 -57
  149. package/lib/dist/extraction/vba-preprocess.js.map +1 -1
  150. package/lib/dist/extraction/vba-source.d.ts +0 -10
  151. package/lib/dist/extraction/vba-source.d.ts.map +1 -1
  152. package/lib/dist/extraction/vba-source.js +0 -13
  153. package/lib/dist/extraction/vba-source.js.map +1 -1
  154. package/lib/dist/extraction/vba-test-manifest-extractor.d.ts +0 -6
  155. package/lib/dist/extraction/vba-test-manifest-extractor.d.ts.map +1 -1
  156. package/lib/dist/extraction/vba-test-manifest-extractor.js +0 -1
  157. package/lib/dist/extraction/vba-test-manifest-extractor.js.map +1 -1
  158. package/lib/dist/graph/behavior-evidence.d.ts +162 -0
  159. package/lib/dist/graph/behavior-evidence.d.ts.map +1 -0
  160. package/lib/dist/graph/behavior-evidence.js +529 -0
  161. package/lib/dist/graph/behavior-evidence.js.map +1 -0
  162. package/lib/dist/index.d.ts +22 -0
  163. package/lib/dist/index.d.ts.map +1 -1
  164. package/lib/dist/index.js +37 -1
  165. package/lib/dist/index.js.map +1 -1
  166. package/lib/dist/mcp/daemon-manager.d.ts.map +1 -1
  167. package/lib/dist/mcp/daemon-manager.js +7 -3
  168. package/lib/dist/mcp/daemon-manager.js.map +1 -1
  169. package/lib/dist/mcp/daemon-paths.d.ts +6 -0
  170. package/lib/dist/mcp/daemon-paths.d.ts.map +1 -1
  171. package/lib/dist/mcp/daemon-paths.js +15 -0
  172. package/lib/dist/mcp/daemon-paths.js.map +1 -1
  173. package/lib/dist/mcp/daemon-registry.d.ts +82 -7
  174. package/lib/dist/mcp/daemon-registry.d.ts.map +1 -1
  175. package/lib/dist/mcp/daemon-registry.js +557 -45
  176. package/lib/dist/mcp/daemon-registry.js.map +1 -1
  177. package/lib/dist/mcp/daemon-watchdog.d.ts +12 -0
  178. package/lib/dist/mcp/daemon-watchdog.d.ts.map +1 -1
  179. package/lib/dist/mcp/daemon-watchdog.js +37 -7
  180. package/lib/dist/mcp/daemon-watchdog.js.map +1 -1
  181. package/lib/dist/mcp/daemon.d.ts +20 -1
  182. package/lib/dist/mcp/daemon.d.ts.map +1 -1
  183. package/lib/dist/mcp/daemon.js +123 -30
  184. package/lib/dist/mcp/daemon.js.map +1 -1
  185. package/lib/dist/mcp/index.d.ts.map +1 -1
  186. package/lib/dist/mcp/index.js +13 -2
  187. package/lib/dist/mcp/index.js.map +1 -1
  188. package/lib/dist/mcp/proxy.d.ts +32 -0
  189. package/lib/dist/mcp/proxy.d.ts.map +1 -1
  190. package/lib/dist/mcp/proxy.js +163 -31
  191. package/lib/dist/mcp/proxy.js.map +1 -1
  192. package/lib/dist/mcp/server-instructions.d.ts +1 -1
  193. package/lib/dist/mcp/server-instructions.d.ts.map +1 -1
  194. package/lib/dist/mcp/server-instructions.js +26 -4
  195. package/lib/dist/mcp/server-instructions.js.map +1 -1
  196. package/lib/dist/mcp/tools.d.ts +12 -0
  197. package/lib/dist/mcp/tools.d.ts.map +1 -1
  198. package/lib/dist/mcp/tools.js +92 -1
  199. package/lib/dist/mcp/tools.js.map +1 -1
  200. package/lib/dist/project-config.d.ts +43 -2
  201. package/lib/dist/project-config.d.ts.map +1 -1
  202. package/lib/dist/project-config.js +86 -2
  203. package/lib/dist/project-config.js.map +1 -1
  204. package/lib/dist/resolution/index.d.ts +47 -2
  205. package/lib/dist/resolution/index.d.ts.map +1 -1
  206. package/lib/dist/resolution/index.js +160 -5
  207. package/lib/dist/resolution/index.js.map +1 -1
  208. package/lib/dist/resolution/name-matcher.d.ts +25 -0
  209. package/lib/dist/resolution/name-matcher.d.ts.map +1 -1
  210. package/lib/dist/resolution/name-matcher.js +59 -0
  211. package/lib/dist/resolution/name-matcher.js.map +1 -1
  212. package/lib/dist/resolution/vba-runtime-objects.d.ts +11 -11
  213. package/lib/dist/resolution/vba-runtime-objects.d.ts.map +1 -1
  214. package/lib/dist/resolution/vba-runtime-objects.js +18 -38
  215. package/lib/dist/resolution/vba-runtime-objects.js.map +1 -1
  216. package/lib/dist/types.d.ts +13 -5
  217. package/lib/dist/types.d.ts.map +1 -1
  218. package/lib/dist/types.js +17 -0
  219. package/lib/dist/types.js.map +1 -1
  220. package/lib/dist/utils/backtrace-helpers.d.ts +14 -2
  221. package/lib/dist/utils/backtrace-helpers.d.ts.map +1 -1
  222. package/lib/dist/utils/backtrace-helpers.js +81 -7
  223. package/lib/dist/utils/backtrace-helpers.js.map +1 -1
  224. package/lib/dist/utils/sql-impact-helpers.d.ts.map +1 -1
  225. package/lib/dist/utils/sql-impact-helpers.js +119 -30
  226. package/lib/dist/utils/sql-impact-helpers.js.map +1 -1
  227. package/lib/node_modules/.modules.yaml +1 -1
  228. package/lib/node_modules/.pnpm-workspace-state-v1.json +1 -1
  229. package/lib/package.json +3 -1
  230. package/package.json +1 -1
@@ -33,12 +33,22 @@ var __importStar = (this && this.__importStar) || (function () {
33
33
  };
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.DAEMON_RELEASE_LEASE_TTL_MS = void 0;
36
37
  exports.getRegistryDir = getRegistryDir;
37
38
  exports.isProcessAlive = isProcessAlive;
38
39
  exports.registerDaemon = registerDaemon;
39
40
  exports.deregisterDaemon = deregisterDaemon;
40
41
  exports.listDaemons = listDaemons;
42
+ exports.tryAcquireDaemonLifecycleLock = tryAcquireDaemonLifecycleLock;
43
+ exports.releaseDaemonLifecycleLock = releaseDaemonLifecycleLock;
44
+ exports.hasActiveDaemonReleaseLease = hasActiveDaemonReleaseLease;
45
+ exports.tryAcquireDaemonReleaseLease = tryAcquireDaemonReleaseLease;
46
+ exports.refreshDaemonReleaseLease = refreshDaemonReleaseLease;
47
+ exports.canonicalDaemonRoot = canonicalDaemonRoot;
48
+ exports.canonicalDaemonRootKey = canonicalDaemonRootKey;
49
+ exports.releaseDaemonAt = releaseDaemonAt;
41
50
  exports.stopDaemonAt = stopDaemonAt;
51
+ exports.requestDaemonRelease = requestDaemonRelease;
42
52
  exports.stopAllDaemons = stopAllDaemons;
43
53
  /**
44
54
  * Global daemon registry + stop/list control — the discovery layer behind
@@ -64,6 +74,7 @@ const fs = __importStar(require("fs"));
64
74
  const os = __importStar(require("os"));
65
75
  const path = __importStar(require("path"));
66
76
  const crypto = __importStar(require("crypto"));
77
+ const net = __importStar(require("net"));
67
78
  const daemon_paths_1 = require("./daemon-paths");
68
79
  /**
69
80
  * `~/.codegraph/daemons` — GLOBAL, keyed off the home install dir. (The
@@ -148,24 +159,46 @@ function listDaemons(opts = {}) {
148
159
  }
149
160
  return live.sort((a, b) => b.startedAt - a.startedAt);
150
161
  }
151
- /** Remove a stopped daemon's leftover lockfile + socket + registry record. */
152
- function cleanupDaemonArtifacts(root) {
162
+ function sameGeneration(actual, expected) {
163
+ return Boolean(actual) && actual.pid === expected.pid && actual.startedAt === expected.startedAt &&
164
+ actual.socketPath === expected.socketPath;
165
+ }
166
+ /** Delete only artifacts that still belong to the stopped daemon generation. */
167
+ function cleanupDaemonArtifacts(root, expected, beforeDelete) {
168
+ let lock = null;
169
+ let registry = null;
153
170
  try {
154
- fs.unlinkSync((0, daemon_paths_1.getDaemonPidPath)(root));
171
+ lock = (0, daemon_paths_1.decodeLockInfo)(fs.readFileSync((0, daemon_paths_1.getDaemonPidPath)(root), 'utf8'));
155
172
  }
156
173
  catch { /* gone */ }
157
- // POSIX sockets are real files; Windows named pipes vanish with the process.
158
- // Sweep every candidate — a daemon that relocated past an unusable in-project
159
- // FS (ExFAT/FAT; #997) left its socket at the tmpdir fallback, not candidate 0.
160
- if (process.platform !== 'win32') {
161
- for (const candidate of (0, daemon_paths_1.getDaemonSocketCandidates)(root)) {
162
- try {
163
- fs.unlinkSync(candidate);
164
- }
165
- catch { /* gone */ }
174
+ try {
175
+ registry = JSON.parse(fs.readFileSync(recordPath(root), 'utf8'));
176
+ }
177
+ catch { /* gone */ }
178
+ const lockMatches = sameGeneration(lock, expected);
179
+ const registryMatches = sameGeneration(registry, expected);
180
+ const successorExists = Boolean(lock && !lockMatches) || Boolean(registry && !registryMatches);
181
+ beforeDelete?.();
182
+ if (lockMatches) {
183
+ try {
184
+ fs.unlinkSync((0, daemon_paths_1.getDaemonPidPath)(root));
185
+ }
186
+ catch { /* raced */ }
187
+ }
188
+ if (registryMatches) {
189
+ try {
190
+ fs.unlinkSync(recordPath(root));
166
191
  }
192
+ catch { /* raced */ }
193
+ }
194
+ // A successor can reuse the stable socket path. Never unlink it when either
195
+ // authoritative ownership record has already advanced to a new generation.
196
+ if (!successorExists && process.platform !== 'win32' && expected.socketPath) {
197
+ try {
198
+ fs.unlinkSync(expected.socketPath);
199
+ }
200
+ catch { /* gone */ }
167
201
  }
168
- deregisterDaemon(root);
169
202
  }
170
203
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
171
204
  async function waitForDeath(pid, timeoutMs) {
@@ -177,50 +210,529 @@ async function waitForDeath(pid, timeoutMs) {
177
210
  }
178
211
  return !isProcessAlive(pid);
179
212
  }
180
- /**
181
- * Stop the daemon serving `root`: SIGTERM, wait, then SIGKILL if it won't go,
182
- * then sweep its artifacts. `root` must be realpath'd (match how the daemon
183
- * keys its socket/lockfile). Resolves the pid from the authoritative lockfile,
184
- * falling back to the registry.
185
- */
186
- async function stopDaemonAt(root) {
187
- let pid = null;
213
+ exports.DAEMON_RELEASE_LEASE_TTL_MS = 30_000;
214
+ const DAEMON_RELEASE_HEARTBEAT_MS = 5_000;
215
+ const DAEMON_LIFECYCLE_TTL_MS = 30_000;
216
+ function lifecycleArtifacts(root) {
217
+ const fixed = (0, daemon_paths_1.getDaemonLifecyclePath)(root);
218
+ const dir = path.dirname(fixed);
219
+ const prefix = path.basename(fixed);
220
+ try {
221
+ return fs.readdirSync(dir).filter((name) => name === prefix || name.startsWith(`${prefix}.claim.`)).map((name) => path.join(dir, name));
222
+ }
223
+ catch {
224
+ return [];
225
+ }
226
+ }
227
+ function cleanupExpiredLifecycleArtifacts(root, now) {
228
+ const fixed = (0, daemon_paths_1.getDaemonLifecyclePath)(root);
229
+ for (const artifact of lifecycleArtifacts(root)) {
230
+ let expiresAt;
231
+ try {
232
+ const value = JSON.parse(fs.readFileSync(artifact, 'utf8'));
233
+ expiresAt = typeof value.expiresAt === 'number' ? value.expiresAt : fs.statSync(artifact).mtimeMs + DAEMON_LIFECYCLE_TTL_MS;
234
+ }
235
+ catch {
236
+ try {
237
+ expiresAt = fs.statSync(artifact).mtimeMs + DAEMON_LIFECYCLE_TTL_MS;
238
+ }
239
+ catch {
240
+ continue;
241
+ }
242
+ }
243
+ if (expiresAt > now)
244
+ continue;
245
+ if (artifact === fixed) {
246
+ const claim = `${fixed}.claim.${crypto.randomUUID()}`;
247
+ try {
248
+ fs.renameSync(fixed, claim);
249
+ }
250
+ catch {
251
+ continue;
252
+ }
253
+ try {
254
+ fs.unlinkSync(claim);
255
+ }
256
+ catch { /* gone */ }
257
+ }
258
+ else {
259
+ try {
260
+ fs.unlinkSync(artifact);
261
+ }
262
+ catch { /* gone */ }
263
+ }
264
+ }
265
+ }
266
+ /** Atomically acquire the shared startup/release publication arbiter. */
267
+ function tryAcquireDaemonLifecycleLock(root, options = {}) {
268
+ const now = options.now ?? Date.now();
269
+ cleanupExpiredLifecycleArtifacts(root, now);
270
+ if (lifecycleArtifacts(root).length !== 0)
271
+ return null;
272
+ const fixed = (0, daemon_paths_1.getDaemonLifecyclePath)(root);
273
+ const lock = { token: crypto.randomUUID(), ownerPid: process.pid, createdAt: now, expiresAt: now + DAEMON_LIFECYCLE_TTL_MS };
274
+ const tmp = `${fixed}.${lock.token}.tmp`;
275
+ try {
276
+ fs.writeFileSync(tmp, JSON.stringify(lock), { mode: 0o600 });
277
+ try {
278
+ (options.linkSync ?? fs.linkSync)(tmp, fixed);
279
+ }
280
+ catch (err) {
281
+ const code = err.code;
282
+ if (code === 'EEXIST')
283
+ return null;
284
+ if (code !== 'EPERM' && code !== 'ENOTSUP' && code !== 'EXDEV')
285
+ throw err;
286
+ let fd;
287
+ try {
288
+ fd = fs.openSync(fixed, 'wx', 0o600);
289
+ }
290
+ catch (openErr) {
291
+ if (openErr.code === 'EEXIST')
292
+ return null;
293
+ throw openErr;
294
+ }
295
+ try {
296
+ fs.writeSync(fd, JSON.stringify(lock));
297
+ fs.fsyncSync(fd);
298
+ }
299
+ finally {
300
+ fs.closeSync(fd);
301
+ }
302
+ }
303
+ }
304
+ finally {
305
+ try {
306
+ fs.unlinkSync(tmp);
307
+ }
308
+ catch { /* gone */ }
309
+ }
310
+ return lock;
311
+ }
312
+ function releaseDaemonLifecycleLock(root, lock) {
313
+ const fixed = (0, daemon_paths_1.getDaemonLifecyclePath)(root);
314
+ try {
315
+ const current = JSON.parse(fs.readFileSync(fixed, 'utf8'));
316
+ if (current.token === lock.token)
317
+ fs.unlinkSync(fixed);
318
+ }
319
+ catch { /* gone */ }
320
+ }
321
+ function decodeReleaseLease(raw) {
322
+ try {
323
+ const value = JSON.parse(raw);
324
+ if (typeof value.token !== 'string' || typeof value.ownerPid !== 'number' ||
325
+ typeof value.createdAt !== 'number' || typeof value.heartbeatAt !== 'number' ||
326
+ typeof value.expiresAt !== 'number' || !value.generation ||
327
+ typeof value.generation.pid !== 'number' || typeof value.generation.startedAt !== 'number' ||
328
+ typeof value.generation.socketPath !== 'string')
329
+ return null;
330
+ return value;
331
+ }
332
+ catch {
333
+ return null;
334
+ }
335
+ }
336
+ function recoveryArtifacts(root) {
337
+ const markerPath = (0, daemon_paths_1.getDaemonReleaseRecoveryPath)(root);
338
+ const dir = path.dirname(markerPath);
339
+ const prefix = path.basename(markerPath);
340
+ try {
341
+ return fs.readdirSync(dir).filter((name) => name === prefix || name.startsWith(`${prefix}.claim.`)).map((name) => path.join(dir, name));
342
+ }
343
+ catch {
344
+ return [];
345
+ }
346
+ }
347
+ /** Recover marker/claim files left by a crashed stale-recovery owner. */
348
+ function cleanupExpiredRecoveryArtifacts(root, now) {
349
+ const markerPath = (0, daemon_paths_1.getDaemonReleaseRecoveryPath)(root);
350
+ for (const artifact of recoveryArtifacts(root)) {
351
+ let expiresAt = 0;
352
+ try {
353
+ const raw = JSON.parse(fs.readFileSync(artifact, 'utf8'));
354
+ expiresAt = typeof raw.expiresAt === 'number' ? raw.expiresAt : fs.statSync(artifact).mtimeMs + exports.DAEMON_RELEASE_LEASE_TTL_MS;
355
+ }
356
+ catch {
357
+ try {
358
+ expiresAt = fs.statSync(artifact).mtimeMs + exports.DAEMON_RELEASE_LEASE_TTL_MS;
359
+ }
360
+ catch {
361
+ continue;
362
+ }
363
+ }
364
+ if (expiresAt > now)
365
+ continue;
366
+ if (artifact === markerPath) {
367
+ const claim = `${markerPath}.claim.marker-${crypto.randomUUID()}`;
368
+ try {
369
+ fs.renameSync(markerPath, claim);
370
+ }
371
+ catch {
372
+ continue;
373
+ }
374
+ try {
375
+ fs.unlinkSync(claim);
376
+ }
377
+ catch { /* gone */ }
378
+ }
379
+ else {
380
+ // Claims are immutable token-specific generations; deleting the expired
381
+ // claim cannot target a newly-created fixed marker or lease.
382
+ try {
383
+ fs.unlinkSync(artifact);
384
+ }
385
+ catch { /* gone */ }
386
+ }
387
+ }
388
+ }
389
+ function acquireRecoveryMarker(root, now) {
390
+ cleanupExpiredRecoveryArtifacts(root, now);
391
+ if (recoveryArtifacts(root).length !== 0)
392
+ return null;
393
+ const markerPath = (0, daemon_paths_1.getDaemonReleaseRecoveryPath)(root);
394
+ const marker = { token: crypto.randomUUID(), createdAt: now, expiresAt: now + exports.DAEMON_RELEASE_LEASE_TTL_MS };
188
395
  try {
189
- const info = (0, daemon_paths_1.decodeLockInfo)(fs.readFileSync((0, daemon_paths_1.getDaemonPidPath)(root), 'utf8'));
190
- pid = info?.pid ?? null;
396
+ fs.writeFileSync(markerPath, JSON.stringify(marker), { flag: 'wx', mode: 0o600 });
191
397
  }
192
398
  catch {
193
- /* no lockfile */
399
+ return null;
400
+ }
401
+ return marker;
402
+ }
403
+ function releaseRecoveryMarker(root, marker) {
404
+ const markerPath = (0, daemon_paths_1.getDaemonReleaseRecoveryPath)(root);
405
+ try {
406
+ const current = JSON.parse(fs.readFileSync(markerPath, 'utf8'));
407
+ if (current.token === marker.token)
408
+ fs.unlinkSync(markerPath);
409
+ }
410
+ catch { /* gone or replaced */ }
411
+ }
412
+ /** Recover one expired immutable lease generation under an exclusive marker. */
413
+ function recoverStaleReleaseLease(root, now, afterRead) {
414
+ const leasePath = (0, daemon_paths_1.getDaemonReleaseLeasePath)(root);
415
+ let current = null;
416
+ let modifiedAt = now;
417
+ try {
418
+ current = decodeReleaseLease(fs.readFileSync(leasePath, 'utf8'));
419
+ modifiedAt = fs.statSync(leasePath).mtimeMs;
420
+ }
421
+ catch {
422
+ return true;
423
+ }
424
+ // Fallback exclusive-create can be observed while its complete body is being
425
+ // written. Fail closed for one full TTL; a crashed partial write then expires.
426
+ const expired = current ? current.expiresAt <= now : modifiedAt + exports.DAEMON_RELEASE_LEASE_TTL_MS <= now;
427
+ if (!expired)
428
+ return false;
429
+ afterRead?.();
430
+ const marker = acquireRecoveryMarker(root, now);
431
+ if (!marker)
432
+ return false;
433
+ const claimPath = `${(0, daemon_paths_1.getDaemonReleaseRecoveryPath)(root)}.claim.lease-${current?.token ?? crypto.randomUUID()}`;
434
+ try {
435
+ try {
436
+ fs.renameSync(leasePath, claimPath);
437
+ }
438
+ catch {
439
+ return true;
440
+ }
441
+ let claimed = null;
442
+ let claimExpiry = now + exports.DAEMON_RELEASE_LEASE_TTL_MS;
443
+ try {
444
+ claimed = decodeReleaseLease(fs.readFileSync(claimPath, 'utf8'));
445
+ claimExpiry = claimed?.expiresAt ?? fs.statSync(claimPath).mtimeMs + exports.DAEMON_RELEASE_LEASE_TTL_MS;
446
+ }
447
+ catch { /* fail closed below */ }
448
+ if (claimExpiry > now) {
449
+ // Heartbeat won after our preliminary expired read. Keep its renewed
450
+ // generation authoritative; the marker blocks startup during restoration.
451
+ if (fs.existsSync(leasePath)) {
452
+ const fixed = decodeReleaseLease(fs.readFileSync(leasePath, 'utf8'));
453
+ if (fixed && fixed.token === claimed?.token && fixed.expiresAt >= claimExpiry) {
454
+ try {
455
+ fs.unlinkSync(claimPath);
456
+ }
457
+ catch { /* gone */ }
458
+ return false;
459
+ }
460
+ return false;
461
+ }
462
+ fs.renameSync(claimPath, leasePath);
463
+ return false;
464
+ }
465
+ try {
466
+ fs.unlinkSync(claimPath);
467
+ }
468
+ catch { /* gone */ }
469
+ return true;
470
+ }
471
+ finally {
472
+ releaseRecoveryMarker(root, marker);
473
+ }
474
+ }
475
+ /** True while intentional release exclusively owns this root's lifecycle. */
476
+ function hasActiveDaemonReleaseLease(root, now = Date.now(), hooks = {}) {
477
+ const leasePath = (0, daemon_paths_1.getDaemonReleaseLeasePath)(root);
478
+ cleanupExpiredRecoveryArtifacts(root, now);
479
+ if (recoveryArtifacts(root).length !== 0)
480
+ return true;
481
+ if (!fs.existsSync(leasePath))
482
+ return false;
483
+ return !recoverStaleReleaseLease(root, now, hooks.afterRecoveryRead);
484
+ }
485
+ /** Atomically claim release/cleanup ownership for one daemon generation. */
486
+ function tryAcquireDaemonReleaseLease(root, expected, options = {}) {
487
+ const now = options.now ?? Date.now();
488
+ if (hasActiveDaemonReleaseLease(root, now))
489
+ return null;
490
+ const leasePath = (0, daemon_paths_1.getDaemonReleaseLeasePath)(root);
491
+ const lease = {
492
+ token: crypto.randomUUID(),
493
+ ownerPid: process.pid,
494
+ createdAt: now,
495
+ heartbeatAt: now,
496
+ expiresAt: now + exports.DAEMON_RELEASE_LEASE_TTL_MS,
497
+ generation: { pid: expected.pid, startedAt: expected.startedAt, socketPath: expected.socketPath },
498
+ };
499
+ // Publish a fully-written immutable record in one atomic exclusive link, the
500
+ // same primitive used by daemon.pid. No observer can see an empty lease.
501
+ const tmpPath = `${leasePath}.${lease.token}.tmp`;
502
+ try {
503
+ fs.writeFileSync(tmpPath, JSON.stringify(lease, null, 2) + '\n', { mode: 0o600 });
504
+ try {
505
+ (options.linkSync ?? fs.linkSync)(tmpPath, leasePath);
506
+ }
507
+ catch (err) {
508
+ if (err.code === 'EEXIST')
509
+ return null;
510
+ // Mirror daemon.pid's portable fallback for filesystems without hard
511
+ // links. O_EXCL keeps acquisition exclusive; readers fail closed while
512
+ // the complete body is written through the owning descriptor.
513
+ let fd;
514
+ try {
515
+ fd = fs.openSync(leasePath, 'wx', 0o600);
516
+ }
517
+ catch (openErr) {
518
+ if (openErr.code === 'EEXIST')
519
+ return null;
520
+ throw openErr;
521
+ }
522
+ try {
523
+ fs.writeSync(fd, JSON.stringify(lease, null, 2) + '\n');
524
+ fs.fsyncSync(fd);
525
+ }
526
+ finally {
527
+ fs.closeSync(fd);
528
+ }
529
+ }
194
530
  }
195
- if (pid == null) {
196
- const rec = listDaemons({ prune: false }).find((r) => path.resolve(r.root) === path.resolve(root));
197
- pid = rec?.pid ?? null;
531
+ finally {
532
+ try {
533
+ fs.unlinkSync(tmpPath);
534
+ }
535
+ catch { /* gone */ }
198
536
  }
199
- if (pid == null) {
200
- cleanupDaemonArtifacts(root);
201
- return { root, pid: null, outcome: 'no-daemon' };
537
+ return lease;
538
+ }
539
+ /** Publish a complete renewed generation atomically under the recovery marker. */
540
+ function refreshDaemonReleaseLease(root, lease, now = Date.now()) {
541
+ const leasePath = (0, daemon_paths_1.getDaemonReleaseLeasePath)(root);
542
+ const marker = acquireRecoveryMarker(root, now);
543
+ if (!marker)
544
+ return false;
545
+ const tmpPath = `${leasePath}.${lease.token}.heartbeat.tmp`;
546
+ try {
547
+ const current = decodeReleaseLease(fs.readFileSync(leasePath, 'utf8'));
548
+ if (current?.token !== lease.token)
549
+ return false;
550
+ lease.heartbeatAt = now;
551
+ lease.expiresAt = now + exports.DAEMON_RELEASE_LEASE_TTL_MS;
552
+ fs.writeFileSync(tmpPath, JSON.stringify(lease, null, 2) + '\n', { mode: 0o600 });
553
+ fs.renameSync(tmpPath, leasePath);
554
+ return true;
555
+ }
556
+ finally {
557
+ try {
558
+ fs.unlinkSync(tmpPath);
559
+ }
560
+ catch { /* published or gone */ }
561
+ releaseRecoveryMarker(root, marker);
562
+ }
563
+ }
564
+ function releaseDaemonReleaseLease(root, lease) {
565
+ const leasePath = (0, daemon_paths_1.getDaemonReleaseLeasePath)(root);
566
+ let current = null;
567
+ try {
568
+ current = decodeReleaseLease(fs.readFileSync(leasePath, 'utf8'));
202
569
  }
203
- if (!isProcessAlive(pid)) {
204
- cleanupDaemonArtifacts(root);
205
- return { root, pid, outcome: 'not-running' };
570
+ catch {
571
+ return;
206
572
  }
207
- // POSIX: SIGTERM runs the daemon's graceful shutdown. Windows: TerminateProcess
208
- // (no graceful path), so we always sweep artifacts ourselves below.
573
+ if (current?.token !== lease.token)
574
+ return;
209
575
  try {
210
- process.kill(pid, 'SIGTERM');
576
+ fs.unlinkSync(leasePath);
577
+ }
578
+ catch { /* gone */ }
579
+ }
580
+ /** Resolve an explicit project root to the same canonical form daemons use. */
581
+ function canonicalDaemonRoot(root) {
582
+ return fs.realpathSync(path.resolve(root));
583
+ }
584
+ /** Stable map/set key for aliases of the same canonical root. */
585
+ function canonicalDaemonRootKey(root) {
586
+ const canonical = canonicalDaemonRoot(root);
587
+ return process.platform === 'win32' ? canonical.toLowerCase() : canonical;
588
+ }
589
+ /** Project-scoped, idempotent release contract shared by CLI and MCP. */
590
+ async function releaseDaemonAt(root, deps = {}) {
591
+ return stopDaemonAt(canonicalDaemonRoot(root), deps);
592
+ }
593
+ /**
594
+ * Release the daemon serving `root` through its authenticated socket handshake,
595
+ * wait for confirmed process death, then sweep ownership artifacts. Never sends
596
+ * a signal based only on pidfile/registry liveness. `root` must be realpath'd.
597
+ */
598
+ async function stopDaemonAt(root, deps = {}) {
599
+ let lifecycle = null;
600
+ for (let attempt = 0; attempt < 100 && !lifecycle; attempt++) {
601
+ lifecycle = tryAcquireDaemonLifecycleLock(root);
602
+ if (!lifecycle)
603
+ await sleep(10);
211
604
  }
212
- catch { /* raced to exit */ }
213
- let outcome = 'term';
214
- if (!(await waitForDeath(pid, 3000))) {
605
+ if (!lifecycle) {
606
+ return { root, pid: null, outcome: 'unreachable', failure: 'Project lifecycle arbitration is busy.' };
607
+ }
608
+ let expected = null;
609
+ let lease = null;
610
+ try {
611
+ deps.afterLifecycleAcquired?.();
215
612
  try {
216
- process.kill(pid, 'SIGKILL');
613
+ const info = (0, daemon_paths_1.decodeLockInfo)(fs.readFileSync((0, daemon_paths_1.getDaemonPidPath)(root), 'utf8'));
614
+ if (info)
615
+ expected = { root, ...info };
616
+ }
617
+ catch { /* no lockfile */ }
618
+ if (!expected) {
619
+ const rec = listDaemons({ prune: false }).find((r) => path.resolve(r.root) === path.resolve(root));
620
+ if (rec)
621
+ expected = { ...rec, root };
217
622
  }
218
- catch { /* raced to exit */ }
219
- await waitForDeath(pid, 2000);
220
- outcome = 'kill';
623
+ if (!expected)
624
+ return { root, pid: null, outcome: 'no-daemon' };
625
+ lease = tryAcquireDaemonReleaseLease(root, expected, { linkSync: deps.leaseLinkSync });
626
+ if (!lease)
627
+ return { root, pid: expected.pid, outcome: 'unreachable', failure: 'A project release is already in progress.' };
628
+ deps.afterReleaseLeasePublished?.(expected);
221
629
  }
222
- cleanupDaemonArtifacts(root);
223
- return { root, pid, outcome };
630
+ finally {
631
+ releaseDaemonLifecycleLock(root, lifecycle);
632
+ }
633
+ const alive = deps.isAlive ?? isProcessAlive;
634
+ const heartbeat = setInterval(() => refreshDaemonReleaseLease(root, lease), DAEMON_RELEASE_HEARTBEAT_MS);
635
+ heartbeat.unref?.();
636
+ try {
637
+ if (!alive(expected.pid)) {
638
+ cleanupDaemonArtifacts(root, expected, deps.beforeArtifactDelete);
639
+ return { root, pid: expected.pid, outcome: 'not-running' };
640
+ }
641
+ const control = await (deps.requestControl ?? requestDaemonRelease)(expected);
642
+ if (control !== 'releasing') {
643
+ return {
644
+ root,
645
+ pid: expected.pid,
646
+ outcome: control,
647
+ failure: control === 'identity-mismatch'
648
+ ? 'The reachable process did not prove ownership of this canonical root and daemon generation.'
649
+ : 'The daemon control socket could not be reached; no process was signalled.',
650
+ };
651
+ }
652
+ const died = await (deps.waitForDeath ?? waitForDeath)(expected.pid, 5000);
653
+ if (!died) {
654
+ return {
655
+ root,
656
+ pid: expected.pid,
657
+ outcome: 'termination-failed',
658
+ failure: 'The daemon accepted release but did not terminate before the timeout; ownership artifacts were preserved.',
659
+ };
660
+ }
661
+ cleanupDaemonArtifacts(root, expected, deps.beforeArtifactDelete);
662
+ return { root, pid: expected.pid, outcome: 'released' };
663
+ }
664
+ finally {
665
+ clearInterval(heartbeat);
666
+ releaseDaemonReleaseLease(root, lease);
667
+ }
668
+ }
669
+ async function requestDaemonRelease(expected) {
670
+ const candidates = [expected.socketPath, ...(0, daemon_paths_1.getDaemonSocketCandidates)(expected.root)]
671
+ .filter((value, index, all) => Boolean(value) && all.indexOf(value) === index);
672
+ for (const socketPath of candidates) {
673
+ const result = await requestReleaseAtSocket(socketPath, expected);
674
+ if (result !== 'unreachable')
675
+ return result;
676
+ }
677
+ return 'unreachable';
678
+ }
679
+ function requestReleaseAtSocket(socketPath, expected) {
680
+ return new Promise((resolve) => {
681
+ const socket = net.createConnection(socketPath);
682
+ socket.setEncoding('utf8');
683
+ let buffer = '';
684
+ let phase = 'hello';
685
+ let settled = false;
686
+ const finish = (result) => {
687
+ if (settled)
688
+ return;
689
+ settled = true;
690
+ clearTimeout(timer);
691
+ socket.destroy();
692
+ resolve(result);
693
+ };
694
+ const timer = setTimeout(() => finish('unreachable'), 1500);
695
+ timer.unref?.();
696
+ socket.on('error', () => finish('unreachable'));
697
+ socket.on('close', () => finish('unreachable'));
698
+ socket.on('data', (chunk) => {
699
+ buffer += chunk;
700
+ let newline;
701
+ while ((newline = buffer.indexOf('\n')) !== -1) {
702
+ const line = buffer.slice(0, newline);
703
+ buffer = buffer.slice(newline + 1);
704
+ let value;
705
+ try {
706
+ value = JSON.parse(line);
707
+ }
708
+ catch {
709
+ finish('identity-mismatch');
710
+ return;
711
+ }
712
+ if (phase === 'hello') {
713
+ const verified = value.protocol === 1 && value.pid === expected.pid &&
714
+ value.codegraph === expected.version && value.root === expected.root &&
715
+ value.startedAt === expected.startedAt && value.socketPath === expected.socketPath;
716
+ if (!verified) {
717
+ finish('identity-mismatch');
718
+ return;
719
+ }
720
+ phase = 'response';
721
+ socket.write(JSON.stringify({
722
+ codegraph_control: 1,
723
+ action: 'release',
724
+ root: expected.root,
725
+ pid: expected.pid,
726
+ startedAt: expected.startedAt,
727
+ }) + '\n');
728
+ }
729
+ else {
730
+ finish(value.codegraph_control === 1 && value.outcome === 'releasing' ? 'releasing' : 'identity-mismatch');
731
+ return;
732
+ }
733
+ }
734
+ });
735
+ });
224
736
  }
225
737
  /** Stop every registered, live daemon. */
226
738
  async function stopAllDaemons() {