@psnext/lscg 0.1.4 → 0.1.6
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 +117 -15
- package/dist/bin/lscg.js +0 -0
- package/dist/src/cli-progress.d.ts +7 -0
- package/dist/src/cli-progress.js +59 -0
- package/dist/src/cli.js +62 -9
- package/dist/src/explore/sigma-provider.d.ts +27 -0
- package/dist/src/explore/sigma-provider.js +87 -0
- package/dist/src/explore/sigma-render.d.ts +18 -0
- package/dist/src/explore/sigma-render.js +67 -0
- package/dist/src/graph/attribution.d.ts +2 -2
- package/dist/src/graph/attribution.js +36 -13
- package/dist/src/graph/explore.d.ts +20 -0
- package/dist/src/graph/explore.js +200 -0
- package/dist/src/graph/repository.d.ts +36 -4
- package/dist/src/graph/repository.js +443 -159
- package/dist/src/graph/repositoryScanWorker.d.ts +21 -0
- package/dist/src/graph/repositoryScanWorker.js +45 -0
- package/dist/src/index.d.ts +6 -0
- package/dist/src/index.js +5 -0
- package/dist/src/mcp/server.js +21 -3
- package/dist/src/parser/treeSitter.js +20 -1
- package/dist/src/scanner/artifactInventory.d.ts +35 -0
- package/dist/src/scanner/artifactInventory.js +139 -0
- package/dist/src/scanner/attributionPlugin.d.ts +5 -0
- package/dist/src/scanner/attributionPlugin.js +16 -0
- package/dist/src/scanner/discover.js +89 -16
- package/dist/src/scanner/fingerprint.js +5 -0
- package/dist/src/scanner/javaDependencyPlugin.d.ts +5 -0
- package/dist/src/scanner/javaDependencyPlugin.js +107 -0
- package/dist/src/scanner/javaPlugin.d.ts +5 -0
- package/dist/src/scanner/javaPlugin.js +199 -0
- package/dist/src/scanner/javaScanWorker.d.ts +2 -0
- package/dist/src/scanner/javaScanWorker.js +8 -0
- package/dist/src/scanner/packageParseWorker.d.ts +17 -0
- package/dist/src/scanner/packageParseWorker.js +30 -0
- package/dist/src/scanner/packagePlugin.js +83 -24
- package/dist/src/scanner/parallelScan.d.ts +2 -0
- package/dist/src/scanner/parallelScan.js +32 -0
- package/dist/src/scanner/plugins.d.ts +38 -4
- package/dist/src/scanner/plugins.js +58 -5
- package/dist/src/scanner/pythonPlugin.d.ts +5 -0
- package/dist/src/scanner/pythonPlugin.js +198 -0
- package/dist/src/scanner/pythonScanWorker.d.ts +2 -0
- package/dist/src/scanner/pythonScanWorker.js +8 -0
- package/dist/src/storage/connection.js +85 -0
- package/dist/src/storage/database.d.ts +1 -0
- package/dist/src/storage/database.js +1 -0
- package/dist/src/storage/explore-queries.d.ts +52 -0
- package/dist/src/storage/explore-queries.js +184 -0
- package/dist/src/storage/graph-writes.d.ts +22 -3
- package/dist/src/storage/graph-writes.js +167 -20
- package/dist/src/storage/manifest-inventory.d.ts +23 -0
- package/dist/src/storage/manifest-inventory.js +82 -0
- package/dist/src/storage/plugin-graph.js +3 -3
- package/dist/src/storage/queries.d.ts +10 -3
- package/dist/src/storage/queries.js +99 -24
- package/dist/src/storage/schema.d.ts +2 -2
- package/dist/src/storage/schema.js +48 -1
- package/dist/src/types.d.ts +110 -6
- package/dist/src/watch.d.ts +20 -2
- package/dist/src/watch.js +211 -46
- package/package.json +9 -3
package/dist/src/watch.js
CHANGED
|
@@ -1,30 +1,118 @@
|
|
|
1
|
+
import { readdirSync, watch as watchFileSystem } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
1
3
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
2
|
-
import { repositoryForRoot, scanRepository } from './graph/repository.js';
|
|
4
|
+
import { drainRepositoryEnrichment, repositoryForRoot, scanRepository } from './graph/repository.js';
|
|
3
5
|
import { collectRepositoryFingerprint } from './scanner/fingerprint.js';
|
|
6
|
+
import { discoverSourceFiles } from './scanner/discover.js';
|
|
7
|
+
import { isSupportedSourceFile } from './parser/treeSitter.js';
|
|
4
8
|
const DEFAULT_POLL_INTERVAL_MS = 1000;
|
|
9
|
+
const DEFAULT_DEBOUNCE_MS = 100;
|
|
10
|
+
const IGNORED_WATCH_DIRECTORIES = new Set(['.git', 'node_modules', 'dist', 'coverage', '.next', '.turbo', '.cache', '.sling']);
|
|
5
11
|
export async function watchRepository(options = {}, dependencies = {}) {
|
|
6
12
|
const scope = resolveWatchScope(options.scope);
|
|
7
13
|
const repository = repositoryForRoot(options.root);
|
|
8
14
|
const scanFn = dependencies.scanRepository ?? scanRepository;
|
|
15
|
+
const drainEnrichment = dependencies.drainEnrichment ?? drainRepositoryEnrichment;
|
|
9
16
|
const fingerprintFn = dependencies.fingerprint ?? ((root) => collectRepositoryFingerprint(root));
|
|
17
|
+
const watcherFactory = dependencies.watcherFactory ?? defaultWatcherFactory;
|
|
10
18
|
const emit = dependencies.emit ?? defaultEmit;
|
|
11
19
|
const wait = dependencies.wait ?? defaultWait;
|
|
12
20
|
const signal = dependencies.signal;
|
|
13
21
|
const mode = dependencies.mode ?? 'watch';
|
|
22
|
+
const progress = dependencies.progress;
|
|
14
23
|
const intervalMs = options.intervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
24
|
+
const debounceMs = options.debounceMs ?? DEFAULT_DEBOUNCE_MS;
|
|
15
25
|
let scans = 0;
|
|
16
26
|
let stopped = false;
|
|
17
|
-
let
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
27
|
+
let scanning = false;
|
|
28
|
+
let polling = false;
|
|
29
|
+
let currentFingerprint;
|
|
30
|
+
let followUpQueued = false;
|
|
31
|
+
let pendingEventPaths = new Set();
|
|
32
|
+
let failed = false;
|
|
33
|
+
let wakeWaiting;
|
|
34
|
+
const watchers = [];
|
|
35
|
+
const wake = () => {
|
|
36
|
+
const resolve = wakeWaiting;
|
|
37
|
+
wakeWaiting = undefined;
|
|
38
|
+
resolve?.();
|
|
39
|
+
};
|
|
40
|
+
const queueReconciliation = () => {
|
|
41
|
+
if (scanning) {
|
|
42
|
+
followUpQueued = true;
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
pendingEventPaths.add('');
|
|
46
|
+
}
|
|
47
|
+
wake();
|
|
48
|
+
};
|
|
49
|
+
const usePollingFallback = () => {
|
|
50
|
+
if (polling)
|
|
51
|
+
return;
|
|
52
|
+
polling = true;
|
|
53
|
+
currentFingerprint = undefined;
|
|
54
|
+
closeWatchers(watchers);
|
|
55
|
+
reportWatchProgress(progress, { event: 'polling-fallback', mode, repository, scope, detail: 'filesystem watcher unavailable; polling fallback enabled' });
|
|
56
|
+
queueReconciliation();
|
|
57
|
+
};
|
|
58
|
+
const handleWatchEvent = (watchedDirectory, event, filename) => {
|
|
59
|
+
if (event === 'rename' || filename === null) {
|
|
60
|
+
usePollingFallback();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
const relativePath = relevantWatchPath(repository.root, watchedDirectory, filename);
|
|
64
|
+
if (!relativePath)
|
|
65
|
+
return;
|
|
66
|
+
pendingEventPaths.add(relativePath);
|
|
67
|
+
if (scanning)
|
|
68
|
+
followUpQueued = true;
|
|
69
|
+
wake();
|
|
70
|
+
};
|
|
71
|
+
const addWatcher = (directory, recursive) => {
|
|
72
|
+
const watcher = watcherFactory(directory, { recursive }, (event, filename) => handleWatchEvent(directory, event, filename));
|
|
73
|
+
watcher.on('error', () => usePollingFallback());
|
|
74
|
+
watchers.push(watcher);
|
|
75
|
+
};
|
|
24
76
|
try {
|
|
77
|
+
try {
|
|
78
|
+
addWatcher(repository.root, true);
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
closeWatchers(watchers);
|
|
82
|
+
try {
|
|
83
|
+
for (const directory of watchDirectories(repository.root)) {
|
|
84
|
+
addWatcher(directory, false);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
closeWatchers(watchers);
|
|
89
|
+
polling = true;
|
|
90
|
+
reportWatchProgress(progress, { event: 'polling-fallback', mode, repository, scope, detail: 'filesystem watcher unavailable; polling fallback enabled' });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
emit({
|
|
94
|
+
event: 'watching',
|
|
95
|
+
mode,
|
|
96
|
+
repository,
|
|
97
|
+
scope
|
|
98
|
+
});
|
|
99
|
+
reportWatchProgress(progress, { event: 'watching', mode, repository, scope, detail: 'watch session started' });
|
|
25
100
|
while (!signal?.aborted) {
|
|
26
|
-
|
|
27
|
-
const
|
|
101
|
+
scanning = true;
|
|
102
|
+
const phase = scans === 0 ? 'initial-scan' : 'rescan';
|
|
103
|
+
reportWatchProgress(progress, { event: phase, mode, repository, scope, detail: scans === 0 ? 'starting initial scan' : 'starting rescan' });
|
|
104
|
+
const scanStartFingerprint = polling ? await fingerprintFn(repository.root) : undefined;
|
|
105
|
+
const structuralSummary = await scanFn({ root: repository.root, scope, attribution: options.attribution, plugins: options.plugins, progress });
|
|
106
|
+
// A watch owns in-process enrichment. Awaiting it while `scanning` is
|
|
107
|
+
// true serializes durable work with structural replacement; a filesystem
|
|
108
|
+
// event during either phase queues exactly one follow-up structural scan.
|
|
109
|
+
const shouldDrainEnrichment = options.attribution === true || dependencies.drainEnrichment !== undefined;
|
|
110
|
+
const enrichment = shouldDrainEnrichment
|
|
111
|
+
? await drainEnrichment({ root: repository.root, scope, historyFingerprint: undefined })
|
|
112
|
+
: { state: 'disabled', pending: 0, complete: 0, failed: 0, unavailable: 0, notApplicable: 0, diagnostics: [] };
|
|
113
|
+
reportWatchProgress(progress, { event: 'enrichment', mode, repository, scope, detail: `state=${enrichment.state} pending=${enrichment.pending} complete=${enrichment.complete} failed=${enrichment.failed}` });
|
|
114
|
+
const summary = { ...structuralSummary, enrichment };
|
|
115
|
+
scanning = false;
|
|
28
116
|
scans += 1;
|
|
29
117
|
emit({
|
|
30
118
|
event: 'scan',
|
|
@@ -34,44 +122,60 @@ export async function watchRepository(options = {}, dependencies = {}) {
|
|
|
34
122
|
scope,
|
|
35
123
|
summary: summarizeScan(summary)
|
|
36
124
|
});
|
|
37
|
-
|
|
38
|
-
|
|
125
|
+
if (polling) {
|
|
126
|
+
const scanEndFingerprint = await fingerprintFn(repository.root);
|
|
127
|
+
const changedDuringScan = scanStartFingerprint !== undefined && scanEndFingerprint !== scanStartFingerprint;
|
|
128
|
+
currentFingerprint = scanEndFingerprint;
|
|
129
|
+
if (changedDuringScan)
|
|
130
|
+
followUpQueued = true;
|
|
131
|
+
}
|
|
39
132
|
if (signal?.aborted)
|
|
40
133
|
break;
|
|
41
|
-
if (
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
reason: 'follow-up',
|
|
46
|
-
repository,
|
|
47
|
-
scope
|
|
48
|
-
});
|
|
134
|
+
if (followUpQueued) {
|
|
135
|
+
followUpQueued = false;
|
|
136
|
+
pendingEventPaths = new Set();
|
|
137
|
+
emitRescanTriggered('follow-up', mode, repository, scope, emit, progress);
|
|
49
138
|
continue;
|
|
50
139
|
}
|
|
51
|
-
emit({
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const nextFingerprint = await fingerprintFn(repository.root);
|
|
60
|
-
if (nextFingerprint !== currentFingerprint) {
|
|
140
|
+
emit({ event: 'waiting', mode, repository, scope });
|
|
141
|
+
reportWatchProgress(progress, { event: 'waiting', mode, repository, scope, detail: polling ? 'waiting for polling interval' : 'waiting for file changes' });
|
|
142
|
+
if (polling) {
|
|
143
|
+
while (!signal?.aborted) {
|
|
144
|
+
await wait(intervalMs, signal);
|
|
145
|
+
const nextFingerprint = await fingerprintFn(repository.root);
|
|
146
|
+
if (nextFingerprint === currentFingerprint)
|
|
147
|
+
continue;
|
|
61
148
|
currentFingerprint = nextFingerprint;
|
|
62
|
-
emit
|
|
63
|
-
event: 'rescan-triggered',
|
|
64
|
-
mode,
|
|
65
|
-
reason: 'file-change',
|
|
66
|
-
repository,
|
|
67
|
-
scope
|
|
68
|
-
});
|
|
149
|
+
emitRescanTriggered('file-change', mode, repository, scope, emit, progress);
|
|
69
150
|
break;
|
|
70
151
|
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (pendingEventPaths.size === 0) {
|
|
155
|
+
await waitForWatchEvent(signal, (resolve) => {
|
|
156
|
+
wakeWaiting = resolve;
|
|
157
|
+
});
|
|
71
158
|
}
|
|
159
|
+
if (signal?.aborted)
|
|
160
|
+
break;
|
|
161
|
+
if (polling)
|
|
162
|
+
continue;
|
|
163
|
+
if (pendingEventPaths.size === 0)
|
|
164
|
+
continue;
|
|
165
|
+
await wait(debounceMs, signal);
|
|
166
|
+
if (signal?.aborted)
|
|
167
|
+
break;
|
|
168
|
+
pendingEventPaths = new Set();
|
|
169
|
+
emitRescanTriggered('file-change', mode, repository, scope, emit, progress);
|
|
72
170
|
}
|
|
73
171
|
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
failed = true;
|
|
174
|
+
reportWatchProgress(progress, { event: 'failed', mode, repository, scope, detail: error instanceof Error ? error.message : String(error) });
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
74
177
|
finally {
|
|
178
|
+
closeWatchers(watchers);
|
|
75
179
|
stopped = true;
|
|
76
180
|
emit({
|
|
77
181
|
event: 'stopped',
|
|
@@ -80,28 +184,90 @@ export async function watchRepository(options = {}, dependencies = {}) {
|
|
|
80
184
|
scope,
|
|
81
185
|
scans
|
|
82
186
|
});
|
|
187
|
+
if (!failed)
|
|
188
|
+
reportWatchProgress(progress, { event: 'stopped', mode, repository, scope, detail: `watch session stopped after ${scans} scan(s)` });
|
|
83
189
|
}
|
|
84
|
-
return {
|
|
85
|
-
repository,
|
|
86
|
-
scope,
|
|
87
|
-
scans,
|
|
88
|
-
stopped
|
|
89
|
-
};
|
|
190
|
+
return { repository, scope, scans, stopped };
|
|
90
191
|
}
|
|
91
192
|
function resolveWatchScope(scope) {
|
|
92
193
|
if (scope === undefined || scope === 'repo')
|
|
93
194
|
return 'repo';
|
|
94
195
|
throw new Error('watch mode supports repo scope only');
|
|
95
196
|
}
|
|
197
|
+
function watchDirectories(root) {
|
|
198
|
+
const directories = new Set([root]);
|
|
199
|
+
for (const relativePath of discoverSourceFiles(root)) {
|
|
200
|
+
directories.add(path.dirname(path.join(root, relativePath)));
|
|
201
|
+
}
|
|
202
|
+
// Immediate module directories contain the supported Maven/Gradle manifests
|
|
203
|
+
// and must also be watched when a module has no source files yet.
|
|
204
|
+
try {
|
|
205
|
+
for (const entry of readdirSync(root, { withFileTypes: true })) {
|
|
206
|
+
if (entry.isDirectory() && !entry.isSymbolicLink() && !IGNORED_WATCH_DIRECTORIES.has(entry.name))
|
|
207
|
+
directories.add(path.join(root, entry.name));
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch { /* scan will report traversal degradation */ }
|
|
211
|
+
return [...directories].sort();
|
|
212
|
+
}
|
|
213
|
+
function relevantWatchPath(root, watchedDirectory, filename) {
|
|
214
|
+
const filenameText = Buffer.isBuffer(filename) ? filename.toString() : filename;
|
|
215
|
+
const absolutePath = path.resolve(watchedDirectory, filenameText);
|
|
216
|
+
const relativePath = path.relative(root, absolutePath);
|
|
217
|
+
if (!relativePath || path.isAbsolute(relativePath) || relativePath === '..' || relativePath.startsWith(`..${path.sep}`))
|
|
218
|
+
return undefined;
|
|
219
|
+
const pathSegments = relativePath.split(path.sep);
|
|
220
|
+
if (pathSegments.some((segment) => IGNORED_WATCH_DIRECTORIES.has(segment)))
|
|
221
|
+
return undefined;
|
|
222
|
+
const basename = path.basename(relativePath);
|
|
223
|
+
const depth = pathSegments.length;
|
|
224
|
+
const supportedManifest = depth <= 2 && (basename === 'pom.xml' || basename === 'build.gradle' || basename === 'build.gradle.kts');
|
|
225
|
+
if (basename !== 'package.json' && !isSupportedSourceFile(absolutePath) && !supportedManifest)
|
|
226
|
+
return undefined;
|
|
227
|
+
return relativePath;
|
|
228
|
+
}
|
|
229
|
+
function closeWatchers(watchers) {
|
|
230
|
+
while (watchers.length > 0) {
|
|
231
|
+
watchers.pop()?.close();
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
function waitForWatchEvent(signal, setWake) {
|
|
235
|
+
return new Promise((resolve) => {
|
|
236
|
+
const done = () => {
|
|
237
|
+
signal?.removeEventListener('abort', done);
|
|
238
|
+
resolve();
|
|
239
|
+
};
|
|
240
|
+
if (signal?.aborted) {
|
|
241
|
+
done();
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
setWake(done);
|
|
245
|
+
signal?.addEventListener('abort', done, { once: true });
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
function emitRescanTriggered(reason, mode, repository, scope, emit, progress) {
|
|
249
|
+
emit({ event: 'rescan-triggered', mode, reason, repository, scope });
|
|
250
|
+
reportWatchProgress(progress, { event: reason === 'follow-up' ? 'follow-up' : 'coalesced-change', mode, repository, scope, detail: reason === 'follow-up' ? 'change queued during scan; running follow-up' : 'coalesced file changes; running one rescan' });
|
|
251
|
+
}
|
|
96
252
|
function summarizeScan(summary) {
|
|
97
253
|
return {
|
|
98
254
|
filesDiscovered: summary.filesDiscovered,
|
|
99
255
|
filesScanned: summary.filesScanned,
|
|
100
256
|
nodesWritten: summary.nodesWritten,
|
|
101
257
|
edgesWritten: summary.edgesWritten,
|
|
102
|
-
skippedCount: summary.skipped.length
|
|
258
|
+
skippedCount: summary.skipped.length,
|
|
259
|
+
enrichment: summary.enrichment
|
|
103
260
|
};
|
|
104
261
|
}
|
|
262
|
+
function reportWatchProgress(progress, event) {
|
|
263
|
+
try {
|
|
264
|
+
progress?.({ kind: 'watch', ...event });
|
|
265
|
+
}
|
|
266
|
+
catch { /* progress is observational */ }
|
|
267
|
+
}
|
|
268
|
+
function defaultWatcherFactory(directory, options, listener) {
|
|
269
|
+
return watchFileSystem(directory, options, (event, filename) => listener(event, filename));
|
|
270
|
+
}
|
|
105
271
|
function defaultEmit(event) {
|
|
106
272
|
console.log(JSON.stringify(event));
|
|
107
273
|
}
|
|
@@ -110,9 +276,8 @@ async function defaultWait(ms, signal) {
|
|
|
110
276
|
await sleep(ms, undefined, signal ? { signal } : undefined);
|
|
111
277
|
}
|
|
112
278
|
catch (error) {
|
|
113
|
-
if (!signal?.aborted)
|
|
279
|
+
if (!signal?.aborted)
|
|
114
280
|
throw error;
|
|
115
|
-
}
|
|
116
281
|
}
|
|
117
282
|
}
|
|
118
283
|
//# sourceMappingURL=watch.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@psnext/lscg",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.6",
|
|
4
4
|
"description": "Local source context graphs for repositories, exposed as a CLI and MCP server.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -33,7 +33,9 @@
|
|
|
33
33
|
"pretest": "npm run build",
|
|
34
34
|
"test": "node --test ./dist/test/*.test.js",
|
|
35
35
|
"prebenchmark:context": "npm run build",
|
|
36
|
-
"benchmark:context": "node ./dist/test/context-benchmark.js"
|
|
36
|
+
"benchmark:context": "node ./dist/test/context-benchmark.js",
|
|
37
|
+
"prebenchmark:scan-watch": "npm run build",
|
|
38
|
+
"benchmark:scan-watch": "node ./dist/test/scan-watch-benchmark.js"
|
|
37
39
|
},
|
|
38
40
|
"keywords": [
|
|
39
41
|
"tree-sitter",
|
|
@@ -51,13 +53,17 @@
|
|
|
51
53
|
"install-scripts": "^1.2.0",
|
|
52
54
|
"tree-sitter": "^0.21.1",
|
|
53
55
|
"tree-sitter-javascript": "^0.23.1",
|
|
56
|
+
"tree-sitter-python": "^0.21.0",
|
|
57
|
+
"tree-sitter-java": "0.21.0",
|
|
54
58
|
"tree-sitter-typescript": "^0.23.2",
|
|
55
59
|
"zod": "^3.25.76"
|
|
56
60
|
},
|
|
57
61
|
"allowScripts": {
|
|
58
62
|
"tree-sitter@0.21.1": true,
|
|
59
63
|
"tree-sitter-javascript@0.23.1": true,
|
|
60
|
-
"tree-sitter-typescript@0.23.2": true
|
|
64
|
+
"tree-sitter-typescript@0.23.2": true,
|
|
65
|
+
"tree-sitter-python@0.21.0": true,
|
|
66
|
+
"tree-sitter-java@0.21.0": true
|
|
61
67
|
},
|
|
62
68
|
"devDependencies": {
|
|
63
69
|
"@types/node": "^26.1.2",
|