gitnexus 1.6.6-rc.33 → 1.6.6-rc.35
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.
|
@@ -73,6 +73,12 @@ interface RepoHandle {
|
|
|
73
73
|
* and cannot be changed mid-process.
|
|
74
74
|
*/
|
|
75
75
|
export declare function resolveWorktreeCwd(repoPath: string, launchCwd: string): string;
|
|
76
|
+
/**
|
|
77
|
+
* Length of the base64url path hash appended to a colliding repo id.
|
|
78
|
+
* Exported so tests can pin the suffix shape without re-deriving the
|
|
79
|
+
* literal; see `repoId()` and the hashed-id resolution tier (#1658).
|
|
80
|
+
*/
|
|
81
|
+
export declare const REPO_ID_HASH_LENGTH = 6;
|
|
76
82
|
export declare class LocalBackend {
|
|
77
83
|
private repos;
|
|
78
84
|
private contextCache;
|
|
@@ -128,8 +134,20 @@ export declare class LocalBackend {
|
|
|
128
134
|
resolveRepo(repoParam?: string): Promise<RepoHandle>;
|
|
129
135
|
/**
|
|
130
136
|
* Try to resolve a repo from the in-memory cache. Returns null on miss.
|
|
137
|
+
* Throws {@link RegistryAmbiguousTargetError} when `repoParam` matches
|
|
138
|
+
* multiple handles by name and cwd cannot disambiguate (#1658).
|
|
131
139
|
*/
|
|
132
140
|
private resolveRepoFromCache;
|
|
141
|
+
/**
|
|
142
|
+
* Prefer the indexed repo whose path matches the git root of process.cwd().
|
|
143
|
+
*
|
|
144
|
+
* In MCP stdio server mode, `process.cwd()` is the server's launch directory,
|
|
145
|
+
* not the agent client's cwd. If the server was started from an unrelated
|
|
146
|
+
* directory, `getGitRoot` returns null and duplicate-name resolution throws
|
|
147
|
+
* {@link RegistryAmbiguousTargetError} — callers should pass an absolute path.
|
|
148
|
+
*/
|
|
149
|
+
private pickRepoHandleForCwd;
|
|
150
|
+
private handleToRegistryEntry;
|
|
133
151
|
private ensureInitialized;
|
|
134
152
|
/**
|
|
135
153
|
* Get context for a specific repo (or the single repo if only one).
|
|
@@ -16,7 +16,7 @@ import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../../core/lbug/l
|
|
|
16
16
|
// import { isGitRepo, getCurrentCommit, getGitRoot } from '../../storage/git.js';
|
|
17
17
|
import { parseDiffHunks, getCanonicalRepoRoot, getGitRoot, } from '../../storage/git.js';
|
|
18
18
|
import { realpathSync } from 'fs';
|
|
19
|
-
import { listRegisteredRepos, cleanupOldKuzuFiles, } from '../../storage/repo-manager.js';
|
|
19
|
+
import { listRegisteredRepos, cleanupOldKuzuFiles, canonicalizePath, RegistryAmbiguousTargetError, } from '../../storage/repo-manager.js';
|
|
20
20
|
import { GroupService } from '../../core/group/service.js';
|
|
21
21
|
import { resolveAtGroupMemberRepoPath } from '../../core/group/resolve-at-member.js';
|
|
22
22
|
import { collectBestChunks } from '../../core/embeddings/types.js';
|
|
@@ -233,6 +233,12 @@ export function resolveWorktreeCwd(repoPath, launchCwd) {
|
|
|
233
233
|
}
|
|
234
234
|
return repoPath;
|
|
235
235
|
}
|
|
236
|
+
/**
|
|
237
|
+
* Length of the base64url path hash appended to a colliding repo id.
|
|
238
|
+
* Exported so tests can pin the suffix shape without re-deriving the
|
|
239
|
+
* literal; see `repoId()` and the hashed-id resolution tier (#1658).
|
|
240
|
+
*/
|
|
241
|
+
export const REPO_ID_HASH_LENGTH = 6;
|
|
236
242
|
export class LocalBackend {
|
|
237
243
|
repos = new Map();
|
|
238
244
|
contextCache = new Map();
|
|
@@ -345,7 +351,13 @@ export class LocalBackend {
|
|
|
345
351
|
for (const [id, handle] of this.repos) {
|
|
346
352
|
if (id === base && handle.repoPath !== path.resolve(repoPath)) {
|
|
347
353
|
// Collision — use path hash
|
|
348
|
-
|
|
354
|
+
// Lowercase the hash so it survives the `paramLower` lookup in
|
|
355
|
+
// resolveRepoFromCache — base64url retains mixed case, but the id
|
|
356
|
+
// tier compares against `repoParam.toLowerCase()` (#1658 follow-up).
|
|
357
|
+
const hash = Buffer.from(repoPath)
|
|
358
|
+
.toString('base64url')
|
|
359
|
+
.slice(0, REPO_ID_HASH_LENGTH)
|
|
360
|
+
.toLowerCase();
|
|
349
361
|
return `${base}-${hash}`;
|
|
350
362
|
}
|
|
351
363
|
}
|
|
@@ -362,7 +374,20 @@ export class LocalBackend {
|
|
|
362
374
|
* while the MCP server was running.
|
|
363
375
|
*/
|
|
364
376
|
async resolveRepo(repoParam) {
|
|
365
|
-
|
|
377
|
+
let refreshedAfterAmbiguity = false;
|
|
378
|
+
let result;
|
|
379
|
+
try {
|
|
380
|
+
result = this.resolveRepoFromCache(repoParam);
|
|
381
|
+
}
|
|
382
|
+
catch (err) {
|
|
383
|
+
if (!(err instanceof RegistryAmbiguousTargetError))
|
|
384
|
+
throw err;
|
|
385
|
+
// Stale in-memory duplicate siblings can linger after unregister; refresh
|
|
386
|
+
// once before re-throwing so a resolved registry can disambiguate (#1658).
|
|
387
|
+
await this.refreshRepos();
|
|
388
|
+
refreshedAfterAmbiguity = true;
|
|
389
|
+
result = this.resolveRepoFromCache(repoParam);
|
|
390
|
+
}
|
|
366
391
|
if (result) {
|
|
367
392
|
// Issue: silent graph drift across sibling clones.
|
|
368
393
|
// If the caller's cwd lives in a *different* on-disk clone of
|
|
@@ -375,8 +400,10 @@ export class LocalBackend {
|
|
|
375
400
|
});
|
|
376
401
|
return result;
|
|
377
402
|
}
|
|
378
|
-
// Miss — refresh registry and try once more
|
|
379
|
-
|
|
403
|
+
// Miss — refresh registry and try once more (skip if already refreshed above)
|
|
404
|
+
if (!refreshedAfterAmbiguity) {
|
|
405
|
+
await this.refreshRepos();
|
|
406
|
+
}
|
|
380
407
|
const retried = this.resolveRepoFromCache(repoParam);
|
|
381
408
|
if (retried) {
|
|
382
409
|
this.maybeWarnSiblingDrift(retried).catch(() => { });
|
|
@@ -403,31 +430,57 @@ export class LocalBackend {
|
|
|
403
430
|
}
|
|
404
431
|
/**
|
|
405
432
|
* Try to resolve a repo from the in-memory cache. Returns null on miss.
|
|
433
|
+
* Throws {@link RegistryAmbiguousTargetError} when `repoParam` matches
|
|
434
|
+
* multiple handles by name and cwd cannot disambiguate (#1658).
|
|
406
435
|
*/
|
|
407
436
|
resolveRepoFromCache(repoParam) {
|
|
408
437
|
if (this.repos.size === 0)
|
|
409
438
|
return null;
|
|
410
439
|
if (repoParam) {
|
|
411
440
|
const paramLower = repoParam.toLowerCase();
|
|
412
|
-
|
|
441
|
+
const looksLikePath = path.isAbsolute(repoParam) || repoParam.includes(path.sep) || repoParam.includes('/');
|
|
442
|
+
const resolvePathMatch = () => {
|
|
443
|
+
const canonicalTarget = canonicalizePath(repoParam);
|
|
444
|
+
return [...this.repos.values()].find((handle) => {
|
|
445
|
+
const stored = canonicalizePath(handle.repoPath);
|
|
446
|
+
return process.platform === 'win32'
|
|
447
|
+
? stored.toLowerCase() === canonicalTarget.toLowerCase()
|
|
448
|
+
: stored === canonicalTarget;
|
|
449
|
+
});
|
|
450
|
+
};
|
|
451
|
+
// Path-like params first (absolute or contains separators) — aligns with
|
|
452
|
+
// resolveRegistryEntry (#829). Bare aliases such as ".tmp-repro-mini" must
|
|
453
|
+
// not be resolved via path.resolve(cwd) before duplicate-name handling.
|
|
454
|
+
if (looksLikePath) {
|
|
455
|
+
const pathMatch = resolvePathMatch();
|
|
456
|
+
if (pathMatch)
|
|
457
|
+
return pathMatch;
|
|
458
|
+
}
|
|
459
|
+
// Exact name before id — the first duplicate sibling keeps id === name
|
|
460
|
+
// (e.g. id "shared"), so a name lookup must not be captured by the id tier.
|
|
461
|
+
const nameMatches = [...this.repos.values()].filter((handle) => handle.name.toLowerCase() === paramLower);
|
|
462
|
+
if (nameMatches.length === 1)
|
|
463
|
+
return nameMatches[0];
|
|
464
|
+
if (nameMatches.length > 1) {
|
|
465
|
+
const cwdPick = this.pickRepoHandleForCwd(nameMatches);
|
|
466
|
+
if (cwdPick)
|
|
467
|
+
return cwdPick;
|
|
468
|
+
throw new RegistryAmbiguousTargetError(repoParam, nameMatches.map((h) => this.handleToRegistryEntry(h)));
|
|
469
|
+
}
|
|
470
|
+
// Stable hashed id (e.g. "shared-abc123") from repoId() collision suffix
|
|
413
471
|
if (this.repos.has(paramLower))
|
|
414
472
|
return this.repos.get(paramLower);
|
|
415
|
-
//
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
// Match by partial name
|
|
427
|
-
for (const handle of this.repos.values()) {
|
|
428
|
-
if (handle.name.toLowerCase().includes(paramLower))
|
|
429
|
-
return handle;
|
|
430
|
-
}
|
|
473
|
+
// Bare name resolved as a cwd-relative path (e.g. "myrepo" against process.cwd()),
|
|
474
|
+
// after name/id tiers. Path-like strings with separators were handled at the top.
|
|
475
|
+
if (!looksLikePath) {
|
|
476
|
+
const pathMatch = resolvePathMatch();
|
|
477
|
+
if (pathMatch)
|
|
478
|
+
return pathMatch;
|
|
479
|
+
}
|
|
480
|
+
// Partial name — only when unambiguous
|
|
481
|
+
const partialMatches = [...this.repos.values()].filter((handle) => handle.name.toLowerCase().includes(paramLower));
|
|
482
|
+
if (partialMatches.length === 1)
|
|
483
|
+
return partialMatches[0];
|
|
431
484
|
return null;
|
|
432
485
|
}
|
|
433
486
|
if (this.repos.size === 1) {
|
|
@@ -435,6 +488,38 @@ export class LocalBackend {
|
|
|
435
488
|
}
|
|
436
489
|
return null; // Multiple repos, no param — ambiguous
|
|
437
490
|
}
|
|
491
|
+
/**
|
|
492
|
+
* Prefer the indexed repo whose path matches the git root of process.cwd().
|
|
493
|
+
*
|
|
494
|
+
* In MCP stdio server mode, `process.cwd()` is the server's launch directory,
|
|
495
|
+
* not the agent client's cwd. If the server was started from an unrelated
|
|
496
|
+
* directory, `getGitRoot` returns null and duplicate-name resolution throws
|
|
497
|
+
* {@link RegistryAmbiguousTargetError} — callers should pass an absolute path.
|
|
498
|
+
*/
|
|
499
|
+
pickRepoHandleForCwd(candidates) {
|
|
500
|
+
const cwdRoot = getGitRoot(process.cwd());
|
|
501
|
+
if (!cwdRoot)
|
|
502
|
+
return null;
|
|
503
|
+
const canonicalCwd = canonicalizePath(cwdRoot);
|
|
504
|
+
const cwdMatches = candidates.filter((handle) => {
|
|
505
|
+
const stored = canonicalizePath(handle.repoPath);
|
|
506
|
+
return process.platform === 'win32'
|
|
507
|
+
? stored.toLowerCase() === canonicalCwd.toLowerCase()
|
|
508
|
+
: stored === canonicalCwd;
|
|
509
|
+
});
|
|
510
|
+
return cwdMatches.length === 1 ? cwdMatches[0] : null;
|
|
511
|
+
}
|
|
512
|
+
handleToRegistryEntry(handle) {
|
|
513
|
+
return {
|
|
514
|
+
name: handle.name,
|
|
515
|
+
path: handle.repoPath,
|
|
516
|
+
storagePath: handle.storagePath,
|
|
517
|
+
indexedAt: handle.indexedAt,
|
|
518
|
+
lastCommit: handle.lastCommit,
|
|
519
|
+
stats: handle.stats,
|
|
520
|
+
remoteUrl: handle.remoteUrl,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
438
523
|
// ─── Lazy LadybugDB Init ────────────────────────────────────────────
|
|
439
524
|
async ensureInitialized(repoId) {
|
|
440
525
|
// If a reinit is already in progress for this repo, wait for it
|
package/package.json
CHANGED