gitnexus 1.6.6-rc.62 → 1.6.6-rc.63

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.
@@ -4,11 +4,13 @@ import { createInterface } from 'readline';
4
4
  import { once } from 'events';
5
5
  import { finished } from 'stream/promises';
6
6
  import path from 'path';
7
+ import os from 'os';
8
+ import crypto from 'crypto';
7
9
  import lbug from '@ladybugdb/core';
8
10
  import { NODE_TABLES, REL_TABLE_NAME, SCHEMA_QUERIES, EMBEDDING_TABLE_NAME, STALE_HASH_SENTINEL, } from './schema.js';
9
11
  import { streamAllCSVsToDisk } from './csv-generator.js';
10
12
  import { extensionManager } from './extension-loader.js';
11
- import { closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
13
+ import { closeLbugConnection, isDbBusyError, isOpenRetryExhausted, isWalCorruptionError, openLbugConnection, toNativeSafePath, WAL_RECOVERY_SUGGESTION, waitForWindowsHandleRelease, } from './lbug-config.js';
12
14
  import { finalizeLbugSidecarsAfterClose, inspectLbugSidecars, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, shadowSidecarRecoveryMessage, } from './sidecar-recovery.js';
13
15
  import { isVectorExtensionSupportedByPlatform } from '../platform/capabilities.js';
14
16
  import { logger } from '../logger.js';
@@ -298,7 +300,7 @@ const runWithSessionLock = async (operation) => {
298
300
  release?.();
299
301
  }
300
302
  };
301
- const normalizeCopyPath = (filePath) => filePath.replace(/\\/g, '/');
303
+ const normalizeCopyPath = (filePath) => toNativeSafePath(filePath).replace(/\\/g, '/');
302
304
  const closeQueryResult = async (result) => {
303
305
  try {
304
306
  await result.close();
@@ -718,7 +720,14 @@ export const loadGraphToLbug = async (graph, repoPath, storagePath, onProgress)
718
720
  throw new Error('LadybugDB not initialized. Call initLbug first.');
719
721
  }
720
722
  const log = onProgress || (() => { });
721
- const csvDir = path.join(storagePath, 'csv');
723
+ let csvDir;
724
+ if (process.platform === 'win32' && /[^\x00-\x7F]/.test(storagePath)) {
725
+ const hash = crypto.createHash('sha256').update(storagePath).digest('hex').slice(0, 16);
726
+ csvDir = toNativeSafePath(path.join(os.tmpdir(), `gitnexus-csv-${hash}`));
727
+ }
728
+ else {
729
+ csvDir = path.join(storagePath, 'csv');
730
+ }
722
731
  log('Streaming CSVs to disk...');
723
732
  const csvResult = await streamAllCSVsToDisk(graph, repoPath, csvDir);
724
733
  const validTables = new Set(NODE_TABLES);
@@ -1,4 +1,6 @@
1
1
  import type lbug from '@ladybugdb/core';
2
+ export declare function cleanupNativePathJunctions(): void;
3
+ export declare function toNativeSafePath(p: string): string;
2
4
  /**
3
5
  * Shared configuration for `@ladybugdb/core` `Database` construction.
4
6
  *
@@ -1,7 +1,184 @@
1
1
  import fs from 'fs/promises';
2
+ import fsSync from 'fs';
2
3
  import os from 'os';
3
4
  import path from 'path';
5
+ import crypto from 'crypto';
6
+ import { execFileSync } from 'child_process';
7
+ import { isMainThread } from 'worker_threads';
4
8
  import { logger } from '../logger.js';
9
+ // ─── Windows non-ASCII path workaround (#1811) ───────────────────────────────
10
+ //
11
+ // KuzuDB's native C++ layer on Windows uses CreateFileA (ANSI), not
12
+ // CreateFileW. Non-ASCII path bytes from Node.js (UTF-8) are
13
+ // misinterpreted via the system's Active Code Page (e.g. GBK), producing
14
+ // a garbled path — "Error 3: The system cannot find the path."
15
+ //
16
+ // Layered workaround:
17
+ // 1. Try 8.3 short-name form (fast, no persistent state)
18
+ // 2. Fall back to an NTFS junction from an ASCII temp path
19
+ // 3. If both fail, log a diagnostic and return the original path
20
+ const NON_ASCII_RE = /[^\x00-\x7F]/;
21
+ const JUNCTION_PREFIX = 'gitnexus-junction-';
22
+ const activeJunctions = new Set();
23
+ let cleanupRegistered = false;
24
+ let orphanScanDone = false;
25
+ function junctionHash(targetDir) {
26
+ return crypto.createHash('sha256').update(targetDir).digest('hex').slice(0, 16);
27
+ }
28
+ function tryShortPath(p) {
29
+ try {
30
+ // Pass the path via environment variable so the command string is
31
+ // static — avoids CodeQL command-injection taint (the path never
32
+ // appears in the shell command text).
33
+ const result = execFileSync('cmd.exe', ['/c', 'for %I in ("%GITNEXUS_SP%") do @echo %~sI'], {
34
+ encoding: 'utf-8',
35
+ timeout: 5000,
36
+ windowsHide: true,
37
+ stdio: ['pipe', 'pipe', 'pipe'],
38
+ env: { ...process.env, GITNEXUS_SP: p },
39
+ });
40
+ const shortPath = result.trim();
41
+ if (shortPath &&
42
+ !NON_ASCII_RE.test(shortPath) &&
43
+ (!shortPath.includes('?') || p.includes('?'))) {
44
+ return shortPath;
45
+ }
46
+ }
47
+ catch {
48
+ // 8.3 unavailable or cmd failed
49
+ }
50
+ return null;
51
+ }
52
+ function tryJunction(targetDir, leaf) {
53
+ const hash = junctionHash(targetDir);
54
+ const junctionLink = path.join(os.tmpdir(), `${JUNCTION_PREFIX}${hash}`);
55
+ if (fsSync.existsSync(junctionLink)) {
56
+ try {
57
+ const existing = fsSync.readlinkSync(junctionLink);
58
+ if (path.resolve(existing) === path.resolve(targetDir)) {
59
+ activeJunctions.add(junctionLink);
60
+ return path.join(junctionLink, leaf);
61
+ }
62
+ fsSync.rmSync(junctionLink, { recursive: true, force: true });
63
+ }
64
+ catch {
65
+ // Stale or broken junction — remove and recreate
66
+ try {
67
+ fsSync.rmSync(junctionLink, { recursive: true, force: true });
68
+ }
69
+ catch {
70
+ /* best effort */
71
+ }
72
+ }
73
+ }
74
+ try {
75
+ fsSync.symlinkSync(targetDir, junctionLink, 'junction');
76
+ activeJunctions.add(junctionLink);
77
+ return path.join(junctionLink, leaf);
78
+ }
79
+ catch (err) {
80
+ if (err.code === 'EEXIST') {
81
+ try {
82
+ const existing = fsSync.readlinkSync(junctionLink);
83
+ if (path.resolve(existing) === path.resolve(targetDir)) {
84
+ activeJunctions.add(junctionLink);
85
+ return path.join(junctionLink, leaf);
86
+ }
87
+ }
88
+ catch {
89
+ /* cannot verify — fall through */
90
+ }
91
+ }
92
+ }
93
+ return null;
94
+ }
95
+ function registerCleanupHandlers() {
96
+ if (cleanupRegistered)
97
+ return;
98
+ cleanupRegistered = true;
99
+ process.on('exit', () => cleanupNativePathJunctions());
100
+ for (const signal of ['SIGTERM', 'SIGINT']) {
101
+ process.on(signal, () => {
102
+ cleanupNativePathJunctions();
103
+ if (process.platform === 'win32') {
104
+ process.exit(signal === 'SIGINT' ? 130 : 143);
105
+ }
106
+ else {
107
+ process.kill(process.pid, signal);
108
+ }
109
+ });
110
+ }
111
+ }
112
+ function scanOrphanedJunctions() {
113
+ if (orphanScanDone)
114
+ return;
115
+ orphanScanDone = true;
116
+ try {
117
+ const tmpdir = os.tmpdir();
118
+ const entries = fsSync.readdirSync(tmpdir);
119
+ for (const entry of entries) {
120
+ if (!entry.startsWith(JUNCTION_PREFIX))
121
+ continue;
122
+ const junctionPath = path.join(tmpdir, entry);
123
+ try {
124
+ const target = fsSync.readlinkSync(junctionPath);
125
+ try {
126
+ fsSync.lstatSync(target);
127
+ }
128
+ catch {
129
+ fsSync.rmSync(junctionPath, { recursive: true, force: true });
130
+ }
131
+ }
132
+ catch {
133
+ // Not a symlink/junction or unreadable — leave it
134
+ }
135
+ }
136
+ }
137
+ catch {
138
+ // tmpdir unreadable — skip scan
139
+ }
140
+ }
141
+ export function cleanupNativePathJunctions() {
142
+ for (const junctionPath of activeJunctions) {
143
+ try {
144
+ fsSync.rmSync(junctionPath, { recursive: true, force: true });
145
+ }
146
+ catch {
147
+ // Best effort — EPERM on Windows is common during exit
148
+ }
149
+ }
150
+ activeJunctions.clear();
151
+ }
152
+ export function toNativeSafePath(p) {
153
+ if (process.platform !== 'win32')
154
+ return p;
155
+ if (!NON_ASCII_RE.test(p))
156
+ return p;
157
+ if (isMainThread) {
158
+ scanOrphanedJunctions();
159
+ registerCleanupHandlers();
160
+ }
161
+ const shortPath = tryShortPath(p);
162
+ if (shortPath)
163
+ return shortPath;
164
+ if (!isMainThread) {
165
+ logger.warn(`GitNexus: non-ASCII path in worker thread — junction fallback skipped. ` +
166
+ `Path: "${p}". 8.3 short names may need to be enabled on this volume.`);
167
+ return p;
168
+ }
169
+ const targetDir = path.dirname(p);
170
+ const leaf = path.basename(p);
171
+ if (fsSync.existsSync(targetDir)) {
172
+ const junctionResult = tryJunction(targetDir, leaf);
173
+ if (junctionResult)
174
+ return junctionResult;
175
+ }
176
+ logger.warn(`GitNexus: non-ASCII path "${p}" could not be converted to an ASCII-safe form. ` +
177
+ 'LadybugDB may fail with "Cannot open file." To fix: move the repo to a path ' +
178
+ 'without CJK/Unicode characters, or enable 8.3 short names on this volume ' +
179
+ '(fsutil 8dot3name set 0).');
180
+ return p;
181
+ }
5
182
  /**
6
183
  * Shared configuration for `@ladybugdb/core` `Database` construction.
7
184
  *
@@ -306,9 +483,10 @@ const openWithLockRetry = async (construct, dbPath) => {
306
483
  throw tagOpenRetryExhausted(originalLockError);
307
484
  };
308
485
  export async function openLbugConnection(lbugModule, databasePath, options = {}) {
486
+ const safePath = toNativeSafePath(databasePath);
309
487
  let db;
310
488
  try {
311
- db = await openWithLockRetry(() => createLbugDatabase(lbugModule, databasePath, options), databasePath);
489
+ db = await openWithLockRetry(() => createLbugDatabase(lbugModule, safePath, options), safePath);
312
490
  return { db, conn: new lbugModule.Connection(db) };
313
491
  }
314
492
  catch (err) {
@@ -19,7 +19,7 @@ import os from 'os';
19
19
  import path from 'path';
20
20
  import lbug from '@ladybugdb/core';
21
21
  import { isReadOnlyDbError, loadFTSExtension } from './lbug-adapter.js';
22
- import { createLbugDatabase, isWalCorruptionError, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
22
+ import { createLbugDatabase, isWalCorruptionError, toNativeSafePath, WAL_RECOVERY_SUGGESTION, } from './lbug-config.js';
23
23
  import { isMissingFsError, isMissingShadowSidecarError, isReadOnlyShadowReplayError, preflightLbugSidecars, quarantineWalForMissingShadow, renameFailureMessage, statIfExists, } from './sidecar-recovery.js';
24
24
  /**
25
25
  * Probe whether a Windows FTS extension binary is locally installed under
@@ -313,7 +313,7 @@ async function probeDatabaseForShadowReplay(db) {
313
313
  async function replayShadowPagesWithWritableOpen(dbPath) {
314
314
  let db;
315
315
  try {
316
- db = createLbugDatabase(lbug, dbPath, { throwOnWalReplayFailure: false });
316
+ db = createLbugDatabase(lbug, toNativeSafePath(dbPath), { throwOnWalReplayFailure: false });
317
317
  await db.init();
318
318
  await probeDatabaseForShadowReplay(db);
319
319
  }
@@ -340,7 +340,7 @@ async function openReadOnlyDatabase(dbPath) {
340
340
  logger: poolSidecarLogger,
341
341
  allowQuarantine: true,
342
342
  });
343
- db = createLbugDatabase(lbug, dbPath, {
343
+ db = createLbugDatabase(lbug, toNativeSafePath(dbPath), {
344
344
  readOnly: true,
345
345
  throwOnWalReplayFailure: false,
346
346
  });
@@ -360,7 +360,7 @@ async function openReadOnlyDatabase(dbPath) {
360
360
  logger: poolSidecarLogger,
361
361
  allowQuarantine: true,
362
362
  });
363
- db = createLbugDatabase(lbug, dbPath, {
363
+ db = createLbugDatabase(lbug, toNativeSafePath(dbPath), {
364
364
  readOnly: true,
365
365
  throwOnWalReplayFailure: false,
366
366
  });
@@ -374,7 +374,7 @@ async function openReadOnlyDatabase(dbPath) {
374
374
  await db.close().catch(() => { });
375
375
  db = undefined;
376
376
  await replayShadowPagesWithWritableOpen(dbPath);
377
- db = createLbugDatabase(lbug, dbPath, {
377
+ db = createLbugDatabase(lbug, toNativeSafePath(dbPath), {
378
378
  readOnly: true,
379
379
  throwOnWalReplayFailure: false,
380
380
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.6-rc.62",
3
+ "version": "1.6.6-rc.63",
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",
@@ -59,6 +59,7 @@ const LBUG_NATIVE = [
59
59
  'test/integration/lbug-close-handle-release.test.ts',
60
60
  'test/integration/lbug-orphan-sidecar-recovery.test.ts',
61
61
  'test/integration/lbug-readonly-init.test.ts',
62
+ 'test/integration/lbug-non-ascii-path.test.ts',
62
63
  'test/integration/local-backend.test.ts',
63
64
  'test/integration/local-backend-calltool.test.ts',
64
65
  'test/integration/search-core.test.ts',