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.
- package/dist/core/run-analyze.js +7 -4
- package/dist/storage/git.d.ts +25 -0
- package/dist/storage/git.js +52 -0
- package/dist/storage/repo-manager.d.ts +11 -3
- package/dist/storage/repo-manager.js +54 -14
- package/package.json +1 -1
package/dist/core/run-analyze.js
CHANGED
|
@@ -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
|
-
|
|
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) {
|
package/dist/storage/git.d.ts
CHANGED
|
@@ -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;
|
package/dist/storage/git.js
CHANGED
|
@@ -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. `
|
|
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<
|
|
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)`
|
|
227
|
-
* `analyze --name <alias>` on a
|
|
228
|
-
* across re-analyses that omit
|
|
229
|
-
|
|
230
|
-
|
|
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. `
|
|
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
|
-
|
|
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 —
|
|
263
|
-
// and the user sees the ambiguity at `-r` / `list`
|
|
264
|
-
// (which is already improved by the disambiguated error
|
|
265
|
-
// list output
|
|
266
|
-
const explicitName = opts?.name !== undefined ||
|
|
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