gitnexus 1.6.11-rc.48 → 1.6.11-rc.49

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 (46) hide show
  1. package/README.md +35 -0
  2. package/dist/cli/analyze.js +0 -1
  3. package/dist/cli/command-banner.d.ts +15 -0
  4. package/dist/cli/command-banner.js +44 -0
  5. package/dist/cli/doctor.d.ts +7 -0
  6. package/dist/cli/doctor.js +14 -0
  7. package/dist/cli/help-i18n.js +1 -0
  8. package/dist/cli/i18n/en.d.ts +8 -0
  9. package/dist/cli/i18n/en.js +8 -0
  10. package/dist/cli/i18n/resources.d.ts +16 -0
  11. package/dist/cli/i18n/zh-CN.d.ts +8 -0
  12. package/dist/cli/i18n/zh-CN.js +8 -0
  13. package/dist/cli/index.js +14 -0
  14. package/dist/cli/mcp.d.ts +15 -0
  15. package/dist/cli/mcp.js +44 -0
  16. package/dist/cli/update-notice.d.ts +34 -0
  17. package/dist/cli/update-notice.js +91 -0
  18. package/dist/cli/update.d.ts +21 -0
  19. package/dist/cli/update.js +95 -0
  20. package/dist/core/group/storage.js +2 -2
  21. package/dist/core/install-context.d.ts +4 -0
  22. package/dist/core/install-context.js +152 -0
  23. package/dist/core/net/url-guard.d.ts +6 -0
  24. package/dist/core/net/url-guard.js +147 -0
  25. package/dist/core/update-cache.d.ts +39 -0
  26. package/dist/core/update-cache.js +141 -0
  27. package/dist/core/update-check.d.ts +31 -0
  28. package/dist/core/update-check.js +276 -0
  29. package/dist/server/api.d.ts +1 -1
  30. package/dist/server/api.js +7 -18
  31. package/dist/server/git-clone.d.ts +2 -7
  32. package/dist/server/git-clone.js +2 -146
  33. package/dist/server/update-controller.d.ts +29 -0
  34. package/dist/server/update-controller.js +84 -0
  35. package/dist/storage/global-dir.d.ts +2 -0
  36. package/dist/storage/global-dir.js +6 -0
  37. package/dist/storage/repo-manager.d.ts +1 -4
  38. package/dist/storage/repo-manager.js +2 -7
  39. package/package.json +1 -1
  40. package/scripts/cross-platform-tests.ts +2 -0
  41. package/web/assets/{agent-CsQH9GJv.js → agent-BMRqXorA.js} +1 -1
  42. package/web/assets/{index-CJDpyH-n.css → index-C2_aej58.css} +1 -1
  43. package/web/assets/{index-TqwlBESG.js → index-Dj3vQOHK.js} +51 -51
  44. package/web/assets/src-Df5C1Nz4.js +1 -0
  45. package/web/index.html +3 -3
  46. package/web/assets/src-sSE475bl.js +0 -1
package/README.md CHANGED
@@ -264,6 +264,7 @@ gitnexus wiki --provider grok # Local Grok Build CLI (uses `grok login`, no A
264
264
  gitnexus wiki --base-url http://llama-box.local:8080/v1 --allow-insecure-connection llama-box.local
265
265
  # Allow an exact LAN/self-hosted HTTP LLM host; env: GITNEXUS_ALLOW_INSECURE_CONNECTION
266
266
  gitnexus doctor # Show runtime platform capabilities and embedding configuration
267
+ gitnexus update # Install the latest published GitNexus (`npm i -g gitnexus@<x.y.z>`)
267
268
 
268
269
  # Direct graph queries — the same tools the MCP server exposes, no MCP daemon needed
269
270
  gitnexus query "<concept>" # Process-grouped hybrid search
@@ -492,6 +493,40 @@ bigger cycle) and `N` increments per published rc. Example sequence:
492
493
  `1.6.3-rc.1`. See the [Releases page](https://github.com/abhigyanpatwari/GitNexus/releases)
493
494
  for the full list; stable `latest` is unaffected.
494
495
 
496
+ ## Update notifications
497
+
498
+ GitNexus checks the npm registry's `latest` dist-tag at most once every 24
499
+ hours per installation and tells you when a newer stable version exists. The
500
+ result is cached under `$GITNEXUS_HOME` (`~/.gitnexus` by default), so the
501
+ check never runs on the command's hot path and never blocks output. Where the
502
+ notice appears:
503
+
504
+ - **CLI** — one line on stderr when you run a command interactively (never on
505
+ stdout, so `gitnexus query … | jq` and other piped output stay clean), a
506
+ line in `gitnexus doctor` when an update is known. Automatic notices never
507
+ install. `gitnexus update` checks even when notices are opted out, then
508
+ runs `npm i -g gitnexus@<x.y.z>` (same idea as `claude update` /
509
+ `codex update`).
510
+ - **MCP server** — one structured log record on the server's stderr per
511
+ process per version (visible in your host's MCP log panel). Tool results,
512
+ resources, prompts, and server instructions never carry update text.
513
+ - **Web UI** — a dismissible banner when the server reports a newer version;
514
+ dismissal persists per version.
515
+
516
+ The check is skipped entirely (no network request, no output) when `CI` is
517
+ truthy, when the install is not an npm global/local install (npx cache, dev
518
+ checkout, Docker image — the Docker CLI image sets the opt-out itself), or
519
+ when opted out:
520
+
521
+ | Variable | Effect |
522
+ | --- | --- |
523
+ | `GITNEXUS_NO_UPDATE_NOTIFIER` | Truthy (`1`, `true`, …) disables the update check on every surface. |
524
+ | `NO_UPDATE_NOTIFIER` | Cross-tool convention; honored the same way. |
525
+ | `npm_config_registry` | The check reads the `latest` dist-tag from this registry instead of `https://registry.npmjs.org`. Credentials are never sent, and registries that require authentication are not supported (the check silently skips). |
526
+
527
+ Eval harnesses running a global install can set `GITNEXUS_NO_UPDATE_NOTIFIER`
528
+ for a quiet registry.
529
+
495
530
  ## Troubleshooting
496
531
 
497
532
  ### `Cannot destructure property 'package' of 'node.target' as it is null`
@@ -609,7 +609,6 @@ export async function analyzeOrWatchCommandWithRunnerIdentity(runnerIdentityAtBo
609
609
  await analyzeCommandWithRunnerIdentity(runnerIdentityAtBootstrap, inputPath, options);
610
610
  }
611
611
  const analyzeCommandImpl = async (inputPath, cliOptions, runnerIdentityAtBootstrap) => {
612
- console.log('\n GitNexus Analyzer\n');
613
612
  // ── Resolve the target repo root ──────────────────────────────────
614
613
  // Resolved FIRST because `.gitnexusrc` is read from the repo root (not the
615
614
  // caller's cwd), and config can set defaults that the validation below
@@ -0,0 +1,15 @@
1
+ /**
2
+ * One-line identity printed before every CLI action:
3
+ * `GitNexus Analyzer (1.6.10)`. Goes to stderr so stdout stays
4
+ * pipe/JSON-safe (`gitnexus query … | jq`, `status --json`).
5
+ */
6
+ import type { Command } from 'commander';
7
+ export declare function installedCliVersion(): string;
8
+ export declare function commandDisplayName(name: string): string;
9
+ export declare function commandBannerTitle(command: Command): string;
10
+ export declare function formatCommandBanner(title: string, version?: string): string;
11
+ export interface WriteCommandBannerDependencies {
12
+ write?: (text: string) => void;
13
+ version?: string;
14
+ }
15
+ export declare function writeCommandBanner(command: Command, deps?: WriteCommandBannerDependencies): void;
@@ -0,0 +1,44 @@
1
+ /**
2
+ * One-line identity printed before every CLI action:
3
+ * `GitNexus Analyzer (1.6.10)`. Goes to stderr so stdout stays
4
+ * pipe/JSON-safe (`gitnexus query … | jq`, `status --json`).
5
+ */
6
+ import { createRequire } from 'node:module';
7
+ const _require = createRequire(import.meta.url);
8
+ const pkg = _require('../../package.json');
9
+ const SKIP_COMMAND_BANNER = new Set(['__update-check', 'help']);
10
+ const SPECIAL_TITLES = {
11
+ analyze: 'Analyzer',
12
+ mcp: 'MCP',
13
+ };
14
+ export function installedCliVersion() {
15
+ return typeof pkg.version === 'string' ? pkg.version : '';
16
+ }
17
+ export function commandDisplayName(name) {
18
+ return (SPECIAL_TITLES[name] ??
19
+ name
20
+ .split('-')
21
+ .filter(Boolean)
22
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
23
+ .join(' '));
24
+ }
25
+ export function commandBannerTitle(command) {
26
+ const names = [];
27
+ for (let current = command; current?.parent; current = current.parent) {
28
+ names.unshift(current.name());
29
+ }
30
+ return names.map(commandDisplayName).join(' ');
31
+ }
32
+ export function formatCommandBanner(title, version = installedCliVersion()) {
33
+ if (!title)
34
+ return '';
35
+ return version ? `\n GitNexus ${title} (${version})\n` : `\n GitNexus ${title}\n`;
36
+ }
37
+ export function writeCommandBanner(command, deps = {}) {
38
+ if (SKIP_COMMAND_BANNER.has(command.name()))
39
+ return;
40
+ const text = formatCommandBanner(commandBannerTitle(command), deps.version ?? installedCliVersion());
41
+ if (!text)
42
+ return;
43
+ (deps.write ?? ((line) => process.stderr.write(line)))(text);
44
+ }
@@ -1,5 +1,6 @@
1
1
  import { type EmbeddingRuntimeResolution } from '../core/embeddings/runtime-install.js';
2
2
  import { type NativeCheckResult } from '../core/lbug/native-check.js';
3
+ import { type ValidatedUpdateCache } from '../core/update-cache.js';
3
4
  export declare function displayWidth(value: string): number;
4
5
  export declare function padDisplayEnd(value: string, columns: number): string;
5
6
  /**
@@ -59,4 +60,10 @@ export declare function poolSizeDoctorLine(pool: number, envRaw: string | undefi
59
60
  * stderr right below said the opposite. Render what the check actually found.
60
61
  */
61
62
  export declare function nativeStatusLine(check: NativeCheckResult): string;
63
+ export declare function cachedUpdateDoctorLine(options: {
64
+ installedVersion: string;
65
+ eligible: boolean;
66
+ env: NodeJS.ProcessEnv;
67
+ readCache: () => ValidatedUpdateCache | null;
68
+ }): string | null;
62
69
  export declare const doctorCommand: () => Promise<void>;
@@ -8,7 +8,10 @@ import { checkLbugNative, probeFtsExtensionLoad, probeVectorExtensionLoad, } fro
8
8
  import { getEffectiveBufferPoolSize, getOsPageSize, isPageSizeAwareLadybug, } from '../core/lbug/lbug-config.js';
9
9
  import { diagnoseExtensionLoad } from '../core/lbug/extension-load-error.js';
10
10
  import { getExtensionInstallPolicy } from '../core/lbug/extension-loader.js';
11
+ import { updateEligibleInstallSync } from '../core/install-context.js';
12
+ import { readValidatedUpdateCacheSync } from '../core/update-cache.js';
11
13
  import { t } from './i18n/index.js';
14
+ import { cachedUpdateNoticeLine } from './update-notice.js';
12
15
  function isCombiningMark(codePoint) {
13
16
  return ((codePoint >= 0x0300 && codePoint <= 0x036f) ||
14
17
  (codePoint >= 0x1ab0 && codePoint <= 0x1aff) ||
@@ -160,6 +163,9 @@ function nativeStatusText(check) {
160
163
  return '✗ lbugjs.node missing';
161
164
  }
162
165
  }
166
+ export function cachedUpdateDoctorLine(options) {
167
+ return cachedUpdateNoticeLine(options);
168
+ }
163
169
  export const doctorCommand = async () => {
164
170
  const fingerprint = getRuntimeFingerprint();
165
171
  const capabilities = getRuntimeCapabilities();
@@ -169,6 +175,14 @@ export const doctorCommand = async () => {
169
175
  console.log(` ${label('doctor.labels.os', 10)}${fingerprint.platform}/${fingerprint.arch}`);
170
176
  console.log(` ${label('doctor.labels.node', 10)}${fingerprint.node}`);
171
177
  console.log(` ${label('doctor.labels.gitnexus', 10)}${fingerprint.gitnexus}`);
178
+ const updateLine = cachedUpdateDoctorLine({
179
+ installedVersion: fingerprint.gitnexus,
180
+ eligible: updateEligibleInstallSync(),
181
+ env: process.env,
182
+ readCache: () => readValidatedUpdateCacheSync(),
183
+ });
184
+ if (updateLine)
185
+ console.log(` ${updateLine}`);
172
186
  console.log(` ${label('doctor.labels.ladybugdb', 10)}${fingerprint.ladybugdb ?? 'unknown'}`);
173
187
  // OS page size next to the LadybugDB version because the two interact:
174
188
  // @ladybugdb/core < 0.18.0 assumed 4 KiB pages in its buffer manager and
@@ -19,6 +19,7 @@ const COMMAND_DESCRIPTION_KEYS = {
19
19
  list: 'help.command.list.description',
20
20
  status: 'help.command.status.description',
21
21
  doctor: 'help.command.doctor.description',
22
+ update: 'help.command.update.description',
22
23
  embeddings: 'help.command.embeddings.description',
23
24
  'embeddings install': 'help.command.embeddings.install.description',
24
25
  clean: 'help.command.clean.description',
@@ -7,6 +7,13 @@ export declare const en: {
7
7
  readonly 'common.storage': "Storage";
8
8
  readonly 'common.deleted': "Deleted: {{target}}";
9
9
  readonly 'common.error': "Error: {{message}}";
10
+ readonly 'update.available': "GitNexus {{latestVersion}} is available (you are running {{installedVersion}}).";
11
+ readonly 'update.current': "GitNexus {{installedVersion}} is current or newer than the latest stable version.";
12
+ readonly 'update.installing': "Installing with {{command}}…";
13
+ readonly 'update.installed': "Installed gitnexus@{{version}}. Restart long-running mcp/serve processes.";
14
+ readonly 'update.installFailed': "npm install failed. You can retry: {{command}}";
15
+ readonly 'update.installError': "Could not run npm: {{message}}";
16
+ readonly 'update.checkFailed': "Could not check for updates (offline, private registry, or the check failed open).";
10
17
  readonly 'list.title': "Indexed Repositories ({{count}})";
11
18
  readonly 'list.indexed': "Indexed";
12
19
  readonly 'list.commit': "Commit";
@@ -137,6 +144,7 @@ export declare const en: {
137
144
  readonly 'help.command.list.description': "List all indexed repositories";
138
145
  readonly 'help.command.status.description': "Show index status for current repo";
139
146
  readonly 'help.command.doctor.description': "Show runtime platform capabilities and embedding configuration";
147
+ readonly 'help.command.update.description': "Install the latest published GitNexus globally (`npm i -g gitnexus@<x.y.z>`).";
140
148
  readonly 'help.command.embeddings.description': "Manage the on-demand local embedding runtime";
141
149
  readonly 'help.command.embeddings.install.description': "Install the local embedding stack (@huggingface/transformers + onnxruntime-node) on demand. Heals installs where npm skipped the optional packages (e.g. behind an HTTP proxy, #2370). Downloads only from your configured npm registry — mirrors and proxies apply.";
142
150
  readonly 'help.command.clean.description': "Delete GitNexus index for current repo";
@@ -7,6 +7,13 @@ export const en = {
7
7
  'common.storage': 'Storage',
8
8
  'common.deleted': 'Deleted: {{target}}',
9
9
  'common.error': 'Error: {{message}}',
10
+ 'update.available': 'GitNexus {{latestVersion}} is available (you are running {{installedVersion}}).',
11
+ 'update.current': 'GitNexus {{installedVersion}} is current or newer than the latest stable version.',
12
+ 'update.installing': 'Installing with {{command}}…',
13
+ 'update.installed': 'Installed gitnexus@{{version}}. Restart long-running mcp/serve processes.',
14
+ 'update.installFailed': 'npm install failed. You can retry: {{command}}',
15
+ 'update.installError': 'Could not run npm: {{message}}',
16
+ 'update.checkFailed': 'Could not check for updates (offline, private registry, or the check failed open).',
10
17
  'list.title': 'Indexed Repositories ({{count}})',
11
18
  'list.indexed': 'Indexed',
12
19
  'list.commit': 'Commit',
@@ -139,6 +146,7 @@ export const en = {
139
146
  'help.command.list.description': 'List all indexed repositories',
140
147
  'help.command.status.description': 'Show index status for current repo',
141
148
  'help.command.doctor.description': 'Show runtime platform capabilities and embedding configuration',
149
+ 'help.command.update.description': 'Install the latest published GitNexus globally (`npm i -g gitnexus@<x.y.z>`).',
142
150
  'help.command.embeddings.description': 'Manage the on-demand local embedding runtime',
143
151
  'help.command.embeddings.install.description': 'Install the local embedding stack (@huggingface/transformers + onnxruntime-node) on demand. Heals installs where npm skipped the optional packages (e.g. behind an HTTP proxy, #2370). Downloads only from your configured npm registry — mirrors and proxies apply.',
144
152
  'help.command.clean.description': 'Delete GitNexus index for current repo',
@@ -8,6 +8,13 @@ export declare const cliResources: {
8
8
  readonly 'common.storage': "Storage";
9
9
  readonly 'common.deleted': "Deleted: {{target}}";
10
10
  readonly 'common.error': "Error: {{message}}";
11
+ readonly 'update.available': "GitNexus {{latestVersion}} is available (you are running {{installedVersion}}).";
12
+ readonly 'update.current': "GitNexus {{installedVersion}} is current or newer than the latest stable version.";
13
+ readonly 'update.installing': "Installing with {{command}}…";
14
+ readonly 'update.installed': "Installed gitnexus@{{version}}. Restart long-running mcp/serve processes.";
15
+ readonly 'update.installFailed': "npm install failed. You can retry: {{command}}";
16
+ readonly 'update.installError': "Could not run npm: {{message}}";
17
+ readonly 'update.checkFailed': "Could not check for updates (offline, private registry, or the check failed open).";
11
18
  readonly 'list.title': "Indexed Repositories ({{count}})";
12
19
  readonly 'list.indexed': "Indexed";
13
20
  readonly 'list.commit': "Commit";
@@ -138,6 +145,7 @@ export declare const cliResources: {
138
145
  readonly 'help.command.list.description': "List all indexed repositories";
139
146
  readonly 'help.command.status.description': "Show index status for current repo";
140
147
  readonly 'help.command.doctor.description': "Show runtime platform capabilities and embedding configuration";
148
+ readonly 'help.command.update.description': "Install the latest published GitNexus globally (`npm i -g gitnexus@<x.y.z>`).";
141
149
  readonly 'help.command.embeddings.description': "Manage the on-demand local embedding runtime";
142
150
  readonly 'help.command.embeddings.install.description': "Install the local embedding stack (@huggingface/transformers + onnxruntime-node) on demand. Heals installs where npm skipped the optional packages (e.g. behind an HTTP proxy, #2370). Downloads only from your configured npm registry — mirrors and proxies apply.";
143
151
  readonly 'help.command.clean.description': "Delete GitNexus index for current repo";
@@ -279,6 +287,13 @@ export declare const cliResources: {
279
287
  'common.storage': string;
280
288
  'common.deleted': string;
281
289
  'common.error': string;
290
+ 'update.available': string;
291
+ 'update.current': string;
292
+ 'update.installing': string;
293
+ 'update.installed': string;
294
+ 'update.installFailed': string;
295
+ 'update.installError': string;
296
+ 'update.checkFailed': string;
282
297
  'list.title': string;
283
298
  'list.indexed': string;
284
299
  'list.commit': string;
@@ -409,6 +424,7 @@ export declare const cliResources: {
409
424
  'help.command.list.description': string;
410
425
  'help.command.status.description': string;
411
426
  'help.command.doctor.description': string;
427
+ 'help.command.update.description': string;
412
428
  'help.command.embeddings.description': string;
413
429
  'help.command.embeddings.install.description': string;
414
430
  'help.command.clean.description': string;
@@ -7,6 +7,13 @@ export declare const zhCN: {
7
7
  'common.storage': string;
8
8
  'common.deleted': string;
9
9
  'common.error': string;
10
+ 'update.available': string;
11
+ 'update.current': string;
12
+ 'update.installing': string;
13
+ 'update.installed': string;
14
+ 'update.installFailed': string;
15
+ 'update.installError': string;
16
+ 'update.checkFailed': string;
10
17
  'list.title': string;
11
18
  'list.indexed': string;
12
19
  'list.commit': string;
@@ -137,6 +144,7 @@ export declare const zhCN: {
137
144
  'help.command.list.description': string;
138
145
  'help.command.status.description': string;
139
146
  'help.command.doctor.description': string;
147
+ 'help.command.update.description': string;
140
148
  'help.command.embeddings.description': string;
141
149
  'help.command.embeddings.install.description': string;
142
150
  'help.command.clean.description': string;
@@ -7,6 +7,13 @@ export const zhCN = {
7
7
  'common.storage': '存储',
8
8
  'common.deleted': '已删除:{{target}}',
9
9
  'common.error': '错误:{{message}}',
10
+ 'update.available': 'GitNexus {{latestVersion}} 已发布(当前运行 {{installedVersion}})。',
11
+ 'update.current': 'GitNexus {{installedVersion}} 已是最新稳定版或不低于该版本。',
12
+ 'update.installing': '正在执行 {{command}}…',
13
+ 'update.installed': '已安装 gitnexus@{{version}}。请重启仍在运行的 mcp/serve 进程。',
14
+ 'update.installFailed': 'npm 安装失败。可重试:{{command}}',
15
+ 'update.installError': '无法运行 npm:{{message}}',
16
+ 'update.checkFailed': '无法检查更新(离线、私有仓库,或检查失败)。',
10
17
  'list.title': '已索引仓库({{count}})',
11
18
  'list.indexed': '索引时间',
12
19
  'list.commit': '提交',
@@ -137,6 +144,7 @@ export const zhCN = {
137
144
  'help.command.list.description': '列出所有已索引仓库',
138
145
  'help.command.status.description': '显示当前仓库的索引状态',
139
146
  'help.command.doctor.description': '显示运行平台能力和嵌入配置',
147
+ 'help.command.update.description': '通过 npm 全局安装最新发布的 GitNexus(`npm i -g gitnexus@<x.y.z>`)。',
140
148
  'help.command.embeddings.description': '管理按需安装的本地嵌入运行时',
141
149
  'help.command.embeddings.install.description': '按需安装本地嵌入组件(@huggingface/transformers + onnxruntime-node)。修复 npm 跳过可选包的安装(例如在 HTTP 代理后,#2370)。仅从你配置的 npm registry 下载 — 镜像和代理均生效。',
142
150
  'help.command.clean.description': '删除当前仓库的 GitNexus 索引',
package/dist/cli/index.js CHANGED
@@ -8,6 +8,8 @@ import { EMBEDDING_DIMS_ERROR, normalizeEmbeddingDims } from './embedding-dims.j
8
8
  import { registerGroupCommands } from './group.js';
9
9
  import { localizeCliHelp } from './help-i18n.js';
10
10
  import { t } from './i18n/index.js';
11
+ import { writeCommandBanner } from './command-banner.js';
12
+ import { runProcessCliUpdateNotice } from './update-notice.js';
11
13
  const _require = createRequire(import.meta.url);
12
14
  const pkg = _require('../../package.json');
13
15
  const program = new Command();
@@ -180,6 +182,10 @@ program
180
182
  .command('doctor')
181
183
  .description('Show runtime platform capabilities and embedding configuration')
182
184
  .action(createLazyAction(() => import('./doctor.js'), 'doctorCommand'));
185
+ program
186
+ .command('update')
187
+ .description('Install the latest published GitNexus globally (`npm i -g gitnexus@<x.y.z>`).')
188
+ .action(createLazyAction(() => import('./update.js'), 'updateCommand'));
183
189
  program
184
190
  .command('embeddings')
185
191
  .description('Manage the on-demand local embedding runtime')
@@ -324,6 +330,14 @@ program
324
330
  .option('--host <host>', 'Bind address or resolvable hostname (default: 127.0.0.1; non-loopback requires GITNEXUS_AUTH_TOKEN; hostnames resolve to IPv4)')
325
331
  .option('--idle-timeout <seconds>', 'Auto-shutdown after N seconds idle (0 = disabled)', '0')
326
332
  .action(createLbugLazyAction(() => import('./eval-server.js'), 'evalServerCommand'));
333
+ program.command('__update-check', { hidden: true }).action(async () => {
334
+ const { refresh } = await import('../core/update-check.js');
335
+ await refresh();
336
+ });
327
337
  registerGroupCommands(program);
328
338
  localizeCliHelp(program);
339
+ program.hook('preAction', (_thisCommand, actionCommand) => {
340
+ writeCommandBanner(actionCommand);
341
+ });
342
+ runProcessCliUpdateNotice(typeof pkg.version === 'string' ? pkg.version : '');
329
343
  program.parse(process.argv);
package/dist/cli/mcp.d.ts CHANGED
@@ -26,9 +26,24 @@
26
26
  * `mcpCommand` after the sentinel install. The regression test at
27
27
  * `gitnexus/test/integration/mcp/import-closure.test.ts` enforces this.
28
28
  */
29
+ import type { UpdateState } from '../core/update-check.js';
30
+ interface McpUpdateLogger {
31
+ info(bindings: Record<string, unknown>, message: string): unknown;
32
+ }
33
+ interface McpUpdateChecker {
34
+ evaluate(): Promise<UpdateState | null>;
35
+ armUpdateRefreshScheduler(onState: (state: UpdateState | null) => void): () => void;
36
+ }
37
+ type LoadMcpUpdateChecker = () => Promise<McpUpdateChecker>;
38
+ /**
39
+ * Start the process-scoped MCP update adapter after its transport startup
40
+ * boundary. All failures stay inside this best-effort side channel.
41
+ */
42
+ export declare function startMcpUpdateNotifier(logger: McpUpdateLogger, loadChecker?: LoadMcpUpdateChecker): Promise<void>;
29
43
  export declare const mcpCommand: (options?: {
30
44
  http?: boolean;
31
45
  port?: string;
32
46
  host?: string;
33
47
  authToken?: string;
34
48
  }) => Promise<void>;
49
+ export {};
package/dist/cli/mcp.js CHANGED
@@ -27,6 +27,48 @@
27
27
  * `gitnexus/test/integration/mcp/import-closure.test.ts` enforces this.
28
28
  */
29
29
  import { installGlobalStdoutSentinel } from '../mcp/stdio-context.js';
30
+ const announcedUpdateVersions = new Set();
31
+ /**
32
+ * Start the process-scoped MCP update adapter after its transport startup
33
+ * boundary. All failures stay inside this best-effort side channel.
34
+ */
35
+ export async function startMcpUpdateNotifier(logger, loadChecker = () => import('../core/update-check.js')) {
36
+ let checker;
37
+ try {
38
+ checker = await loadChecker();
39
+ }
40
+ catch {
41
+ return;
42
+ }
43
+ const announce = (state) => {
44
+ try {
45
+ if (!state?.updateAvailable ||
46
+ !state.latestVersion ||
47
+ announcedUpdateVersions.has(state.latestVersion)) {
48
+ return;
49
+ }
50
+ announcedUpdateVersions.add(state.latestVersion);
51
+ logger.info({ event: 'gitnexus.update_available', latestVersion: state.latestVersion }, 'GitNexus update available');
52
+ }
53
+ catch {
54
+ // Logging must never escape into MCP startup or scheduler promises.
55
+ }
56
+ };
57
+ try {
58
+ announce(await checker.evaluate());
59
+ }
60
+ catch {
61
+ // Cache evaluation and any detached refresh are best-effort.
62
+ }
63
+ let stop;
64
+ try {
65
+ stop = checker.armUpdateRefreshScheduler(announce);
66
+ }
67
+ catch {
68
+ return;
69
+ }
70
+ process.once('exit', () => stop?.());
71
+ }
30
72
  export const mcpCommand = async (options) => {
31
73
  // Install the global stdout sentinel as the very first thing — before
32
74
  // ANY other module loads. The static-import closure above is leaf-only
@@ -94,8 +136,10 @@ export const mcpCommand = async (options) => {
94
136
  logger.error({ err: err instanceof Error ? err.message : err }, 'Failed to start the MCP HTTP server');
95
137
  process.exit(1);
96
138
  }
139
+ void startMcpUpdateNotifier(logger).catch(() => { });
97
140
  return;
98
141
  }
99
142
  // Start MCP server (serves all repos, discovers new ones lazily)
100
143
  await startMCPServer(backend, repositoryPolicy);
144
+ void startMcpUpdateNotifier(logger).catch(() => { });
101
145
  };
@@ -0,0 +1,34 @@
1
+ import { type ValidatedUpdateCache } from '../core/update-cache.js';
2
+ type SpawnResult = {
3
+ unref(): void;
4
+ };
5
+ type SpawnLike = (command: string, args: readonly string[], options: {
6
+ detached: true;
7
+ stdio: 'ignore';
8
+ windowsHide: true;
9
+ }) => SpawnResult;
10
+ export interface CliUpdateNoticeDependencies {
11
+ argv: string[];
12
+ env: NodeJS.ProcessEnv;
13
+ installedVersion: string;
14
+ isTTY: boolean | undefined;
15
+ eligible: boolean;
16
+ now: number;
17
+ readCache: (options: {
18
+ env: NodeJS.ProcessEnv;
19
+ now: number;
20
+ }) => ValidatedUpdateCache | null;
21
+ writeStderr: (line: string) => unknown;
22
+ spawn: SpawnLike;
23
+ }
24
+ export declare function updateNoticeText(installedVersion: string, latestVersion: string): string;
25
+ /** Shared cache-gated notice line used by the CLI banner and `doctor`. */
26
+ export declare function cachedUpdateNoticeLine(options: {
27
+ installedVersion: string;
28
+ eligible: boolean;
29
+ env: NodeJS.ProcessEnv;
30
+ readCache: () => ValidatedUpdateCache | null;
31
+ }): string | null;
32
+ export declare function runCliUpdateNotice(deps: CliUpdateNoticeDependencies): void;
33
+ export declare function runProcessCliUpdateNotice(installedVersion: string): void;
34
+ export {};
@@ -0,0 +1,91 @@
1
+ import { spawn as nodeSpawn } from 'node:child_process';
2
+ import { updateEligibleInstallSync } from '../core/install-context.js';
3
+ import { isNewerVersion, readValidatedUpdateCacheSync, updateNotifierOptedOut, updateRefreshInProgress, } from '../core/update-cache.js';
4
+ import { t } from './i18n/index.js';
5
+ const EXCLUDED_COMMANDS = new Set([
6
+ 'augment',
7
+ 'mcp',
8
+ 'serve',
9
+ 'eval-server',
10
+ 'update',
11
+ '__update-check',
12
+ ]);
13
+ const EXCLUDED_FLAGS = new Set(['--help', '-h', '--version', '-V']);
14
+ function excludedInvocation(argv) {
15
+ const args = argv.slice(2);
16
+ if (args.some((arg) => EXCLUDED_FLAGS.has(arg)))
17
+ return true;
18
+ const command = args.find((arg) => !arg.startsWith('-'));
19
+ return command !== undefined && EXCLUDED_COMMANDS.has(command);
20
+ }
21
+ export function updateNoticeText(installedVersion, latestVersion) {
22
+ return t('update.available', { installedVersion, latestVersion });
23
+ }
24
+ /** Shared cache-gated notice line used by the CLI banner and `doctor`. */
25
+ export function cachedUpdateNoticeLine(options) {
26
+ try {
27
+ if (!options.eligible || updateNotifierOptedOut(options.env))
28
+ return null;
29
+ const cache = options.readCache();
30
+ if (!cache?.latestVersion || !isNewerVersion(options.installedVersion, cache.latestVersion)) {
31
+ return null;
32
+ }
33
+ return updateNoticeText(options.installedVersion, cache.latestVersion);
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export function runCliUpdateNotice(deps) {
40
+ try {
41
+ if (deps.isTTY !== true ||
42
+ updateNotifierOptedOut(deps.env) ||
43
+ !deps.eligible ||
44
+ excludedInvocation(deps.argv)) {
45
+ return;
46
+ }
47
+ const cache = deps.readCache({ env: deps.env, now: deps.now });
48
+ if (cache?.latestVersion && isNewerVersion(deps.installedVersion, cache.latestVersion)) {
49
+ deps.writeStderr(`${updateNoticeText(deps.installedVersion, cache.latestVersion)}\n`);
50
+ }
51
+ if (cache === null || cache.stale) {
52
+ // Coalesce parallel invocations: when a live process holds the refresh
53
+ // lock, its refresh covers us, so don't fork another CLI.
54
+ if (!updateRefreshInProgress(deps.env)) {
55
+ try {
56
+ deps
57
+ .spawn(process.execPath, [deps.argv[1] ?? '', '__update-check'], {
58
+ detached: true,
59
+ stdio: 'ignore',
60
+ windowsHide: true,
61
+ })
62
+ .unref();
63
+ }
64
+ catch {
65
+ // Update refresh is best-effort and must never reach Commander parsing.
66
+ }
67
+ }
68
+ }
69
+ }
70
+ catch {
71
+ // Cache reads and all adapter logic fail open.
72
+ }
73
+ }
74
+ export function runProcessCliUpdateNotice(installedVersion) {
75
+ if (process.stderr.isTTY !== true ||
76
+ updateNotifierOptedOut(process.env) ||
77
+ excludedInvocation(process.argv)) {
78
+ return;
79
+ }
80
+ runCliUpdateNotice({
81
+ argv: process.argv,
82
+ env: process.env,
83
+ installedVersion,
84
+ isTTY: process.stderr.isTTY,
85
+ eligible: updateEligibleInstallSync(),
86
+ now: Date.now(),
87
+ readCache: readValidatedUpdateCacheSync,
88
+ writeStderr: (line) => process.stderr.write(line),
89
+ spawn: nodeSpawn,
90
+ });
91
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * Explicit `gitnexus update`: refresh the latest dist-tag, then install
3
+ * `gitnexus@<version>` globally with npm — the same shape as `claude update`
4
+ * / `codex update`. Other commands only notify; they never spawn npm.
5
+ */
6
+ import { type UpdateState } from '../core/update-check.js';
7
+ export declare const UPDATE_PACKAGE = "gitnexus";
8
+ export interface UpdateCommandDependencies {
9
+ installedVersion: string;
10
+ refresh: (options: {
11
+ eligible: true;
12
+ ignoreOptOut: true;
13
+ installedVersion: string;
14
+ }) => Promise<UpdateState | null>;
15
+ writeStdout: (line: string) => void;
16
+ runInstall: (version: string) => Promise<number>;
17
+ setExitCode: (code: number) => void;
18
+ }
19
+ export declare function updateInstallArgs(version: string): string[];
20
+ export declare function updateInstallCommand(version: string): string;
21
+ export declare function updateCommand(maybeDeps?: unknown): Promise<void>;