gitnexus 1.6.11-rc.34 → 1.6.11-rc.36
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 +23 -0
- package/dist/cli/analyze-watch.d.ts +20 -0
- package/dist/cli/analyze-watch.js +375 -0
- package/dist/cli/analyze.js +1 -1
- package/dist/cli/auto-sync.d.ts +1 -0
- package/dist/cli/auto-sync.js +107 -0
- package/dist/cli/detect-changes-format.js +21 -0
- package/dist/cli/help-i18n.js +2 -0
- package/dist/cli/i18n/en.d.ts +6 -0
- package/dist/cli/i18n/en.js +6 -0
- package/dist/cli/i18n/resources.d.ts +12 -0
- package/dist/cli/i18n/zh-CN.d.ts +6 -0
- package/dist/cli/i18n/zh-CN.js +6 -0
- package/dist/cli/index.js +10 -0
- package/dist/cli/watch.d.ts +1 -20
- package/dist/cli/watch.js +5 -373
- package/dist/core/auto-sync/analysis-worker-launch.d.ts +30 -0
- package/dist/core/auto-sync/analysis-worker-launch.js +166 -0
- package/dist/core/auto-sync/config.d.ts +33 -0
- package/dist/core/auto-sync/config.js +320 -0
- package/dist/core/auto-sync/index.d.ts +6 -0
- package/dist/core/auto-sync/index.js +6 -0
- package/dist/core/auto-sync/path-security.d.ts +9 -0
- package/dist/core/auto-sync/path-security.js +251 -0
- package/dist/core/auto-sync/repo.d.ts +1 -0
- package/dist/core/auto-sync/repo.js +6 -0
- package/dist/core/auto-sync/runner.d.ts +44 -0
- package/dist/core/auto-sync/runner.js +428 -0
- package/dist/core/auto-sync/starter.d.ts +50 -0
- package/dist/core/auto-sync/starter.js +497 -0
- package/dist/core/auto-sync/state.d.ts +37 -0
- package/dist/core/auto-sync/state.js +119 -0
- package/dist/core/group/group-lock.js +5 -6
- package/dist/core/lbug/lbug-adapter.js +1 -14
- package/dist/mcp/local/local-backend.js +3 -0
- package/dist/server/analyze-worker-protocol.d.ts +6 -1
- package/dist/server/analyze-worker.d.ts +1 -0
- package/dist/server/analyze-worker.js +38 -20
- package/dist/server/api.js +2 -2
- package/dist/server/git-clone.d.ts +39 -10
- package/dist/server/git-clone.js +254 -56
- package/dist/storage/file-lock.d.ts +15 -0
- package/dist/storage/file-lock.js +155 -0
- package/dist/storage/repo-manager.js +10 -16
- package/dist/utils/process-identity.d.ts +2 -0
- package/dist/utils/process-identity.js +35 -0
- package/package.json +1 -1
- package/skills/gitnexus-cli.md +1 -1
package/README.md
CHANGED
|
@@ -249,6 +249,7 @@ gitnexus analyze --verbose # Log skipped files when parsers are unavailabl
|
|
|
249
249
|
gitnexus analyze --max-file-size 1024 # Skip files larger than N KB (default: 512, cap: 32768)
|
|
250
250
|
gitnexus analyze --worker-timeout 60 # Increase worker idle timeout for slow parses
|
|
251
251
|
gitnexus analyze --wal-checkpoint-threshold 67108864 # 64 MiB. Control LadybugDB WAL auto-checkpoint threshold (default: 67108864 = 64 MiB; -1 keeps Ladybug stock ~16 MiB)
|
|
252
|
+
gitnexus auto-sync [init|start|restart|stop|status|reset] # Scheduled remote clone/pull + analyze from GITNEXUS_HOME/watch_config.yml
|
|
252
253
|
gitnexus mcp # Start MCP server (stdio) — serves all indexed repos
|
|
253
254
|
gitnexus serve # Start local HTTP server (multi-repo) for web UI
|
|
254
255
|
gitnexus index # Register an existing .gitnexus/ folder into the global registry
|
|
@@ -309,6 +310,28 @@ and `serve` processes periodically check for a newly published index and reopen
|
|
|
309
310
|
it without a restart. MCP checks are throttled to once every five seconds, so a
|
|
310
311
|
tool call before the next check can briefly use the previous index.
|
|
311
312
|
|
|
313
|
+
### `gitnexus auto-sync`
|
|
314
|
+
|
|
315
|
+
`gitnexus auto-sync` is a different product from `gitnexus analyze --watch`. It is the explicit long-running auto-sync entrypoint that clones or pulls configured remotes. `gitnexus watch` is reserved and does not start either job: it prints this split. `GITNEXUS_HOME` defaults to `~/.gitnexus`; `gitnexus auto-sync init` creates its default `$GITNEXUS_HOME/watch_config.yml`. Bare `gitnexus auto-sync` is the same as `gitnexus auto-sync start`; `restart`, `stop`, `status`, and `reset` manage the same `GITNEXUS_HOME` instance. `reset` removes only the derived analysis state and commit snapshot; clones, indexes, and registry entries are untouched. `start` runs in the foreground, reads the configuration once at startup, runs once immediately, then repeats on `sync_interval_minutes`; restart it after changing the configuration. Watch runtime artifacts live under `$GITNEXUS_HOME/watch/`: `project_commit_info.txt` is the human-readable per-loop snapshot, `auto-sync-state.json` is the machine state used for commit skipping and analyze failure thresholds, `watch.mutex` prevents multiple auto-sync processes for one home, `watch.owner.json` records ownership metadata, `watch.pid` plus `watch.status.json` expose process state, `watch.stop.<ownerId>.json` is a temporary owner-fenced stop request, and `quarantine/` stores partial clone output before entries are removed after 14 days, keeping at most the five newest entries per repository regardless of age. Mutexes with verified dead owners are reclaimed automatically after an abnormal exit. Invalid or legacy mutexes fail closed; confirm no auto-sync process is running before manually removing `watch.mutex` and stale `watch.pid` / `watch.owner.json`.
|
|
316
|
+
|
|
317
|
+
```yaml
|
|
318
|
+
sync_interval_minutes: 10
|
|
319
|
+
max_concurrency: 1
|
|
320
|
+
repo_git_timeout: 10s
|
|
321
|
+
analyze_timeout: 5m
|
|
322
|
+
analyze_failure_threshold: 3
|
|
323
|
+
projects:
|
|
324
|
+
- local_path: /abs/path/to/repos
|
|
325
|
+
branches: [master, main]
|
|
326
|
+
overwrite_local_changes: false
|
|
327
|
+
remote_urls:
|
|
328
|
+
- git@github.com:owner/repo.git
|
|
329
|
+
- git@gitlab.com:group/repo.git
|
|
330
|
+
- git@gitee.com:owner/repo.git
|
|
331
|
+
```
|
|
332
|
+
|
|
333
|
+
`sync_interval_minutes` must be an integer of at least `5`. `local_path` must be an absolute path without traversal; each remote is cloned below it as `host/namespace/repo`, preventing same-basename repositories from colliding. `remote_urls` must use SSH SCP form for github.com, gitlab.com, or gitee.com. `repo_git_timeout` applies to each repo clone/pull and defaults to `10s`; a bare number such as `10` is interpreted as seconds, while `10000ms`, `10s`, and `1m` keep their explicit units. It must not exceed one hour or `sync_interval_minutes`, whichever is smaller — so a bare `600000` is rejected, because it means 600000 seconds rather than milliseconds. `analyze_timeout` applies to each isolated analysis worker, defaults to half of `sync_interval_minutes`, and cannot exceed that value; this keeps it within Node's timer range. Timeout and `auto-sync stop` request safe cancellation; a worker already in native work exits after it returns to a JS-visible safe point. While waiting, auto-sync reports `cancelling` or `stopping` and keeps its ownership files so another auto-sync cannot take over. The parent waits up to 5 seconds for the worker to exit; after that it stops waiting, releases its ownership files, and leaves the worker to finish and exit on its own rather than killing it mid-write. `auto-sync stop` uses this same control path on macOS and Windows. `overwrite_local_changes` defaults to `false`; a dirty local clone is skipped with an error log, while `true` allows branch fallback to replace local changes and additionally discards untracked files and directories in the clone after checkout — ignored paths, including GitNexus's own `.gitnexus/` storage, are preserved. `max_concurrency` defaults to `1` and is capped at runtime by `floor(availableMemoryGB / 2)` with a minimum of `1`; the effective value is printed at the start of each loop. Each analysis worker's heap cap is the machine-wide cap divided by the number of repositories analyzed in parallel, so concurrent workers share one memory budget instead of each claiming the whole machine. `analyze_failure_threshold` defaults to `3`, must be at least `2`, and pauses repeated failures only for the same repo branch and commit; a new commit or `gitnexus auto-sync reset` clears the block and allows analysis again. Repositories are registered and added to groups by their full remote identity (`host/namespace/repo`), so repositories with the same basename remain distinct. Use `branches` to try branches in order; legacy `branch` remains supported, but the two fields cannot be set together. If all branches are unavailable or time out, watch logs an error, records the repo status, and skips that repo for the loop. Leave `group_name` empty or omit it to skip group add/sync for that project; otherwise create the group first with `gitnexus group create <name>`. `$GITNEXUS_HOME/watch/project_commit_info.txt` is for inspection only; GitNexus stores machine state separately in `$GITNEXUS_HOME/watch/auto-sync-state.json`.
|
|
334
|
+
|
|
312
335
|
GraphQL contract matching is opt-in in the group's `group.yaml`:
|
|
313
336
|
|
|
314
337
|
```yaml
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AnalyzeOptions as CoreAnalyzeOptions } from '../core/run-analyze.js';
|
|
2
|
+
import type { AnalyzerRunnerIdentity } from '../storage/repo-manager.js';
|
|
3
|
+
import type { AnalyzeOptions } from './analyze-options.js';
|
|
4
|
+
import { type WatchRefreshError } from './watch-queue.js';
|
|
5
|
+
export type WatchCliOptions = AnalyzeOptions;
|
|
6
|
+
export declare function isRelevantWatchPath(filePath: string): boolean;
|
|
7
|
+
export interface WatchEnvironmentBaseline {
|
|
8
|
+
readonly maxFileSize: string | undefined;
|
|
9
|
+
readonly workerTimeout: string | undefined;
|
|
10
|
+
readonly verbose: string | undefined;
|
|
11
|
+
}
|
|
12
|
+
export declare function resolveWatchOptions(repoPath: string, cli: WatchCliOptions, baseline: WatchEnvironmentBaseline, reportIgnoredConfig?: (names: readonly string[]) => void): Promise<CoreAnalyzeOptions>;
|
|
13
|
+
export interface WatchFileLoop {
|
|
14
|
+
readonly waitForIdle: () => Promise<void>;
|
|
15
|
+
readonly close: () => Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
export declare function shouldStopAfterWatchRefreshFailure(error: unknown, paths: readonly string[]): boolean;
|
|
18
|
+
/** Start the real filesystem watcher with bounded, serialized refreshes. */
|
|
19
|
+
export declare function startWatchFileLoop(repoPath: string, debounceMs: number, refresh: (paths: readonly string[]) => Promise<void>, onError: WatchRefreshError, onWatcherError?: (error: unknown) => void): Promise<WatchFileLoop>;
|
|
20
|
+
export declare function watchCommandWithRunnerIdentity(runnerIdentityAtBootstrap: AnalyzerRunnerIdentity, inputPath?: string, cliOptions?: WatchCliOptions): Promise<void>;
|
|
@@ -0,0 +1,375 @@
|
|
|
1
|
+
/** Local incremental watch (`gitnexus analyze --watch`). Remote auto-sync lives in `auto-sync.ts`. */
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import fs from 'node:fs/promises';
|
|
4
|
+
import { watch } from 'chokidar';
|
|
5
|
+
import { createWatchIgnorePredicate } from '../config/ignore-service.js';
|
|
6
|
+
import { analyzeFailureMayHaveMutatedLiveIndex, runFullAnalysis, } from '../core/run-analyze.js';
|
|
7
|
+
import { getGitRoot, hasGitDir } from '../storage/git.js';
|
|
8
|
+
import { GITNEXUS_DIR } from '../storage/repo-meta.js';
|
|
9
|
+
import { loadAnalyzeConfigStrict, mergeAnalyzeOptions, validateBranchName, } from './analyze-config.js';
|
|
10
|
+
import { ensureHeap } from './analyze.js';
|
|
11
|
+
import { cliError, cliInfo, cliWarn } from './cli-message.js';
|
|
12
|
+
import { WATCH_FULL_REFRESH_PATH, WatchRefreshQueue, } from './watch-queue.js';
|
|
13
|
+
const DEFAULT_DEBOUNCE_MS = 300;
|
|
14
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
15
|
+
const MAX_FILE_SIZE_KB = 32 * 1024;
|
|
16
|
+
const TRANSIENT_WATCH_ERROR_CODES = new Set(['EACCES', 'ENOENT', 'ENOTDIR', 'EPERM']);
|
|
17
|
+
function posixWatchPath(filePath) {
|
|
18
|
+
return filePath.replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
19
|
+
}
|
|
20
|
+
export function isRelevantWatchPath(filePath) {
|
|
21
|
+
const normalized = posixWatchPath(filePath);
|
|
22
|
+
return (normalized.length > 0 &&
|
|
23
|
+
normalized !== '.' &&
|
|
24
|
+
!normalized.startsWith('../') &&
|
|
25
|
+
!path.posix.isAbsolute(normalized) &&
|
|
26
|
+
!path.win32.isAbsolute(filePath));
|
|
27
|
+
}
|
|
28
|
+
function isIgnoreControlPath(filePath) {
|
|
29
|
+
const normalized = posixWatchPath(filePath);
|
|
30
|
+
return normalized === '.gitignore' || normalized === '.gitnexusignore';
|
|
31
|
+
}
|
|
32
|
+
function isConfigControlPath(filePath) {
|
|
33
|
+
return posixWatchPath(filePath) === '.gitnexusrc';
|
|
34
|
+
}
|
|
35
|
+
function isAnalyzerOwnedWatchPath(filePath) {
|
|
36
|
+
const normalized = posixWatchPath(filePath).replace(/\/+$/, '');
|
|
37
|
+
return normalized === GITNEXUS_DIR || normalized.startsWith(`${GITNEXUS_DIR}/`);
|
|
38
|
+
}
|
|
39
|
+
function repoRelativeWatchPath(repoPath, candidate) {
|
|
40
|
+
const relative = path.relative(repoPath, candidate).replace(/\\/g, '/');
|
|
41
|
+
if (!relative || relative.startsWith('../') || path.isAbsolute(relative))
|
|
42
|
+
return null;
|
|
43
|
+
return relative;
|
|
44
|
+
}
|
|
45
|
+
function setEnvironment(name, value) {
|
|
46
|
+
if (value === undefined)
|
|
47
|
+
delete process.env[name];
|
|
48
|
+
else
|
|
49
|
+
process.env[name] = value;
|
|
50
|
+
}
|
|
51
|
+
function positiveInteger(value, flag, maximum) {
|
|
52
|
+
if (value === undefined)
|
|
53
|
+
return undefined;
|
|
54
|
+
const parsed = Number(value);
|
|
55
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
56
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
57
|
+
if (maximum !== undefined && parsed > maximum) {
|
|
58
|
+
throw new Error(`${flag} must not exceed ${maximum}`);
|
|
59
|
+
}
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
export async function resolveWatchOptions(repoPath, cli, baseline, reportIgnoredConfig = () => { }) {
|
|
63
|
+
const config = (await loadAnalyzeConfigStrict(repoPath)) ?? {};
|
|
64
|
+
const merged = mergeAnalyzeOptions(cli, config);
|
|
65
|
+
const unsupported = [
|
|
66
|
+
['--force', cli.force],
|
|
67
|
+
['--repair-fts', cli.repairFts],
|
|
68
|
+
['--embeddings', cli.embeddings],
|
|
69
|
+
['--drop-embeddings', cli.dropEmbeddings],
|
|
70
|
+
['--skills', cli.skills],
|
|
71
|
+
['--default-branch', cli.defaultBranch],
|
|
72
|
+
['--skip-agents-md', cli.skipAgentsMd],
|
|
73
|
+
['--skip-skills', cli.skipSkills],
|
|
74
|
+
['--no-stats', cli.stats === false],
|
|
75
|
+
['--self-commit', cli.selfCommit],
|
|
76
|
+
['--index-only', cli.indexOnly],
|
|
77
|
+
['--skip-git', cli.skipGit],
|
|
78
|
+
['--spring-actuator', cli.springActuator],
|
|
79
|
+
['walCheckpointThreshold', cli.walCheckpointThreshold],
|
|
80
|
+
['embeddingThreads', cli.embeddingThreads],
|
|
81
|
+
['embeddingBatchSize', cli.embeddingBatchSize],
|
|
82
|
+
['embeddingSubBatchSize', cli.embeddingSubBatchSize],
|
|
83
|
+
['embeddingDevice', cli.embeddingDevice],
|
|
84
|
+
['embeddingBaseUrl', cli.embeddingBaseUrl],
|
|
85
|
+
['embeddingModel', cli.embeddingModel],
|
|
86
|
+
['--embedding-auth-token', cli.embeddingAuthToken],
|
|
87
|
+
['--embedding-dims', cli.embeddingDims],
|
|
88
|
+
].filter(([, value]) => value !== undefined && value !== false);
|
|
89
|
+
if (unsupported.length > 0) {
|
|
90
|
+
throw new Error(`analyze --watch does not support ${unsupported.map(([name]) => name).join(', ')}`);
|
|
91
|
+
}
|
|
92
|
+
reportIgnoredConfig([
|
|
93
|
+
['embeddings', config.embeddings],
|
|
94
|
+
['dropEmbeddings', config.dropEmbeddings],
|
|
95
|
+
['defaultBranch', config.defaultBranch],
|
|
96
|
+
['skipAgentsMd', config.skipAgentsMd !== undefined],
|
|
97
|
+
['skipSkills', config.skipSkills !== undefined],
|
|
98
|
+
['stats', config.stats !== undefined],
|
|
99
|
+
['springActuator', config.springActuator],
|
|
100
|
+
['walCheckpointThreshold', config.walCheckpointThreshold],
|
|
101
|
+
['embeddingThreads', config.embeddingThreads],
|
|
102
|
+
['embeddingBatchSize', config.embeddingBatchSize],
|
|
103
|
+
['embeddingSubBatchSize', config.embeddingSubBatchSize],
|
|
104
|
+
['embeddingDevice', config.embeddingDevice],
|
|
105
|
+
['embeddingBaseUrl', config.embeddingBaseUrl],
|
|
106
|
+
['embeddingModel', config.embeddingModel],
|
|
107
|
+
]
|
|
108
|
+
.filter(([, value]) => value !== undefined && value !== false)
|
|
109
|
+
.map(([name]) => String(name)));
|
|
110
|
+
const branch = merged.branch === undefined ? undefined : validateBranchName(merged.branch, '--branch');
|
|
111
|
+
const workerPoolSize = positiveInteger(merged.workers, '--workers');
|
|
112
|
+
const workerTimeoutSeconds = positiveInteger(merged.workerTimeout, 'workerTimeout');
|
|
113
|
+
const maxFileSize = positiveInteger(merged.maxFileSize, 'maxFileSize', MAX_FILE_SIZE_KB);
|
|
114
|
+
setEnvironment('GITNEXUS_MAX_FILE_SIZE', maxFileSize === undefined ? baseline.maxFileSize : String(maxFileSize));
|
|
115
|
+
if (workerTimeoutSeconds !== undefined) {
|
|
116
|
+
process.env.GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS = String(workerTimeoutSeconds * 1000);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
setEnvironment('GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS', baseline.workerTimeout);
|
|
120
|
+
}
|
|
121
|
+
setEnvironment('GITNEXUS_VERBOSE', merged.verbose ? '1' : baseline.verbose);
|
|
122
|
+
return {
|
|
123
|
+
pdg: merged.pdg,
|
|
124
|
+
branch,
|
|
125
|
+
registryName: merged.name,
|
|
126
|
+
allowDuplicateName: merged.allowDuplicateName,
|
|
127
|
+
workerPoolSize,
|
|
128
|
+
fetchWrappers: merged.fetchWrappers,
|
|
129
|
+
skipAgentsMd: true,
|
|
130
|
+
skipSkills: true,
|
|
131
|
+
noStats: true,
|
|
132
|
+
atomicIncremental: process.platform !== 'win32',
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function refreshSummary(result, observedPaths, durationMs, lastSuccessfulRefreshAt) {
|
|
136
|
+
const measured = result.incrementalStats;
|
|
137
|
+
const changed = measured?.changedFiles ?? (result.alreadyUpToDate ? 0 : observedPaths.length);
|
|
138
|
+
const reparsed = measured?.reparsedFiles ??
|
|
139
|
+
(typeof result.pipelineResult?.reparsedFileCount === 'number'
|
|
140
|
+
? result.pipelineResult.reparsedFileCount
|
|
141
|
+
: 0);
|
|
142
|
+
const dependents = measured?.affectedDependents ?? 0;
|
|
143
|
+
const mode = measured?.writeMode ?? (result.alreadyUpToDate ? 'no-op' : 'full');
|
|
144
|
+
return (`Refresh complete: ${changed} changed, ${reparsed} re-parsed, ` +
|
|
145
|
+
`${dependents} affected dependent(s), ${durationMs}ms, ${mode}; ` +
|
|
146
|
+
`last success ${lastSuccessfulRefreshAt}`);
|
|
147
|
+
}
|
|
148
|
+
async function waitUntilReady(watcher) {
|
|
149
|
+
await new Promise((resolve, reject) => {
|
|
150
|
+
const ready = () => {
|
|
151
|
+
watcher.off('error', failed);
|
|
152
|
+
resolve();
|
|
153
|
+
};
|
|
154
|
+
const failed = (error) => {
|
|
155
|
+
watcher.off('ready', ready);
|
|
156
|
+
reject(error);
|
|
157
|
+
};
|
|
158
|
+
watcher.once('ready', ready);
|
|
159
|
+
watcher.once('error', failed);
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
class WatchControlReloadError extends Error {
|
|
163
|
+
constructor(cause) {
|
|
164
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
165
|
+
this.name = 'WatchControlReloadError';
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
export function shouldStopAfterWatchRefreshFailure(error, paths) {
|
|
169
|
+
return (paths.length > 0 &&
|
|
170
|
+
!(error instanceof WatchControlReloadError) &&
|
|
171
|
+
analyzeFailureMayHaveMutatedLiveIndex(error));
|
|
172
|
+
}
|
|
173
|
+
/** Start the real filesystem watcher with bounded, serialized refreshes. */
|
|
174
|
+
export async function startWatchFileLoop(repoPath, debounceMs, refresh, onError, onWatcherError = (error) => onError(error, [])) {
|
|
175
|
+
let ignorePath = await createWatchIgnorePredicate(repoPath);
|
|
176
|
+
let ignoreControlValid = true;
|
|
177
|
+
const queue = new WatchRefreshQueue(async (paths) => {
|
|
178
|
+
if (paths.some(isIgnoreControlPath) || !ignoreControlValid) {
|
|
179
|
+
const retryingInvalidControls = !ignoreControlValid;
|
|
180
|
+
try {
|
|
181
|
+
ignorePath = await createWatchIgnorePredicate(repoPath);
|
|
182
|
+
ignoreControlValid = true;
|
|
183
|
+
watcher.add(repoPath);
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
ignoreControlValid = false;
|
|
187
|
+
throw new WatchControlReloadError(retryingInvalidControls
|
|
188
|
+
? new Error('Ignore controls remain invalid; fix them before indexing more changes.', {
|
|
189
|
+
cause: error,
|
|
190
|
+
})
|
|
191
|
+
: error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
await refresh(paths);
|
|
195
|
+
}, onError, debounceMs, {
|
|
196
|
+
maxWaitMs: Math.max(2_000, debounceMs * 10),
|
|
197
|
+
maxPendingPaths: 1_000,
|
|
198
|
+
holdEventsUntilInitialRefresh: true,
|
|
199
|
+
isPriorityPath: (filePath) => isIgnoreControlPath(filePath) || isConfigControlPath(filePath),
|
|
200
|
+
});
|
|
201
|
+
const watcher = watch(repoPath, {
|
|
202
|
+
ignoreInitial: true,
|
|
203
|
+
atomic: true,
|
|
204
|
+
followSymlinks: false,
|
|
205
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 20 },
|
|
206
|
+
ignored: (candidate, stats) => {
|
|
207
|
+
const relative = repoRelativeWatchPath(repoPath, candidate);
|
|
208
|
+
if (relative !== null && isAnalyzerOwnedWatchPath(relative))
|
|
209
|
+
return true;
|
|
210
|
+
if (relative !== null && (isIgnoreControlPath(relative) || isConfigControlPath(relative))) {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
return ignorePath(candidate, stats?.isDirectory() ?? false);
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
watcher.on('all', (event, changedPath) => {
|
|
217
|
+
if (event !== 'add' && event !== 'change' && event !== 'unlink')
|
|
218
|
+
return;
|
|
219
|
+
const relative = repoRelativeWatchPath(repoPath, changedPath);
|
|
220
|
+
if (relative && isRelevantWatchPath(relative) && !isAnalyzerOwnedWatchPath(relative)) {
|
|
221
|
+
queue.enqueue(relative);
|
|
222
|
+
}
|
|
223
|
+
});
|
|
224
|
+
watcher.on('error', (error) => {
|
|
225
|
+
// Chokidar can surface a transient EPERM on Windows while an ignored
|
|
226
|
+
// analyzer-owned path is replaced. Re-arm the root and force one bounded
|
|
227
|
+
// catch-up refresh so a missed event cannot leave the graph stale. Other
|
|
228
|
+
// watcher errors may mean coverage was lost and remain fatal.
|
|
229
|
+
if (TRANSIENT_WATCH_ERROR_CODES.has(error.code ?? '')) {
|
|
230
|
+
watcher.add(repoPath);
|
|
231
|
+
queue.enqueue(WATCH_FULL_REFRESH_PATH);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
onWatcherError(error);
|
|
235
|
+
});
|
|
236
|
+
try {
|
|
237
|
+
await waitUntilReady(watcher);
|
|
238
|
+
await queue.runInitial();
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
await watcher.close();
|
|
242
|
+
await queue.close();
|
|
243
|
+
throw error;
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
waitForIdle: () => queue.waitForIdle(),
|
|
247
|
+
close: async () => {
|
|
248
|
+
await watcher.close();
|
|
249
|
+
await queue.close();
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
export async function watchCommandWithRunnerIdentity(runnerIdentityAtBootstrap, inputPath, cliOptions = {}) {
|
|
254
|
+
if (await ensureHeap({ cleanForwardedTermination: true }))
|
|
255
|
+
return;
|
|
256
|
+
const requestedRepoPath = inputPath ? path.resolve(inputPath) : getGitRoot(process.cwd());
|
|
257
|
+
if (requestedRepoPath === null || !hasGitDir(requestedRepoPath)) {
|
|
258
|
+
cliError(' gitnexus analyze --watch requires a Git repository.');
|
|
259
|
+
process.exitCode = 1;
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
const repoPath = await fs.realpath(requestedRepoPath);
|
|
263
|
+
const baselineEnvironment = {
|
|
264
|
+
maxFileSize: process.env.GITNEXUS_MAX_FILE_SIZE,
|
|
265
|
+
workerTimeout: process.env.GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS,
|
|
266
|
+
verbose: process.env.GITNEXUS_VERBOSE,
|
|
267
|
+
};
|
|
268
|
+
try {
|
|
269
|
+
let ignoredConfigSignature;
|
|
270
|
+
const reportIgnoredConfig = (names) => {
|
|
271
|
+
const signature = [...names].sort().join(',');
|
|
272
|
+
if (signature === ignoredConfigSignature)
|
|
273
|
+
return;
|
|
274
|
+
ignoredConfigSignature = signature;
|
|
275
|
+
if (names.length > 0) {
|
|
276
|
+
cliWarn(`Watch mode ignores unsupported .gitnexusrc settings: ${names.join(', ')}.`);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
279
|
+
let debounceMs;
|
|
280
|
+
let analyzeOptions;
|
|
281
|
+
try {
|
|
282
|
+
debounceMs =
|
|
283
|
+
positiveInteger(cliOptions.debounce ?? String(DEFAULT_DEBOUNCE_MS), '--debounce', MAX_TIMER_DELAY_MS) ?? DEFAULT_DEBOUNCE_MS;
|
|
284
|
+
analyzeOptions = await resolveWatchOptions(repoPath, cliOptions, baselineEnvironment, reportIgnoredConfig);
|
|
285
|
+
}
|
|
286
|
+
catch (error) {
|
|
287
|
+
cliError(` ${error instanceof Error ? error.message : String(error)}`);
|
|
288
|
+
process.exitCode = 1;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
let stopWatching;
|
|
292
|
+
const stopped = new Promise((resolve) => {
|
|
293
|
+
stopWatching = resolve;
|
|
294
|
+
});
|
|
295
|
+
const stop = () => stopWatching();
|
|
296
|
+
process.once('SIGINT', stop);
|
|
297
|
+
process.once('SIGTERM', stop);
|
|
298
|
+
try {
|
|
299
|
+
let loop;
|
|
300
|
+
let fatalRefreshError;
|
|
301
|
+
let configControlValid = true;
|
|
302
|
+
let lastSuccessfulRefreshAt;
|
|
303
|
+
try {
|
|
304
|
+
loop = await startWatchFileLoop(repoPath, debounceMs, async (paths) => {
|
|
305
|
+
if (paths.some(isConfigControlPath) || !configControlValid) {
|
|
306
|
+
const retryingInvalidConfig = !configControlValid;
|
|
307
|
+
try {
|
|
308
|
+
analyzeOptions = await resolveWatchOptions(repoPath, cliOptions, baselineEnvironment, reportIgnoredConfig);
|
|
309
|
+
configControlValid = true;
|
|
310
|
+
}
|
|
311
|
+
catch (error) {
|
|
312
|
+
configControlValid = false;
|
|
313
|
+
throw new WatchControlReloadError(retryingInvalidConfig
|
|
314
|
+
? new Error('Configuration remains invalid; fix it before indexing more changes.', {
|
|
315
|
+
cause: error,
|
|
316
|
+
})
|
|
317
|
+
: error);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
const startedAt = Date.now();
|
|
321
|
+
const result = await runFullAnalysis(repoPath, analyzeOptions, {
|
|
322
|
+
onProgress: () => { },
|
|
323
|
+
onLog: process.env.GITNEXUS_VERBOSE === '1'
|
|
324
|
+
? (message) => cliInfo(` ${message}`)
|
|
325
|
+
: undefined,
|
|
326
|
+
}, runnerIdentityAtBootstrap);
|
|
327
|
+
lastSuccessfulRefreshAt = new Date().toISOString();
|
|
328
|
+
if (paths.length === 0) {
|
|
329
|
+
cliInfo(result.alreadyUpToDate
|
|
330
|
+
? `Watching ${repoPath}; index is up to date.`
|
|
331
|
+
: `Watching ${repoPath}; initial index ready in ${Date.now() - startedAt}ms.`);
|
|
332
|
+
}
|
|
333
|
+
else {
|
|
334
|
+
cliInfo(refreshSummary(result, paths, Date.now() - startedAt, lastSuccessfulRefreshAt));
|
|
335
|
+
}
|
|
336
|
+
}, (error, paths) => {
|
|
337
|
+
const detail = paths.length > 0 ? ` (${paths.length} queued path(s))` : '';
|
|
338
|
+
if (shouldStopAfterWatchRefreshFailure(error, paths)) {
|
|
339
|
+
fatalRefreshError = error;
|
|
340
|
+
cliError(`Refresh failed${detail}: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
341
|
+
'Watch mode is stopping because the live index may have been updated in place.');
|
|
342
|
+
stopWatching();
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
const lastSuccess = lastSuccessfulRefreshAt ?? 'none yet';
|
|
346
|
+
cliWarn(`Refresh failed${detail}: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
347
|
+
`Retry scheduled; last success ${lastSuccess}.`);
|
|
348
|
+
}, (error) => {
|
|
349
|
+
fatalRefreshError = error;
|
|
350
|
+
cliError(`Watcher failed: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
351
|
+
'Watch mode is stopping.');
|
|
352
|
+
stopWatching();
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
catch (error) {
|
|
356
|
+
cliError(` Unable to start watcher: ${error instanceof Error ? error.message : String(error)}`);
|
|
357
|
+
process.exitCode = 1;
|
|
358
|
+
return;
|
|
359
|
+
}
|
|
360
|
+
await stopped;
|
|
361
|
+
await loop.close();
|
|
362
|
+
if (fatalRefreshError !== undefined)
|
|
363
|
+
process.exitCode = 1;
|
|
364
|
+
}
|
|
365
|
+
finally {
|
|
366
|
+
process.removeListener('SIGINT', stop);
|
|
367
|
+
process.removeListener('SIGTERM', stop);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
setEnvironment('GITNEXUS_MAX_FILE_SIZE', baselineEnvironment.maxFileSize);
|
|
372
|
+
setEnvironment('GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS', baselineEnvironment.workerTimeout);
|
|
373
|
+
setEnvironment('GITNEXUS_VERBOSE', baselineEnvironment.verbose);
|
|
374
|
+
}
|
|
375
|
+
}
|
package/dist/cli/analyze.js
CHANGED
|
@@ -602,7 +602,7 @@ export const analyzeCommand = async (inputPath, options, runnerIdentityAtBootstr
|
|
|
602
602
|
export const analyzeCommandWithRunnerIdentity = async (runnerIdentityAtBootstrap, inputPath, options) => analyzeCommand(inputPath, options, runnerIdentityAtBootstrap);
|
|
603
603
|
export async function analyzeOrWatchCommandWithRunnerIdentity(runnerIdentityAtBootstrap, inputPath, options = {}) {
|
|
604
604
|
if (options.watch) {
|
|
605
|
-
const { watchCommandWithRunnerIdentity } = await import('./watch.js');
|
|
605
|
+
const { watchCommandWithRunnerIdentity } = await import('./analyze-watch.js');
|
|
606
606
|
await watchCommandWithRunnerIdentity(runnerIdentityAtBootstrap, inputPath, options);
|
|
607
607
|
return;
|
|
608
608
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function autoSyncCommand(action?: string): Promise<void>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** Remote auto-sync CLI (`gitnexus auto-sync`). Local incremental watch lives in `analyze-watch.ts`. */
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { getAutoSyncConfigPath, getAutoSyncMutexPath, readAutoSyncWatchStatus, resetAutoSyncState, startAutoSyncWatch, stopAutoSyncWatch, } from '../core/auto-sync/index.js';
|
|
5
|
+
export async function autoSyncCommand(action = 'start') {
|
|
6
|
+
if (action === 'init') {
|
|
7
|
+
await initWatchConfig();
|
|
8
|
+
return;
|
|
9
|
+
}
|
|
10
|
+
if (action === 'reset') {
|
|
11
|
+
if (!(await resetAutoSyncState())) {
|
|
12
|
+
process.stderr.write(`[auto-sync] Cannot reset analysis state while the watch mutex is held. Confirm no watch process is running, then remove ${getAutoSyncMutexPath()}.\n`);
|
|
13
|
+
process.exitCode = 1;
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
process.stdout.write('[auto-sync] Reset analysis state.\n');
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (action === 'status') {
|
|
20
|
+
printStatus(await readAutoSyncWatchStatus());
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
if (action === 'stop') {
|
|
24
|
+
if ((await stopAutoSyncWatch()) !== 'stopped')
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (action === 'restart') {
|
|
29
|
+
const result = await stopAutoSyncWatch();
|
|
30
|
+
if (result === 'refused' || result === 'timeout') {
|
|
31
|
+
process.exitCode = 1;
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
await startWatchProcess();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
if (action !== 'start') {
|
|
38
|
+
process.stderr.write(`[auto-sync] Unknown auto-sync action: ${action}\n`);
|
|
39
|
+
process.exitCode = 1;
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
await startWatchProcess();
|
|
43
|
+
}
|
|
44
|
+
async function startWatchProcess() {
|
|
45
|
+
const handle = await startAutoSyncWatch();
|
|
46
|
+
if (!handle) {
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const stop = () => {
|
|
51
|
+
void handle.stop().then(() => {
|
|
52
|
+
process.stderr.write('[auto-sync] Watch stopped.\n');
|
|
53
|
+
process.exit(0);
|
|
54
|
+
}, (error) => {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
process.stderr.write(`[auto-sync] Failed to stop watch: ${message}\n`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
});
|
|
59
|
+
};
|
|
60
|
+
process.once('SIGINT', stop);
|
|
61
|
+
process.once('SIGTERM', stop);
|
|
62
|
+
}
|
|
63
|
+
function printStatus(status) {
|
|
64
|
+
const parts = [`state=${status.state}`];
|
|
65
|
+
if (status.pid)
|
|
66
|
+
parts.push(`pid=${status.pid}`);
|
|
67
|
+
if (status.configPath)
|
|
68
|
+
parts.push(`config=${status.configPath}`);
|
|
69
|
+
if (status.message)
|
|
70
|
+
parts.push(`message=${status.message}`);
|
|
71
|
+
parts.push(`updated_at=${status.updatedAt}`);
|
|
72
|
+
process.stdout.write(`${parts.join(' ')}\n`);
|
|
73
|
+
}
|
|
74
|
+
async function initWatchConfig() {
|
|
75
|
+
const configPath = getAutoSyncConfigPath();
|
|
76
|
+
try {
|
|
77
|
+
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
|
78
|
+
await fs.writeFile(configPath, defaultSyncConfig(path.resolve(path.dirname(configPath), 'repos')), {
|
|
79
|
+
flag: 'wx',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (err) {
|
|
83
|
+
if (err.code === 'EEXIST') {
|
|
84
|
+
process.stderr.write(`[auto-sync] Config already exists: ${configPath}\n`);
|
|
85
|
+
process.exitCode = 1;
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
throw err;
|
|
89
|
+
}
|
|
90
|
+
process.stdout.write(`[auto-sync] Created ${configPath}\n`);
|
|
91
|
+
}
|
|
92
|
+
function defaultSyncConfig(localPath) {
|
|
93
|
+
return [
|
|
94
|
+
'sync_interval_minutes: 10',
|
|
95
|
+
'max_concurrency: 1',
|
|
96
|
+
'repo_git_timeout: 10s',
|
|
97
|
+
'analyze_timeout: 5m',
|
|
98
|
+
'analyze_failure_threshold: 3',
|
|
99
|
+
'projects:',
|
|
100
|
+
` - local_path: ${localPath}`,
|
|
101
|
+
' branches: [master, main]',
|
|
102
|
+
' overwrite_local_changes: false',
|
|
103
|
+
' remote_urls:',
|
|
104
|
+
' - git@github.com:owner/repo.git',
|
|
105
|
+
'',
|
|
106
|
+
].join('\n');
|
|
107
|
+
}
|
|
@@ -20,6 +20,27 @@ export function formatDetectChangesResult(result) {
|
|
|
20
20
|
if (payload.truncated)
|
|
21
21
|
notes.push(t(payload.partial ? 'tool.detectChanges.truncatedDegraded' : 'tool.detectChanges.truncated'));
|
|
22
22
|
if ((summary.changed_count ?? 0) === 0) {
|
|
23
|
+
// Parse-fail payloads set `partial` and an honest `message` (#2915/#3131).
|
|
24
|
+
// Production *clean* trees also set English `message: 'No changes detected.'`
|
|
25
|
+
// — that must go through `t('tool.detectChanges.noChanges')` or zh-CN never
|
|
26
|
+
// fires. Only pass the backend string through on a degraded/parse-fail run.
|
|
27
|
+
if (payload.partial &&
|
|
28
|
+
typeof summary.message === 'string' &&
|
|
29
|
+
summary.message.trim().length > 0) {
|
|
30
|
+
return [...notes, summary.message.trim()].join('\n');
|
|
31
|
+
}
|
|
32
|
+
// Confirmed no-overlap: files parsed, mapping succeeded, zero symbols.
|
|
33
|
+
// `queryDegraded` is `partial: true` with the same counts and no message —
|
|
34
|
+
// do not call that a confirmed mapping (#3131 honesty).
|
|
35
|
+
if (!payload.partial && (summary.changed_files ?? 0) > 0) {
|
|
36
|
+
return [
|
|
37
|
+
...notes,
|
|
38
|
+
t('tool.detectChanges.noOverlappingSymbols', { files: summary.changed_files }),
|
|
39
|
+
].join('\n');
|
|
40
|
+
}
|
|
41
|
+
if (payload.partial) {
|
|
42
|
+
return notes.join('\n');
|
|
43
|
+
}
|
|
23
44
|
return [...notes, t('tool.detectChanges.noChanges')].join('\n');
|
|
24
45
|
}
|
|
25
46
|
const lines = [];
|
package/dist/cli/help-i18n.js
CHANGED
|
@@ -10,6 +10,8 @@ const COMMAND_DESCRIPTION_KEYS = {
|
|
|
10
10
|
'': 'help.description.root',
|
|
11
11
|
setup: 'help.command.setup.description',
|
|
12
12
|
uninstall: 'help.command.uninstall.description',
|
|
13
|
+
watch: 'help.command.watch.description',
|
|
14
|
+
'auto-sync': 'help.command.autoSync.description',
|
|
13
15
|
analyze: 'help.command.analyze.description',
|
|
14
16
|
index: 'help.command.index.description',
|
|
15
17
|
serve: 'help.command.serve.description',
|
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -66,6 +66,7 @@ export declare const en: {
|
|
|
66
66
|
readonly 'tool.usage.cypher': "Usage: gitnexus cypher <cypher_query>";
|
|
67
67
|
readonly 'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.";
|
|
68
68
|
readonly 'tool.detectChanges.noChanges': "No changes detected.";
|
|
69
|
+
readonly 'tool.detectChanges.noOverlappingSymbols': "Diff touched {{files}} file(s) but no indexed symbols overlap those hunks — not a clean tree.";
|
|
69
70
|
readonly 'tool.detectChanges.partial': "PARTIAL RESULT: a graph query failed, so changed symbols may be missing. Do not read this as a clean pre-commit check.";
|
|
70
71
|
readonly 'tool.detectChanges.truncated': "LISTING CAPPED: the changed-symbol list was capped, so it does not name every changed symbol. The counts and risk level still cover all of them.";
|
|
71
72
|
readonly 'tool.detectChanges.truncatedDegraded': "LISTING CAPPED: the changed-symbol list was capped. The run also degraded, so the counts are a lower bound, not a total.";
|
|
@@ -124,6 +125,11 @@ export declare const en: {
|
|
|
124
125
|
readonly 'help.option.version': "output the version number";
|
|
125
126
|
readonly 'help.command.setup.description': "One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex";
|
|
126
127
|
readonly 'help.command.uninstall.description': "Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors";
|
|
128
|
+
readonly 'help.command.autoSync.description': "Control scheduled repository clone/pull and analysis from GITNEXUS_HOME/watch_config.yml";
|
|
129
|
+
readonly 'help.autoSync.details': "\nActions: init, start (default), restart, stop, status, reset\nConfiguration: GITNEXUS_HOME/watch_config.yml\nRuntime files: GITNEXUS_HOME/watch/watch.pid, watch.mutex, watch.owner.json, watch.status.json, auto-sync-state.json\nRecovery: mutexes with verified dead owners are reclaimed automatically; invalid or legacy mutexes fail closed and require manual removal after confirming no watch process is running.\nWrites: GITNEXUS_HOME/watch/project_commit_info.txt\nRemote URLs: only SSH URLs on github.com, gitlab.com, and gitee.com are allowed.\nRuns once immediately, then repeats on sync_interval_minutes.";
|
|
130
|
+
readonly 'help.command.watch.description': "Ambiguous: use `analyze --watch` for local files, or `auto-sync` for scheduled remotes";
|
|
131
|
+
readonly 'help.watch.details': "\n`gitnexus watch` does not start a watcher.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n";
|
|
132
|
+
readonly 'error.watch.ambiguous': "`gitnexus watch` is ambiguous.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n";
|
|
127
133
|
readonly 'help.command.analyze.description': "Index a repository (full analysis)";
|
|
128
134
|
readonly 'help.command.index.description': "Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)";
|
|
129
135
|
readonly 'help.command.serve.description': "Start local HTTP server for web UI connection";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -66,6 +66,7 @@ export const en = {
|
|
|
66
66
|
'tool.usage.cypher': 'Usage: gitnexus cypher <cypher_query>',
|
|
67
67
|
'tool.warn.unknownKind': "--kind '{{kind}}' is not a known symbol kind (e.g. Function, Class, Method); it will not narrow the result.",
|
|
68
68
|
'tool.detectChanges.noChanges': 'No changes detected.',
|
|
69
|
+
'tool.detectChanges.noOverlappingSymbols': 'Diff touched {{files}} file(s) but no indexed symbols overlap those hunks — not a clean tree.',
|
|
69
70
|
'tool.detectChanges.partial': 'PARTIAL RESULT: a graph query failed, so changed symbols may be missing. Do not read this as a clean pre-commit check.',
|
|
70
71
|
'tool.detectChanges.truncated': 'LISTING CAPPED: the changed-symbol list was capped, so it does not name every changed symbol. The counts and risk level still cover all of them.',
|
|
71
72
|
// The reassurance above is only true on its own. When the run also degraded,
|
|
@@ -126,6 +127,11 @@ export const en = {
|
|
|
126
127
|
'help.option.version': 'output the version number',
|
|
127
128
|
'help.command.setup.description': 'One-time setup: configure MCP for Cursor, Claude Code, Antigravity, OpenCode, CodeBuddy, Qoder, Codex',
|
|
128
129
|
'help.command.uninstall.description': 'Reverse `setup`: remove GitNexus MCP entries, skills, and hooks from all detected editors',
|
|
130
|
+
'help.command.autoSync.description': 'Control scheduled repository clone/pull and analysis from GITNEXUS_HOME/watch_config.yml',
|
|
131
|
+
'help.autoSync.details': '\nActions: init, start (default), restart, stop, status, reset\nConfiguration: GITNEXUS_HOME/watch_config.yml\nRuntime files: GITNEXUS_HOME/watch/watch.pid, watch.mutex, watch.owner.json, watch.status.json, auto-sync-state.json\nRecovery: mutexes with verified dead owners are reclaimed automatically; invalid or legacy mutexes fail closed and require manual removal after confirming no watch process is running.\nWrites: GITNEXUS_HOME/watch/project_commit_info.txt\nRemote URLs: only SSH URLs on github.com, gitlab.com, and gitee.com are allowed.\nRuns once immediately, then repeats on sync_interval_minutes.',
|
|
132
|
+
'help.command.watch.description': 'Ambiguous: use `analyze --watch` for local files, or `auto-sync` for scheduled remotes',
|
|
133
|
+
'help.watch.details': '\n`gitnexus watch` does not start a watcher.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n',
|
|
134
|
+
'error.watch.ambiguous': '`gitnexus watch` is ambiguous.\n Local working-tree incremental index: gitnexus analyze --watch\n Scheduled remote clone/pull + analyze: gitnexus auto-sync start\n',
|
|
129
135
|
'help.command.analyze.description': 'Index a repository (full analysis)',
|
|
130
136
|
'help.command.index.description': 'Register an existing .gitnexus/ folder into the global registry (no re-analysis needed)',
|
|
131
137
|
'help.command.serve.description': 'Start local HTTP server for web UI connection',
|