gitnexus 1.6.9 → 1.6.10-rc.10
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/README.md +126 -41
- package/dist/cli/ai-context.d.ts +1 -1
- package/dist/cli/ai-context.js +1 -1
- package/dist/cli/analyze.js +106 -3
- package/dist/cli/cli-message.d.ts +1 -1
- package/dist/cli/doctor.d.ts +5 -0
- package/dist/cli/doctor.js +41 -3
- package/dist/cli/editor-targets.d.ts +17 -2
- package/dist/cli/editor-targets.js +44 -0
- package/dist/cli/embeddings.d.ts +12 -0
- package/dist/cli/embeddings.js +49 -0
- package/dist/cli/help-i18n.js +4 -0
- package/dist/cli/i18n/en.d.ts +5 -1
- package/dist/cli/i18n/en.js +5 -1
- package/dist/cli/i18n/resources.d.ts +9 -1
- package/dist/cli/i18n/zh-CN.d.ts +4 -0
- package/dist/cli/i18n/zh-CN.js +5 -1
- package/dist/cli/index.js +12 -1
- package/dist/cli/setup.js +204 -28
- package/dist/cli/uninstall.js +70 -16
- package/dist/core/embeddings/embedder.js +16 -2
- package/dist/core/embeddings/http-client.d.ts +28 -1
- package/dist/core/embeddings/http-client.js +107 -15
- package/dist/core/embeddings/node-module-compat.d.ts +23 -0
- package/dist/core/embeddings/node-module-compat.js +23 -0
- package/dist/core/embeddings/onnxruntime-common-resolver.js +6 -3
- package/dist/core/embeddings/onnxruntime-node-resolver.js +31 -5
- package/dist/core/embeddings/runtime-install.d.ts +119 -0
- package/dist/core/embeddings/runtime-install.js +372 -0
- package/dist/core/embeddings/runtime-support.d.ts +48 -17
- package/dist/core/embeddings/runtime-support.js +96 -0
- package/dist/core/group/extractors/http-patterns/python.js +246 -108
- package/dist/core/group/extractors/manifest-extractor.js +4 -0
- package/dist/core/ingestion/cobol/jcl-processor.js +9 -8
- package/dist/core/ingestion/cobol-processor.js +27 -26
- package/dist/core/ingestion/community-processor.d.ts +51 -1
- package/dist/core/ingestion/community-processor.js +316 -63
- package/dist/core/ingestion/emit-references.js +3 -2
- package/dist/core/ingestion/markdown-processor.js +3 -2
- package/dist/core/ingestion/parsing-processor.d.ts +3 -1
- package/dist/core/ingestion/parsing-processor.js +4 -0
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +42 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +83 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +131 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.d.ts +75 -0
- package/dist/core/ingestion/route-extractors/python-const-resolver.js +299 -0
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -1
- package/dist/core/ingestion/tree-sitter-queries.js +12 -1
- package/dist/core/ingestion/utils/line-base.d.ts +20 -0
- package/dist/core/ingestion/utils/line-base.js +20 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +21 -0
- package/dist/core/ingestion/utils/symbol-labels.js +45 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +34 -0
- package/dist/core/ingestion/workers/parse-worker.js +39 -4
- package/dist/core/ingestion/workers/result-merge.js +4 -0
- package/dist/core/lbug/csv-generator.js +8 -2
- package/dist/core/lbug/extension-load-error.d.ts +67 -0
- package/dist/core/lbug/extension-load-error.js +320 -0
- package/dist/core/lbug/extension-loader.d.ts +17 -2
- package/dist/core/lbug/extension-loader.js +38 -13
- package/dist/core/lbug/lbug-adapter.js +7 -16
- package/dist/core/lbug/native-check.d.ts +22 -0
- package/dist/core/lbug/native-check.js +66 -0
- package/dist/core/lbug/pool-adapter.js +10 -1
- package/dist/core/lbug/sidecar-recovery.d.ts +32 -0
- package/dist/core/lbug/sidecar-recovery.js +91 -7
- package/dist/core/platform/capabilities.js +27 -1
- package/dist/core/run-analyze.js +33 -6
- package/dist/core/search/fts-indexes.d.ts +7 -0
- package/dist/core/search/fts-indexes.js +37 -0
- package/dist/mcp/core/embedder.js +16 -2
- package/dist/mcp/local/line-display.d.ts +22 -0
- package/dist/mcp/local/line-display.js +3 -0
- package/dist/mcp/local/local-backend.d.ts +14 -0
- package/dist/mcp/local/local-backend.js +63 -19
- package/dist/mcp/local/pdg-impact.d.ts +5 -3
- package/dist/mcp/local/pdg-impact.js +5 -2
- package/dist/mcp/resources.js +1 -0
- package/dist/mcp/tools.js +1 -1
- package/dist/server/api.js +2 -2
- package/dist/storage/parse-cache.js +1 -1
- package/dist/storage/repo-manager.d.ts +6 -1
- package/dist/storage/repo-manager.js +6 -1
- package/hooks/antigravity/gitnexus-antigravity-hook.cjs +50 -3
- package/hooks/claude/gitnexus-hook.cjs +59 -9
- package/package.json +5 -3
- package/scripts/cross-platform-tests.ts +26 -0
- package/scripts/ensure-fts.ts +32 -0
- package/scripts/install-duckdb-extension.mjs +85 -24
- package/scripts/run-cross-platform.ts +46 -6
- package/scripts/shard-arg.ts +30 -0
|
@@ -349,6 +349,49 @@ function runGitNexusCli(cliPath, args, cwd, timeout) {
|
|
|
349
349
|
});
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
+
/**
|
|
353
|
+
* Fallback augmentation for the #2396 path: when a GitNexus process holds the
|
|
354
|
+
* lbug DB write lock the CLI `augment` can't run, so point the agent at the MCP
|
|
355
|
+
* `query` tool instead. Phrased conditionally ("if the MCP tools are live") so it
|
|
356
|
+
* stays truthful on every owner path — a confirmed MCP owner, a `serve` owner, or
|
|
357
|
+
* a fail-closed probe where no server is actually confirmed. `pattern` is embedded
|
|
358
|
+
* verbatim; the caller (sendHookResponse) JSON-escapes it structurally.
|
|
359
|
+
*/
|
|
360
|
+
function buildMcpQueryHint(pattern) {
|
|
361
|
+
return (
|
|
362
|
+
`[GitNexus] Local augment is unavailable (the graph DB is held by another ` +
|
|
363
|
+
`GitNexus process). If the GitNexus MCP tools are live in this session, call ` +
|
|
364
|
+
`the GitNexus \`query\` MCP tool (e.g. mcp__gitnexus__query) with ` +
|
|
365
|
+
`search_query "${pattern}".`
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* #2396 throttle: emit the MCP-query hint at most once per repo per window, so an
|
|
371
|
+
* owner-locked session isn't nudged on every search. Window (ms) via
|
|
372
|
+
* GITNEXUS_MCP_HINT_THROTTLE_MS (default 10min; 0/invalid disables). Best-effort —
|
|
373
|
+
* any fs error falls back to emitting.
|
|
374
|
+
* ponytail: per-repo mtime marker, shared across concurrent sessions on the same
|
|
375
|
+
* repo; add per-session dedup only if that sharing becomes a problem.
|
|
376
|
+
*/
|
|
377
|
+
function shouldEmitMcpHint(gitNexusDir) {
|
|
378
|
+
const raw = process.env.GITNEXUS_MCP_HINT_THROTTLE_MS;
|
|
379
|
+
const windowMs = raw === undefined || raw === '' ? 600000 : Number(raw);
|
|
380
|
+
if (!Number.isFinite(windowMs) || windowMs <= 0) return true;
|
|
381
|
+
const marker = path.join(gitNexusDir, '.mcp-hint-shown');
|
|
382
|
+
try {
|
|
383
|
+
if (Date.now() - fs.statSync(marker).mtimeMs < windowMs) return false;
|
|
384
|
+
} catch {
|
|
385
|
+
/* marker missing/unreadable → emit */
|
|
386
|
+
}
|
|
387
|
+
try {
|
|
388
|
+
fs.writeFileSync(marker, '');
|
|
389
|
+
} catch {
|
|
390
|
+
/* best-effort; still emit */
|
|
391
|
+
}
|
|
392
|
+
return true;
|
|
393
|
+
}
|
|
394
|
+
|
|
352
395
|
/**
|
|
353
396
|
* PreToolUse handler — augment searches with graph context.
|
|
354
397
|
*/
|
|
@@ -385,18 +428,25 @@ function handlePreToolUse(input) {
|
|
|
385
428
|
let result = '';
|
|
386
429
|
try {
|
|
387
430
|
if (hasGitNexusServerOwner(gitNexusDir)) {
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
431
|
+
// #2396: the MCP server holds the DB write lock, so a competing CLI
|
|
432
|
+
// `augment` would only contend on it (LadybugDB is single-writer). But the
|
|
433
|
+
// session that triggered this hook has the GitNexus MCP tools live — route
|
|
434
|
+
// the augmentation to the agent via additionalContext instead of silently
|
|
435
|
+
// doing nothing. Mirror the skip reason to stderr only under GITNEXUS_DEBUG
|
|
436
|
+
// (strict-runner contract, #1913); the hint itself rides the sanctioned
|
|
437
|
+
// additionalContext stdout channel the successful augment already uses.
|
|
391
438
|
if (isDebugEnabled()) {
|
|
392
439
|
process.stderr.write('[GitNexus] augment skipped: MCP server owns DB\n');
|
|
393
440
|
}
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
441
|
+
if (shouldEmitMcpHint(gitNexusDir)) {
|
|
442
|
+
result = buildMcpQueryHint(pattern);
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
const cliPath = resolveCliPath();
|
|
446
|
+
const child = runGitNexusCli(cliPath, ['augment', '--', pattern], cwd, 7000);
|
|
447
|
+
if (!child.error && child.status === 0) {
|
|
448
|
+
result = extractAugmentContext(child.stderr || '');
|
|
449
|
+
}
|
|
400
450
|
}
|
|
401
451
|
} catch {
|
|
402
452
|
/* graceful failure */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gitnexus",
|
|
3
|
-
"version": "1.6.
|
|
3
|
+
"version": "1.6.10-rc.10",
|
|
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",
|
|
@@ -55,7 +55,6 @@
|
|
|
55
55
|
"prepack": "node scripts/assert-publish-grammar-coverage.cjs && node scripts/build.js"
|
|
56
56
|
},
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@huggingface/transformers": "^4.1.0",
|
|
59
58
|
"@ladybugdb/core": "^0.18.0",
|
|
60
59
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
61
60
|
"@scarf/scarf": "^1.4.0",
|
|
@@ -76,7 +75,6 @@
|
|
|
76
75
|
"node-addon-api": "^8.0.0",
|
|
77
76
|
"node-gyp-build": "^4.8.0",
|
|
78
77
|
"onnxruntime-common": "^1.26.0",
|
|
79
|
-
"onnxruntime-node": "^1.24.0",
|
|
80
78
|
"pandemonium": "^2.4.0",
|
|
81
79
|
"pino": "^10.3.1",
|
|
82
80
|
"pino-pretty": "^13.1.3",
|
|
@@ -93,6 +91,10 @@
|
|
|
93
91
|
"tree-sitter-typescript": "^0.23.2",
|
|
94
92
|
"uuid": "^14.0.0"
|
|
95
93
|
},
|
|
94
|
+
"optionalDependencies": {
|
|
95
|
+
"@huggingface/transformers": "^4.1.0",
|
|
96
|
+
"onnxruntime-node": "^1.24.0"
|
|
97
|
+
},
|
|
96
98
|
"devDependencies": {
|
|
97
99
|
"@babel/generator": "^7.29.7",
|
|
98
100
|
"@babel/parser": "^7.29.7",
|
|
@@ -30,7 +30,11 @@ const PLATFORM_LOGIC = [
|
|
|
30
30
|
'test/unit/setup-jsonc.test.ts',
|
|
31
31
|
'test/unit/setup-codex.test.ts',
|
|
32
32
|
'test/unit/setup-antigravity.test.ts',
|
|
33
|
+
'test/integration/setup-uninstall-roundtrip.test.ts',
|
|
33
34
|
'test/unit/resolve-invocation.test.ts',
|
|
35
|
+
// CLI-spawn entry-point resolution; its path-separator assertion (cli[/\\]index)
|
|
36
|
+
// must exercise the Windows backslash branch, so run it on the OS matrix (#2394).
|
|
37
|
+
'test/unit/cli-entry.test.ts',
|
|
34
38
|
'test/unit/platform-capabilities.test.ts',
|
|
35
39
|
'test/unit/worker-pool-windows-quarantine.test.ts',
|
|
36
40
|
'test/unit/lbug-pool-fts-load.test.ts',
|
|
@@ -42,12 +46,30 @@ const PLATFORM_LOGIC = [
|
|
|
42
46
|
'test/unit/cursor-hook.test.ts',
|
|
43
47
|
'test/unit/sidecar-recovery.test.ts',
|
|
44
48
|
'test/unit/pool-wal-recovery.test.ts',
|
|
49
|
+
'test/unit/lbug-adapter-wal-schema.test.ts',
|
|
45
50
|
'test/unit/detect-changes-worktree.test.ts',
|
|
46
51
|
'test/unit/eval-server-bind-restriction.test.ts',
|
|
47
52
|
'test/unit/ignore-service.test.ts',
|
|
48
53
|
'test/unit/group/bridge-db.test.ts',
|
|
49
54
|
'test/unit/group/bridge-db-edge.test.ts',
|
|
50
55
|
'test/unit/onnxruntime-node-resolver.test.ts',
|
|
56
|
+
// Windows cmd.exe arg-quoting + compose-and-spawn for the npm install (#2372):
|
|
57
|
+
// the quoting rules and win32 single-string spawn shape are OS-sensitive, so
|
|
58
|
+
// exercise them on real windows-latest. The spawn-shape/path tests force their
|
|
59
|
+
// platform branch and derive expected paths via the real fns, so they pass on
|
|
60
|
+
// any host (see the platform stubs + resolve() in the test file).
|
|
61
|
+
'test/unit/embedding-runtime-install.test.ts',
|
|
62
|
+
// Real-spawn arg-delivery round-trip: proves the install spawn delivers args
|
|
63
|
+
// to the child intact on each platform — win32 via the cmd.exe -> .cmd %* ->
|
|
64
|
+
// node chain (real cmd.exe, not just our model), macos/linux via the no-shell
|
|
65
|
+
// array form. Runs on every platform (the ubuntu suite covers Linux; this
|
|
66
|
+
// registration adds windows + macos).
|
|
67
|
+
'test/unit/embedding-install-arg-delivery.test.ts',
|
|
68
|
+
// Structural FTS-extension classifier against REAL binaries (#2374): on this
|
|
69
|
+
// matrix `process.execPath` / `lbugjs.node` are a real PE (windows) and Mach-O
|
|
70
|
+
// (macos), so the header parsing is proven on genuine binaries, not synthetic
|
|
71
|
+
// buffers (the ubuntu suite covers the ELF path).
|
|
72
|
+
'test/integration/extension-binary-real.test.ts',
|
|
51
73
|
];
|
|
52
74
|
|
|
53
75
|
// Native LadybugDB integration tests — exercise the @ladybugdb/core
|
|
@@ -87,6 +109,10 @@ const SPAWN_CLI = [
|
|
|
87
109
|
'test/integration/cli-limit-e2e.test.ts',
|
|
88
110
|
'test/integration/hooks-e2e.test.ts',
|
|
89
111
|
'test/integration/skills-e2e.test.ts',
|
|
112
|
+
// Spawns the real CLI across hermetic HOME/USERPROFILE homes to exercise the
|
|
113
|
+
// FTS extension lifecycle — the #2374 bug was Windows-reported, so this must
|
|
114
|
+
// run on the Windows/macOS matrix, not just the Ubuntu full suite.
|
|
115
|
+
'test/integration/fts-extension-e2e.test.ts',
|
|
90
116
|
'test/integration/server-http-startup.test.ts',
|
|
91
117
|
'test/integration/mcp/server-startup.test.ts',
|
|
92
118
|
'test/integration/analyze-heap-oom-e2e.test.ts',
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install the LadybugDB FTS extension into the shared home (~/.lbdb) up front, so
|
|
3
|
+
* every test in a sharded CI run finds it regardless of which shard it lands in.
|
|
4
|
+
*
|
|
5
|
+
* FTS-dependent tests split two ways: the LOAD-path gate (skipUnlessFtsAvailable)
|
|
6
|
+
* self-installs on miss, but the FILE-path gate (requireFtsResourceOrSkip, e.g.
|
|
7
|
+
* extension-binary-real.test.ts) resolves the extension path at module load and
|
|
8
|
+
* cannot self-install. Sharding (and the balancing sequencer) can drop such a
|
|
9
|
+
* test into a shard with no installer sibling — this step removes that ordering
|
|
10
|
+
* dependency by installing FTS once before vitest starts. `auto` is LOAD-first,
|
|
11
|
+
* so a cache-warmed extension costs no network.
|
|
12
|
+
*
|
|
13
|
+
* Best-effort: exits 0 on failure (offline etc.) — the per-test gates still
|
|
14
|
+
* hard-fail under GITNEXUS_REQUIRE_FTS=1 if FTS is genuinely unavailable, which
|
|
15
|
+
* is where the loud signal belongs.
|
|
16
|
+
*/
|
|
17
|
+
import { mkdtempSync, rmSync } from 'node:fs';
|
|
18
|
+
import { tmpdir } from 'node:os';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { initLbug, loadFTSExtension, closeLbug } from '../src/core/lbug/lbug-adapter.js';
|
|
21
|
+
|
|
22
|
+
const dir = mkdtempSync(join(tmpdir(), 'gn-ensure-fts-'));
|
|
23
|
+
try {
|
|
24
|
+
await initLbug(join(dir, 'ensure-fts.lbug'));
|
|
25
|
+
const ok = await loadFTSExtension(undefined, { policy: 'auto' });
|
|
26
|
+
console.log(ok ? 'FTS extension ready.' : 'FTS extension unavailable (continuing).');
|
|
27
|
+
} catch (err) {
|
|
28
|
+
console.warn(`ensure-fts: skipped (${err instanceof Error ? err.message : String(err)})`);
|
|
29
|
+
} finally {
|
|
30
|
+
await closeLbug();
|
|
31
|
+
rmSync(dir, { recursive: true, force: true });
|
|
32
|
+
}
|
|
@@ -3,9 +3,41 @@ import fs from 'node:fs/promises';
|
|
|
3
3
|
import os from 'node:os';
|
|
4
4
|
import path from 'node:path';
|
|
5
5
|
import { createRequire } from 'node:module';
|
|
6
|
+
import { pathToFileURL } from 'node:url';
|
|
6
7
|
|
|
7
8
|
const EXTENSION_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
8
9
|
|
|
10
|
+
// Positive on-disk-corruption signatures. `FORCE INSTALL` re-downloads even when
|
|
11
|
+
// a file is already present; we only want that when the LOAD error proves the
|
|
12
|
+
// existing file is bad (truncated/wrong-platform, #2374). For everything else —
|
|
13
|
+
// a missing file (plain INSTALL downloads it), or a permanent non-file failure a
|
|
14
|
+
// re-download can never fix (missing runtime dep: "cannot open shared object") —
|
|
15
|
+
// plain INSTALL avoids re-downloading ~2 MB on every analyze run forever.
|
|
16
|
+
// Exported so a parity test keeps this byte-identical to the copy in
|
|
17
|
+
// src/core/lbug/extension-load-error.ts (this `.mjs` cannot import that `.ts`), #2383 F5b.
|
|
18
|
+
export const FILE_CORRUPTION_SIGNATURES = [
|
|
19
|
+
/invalid elf/i,
|
|
20
|
+
/file too short/i,
|
|
21
|
+
/not a valid/i,
|
|
22
|
+
/bad magic/i,
|
|
23
|
+
/wrong architecture/i,
|
|
24
|
+
/mach-o/i,
|
|
25
|
+
/truncat/i,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Decide the install verb from the LOAD error that triggered this install.
|
|
30
|
+
* `FORCE INSTALL` only when the error positively indicates file-level breakage;
|
|
31
|
+
* otherwise plain `INSTALL` (missing file, missing-dependency dlopen failure,
|
|
32
|
+
* or unknown/absent error).
|
|
33
|
+
*/
|
|
34
|
+
export function chooseInstallVerb(loadError) {
|
|
35
|
+
if (loadError && FILE_CORRUPTION_SIGNATURES.some((re) => re.test(loadError))) {
|
|
36
|
+
return 'FORCE INSTALL';
|
|
37
|
+
}
|
|
38
|
+
return 'INSTALL';
|
|
39
|
+
}
|
|
40
|
+
|
|
9
41
|
function parseLbugMaxDbSize(raw) {
|
|
10
42
|
const parsed = raw ? Number(raw) : NaN;
|
|
11
43
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
@@ -14,28 +46,54 @@ function parseLbugMaxDbSize(raw) {
|
|
|
14
46
|
return Math.floor(parsed);
|
|
15
47
|
}
|
|
16
48
|
|
|
17
|
-
|
|
18
|
-
if (!extensionName || !EXTENSION_NAME_PATTERN.test(extensionName)) {
|
|
19
|
-
throw new Error(`Invalid DuckDB extension name: ${extensionName ?? '<missing>'}`);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
const require = createRequire(import.meta.url);
|
|
23
|
-
const lbugModule = require('@ladybugdb/core');
|
|
24
|
-
const lbug = lbugModule.default ?? lbugModule;
|
|
49
|
+
function resolveMaxDbSize() {
|
|
25
50
|
// argv[3] is the optional positional size; ignore it when it is actually a
|
|
26
51
|
// flag token (e.g. `--verify-only`) and fall back to the env default.
|
|
27
52
|
const sizeArg =
|
|
28
53
|
process.argv[3] && !process.argv[3].startsWith('--') ? process.argv[3] : undefined;
|
|
29
|
-
|
|
54
|
+
return parseLbugMaxDbSize(sizeArg ?? process.env.GITNEXUS_LBUG_MAX_DB_SIZE);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Open a scratch LadybugDB and return its connection plus a disposer. */
|
|
58
|
+
async function defaultConnect(lbugMaxDbSize) {
|
|
59
|
+
const require = createRequire(import.meta.url);
|
|
60
|
+
const lbugModule = require('@ladybugdb/core');
|
|
61
|
+
const lbug = lbugModule.default ?? lbugModule;
|
|
30
62
|
|
|
31
63
|
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'gitnexus-ext-install-'));
|
|
32
64
|
const dbPath = path.join(tmpDir, 'install.lbug');
|
|
33
|
-
|
|
34
|
-
|
|
65
|
+
const db = new lbug.Database(dbPath, 0, false, false, lbugMaxDbSize);
|
|
66
|
+
const conn = new lbug.Connection(db);
|
|
67
|
+
return {
|
|
68
|
+
conn,
|
|
69
|
+
dispose: async () => {
|
|
70
|
+
await conn.close().catch(() => {});
|
|
71
|
+
await db.close().catch(() => {});
|
|
72
|
+
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Install (or verify) an optional LadybugDB extension in this short-lived process.
|
|
79
|
+
*
|
|
80
|
+
* @param {string} extensionName
|
|
81
|
+
* @param {object} [options]
|
|
82
|
+
* @param {boolean} [options.verifyOnly] LOAD-only Docker build gate — no install.
|
|
83
|
+
* @param {string} [options.loadError] The parent's LOAD failure; selects the verb.
|
|
84
|
+
* @param {(size: number) => Promise<{conn: {query: (sql: string) => Promise<unknown>}, dispose: () => Promise<void>}>} [options.connect]
|
|
85
|
+
* Connection factory; injectable for offline unit tests.
|
|
86
|
+
*/
|
|
87
|
+
export async function installDuckDbExtension(extensionName, options = {}) {
|
|
88
|
+
const { verifyOnly = false, loadError, connect } = options;
|
|
89
|
+
if (!extensionName || !EXTENSION_NAME_PATTERN.test(extensionName)) {
|
|
90
|
+
throw new Error(`Invalid DuckDB extension name: ${extensionName ?? '<missing>'}`);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const makeConnection = connect ?? (() => defaultConnect(resolveMaxDbSize()));
|
|
94
|
+
const { conn, dispose } = await makeConnection();
|
|
35
95
|
|
|
36
96
|
try {
|
|
37
|
-
db = new lbug.Database(dbPath, 0, false, false, lbugMaxDbSize);
|
|
38
|
-
conn = new lbug.Connection(db);
|
|
39
97
|
if (verifyOnly) {
|
|
40
98
|
// Prove a previously-baked extension is resolvable by a FRESH process
|
|
41
99
|
// under the current HOME (the runtime `LOAD EXTENSION` path) — no INSTALL,
|
|
@@ -46,19 +104,22 @@ async function installDuckDbExtension(extensionName, verifyOnly = false) {
|
|
|
46
104
|
`[install-ext] LOAD-only verify OK for '${extensionName}' (HOME=${process.env.HOME})`,
|
|
47
105
|
);
|
|
48
106
|
} else {
|
|
49
|
-
|
|
107
|
+
// Plain INSTALL is a no-op when the file already exists; escalate to FORCE
|
|
108
|
+
// only when the LOAD error proves the on-disk file is broken (#2374).
|
|
109
|
+
await conn.query(`${chooseInstallVerb(loadError)} ${extensionName}`);
|
|
50
110
|
}
|
|
51
111
|
} finally {
|
|
52
|
-
|
|
53
|
-
if (db) await db.close().catch(() => {});
|
|
54
|
-
await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
|
|
112
|
+
await dispose();
|
|
55
113
|
}
|
|
56
114
|
}
|
|
57
115
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
process.argv.
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
116
|
+
// Only run when executed directly — imported (e.g. by unit tests) it stays inert.
|
|
117
|
+
if (import.meta.url === pathToFileURL(process.argv[1] ?? '').href) {
|
|
118
|
+
installDuckDbExtension(process.argv[2] ?? process.env.GITNEXUS_LBUG_EXTENSION_NAME, {
|
|
119
|
+
verifyOnly: process.argv.includes('--verify-only'),
|
|
120
|
+
loadError: process.env.GITNEXUS_LBUG_EXTENSION_LOAD_ERROR,
|
|
121
|
+
}).catch((err) => {
|
|
122
|
+
console.error(err instanceof Error ? (err.stack ?? err.message) : String(err));
|
|
123
|
+
process.exitCode = 1;
|
|
124
|
+
});
|
|
125
|
+
}
|
|
@@ -14,6 +14,7 @@ import fs from 'fs';
|
|
|
14
14
|
import path from 'path';
|
|
15
15
|
import { fileURLToPath } from 'url';
|
|
16
16
|
import { ALL_CROSS_PLATFORM } from './cross-platform-tests.js';
|
|
17
|
+
import { parseShardArg } from './shard-arg.js';
|
|
17
18
|
|
|
18
19
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
19
20
|
const ROOT = path.resolve(__dirname, '..');
|
|
@@ -27,18 +28,57 @@ if (missing.length > 0) {
|
|
|
27
28
|
process.exit(1);
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
|
|
31
|
+
// Optional sharding (CI): `--shard=<i>/<n>` splits the fixed file list across
|
|
32
|
+
// parallel matrix shards so each runner processes ~1/n of it. Passed straight
|
|
33
|
+
// through to vitest, which partitions the *given* files deterministically. The
|
|
34
|
+
// Windows runner is ~5x slower than macOS/Linux on this spawn-heavy suite (~50
|
|
35
|
+
// CLI/worker process spawns), so a single shard was creeping past the watchdog
|
|
36
|
+
// below; sharding keeps each runner well under it (see ci-tests.yml matrix).
|
|
37
|
+
// Fail loud on a malformed --shard arg (mirrors the missing-files check above):
|
|
38
|
+
// a silently-dropped shard flag would run the full unsharded suite and re-trip
|
|
39
|
+
// the watchdog. Kept outside the execFileSync try/catch below so the message
|
|
40
|
+
// isn't swallowed by that catch's watchdog-only branch.
|
|
41
|
+
let shardArg: string | undefined;
|
|
42
|
+
try {
|
|
43
|
+
shardArg = parseShardArg(process.argv.slice(2));
|
|
44
|
+
} catch (err) {
|
|
45
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Per-shard watchdog, default 15 min. Sharding splits the file list by COUNT, not
|
|
50
|
+
// runtime, so the heaviest spawn suites can cluster on one shard — what this
|
|
51
|
+
// bounds is the *busiest* shard, not an even 1/n of wall-clock. With 3 shards
|
|
52
|
+
// even that shard clears the watchdog, where the whole unsharded Windows run
|
|
53
|
+
// used to trip it. Allow CI/manual runs to add headroom without editing the
|
|
54
|
+
// script again.
|
|
55
|
+
const DEFAULT_TIMEOUT_MIN = 15;
|
|
56
|
+
const timeoutMinutes = Number.parseInt(
|
|
57
|
+
process.env.GITNEXUS_CROSS_PLATFORM_TIMEOUT_MINUTES ?? String(DEFAULT_TIMEOUT_MIN),
|
|
58
|
+
10,
|
|
59
|
+
);
|
|
60
|
+
const timeoutMs =
|
|
61
|
+
Number.isFinite(timeoutMinutes) && timeoutMinutes > 0
|
|
62
|
+
? timeoutMinutes * 60 * 1000
|
|
63
|
+
: DEFAULT_TIMEOUT_MIN * 60 * 1000;
|
|
64
|
+
|
|
65
|
+
console.log(
|
|
66
|
+
`Running ${ALL_CROSS_PLATFORM.length} platform-sensitive tests` +
|
|
67
|
+
`${shardArg ? ` (${shardArg.replace('--shard=', 'shard ')})` : ''}...\n`,
|
|
68
|
+
);
|
|
31
69
|
|
|
32
70
|
try {
|
|
33
|
-
execFileSync('npx', ['vitest', 'run', ...ALL_CROSS_PLATFORM], {
|
|
71
|
+
execFileSync('npx', ['vitest', 'run', ...ALL_CROSS_PLATFORM, ...(shardArg ? [shardArg] : [])], {
|
|
34
72
|
cwd: ROOT,
|
|
35
73
|
stdio: 'inherit',
|
|
36
|
-
timeout:
|
|
74
|
+
timeout: timeoutMs,
|
|
37
75
|
shell: true,
|
|
38
76
|
});
|
|
39
|
-
} catch (err
|
|
40
|
-
|
|
41
|
-
|
|
77
|
+
} catch (err) {
|
|
78
|
+
// execFileSync sets `killed`/`signal` when the watchdog above kills vitest.
|
|
79
|
+
const e = err as { killed?: boolean; signal?: NodeJS.Signals | null };
|
|
80
|
+
if (e.killed || e.signal) {
|
|
81
|
+
console.error(`vitest timed out after ${Math.round(timeoutMs / 60_000)} minutes`);
|
|
42
82
|
}
|
|
43
83
|
process.exit(1);
|
|
44
84
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolves the optional `--shard=<index>/<total>` argument for
|
|
3
|
+
* `run-cross-platform.ts`.
|
|
4
|
+
*
|
|
5
|
+
* Extracted as a pure, side-effect-free function so the branch logic is
|
|
6
|
+
* unit-testable without the script's top-level `execFileSync` (see
|
|
7
|
+
* `test/unit/shard-arg.test.ts`). Mirrors the `computeSpawnPrefix` extraction
|
|
8
|
+
* pattern in `test/helpers/cli-entry.ts`.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const SHARD_RE = /^--shard=\d+\/\d+$/;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the matched `--shard=<index>/<total>` token (e.g. `--shard=1/3`) to
|
|
15
|
+
* pass straight through to vitest, or `undefined` when no shard arg is present.
|
|
16
|
+
*
|
|
17
|
+
* Fails loud on a shard-shaped-but-malformed arg (e.g. `--shard=1`, `--shard`,
|
|
18
|
+
* `--shard=abc`): a silently-ignored malformed arg would drop the shard flag and
|
|
19
|
+
* run the full unsharded ~50-spawn suite, re-arming the Windows watchdog timeout
|
|
20
|
+
* with no signal. Only `--shard` / `--shard=…` args are inspected, so unrelated
|
|
21
|
+
* flags (including a hypothetical `--shardx=…`) pass through untouched.
|
|
22
|
+
*/
|
|
23
|
+
export function parseShardArg(argv: string[]): string | undefined {
|
|
24
|
+
const shardArgs = argv.filter((a) => a === '--shard' || a.startsWith('--shard='));
|
|
25
|
+
const malformed = shardArgs.find((a) => !SHARD_RE.test(a));
|
|
26
|
+
if (malformed !== undefined) {
|
|
27
|
+
throw new Error(`Malformed --shard arg '${malformed}' — expected --shard=<index>/<total>`);
|
|
28
|
+
}
|
|
29
|
+
return shardArgs[0];
|
|
30
|
+
}
|