@mnemonik/shared 7.140.1 → 7.140.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.
Files changed (58) hide show
  1. package/dist/codeScanner.d.ts +46 -1
  2. package/dist/codeScanner.d.ts.map +1 -1
  3. package/dist/codeScanner.js +100 -12
  4. package/dist/codeScanner.js.map +1 -1
  5. package/dist/contextBudget.d.ts +2 -2
  6. package/dist/contextBudget.d.ts.map +1 -1
  7. package/dist/contextBudget.js +3 -4
  8. package/dist/contextBudget.js.map +1 -1
  9. package/dist/gitEditDetector.d.ts +11 -6
  10. package/dist/gitEditDetector.d.ts.map +1 -1
  11. package/dist/gitEditDetector.js +119 -9
  12. package/dist/gitEditDetector.js.map +1 -1
  13. package/dist/hookRuntime.d.ts +18 -8
  14. package/dist/hookRuntime.d.ts.map +1 -1
  15. package/dist/hookRuntime.js +31 -20
  16. package/dist/hookRuntime.js.map +1 -1
  17. package/dist/hookTrace.d.ts +149 -0
  18. package/dist/hookTrace.d.ts.map +1 -0
  19. package/dist/hookTrace.js +483 -0
  20. package/dist/hookTrace.js.map +1 -0
  21. package/dist/index.d.ts +1 -1
  22. package/dist/index.d.ts.map +1 -1
  23. package/dist/index.js +1 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/instructions.d.ts +1 -1
  26. package/dist/instructions.d.ts.map +1 -1
  27. package/dist/instructions.js +1 -1
  28. package/dist/projectIdentityFile.d.ts +13 -0
  29. package/dist/projectIdentityFile.d.ts.map +1 -1
  30. package/dist/projectIdentityFile.js +31 -18
  31. package/dist/projectIdentityFile.js.map +1 -1
  32. package/dist/protectedPaths.d.ts +6 -0
  33. package/dist/protectedPaths.d.ts.map +1 -1
  34. package/dist/protectedPaths.js +15 -6
  35. package/dist/protectedPaths.js.map +1 -1
  36. package/dist/repositoryRoot.d.ts +20 -8
  37. package/dist/repositoryRoot.d.ts.map +1 -1
  38. package/dist/repositoryRoot.js +167 -79
  39. package/dist/repositoryRoot.js.map +1 -1
  40. package/dist/runtimeReader.d.ts +1 -1
  41. package/dist/runtimeReader.d.ts.map +1 -1
  42. package/dist/runtimeReader.js.map +1 -1
  43. package/package.json +2 -2
  44. package/src/codeScanner.ts +115 -9
  45. package/src/contextBudget.ts +3 -4
  46. package/src/gitEditDetector.ts +113 -8
  47. package/src/hookRuntime.ts +39 -27
  48. package/src/hookTrace.ts +588 -0
  49. package/src/index.ts +4 -0
  50. package/src/instructions.ts +1 -1
  51. package/src/projectIdentityFile.ts +34 -20
  52. package/src/protectedPaths.ts +18 -7
  53. package/src/repositoryRoot.ts +188 -82
  54. package/src/runtimeReader.ts +6 -1
  55. package/test-fixtures/identity/runner.d.mts +1 -1
  56. package/test-fixtures/identity/runner.mjs +299 -25
  57. package/tests/hookTrace.test.ts +350 -0
  58. package/tests/nearestProjectIdentity.test.ts +253 -0
@@ -1,6 +1,6 @@
1
1
  import assert from 'node:assert/strict';
2
2
  import { execFile } from 'node:child_process';
3
- import { mkdtemp, mkdir, readFile, rm, symlink, writeFile } from 'node:fs/promises';
3
+ import { mkdtemp, mkdir, readFile, realpath, rm, symlink, writeFile } from 'node:fs/promises';
4
4
  import { tmpdir } from 'node:os';
5
5
  import { join } from 'node:path';
6
6
  import { promisify } from 'node:util';
@@ -13,6 +13,11 @@ async function git(cwd, ...args) {
13
13
  await exec('git', args, { cwd, env: { ...process.env, LC_ALL: 'C' } });
14
14
  }
15
15
 
16
+ async function commitAll(path, message = 'fixture') {
17
+ await git(path, 'add', '-A');
18
+ await git(path, 'commit', '-q', '-m', message);
19
+ }
20
+
16
21
  async function initRepo(path) {
17
22
  await mkdir(path, { recursive: true });
18
23
  await git(path, 'init', '--initial-branch=main');
@@ -33,7 +38,8 @@ async function identity(path, projectId, projectName = 'fixture') {
33
38
  }
34
39
 
35
40
  async function fixture(run) {
36
- const path = await mkdtemp(join(tmpdir(), 'mnemonik-identity-'));
41
+ // Real path: resolvers compare with the real paths git prints (macOS /var -> /private/var).
42
+ const path = await realpath(await mkdtemp(join(tmpdir(), 'mnemonik-identity-')));
37
43
  try {
38
44
  await run(path);
39
45
  } finally {
@@ -69,6 +75,10 @@ export function runIdentityFixtureSuite(register, adapter) {
69
75
  algorithmVersion: 1,
70
76
  hash: 'fixture-hash',
71
77
  });
78
+ // The project folder is the main checkout, so its repository is reported.
79
+ assert.equal(result.repository.kind, 'git');
80
+ assert.equal(result.repository.root, main);
81
+ assert.equal(result.repository.isLinkedWorktree, false);
72
82
  })
73
83
  );
74
84
 
@@ -136,24 +146,274 @@ export function runIdentityFixtureSuite(register, adapter) {
136
146
  })
137
147
  );
138
148
 
139
- register('nested repository inherits only with allowNestedInherit', () =>
149
+ register('a nested clone without its own identity file belongs to the project around it', () =>
140
150
  fixture(async (base) => {
141
151
  const main = join(base, 'main');
142
152
  const nested = join(main, 'packages', 'nested');
143
153
  await initRepo(main);
144
154
  await identity(main, ROOT_ID, 'main');
145
155
  await initRepo(nested);
156
+ await mkdir(join(nested, 'lib'));
157
+
158
+ for (const cwd of [nested, join(nested, 'lib')]) {
159
+ const resolved = await adapter.resolve(cwd);
160
+ assert.equal(resolved.kind, 'ok');
161
+ assert.equal(resolved.root, main);
162
+ assert.equal(resolved.identity.projectId, ROOT_ID);
163
+ if (adapter.find) assert.equal((await adapter.find(cwd)).projectId, ROOT_ID);
164
+ }
165
+ })
166
+ );
167
+
168
+ register('a subfolder of a repository belongs to the repository project', () =>
169
+ fixture(async (base) => {
170
+ const main = join(base, 'main');
171
+ const deep = join(main, 'src', 'deep');
172
+ await initRepo(main);
173
+ await identity(main, ROOT_ID, 'main');
174
+ await mkdir(deep, { recursive: true });
175
+
176
+ const resolved = await adapter.resolve(deep);
177
+ assert.equal(resolved.kind, 'ok');
178
+ assert.equal(resolved.root, main);
179
+ assert.equal(resolved.identity.projectId, ROOT_ID);
180
+ assert.equal(resolved.repository.kind, 'git');
181
+ if (adapter.find) assert.equal((await adapter.find(deep)).projectId, ROOT_ID);
182
+ })
183
+ );
184
+
185
+ register('a plain subfolder with its own identity file is its own project', () =>
186
+ fixture(async (base) => {
187
+ const main = join(base, 'main');
188
+ const sub = join(main, 'home-agent');
189
+ await initRepo(main);
190
+ await identity(main, ROOT_ID, 'main');
191
+ await mkdir(join(sub, 'src'), { recursive: true });
192
+ await identity(sub, NESTED_ID, 'home-agent');
193
+
194
+ const resolved = await adapter.resolve(join(sub, 'src'));
195
+ assert.equal(resolved.kind, 'ok');
196
+ assert.equal(resolved.root, sub);
197
+ assert.equal(resolved.identity.projectId, NESTED_ID);
198
+ // The project folder is not the repository's top, so it is a plain folder.
199
+ assert.equal(resolved.repository.kind, 'plain');
200
+ if (adapter.find) assert.equal((await adapter.find(sub)).projectId, NESTED_ID);
201
+ const parent = await adapter.resolve(main);
202
+ assert.equal(parent.root, main);
203
+ assert.equal(parent.identity.projectId, ROOT_ID);
204
+ })
205
+ );
206
+
207
+ register('a worktree kept inside its repository belongs to the main checkout', () =>
208
+ fixture(async (base) => {
209
+ const main = join(base, 'main');
210
+ const worktree = join(main, '.claude', 'worktrees', 'feature');
211
+ await initRepo(main);
212
+ await identity(main, ROOT_ID, 'main');
213
+ await commitAll(main);
214
+ await git(main, 'worktree', 'add', '-q', '-b', 'fixture-inside', worktree);
215
+ // The committed identity file is checked out into the worktree's top;
216
+ // a different copy there still does not override the main checkout.
217
+ await identity(worktree, NESTED_ID, 'wrong-worktree-copy');
218
+ await mkdir(join(worktree, 'src'));
219
+
220
+ for (const cwd of [worktree, join(worktree, 'src')]) {
221
+ const resolved = await adapter.resolve(cwd);
222
+ assert.equal(resolved.kind, 'ok');
223
+ assert.equal(resolved.root, main);
224
+ assert.equal(resolved.identity.projectId, ROOT_ID);
225
+ if (adapter.find) assert.equal((await adapter.find(cwd)).projectId, ROOT_ID);
226
+ }
227
+ })
228
+ );
229
+
230
+ register('a project folder inside a worktree is its own project', () =>
231
+ fixture(async (base) => {
232
+ const main = join(base, 'main');
233
+ const worktree = join(base, 'elsewhere', 'feature');
234
+ await initRepo(main);
235
+ await identity(main, ROOT_ID, 'main');
236
+ await commitAll(main);
237
+ await git(main, 'worktree', 'add', '-q', '-b', 'fixture-project-inside', worktree);
238
+ const project = join(worktree, 'tools', 'agent');
239
+ await mkdir(join(project, 'src'), { recursive: true });
240
+ await identity(project, NESTED_ID, 'agent');
241
+
242
+ const resolved = await adapter.resolve(join(project, 'src'));
243
+ assert.equal(resolved.kind, 'ok');
244
+ assert.equal(resolved.root, project);
245
+ assert.equal(resolved.identity.projectId, NESTED_ID);
246
+ assert.equal(resolved.repository.kind, 'plain');
247
+ const beside = await adapter.resolve(join(worktree, 'tools'));
248
+ assert.equal(beside.root, main);
249
+ assert.equal(beside.identity.projectId, ROOT_ID);
250
+ })
251
+ );
146
252
 
147
- const unresolved = await adapter.resolve(nested);
148
- assert.equal(unresolved.kind, 'nested');
149
- assert.equal(unresolved.parentIdentity?.projectId, ROOT_ID);
150
- assert.ok(unresolved.nested.some((entry) => entry.path === nested));
151
- if (adapter.find) assert.equal(await adapter.find(nested), null);
253
+ register('a submodule is its own project only with its own identity file', () =>
254
+ fixture(async (base) => {
255
+ const upstream = join(base, 'upstream');
256
+ const main = join(base, 'main');
257
+ await initRepo(upstream);
258
+ await writeFile(join(upstream, 'README'), 'upstream');
259
+ await commitAll(upstream);
260
+ await initRepo(main);
261
+ await identity(main, ROOT_ID, 'main');
262
+ // The layout `git submodule add` makes (its git directory kept under the
263
+ // parent's .git/modules), without the submodule shell script.
264
+ const sub = join(main, 'sub');
265
+ await mkdir(join(main, '.git', 'modules'));
266
+ await git(
267
+ main,
268
+ 'clone',
269
+ '-q',
270
+ `--separate-git-dir=${join(main, '.git', 'modules', 'sub')}`,
271
+ upstream,
272
+ sub
273
+ );
274
+
275
+ const repository = await adapter.resolveRoot(sub);
276
+ assert.equal(repository.kind, 'git');
277
+ assert.equal(repository.root, sub);
278
+ assert.equal(repository.isLinkedWorktree, false);
279
+ assert.equal(repository.commonDir, join(main, '.git', 'modules', 'sub'));
152
280
 
153
- const inherited = await adapter.resolve(nested, { allowNestedInherit: true });
281
+ const inherited = await adapter.resolve(sub);
154
282
  assert.equal(inherited.kind, 'ok');
155
283
  assert.equal(inherited.root, main);
156
284
  assert.equal(inherited.identity.projectId, ROOT_ID);
285
+
286
+ await identity(sub, NESTED_ID, 'sub');
287
+ const own = await adapter.resolve(sub);
288
+ assert.equal(own.kind, 'ok');
289
+ assert.equal(own.root, sub);
290
+ assert.equal(own.identity.projectId, NESTED_ID);
291
+ assert.equal(own.repository.kind, 'git');
292
+ if (adapter.find) assert.equal((await adapter.find(sub)).projectId, NESTED_ID);
293
+ })
294
+ );
295
+
296
+ register(
297
+ 'a nested clone or submodule inside a linked worktree belongs to the main checkout',
298
+ () =>
299
+ fixture(async (base) => {
300
+ const upstream = join(base, 'upstream');
301
+ const main = join(base, 'main');
302
+ const worktree = join(base, 'elsewhere', 'feature');
303
+ await initRepo(upstream);
304
+ await writeFile(join(upstream, 'README'), 'upstream');
305
+ await commitAll(upstream);
306
+ await initRepo(main);
307
+ await identity(main, ROOT_ID, 'main');
308
+ await commitAll(main);
309
+ await git(main, 'worktree', 'add', '-q', '-b', 'fixture-nested-in-worktree', worktree);
310
+ await identity(worktree, NESTED_ID, 'wrong-worktree-copy');
311
+ const clone = join(worktree, 'libs', 'clone');
312
+ await mkdir(join(clone, 'src'), { recursive: true });
313
+ await git(clone, 'init', '-q');
314
+ const sub = join(worktree, 'sub');
315
+ await mkdir(join(main, '.git', 'worktrees', 'feature', 'modules'), { recursive: true });
316
+ await git(
317
+ worktree,
318
+ 'clone',
319
+ '-q',
320
+ `--separate-git-dir=${join(main, '.git', 'worktrees', 'feature', 'modules', 'sub')}`,
321
+ upstream,
322
+ sub
323
+ );
324
+
325
+ for (const cwd of [join(clone, 'src'), sub]) {
326
+ const resolved = await adapter.resolve(cwd);
327
+ assert.equal(resolved.kind, 'ok', cwd);
328
+ assert.equal(resolved.root, main, cwd);
329
+ assert.equal(resolved.identity.projectId, ROOT_ID, cwd);
330
+ if (adapter.find) assert.equal((await adapter.find(cwd)).projectId, ROOT_ID);
331
+ }
332
+ })
333
+ );
334
+
335
+ register('worktrees of a bare repository belong to the folder holding it', () =>
336
+ fixture(async (base) => {
337
+ const seed = join(base, 'seed');
338
+ const project = join(base, 'bareproj');
339
+ const bare = join(project, '.bare');
340
+ const worktree = join(project, 'main');
341
+ await initRepo(seed);
342
+ await writeFile(join(seed, 'README'), 'seed');
343
+ await commitAll(seed);
344
+ await mkdir(project);
345
+ await git(project, 'clone', '-q', '--bare', seed, bare);
346
+ await writeFile(join(project, '.git'), 'gitdir: ./.bare\n');
347
+ await git(project, 'worktree', 'add', '-q', worktree, 'main');
348
+ await mkdir(join(worktree, 'src'));
349
+
350
+ // No identity file yet: a new project would be made in the folder
351
+ // holding the bare repository, never inside the git directory.
352
+ const absent = await adapter.resolve(join(worktree, 'src'));
353
+ assert.equal(absent.kind, 'absent');
354
+ assert.equal(absent.root, project);
355
+
356
+ await identity(project, ROOT_ID, 'bareproj');
357
+ const resolved = await adapter.resolve(join(worktree, 'src'));
358
+ assert.equal(resolved.kind, 'ok');
359
+ assert.equal(resolved.root, project);
360
+ assert.equal(resolved.identity.projectId, ROOT_ID);
361
+ if (adapter.find) assert.equal((await adapter.find(worktree)).projectId, ROOT_ID);
362
+ })
363
+ );
364
+
365
+ register('a repository with a separate git directory is not a worktree', () =>
366
+ fixture(async (base) => {
367
+ const main = join(base, 'main');
368
+ const separate = join(main, 'separate');
369
+ await initRepo(main);
370
+ await identity(main, ROOT_ID, 'main');
371
+ await mkdir(separate);
372
+ await mkdir(join(base, 'gitdirs'));
373
+ await git(separate, 'init', '-q', `--separate-git-dir=${join(base, 'gitdirs', 'separate')}`);
374
+ await identity(separate, NESTED_ID, 'separate');
375
+
376
+ const repository = await adapter.resolveRoot(separate);
377
+ assert.equal(repository.kind, 'git');
378
+ assert.equal(repository.root, separate);
379
+ assert.equal(repository.isLinkedWorktree, false);
380
+ const resolved = await adapter.resolve(separate);
381
+ assert.equal(resolved.kind, 'ok');
382
+ assert.equal(resolved.root, separate);
383
+ assert.equal(resolved.identity.projectId, NESTED_ID);
384
+ })
385
+ );
386
+
387
+ register('a symlinked path resolves as the real folder it points to', () =>
388
+ fixture(async (base) => {
389
+ const main = join(base, 'main');
390
+ const worktree = join(base, 'elsewhere', 'feature');
391
+ const links = join(base, 'links');
392
+ await initRepo(main);
393
+ await identity(main, ROOT_ID, 'main');
394
+ await mkdir(join(main, 'src', 'deep'), { recursive: true });
395
+ await writeFile(join(main, 'src', 'deep', 'file'), 'x');
396
+ await commitAll(main);
397
+ await git(main, 'worktree', 'add', '-q', '-b', 'fixture-symlink', worktree);
398
+ await identity(worktree, NESTED_ID, 'wrong-worktree-copy');
399
+ await mkdir(links);
400
+ await symlink(main, join(links, 'root'));
401
+ await symlink(join(main, 'src'), join(links, 'sub'));
402
+ await symlink(worktree, join(links, 'worktree'));
403
+
404
+ for (const link of ['root', 'sub', 'worktree']) {
405
+ const resolved = await adapter.resolve(join(links, link));
406
+ assert.equal(resolved.kind, 'ok', link);
407
+ assert.equal(resolved.root, main, link);
408
+ assert.equal(resolved.identity.projectId, ROOT_ID, link);
409
+ if (adapter.find) assert.equal((await adapter.find(join(links, link))).projectId, ROOT_ID);
410
+ }
411
+ const repository = await adapter.resolveRoot(join(links, 'sub', 'deep'));
412
+ assert.equal(repository.root, main);
413
+ assert.equal(repository.isLinkedWorktree, false);
414
+ const linked = await adapter.resolveRoot(join(links, 'worktree'));
415
+ assert.equal(linked.root, worktree);
416
+ assert.equal(linked.isLinkedWorktree, true);
157
417
  })
158
418
  );
159
419
 
@@ -261,21 +521,35 @@ export function runIdentityFixtureSuite(register, adapter) {
261
521
  })
262
522
  );
263
523
 
264
- register('missing git binary is git_unavailable and hooks fail closed', () =>
265
- fixture(async (base) => {
266
- const emptyPath = join(base, 'empty-path');
267
- await mkdir(emptyPath);
268
- const savedPath = process.env.PATH;
269
- process.env.PATH = emptyPath;
270
- try {
271
- const repository = await adapter.resolveRoot(base);
272
- assert.equal(repository.kind, 'git_unavailable');
273
- assert.match(repository.detail, /git|ENOENT/i);
274
- if (adapter.find) assert.equal(await adapter.find(base), null);
275
- } finally {
276
- if (savedPath === undefined) delete process.env.PATH;
277
- else process.env.PATH = savedPath;
278
- }
279
- })
524
+ register(
525
+ 'missing git binary is git_unavailable; the plain walk still finds the identity file',
526
+ () =>
527
+ fixture(async (base) => {
528
+ const emptyPath = join(base, 'empty-path');
529
+ const project = join(base, 'project');
530
+ const sub = join(project, 'src');
531
+ await mkdir(emptyPath);
532
+ await initRepo(project);
533
+ await identity(project, ROOT_ID, 'project');
534
+ await mkdir(sub);
535
+ const bare = join(base, 'bare');
536
+ await mkdir(bare);
537
+ const savedPath = process.env.PATH;
538
+ process.env.PATH = emptyPath;
539
+ try {
540
+ const repository = await adapter.resolveRoot(base);
541
+ assert.equal(repository.kind, 'git_unavailable');
542
+ assert.match(repository.detail, /git|ENOENT/i);
543
+ if (adapter.find) assert.equal(await adapter.find(bare), null);
544
+ const resolved = await adapter.resolve(sub);
545
+ assert.equal(resolved.kind, 'ok');
546
+ assert.equal(resolved.root, project);
547
+ assert.equal(resolved.identity.projectId, ROOT_ID);
548
+ if (adapter.find) assert.equal((await adapter.find(sub)).projectId, ROOT_ID);
549
+ } finally {
550
+ if (savedPath === undefined) delete process.env.PATH;
551
+ else process.env.PATH = savedPath;
552
+ }
553
+ })
280
554
  );
281
555
  }