@arnilo/prism-coding-agent 0.2.3 → 0.2.5
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/dist/language/framing.d.ts +9 -1
- package/dist/language/framing.js +89 -17
- package/dist/repository/glob.d.ts +4 -0
- package/dist/repository/glob.js +143 -0
- package/dist/repository/list.d.ts +3 -0
- package/dist/repository/list.js +119 -0
- package/dist/repository/operations.d.ts +5 -0
- package/dist/repository/operations.js +14 -0
- package/dist/repository/path.d.ts +18 -0
- package/dist/repository/path.js +91 -0
- package/dist/repository/search.d.ts +9 -0
- package/dist/repository/search.js +284 -0
- package/dist/repository/types.d.ts +126 -0
- package/dist/repository/types.js +31 -0
- package/dist/repository/walk.d.ts +22 -0
- package/dist/repository/walk.js +99 -0
- package/dist/repository.d.ts +10 -172
- package/dist/repository.js +10 -748
- package/package.json +3 -3
|
@@ -14,10 +14,18 @@ export declare function encodeLspFrame(message: unknown): Buffer;
|
|
|
14
14
|
* Rejects malformed headers, non-decimal Content-Length, and oversized bodies.
|
|
15
15
|
*/
|
|
16
16
|
export declare class LspFrameReader {
|
|
17
|
-
private
|
|
17
|
+
private chunks;
|
|
18
|
+
private offset;
|
|
19
|
+
private retained;
|
|
20
|
+
private cachedBodyStart;
|
|
21
|
+
private cachedContentLength;
|
|
18
22
|
private readonly maxMessageBytes;
|
|
19
23
|
constructor(maxMessageBytes: number);
|
|
20
24
|
/** Push stdout/stderr chunk; return complete parsed JSON values (order preserved). */
|
|
21
25
|
push(chunk: Buffer): unknown[];
|
|
26
|
+
/** Copy `n` unconsumed bytes starting at absolute unconsumed offset `start` (no advance). */
|
|
27
|
+
private peekAt;
|
|
28
|
+
/** Advance the unconsumed cursor by `n` (drop fully-consumed chunks). */
|
|
29
|
+
private drop;
|
|
22
30
|
private tryParseOne;
|
|
23
31
|
}
|
package/dist/language/framing.js
CHANGED
|
@@ -21,7 +21,20 @@ export function encodeLspFrame(message) {
|
|
|
21
21
|
* Rejects malformed headers, non-decimal Content-Length, and oversized bodies.
|
|
22
22
|
*/
|
|
23
23
|
export class LspFrameReader {
|
|
24
|
-
|
|
24
|
+
// ponytail: chunk-array accumulator — O(1) append per push, no whole-buffer re-concat
|
|
25
|
+
// (the 0.2.4 `this.buf = Buffer.concat([this.buf, chunk])` was O(input * chunks)).
|
|
26
|
+
// A completed frame copies only its header+body region (bounded by maxMessageBytes),
|
|
27
|
+
// so total copying is O(input). The header separator scan peeks min(retained, 64KiB)
|
|
28
|
+
// per unparsed frame; for the rare many-frames-per-large-chunk case that is a 64KiB-
|
|
29
|
+
// per-frame copy (linear, 64x constant) — upgrade to a streaming separator search
|
|
30
|
+
// if pipelined-frame throughput matters. A separator beyond the 64KiB header bound
|
|
31
|
+
// is rejected (stricter DoS guard than 0.2.4, which accepted it; no test exercises
|
|
32
|
+
// >64KiB headers — the bound exists precisely to reject unbounded header growth).
|
|
33
|
+
chunks = [];
|
|
34
|
+
offset = 0; // consumed prefix bytes in chunks[0]
|
|
35
|
+
retained = 0; // total unconsumed bytes
|
|
36
|
+
cachedBodyStart = -1; // -1 = header not yet parsed; else body starts at this absolute unconsumed offset
|
|
37
|
+
cachedContentLength = 0;
|
|
25
38
|
maxMessageBytes;
|
|
26
39
|
constructor(maxMessageBytes) {
|
|
27
40
|
this.maxMessageBytes = maxMessageBytes;
|
|
@@ -30,7 +43,8 @@ export class LspFrameReader {
|
|
|
30
43
|
push(chunk) {
|
|
31
44
|
if (chunk.length === 0)
|
|
32
45
|
return [];
|
|
33
|
-
this.
|
|
46
|
+
this.chunks.push(chunk);
|
|
47
|
+
this.retained += chunk.length;
|
|
34
48
|
const out = [];
|
|
35
49
|
for (;;) {
|
|
36
50
|
const parsed = this.tryParseOne();
|
|
@@ -40,26 +54,84 @@ export class LspFrameReader {
|
|
|
40
54
|
}
|
|
41
55
|
return out;
|
|
42
56
|
}
|
|
57
|
+
/** Copy `n` unconsumed bytes starting at absolute unconsumed offset `start` (no advance). */
|
|
58
|
+
peekAt(start, n) {
|
|
59
|
+
const out = Buffer.allocUnsafe(n);
|
|
60
|
+
let written = 0;
|
|
61
|
+
let i = 0;
|
|
62
|
+
let off = this.offset;
|
|
63
|
+
let skip = start;
|
|
64
|
+
while (skip > 0) {
|
|
65
|
+
const c = this.chunks[i];
|
|
66
|
+
const avail = c.length - off;
|
|
67
|
+
if (skip >= avail) {
|
|
68
|
+
skip -= avail;
|
|
69
|
+
i++;
|
|
70
|
+
off = 0;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
off += skip;
|
|
74
|
+
skip = 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
while (written < n) {
|
|
78
|
+
const c = this.chunks[i];
|
|
79
|
+
const take = Math.min(c.length - off, n - written);
|
|
80
|
+
c.copy(out, written, off, off + take);
|
|
81
|
+
written += take;
|
|
82
|
+
i++;
|
|
83
|
+
off = 0;
|
|
84
|
+
}
|
|
85
|
+
return out;
|
|
86
|
+
}
|
|
87
|
+
/** Advance the unconsumed cursor by `n` (drop fully-consumed chunks). */
|
|
88
|
+
drop(n) {
|
|
89
|
+
let remaining = n;
|
|
90
|
+
while (remaining > 0 && this.chunks.length > 0) {
|
|
91
|
+
const first = this.chunks[0];
|
|
92
|
+
const avail = first.length - this.offset;
|
|
93
|
+
if (remaining >= avail) {
|
|
94
|
+
remaining -= avail;
|
|
95
|
+
this.chunks.shift();
|
|
96
|
+
this.offset = 0;
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
this.offset += remaining;
|
|
100
|
+
remaining = 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
this.retained -= n;
|
|
104
|
+
}
|
|
43
105
|
tryParseOne() {
|
|
44
|
-
|
|
45
|
-
|
|
106
|
+
if (this.retained === 0)
|
|
107
|
+
return undefined;
|
|
108
|
+
const headerBound = Math.min(this.maxMessageBytes, 64 * 1024);
|
|
109
|
+
if (this.cachedBodyStart < 0) {
|
|
46
110
|
// Bound header scan buffer so a missing separator cannot grow forever.
|
|
47
|
-
|
|
48
|
-
|
|
111
|
+
const scanLen = Math.min(this.retained, headerBound);
|
|
112
|
+
const view = this.peekAt(0, scanLen);
|
|
113
|
+
const sep = indexOfHeaderSep(view);
|
|
114
|
+
if (sep < 0) {
|
|
115
|
+
if (this.retained > headerBound) {
|
|
116
|
+
throw new LspFrameError("ERR_PRISM_LSP_FRAMING", "LSP header exceeds bound without separator");
|
|
117
|
+
}
|
|
118
|
+
return undefined;
|
|
49
119
|
}
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
120
|
+
const headerText = view.subarray(0, sep).toString("ascii");
|
|
121
|
+
const contentLength = parseContentLength(headerText);
|
|
122
|
+
if (contentLength > this.maxMessageBytes) {
|
|
123
|
+
throw new LspFrameError("ERR_PRISM_LSP_LIMIT", `LSP message body ${contentLength} exceeds maxMessageBytes ${this.maxMessageBytes}`);
|
|
124
|
+
}
|
|
125
|
+
this.cachedBodyStart = sep + 4;
|
|
126
|
+
this.cachedContentLength = contentLength;
|
|
56
127
|
}
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
if (this.buf.length < bodyEnd)
|
|
128
|
+
const bodyEnd = this.cachedBodyStart + this.cachedContentLength;
|
|
129
|
+
if (this.retained < bodyEnd)
|
|
60
130
|
return undefined;
|
|
61
|
-
const body = this.
|
|
62
|
-
this.
|
|
131
|
+
const body = this.peekAt(this.cachedBodyStart, this.cachedContentLength);
|
|
132
|
+
this.drop(bodyEnd);
|
|
133
|
+
this.cachedBodyStart = -1;
|
|
134
|
+
this.cachedContentLength = 0;
|
|
63
135
|
let value;
|
|
64
136
|
try {
|
|
65
137
|
value = JSON.parse(body.toString("utf8"));
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { RepositoryGlobRequest, RepositoryGlobResult, ResolvedRepositoryLimits } from "./types.js";
|
|
2
|
+
import type { RepositoryWalk } from "./walk.js";
|
|
3
|
+
export declare function globLocal(request: RepositoryGlobRequest, defaults: ResolvedRepositoryLimits, walk: RepositoryWalk): Promise<RepositoryGlobResult>;
|
|
4
|
+
/** Local filesystem repository operations (default backend). */
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
/** Repository glob family (0.2.5 plan 025 Task 1 split).
|
|
2
|
+
* Moved verbatim from repository.ts; public surface unchanged behind the barrel. */
|
|
3
|
+
import { HARD_MAX_REPO_DEPTH, HARD_MAX_REPO_ENTRIES, HARD_MAX_REPO_RESULTS, validateCodingLimit, validateCodingLimitAllowZero, } from "../limits.js";
|
|
4
|
+
import { expandGlobBraces, matchGlobPattern, validateGlobPattern } from "../glob-match.js";
|
|
5
|
+
import { lstat } from "node:fs/promises";
|
|
6
|
+
import { RepositoryError } from "./types.js";
|
|
7
|
+
import { resolveRepoPath } from "./path.js";
|
|
8
|
+
export async function globLocal(request, defaults, walk) {
|
|
9
|
+
try {
|
|
10
|
+
validateGlobPattern(request.pattern, defaults.maxPatternBytes, { braceExpansion: request.braceExpansion === true });
|
|
11
|
+
}
|
|
12
|
+
catch (error) {
|
|
13
|
+
throw new RepositoryError(error instanceof Error ? error.message : String(error));
|
|
14
|
+
}
|
|
15
|
+
// Opt-in bounded brace expansion: textual alternatives only (never touches the
|
|
16
|
+
// filesystem); bounds enforced by expandGlobBraces (max alternatives / bytes).
|
|
17
|
+
const patterns = request.braceExpansion === true ? expandGlobBraces(request.pattern) : [request.pattern];
|
|
18
|
+
const resolved = await resolveRepoPath(request.root, request.path);
|
|
19
|
+
const maxResults = validateCodingLimit("maxResults", request.maxResults ?? defaults.maxResults, HARD_MAX_REPO_RESULTS);
|
|
20
|
+
const offset = validateCodingLimitAllowZero("offset", request.offset ?? 0, HARD_MAX_REPO_ENTRIES);
|
|
21
|
+
const maxDepth = validateCodingLimit("maxDepth", request.maxDepth ?? defaults.maxDepth, HARD_MAX_REPO_DEPTH);
|
|
22
|
+
const exclude = new Set(request.exclude ?? defaults.exclude);
|
|
23
|
+
const deadlineAt = request.deadlineMs !== undefined ? Date.now() + request.deadlineMs : Date.now() + defaults.maxTimeMs;
|
|
24
|
+
const collected = [];
|
|
25
|
+
let scannedEntries = 0;
|
|
26
|
+
let scannedFiles = 0;
|
|
27
|
+
let seen = 0;
|
|
28
|
+
let truncated = false;
|
|
29
|
+
let truncatedBy = null;
|
|
30
|
+
const matchesAnyPattern = (relativePath) => {
|
|
31
|
+
for (const p of patterns) {
|
|
32
|
+
if (matchGlobPattern(p, relativePath))
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
return false;
|
|
36
|
+
};
|
|
37
|
+
const maybeCollect = (relativePath) => {
|
|
38
|
+
if (!matchesAnyPattern(relativePath))
|
|
39
|
+
return false;
|
|
40
|
+
if (seen < offset) {
|
|
41
|
+
seen++;
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
if (collected.length >= maxResults) {
|
|
45
|
+
truncated = true;
|
|
46
|
+
truncatedBy = "results";
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
collected.push(relativePath);
|
|
50
|
+
seen++;
|
|
51
|
+
return truncated;
|
|
52
|
+
};
|
|
53
|
+
try {
|
|
54
|
+
const startStat = await lstat(resolved.absolute);
|
|
55
|
+
if (!startStat.isDirectory()) {
|
|
56
|
+
scannedEntries = 1;
|
|
57
|
+
if (startStat.isFile()) {
|
|
58
|
+
scannedFiles = 1;
|
|
59
|
+
if (matchesAnyPattern(resolved.relative)) {
|
|
60
|
+
if (offset === 0 && maxResults > 0)
|
|
61
|
+
collected.push(resolved.relative);
|
|
62
|
+
else if (offset === 0 && maxResults === 0) {
|
|
63
|
+
truncated = true;
|
|
64
|
+
truncatedBy = "results";
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
paths: collected,
|
|
70
|
+
truncated,
|
|
71
|
+
truncatedBy,
|
|
72
|
+
scannedEntries,
|
|
73
|
+
scannedFiles,
|
|
74
|
+
offset,
|
|
75
|
+
nextOffset: undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
81
|
+
throw new RepositoryError(`cannot open path: ${message}`);
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
for await (const event of walk(resolved.rootReal, resolved.absolute, {
|
|
85
|
+
maxDepth,
|
|
86
|
+
maxEntries: defaults.maxEntries,
|
|
87
|
+
maxFiles: defaults.maxFiles,
|
|
88
|
+
exclude,
|
|
89
|
+
includeHidden: request.includeHidden === true,
|
|
90
|
+
signal: request.signal,
|
|
91
|
+
deadlineAt,
|
|
92
|
+
})) {
|
|
93
|
+
if (event.type === "limit") {
|
|
94
|
+
truncated = true;
|
|
95
|
+
truncatedBy = event.truncatedBy;
|
|
96
|
+
break;
|
|
97
|
+
}
|
|
98
|
+
scannedEntries++;
|
|
99
|
+
if (event.entry.kind === "file")
|
|
100
|
+
scannedFiles++;
|
|
101
|
+
if (event.entry.kind !== "file")
|
|
102
|
+
continue;
|
|
103
|
+
if (maybeCollect(event.entry.path))
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (error) {
|
|
108
|
+
if (error instanceof RepositoryError && error.message === "Operation aborted") {
|
|
109
|
+
return {
|
|
110
|
+
paths: collected,
|
|
111
|
+
truncated: true,
|
|
112
|
+
truncatedBy: "abort",
|
|
113
|
+
scannedEntries,
|
|
114
|
+
scannedFiles,
|
|
115
|
+
offset,
|
|
116
|
+
nextOffset: collected.length > 0 || offset > 0 ? offset + collected.length : undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
if (error instanceof RepositoryError && error.message === "Repository operation exceeded time limit") {
|
|
120
|
+
return {
|
|
121
|
+
paths: collected,
|
|
122
|
+
truncated: true,
|
|
123
|
+
truncatedBy: "time",
|
|
124
|
+
scannedEntries,
|
|
125
|
+
scannedFiles,
|
|
126
|
+
offset,
|
|
127
|
+
nextOffset: offset + collected.length,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
paths: collected,
|
|
134
|
+
truncated,
|
|
135
|
+
truncatedBy,
|
|
136
|
+
scannedEntries,
|
|
137
|
+
scannedFiles,
|
|
138
|
+
offset,
|
|
139
|
+
nextOffset: truncated ? offset + collected.length : undefined,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
/** Local filesystem repository operations (default backend). */
|
|
143
|
+
//# sourceMappingURL=glob.js.map
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import type { RepositoryListRequest, RepositoryListResult, ResolvedRepositoryLimits } from "./types.js";
|
|
2
|
+
import type { RepositoryWalk } from "./walk.js";
|
|
3
|
+
export declare function listLocal(request: RepositoryListRequest, defaults: ResolvedRepositoryLimits, walk: RepositoryWalk): Promise<RepositoryListResult>;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/** Repository list family (0.2.5 plan 025 Task 1 split).
|
|
2
|
+
* Moved verbatim from repository.ts; public surface unchanged behind the barrel. */
|
|
3
|
+
import { HARD_MAX_REPO_DEPTH, HARD_MAX_REPO_ENTRIES, HARD_MAX_REPO_RESULTS, validateCodingLimit, validateCodingLimitAllowZero, } from "../limits.js";
|
|
4
|
+
import { lstat } from "node:fs/promises";
|
|
5
|
+
import { RepositoryError } from "./types.js";
|
|
6
|
+
import { resolveRepoPath } from "./path.js";
|
|
7
|
+
export async function listLocal(request, defaults, walk) {
|
|
8
|
+
const resolved = await resolveRepoPath(request.root, request.path);
|
|
9
|
+
const maxResults = validateCodingLimit("maxResults", request.maxResults ?? defaults.maxResults, HARD_MAX_REPO_RESULTS);
|
|
10
|
+
const offset = validateCodingLimitAllowZero("offset", request.offset ?? 0, HARD_MAX_REPO_ENTRIES);
|
|
11
|
+
const maxDepth = validateCodingLimit("maxDepth", request.maxDepth ?? defaults.maxDepth, HARD_MAX_REPO_DEPTH);
|
|
12
|
+
const exclude = new Set(request.exclude ?? defaults.exclude);
|
|
13
|
+
const deadlineAt = request.deadlineMs !== undefined ? Date.now() + request.deadlineMs : Date.now() + defaults.maxTimeMs;
|
|
14
|
+
const collected = [];
|
|
15
|
+
let scannedEntries = 0;
|
|
16
|
+
let scannedFiles = 0;
|
|
17
|
+
let seen = 0;
|
|
18
|
+
let truncated = false;
|
|
19
|
+
let truncatedBy = null;
|
|
20
|
+
// Single-file start: return that entry when it falls within the page window.
|
|
21
|
+
try {
|
|
22
|
+
const startStat = await lstat(resolved.absolute);
|
|
23
|
+
if (!startStat.isDirectory()) {
|
|
24
|
+
let kind = "other";
|
|
25
|
+
if (startStat.isSymbolicLink())
|
|
26
|
+
kind = "symlink";
|
|
27
|
+
else if (startStat.isFile())
|
|
28
|
+
kind = "file";
|
|
29
|
+
const entry = kind === "file" ? { path: resolved.relative, kind, size: startStat.size } : { path: resolved.relative, kind };
|
|
30
|
+
scannedEntries = 1;
|
|
31
|
+
scannedFiles = kind === "file" ? 1 : 0;
|
|
32
|
+
if (offset === 0 && maxResults > 0)
|
|
33
|
+
collected.push(entry);
|
|
34
|
+
else if (offset === 0 && maxResults === 0) {
|
|
35
|
+
truncated = true;
|
|
36
|
+
truncatedBy = "results";
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
entries: collected,
|
|
40
|
+
truncated,
|
|
41
|
+
truncatedBy,
|
|
42
|
+
scannedEntries,
|
|
43
|
+
scannedFiles,
|
|
44
|
+
offset,
|
|
45
|
+
nextOffset: undefined,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
throw new RepositoryError(`cannot open path: ${message}`);
|
|
52
|
+
}
|
|
53
|
+
try {
|
|
54
|
+
for await (const event of walk(resolved.rootReal, resolved.absolute, {
|
|
55
|
+
maxDepth,
|
|
56
|
+
maxEntries: defaults.maxEntries,
|
|
57
|
+
maxFiles: defaults.maxFiles,
|
|
58
|
+
exclude,
|
|
59
|
+
includeHidden: request.includeHidden === true,
|
|
60
|
+
signal: request.signal,
|
|
61
|
+
deadlineAt,
|
|
62
|
+
})) {
|
|
63
|
+
if (event.type === "limit") {
|
|
64
|
+
truncated = true;
|
|
65
|
+
truncatedBy = event.truncatedBy;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
scannedEntries++;
|
|
69
|
+
if (event.entry.kind === "file")
|
|
70
|
+
scannedFiles++;
|
|
71
|
+
if (seen < offset) {
|
|
72
|
+
seen++;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (collected.length >= maxResults) {
|
|
76
|
+
truncated = true;
|
|
77
|
+
truncatedBy = "results";
|
|
78
|
+
break;
|
|
79
|
+
}
|
|
80
|
+
collected.push(event.entry);
|
|
81
|
+
seen++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
if (error instanceof RepositoryError && error.message === "Operation aborted") {
|
|
86
|
+
return {
|
|
87
|
+
entries: collected,
|
|
88
|
+
truncated: true,
|
|
89
|
+
truncatedBy: "abort",
|
|
90
|
+
scannedEntries,
|
|
91
|
+
scannedFiles,
|
|
92
|
+
offset,
|
|
93
|
+
nextOffset: collected.length > 0 || offset > 0 ? offset + collected.length : undefined,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
if (error instanceof RepositoryError && error.message === "Repository operation exceeded time limit") {
|
|
97
|
+
return {
|
|
98
|
+
entries: collected,
|
|
99
|
+
truncated: true,
|
|
100
|
+
truncatedBy: "time",
|
|
101
|
+
scannedEntries,
|
|
102
|
+
scannedFiles,
|
|
103
|
+
offset,
|
|
104
|
+
nextOffset: offset + collected.length,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
throw error;
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
entries: collected,
|
|
111
|
+
truncated,
|
|
112
|
+
truncatedBy,
|
|
113
|
+
scannedEntries,
|
|
114
|
+
scannedFiles,
|
|
115
|
+
offset,
|
|
116
|
+
nextOffset: truncated ? offset + collected.length : undefined,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
//# sourceMappingURL=list.js.map
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Repository operations family (0.2.5 plan 025 Task 1 split).
|
|
2
|
+
* Moved verbatim from repository.ts; public surface unchanged behind the barrel. */
|
|
3
|
+
import type { RepositoryLimitOptions, RepositoryOperations } from "./types.js";
|
|
4
|
+
import type { RepositoryWalk } from "./walk.js";
|
|
5
|
+
export declare function createLocalRepositoryOperations(limits?: RepositoryLimitOptions, walk?: RepositoryWalk): RepositoryOperations;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { resolveRepositoryLimits } from "./types.js";
|
|
2
|
+
import { walkRepository } from "./walk.js";
|
|
3
|
+
import { globLocal } from "./glob.js";
|
|
4
|
+
import { listLocal } from "./list.js";
|
|
5
|
+
import { searchLocal } from "./search.js";
|
|
6
|
+
export function createLocalRepositoryOperations(limits, walk = walkRepository) {
|
|
7
|
+
const resolved = resolveRepositoryLimits(limits);
|
|
8
|
+
return {
|
|
9
|
+
list: (request) => listLocal(request, resolved, walk),
|
|
10
|
+
search: (request) => searchLocal(request, resolved, walk),
|
|
11
|
+
glob: (request) => globLocal(request, resolved, walk),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=operations.js.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Dirent } from "node:fs";
|
|
2
|
+
import type { RepoEntryKind } from "./types.js";
|
|
3
|
+
export declare function toRepoRelative(root: string, absolutePath: string): string;
|
|
4
|
+
export declare function isPathInsideRoot(root: string, target: string): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a list/search start path under the workspace root.
|
|
7
|
+
* Symlink escapes fail closed after realpath when the path exists.
|
|
8
|
+
*/
|
|
9
|
+
export declare function resolveRepoPath(root: string, inputPath: string | undefined): Promise<{
|
|
10
|
+
absolute: string;
|
|
11
|
+
relative: string;
|
|
12
|
+
rootReal: string;
|
|
13
|
+
}>;
|
|
14
|
+
export declare function shouldSkipName(name: string, includeHidden: boolean, exclude: ReadonlySet<string>): boolean;
|
|
15
|
+
export declare function kindFromDirent(dirent: Dirent): RepoEntryKind;
|
|
16
|
+
export declare function assertNotAborted(signal: AbortSignal | undefined): void;
|
|
17
|
+
export declare function assertDeadline(deadlineAt: number | undefined): void;
|
|
18
|
+
export declare function isBinaryBuffer(buffer: Buffer): boolean;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** Repository path family (0.2.5 plan 025 Task 1 split).
|
|
2
|
+
* Moved verbatim from repository.ts; public surface unchanged behind the barrel. */
|
|
3
|
+
import { DEFAULT_BINARY_SNIFF_BYTES } from "../limits.js";
|
|
4
|
+
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
5
|
+
import { realpath } from "node:fs/promises";
|
|
6
|
+
import { resolveToCwd } from "../path-utils.js";
|
|
7
|
+
import { RepositoryError } from "./types.js";
|
|
8
|
+
export function toRepoRelative(root, absolutePath) {
|
|
9
|
+
const rel = relative(root, absolutePath);
|
|
10
|
+
if (rel === "")
|
|
11
|
+
return ".";
|
|
12
|
+
return rel.split(sep).join("/");
|
|
13
|
+
}
|
|
14
|
+
export function isPathInsideRoot(root, target) {
|
|
15
|
+
const from = resolve(root);
|
|
16
|
+
const to = resolve(target);
|
|
17
|
+
if (to === from)
|
|
18
|
+
return true;
|
|
19
|
+
const rel = relative(from, to);
|
|
20
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve a list/search start path under the workspace root.
|
|
24
|
+
* Symlink escapes fail closed after realpath when the path exists.
|
|
25
|
+
*/
|
|
26
|
+
export async function resolveRepoPath(root, inputPath) {
|
|
27
|
+
const rootResolved = resolve(root);
|
|
28
|
+
let rootReal;
|
|
29
|
+
try {
|
|
30
|
+
rootReal = await realpath(rootResolved);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw new RepositoryError(`workspace root is missing or unreadable: ${rootResolved}`);
|
|
34
|
+
}
|
|
35
|
+
if (!inputPath || inputPath === "." || inputPath === "./") {
|
|
36
|
+
return { absolute: rootReal, relative: ".", rootReal };
|
|
37
|
+
}
|
|
38
|
+
const candidate = resolveToCwd(inputPath, rootReal);
|
|
39
|
+
if (!isPathInsideRoot(rootReal, candidate)) {
|
|
40
|
+
throw new RepositoryError(`path escapes workspace root: ${inputPath}`);
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const real = await realpath(candidate);
|
|
44
|
+
if (!isPathInsideRoot(rootReal, real)) {
|
|
45
|
+
throw new RepositoryError(`path resolves outside workspace root: ${inputPath}`);
|
|
46
|
+
}
|
|
47
|
+
return { absolute: real, relative: toRepoRelative(rootReal, real), rootReal };
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
if (error instanceof RepositoryError)
|
|
51
|
+
throw error;
|
|
52
|
+
// ENOENT: allow listing a missing path to fail later with a clear error.
|
|
53
|
+
return { absolute: candidate, relative: toRepoRelative(rootReal, candidate), rootReal };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
export function shouldSkipName(name, includeHidden, exclude) {
|
|
57
|
+
if (name === "." || name === "..")
|
|
58
|
+
return true;
|
|
59
|
+
if (exclude.has(name))
|
|
60
|
+
return true;
|
|
61
|
+
if (!includeHidden && name.startsWith("."))
|
|
62
|
+
return true;
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
export function kindFromDirent(dirent) {
|
|
66
|
+
if (dirent.isSymbolicLink())
|
|
67
|
+
return "symlink";
|
|
68
|
+
if (dirent.isDirectory())
|
|
69
|
+
return "directory";
|
|
70
|
+
if (dirent.isFile())
|
|
71
|
+
return "file";
|
|
72
|
+
return "other";
|
|
73
|
+
}
|
|
74
|
+
export function assertNotAborted(signal) {
|
|
75
|
+
if (signal?.aborted)
|
|
76
|
+
throw new RepositoryError("Operation aborted");
|
|
77
|
+
}
|
|
78
|
+
export function assertDeadline(deadlineAt) {
|
|
79
|
+
if (deadlineAt !== undefined && Date.now() >= deadlineAt) {
|
|
80
|
+
throw new RepositoryError("Repository operation exceeded time limit");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
export function isBinaryBuffer(buffer) {
|
|
84
|
+
const length = Math.min(buffer.length, DEFAULT_BINARY_SNIFF_BYTES);
|
|
85
|
+
for (let i = 0; i < length; i++) {
|
|
86
|
+
if (buffer[i] === 0)
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
//# sourceMappingURL=path.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { RepositorySearchRequest, RepositorySearchResult, ResolvedRepositoryLimits } from "./types.js";
|
|
2
|
+
import type { RepositoryWalk } from "./walk.js";
|
|
3
|
+
export declare function compileSearchPattern(query: string, caseSensitive: boolean, maxPatternBytes: number): {
|
|
4
|
+
testLine: (line: string) => {
|
|
5
|
+
column: number;
|
|
6
|
+
} | null;
|
|
7
|
+
patternBytes: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function searchLocal(request: RepositorySearchRequest, defaults: ResolvedRepositoryLimits, walk: RepositoryWalk): Promise<RepositorySearchResult>;
|