gitnexus 1.6.11-rc.2 → 1.6.11-rc.21
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 +43 -1
- package/dist/_shared/impact-risk.d.ts +37 -0
- package/dist/_shared/impact-risk.d.ts.map +1 -0
- package/dist/_shared/impact-risk.js +92 -0
- package/dist/_shared/impact-risk.js.map +1 -0
- package/dist/_shared/index.d.ts +2 -0
- package/dist/_shared/index.d.ts.map +1 -1
- package/dist/_shared/index.js +2 -0
- package/dist/_shared/index.js.map +1 -1
- package/dist/cli/ai-context.js +4 -4
- package/dist/cli/analyze-config.d.ts +2 -0
- package/dist/cli/analyze-config.js +16 -0
- package/dist/cli/analyze-options.d.ts +4 -0
- package/dist/cli/analyze.d.ts +17 -0
- package/dist/cli/analyze.js +43 -7
- package/dist/cli/eval-server.js +22 -5
- package/dist/cli/group.js +7 -1
- package/dist/cli/help-i18n.js +2 -0
- package/dist/cli/i18n/en.d.ts +12 -2
- package/dist/cli/i18n/en.js +12 -2
- package/dist/cli/i18n/resources.d.ts +22 -2
- package/dist/cli/i18n/zh-CN.d.ts +10 -0
- package/dist/cli/i18n/zh-CN.js +12 -2
- package/dist/cli/index.js +11 -4
- package/dist/cli/status.js +91 -6
- package/dist/cli/watch-queue.d.ts +41 -0
- package/dist/cli/watch-queue.js +184 -0
- package/dist/cli/watch.d.ts +20 -0
- package/dist/cli/watch.js +372 -0
- package/dist/cli/wiki.js +15 -2
- package/dist/config/ignore-service.d.ts +11 -0
- package/dist/config/ignore-service.js +41 -4
- package/dist/config/repo-control-file.d.ts +3 -0
- package/dist/config/repo-control-file.js +115 -0
- package/dist/core/group/config-parser.js +20 -2
- package/dist/core/group/cross-impact.d.ts +2 -1
- package/dist/core/group/cross-impact.js +33 -2
- package/dist/core/group/extractors/fs-utils.d.ts +2 -0
- package/dist/core/group/extractors/fs-utils.js +86 -0
- package/dist/core/group/extractors/graphql-extractor.d.ts +17 -0
- package/dist/core/group/extractors/graphql-extractor.js +652 -0
- package/dist/core/group/extractors/java-workspace-extractor.js +244 -31
- package/dist/core/group/extractors/manifest-extractor.d.ts +1 -1
- package/dist/core/group/extractors/manifest-extractor.js +1 -1
- package/dist/core/group/matching.js +8 -1
- package/dist/core/group/service.js +5 -1
- package/dist/core/group/storage.js +1 -0
- package/dist/core/group/sync.d.ts +3 -1
- package/dist/core/group/sync.js +43 -5
- package/dist/core/group/types.d.ts +19 -5
- package/dist/core/incremental/derived-writeback.d.ts +36 -0
- package/dist/core/incremental/derived-writeback.js +68 -0
- package/dist/core/incremental/subgraph-extract.d.ts +6 -4
- package/dist/core/incremental/subgraph-extract.js +7 -5
- package/dist/core/index-content-drift.d.ts +54 -0
- package/dist/core/index-content-drift.js +127 -0
- package/dist/core/ingestion/filesystem-walker.d.ts +15 -5
- package/dist/core/ingestion/filesystem-walker.js +20 -3
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.d.ts +22 -0
- package/dist/core/ingestion/frameworks/spring/dynamic-lookups.js +124 -0
- package/dist/core/ingestion/language-provider.d.ts +43 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.d.ts +62 -0
- package/dist/core/ingestion/languages/csharp/razor-view-components.js +954 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.d.ts +3 -0
- package/dist/core/ingestion/languages/csharp/resolution-config.js +6 -1
- package/dist/core/ingestion/languages/csharp/scope-resolver.js +8 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.d.ts +4 -0
- package/dist/core/ingestion/languages/java/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/java/captures.js +14 -1
- package/dist/core/ingestion/languages/java/lombok-synthesizer.d.ts +42 -0
- package/dist/core/ingestion/languages/java/lombok-synthesizer.js +439 -0
- package/dist/core/ingestion/languages/java/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/java/spring-dynamic-lookup.js +62 -0
- package/dist/core/ingestion/languages/java.js +2 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.d.ts +99 -0
- package/dist/core/ingestion/languages/jvm/accessor-synthesis.js +173 -0
- package/dist/core/ingestion/languages/jvm/beanspec.d.ts +17 -0
- package/dist/core/ingestion/languages/jvm/beanspec.js +42 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.d.ts +5 -0
- package/dist/core/ingestion/languages/kotlin/capture-side-channel.js +16 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +14 -1
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.d.ts +26 -0
- package/dist/core/ingestion/languages/kotlin/lombok-synthesizer.js +417 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +2 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/spring-dynamic-lookup.js +77 -0
- package/dist/core/ingestion/languages/kotlin.js +2 -0
- package/dist/core/ingestion/pipeline-phases/di.js +47 -14
- package/dist/core/ingestion/pipeline-phases/parse-impl.d.ts +3 -1
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +57 -86
- package/dist/core/ingestion/pipeline-phases/parse.d.ts +2 -0
- package/dist/core/ingestion/pipeline-phases/runner.d.ts +4 -1
- package/dist/core/ingestion/pipeline-phases/runner.js +34 -14
- package/dist/core/ingestion/pipeline-phases/scan.js +25 -13
- package/dist/core/ingestion/pipeline.d.ts +6 -0
- package/dist/core/ingestion/pipeline.js +44 -12
- package/dist/core/ingestion/scope-extractor.js +1 -0
- package/dist/core/ingestion/utils/symbol-labels.d.ts +2 -2
- package/dist/core/ingestion/utils/symbol-labels.js +2 -2
- package/dist/core/ingestion/workers/parse-worker.js +28 -2
- package/dist/core/lbug/lbug-adapter.d.ts +59 -0
- package/dist/core/lbug/lbug-adapter.js +154 -1
- package/dist/core/run-analyze.d.ts +17 -0
- package/dist/core/run-analyze.js +231 -23
- package/dist/core/search/fts-indexes.d.ts +19 -1
- package/dist/core/search/fts-indexes.js +28 -1
- package/dist/core/wiki/generator.js +8 -0
- package/dist/core/wiki/grok-client.d.ts +21 -0
- package/dist/core/wiki/grok-client.js +287 -0
- package/dist/core/wiki/llm-client.d.ts +1 -1
- package/dist/core/wiki/llm-client.js +5 -2
- package/dist/core/wiki/local-cli-client.d.ts +11 -0
- package/dist/core/wiki/local-cli-client.js +22 -9
- package/dist/mcp/local/local-backend.d.ts +24 -7
- package/dist/mcp/local/local-backend.js +190 -81
- package/dist/mcp/local/pdg-impact.d.ts +8 -4
- package/dist/mcp/local/pdg-impact.js +7 -2
- package/dist/mcp/repository-policy.d.ts +5 -1
- package/dist/mcp/repository-policy.js +48 -4
- package/dist/mcp/resources.js +2 -1
- package/dist/mcp/server.js +6 -5
- package/dist/mcp/tools.js +31 -19
- package/dist/server/api.js +23 -64
- package/dist/server/grep-params.d.ts +18 -0
- package/dist/server/grep-params.js +83 -0
- package/dist/server/grep-scan.d.ts +23 -0
- package/dist/server/grep-scan.js +107 -0
- package/dist/server/grep-worker.d.ts +1 -0
- package/dist/server/grep-worker.js +12 -0
- package/dist/server/mcp-http.d.ts +8 -0
- package/dist/server/mcp-http.js +16 -1
- package/dist/storage/file-hash.d.ts +5 -0
- package/dist/storage/file-hash.js +16 -6
- package/dist/storage/fs-atomic.d.ts +24 -0
- package/dist/storage/fs-atomic.js +86 -2
- package/dist/storage/git.d.ts +19 -8
- package/dist/storage/git.js +83 -24
- package/dist/storage/gitnexus-managed-paths.d.ts +36 -0
- package/dist/storage/gitnexus-managed-paths.js +46 -0
- package/dist/storage/parse-cache.d.ts +22 -4
- package/dist/storage/parse-cache.js +106 -29
- package/dist/storage/parsedfile-store.d.ts +34 -60
- package/dist/storage/parsedfile-store.js +177 -171
- package/dist/storage/repo-manager.d.ts +11 -1
- package/dist/storage/repo-manager.js +23 -2
- package/dist/storage/repo-meta.d.ts +12 -0
- package/dist/storage/v8-sidecar.d.ts +48 -0
- package/dist/storage/v8-sidecar.js +347 -0
- package/dist/types/pipeline.d.ts +14 -0
- package/package.json +4 -1
- package/scripts/cross-platform-shard.ts +4 -2
- package/scripts/cross-platform-tests.ts +7 -1
- package/skills/gitnexus-cli.md +11 -3
- package/skills/gitnexus-impact-analysis.md +9 -0
- package/web/assets/{agent-Dr4l5EOp.js → agent-CFqT4hjR.js} +108 -104
- package/web/assets/{index-2zdvEdzg.js → index-BMIniRtX.js} +3 -3
- package/web/index.html +1 -1
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
import { watch } from 'chokidar';
|
|
4
|
+
import { createWatchIgnorePredicate } from '../config/ignore-service.js';
|
|
5
|
+
import { analyzeFailureMayHaveMutatedLiveIndex, runFullAnalysis, } from '../core/run-analyze.js';
|
|
6
|
+
import { getGitRoot, hasGitDir } from '../storage/git.js';
|
|
7
|
+
import { GITNEXUS_DIR } from '../storage/repo-meta.js';
|
|
8
|
+
import { loadAnalyzeConfigStrict, mergeAnalyzeOptions, validateBranchName, } from './analyze-config.js';
|
|
9
|
+
import { ensureHeap } from './analyze.js';
|
|
10
|
+
import { cliError, cliInfo, cliWarn } from './cli-message.js';
|
|
11
|
+
import { WATCH_FULL_REFRESH_PATH, WatchRefreshQueue, } from './watch-queue.js';
|
|
12
|
+
const DEFAULT_DEBOUNCE_MS = 300;
|
|
13
|
+
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
14
|
+
const MAX_FILE_SIZE_KB = 32 * 1024;
|
|
15
|
+
const TRANSIENT_WATCH_ERROR_CODES = new Set(['EACCES', 'ENOENT', 'ENOTDIR', 'EPERM']);
|
|
16
|
+
function posixWatchPath(filePath) {
|
|
17
|
+
return filePath.replace(/\\/g, '/').replace(/^\.\/+/, '');
|
|
18
|
+
}
|
|
19
|
+
export function isRelevantWatchPath(filePath) {
|
|
20
|
+
const normalized = posixWatchPath(filePath);
|
|
21
|
+
return (normalized.length > 0 &&
|
|
22
|
+
normalized !== '.' &&
|
|
23
|
+
!normalized.startsWith('../') &&
|
|
24
|
+
!path.posix.isAbsolute(normalized) &&
|
|
25
|
+
!path.win32.isAbsolute(filePath));
|
|
26
|
+
}
|
|
27
|
+
function isIgnoreControlPath(filePath) {
|
|
28
|
+
const normalized = posixWatchPath(filePath);
|
|
29
|
+
return normalized === '.gitignore' || normalized === '.gitnexusignore';
|
|
30
|
+
}
|
|
31
|
+
function isConfigControlPath(filePath) {
|
|
32
|
+
return posixWatchPath(filePath) === '.gitnexusrc';
|
|
33
|
+
}
|
|
34
|
+
function isAnalyzerOwnedWatchPath(filePath) {
|
|
35
|
+
const normalized = posixWatchPath(filePath).replace(/\/+$/, '');
|
|
36
|
+
return normalized === GITNEXUS_DIR || normalized.startsWith(`${GITNEXUS_DIR}/`);
|
|
37
|
+
}
|
|
38
|
+
function repoRelativeWatchPath(repoPath, candidate) {
|
|
39
|
+
const relative = path.relative(repoPath, candidate).replace(/\\/g, '/');
|
|
40
|
+
if (!relative || relative.startsWith('../') || path.isAbsolute(relative))
|
|
41
|
+
return null;
|
|
42
|
+
return relative;
|
|
43
|
+
}
|
|
44
|
+
function setEnvironment(name, value) {
|
|
45
|
+
if (value === undefined)
|
|
46
|
+
delete process.env[name];
|
|
47
|
+
else
|
|
48
|
+
process.env[name] = value;
|
|
49
|
+
}
|
|
50
|
+
function positiveInteger(value, flag, maximum) {
|
|
51
|
+
if (value === undefined)
|
|
52
|
+
return undefined;
|
|
53
|
+
const parsed = Number(value);
|
|
54
|
+
if (!Number.isInteger(parsed) || parsed < 1)
|
|
55
|
+
throw new Error(`${flag} must be a positive integer`);
|
|
56
|
+
if (maximum !== undefined && parsed > maximum) {
|
|
57
|
+
throw new Error(`${flag} must not exceed ${maximum}`);
|
|
58
|
+
}
|
|
59
|
+
return parsed;
|
|
60
|
+
}
|
|
61
|
+
export async function resolveWatchOptions(repoPath, cli, baseline, reportIgnoredConfig = () => { }) {
|
|
62
|
+
const config = (await loadAnalyzeConfigStrict(repoPath)) ?? {};
|
|
63
|
+
const merged = mergeAnalyzeOptions(cli, config);
|
|
64
|
+
const unsupported = [
|
|
65
|
+
['--force', cli.force],
|
|
66
|
+
['--repair-fts', cli.repairFts],
|
|
67
|
+
['--embeddings', cli.embeddings],
|
|
68
|
+
['--drop-embeddings', cli.dropEmbeddings],
|
|
69
|
+
['--skills', cli.skills],
|
|
70
|
+
['--default-branch', cli.defaultBranch],
|
|
71
|
+
['--skip-agents-md', cli.skipAgentsMd],
|
|
72
|
+
['--skip-skills', cli.skipSkills],
|
|
73
|
+
['--no-stats', cli.stats === false],
|
|
74
|
+
['--self-commit', cli.selfCommit],
|
|
75
|
+
['--index-only', cli.indexOnly],
|
|
76
|
+
['--skip-git', cli.skipGit],
|
|
77
|
+
['walCheckpointThreshold', cli.walCheckpointThreshold],
|
|
78
|
+
['embeddingThreads', cli.embeddingThreads],
|
|
79
|
+
['embeddingBatchSize', cli.embeddingBatchSize],
|
|
80
|
+
['embeddingSubBatchSize', cli.embeddingSubBatchSize],
|
|
81
|
+
['embeddingDevice', cli.embeddingDevice],
|
|
82
|
+
['embeddingBaseUrl', cli.embeddingBaseUrl],
|
|
83
|
+
['embeddingModel', cli.embeddingModel],
|
|
84
|
+
['--embedding-auth-token', cli.embeddingAuthToken],
|
|
85
|
+
['--embedding-dims', cli.embeddingDims],
|
|
86
|
+
].filter(([, value]) => value !== undefined && value !== false);
|
|
87
|
+
if (unsupported.length > 0) {
|
|
88
|
+
throw new Error(`analyze --watch does not support ${unsupported.map(([name]) => name).join(', ')}`);
|
|
89
|
+
}
|
|
90
|
+
reportIgnoredConfig([
|
|
91
|
+
['embeddings', config.embeddings],
|
|
92
|
+
['dropEmbeddings', config.dropEmbeddings],
|
|
93
|
+
['defaultBranch', config.defaultBranch],
|
|
94
|
+
['skipAgentsMd', config.skipAgentsMd !== undefined],
|
|
95
|
+
['skipSkills', config.skipSkills !== undefined],
|
|
96
|
+
['stats', config.stats !== undefined],
|
|
97
|
+
['walCheckpointThreshold', config.walCheckpointThreshold],
|
|
98
|
+
['embeddingThreads', config.embeddingThreads],
|
|
99
|
+
['embeddingBatchSize', config.embeddingBatchSize],
|
|
100
|
+
['embeddingSubBatchSize', config.embeddingSubBatchSize],
|
|
101
|
+
['embeddingDevice', config.embeddingDevice],
|
|
102
|
+
['embeddingBaseUrl', config.embeddingBaseUrl],
|
|
103
|
+
['embeddingModel', config.embeddingModel],
|
|
104
|
+
]
|
|
105
|
+
.filter(([, value]) => value !== undefined && value !== false)
|
|
106
|
+
.map(([name]) => String(name)));
|
|
107
|
+
const branch = merged.branch === undefined ? undefined : validateBranchName(merged.branch, '--branch');
|
|
108
|
+
const workerPoolSize = positiveInteger(merged.workers, '--workers');
|
|
109
|
+
const workerTimeoutSeconds = positiveInteger(merged.workerTimeout, 'workerTimeout');
|
|
110
|
+
const maxFileSize = positiveInteger(merged.maxFileSize, 'maxFileSize', MAX_FILE_SIZE_KB);
|
|
111
|
+
setEnvironment('GITNEXUS_MAX_FILE_SIZE', maxFileSize === undefined ? baseline.maxFileSize : String(maxFileSize));
|
|
112
|
+
if (workerTimeoutSeconds !== undefined) {
|
|
113
|
+
process.env.GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS = String(workerTimeoutSeconds * 1000);
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
setEnvironment('GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS', baseline.workerTimeout);
|
|
117
|
+
}
|
|
118
|
+
setEnvironment('GITNEXUS_VERBOSE', merged.verbose ? '1' : baseline.verbose);
|
|
119
|
+
return {
|
|
120
|
+
pdg: merged.pdg,
|
|
121
|
+
branch,
|
|
122
|
+
registryName: merged.name,
|
|
123
|
+
allowDuplicateName: merged.allowDuplicateName,
|
|
124
|
+
workerPoolSize,
|
|
125
|
+
fetchWrappers: merged.fetchWrappers,
|
|
126
|
+
skipAgentsMd: true,
|
|
127
|
+
skipSkills: true,
|
|
128
|
+
noStats: true,
|
|
129
|
+
atomicIncremental: process.platform !== 'win32',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
function refreshSummary(result, observedPaths, durationMs, lastSuccessfulRefreshAt) {
|
|
133
|
+
const measured = result.incrementalStats;
|
|
134
|
+
const changed = measured?.changedFiles ?? (result.alreadyUpToDate ? 0 : observedPaths.length);
|
|
135
|
+
const reparsed = measured?.reparsedFiles ??
|
|
136
|
+
(typeof result.pipelineResult?.reparsedFileCount === 'number'
|
|
137
|
+
? result.pipelineResult.reparsedFileCount
|
|
138
|
+
: 0);
|
|
139
|
+
const dependents = measured?.affectedDependents ?? 0;
|
|
140
|
+
const mode = measured?.writeMode ?? (result.alreadyUpToDate ? 'no-op' : 'full');
|
|
141
|
+
return (`Refresh complete: ${changed} changed, ${reparsed} re-parsed, ` +
|
|
142
|
+
`${dependents} affected dependent(s), ${durationMs}ms, ${mode}; ` +
|
|
143
|
+
`last success ${lastSuccessfulRefreshAt}`);
|
|
144
|
+
}
|
|
145
|
+
async function waitUntilReady(watcher) {
|
|
146
|
+
await new Promise((resolve, reject) => {
|
|
147
|
+
const ready = () => {
|
|
148
|
+
watcher.off('error', failed);
|
|
149
|
+
resolve();
|
|
150
|
+
};
|
|
151
|
+
const failed = (error) => {
|
|
152
|
+
watcher.off('ready', ready);
|
|
153
|
+
reject(error);
|
|
154
|
+
};
|
|
155
|
+
watcher.once('ready', ready);
|
|
156
|
+
watcher.once('error', failed);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
class WatchControlReloadError extends Error {
|
|
160
|
+
constructor(cause) {
|
|
161
|
+
super(cause instanceof Error ? cause.message : String(cause), { cause });
|
|
162
|
+
this.name = 'WatchControlReloadError';
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
export function shouldStopAfterWatchRefreshFailure(error, paths) {
|
|
166
|
+
return (paths.length > 0 &&
|
|
167
|
+
!(error instanceof WatchControlReloadError) &&
|
|
168
|
+
analyzeFailureMayHaveMutatedLiveIndex(error));
|
|
169
|
+
}
|
|
170
|
+
/** Start the real filesystem watcher with bounded, serialized refreshes. */
|
|
171
|
+
export async function startWatchFileLoop(repoPath, debounceMs, refresh, onError, onWatcherError = (error) => onError(error, [])) {
|
|
172
|
+
let ignorePath = await createWatchIgnorePredicate(repoPath);
|
|
173
|
+
let ignoreControlValid = true;
|
|
174
|
+
const queue = new WatchRefreshQueue(async (paths) => {
|
|
175
|
+
if (paths.some(isIgnoreControlPath) || !ignoreControlValid) {
|
|
176
|
+
const retryingInvalidControls = !ignoreControlValid;
|
|
177
|
+
try {
|
|
178
|
+
ignorePath = await createWatchIgnorePredicate(repoPath);
|
|
179
|
+
ignoreControlValid = true;
|
|
180
|
+
watcher.add(repoPath);
|
|
181
|
+
}
|
|
182
|
+
catch (error) {
|
|
183
|
+
ignoreControlValid = false;
|
|
184
|
+
throw new WatchControlReloadError(retryingInvalidControls
|
|
185
|
+
? new Error('Ignore controls remain invalid; fix them before indexing more changes.', {
|
|
186
|
+
cause: error,
|
|
187
|
+
})
|
|
188
|
+
: error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
await refresh(paths);
|
|
192
|
+
}, onError, debounceMs, {
|
|
193
|
+
maxWaitMs: Math.max(2_000, debounceMs * 10),
|
|
194
|
+
maxPendingPaths: 1_000,
|
|
195
|
+
holdEventsUntilInitialRefresh: true,
|
|
196
|
+
isPriorityPath: (filePath) => isIgnoreControlPath(filePath) || isConfigControlPath(filePath),
|
|
197
|
+
});
|
|
198
|
+
const watcher = watch(repoPath, {
|
|
199
|
+
ignoreInitial: true,
|
|
200
|
+
atomic: true,
|
|
201
|
+
followSymlinks: false,
|
|
202
|
+
awaitWriteFinish: { stabilityThreshold: 100, pollInterval: 20 },
|
|
203
|
+
ignored: (candidate, stats) => {
|
|
204
|
+
const relative = repoRelativeWatchPath(repoPath, candidate);
|
|
205
|
+
if (relative !== null && isAnalyzerOwnedWatchPath(relative))
|
|
206
|
+
return true;
|
|
207
|
+
if (relative !== null && (isIgnoreControlPath(relative) || isConfigControlPath(relative))) {
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
return ignorePath(candidate, stats?.isDirectory() ?? false);
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
watcher.on('all', (event, changedPath) => {
|
|
214
|
+
if (event !== 'add' && event !== 'change' && event !== 'unlink')
|
|
215
|
+
return;
|
|
216
|
+
const relative = repoRelativeWatchPath(repoPath, changedPath);
|
|
217
|
+
if (relative && isRelevantWatchPath(relative) && !isAnalyzerOwnedWatchPath(relative)) {
|
|
218
|
+
queue.enqueue(relative);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
watcher.on('error', (error) => {
|
|
222
|
+
// Chokidar can surface a transient EPERM on Windows while an ignored
|
|
223
|
+
// analyzer-owned path is replaced. Re-arm the root and force one bounded
|
|
224
|
+
// catch-up refresh so a missed event cannot leave the graph stale. Other
|
|
225
|
+
// watcher errors may mean coverage was lost and remain fatal.
|
|
226
|
+
if (TRANSIENT_WATCH_ERROR_CODES.has(error.code ?? '')) {
|
|
227
|
+
watcher.add(repoPath);
|
|
228
|
+
queue.enqueue(WATCH_FULL_REFRESH_PATH);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
onWatcherError(error);
|
|
232
|
+
});
|
|
233
|
+
try {
|
|
234
|
+
await waitUntilReady(watcher);
|
|
235
|
+
await queue.runInitial();
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
await watcher.close();
|
|
239
|
+
await queue.close();
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
waitForIdle: () => queue.waitForIdle(),
|
|
244
|
+
close: async () => {
|
|
245
|
+
await watcher.close();
|
|
246
|
+
await queue.close();
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export async function watchCommandWithRunnerIdentity(runnerIdentityAtBootstrap, inputPath, cliOptions = {}) {
|
|
251
|
+
if (await ensureHeap({ cleanForwardedTermination: true }))
|
|
252
|
+
return;
|
|
253
|
+
const requestedRepoPath = inputPath ? path.resolve(inputPath) : getGitRoot(process.cwd());
|
|
254
|
+
if (requestedRepoPath === null || !hasGitDir(requestedRepoPath)) {
|
|
255
|
+
cliError(' gitnexus analyze --watch requires a Git repository.');
|
|
256
|
+
process.exitCode = 1;
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const repoPath = await fs.realpath(requestedRepoPath);
|
|
260
|
+
const baselineEnvironment = {
|
|
261
|
+
maxFileSize: process.env.GITNEXUS_MAX_FILE_SIZE,
|
|
262
|
+
workerTimeout: process.env.GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS,
|
|
263
|
+
verbose: process.env.GITNEXUS_VERBOSE,
|
|
264
|
+
};
|
|
265
|
+
try {
|
|
266
|
+
let ignoredConfigSignature;
|
|
267
|
+
const reportIgnoredConfig = (names) => {
|
|
268
|
+
const signature = [...names].sort().join(',');
|
|
269
|
+
if (signature === ignoredConfigSignature)
|
|
270
|
+
return;
|
|
271
|
+
ignoredConfigSignature = signature;
|
|
272
|
+
if (names.length > 0) {
|
|
273
|
+
cliWarn(`Watch mode ignores unsupported .gitnexusrc settings: ${names.join(', ')}.`);
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
let debounceMs;
|
|
277
|
+
let analyzeOptions;
|
|
278
|
+
try {
|
|
279
|
+
debounceMs =
|
|
280
|
+
positiveInteger(cliOptions.debounce ?? String(DEFAULT_DEBOUNCE_MS), '--debounce', MAX_TIMER_DELAY_MS) ?? DEFAULT_DEBOUNCE_MS;
|
|
281
|
+
analyzeOptions = await resolveWatchOptions(repoPath, cliOptions, baselineEnvironment, reportIgnoredConfig);
|
|
282
|
+
}
|
|
283
|
+
catch (error) {
|
|
284
|
+
cliError(` ${error instanceof Error ? error.message : String(error)}`);
|
|
285
|
+
process.exitCode = 1;
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
let stopWatching;
|
|
289
|
+
const stopped = new Promise((resolve) => {
|
|
290
|
+
stopWatching = resolve;
|
|
291
|
+
});
|
|
292
|
+
const stop = () => stopWatching();
|
|
293
|
+
process.once('SIGINT', stop);
|
|
294
|
+
process.once('SIGTERM', stop);
|
|
295
|
+
try {
|
|
296
|
+
let loop;
|
|
297
|
+
let fatalRefreshError;
|
|
298
|
+
let configControlValid = true;
|
|
299
|
+
let lastSuccessfulRefreshAt;
|
|
300
|
+
try {
|
|
301
|
+
loop = await startWatchFileLoop(repoPath, debounceMs, async (paths) => {
|
|
302
|
+
if (paths.some(isConfigControlPath) || !configControlValid) {
|
|
303
|
+
const retryingInvalidConfig = !configControlValid;
|
|
304
|
+
try {
|
|
305
|
+
analyzeOptions = await resolveWatchOptions(repoPath, cliOptions, baselineEnvironment, reportIgnoredConfig);
|
|
306
|
+
configControlValid = true;
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
configControlValid = false;
|
|
310
|
+
throw new WatchControlReloadError(retryingInvalidConfig
|
|
311
|
+
? new Error('Configuration remains invalid; fix it before indexing more changes.', {
|
|
312
|
+
cause: error,
|
|
313
|
+
})
|
|
314
|
+
: error);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
const startedAt = Date.now();
|
|
318
|
+
const result = await runFullAnalysis(repoPath, analyzeOptions, {
|
|
319
|
+
onProgress: () => { },
|
|
320
|
+
onLog: process.env.GITNEXUS_VERBOSE === '1'
|
|
321
|
+
? (message) => cliInfo(` ${message}`)
|
|
322
|
+
: undefined,
|
|
323
|
+
}, runnerIdentityAtBootstrap);
|
|
324
|
+
lastSuccessfulRefreshAt = new Date().toISOString();
|
|
325
|
+
if (paths.length === 0) {
|
|
326
|
+
cliInfo(result.alreadyUpToDate
|
|
327
|
+
? `Watching ${repoPath}; index is up to date.`
|
|
328
|
+
: `Watching ${repoPath}; initial index ready in ${Date.now() - startedAt}ms.`);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
cliInfo(refreshSummary(result, paths, Date.now() - startedAt, lastSuccessfulRefreshAt));
|
|
332
|
+
}
|
|
333
|
+
}, (error, paths) => {
|
|
334
|
+
const detail = paths.length > 0 ? ` (${paths.length} queued path(s))` : '';
|
|
335
|
+
if (shouldStopAfterWatchRefreshFailure(error, paths)) {
|
|
336
|
+
fatalRefreshError = error;
|
|
337
|
+
cliError(`Refresh failed${detail}: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
338
|
+
'Watch mode is stopping because the live index may have been updated in place.');
|
|
339
|
+
stopWatching();
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const lastSuccess = lastSuccessfulRefreshAt ?? 'none yet';
|
|
343
|
+
cliWarn(`Refresh failed${detail}: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
344
|
+
`Retry scheduled; last success ${lastSuccess}.`);
|
|
345
|
+
}, (error) => {
|
|
346
|
+
fatalRefreshError = error;
|
|
347
|
+
cliError(`Watcher failed: ${error instanceof Error ? error.message : String(error)}. ` +
|
|
348
|
+
'Watch mode is stopping.');
|
|
349
|
+
stopWatching();
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
cliError(` Unable to start watcher: ${error instanceof Error ? error.message : String(error)}`);
|
|
354
|
+
process.exitCode = 1;
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
await stopped;
|
|
358
|
+
await loop.close();
|
|
359
|
+
if (fatalRefreshError !== undefined)
|
|
360
|
+
process.exitCode = 1;
|
|
361
|
+
}
|
|
362
|
+
finally {
|
|
363
|
+
process.removeListener('SIGINT', stop);
|
|
364
|
+
process.removeListener('SIGTERM', stop);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
finally {
|
|
368
|
+
setEnvironment('GITNEXUS_MAX_FILE_SIZE', baselineEnvironment.maxFileSize);
|
|
369
|
+
setEnvironment('GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS', baselineEnvironment.workerTimeout);
|
|
370
|
+
setEnvironment('GITNEXUS_VERBOSE', baselineEnvironment.verbose);
|
|
371
|
+
}
|
|
372
|
+
}
|
package/dist/cli/wiki.js
CHANGED
|
@@ -13,6 +13,7 @@ import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig, } from '../sto
|
|
|
13
13
|
import { WikiGenerator } from '../core/wiki/generator.js';
|
|
14
14
|
import { MINIMAX_MODEL_IDS, MINIMAX_OPENAI_BASE_URLS, parseLLMAllowedInsecureHttpHosts, resolveLLMConfig, } from '../core/wiki/llm-client.js';
|
|
15
15
|
import { detectCursorCLI } from '../core/wiki/cursor-client.js';
|
|
16
|
+
import { detectGrokCLI } from '../core/wiki/grok-client.js';
|
|
16
17
|
import { detectLocalCLI } from '../core/wiki/local-cli-client.js';
|
|
17
18
|
import { logger } from '../core/logger.js';
|
|
18
19
|
function parsePositiveIntegerOption(value, flag, multiplier = 1) {
|
|
@@ -32,7 +33,8 @@ function isLocalProvider(provider) {
|
|
|
32
33
|
return (provider === 'cursor' ||
|
|
33
34
|
provider === 'claude' ||
|
|
34
35
|
provider === 'codex' ||
|
|
35
|
-
provider === 'opencode'
|
|
36
|
+
provider === 'opencode' ||
|
|
37
|
+
provider === 'grok');
|
|
36
38
|
}
|
|
37
39
|
function localModelConfigKey(provider) {
|
|
38
40
|
if (provider === 'cursor')
|
|
@@ -43,6 +45,8 @@ function localModelConfigKey(provider) {
|
|
|
43
45
|
return 'codexModel';
|
|
44
46
|
if (provider === 'opencode')
|
|
45
47
|
return 'opencodeModel';
|
|
48
|
+
if (provider === 'grok')
|
|
49
|
+
return 'grokModel';
|
|
46
50
|
throw new Error(`Unsupported local provider: ${provider}`);
|
|
47
51
|
}
|
|
48
52
|
/**
|
|
@@ -244,7 +248,7 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
244
248
|
if (!llmConfig.apiKey && !isLocalProvider(llmConfig.provider)) {
|
|
245
249
|
console.log(' Error: No LLM API key found.');
|
|
246
250
|
console.log(' Set MINIMAX_API_KEY, GITNEXUS_API_KEY, or OPENAI_API_KEY,');
|
|
247
|
-
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex|opencode.\n');
|
|
251
|
+
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex|opencode|grok.\n');
|
|
248
252
|
process.exitCode = 1;
|
|
249
253
|
return;
|
|
250
254
|
}
|
|
@@ -258,6 +262,7 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
258
262
|
const hasClaude = detectLocalCLI('claude');
|
|
259
263
|
const hasCodex = detectLocalCLI('codex');
|
|
260
264
|
const hasOpenCode = detectLocalCLI('opencode');
|
|
265
|
+
const hasGrok = detectGrokCLI();
|
|
261
266
|
const localChoices = [];
|
|
262
267
|
// Provider selection
|
|
263
268
|
console.log(' [1] OpenAI (api.openai.com)');
|
|
@@ -299,6 +304,14 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
299
304
|
});
|
|
300
305
|
console.log(` [${choice}] OpenCode CLI (local, uses your OpenCode login/config)`);
|
|
301
306
|
}
|
|
307
|
+
if (hasGrok) {
|
|
308
|
+
const choice = String(nextChoice++);
|
|
309
|
+
localChoices.push({
|
|
310
|
+
choice,
|
|
311
|
+
provider: 'grok',
|
|
312
|
+
});
|
|
313
|
+
console.log(` [${choice}] Grok CLI (local, uses your Grok Build login)`);
|
|
314
|
+
}
|
|
302
315
|
console.log('');
|
|
303
316
|
const maxChoice = String(nextChoice - 1);
|
|
304
317
|
const choice = await prompt(` Select provider (1/${maxChoice}): `);
|
|
@@ -14,8 +14,19 @@ export interface IgnoreOptions {
|
|
|
14
14
|
noGitignore?: boolean;
|
|
15
15
|
/** Skip core.excludesFile and $GIT_COMMON_DIR/info/exclude. Defaults to GITNEXUS_NO_GLOBAL_IGNORE env var. */
|
|
16
16
|
noGlobalIgnore?: boolean;
|
|
17
|
+
/** Fail repository-control reloads closed so long-lived watchers keep their prior predicate. */
|
|
18
|
+
strictRepoControlFiles?: boolean;
|
|
17
19
|
}
|
|
18
20
|
export declare const loadIgnoreRules: (repoPath: string, options?: IgnoreOptions) => Promise<Ignore | null>;
|
|
21
|
+
/**
|
|
22
|
+
* Build a synchronous predicate for long-lived filesystem watchers.
|
|
23
|
+
*
|
|
24
|
+
* Unlike {@link createIgnoreFilter}, callers pass ordinary absolute or
|
|
25
|
+
* repository-relative paths instead of path-scurry `Path` objects. The rule
|
|
26
|
+
* precedence deliberately mirrors the scanner: explicit negations win over
|
|
27
|
+
* hardcoded defaults unless a more-specific rule re-ignores the path.
|
|
28
|
+
*/
|
|
29
|
+
export declare const createWatchIgnorePredicate: (repoPath: string, options?: IgnoreOptions) => Promise<(candidatePath: string, isDirectory?: boolean) => boolean>;
|
|
19
30
|
/**
|
|
20
31
|
* Create a glob-compatible ignore filter combining:
|
|
21
32
|
* - .gitignore / .gitnexusignore patterns (via `ignore` package)
|
|
@@ -2,6 +2,7 @@ import ignore from 'ignore';
|
|
|
2
2
|
import { existsSync } from 'fs';
|
|
3
3
|
import fs from 'fs/promises';
|
|
4
4
|
import nodePath from 'path';
|
|
5
|
+
import { readRepoControlFile } from './repo-control-file.js';
|
|
5
6
|
import { logger } from '../core/logger.js';
|
|
6
7
|
import { getCoreExcludesFilePath, getGitInfoExcludePath } from '../storage/git.js';
|
|
7
8
|
const DEFAULT_IGNORE_LIST = new Set([
|
|
@@ -384,19 +385,55 @@ export const loadIgnoreRules = async (repoPath, options) => {
|
|
|
384
385
|
const filenames = skipGitignore ? ['.gitnexusignore'] : ['.gitignore', '.gitnexusignore'];
|
|
385
386
|
for (const filename of filenames) {
|
|
386
387
|
try {
|
|
387
|
-
const content =
|
|
388
|
+
const content = options?.strictRepoControlFiles
|
|
389
|
+
? await readRepoControlFile(repoPath, filename)
|
|
390
|
+
: await fs.readFile(nodePath.join(repoPath, filename), 'utf-8');
|
|
391
|
+
if (content === null)
|
|
392
|
+
continue;
|
|
388
393
|
ig.add(content);
|
|
389
394
|
hasRules = true;
|
|
390
395
|
}
|
|
391
396
|
catch (err) {
|
|
392
397
|
const code = err.code;
|
|
393
|
-
if (code
|
|
394
|
-
|
|
395
|
-
|
|
398
|
+
if (!options?.strictRepoControlFiles && code === 'ENOENT')
|
|
399
|
+
continue;
|
|
400
|
+
if (options?.strictRepoControlFiles)
|
|
401
|
+
throw err;
|
|
402
|
+
logger.warn(` Warning: could not read ${filename}: ${err.message}`);
|
|
396
403
|
}
|
|
397
404
|
}
|
|
398
405
|
return hasRules ? ig : null;
|
|
399
406
|
};
|
|
407
|
+
/**
|
|
408
|
+
* Build a synchronous predicate for long-lived filesystem watchers.
|
|
409
|
+
*
|
|
410
|
+
* Unlike {@link createIgnoreFilter}, callers pass ordinary absolute or
|
|
411
|
+
* repository-relative paths instead of path-scurry `Path` objects. The rule
|
|
412
|
+
* precedence deliberately mirrors the scanner: explicit negations win over
|
|
413
|
+
* hardcoded defaults unless a more-specific rule re-ignores the path.
|
|
414
|
+
*/
|
|
415
|
+
export const createWatchIgnorePredicate = async (repoPath, options) => {
|
|
416
|
+
const ig = await loadIgnoreRules(repoPath, { ...options, strictRepoControlFiles: true });
|
|
417
|
+
const repoRoot = nodePath.resolve(repoPath);
|
|
418
|
+
return (candidatePath, isDirectory = false) => {
|
|
419
|
+
const absolute = nodePath.isAbsolute(candidatePath)
|
|
420
|
+
? nodePath.resolve(candidatePath)
|
|
421
|
+
: nodePath.resolve(repoRoot, candidatePath);
|
|
422
|
+
const rel = nodePath.relative(repoRoot, absolute).replace(/\\/g, '/');
|
|
423
|
+
if (!rel)
|
|
424
|
+
return false;
|
|
425
|
+
if (rel === '..' || rel.startsWith('../') || nodePath.isAbsolute(rel))
|
|
426
|
+
return true;
|
|
427
|
+
if (ig && hasExplicitUnignore(ig, rel) && !ig.ignores(isDirectory ? `${rel}/` : rel)) {
|
|
428
|
+
return false;
|
|
429
|
+
}
|
|
430
|
+
if (ig && ig.ignores(isDirectory ? `${rel}/` : rel))
|
|
431
|
+
return true;
|
|
432
|
+
if (isDirectory && isHardcodedIgnoredDirectoryAtPath(repoRoot, absolute))
|
|
433
|
+
return true;
|
|
434
|
+
return shouldIgnorePath(rel);
|
|
435
|
+
};
|
|
436
|
+
};
|
|
400
437
|
/**
|
|
401
438
|
* Walk ancestor segments of `rel` and check whether `.gitnexusignore`
|
|
402
439
|
* (or `.gitignore`) contains an explicit `!pattern` negation that
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import * as path from 'node:path';
|
|
3
|
+
export const MAX_REPO_CONTROL_FILE_BYTES = 1024 * 1024;
|
|
4
|
+
/** Read a bounded, regular control file owned by the repository root. */
|
|
5
|
+
export async function readRepoControlFile(repoRoot, filename) {
|
|
6
|
+
const requestedRoot = path.resolve(repoRoot);
|
|
7
|
+
const requested = path.resolve(requestedRoot, filename);
|
|
8
|
+
const relative = path.relative(requestedRoot, requested);
|
|
9
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
10
|
+
throw new Error(`${filename} resolves outside the repository root`);
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const canonicalRoot = fs.realpathSync(requestedRoot);
|
|
14
|
+
const beforeOpen = fs.lstatSync(requested);
|
|
15
|
+
if (beforeOpen.isSymbolicLink())
|
|
16
|
+
throw new Error(`${filename} must not be a symbolic link`);
|
|
17
|
+
if (!beforeOpen.isFile())
|
|
18
|
+
throw new Error(`${filename} must be a regular file`);
|
|
19
|
+
if (beforeOpen.nlink !== 1)
|
|
20
|
+
throw new Error(`${filename} must not be a hard link`);
|
|
21
|
+
if (beforeOpen.size > MAX_REPO_CONTROL_FILE_BYTES) {
|
|
22
|
+
throw new Error(`${filename} exceeds ${MAX_REPO_CONTROL_FILE_BYTES} bytes`);
|
|
23
|
+
}
|
|
24
|
+
return await new Promise((resolve, reject) => {
|
|
25
|
+
const stream = fs.createReadStream(requested, {
|
|
26
|
+
flags: 'r',
|
|
27
|
+
start: 0,
|
|
28
|
+
end: MAX_REPO_CONTROL_FILE_BYTES,
|
|
29
|
+
autoClose: true,
|
|
30
|
+
});
|
|
31
|
+
const chunks = [];
|
|
32
|
+
let totalBytes = 0;
|
|
33
|
+
let validated = false;
|
|
34
|
+
let settled = false;
|
|
35
|
+
const finish = (value) => {
|
|
36
|
+
if (settled)
|
|
37
|
+
return;
|
|
38
|
+
settled = true;
|
|
39
|
+
resolve(value);
|
|
40
|
+
};
|
|
41
|
+
const fail = (error) => {
|
|
42
|
+
if (settled)
|
|
43
|
+
return;
|
|
44
|
+
settled = true;
|
|
45
|
+
reject(error);
|
|
46
|
+
};
|
|
47
|
+
stream.pause();
|
|
48
|
+
stream.once('open', (fd) => {
|
|
49
|
+
try {
|
|
50
|
+
const opened = fs.fstatSync(fd);
|
|
51
|
+
if (!opened.isFile())
|
|
52
|
+
throw new Error(`${filename} must be a regular file`);
|
|
53
|
+
if (opened.nlink !== 1)
|
|
54
|
+
throw new Error(`${filename} must not be a hard link`);
|
|
55
|
+
if (opened.size > MAX_REPO_CONTROL_FILE_BYTES) {
|
|
56
|
+
throw new Error(`${filename} exceeds ${MAX_REPO_CONTROL_FILE_BYTES} bytes`);
|
|
57
|
+
}
|
|
58
|
+
const entry = fs.lstatSync(requested);
|
|
59
|
+
if (entry.isSymbolicLink())
|
|
60
|
+
throw new Error(`${filename} must not be a symbolic link`);
|
|
61
|
+
if (!entry.isFile() ||
|
|
62
|
+
entry.nlink !== 1 ||
|
|
63
|
+
entry.dev !== opened.dev ||
|
|
64
|
+
entry.ino !== opened.ino) {
|
|
65
|
+
throw new Error(`${filename} moved or was replaced while being opened`);
|
|
66
|
+
}
|
|
67
|
+
const canonicalFile = fs.realpathSync(requested);
|
|
68
|
+
const canonicalRelative = path.relative(canonicalRoot, canonicalFile);
|
|
69
|
+
if (canonicalRelative.startsWith('..') || path.isAbsolute(canonicalRelative)) {
|
|
70
|
+
throw new Error(`${filename} resolves outside the repository root`);
|
|
71
|
+
}
|
|
72
|
+
const canonical = fs.statSync(canonicalFile);
|
|
73
|
+
if (canonical.nlink !== 1 ||
|
|
74
|
+
canonical.dev !== opened.dev ||
|
|
75
|
+
canonical.ino !== opened.ino) {
|
|
76
|
+
throw new Error(`${filename} moved or was replaced while being opened`);
|
|
77
|
+
}
|
|
78
|
+
validated = true;
|
|
79
|
+
stream.resume();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
fail(error);
|
|
83
|
+
stream.destroy();
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
stream.on('data', (chunk) => {
|
|
87
|
+
const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
88
|
+
totalBytes += bytes.length;
|
|
89
|
+
if (totalBytes > MAX_REPO_CONTROL_FILE_BYTES) {
|
|
90
|
+
fail(new Error(`${filename} exceeds ${MAX_REPO_CONTROL_FILE_BYTES} bytes`));
|
|
91
|
+
stream.destroy();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
chunks.push(bytes);
|
|
95
|
+
});
|
|
96
|
+
stream.once('end', () => {
|
|
97
|
+
if (!validated) {
|
|
98
|
+
fail(new Error(`${filename} could not be validated`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
finish(Buffer.concat(chunks, totalBytes).toString('utf8'));
|
|
102
|
+
});
|
|
103
|
+
stream.once('error', fail);
|
|
104
|
+
stream.once('close', () => {
|
|
105
|
+
if (!settled)
|
|
106
|
+
fail(new Error(`${filename} closed before it could be read`));
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
if (error.code === 'ENOENT')
|
|
112
|
+
return null;
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|