gitnexus 1.6.3-rc.19 → 1.6.3-rc.20

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.
@@ -13,7 +13,7 @@ import fs from 'fs/promises';
13
13
  import { runPipelineFromRepo } from './ingestion/pipeline.js';
14
14
  import { initLbug, loadGraphToLbug, getLbugStats, executeQuery, executeWithReusedStatement, closeLbug, createFTSIndex, loadCachedEmbeddings, } from './lbug/lbug-adapter.js';
15
15
  import { getStoragePaths, saveMeta, loadMeta, addToGitignore, registerRepo, cleanupOldKuzuFiles, } from '../storage/repo-manager.js';
16
- import { getCurrentCommit, hasGitDir } from '../storage/git.js';
16
+ import { getCurrentCommit, hasGitDir, getInferredRepoName } from '../storage/git.js';
17
17
  import { generateAIContextFiles } from '../cli/ai-context.js';
18
18
  import { EMBEDDING_TABLE_NAME } from './lbug/schema.js';
19
19
  import { STALE_HASH_SENTINEL } from './lbug/schema.js';
@@ -65,7 +65,7 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
65
65
  // Non-git folders have currentCommit = '' — always rebuild since we can't detect changes
66
66
  if (currentCommit !== '') {
67
67
  return {
68
- repoName: path.basename(repoPath),
68
+ repoName: options.registryName ?? getInferredRepoName(repoPath) ?? path.basename(repoPath),
69
69
  repoPath,
70
70
  stats: existingMeta.stats ?? {},
71
71
  alreadyUpToDate: true,
@@ -223,7 +223,11 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
223
223
  // pipeline `force` above. The CLI maps it from
224
224
  // `--allow-duplicate-name` only; `--force` and `--skills` both
225
225
  // trigger pipeline re-run but never bypass the registry guard.
226
- await registerRepo(repoPath, meta, {
226
+ // The returned name is the one actually written to the registry
227
+ // (after applying the precedence chain in registerRepo) — reuse it
228
+ // so AGENTS.md / skill files reference the same name MCP clients
229
+ // will look up (#979).
230
+ const projectName = await registerRepo(repoPath, meta, {
227
231
  name: options.registryName,
228
232
  allowDuplicateName: options.allowDuplicateName,
229
233
  });
@@ -231,7 +235,6 @@ export async function runFullAnalysis(repoPath, options, callbacks) {
231
235
  if (hasGitDir(repoPath)) {
232
236
  await addToGitignore(repoPath);
233
237
  }
234
- const projectName = path.basename(repoPath);
235
238
  // ── Generate AI context files (best-effort) ───────────────────────
236
239
  let aggregatedClusterCount = 0;
237
240
  if (pipelineResult.communityResult?.communities) {
@@ -16,6 +16,31 @@ export declare const getGitRoot: (fromPath: string) => string | null;
16
16
  * @returns `true` when `.git` is present, `false` otherwise.
17
17
  */
18
18
  export declare const hasGitDir: (dirPath: string) => boolean;
19
+ /**
20
+ * Read `remote.origin.url` from a git repository, or `null` if not a
21
+ * git repo, has no `origin` remote, or git is unavailable.
22
+ *
23
+ * Used by the registry-name inference path (#979) to recover a
24
+ * meaningful repo name when `path.basename(repoPath)` is generic
25
+ * (e.g. monorepo subprojects, git worktrees, Gas-Town-style
26
+ * `<rig>/refinery/rig/` layouts).
27
+ */
28
+ export declare const getRemoteOriginUrl: (repoPath: string) => string | null;
29
+ /**
30
+ * Parse a repository name out of a git remote URL. Handles the common
31
+ * SSH (`git@host:owner/repo.git`), HTTPS (`https://host/owner/repo.git`),
32
+ * `git://`, `ssh://`, and `file://` shapes. Returns `null` for empty /
33
+ * unparseable input.
34
+ *
35
+ * The heuristic: strip a trailing `.git` and trailing slashes, then
36
+ * take the segment after the last `/` or `:`.
37
+ */
38
+ export declare const parseRepoNameFromUrl: (url: string | null | undefined) => string | null;
39
+ /**
40
+ * Convenience wrapper: derive a registry-friendly name from the repo's
41
+ * `origin` remote, or `null` when it cannot be inferred.
42
+ */
43
+ export declare const getInferredRepoName: (repoPath: string) => string | null;
19
44
  export interface DiffHunk {
20
45
  startLine: number;
21
46
  endLine: number;
@@ -52,6 +52,58 @@ export const hasGitDir = (dirPath) => {
52
52
  return false;
53
53
  }
54
54
  };
55
+ /**
56
+ * Read `remote.origin.url` from a git repository, or `null` if not a
57
+ * git repo, has no `origin` remote, or git is unavailable.
58
+ *
59
+ * Used by the registry-name inference path (#979) to recover a
60
+ * meaningful repo name when `path.basename(repoPath)` is generic
61
+ * (e.g. monorepo subprojects, git worktrees, Gas-Town-style
62
+ * `<rig>/refinery/rig/` layouts).
63
+ */
64
+ export const getRemoteOriginUrl = (repoPath) => {
65
+ try {
66
+ const url = execSync('git config --get remote.origin.url', {
67
+ cwd: repoPath,
68
+ stdio: ['ignore', 'pipe', 'ignore'],
69
+ })
70
+ .toString()
71
+ .trim();
72
+ return url || null;
73
+ }
74
+ catch {
75
+ return null;
76
+ }
77
+ };
78
+ /**
79
+ * Parse a repository name out of a git remote URL. Handles the common
80
+ * SSH (`git@host:owner/repo.git`), HTTPS (`https://host/owner/repo.git`),
81
+ * `git://`, `ssh://`, and `file://` shapes. Returns `null` for empty /
82
+ * unparseable input.
83
+ *
84
+ * The heuristic: strip a trailing `.git` and trailing slashes, then
85
+ * take the segment after the last `/` or `:`.
86
+ */
87
+ export const parseRepoNameFromUrl = (url) => {
88
+ if (!url)
89
+ return null;
90
+ const trimmed = url.trim();
91
+ if (!trimmed)
92
+ return null;
93
+ // Strip `.git` suffix (case-insensitive) and any trailing slashes.
94
+ const withoutSuffix = trimmed.replace(/\.git\/*$/i, '').replace(/\/+$/, '');
95
+ // Last path segment, splitting on either `/` or `:` (covers SSH form).
96
+ const m = withoutSuffix.match(/[/:]([^/:]+)$/);
97
+ const candidate = m ? m[1] : withoutSuffix;
98
+ return candidate || null;
99
+ };
100
+ /**
101
+ * Convenience wrapper: derive a registry-friendly name from the repo's
102
+ * `origin` remote, or `null` when it cannot be inferred.
103
+ */
104
+ export const getInferredRepoName = (repoPath) => {
105
+ return parseRepoNameFromUrl(getRemoteOriginUrl(repoPath));
106
+ };
55
107
  /**
56
108
  * Parse unified diff output (with -U0) into per-file hunk ranges.
57
109
  * Extracts the new-file line ranges from @@ hunk headers.
@@ -155,10 +155,14 @@ export declare class RegistryNameCollisionError extends Error {
155
155
  * Register (add or update) a repo in the global registry.
156
156
  * Called after `gitnexus analyze` completes.
157
157
  *
158
- * Name resolution precedence (#829):
158
+ * Name resolution precedence (#829, #979):
159
159
  * 1. explicit `opts.name` (from `analyze --name <alias>`)
160
160
  * 2. preserved alias on an existing entry for this path
161
- * 3. `path.basename(repoPath)` (the original default)
161
+ * 3. `git config --get remote.origin.url` repo name (#979 recovers
162
+ * a meaningful name for monorepo subprojects, git worktrees, and
163
+ * Gas-Town-style `<rig>/refinery/rig/` layouts where the basename
164
+ * is generic)
165
+ * 4. `path.basename(repoPath)` (the original default)
162
166
  *
163
167
  * Duplicate-name guard: if another path already uses the resolved
164
168
  * `name`, throw {@link RegistryNameCollisionError} unless
@@ -166,8 +170,12 @@ export declare class RegistryNameCollisionError extends Error {
166
170
  * `name`; un-aliased basename collisions continue to register silently
167
171
  * so existing users who don't know about `--name` see no behaviour
168
172
  * change.
173
+ *
174
+ * Returns the `name` that was actually written to the registry — the
175
+ * caller can re-use it to keep AGENTS.md / skill files aligned with the
176
+ * MCP-visible repo name (#979).
169
177
  */
170
- export declare const registerRepo: (repoPath: string, meta: RepoMeta, opts?: RegisterRepoOptions) => Promise<void>;
178
+ export declare const registerRepo: (repoPath: string, meta: RepoMeta, opts?: RegisterRepoOptions) => Promise<string>;
171
179
  /**
172
180
  * Remove a repo from the global registry.
173
181
  * Called after `gitnexus clean`.
@@ -8,6 +8,7 @@
8
8
  import fs from 'fs/promises';
9
9
  import path from 'path';
10
10
  import os from 'os';
11
+ import { getInferredRepoName } from './git.js';
11
12
  const GITNEXUS_DIR = '.gitnexus';
12
13
  // ─── Local Storage Helpers ─────────────────────────────────────────────
13
14
  /**
@@ -223,20 +224,34 @@ export class RegistryNameCollisionError extends Error {
223
224
  }
224
225
  }
225
226
  /** Returns true when a previously-registered entry's `name` differs from
226
- * `path.basename(entry.path)` i.e. a user explicitly aliased it via
227
- * `analyze --name <alias>` on a prior run. Used to preserve the alias
228
- * across re-analyses that omit `--name`. */
229
- const hasCustomAlias = (entry) => {
230
- return entry.name !== path.basename(path.resolve(entry.path));
227
+ * both `path.basename(entry.path)` and the git-remote-derived name
228
+ * i.e. a user explicitly aliased it via `analyze --name <alias>` on a
229
+ * prior run. Used to preserve the alias across re-analyses that omit
230
+ * `--name`. The remote-derived name is treated as an inference, not a
231
+ * custom alias, so re-analyses keep tracking remote renames.
232
+ *
233
+ * `inferredName` is passed in (rather than re-derived) so callers can
234
+ * avoid a second `git config` subprocess invocation. */
235
+ const hasCustomAlias = (entry, inferredName) => {
236
+ const resolved = path.resolve(entry.path);
237
+ if (entry.name === path.basename(resolved))
238
+ return false;
239
+ if (inferredName && entry.name === inferredName)
240
+ return false;
241
+ return true;
231
242
  };
232
243
  /**
233
244
  * Register (add or update) a repo in the global registry.
234
245
  * Called after `gitnexus analyze` completes.
235
246
  *
236
- * Name resolution precedence (#829):
247
+ * Name resolution precedence (#829, #979):
237
248
  * 1. explicit `opts.name` (from `analyze --name <alias>`)
238
249
  * 2. preserved alias on an existing entry for this path
239
- * 3. `path.basename(repoPath)` (the original default)
250
+ * 3. `git config --get remote.origin.url` repo name (#979 recovers
251
+ * a meaningful name for monorepo subprojects, git worktrees, and
252
+ * Gas-Town-style `<rig>/refinery/rig/` layouts where the basename
253
+ * is generic)
254
+ * 4. `path.basename(repoPath)` (the original default)
240
255
  *
241
256
  * Duplicate-name guard: if another path already uses the resolved
242
257
  * `name`, throw {@link RegistryNameCollisionError} unless
@@ -244,6 +259,10 @@ const hasCustomAlias = (entry) => {
244
259
  * `name`; un-aliased basename collisions continue to register silently
245
260
  * so existing users who don't know about `--name` see no behaviour
246
261
  * change.
262
+ *
263
+ * Returns the `name` that was actually written to the registry — the
264
+ * caller can re-use it to keep AGENTS.md / skill files aligned with the
265
+ * MCP-visible repo name (#979).
247
266
  */
248
267
  export const registerRepo = async (repoPath, meta, opts) => {
249
268
  const resolved = path.resolve(repoPath);
@@ -255,15 +274,35 @@ export const registerRepo = async (repoPath, meta, opts) => {
255
274
  return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b;
256
275
  });
257
276
  const existing = existingIdx >= 0 ? entries[existingIdx] : null;
258
- // Precedence: explicit --name > preserved alias > basename.
259
- const name = opts?.name ?? (existing && hasCustomAlias(existing) ? existing.name : path.basename(resolved));
277
+ // Precedence: explicit --name > preserved alias > remote-inferred > basename.
278
+ // Skip the `git config` subprocess entirely when --name was passed —
279
+ // the remote isn't consulted in that case.
280
+ let name;
281
+ let isPreservedAlias = false;
282
+ if (opts?.name !== undefined) {
283
+ name = opts.name;
284
+ }
285
+ else {
286
+ // Compute the remote-derived name at most once. It feeds both the
287
+ // alias-preservation check (`hasCustomAlias` needs it to distinguish
288
+ // a sticky user alias from a previously-stored remote inference) and
289
+ // the fallback name when neither --name nor a preserved alias apply.
290
+ const inferred = getInferredRepoName(resolved);
291
+ if (existing && hasCustomAlias(existing, inferred)) {
292
+ name = existing.name;
293
+ isPreservedAlias = true;
294
+ }
295
+ else {
296
+ name = inferred ?? path.basename(resolved);
297
+ }
298
+ }
260
299
  // Duplicate-name guard: only fire when the user EXPLICITLY asked for
261
300
  // this name (via opts.name or a preserved alias). Unqualified basename
262
- // collisions are preserved for backward-compat — they still register,
263
- // and the user sees the ambiguity at `-r` / `list` resolution time
264
- // (which is already improved by the disambiguated error messages and
265
- // list output this PR also ships).
266
- const explicitName = opts?.name !== undefined || (existing && hasCustomAlias(existing));
301
+ // and remote-inferred collisions are preserved for backward-compat —
302
+ // they still register, and the user sees the ambiguity at `-r` / `list`
303
+ // resolution time (which is already improved by the disambiguated error
304
+ // messages and list output #829 ships).
305
+ const explicitName = opts?.name !== undefined || isPreservedAlias;
267
306
  if (explicitName && !opts?.allowDuplicateName) {
268
307
  const collidingEntry = entries.find((e, i) => i !== existingIdx &&
269
308
  e.name.toLowerCase() === name.toLowerCase() &&
@@ -287,6 +326,7 @@ export const registerRepo = async (repoPath, meta, opts) => {
287
326
  entries.push(entry);
288
327
  }
289
328
  await writeRegistry(entries);
329
+ return name;
290
330
  };
291
331
  /**
292
332
  * Remove a repo from the global registry.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.3-rc.19",
3
+ "version": "1.6.3-rc.20",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",