gitnexus 1.6.6-rc.42 → 1.6.6-rc.44

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.
Files changed (67) hide show
  1. package/README.md +2 -0
  2. package/dist/cli/analyze.d.ts +2 -0
  3. package/dist/cli/analyze.js +30 -1
  4. package/dist/cli/clean.d.ts +1 -0
  5. package/dist/cli/clean.js +39 -9
  6. package/dist/cli/cli-message.d.ts +33 -3
  7. package/dist/cli/cli-message.js +14 -0
  8. package/dist/cli/detect-changes-format.d.ts +1 -0
  9. package/dist/cli/detect-changes-format.js +45 -0
  10. package/dist/cli/doctor.d.ts +2 -0
  11. package/dist/cli/doctor.js +59 -20
  12. package/dist/cli/eval-server.d.ts +1 -1
  13. package/dist/cli/eval-server.js +2 -31
  14. package/dist/cli/help-i18n.d.ts +2 -0
  15. package/dist/cli/help-i18n.js +198 -0
  16. package/dist/cli/i18n/en.d.ts +206 -0
  17. package/dist/cli/i18n/en.js +206 -0
  18. package/dist/cli/i18n/index.d.ts +9 -0
  19. package/dist/cli/i18n/index.js +41 -0
  20. package/dist/cli/i18n/resources.d.ts +414 -0
  21. package/dist/cli/i18n/resources.js +6 -0
  22. package/dist/cli/i18n/zh-CN.d.ts +206 -0
  23. package/dist/cli/i18n/zh-CN.js +206 -0
  24. package/dist/cli/index.js +7 -14
  25. package/dist/cli/list.js +15 -10
  26. package/dist/cli/remove.js +12 -11
  27. package/dist/cli/serve.js +4 -13
  28. package/dist/cli/status.js +11 -10
  29. package/dist/cli/tool.js +7 -39
  30. package/dist/core/lbug/lbug-adapter.d.ts +15 -0
  31. package/dist/core/lbug/lbug-adapter.js +21 -0
  32. package/dist/core/lbug/lbug-config.d.ts +7 -0
  33. package/dist/core/lbug/lbug-config.js +82 -2
  34. package/dist/core/lbug/wal-checkpoint-driver.d.ts +98 -0
  35. package/dist/core/lbug/wal-checkpoint-driver.js +189 -0
  36. package/dist/core/run-analyze.js +21 -1
  37. package/package.json +1 -1
  38. package/web/assets/{agent-CBcds30d.js → agent-CNGl256w.js} +1 -1
  39. package/web/assets/{architectureDiagram-UL44E2DR-dIoPPr6x.js → architectureDiagram-UL44E2DR-DJTnN4-A.js} +1 -1
  40. package/web/assets/{chunk-LCXTWHL2-B8hbjKUm.js → chunk-LCXTWHL2-D6tMtD_-.js} +1 -1
  41. package/web/assets/{chunk-RG4AUYOV-EfsAenro.js → chunk-RG4AUYOV-C3CY7gW4.js} +1 -1
  42. package/web/assets/{classDiagram-KGZ6W3CR-_hSUwNQJ.js → classDiagram-KGZ6W3CR-COdiqi1G.js} +1 -1
  43. package/web/assets/{classDiagram-v2-72OJOZXJ-C0NcgLqj.js → classDiagram-v2-72OJOZXJ-B7YiUGDv.js} +1 -1
  44. package/web/assets/{diagram-3NCE3AQN-CYrNJJUh.js → diagram-3NCE3AQN-aSkD3QID.js} +1 -1
  45. package/web/assets/{diagram-GF46GFSD-56NpS1jw.js → diagram-GF46GFSD-DlsGDkUv.js} +1 -1
  46. package/web/assets/{diagram-QXG6HAR7-DwXkFq_r.js → diagram-QXG6HAR7-NPw8jZAE.js} +1 -1
  47. package/web/assets/{diagram-WEQXMOUZ-C6BTq9za.js → diagram-WEQXMOUZ-CsLi0zm5.js} +1 -1
  48. package/web/assets/{erDiagram-L5TCEMPS-BcEjYsUQ.js → erDiagram-L5TCEMPS-cjritYTk.js} +1 -1
  49. package/web/assets/{flowDiagram-H6V6AXG4-DWAVIV6V.js → flowDiagram-H6V6AXG4-hAr62LB-.js} +1 -1
  50. package/web/assets/index-BeHwDjNI.css +2 -0
  51. package/web/assets/index-Cj2GDX22.js +626 -0
  52. package/web/assets/{infoDiagram-3YFTVSEB-ui-e52GZ.js → infoDiagram-3YFTVSEB-_sF9KVQz.js} +1 -1
  53. package/web/assets/{ishikawaDiagram-BNXS4ZKH-DGimV4zg.js → ishikawaDiagram-BNXS4ZKH-BtwawoWC.js} +1 -1
  54. package/web/assets/{kanban-definition-75IXJCU3-BOyfgvKL.js → kanban-definition-75IXJCU3-CljJPOuK.js} +1 -1
  55. package/web/assets/{mindmap-definition-2TDM6QVE-Ba3QrYSU.js → mindmap-definition-2TDM6QVE-DsqPS_X-.js} +1 -1
  56. package/web/assets/{pieDiagram-CU6KROY3-DMFBXNrM.js → pieDiagram-CU6KROY3-CTUDdOgg.js} +1 -1
  57. package/web/assets/{requirementDiagram-JXO7QTGE-bS4xboSz.js → requirementDiagram-JXO7QTGE-DM8hDKq-.js} +1 -1
  58. package/web/assets/{sequenceDiagram-VS2MUI6T-BqKET_2i.js → sequenceDiagram-VS2MUI6T-m6_R47U2.js} +1 -1
  59. package/web/assets/{stateDiagram-7D4R322I-DP9kvX2i.js → stateDiagram-7D4R322I-Cc1HvF6o.js} +1 -1
  60. package/web/assets/{stateDiagram-v2-36443NZ5-DB-cZ1VL.js → stateDiagram-v2-36443NZ5-Kw9j23FO.js} +1 -1
  61. package/web/assets/{timeline-definition-O6YCAMPW-DNScSOi7.js → timeline-definition-O6YCAMPW-BtENAtHS.js} +1 -1
  62. package/web/assets/{vennDiagram-MWXL3ELB-Bd1zTNWW.js → vennDiagram-MWXL3ELB-DYHpZsEN.js} +1 -1
  63. package/web/assets/{wardleyDiagram-CUQ6CDDI-DqCDQKFt.js → wardleyDiagram-CUQ6CDDI-BNPunZ-h.js} +1 -1
  64. package/web/assets/{xychartDiagram-N2JHSOCM-B8Cje_Ei.js → xychartDiagram-N2JHSOCM-BGBiR7xJ.js} +1 -1
  65. package/web/index.html +2 -2
  66. package/web/assets/index-Czp-OFT-.js +0 -624
  67. package/web/assets/index-nSZgUaIx.css +0 -2
package/README.md CHANGED
@@ -158,6 +158,7 @@ gitnexus analyze --skip-agents-md # Preserve custom AGENTS.md/CLAUDE.md gitnexu
158
158
  gitnexus analyze --verbose # Log skipped files when parsers are unavailable
159
159
  gitnexus analyze --max-file-size 1024 # Skip files larger than N KB (default: 512, cap: 32768)
160
160
  gitnexus analyze --worker-timeout 60 # Increase worker idle timeout for slow parses
161
+ gitnexus analyze --wal-checkpoint-threshold 67108864 # 64 MiB. Control LadybugDB WAL auto-checkpoint threshold (default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB)
161
162
  gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
162
163
  gitnexus serve # Start local HTTP server (multi-repo) for web UI
163
164
  gitnexus index # Register an existing .gitnexus/ folder into the global registry
@@ -307,6 +308,7 @@ Configure the behavior with two environment variables:
307
308
  |----------|--------|---------|--------|
308
309
  | `GITNEXUS_LBUG_EXTENSION_INSTALL` | `auto`, `load-only`, `never` | `auto` | `auto` runs one bounded INSTALL if LOAD fails. `load-only` only uses already-installed extensions (recommended for offline / firewalled environments). `never` skips optional extensions entirely. |
309
310
  | `GITNEXUS_LBUG_EXTENSION_INSTALL_TIMEOUT_MS` | positive integer | `15000` | Wall-clock budget for the out-of-process `INSTALL` child before it is killed. |
311
+ | `GITNEXUS_WAL_CHECKPOINT_THRESHOLD` | integer `>= -1` | `67108864` (64 MiB) | LadybugDB WAL auto-checkpoint threshold during analyze (bytes). Auto-checkpoint remains enabled; `-1` keeps Ladybug's stock ~16 MiB. Larger thresholds reduce checkpoint frequency but increase the WAL size at rotation time — choose a smaller value on disk-constrained environments. |
310
312
 
311
313
  ```bash
312
314
  # Offline/airgapped: never reach the network for extensions
@@ -69,6 +69,8 @@ export interface AnalyzeOptions {
69
69
  maxFileSize?: string;
70
70
  /** Override worker sub-batch idle timeout in seconds. */
71
71
  workerTimeout?: string;
72
+ /** Control LadybugDB WAL auto-checkpoint threshold during analyze. */
73
+ walCheckpointThreshold?: string;
72
74
  /** Parse worker pool size; 0 disables workers (sequential fallback). */
73
75
  workers?: string;
74
76
  embeddingThreads?: string;
@@ -12,7 +12,7 @@ import { spawn } from 'child_process';
12
12
  import v8 from 'v8';
13
13
  import cliProgress from 'cli-progress';
14
14
  import { closeLbug } from '../core/lbug/lbug-adapter.js';
15
- import { isWalCorruptionError, WAL_RECOVERY_SUGGESTION } from '../core/lbug/lbug-config.js';
15
+ import { isLbugCheckpointIoError, isWalCorruptionError, parseWalCheckpointThreshold, WAL_RECOVERY_SUGGESTION, } from '../core/lbug/lbug-config.js';
16
16
  import { getStoragePaths, getGlobalRegistryPath, RegistryNameCollisionError, AnalysisNotFinalizedError, assertAnalysisFinalized, } from '../storage/repo-manager.js';
17
17
  import { getGitRoot, hasGitDir } from '../storage/git.js';
18
18
  import { runFullAnalysis } from '../core/run-analyze.js';
@@ -322,6 +322,14 @@ const forceHeapOOMForTestIfEnabled = () => {
322
322
  for (;;)
323
323
  chunks.push('x'.repeat(1024 * 1024));
324
324
  };
325
+ // 64 MiB keeps auto-checkpoint enabled but triggers less frequently than
326
+ // Ladybug's stock ~16 MiB threshold, reducing rename/remove churn on large
327
+ // runs. Also matches the GitNexus default in `lbug-config.ts`.
328
+ //
329
+ // IMPORTANT: keep README examples (`README.md`, `gitnexus/README.md`) and
330
+ // the `DEFAULT_WAL_CHECKPOINT_THRESHOLD` constant in
331
+ // `gitnexus/src/core/lbug/lbug-config.ts` in sync with this value.
332
+ const RECOMMENDED_WAL_CHECKPOINT_THRESHOLD = 64 * 1024 * 1024;
325
333
  /** Re-exec the process with a 16GB heap and larger stack if we're currently below that. */
326
334
  async function ensureHeap() {
327
335
  const nodeOpts = process.env.NODE_OPTIONS || '';
@@ -378,6 +386,8 @@ const ANALYZE_CLI_ENV_KEYS = [
378
386
  'GITNEXUS_PROFILE_DEFERRED_SLOW_MS',
379
387
  'GITNEXUS_MAX_FILE_SIZE',
380
388
  'GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS',
389
+ 'GITNEXUS_WAL_CHECKPOINT_THRESHOLD',
390
+ 'GITNEXUS_WAL_MANUAL_CHECKPOINT',
381
391
  'GITNEXUS_EMBEDDING_THREADS',
382
392
  'GITNEXUS_EMBEDDING_BATCH_SIZE',
383
393
  'GITNEXUS_EMBEDDING_SUB_BATCH_SIZE',
@@ -452,6 +462,15 @@ const analyzeCommandImpl = async (inputPath, options) => {
452
462
  }
453
463
  process.env.GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS = String(Math.round(workerTimeoutSeconds * 1000));
454
464
  }
465
+ if (options?.walCheckpointThreshold !== undefined) {
466
+ const parsed = parseWalCheckpointThreshold(options.walCheckpointThreshold);
467
+ if (parsed === undefined) {
468
+ cliError(' --wal-checkpoint-threshold must be an integer >= -1.\n');
469
+ process.exitCode = 1;
470
+ return;
471
+ }
472
+ process.env.GITNEXUS_WAL_CHECKPOINT_THRESHOLD = String(parsed);
473
+ }
455
474
  // `--workers` is threaded through `runFullAnalysis` options → PipelineOptions
456
475
  // → createWorkerPool, intentionally bypassing the GITNEXUS_WORKER_POOL_SIZE
457
476
  // env channel so this CLI surface never mutates `process.env` for pool size.
@@ -859,6 +878,16 @@ const analyzeCommandImpl = async (inputPath, options) => {
859
878
  process.exitCode = 1;
860
879
  return;
861
880
  }
881
+ if (isLbugCheckpointIoError(err)) {
882
+ cliError(` LadybugDB failed while rotating/removing WAL checkpoint files.\n` +
883
+ ` This can happen when auto-checkpoint runs at the default threshold (~16MB).\n` +
884
+ ` Retry with a larger checkpoint threshold to reduce checkpoint frequency:\n` +
885
+ ` gitnexus analyze --wal-checkpoint-threshold ${RECOMMENDED_WAL_CHECKPOINT_THRESHOLD}\n` +
886
+ ` (or set GITNEXUS_WAL_CHECKPOINT_THRESHOLD=${RECOMMENDED_WAL_CHECKPOINT_THRESHOLD})\n` +
887
+ ` (Try 33554432 = 32 MiB on small-disk / CI runners.)\n`, { recoveryHint: 'wal-checkpoint-threshold' });
888
+ process.exitCode = 1;
889
+ return;
890
+ }
862
891
  // HF download failure — show clean guidance without the raw stack trace.
863
892
  // Checked before writeFatalToStderr so the user sees one focused message
864
893
  // rather than a stack-trace dump followed by a second remediation block.
@@ -7,4 +7,5 @@
7
7
  export declare const cleanCommand: (options?: {
8
8
  force?: boolean;
9
9
  all?: boolean;
10
+ lbugSidecars?: boolean;
10
11
  }) => Promise<void>;
package/dist/cli/clean.js CHANGED
@@ -5,22 +5,52 @@
5
5
  * Also unregisters it from the global registry.
6
6
  */
7
7
  import fs from 'fs/promises';
8
+ import path from 'path';
8
9
  import { logger } from '../core/logger.js';
9
10
  import { findRepo, unregisterRepo, listRegisteredRepos, assertSafeStoragePath, UnsafeStoragePathError, } from '../storage/repo-manager.js';
11
+ import { cleanQuarantinedMissingShadowWals, inspectLbugSidecars, listQuarantinedMissingShadowWals, } from '../core/lbug/sidecar-recovery.js';
12
+ import { t } from './i18n/index.js';
10
13
  export const cleanCommand = async (options) => {
14
+ if (options?.lbugSidecars) {
15
+ const cwd = process.cwd();
16
+ const repo = await findRepo(cwd);
17
+ if (!repo) {
18
+ console.log(t('clean.notFoundHere'));
19
+ return;
20
+ }
21
+ const lbugPath = path.join(repo.storagePath, 'lbug');
22
+ const state = await inspectLbugSidecars(lbugPath);
23
+ const quarantined = await listQuarantinedMissingShadowWals(lbugPath);
24
+ console.log(t('clean.lbugSidecars.state', { state: state.kind }));
25
+ if (quarantined.length === 0) {
26
+ console.log(t('clean.lbugSidecars.none'));
27
+ return;
28
+ }
29
+ if (!options.force) {
30
+ console.log(t('clean.lbugSidecars.preview', { count: quarantined.length }));
31
+ for (const file of quarantined) {
32
+ console.log(` - ${file}`);
33
+ }
34
+ console.log(`\n${t('common.runForceConfirm')}`);
35
+ return;
36
+ }
37
+ const deleted = await cleanQuarantinedMissingShadowWals(lbugPath);
38
+ console.log(t('clean.lbugSidecars.deleted', { count: deleted.length }));
39
+ return;
40
+ }
11
41
  // --all flag: clean all indexed repos
12
42
  if (options?.all) {
13
43
  if (!options?.force) {
14
44
  const entries = await listRegisteredRepos();
15
45
  if (entries.length === 0) {
16
- console.log('No indexed repositories found.');
46
+ console.log(t('common.notIndexed'));
17
47
  return;
18
48
  }
19
- console.log(`This will delete GitNexus indexes for ${entries.length} repo(s):`);
49
+ console.log(t('clean.deleteAll', { count: entries.length }));
20
50
  for (const entry of entries) {
21
51
  console.log(` - ${entry.name} (${entry.path})`);
22
52
  }
23
- console.log('\nRun with --force to confirm deletion.');
53
+ console.log(`\n${t('common.runForceConfirm')}`);
24
54
  return;
25
55
  }
26
56
  const entries = await listRegisteredRepos();
@@ -46,7 +76,7 @@ export const cleanCommand = async (options) => {
46
76
  try {
47
77
  await fs.rm(entry.storagePath, { recursive: true, force: true });
48
78
  await unregisterRepo(entry.path);
49
- console.log(`Deleted: ${entry.name} (${entry.storagePath})`);
79
+ console.log(t('clean.deletedRepo', { name: entry.name, storagePath: entry.storagePath }));
50
80
  }
51
81
  catch (err) {
52
82
  logger.error({ err }, `Failed to delete ${entry.name}:`);
@@ -58,20 +88,20 @@ export const cleanCommand = async (options) => {
58
88
  const cwd = process.cwd();
59
89
  const repo = await findRepo(cwd);
60
90
  if (!repo) {
61
- console.log('No indexed repository found in this directory.');
91
+ console.log(t('clean.notFoundHere'));
62
92
  return;
63
93
  }
64
94
  const repoName = repo.repoPath.split(/[/\\]/).pop() || repo.repoPath;
65
95
  if (!options?.force) {
66
- console.log(`This will delete the GitNexus index for: ${repoName}`);
67
- console.log(` Path: ${repo.storagePath}`);
68
- console.log('\nRun with --force to confirm deletion.');
96
+ console.log(t('clean.deleteCurrent', { repoName }));
97
+ console.log(` ${t('common.path')}: ${repo.storagePath}`);
98
+ console.log(`\n${t('common.runForceConfirm')}`);
69
99
  return;
70
100
  }
71
101
  try {
72
102
  await fs.rm(repo.storagePath, { recursive: true, force: true });
73
103
  await unregisterRepo(repo.repoPath);
74
- console.log(`Deleted: ${repo.storagePath}`);
104
+ console.log(t('common.deleted', { target: repo.storagePath }));
75
105
  }
76
106
  catch (err) {
77
107
  logger.error({ err }, 'Failed to delete:');
@@ -1,15 +1,45 @@
1
+ import { type CliMessageKey, type CliMessageVars } from './i18n/index.js';
2
+ /**
3
+ * String-literal union of all `recoveryHint` tags emitted by the CLI.
4
+ *
5
+ * Centralized so a new recovery branch added in `analyze.ts` cannot land
6
+ * without updating this union — TypeScript will reject the unknown literal
7
+ * passed via `cliError({ recoveryHint: '...' })`. To add a new hint:
8
+ * 1. Add the tag string to this union.
9
+ * 2. Pass it as the `recoveryHint` field at the relevant `cliError`
10
+ * call site.
11
+ *
12
+ * Consumers can import this type to narrow log-record `recoveryHint`
13
+ * fields without restating the literal list.
14
+ */
15
+ export type RecoveryHint = 'wal-corruption' | 'wal-checkpoint-threshold' | 'heap-oom-respawn' | 'native-worker-abort' | 'hf-endpoint-unreachable' | 'large-repo' | 'npm-resolution' | 'module-not-found';
16
+ /**
17
+ * Common shape for the optional structured-field bag passed to
18
+ * `cliError`/`cliWarn`/`cliInfo`. Typed so the `recoveryHint` slot is
19
+ * checked against the {@link RecoveryHint} union.
20
+ */
21
+ export interface CliMessageFields extends Record<string, unknown> {
22
+ recoveryHint?: RecoveryHint;
23
+ }
1
24
  /**
2
25
  * User-facing informational message. Use for banners, listening URLs,
3
26
  * and any message the user expects to read in plain text.
4
27
  */
5
- export declare function cliInfo(msg: string, fields?: Record<string, unknown>): void;
28
+ export declare function cliInfo(msg: string, fields?: CliMessageFields): void;
29
+ /**
30
+ * Key-based informational message. Keeps the legacy string API intact while
31
+ * allowing commands to opt into localized user-facing stderr output.
32
+ */
33
+ export declare function cliInfoKey(key: CliMessageKey, vars?: CliMessageVars, fields?: Record<string, unknown>): void;
6
34
  /**
7
35
  * User-facing warning. Operator-actionable but non-fatal — `cliWarn`
8
36
  * indicates the command can still proceed in some form.
9
37
  */
10
- export declare function cliWarn(msg: string, fields?: Record<string, unknown>): void;
38
+ export declare function cliWarn(msg: string, fields?: CliMessageFields): void;
39
+ export declare function cliWarnKey(key: CliMessageKey, vars?: CliMessageVars, fields?: Record<string, unknown>): void;
11
40
  /**
12
41
  * User-facing error. Indicates the command cannot proceed; usually
13
42
  * paired with a non-zero exit code at the call site.
14
43
  */
15
- export declare function cliError(msg: string, fields?: Record<string, unknown>): void;
44
+ export declare function cliError(msg: string, fields?: CliMessageFields): void;
45
+ export declare function cliErrorKey(key: CliMessageKey, vars?: CliMessageVars, fields?: Record<string, unknown>): void;
@@ -28,6 +28,7 @@
28
28
  * stdout would corrupt that pipeline.
29
29
  */
30
30
  import { logger } from '../core/logger.js';
31
+ import { t } from './i18n/index.js';
31
32
  function writeStderr(msg) {
32
33
  // Direct write — bypassing `console.*` so it cannot be intercepted by
33
34
  // progress-bar redirection (see `cli/analyze.ts:barLog`) or other
@@ -43,6 +44,13 @@ export function cliInfo(msg, fields) {
43
44
  writeStderr(msg);
44
45
  logger.info(fields ?? {}, msg);
45
46
  }
47
+ /**
48
+ * Key-based informational message. Keeps the legacy string API intact while
49
+ * allowing commands to opt into localized user-facing stderr output.
50
+ */
51
+ export function cliInfoKey(key, vars, fields) {
52
+ cliInfo(t(key, vars), fields);
53
+ }
46
54
  /**
47
55
  * User-facing warning. Operator-actionable but non-fatal — `cliWarn`
48
56
  * indicates the command can still proceed in some form.
@@ -51,6 +59,9 @@ export function cliWarn(msg, fields) {
51
59
  writeStderr(msg);
52
60
  logger.warn(fields ?? {}, msg);
53
61
  }
62
+ export function cliWarnKey(key, vars, fields) {
63
+ cliWarn(t(key, vars), fields);
64
+ }
54
65
  /**
55
66
  * User-facing error. Indicates the command cannot proceed; usually
56
67
  * paired with a non-zero exit code at the call site.
@@ -59,3 +70,6 @@ export function cliError(msg, fields) {
59
70
  writeStderr(msg);
60
71
  logger.error(fields ?? {}, msg);
61
72
  }
73
+ export function cliErrorKey(key, vars, fields) {
74
+ cliError(t(key, vars), fields);
75
+ }
@@ -0,0 +1 @@
1
+ export declare function formatDetectChangesResult(result: unknown): string;
@@ -0,0 +1,45 @@
1
+ import { t } from './i18n/index.js';
2
+ export function formatDetectChangesResult(result) {
3
+ const payload = (result ?? {});
4
+ if (payload.error)
5
+ return t('common.error', { message: String(payload.error) });
6
+ const summary = payload.summary ?? {};
7
+ if ((summary.changed_count ?? 0) === 0) {
8
+ return t('tool.detectChanges.noChanges');
9
+ }
10
+ const lines = [];
11
+ lines.push(t('tool.detectChanges.changesSummary', {
12
+ files: summary.changed_files ?? 0,
13
+ symbols: summary.changed_count ?? 0,
14
+ }));
15
+ lines.push(t('tool.detectChanges.affectedProcesses', { count: summary.affected_count ?? 0 }));
16
+ lines.push(t('tool.detectChanges.riskLevel', {
17
+ risk: summary.risk_level || t('tool.detectChanges.unknownRisk'),
18
+ }));
19
+ lines.push('');
20
+ const changed = Array.isArray(payload.changed_symbols) ? payload.changed_symbols : [];
21
+ if (changed.length > 0) {
22
+ lines.push(t('tool.detectChanges.changedSymbols'));
23
+ for (const symbol of changed.slice(0, 15)) {
24
+ lines.push(` ${symbol.type ?? 'Symbol'} ${symbol.name ?? '?'} → ${symbol.filePath ?? '?'}`);
25
+ }
26
+ if (changed.length > 15) {
27
+ lines.push(t('tool.detectChanges.overflowMore', { count: changed.length - 15 }));
28
+ }
29
+ lines.push('');
30
+ }
31
+ const affected = Array.isArray(payload.affected_processes) ? payload.affected_processes : [];
32
+ if (affected.length > 0) {
33
+ lines.push(t('tool.detectChanges.affectedExecutionFlows'));
34
+ for (const processInfo of affected.slice(0, 10)) {
35
+ const changedSteps = Array.isArray(processInfo.changed_steps)
36
+ ? processInfo.changed_steps
37
+ : [];
38
+ const steps = changedSteps.map((step) => step.symbol ?? '?').join(', ');
39
+ lines.push(` • ${processInfo.name ?? '?'} (${t('tool.detectChanges.steps', {
40
+ count: processInfo.step_count ?? 0,
41
+ })}) — ${t('tool.detectChanges.changedSteps', { steps })}`);
42
+ }
43
+ }
44
+ return lines.join('\n').trim();
45
+ }
@@ -1 +1,3 @@
1
+ export declare function displayWidth(value: string): number;
2
+ export declare function padDisplayEnd(value: string, columns: number): string;
1
3
  export declare const doctorCommand: () => Promise<void>;
@@ -1,31 +1,70 @@
1
1
  import { getRuntimeCapabilities, getRuntimeFingerprint } from '../core/platform/capabilities.js';
2
2
  import { resolveEmbeddingConfig } from '../core/embeddings/config.js';
3
3
  import { isHttpMode } from '../core/embeddings/http-client.js';
4
+ import { t } from './i18n/index.js';
5
+ function isCombiningMark(codePoint) {
6
+ return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
7
+ (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
8
+ (codePoint >= 0x1dc0 && codePoint <= 0x1dff) ||
9
+ (codePoint >= 0x20d0 && codePoint <= 0x20ff) ||
10
+ (codePoint >= 0xfe20 && codePoint <= 0xfe2f));
11
+ }
12
+ function isWideCodePoint(codePoint) {
13
+ return ((codePoint >= 0x1100 && codePoint <= 0x115f) ||
14
+ codePoint === 0x2329 ||
15
+ codePoint === 0x232a ||
16
+ (codePoint >= 0x2e80 && codePoint <= 0xa4cf && codePoint !== 0x303f) ||
17
+ (codePoint >= 0xac00 && codePoint <= 0xd7a3) ||
18
+ (codePoint >= 0xf900 && codePoint <= 0xfaff) ||
19
+ (codePoint >= 0xfe10 && codePoint <= 0xfe19) ||
20
+ (codePoint >= 0xfe30 && codePoint <= 0xfe6f) ||
21
+ (codePoint >= 0xff00 && codePoint <= 0xff60) ||
22
+ (codePoint >= 0xffe0 && codePoint <= 0xffe6) ||
23
+ (codePoint >= 0x1f300 && codePoint <= 0x1f64f) ||
24
+ (codePoint >= 0x1f900 && codePoint <= 0x1f9ff) ||
25
+ (codePoint >= 0x20000 && codePoint <= 0x3fffd));
26
+ }
27
+ export function displayWidth(value) {
28
+ let width = 0;
29
+ for (const char of value) {
30
+ const codePoint = char.codePointAt(0);
31
+ if (codePoint === undefined || codePoint === 0)
32
+ continue;
33
+ if (isCombiningMark(codePoint))
34
+ continue;
35
+ width += isWideCodePoint(codePoint) ? 2 : 1;
36
+ }
37
+ return width;
38
+ }
39
+ export function padDisplayEnd(value, columns) {
40
+ return value + ' '.repeat(Math.max(0, columns - displayWidth(value)));
41
+ }
42
+ const label = (key, width) => padDisplayEnd(t(key), width);
4
43
  export const doctorCommand = async () => {
5
44
  const fingerprint = getRuntimeFingerprint();
6
45
  const capabilities = getRuntimeCapabilities();
7
46
  const embeddingConfig = resolveEmbeddingConfig();
8
- console.log('GitNexus Doctor\n');
9
- console.log('Runtime');
10
- console.log(` OS: ${fingerprint.platform}/${fingerprint.arch}`);
11
- console.log(` Node: ${fingerprint.node}`);
12
- console.log(` GitNexus: ${fingerprint.gitnexus}`);
13
- console.log(` LadybugDB: ${fingerprint.ladybugdb ?? 'unknown'}`);
14
- console.log(` ONNX: ${fingerprint.onnxruntime ?? 'unknown'}`);
47
+ console.log(t('doctor.title') + '\n');
48
+ console.log(t('doctor.runtime'));
49
+ console.log(` ${label('doctor.labels.os', 10)}${fingerprint.platform}/${fingerprint.arch}`);
50
+ console.log(` ${label('doctor.labels.node', 10)}${fingerprint.node}`);
51
+ console.log(` ${label('doctor.labels.gitnexus', 10)}${fingerprint.gitnexus}`);
52
+ console.log(` ${label('doctor.labels.ladybugdb', 10)}${fingerprint.ladybugdb ?? 'unknown'}`);
53
+ console.log(` ${label('doctor.labels.onnx', 10)}${fingerprint.onnxruntime ?? 'unknown'}`);
15
54
  console.log('');
16
- console.log('Capabilities');
17
- console.log(` Graph store: ${capabilities.graph}`);
18
- console.log(` Full-text search:${capabilities.fts.padStart(10)}`);
19
- console.log(` VECTOR index: ${capabilities.vector}`);
20
- console.log(` Semantic mode: ${capabilities.semanticMode}`);
21
- console.log(` Exact scan limit:${String(capabilities.exactScanLimit).padStart(9)} chunks`);
55
+ console.log(t('doctor.capabilities'));
56
+ console.log(` ${label('doctor.labels.graphStore', 18)}${capabilities.graph}`);
57
+ console.log(` ${label('doctor.labels.fullTextSearch', 18)}${capabilities.fts}`);
58
+ console.log(` ${label('doctor.labels.vectorIndex', 18)}${capabilities.vector}`);
59
+ console.log(` ${label('doctor.labels.semanticMode', 18)}${capabilities.semanticMode}`);
60
+ console.log(` ${label('doctor.labels.exactScanLimit', 18)}${t('doctor.chunks', { count: capabilities.exactScanLimit })}`);
22
61
  if (capabilities.reason)
23
- console.log(` Note: ${capabilities.reason}`);
62
+ console.log(` ${label('doctor.labels.note', 18)}${capabilities.reason}`);
24
63
  console.log('');
25
- console.log('Embeddings');
26
- console.log(` Backend: ${isHttpMode() ? 'http' : 'local'}`);
27
- console.log(` Device: ${embeddingConfig.device}`);
28
- console.log(` Threads: ${embeddingConfig.threads}`);
29
- console.log(` Batch: ${embeddingConfig.batchSize} nodes`);
30
- console.log(` Sub-batch: ${embeddingConfig.subBatchSize} chunks`);
64
+ console.log(t('doctor.embeddings'));
65
+ console.log(` ${label('doctor.labels.backend', 12)}${isHttpMode() ? 'http' : 'local'}`);
66
+ console.log(` ${label('doctor.labels.device', 12)}${embeddingConfig.device}`);
67
+ console.log(` ${label('doctor.labels.threads', 12)}${embeddingConfig.threads}`);
68
+ console.log(` ${label('doctor.labels.batch', 12)}${t('doctor.nodes', { count: embeddingConfig.batchSize })}`);
69
+ console.log(` ${label('doctor.labels.subBatch', 12)}${t('doctor.chunks', { count: embeddingConfig.subBatchSize })}`);
31
70
  };
@@ -28,6 +28,7 @@
28
28
  * GET /health — Health check. Returns {"status":"ok","repos":[...]}
29
29
  * POST /shutdown — Graceful shutdown.
30
30
  */
31
+ export { formatDetectChangesResult } from './detect-changes-format.js';
31
32
  export interface EvalServerOptions {
32
33
  port?: string;
33
34
  host?: string;
@@ -44,7 +45,6 @@ export declare function formatQueryResult(result: any): string;
44
45
  export declare function formatContextResult(result: any): string;
45
46
  export declare function formatImpactResult(result: any): string;
46
47
  export declare function formatCypherResult(result: any): string;
47
- export declare function formatDetectChangesResult(result: any): string;
48
48
  export declare function formatListReposResult(result: any): string;
49
49
  export declare function evalServerCommand(options?: EvalServerOptions): Promise<void>;
50
50
  export declare const MAX_BODY_SIZE: number;
@@ -34,6 +34,8 @@ import { writeSync } from 'node:fs';
34
34
  import { LocalBackend } from '../mcp/local/local-backend.js';
35
35
  import { logger } from '../core/logger.js';
36
36
  import { cliInfo, cliWarn, cliError } from './cli-message.js';
37
+ import { formatDetectChangesResult } from './detect-changes-format.js';
38
+ export { formatDetectChangesResult } from './detect-changes-format.js';
37
39
  /**
38
40
  * Validate the --host value. Accepts IPv4, IPv6, or "localhost".
39
41
  * Returns the host string unchanged, or null if invalid.
@@ -204,37 +206,6 @@ export function formatCypherResult(result) {
204
206
  }
205
207
  return typeof result === 'string' ? result : JSON.stringify(result, null, 2);
206
208
  }
207
- export function formatDetectChangesResult(result) {
208
- if (result.error)
209
- return `Error: ${result.error}`;
210
- const summary = result.summary || {};
211
- const lines = [];
212
- if (summary.changed_count === 0) {
213
- return 'No changes detected.';
214
- }
215
- lines.push(`Changes: ${summary.changed_files || 0} files, ${summary.changed_count || 0} symbols`);
216
- lines.push(`Affected processes: ${summary.affected_count || 0}`);
217
- lines.push(`Risk level: ${summary.risk_level || 'unknown'}\n`);
218
- const changed = result.changed_symbols || [];
219
- if (changed.length > 0) {
220
- lines.push(`Changed symbols:`);
221
- for (const s of changed.slice(0, 15)) {
222
- lines.push(` ${s.type} ${s.name} → ${s.filePath}`);
223
- }
224
- if (changed.length > 15)
225
- lines.push(` ... and ${changed.length - 15} more`);
226
- lines.push('');
227
- }
228
- const affected = result.affected_processes || [];
229
- if (affected.length > 0) {
230
- lines.push(`Affected execution flows:`);
231
- for (const p of affected.slice(0, 10)) {
232
- const steps = (p.changed_steps || []).map((s) => s.symbol).join(', ');
233
- lines.push(` • ${p.name} (${p.step_count} steps) — changed: ${steps}`);
234
- }
235
- }
236
- return lines.join('\n').trim();
237
- }
238
209
  export function formatListReposResult(result) {
239
210
  if (!Array.isArray(result) || result.length === 0) {
240
211
  return 'No indexed repositories.';
@@ -0,0 +1,2 @@
1
+ import type { Command } from 'commander';
2
+ export declare function localizeCliHelp(program: Command): Command;