dsh-plugin-codegraph-tree-sitter 0.3.0 → 0.4.0
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/lib/cache.d.ts +88 -11
- package/lib/cache.js +41 -21
- package/lib/index.d.ts +2 -2
- package/lib/index.js +20 -13
- package/lib/walk.d.ts +13 -4
- package/lib/walk.js +9 -2
- package/lib/worktree.d.ts +107 -7
- package/lib/worktree.js +89 -14
- package/package.json +2 -2
package/lib/cache.d.ts
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
* repeat index of an unchanged workspace can skip the read, the parse, and the tree walk for every
|
|
4
4
|
* file whose content did not change.
|
|
5
5
|
*
|
|
6
|
+
* The cache document is read and written through the host's filesystem seam (`ctx.fs`), named here as
|
|
7
|
+
* the structural {@link ParseCacheFileSystem} port — the same discipline `./watcher.ts` applies to
|
|
8
|
+
* `WatchFileSystem`. The seam is what DSH's read policy and sandbox actually gate, and it is the only
|
|
9
|
+
* way this file can follow a backend whose files live in another execution world; a path-based
|
|
10
|
+
* `node:fs` write would silently land somewhere the graph's readers are not looking.
|
|
11
|
+
*
|
|
6
12
|
* The cache is an accelerator with no authority. It is read only as a hint — anything missing,
|
|
7
13
|
* unreadable, corrupt, written by another version, or shaped unexpectedly is discarded and the file
|
|
8
14
|
* is parsed again, so losing it costs time and never correctness. That is what lets
|
|
@@ -21,6 +27,67 @@ import type { ExtractedFile } from './resolve.ts';
|
|
|
21
27
|
export declare const EXTRACTION_CACHE_VERSION = 1;
|
|
22
28
|
/** Path of the parse cache, relative to a project root. */
|
|
23
29
|
export declare const EXTRACTION_CACHE_RELATIVE_PATH = ".codegraph/cache/extractions.json";
|
|
30
|
+
/**
|
|
31
|
+
* One resolved filesystem target — the opaque identity `ctx.fs` hands back and every other call
|
|
32
|
+
* takes. Only `targetKey` is read here: the module never parses it, and a remote backend's key is as
|
|
33
|
+
* valid as a local path.
|
|
34
|
+
*/
|
|
35
|
+
export interface CacheTarget {
|
|
36
|
+
/** Opaque backend identity of the target. */
|
|
37
|
+
readonly targetKey: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* The metadata this module consumes from a target, as `ctx.fs.stat` reports it.
|
|
41
|
+
*
|
|
42
|
+
* Deliberately narrower than the seam's own metadata: a cache document has no version to guard and no
|
|
43
|
+
* mtime to compare (the files it describes carry their own identity inside the document), so presence
|
|
44
|
+
* and kind are the whole decision. Naming the one field it reads rather than the whole `FsInfo` is
|
|
45
|
+
* also what keeps this port assignable to the host's `FileSystem`.
|
|
46
|
+
*/
|
|
47
|
+
export interface CacheTargetInfo {
|
|
48
|
+
/** Whether the target is a regular file, a directory, or something else. */
|
|
49
|
+
readonly type: 'file' | 'directory' | 'other';
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The narrow slice of `ctx.fs` the parse cache consumes.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately structural rather than an import of the host's `FileSystem`: the tree-sitter package
|
|
55
|
+
* declares no dependency on `@deepseek-ai/dsh-fs`, so it names the four calls it uses instead —
|
|
56
|
+
* resolve a path, ask whether it exists, read it, replace it. That keeps this module compilable and
|
|
57
|
+
* loadable across the peer range the package supports, and keeps the port small enough that a test
|
|
58
|
+
* double is a few lines.
|
|
59
|
+
*/
|
|
60
|
+
export interface ParseCacheFileSystem {
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a path into a stable backend target.
|
|
63
|
+
* @param path - the path to resolve; absolute paths are what this module passes.
|
|
64
|
+
* @param opts - optional resolution base for a relative path.
|
|
65
|
+
* @returns the resolved target, resolvable even before the file exists.
|
|
66
|
+
*/
|
|
67
|
+
resolve(path: string, opts?: {
|
|
68
|
+
readonly cwd?: string;
|
|
69
|
+
}): Promise<CacheTarget>;
|
|
70
|
+
/**
|
|
71
|
+
* Report a target's metadata.
|
|
72
|
+
* @param target - the resolved target to probe.
|
|
73
|
+
* @returns its metadata, or undefined when nothing is there.
|
|
74
|
+
*/
|
|
75
|
+
stat(target: CacheTarget): Promise<CacheTargetInfo | undefined>;
|
|
76
|
+
/**
|
|
77
|
+
* Read the whole file as decoded text.
|
|
78
|
+
* @param target - the resolved target to read.
|
|
79
|
+
* @returns the file's content.
|
|
80
|
+
*/
|
|
81
|
+
readText(target: CacheTarget): Promise<string>;
|
|
82
|
+
/**
|
|
83
|
+
* Atomically create or replace the whole file. The outcome is ignored: this module only needs the
|
|
84
|
+
* write to have happened, and the seam's guards are not this call's business.
|
|
85
|
+
* @param target - the resolved target to write.
|
|
86
|
+
* @param content - the full new content.
|
|
87
|
+
* @returns the publication outcome, whose details this module does not read.
|
|
88
|
+
*/
|
|
89
|
+
writeText(target: CacheTarget, content: string): Promise<unknown>;
|
|
90
|
+
}
|
|
24
91
|
/** One file's cached extraction, together with the identity it was computed from. */
|
|
25
92
|
export interface CachedFile {
|
|
26
93
|
/** Epoch milliseconds of the file's modification time when its extraction was produced. */
|
|
@@ -39,27 +106,37 @@ export interface CachedFile {
|
|
|
39
106
|
/**
|
|
40
107
|
* Load the previous run's extractions for `projectRoot`.
|
|
41
108
|
*
|
|
42
|
-
* Never throws: a cache that cannot be read, parsed, or trusted yields an empty map, which
|
|
43
|
-
* caller parse everything. A single malformed entry discards the whole file — an entry is
|
|
44
|
-
* complete record this package wrote or evidence the document was damaged, and reasoning
|
|
45
|
-
* partial reuse is not worth the risk of a graph built from a half-written entry.
|
|
109
|
+
* Never throws: a cache that cannot be resolved, read, parsed, or trusted yields an empty map, which
|
|
110
|
+
* makes the caller parse everything. A single malformed entry discards the whole file — an entry is
|
|
111
|
+
* either a complete record this package wrote or evidence the document was damaged, and reasoning
|
|
112
|
+
* about partial reuse is not worth the risk of a graph built from a half-written entry.
|
|
113
|
+
* @param fs - the filesystem seam to resolve, probe, and read through.
|
|
46
114
|
* @param projectRoot - absolute path of the workspace whose cache to load.
|
|
47
115
|
* @returns the cached extractions keyed by project-relative path, empty when none can be trusted.
|
|
48
116
|
*/
|
|
49
|
-
export declare function readParseCache(projectRoot: string): Promise<Map<string, CachedFile>>;
|
|
117
|
+
export declare function readParseCache(fs: ParseCacheFileSystem, projectRoot: string): Promise<Map<string, CachedFile>>;
|
|
50
118
|
/**
|
|
51
119
|
* Record this run's extractions as the next run's cache.
|
|
52
120
|
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
121
|
+
* Publication is the backend's contract, not this module's. `writeText` is specified as an atomic
|
|
122
|
+
* create-or-replace — the local backend stages a temporary sibling and renames it into place — so the
|
|
123
|
+
* previous implementation's own `.tmp` write plus `rename` is gone. That is not a shortcut taken for
|
|
124
|
+
* its own sake: a path-based rename has no meaning for a backend whose files live in another
|
|
125
|
+
* execution world, which is exactly the backend this module moved onto the seam to follow. The seam
|
|
126
|
+
* also has no `mkdir`, so this write relies on the backend creating a missing parent directory (the
|
|
127
|
+
* local one does, before staging); a backend that did not would make this write fail, which the
|
|
128
|
+
* swallow below already treats as "no cache this run".
|
|
129
|
+
*
|
|
130
|
+
* Should a backend ever publish non-atomically, or a crash interrupt it, the worst case is a torn
|
|
131
|
+
* document: {@link readParseCache} rejects it as unparseable and the next run parses everything.
|
|
132
|
+
* Losing one run's reuse is the acceptable side of that trade, because the cache has no authority to
|
|
133
|
+
* lose — a corrupted graph would be the unacceptable one, and no torn cache can produce it.
|
|
58
134
|
*
|
|
59
135
|
* A failure is swallowed on purpose. A read-only workspace, a full disk, or a locked directory must
|
|
60
136
|
* still be indexable; the cache is the optimization, never the product.
|
|
137
|
+
* @param fs - the filesystem seam to resolve and write through.
|
|
61
138
|
* @param projectRoot - absolute path of the workspace to record the cache for.
|
|
62
139
|
* @param files - every file this run produced an extraction for.
|
|
63
140
|
*/
|
|
64
|
-
export declare function writeParseCache(projectRoot: string, files: readonly ExtractedFile[]): Promise<void>;
|
|
141
|
+
export declare function writeParseCache(fs: ParseCacheFileSystem, projectRoot: string, files: readonly ExtractedFile[]): Promise<void>;
|
|
65
142
|
//# sourceMappingURL=cache.d.ts.map
|
package/lib/cache.js
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
* repeat index of an unchanged workspace can skip the read, the parse, and the tree walk for every
|
|
4
4
|
* file whose content did not change.
|
|
5
5
|
*
|
|
6
|
+
* The cache document is read and written through the host's filesystem seam (`ctx.fs`), named here as
|
|
7
|
+
* the structural {@link ParseCacheFileSystem} port — the same discipline `./watcher.ts` applies to
|
|
8
|
+
* `WatchFileSystem`. The seam is what DSH's read policy and sandbox actually gate, and it is the only
|
|
9
|
+
* way this file can follow a backend whose files live in another execution world; a path-based
|
|
10
|
+
* `node:fs` write would silently land somewhere the graph's readers are not looking.
|
|
11
|
+
*
|
|
6
12
|
* The cache is an accelerator with no authority. It is read only as a hint — anything missing,
|
|
7
13
|
* unreadable, corrupt, written by another version, or shaped unexpectedly is discarded and the file
|
|
8
14
|
* is parsed again, so losing it costs time and never correctness. That is what lets
|
|
@@ -10,8 +16,7 @@
|
|
|
10
16
|
* process may not write to.
|
|
11
17
|
* @module dsh-plugin-codegraph-tree-sitter/cache
|
|
12
18
|
*/
|
|
13
|
-
import {
|
|
14
|
-
import { dirname, join } from 'node:path';
|
|
19
|
+
import { join } from 'node:path';
|
|
15
20
|
/**
|
|
16
21
|
* Version of the extraction semantics this cache records. Bump it by hand whenever `extract.ts` (or
|
|
17
22
|
* a grammar that feeds it) starts producing a different result for the same source text: a stale
|
|
@@ -61,22 +66,31 @@ function isCachedFile(value) {
|
|
|
61
66
|
/**
|
|
62
67
|
* Load the previous run's extractions for `projectRoot`.
|
|
63
68
|
*
|
|
64
|
-
* Never throws: a cache that cannot be read, parsed, or trusted yields an empty map, which
|
|
65
|
-
* caller parse everything. A single malformed entry discards the whole file — an entry is
|
|
66
|
-
* complete record this package wrote or evidence the document was damaged, and reasoning
|
|
67
|
-
* partial reuse is not worth the risk of a graph built from a half-written entry.
|
|
69
|
+
* Never throws: a cache that cannot be resolved, read, parsed, or trusted yields an empty map, which
|
|
70
|
+
* makes the caller parse everything. A single malformed entry discards the whole file — an entry is
|
|
71
|
+
* either a complete record this package wrote or evidence the document was damaged, and reasoning
|
|
72
|
+
* about partial reuse is not worth the risk of a graph built from a half-written entry.
|
|
73
|
+
* @param fs - the filesystem seam to resolve, probe, and read through.
|
|
68
74
|
* @param projectRoot - absolute path of the workspace whose cache to load.
|
|
69
75
|
* @returns the cached extractions keyed by project-relative path, empty when none can be trusted.
|
|
70
76
|
*/
|
|
71
|
-
export async function readParseCache(projectRoot) {
|
|
77
|
+
export async function readParseCache(fs, projectRoot) {
|
|
72
78
|
const cachePath = join(projectRoot, EXTRACTION_CACHE_RELATIVE_PATH);
|
|
73
79
|
let raw;
|
|
74
80
|
try {
|
|
75
|
-
|
|
81
|
+
const target = await fs.resolve(cachePath);
|
|
82
|
+
// The seam reports an absent target as `undefined` from `stat` but as a thrown error from
|
|
83
|
+
// `readText`. Probing first keeps the ordinary first run — no cache document written yet — a plain
|
|
84
|
+
// return instead of exception control flow, and rejects a directory or special file that took the
|
|
85
|
+
// document's place before asking a reader to decode it.
|
|
86
|
+
const info = await fs.stat(target);
|
|
87
|
+
if (info === undefined || info.type !== 'file')
|
|
88
|
+
return new Map();
|
|
89
|
+
raw = await fs.readText(target);
|
|
76
90
|
}
|
|
77
91
|
catch {
|
|
78
|
-
//
|
|
79
|
-
//
|
|
92
|
+
// An unreadable document is the same answer to the caller as an absent one — nothing to reuse —
|
|
93
|
+
// and must not fail an index that can still parse the workspace.
|
|
80
94
|
return new Map();
|
|
81
95
|
}
|
|
82
96
|
let document;
|
|
@@ -104,20 +118,27 @@ export async function readParseCache(projectRoot) {
|
|
|
104
118
|
/**
|
|
105
119
|
* Record this run's extractions as the next run's cache.
|
|
106
120
|
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
121
|
+
* Publication is the backend's contract, not this module's. `writeText` is specified as an atomic
|
|
122
|
+
* create-or-replace — the local backend stages a temporary sibling and renames it into place — so the
|
|
123
|
+
* previous implementation's own `.tmp` write plus `rename` is gone. That is not a shortcut taken for
|
|
124
|
+
* its own sake: a path-based rename has no meaning for a backend whose files live in another
|
|
125
|
+
* execution world, which is exactly the backend this module moved onto the seam to follow. The seam
|
|
126
|
+
* also has no `mkdir`, so this write relies on the backend creating a missing parent directory (the
|
|
127
|
+
* local one does, before staging); a backend that did not would make this write fail, which the
|
|
128
|
+
* swallow below already treats as "no cache this run".
|
|
129
|
+
*
|
|
130
|
+
* Should a backend ever publish non-atomically, or a crash interrupt it, the worst case is a torn
|
|
131
|
+
* document: {@link readParseCache} rejects it as unparseable and the next run parses everything.
|
|
132
|
+
* Losing one run's reuse is the acceptable side of that trade, because the cache has no authority to
|
|
133
|
+
* lose — a corrupted graph would be the unacceptable one, and no torn cache can produce it.
|
|
112
134
|
*
|
|
113
135
|
* A failure is swallowed on purpose. A read-only workspace, a full disk, or a locked directory must
|
|
114
136
|
* still be indexable; the cache is the optimization, never the product.
|
|
137
|
+
* @param fs - the filesystem seam to resolve and write through.
|
|
115
138
|
* @param projectRoot - absolute path of the workspace to record the cache for.
|
|
116
139
|
* @param files - every file this run produced an extraction for.
|
|
117
140
|
*/
|
|
118
|
-
export async function writeParseCache(projectRoot, files) {
|
|
119
|
-
const cachePath = join(projectRoot, EXTRACTION_CACHE_RELATIVE_PATH);
|
|
120
|
-
const tempPath = `${cachePath}.tmp`;
|
|
141
|
+
export async function writeParseCache(fs, projectRoot, files) {
|
|
121
142
|
try {
|
|
122
143
|
const entries = {};
|
|
123
144
|
for (const file of files) {
|
|
@@ -131,9 +152,8 @@ export async function writeParseCache(projectRoot, files) {
|
|
|
131
152
|
};
|
|
132
153
|
}
|
|
133
154
|
const document = { cacheVersion: EXTRACTION_CACHE_VERSION, files: entries };
|
|
134
|
-
await
|
|
135
|
-
await
|
|
136
|
-
await rename(tempPath, cachePath);
|
|
155
|
+
const target = await fs.resolve(join(projectRoot, EXTRACTION_CACHE_RELATIVE_PATH));
|
|
156
|
+
await fs.writeText(target, JSON.stringify(document));
|
|
137
157
|
}
|
|
138
158
|
catch {
|
|
139
159
|
// See above: an unusable cache location only means the next run parses everything again.
|
package/lib/index.d.ts
CHANGED
|
@@ -41,7 +41,7 @@ export { isMinifiedName, looksMinified, measureText, MINIFIED_AVERAGE_LINE_LENGT
|
|
|
41
41
|
export type { TextShape } from './minified.ts';
|
|
42
42
|
export { SCHEMA_VERSION, writeGraph } from './schema.ts';
|
|
43
43
|
export { EXTRACTION_CACHE_RELATIVE_PATH, EXTRACTION_CACHE_VERSION, readParseCache, writeParseCache } from './cache.ts';
|
|
44
|
-
export type { CachedFile } from './cache.ts';
|
|
44
|
+
export type { CachedFile, CacheTarget, CacheTargetInfo, ParseCacheFileSystem } from './cache.ts';
|
|
45
45
|
export { isExcluded, walkAndExtract } from './walk.ts';
|
|
46
46
|
export type { WalkConfig, WalkResult } from './walk.ts';
|
|
47
47
|
export { createWatcher } from './watcher.ts';
|
|
@@ -49,7 +49,7 @@ export type { DegradeReason, WatchConfig, WatchDirEntry, WatchFileSystem, WatchT
|
|
|
49
49
|
export { decideWatch, readProcVersion } from './watch-policy.ts';
|
|
50
50
|
export type { WatchDecision, WatchPolicyInput } from './watch-policy.ts';
|
|
51
51
|
export { detectWorktree, execGit } from './worktree.ts';
|
|
52
|
-
export type { GitExec, WorktreeInfo } from './worktree.ts';
|
|
52
|
+
export type { GitExec, GitSpawnHandle, GitSpawnOutcome, GitSpawnPort, GitSpawnReader, GitSpawnSpec, WorktreeInfo, } from './worktree.ts';
|
|
53
53
|
export { HOOK_MARKER_BEGIN, HOOK_MARKER_END, installGitHooks, uninstallGitHooks } from './git-hooks.ts';
|
|
54
54
|
export type { GitHooksOptions } from './git-hooks.ts';
|
|
55
55
|
/** Cordis plugin name for loader diagnostics. */
|
package/lib/index.js
CHANGED
|
@@ -49,6 +49,10 @@ export { EXTRACTION_CACHE_RELATIVE_PATH, EXTRACTION_CACHE_VERSION, readParseCach
|
|
|
49
49
|
export { isExcluded, walkAndExtract } from "./walk.js";
|
|
50
50
|
export { createWatcher } from "./watcher.js";
|
|
51
51
|
export { decideWatch, readProcVersion } from "./watch-policy.js";
|
|
52
|
+
// Git diagnostics stay library functions rather than plugin wiring: nothing in `apply` runs git, so
|
|
53
|
+
// `inject` deliberately omits `subprocess` — requiring it would hide this whole indexer from a
|
|
54
|
+
// composition that provides none. A caller that wants worktree diagnostics passes `ctx.subprocess`
|
|
55
|
+
// to `execGit` itself; installing the git hooks touches no subprocess at all.
|
|
52
56
|
export { detectWorktree, execGit } from "./worktree.js";
|
|
53
57
|
export { HOOK_MARKER_BEGIN, HOOK_MARKER_END, installGitHooks, uninstallGitHooks } from "./git-hooks.js";
|
|
54
58
|
/** Cordis plugin name for loader diagnostics. */
|
|
@@ -103,12 +107,13 @@ export function apply(ctx, config) {
|
|
|
103
107
|
assertPositiveInteger('concurrency', resolved.concurrency);
|
|
104
108
|
assertPositiveInteger('watchDebounceMs', resolved.watchDebounceMs);
|
|
105
109
|
assertPositiveInteger('maxWatchedDirectories', resolved.maxWatchedDirectories);
|
|
106
|
-
// Watching
|
|
107
|
-
// execution world the rest of the harness reads: a remote or confined backend
|
|
108
|
-
//
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
//
|
|
110
|
+
// Watching and the parse cache both go through the host's filesystem seam rather than `node:fs`, so
|
|
111
|
+
// they follow the same execution world the rest of the harness reads: a remote or confined backend
|
|
112
|
+
// watches and caches on its own side, and the cache's read/write pass through the same policy the
|
|
113
|
+
// graph's readers do. The `fs` service is a declared dependency (see `inject`), so it is present
|
|
114
|
+
// whenever `apply` runs. Both ports are read structurally instead of importing
|
|
115
|
+
// `@deepseek-ai/dsh-fs`: that package is not a dependency here, and its `watch` method only exists
|
|
116
|
+
// from host 0.2 onward, while this package supports older peers that have no such member to import.
|
|
112
117
|
const fileSystem = ctx.fs;
|
|
113
118
|
// Keyed by project root, private to this plugin instance: never registered on `ctx`, so there is no
|
|
114
119
|
// cross-plugin relation here for an invariant to describe — just process-local bookkeeping this
|
|
@@ -141,7 +146,7 @@ export function apply(ctx, config) {
|
|
|
141
146
|
respectGitignore: resolved.respectGitignore,
|
|
142
147
|
debounceMs: resolved.watchDebounceMs,
|
|
143
148
|
maxWatchedDirectories: resolved.maxWatchedDirectories,
|
|
144
|
-
sync: () => runSync(projectRoot, resolved),
|
|
149
|
+
sync: () => runSync(projectRoot, resolved, fileSystem),
|
|
145
150
|
// No diagnostics service is wired into this package's dependencies; this is the floor for
|
|
146
151
|
// "degradation must be visible" until one is, per NOTES.local.md.
|
|
147
152
|
onDegraded: (reason) => {
|
|
@@ -166,7 +171,7 @@ export function apply(ctx, config) {
|
|
|
166
171
|
}
|
|
167
172
|
},
|
|
168
173
|
async index(projectRoot, signal) {
|
|
169
|
-
const report = await runIndex(projectRoot, resolved, signal);
|
|
174
|
+
const report = await runIndex(projectRoot, resolved, fileSystem, signal);
|
|
170
175
|
// Watching starts only after a successful index establishes the baseline it refreshes — never
|
|
171
176
|
// from a canIndex() probe or a failed run.
|
|
172
177
|
ensureWatching(projectRoot);
|
|
@@ -270,21 +275,23 @@ function runAutoIndexJob(ctx, root, handle) {
|
|
|
270
275
|
* a file subset — so the watcher's own collected paths are not consulted here.
|
|
271
276
|
* @param projectRoot - absolute path of the workspace to refresh.
|
|
272
277
|
* @param config - the resolved plugin configuration.
|
|
278
|
+
* @param fs - the filesystem seam the parse cache is read and written through.
|
|
273
279
|
* @returns how many files the rebuild indexed, and how long it took.
|
|
274
280
|
*/
|
|
275
|
-
async function runSync(projectRoot, config) {
|
|
281
|
+
async function runSync(projectRoot, config, fs) {
|
|
276
282
|
const startedAt = Date.now();
|
|
277
|
-
const report = await runIndex(projectRoot, config);
|
|
283
|
+
const report = await runIndex(projectRoot, config, fs);
|
|
278
284
|
return { filesChanged: report.filesIndexed, durationMs: Date.now() - startedAt };
|
|
279
285
|
}
|
|
280
286
|
/**
|
|
281
287
|
* One indexing run: walk, parse, resolve, and write.
|
|
282
288
|
* @param projectRoot - absolute path of the workspace to index.
|
|
283
289
|
* @param config - the resolved plugin configuration.
|
|
290
|
+
* @param fs - the filesystem seam the parse cache is read and written through.
|
|
284
291
|
* @param signal - aborts the run.
|
|
285
292
|
* @returns the run's report.
|
|
286
293
|
*/
|
|
287
|
-
async function runIndex(projectRoot, config, signal) {
|
|
294
|
+
async function runIndex(projectRoot, config, fs, signal) {
|
|
288
295
|
const { files, filesSkipped, filesMinified, filesReused } = await walkAndExtract(projectRoot, {
|
|
289
296
|
exclude: config.exclude,
|
|
290
297
|
respectGitignore: config.respectGitignore,
|
|
@@ -295,7 +302,7 @@ async function runIndex(projectRoot, config, signal) {
|
|
|
295
302
|
languages: config.languages,
|
|
296
303
|
reuseParsedCache: config.reuseParsedCache,
|
|
297
304
|
cacheVerify: config.cacheVerify,
|
|
298
|
-
}, signal);
|
|
305
|
+
}, fs, signal);
|
|
299
306
|
signal?.throwIfAborted();
|
|
300
307
|
const indexedAt = Date.now();
|
|
301
308
|
const graph = resolveWorkspace(files, indexedAt);
|
|
@@ -307,7 +314,7 @@ async function runIndex(projectRoot, config, signal) {
|
|
|
307
314
|
// Only after the graph is in place: the cache records the extractions this run actually committed,
|
|
308
315
|
// so a run that failed before writing its graph leaves the previous cache — and the mtimes it was
|
|
309
316
|
// built from — alone instead of vouching for a graph that was never written.
|
|
310
|
-
await writeParseCache(projectRoot, files);
|
|
317
|
+
await writeParseCache(fs, projectRoot, files);
|
|
311
318
|
const languageCounts = new Map();
|
|
312
319
|
for (const file of files)
|
|
313
320
|
languageCounts.set(file.language, (languageCounts.get(file.language) ?? 0) + 1);
|
package/lib/walk.d.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* package's `Config` exposes.
|
|
9
9
|
* @module dsh-plugin-codegraph-tree-sitter/walk
|
|
10
10
|
*/
|
|
11
|
+
import type { ParseCacheFileSystem } from './cache.ts';
|
|
11
12
|
import type { GitignoreRule } from './gitignore.ts';
|
|
12
13
|
import type { ExtractedFile } from './resolve.ts';
|
|
13
14
|
/**
|
|
@@ -48,9 +49,10 @@ export interface WalkConfig {
|
|
|
48
49
|
readonly respectGitignore: boolean;
|
|
49
50
|
/**
|
|
50
51
|
* Reuse the extraction a previous run cached for every file whose content is unchanged, instead of
|
|
51
|
-
* reading and parsing it again. The cache lives under `<projectRoot>/.codegraph/cache
|
|
52
|
-
*
|
|
53
|
-
* is
|
|
52
|
+
* reading and parsing it again. The cache lives under `<projectRoot>/.codegraph/cache`, is read
|
|
53
|
+
* through the host's filesystem seam (the `fs` argument to {@link walkAndExtract}), and is written
|
|
54
|
+
* by the indexer after a run succeeds; a missing or unusable cache simply means every file is
|
|
55
|
+
* parsed.
|
|
54
56
|
*/
|
|
55
57
|
readonly reuseParsedCache: boolean;
|
|
56
58
|
/**
|
|
@@ -80,11 +82,18 @@ export interface WalkResult {
|
|
|
80
82
|
/**
|
|
81
83
|
* Walk `projectRoot` and parse every candidate file it finds — or reuse the extraction a previous
|
|
82
84
|
* run cached for it.
|
|
85
|
+
*
|
|
86
|
+
* `fs` is a parameter rather than a `WalkConfig` field on purpose: the walk's own directory scan and
|
|
87
|
+
* file reads still go straight to `node:fs` — they compare the mtime and size it stats itself, which
|
|
88
|
+
* is the identity every cache entry is recorded under — and the only seam-mediated work here is the
|
|
89
|
+
* parse cache's read. A capability is not a bound, so it does not belong in the config that mirrors
|
|
90
|
+
* this package's `Config`.
|
|
83
91
|
* @param projectRoot - absolute path of the workspace to index.
|
|
84
92
|
* @param config - the walk's bounds.
|
|
93
|
+
* @param fs - the filesystem seam the parse cache is read through.
|
|
85
94
|
* @param signal - aborts the walk.
|
|
86
95
|
* @returns the parsed files, how many candidates were skipped (over size, or past `maxFiles`), and
|
|
87
96
|
* how many files reused a cached extraction.
|
|
88
97
|
*/
|
|
89
|
-
export declare function walkAndExtract(projectRoot: string, config: WalkConfig, signal?: AbortSignal): Promise<WalkResult>;
|
|
98
|
+
export declare function walkAndExtract(projectRoot: string, config: WalkConfig, fs: ParseCacheFileSystem, signal?: AbortSignal): Promise<WalkResult>;
|
|
90
99
|
//# sourceMappingURL=walk.d.ts.map
|
package/lib/walk.js
CHANGED
|
@@ -190,15 +190,22 @@ async function mapWithConcurrency(items, concurrency, task) {
|
|
|
190
190
|
/**
|
|
191
191
|
* Walk `projectRoot` and parse every candidate file it finds — or reuse the extraction a previous
|
|
192
192
|
* run cached for it.
|
|
193
|
+
*
|
|
194
|
+
* `fs` is a parameter rather than a `WalkConfig` field on purpose: the walk's own directory scan and
|
|
195
|
+
* file reads still go straight to `node:fs` — they compare the mtime and size it stats itself, which
|
|
196
|
+
* is the identity every cache entry is recorded under — and the only seam-mediated work here is the
|
|
197
|
+
* parse cache's read. A capability is not a bound, so it does not belong in the config that mirrors
|
|
198
|
+
* this package's `Config`.
|
|
193
199
|
* @param projectRoot - absolute path of the workspace to index.
|
|
194
200
|
* @param config - the walk's bounds.
|
|
201
|
+
* @param fs - the filesystem seam the parse cache is read through.
|
|
195
202
|
* @param signal - aborts the walk.
|
|
196
203
|
* @returns the parsed files, how many candidates were skipped (over size, or past `maxFiles`), and
|
|
197
204
|
* how many files reused a cached extraction.
|
|
198
205
|
*/
|
|
199
|
-
export async function walkAndExtract(projectRoot, config, signal) {
|
|
206
|
+
export async function walkAndExtract(projectRoot, config, fs, signal) {
|
|
200
207
|
const { candidates, overflow } = await discover(projectRoot, config, signal);
|
|
201
|
-
const cache = config.reuseParsedCache ? await readParseCache(projectRoot) : new Map();
|
|
208
|
+
const cache = config.reuseParsedCache ? await readParseCache(fs, projectRoot) : new Map();
|
|
202
209
|
const parsed = await mapWithConcurrency(candidates, config.concurrency, candidate => parseCandidate(candidate, config, cache, signal));
|
|
203
210
|
const files = [];
|
|
204
211
|
let filesSkipped = overflow;
|
package/lib/worktree.d.ts
CHANGED
|
@@ -9,15 +9,115 @@
|
|
|
9
9
|
* never looks outside the `projectRoot` it was given. If a caller ever *does* end up pointed at the
|
|
10
10
|
* wrong root, the bug is almost certainly in how the host resolves a session's working directory, not
|
|
11
11
|
* in this package — this module exists to make that distinction diagnosable, not to route around it.
|
|
12
|
+
*
|
|
13
|
+
* Git runs through the host's subprocess seam (`ctx.subprocess`) instead of `node:child_process`.
|
|
14
|
+
* That seam is where executable resolution, environment scrubbing, bounded output collection,
|
|
15
|
+
* termination grace, timeouts, and sandbox containment live: a child started here inherits a parent
|
|
16
|
+
* environment with ambient credentials and `DSH_*` deployment facts removed, its stdout/stderr are
|
|
17
|
+
* capped before they can exhaust this process, and the host may satisfy the request by running the
|
|
18
|
+
* command in another execution world entirely. `execFileSync` would bypass all of that, so the module
|
|
19
|
+
* takes the seam as a structural port and the caller supplies `execGit(ctx.subprocess)`.
|
|
12
20
|
* @module dsh-plugin-codegraph-tree-sitter/worktree
|
|
13
21
|
*/
|
|
22
|
+
/** Exit facts a spawned child settles with, as `handle.done` reports them. */
|
|
23
|
+
export interface GitSpawnOutcome {
|
|
24
|
+
/** Exit status of the direct child; `null` when a signal terminated it instead. */
|
|
25
|
+
readonly exitCode: number | null;
|
|
26
|
+
/** Name of the signal that terminated the child; `null` when it exited on its own. */
|
|
27
|
+
readonly signal: string | null;
|
|
28
|
+
}
|
|
29
|
+
/** One collected output stream, read in whole-stream byte offsets. */
|
|
30
|
+
export interface GitSpawnReader {
|
|
31
|
+
/**
|
|
32
|
+
* Read everything the collector still retains from `fromByte` onward.
|
|
33
|
+
* @param fromByte - whole-stream byte offset to resume from; `0` for a first read.
|
|
34
|
+
* @returns the collected text, and whether bytes before `fromByte` had already been dropped.
|
|
35
|
+
*/
|
|
36
|
+
readFrom(fromByte: number): {
|
|
37
|
+
readonly text: string;
|
|
38
|
+
readonly lossy: boolean;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** One live child returned by {@link GitSpawnPort.spawn}. */
|
|
42
|
+
export interface GitSpawnHandle {
|
|
43
|
+
/**
|
|
44
|
+
* Settles with the direct child's exit facts. Rejects when the child never started — a missing
|
|
45
|
+
* executable, a refused spawn, or a failing provider — and carries no collected output then.
|
|
46
|
+
*/
|
|
47
|
+
readonly done: Promise<GitSpawnOutcome>;
|
|
48
|
+
/** Buffered output, one entry per stream the spec asked to have collected. */
|
|
49
|
+
readonly collected: {
|
|
50
|
+
/** Collected stdout, present because {@link GitSpawnSpec.stdio} always asks for it. */
|
|
51
|
+
readonly stdout?: GitSpawnReader;
|
|
52
|
+
/** Collected stderr, present because {@link GitSpawnSpec.stdio} always asks for it. */
|
|
53
|
+
readonly stderr?: GitSpawnReader;
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
/** How one child is launched: fully explicit, never shell-interpreted. */
|
|
57
|
+
export interface GitSpawnSpec {
|
|
58
|
+
/** Program and arguments; `argv[0]` is the program itself, never a shell command line. */
|
|
59
|
+
readonly argv: readonly string[];
|
|
60
|
+
/** Working directory the child starts in. */
|
|
61
|
+
readonly cwd: string;
|
|
62
|
+
/** One disposition per standard stream. */
|
|
63
|
+
readonly stdio: {
|
|
64
|
+
/** stdin stays closed: none of these probes wants input, and a prompt would hang the run. */
|
|
65
|
+
readonly stdin: 'ignore';
|
|
66
|
+
/** Collect stdout, retaining at most this many bytes. */
|
|
67
|
+
readonly stdout: {
|
|
68
|
+
readonly maxBytes: number;
|
|
69
|
+
};
|
|
70
|
+
/** Collect stderr, retaining at most this many bytes. */
|
|
71
|
+
readonly stderr: {
|
|
72
|
+
readonly maxBytes: number;
|
|
73
|
+
};
|
|
74
|
+
};
|
|
75
|
+
/** Milliseconds the host may take to terminate the child once termination has started. */
|
|
76
|
+
readonly graceMs: number;
|
|
77
|
+
/** Cancels the child; aborting runs the host's own termination procedure, not a bare kill. */
|
|
78
|
+
readonly signal: AbortSignal;
|
|
79
|
+
/** Environment overrides, merged over the host's scrubbed parent environment. */
|
|
80
|
+
readonly env: {
|
|
81
|
+
readonly [name: string]: string;
|
|
82
|
+
};
|
|
83
|
+
}
|
|
14
84
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
85
|
+
* The narrow slice of `ctx.subprocess` this module consumes.
|
|
86
|
+
*
|
|
87
|
+
* Deliberately structural rather than an import of `@deepseek-ai/dsh-subprocess`: the tree-sitter
|
|
88
|
+
* package declares no dependency on the subprocess family, and only three members of a spawn are
|
|
89
|
+
* read here (argv, stdio, exit facts). Naming those keeps the port small enough that a test double
|
|
90
|
+
* is a few lines, and keeps the module loadable across the host versions this package supports.
|
|
91
|
+
*/
|
|
92
|
+
export interface GitSpawnPort {
|
|
93
|
+
/**
|
|
94
|
+
* Start one managed child.
|
|
95
|
+
* @param spec - program, arguments, cwd, stdio, termination grace, cancellation, environment.
|
|
96
|
+
* @returns the live handle whose `done` reports the child's exit facts.
|
|
97
|
+
*/
|
|
98
|
+
spawn(spec: GitSpawnSpec): GitSpawnHandle;
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Runs one git command and returns its stdout, rejecting on any non-zero exit, timeout, output
|
|
102
|
+
* overflow, or spawn failure.
|
|
103
|
+
*
|
|
104
|
+
* Async, because the host owns the child's lifetime: a synchronous exec cannot express the seam's
|
|
105
|
+
* cancellation, and blocking this process for the length of a git probe is exactly what the
|
|
106
|
+
* subprocess service exists to avoid. Callers inject this instead of forking a real process.
|
|
107
|
+
*/
|
|
108
|
+
export type GitExec = (args: readonly string[], cwd: string) => Promise<string>;
|
|
109
|
+
/**
|
|
110
|
+
* The real `git`-backed {@link GitExec}, running through the host's subprocess port.
|
|
111
|
+
*
|
|
112
|
+
* Every failure is a rejection, never an empty string: the callers here parse git's output
|
|
113
|
+
* positionally, so a silent empty answer would be read as "truncated output" rather than "the probe
|
|
114
|
+
* failed", and the module's "a failed probe is a negative answer" contract lives above this layer.
|
|
115
|
+
* Environment overrides are git's own hardening — no user config, no terminal prompt, no optional
|
|
116
|
+
* locks — copied from the host's own git consumer so both behave identically.
|
|
117
|
+
* @param port - the spawn port; normally the host's `ctx.subprocess`.
|
|
118
|
+
* @returns a git runner whose rejections describe the command and git's stderr tail.
|
|
17
119
|
*/
|
|
18
|
-
export
|
|
19
|
-
/** The real `git`-backed {@link GitExec}. Array arguments, no shell, a hard timeout. */
|
|
20
|
-
export declare const execGit: GitExec;
|
|
120
|
+
export declare function execGit(port: GitSpawnPort): GitExec;
|
|
21
121
|
/** What `detectWorktree` learned about one root. */
|
|
22
122
|
export interface WorktreeInfo {
|
|
23
123
|
/** Whether `root` is a linked worktree rather than the main working tree (or a lone clone). */
|
|
@@ -34,8 +134,8 @@ export interface WorktreeInfo {
|
|
|
34
134
|
* way as "not a worktree" — `undefined`, never a thrown error — matching this package's existing
|
|
35
135
|
* `canIndex`/`indexes` idiom of "a failed probe is a negative answer, not a failure to report."
|
|
36
136
|
* @param root - absolute path to probe.
|
|
37
|
-
* @param exec - the git-invoking primitive;
|
|
137
|
+
* @param exec - the git-invoking primitive; pass `execGit(ctx.subprocess)` for a real probe.
|
|
38
138
|
* @returns worktree diagnostics, or `undefined` when `root` isn't inside a (usable) git repository.
|
|
39
139
|
*/
|
|
40
|
-
export declare function detectWorktree(root: string, exec
|
|
140
|
+
export declare function detectWorktree(root: string, exec: GitExec): Promise<WorktreeInfo | undefined>;
|
|
41
141
|
//# sourceMappingURL=worktree.d.ts.map
|
package/lib/worktree.js
CHANGED
|
@@ -9,13 +9,86 @@
|
|
|
9
9
|
* never looks outside the `projectRoot` it was given. If a caller ever *does* end up pointed at the
|
|
10
10
|
* wrong root, the bug is almost certainly in how the host resolves a session's working directory, not
|
|
11
11
|
* in this package — this module exists to make that distinction diagnosable, not to route around it.
|
|
12
|
+
*
|
|
13
|
+
* Git runs through the host's subprocess seam (`ctx.subprocess`) instead of `node:child_process`.
|
|
14
|
+
* That seam is where executable resolution, environment scrubbing, bounded output collection,
|
|
15
|
+
* termination grace, timeouts, and sandbox containment live: a child started here inherits a parent
|
|
16
|
+
* environment with ambient credentials and `DSH_*` deployment facts removed, its stdout/stderr are
|
|
17
|
+
* capped before they can exhaust this process, and the host may satisfy the request by running the
|
|
18
|
+
* command in another execution world entirely. `execFileSync` would bypass all of that, so the module
|
|
19
|
+
* takes the seam as a structural port and the caller supplies `execGit(ctx.subprocess)`.
|
|
12
20
|
* @module dsh-plugin-codegraph-tree-sitter/worktree
|
|
13
21
|
*/
|
|
14
|
-
import { execFileSync } from 'node:child_process';
|
|
15
22
|
import { dirname, resolve } from 'node:path';
|
|
16
|
-
/**
|
|
17
|
-
|
|
18
|
-
/**
|
|
23
|
+
/** Bound on one git probe's lifetime; the same ceiling the sync implementation used. */
|
|
24
|
+
const GIT_TIMEOUT_MS = 2_000;
|
|
25
|
+
/**
|
|
26
|
+
* Ceiling on collected stdout. `git rev-parse` answers in tens of bytes; the limit exists so a
|
|
27
|
+
* pathological repository cannot push unbounded output through this call, not because an answer
|
|
28
|
+
* of this size is expected.
|
|
29
|
+
*/
|
|
30
|
+
const GIT_MAX_OUTPUT_BYTES = 64 * 1024;
|
|
31
|
+
/**
|
|
32
|
+
* The real `git`-backed {@link GitExec}, running through the host's subprocess port.
|
|
33
|
+
*
|
|
34
|
+
* Every failure is a rejection, never an empty string: the callers here parse git's output
|
|
35
|
+
* positionally, so a silent empty answer would be read as "truncated output" rather than "the probe
|
|
36
|
+
* failed", and the module's "a failed probe is a negative answer" contract lives above this layer.
|
|
37
|
+
* Environment overrides are git's own hardening — no user config, no terminal prompt, no optional
|
|
38
|
+
* locks — copied from the host's own git consumer so both behave identically.
|
|
39
|
+
* @param port - the spawn port; normally the host's `ctx.subprocess`.
|
|
40
|
+
* @returns a git runner whose rejections describe the command and git's stderr tail.
|
|
41
|
+
*/
|
|
42
|
+
export function execGit(port) {
|
|
43
|
+
return async (args, cwd) => {
|
|
44
|
+
const handle = port.spawn({
|
|
45
|
+
argv: ['git', ...args],
|
|
46
|
+
cwd,
|
|
47
|
+
stdio: {
|
|
48
|
+
stdin: 'ignore',
|
|
49
|
+
stdout: { maxBytes: GIT_MAX_OUTPUT_BYTES },
|
|
50
|
+
stderr: { maxBytes: GIT_MAX_OUTPUT_BYTES },
|
|
51
|
+
},
|
|
52
|
+
graceMs: GIT_TIMEOUT_MS,
|
|
53
|
+
// The host drives termination from this signal, so a hung git is stopped the same way a
|
|
54
|
+
// cancelled tool call is, and `done` settles with a signal exit instead of blocking forever.
|
|
55
|
+
signal: AbortSignal.timeout(GIT_TIMEOUT_MS),
|
|
56
|
+
env: {
|
|
57
|
+
GIT_CONFIG_COUNT: '0',
|
|
58
|
+
GIT_TERMINAL_PROMPT: '0',
|
|
59
|
+
GIT_OPTIONAL_LOCKS: '0',
|
|
60
|
+
LC_ALL: 'C',
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
const outcome = await handle.done;
|
|
64
|
+
if (outcome.exitCode !== 0) {
|
|
65
|
+
const stderr = handle.collected.stderr;
|
|
66
|
+
const detail = stderr === undefined ? '' : stderr.readFrom(0).text.trim();
|
|
67
|
+
// A signal exit carries no code, so name the signal instead of printing `null`.
|
|
68
|
+
const how = outcome.exitCode === null
|
|
69
|
+
? `was terminated${outcome.signal === null ? '' : ` by ${outcome.signal}`}`
|
|
70
|
+
: `exited with code ${outcome.exitCode}`;
|
|
71
|
+
throw new Error(`git ${args.join(' ')} ${how}${detail === '' ? '' : `: ${detail}`}`);
|
|
72
|
+
}
|
|
73
|
+
const stdout = handle.collected.stdout;
|
|
74
|
+
if (stdout === undefined)
|
|
75
|
+
throw new Error(`git ${args.join(' ')} produced no collected stdout`);
|
|
76
|
+
const collected = stdout.readFrom(0);
|
|
77
|
+
// A lossy first read means the child wrote past the collection limit before exiting. What
|
|
78
|
+
// survived is a tail, not an answer — and a first line read off it would be a fragment of the
|
|
79
|
+
// real path or hash, so this is a failure rather than a truncated success.
|
|
80
|
+
if (collected.lossy) {
|
|
81
|
+
throw new Error(`git ${args.join(' ')} wrote more than ${GIT_MAX_OUTPUT_BYTES} bytes of stdout`);
|
|
82
|
+
}
|
|
83
|
+
return collected.text;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Memoized per root: git diagnostics are cheap to answer wrong (a stale worktree list) but not free.
|
|
88
|
+
* The promise itself is cached so two concurrent probes of one root share a single child process
|
|
89
|
+
* instead of racing two; `probeWorktree` resolves every failure to `undefined`, so this map never
|
|
90
|
+
* holds a rejection.
|
|
91
|
+
*/
|
|
19
92
|
const cache = new Map();
|
|
20
93
|
/**
|
|
21
94
|
* Whether `root` sits inside a linked `git worktree`, and the main repository's root when it does.
|
|
@@ -24,22 +97,23 @@ const cache = new Map();
|
|
|
24
97
|
* way as "not a worktree" — `undefined`, never a thrown error — matching this package's existing
|
|
25
98
|
* `canIndex`/`indexes` idiom of "a failed probe is a negative answer, not a failure to report."
|
|
26
99
|
* @param root - absolute path to probe.
|
|
27
|
-
* @param exec - the git-invoking primitive;
|
|
100
|
+
* @param exec - the git-invoking primitive; pass `execGit(ctx.subprocess)` for a real probe.
|
|
28
101
|
* @returns worktree diagnostics, or `undefined` when `root` isn't inside a (usable) git repository.
|
|
29
102
|
*/
|
|
30
|
-
export function detectWorktree(root, exec
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
103
|
+
export function detectWorktree(root, exec) {
|
|
104
|
+
const cached = cache.get(root);
|
|
105
|
+
if (cached !== undefined)
|
|
106
|
+
return cached;
|
|
107
|
+
const pending = probeWorktree(root, exec);
|
|
108
|
+
cache.set(root, pending);
|
|
109
|
+
return pending;
|
|
36
110
|
}
|
|
37
|
-
function probeWorktree(root, exec) {
|
|
111
|
+
async function probeWorktree(root, exec) {
|
|
38
112
|
let output;
|
|
39
113
|
try {
|
|
40
114
|
// One call, not three: `git rev-parse` answers every query in the order given, so a single
|
|
41
115
|
// process fork either answers all three questions or fails all three the same way.
|
|
42
|
-
output = exec(['rev-parse', '--show-toplevel', '--git-dir', '--git-common-dir'], root);
|
|
116
|
+
output = await exec(['rev-parse', '--show-toplevel', '--git-dir', '--git-common-dir'], root);
|
|
43
117
|
}
|
|
44
118
|
catch {
|
|
45
119
|
return undefined;
|
|
@@ -48,8 +122,9 @@ function probeWorktree(root, exec) {
|
|
|
48
122
|
const toplevelLine = lines[0];
|
|
49
123
|
const gitDirLine = lines[1];
|
|
50
124
|
const commonDirLine = lines[2];
|
|
51
|
-
if (toplevelLine === undefined || gitDirLine === undefined || commonDirLine === undefined)
|
|
125
|
+
if (toplevelLine === undefined || gitDirLine === undefined || commonDirLine === undefined) {
|
|
52
126
|
return undefined;
|
|
127
|
+
}
|
|
53
128
|
// `git rev-parse` reports these relative to `root` (the cwd it ran in) unless they point outside
|
|
54
129
|
// that tree entirely, as `--git-common-dir` does from inside a linked worktree — `resolve` is a
|
|
55
130
|
// no-op on an already-absolute path either way.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-plugin-codegraph-tree-sitter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Self-built code-graph indexer for DeepSeek Harness — parses a workspace with web-tree-sitter and writes schema version 4 to .codegraph/codegraph.db, the same on-disk format the codegraph CLI writes, so no external indexer is required",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"dsh",
|
|
@@ -51,7 +51,7 @@
|
|
|
51
51
|
"@deepseek-ai/schemastery": "^3.18.1",
|
|
52
52
|
"tree-sitter-wasms": "^0.1.13",
|
|
53
53
|
"web-tree-sitter": "^0.25.10",
|
|
54
|
-
"dsh-plugin-codegraph-service": "^0.
|
|
54
|
+
"dsh-plugin-codegraph-service": "^0.4.0"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@deepseek-ai/cordis": ">=4.0.1",
|