pi-readseek 0.3.21 → 0.3.23
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/package.json +2 -2
- package/src/coerce-obvious-int.ts +21 -10
- package/src/edit-output.ts +24 -6
- package/src/grep-output.ts +18 -8
- package/src/grep.ts +36 -1
- package/src/hashline.ts +8 -4
- package/src/map-cache.ts +7 -4
- package/src/read.ts +21 -2
- package/src/readseek-client.ts +66 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-readseek",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.23",
|
|
4
4
|
"description": "Pi extension for readseek-backed hash-anchored read/edit/grep, structural code maps, structural search, and file exploration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"node": ">=20.0.0"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@jarkkojs/readseek": "^0.3.
|
|
42
|
+
"@jarkkojs/readseek": "^0.3.20",
|
|
43
43
|
"diff": "^8.0.3",
|
|
44
44
|
"ignore": "^7.0.5",
|
|
45
45
|
"picomatch": "^4.0.4",
|
|
@@ -1,30 +1,41 @@
|
|
|
1
1
|
const BASE10_INT_RE = /^-?\d+$/;
|
|
2
|
+
const MIN_SAFE_INTEGER = BigInt(Number.MIN_SAFE_INTEGER);
|
|
3
|
+
const MAX_SAFE_INTEGER = BigInt(Number.MAX_SAFE_INTEGER);
|
|
2
4
|
|
|
3
5
|
type CoercedIntResult =
|
|
4
6
|
| { ok: true; value: number | undefined }
|
|
5
7
|
| { ok: false; message: string };
|
|
6
8
|
|
|
9
|
+
function formatReceived(value: unknown): string {
|
|
10
|
+
if (typeof value === "number" || typeof value === "bigint") return String(value);
|
|
11
|
+
return JSON.stringify(value) ?? String(value);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function invalidInteger(value: unknown, name: string): CoercedIntResult {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
message: `Invalid ${name}: expected a safe base-10 integer, received ${formatReceived(value)}.`,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
export function coerceObviousBase10Int(value: unknown, name: string): CoercedIntResult {
|
|
8
22
|
if (value === undefined) {
|
|
9
23
|
return { ok: true, value: undefined };
|
|
10
24
|
}
|
|
11
25
|
|
|
12
26
|
if (typeof value === "number") {
|
|
13
|
-
if (Number.
|
|
27
|
+
if (Number.isSafeInteger(value)) {
|
|
14
28
|
return { ok: true, value };
|
|
15
29
|
}
|
|
16
|
-
return
|
|
17
|
-
ok: false,
|
|
18
|
-
message: `Invalid ${name}: expected a base-10 integer, received ${value}.`,
|
|
19
|
-
};
|
|
30
|
+
return invalidInteger(value, name);
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
if (typeof value === "string" && BASE10_INT_RE.test(value)) {
|
|
23
|
-
|
|
34
|
+
const parsed = BigInt(value);
|
|
35
|
+
if (parsed >= MIN_SAFE_INTEGER && parsed <= MAX_SAFE_INTEGER) {
|
|
36
|
+
return { ok: true, value: Number(parsed) };
|
|
37
|
+
}
|
|
24
38
|
}
|
|
25
39
|
|
|
26
|
-
return
|
|
27
|
-
ok: false,
|
|
28
|
-
message: `Invalid ${name}: expected a base-10 integer, received ${JSON.stringify(value)}.`,
|
|
29
|
-
};
|
|
40
|
+
return invalidInteger(value, name);
|
|
30
41
|
}
|
package/src/edit-output.ts
CHANGED
|
@@ -19,6 +19,19 @@ export interface EditOutputResult {
|
|
|
19
19
|
patch: string;
|
|
20
20
|
readseekValue: ReturnType<typeof buildReadseekEditResult>;
|
|
21
21
|
}
|
|
22
|
+
|
|
23
|
+
const EDIT_OPERATION_NAMES = ["set_line", "replace_lines", "insert_after", "replace"] as const;
|
|
24
|
+
|
|
25
|
+
type EditOperationName = (typeof EDIT_OPERATION_NAMES)[number];
|
|
26
|
+
|
|
27
|
+
type EditOperationWithNewText = {
|
|
28
|
+
new_text: string;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
function hasNewText(value: unknown): value is EditOperationWithNewText {
|
|
32
|
+
return typeof value === "object" && value !== null && typeof (value as { new_text?: unknown }).new_text === "string";
|
|
33
|
+
}
|
|
34
|
+
|
|
22
35
|
function getVisibleDiffStats(diff: string): { added: number; removed: number } {
|
|
23
36
|
const stats = parseDiffStats(diff);
|
|
24
37
|
if (stats.added > 0 || stats.removed > 0) return stats;
|
|
@@ -49,10 +62,12 @@ function extractNewTextValues(edits: unknown[] | undefined): string[] {
|
|
|
49
62
|
const values: string[] = [];
|
|
50
63
|
for (const edit of edits ?? []) {
|
|
51
64
|
if (!edit || typeof edit !== "object") continue;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
65
|
+
|
|
66
|
+
const operations = edit as Partial<Record<EditOperationName, unknown>>;
|
|
67
|
+
for (const operationName of EDIT_OPERATION_NAMES) {
|
|
68
|
+
const operation = operations[operationName];
|
|
69
|
+
if (hasNewText(operation)) values.push(operation.new_text);
|
|
70
|
+
}
|
|
56
71
|
}
|
|
57
72
|
return values;
|
|
58
73
|
}
|
|
@@ -62,10 +77,13 @@ function formatWhitespaceOnlyWarning(semanticSummary: SemanticSummary | undefine
|
|
|
62
77
|
return "⚠ Edit classified as whitespace-only — if you intended a behavior change, re-read to verify.";
|
|
63
78
|
}
|
|
64
79
|
function formatSemanticSuffix(semanticSummary: SemanticSummary | undefined): string {
|
|
65
|
-
|
|
80
|
+
if (!semanticSummary) return "";
|
|
81
|
+
|
|
82
|
+
const movedBlocks = semanticSummary.movedBlocks ?? 0;
|
|
66
83
|
if (movedBlocks <= 0) return "";
|
|
84
|
+
|
|
67
85
|
const blockWord = movedBlocks === 1 ? "block" : "blocks";
|
|
68
|
-
return ` [semantic: ${semanticSummary
|
|
86
|
+
return ` [semantic: ${semanticSummary.classification}, ${movedBlocks} ${blockWord} moved]`;
|
|
69
87
|
}
|
|
70
88
|
function formatReplaceHint(edits: unknown[] | undefined, noopEdits: unknown[]): string | undefined {
|
|
71
89
|
if ((noopEdits ?? []).length > 0) return undefined;
|
package/src/grep-output.ts
CHANGED
|
@@ -47,6 +47,14 @@ export interface GrepOutputGroup {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
type ScopedGrepOutputGroup = GrepOutputGroup & {
|
|
51
|
+
scope: NonNullable<GrepOutputGroup["scope"]>;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
function hasScope(group: GrepOutputGroup): group is ScopedGrepOutputGroup {
|
|
55
|
+
return group.scope !== undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
50
58
|
export interface BuildGrepOutputInput {
|
|
51
59
|
summary: boolean;
|
|
52
60
|
totalMatches: number;
|
|
@@ -100,13 +108,13 @@ function buildScopeMetadata(groups: GrepOutputGroup[], warnings: GrepScopeWarnin
|
|
|
100
108
|
return {
|
|
101
109
|
mode: "symbol" as const,
|
|
102
110
|
groups: groups
|
|
103
|
-
.filter(
|
|
111
|
+
.filter(hasScope)
|
|
104
112
|
.map((group) => ({
|
|
105
113
|
path: group.absolutePath,
|
|
106
114
|
displayPath: group.displayPath,
|
|
107
|
-
symbol: group.scope
|
|
115
|
+
symbol: group.scope.symbol,
|
|
108
116
|
matchCount: group.matchCount,
|
|
109
|
-
matchLines: [...group.scope
|
|
117
|
+
matchLines: [...group.scope.matchLines],
|
|
110
118
|
lineAnchors: group.entries.flatMap((entry) => (entry.kind === "separator" ? [] : [entry.line.anchor])),
|
|
111
119
|
})),
|
|
112
120
|
warnings: [...warnings],
|
|
@@ -132,14 +140,16 @@ export function buildGrepOutput(input: BuildGrepOutputInput): GrepOutputResult {
|
|
|
132
140
|
}
|
|
133
141
|
text = blocks.join("\n");
|
|
134
142
|
}
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
const passthroughLines = input.passthroughLines ?? [];
|
|
144
|
+
if (passthroughLines.length > 0) {
|
|
145
|
+
text += `\n\n${passthroughLines.join("\n")}`;
|
|
137
146
|
}
|
|
138
147
|
if (input.limit !== undefined && input.totalMatches === input.limit) {
|
|
139
148
|
text += `\n\n[Results truncated at ${input.limit} matches — refine pattern or increase limit]`;
|
|
140
149
|
}
|
|
141
|
-
|
|
142
|
-
|
|
150
|
+
const scopeWarnings = input.scopeWarnings ?? [];
|
|
151
|
+
if (!input.summary && input.scopeMode === "symbol" && scopeWarnings.length > 0) {
|
|
152
|
+
text = `${scopeWarnings.map((warning) => warning.message).join("\n\n")}\n\n${text}`;
|
|
143
153
|
}
|
|
144
154
|
const budget = resolveGrepOutputBudget();
|
|
145
155
|
const truncated = truncateHead(text, {
|
|
@@ -161,7 +171,7 @@ export function buildGrepOutput(input: BuildGrepOutputInput): GrepOutputResult {
|
|
|
161
171
|
})),
|
|
162
172
|
};
|
|
163
173
|
if (!input.summary && input.scopeMode === "symbol") {
|
|
164
|
-
readseekValue.scopes = buildScopeMetadata(input.groups,
|
|
174
|
+
readseekValue.scopes = buildScopeMetadata(input.groups, scopeWarnings);
|
|
165
175
|
}
|
|
166
176
|
|
|
167
177
|
return {
|
package/src/grep.ts
CHANGED
|
@@ -313,6 +313,16 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
313
313
|
let candidateUnparsedCount = 0;
|
|
314
314
|
const candidateLinePattern = /^.+(?::|-)\d+(?::|-)\s/;
|
|
315
315
|
|
|
316
|
+
const addSummaryMatch = (displayPath: string, absolutePath: string) => {
|
|
317
|
+
let group = groupsByPath.get(displayPath);
|
|
318
|
+
if (!group) {
|
|
319
|
+
group = { displayPath, absolutePath, matchCount: 0, entries: [] };
|
|
320
|
+
groupsByPath.set(displayPath, group);
|
|
321
|
+
}
|
|
322
|
+
group.matchCount++;
|
|
323
|
+
totalMatches++;
|
|
324
|
+
};
|
|
325
|
+
|
|
316
326
|
const addEntry = (displayPath: string, absolutePath: string, kind: "match" | "context", line: ReadseekLine) => {
|
|
317
327
|
let group = groupsByPath.get(displayPath);
|
|
318
328
|
if (!group) {
|
|
@@ -341,6 +351,12 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
341
351
|
}
|
|
342
352
|
parsedCount++;
|
|
343
353
|
const absolute = toAbsolutePath(parsed.displayPath);
|
|
354
|
+
if (p.summary) {
|
|
355
|
+
if (parsed.kind === "match") {
|
|
356
|
+
addSummaryMatch(parsed.displayPath, absolute);
|
|
357
|
+
}
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
344
360
|
const fileLines = await getFileLines(absolute);
|
|
345
361
|
if (fileLines === undefined) continue;
|
|
346
362
|
// Bare-CR remapping: rg treats the entire bare-CR file as line 1, and the
|
|
@@ -379,6 +395,25 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
379
395
|
});
|
|
380
396
|
}
|
|
381
397
|
|
|
398
|
+
if (p.summary && parsedCount === 0 && candidateUnparsedCount > 0) {
|
|
399
|
+
const passthroughDetails =
|
|
400
|
+
typeof result.details === "object" && result.details !== null
|
|
401
|
+
? (result.details as Record<string, unknown>)
|
|
402
|
+
: {};
|
|
403
|
+
return {
|
|
404
|
+
...result,
|
|
405
|
+
details: {
|
|
406
|
+
...passthroughDetails,
|
|
407
|
+
readseekValue: {
|
|
408
|
+
tool: "grep",
|
|
409
|
+
summary: true,
|
|
410
|
+
totalMatches: 0,
|
|
411
|
+
records: [],
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
382
417
|
if (parsedCount === 0 && candidateUnparsedCount > 0) {
|
|
383
418
|
const warning =
|
|
384
419
|
"[hashline grep passthrough] Unparsed grep format; returned original output.";
|
|
@@ -405,7 +440,7 @@ export async function executeGrep(opts: ExecuteGrepOptions): Promise<any> {
|
|
|
405
440
|
};
|
|
406
441
|
}
|
|
407
442
|
|
|
408
|
-
const summary = p.summary;
|
|
443
|
+
const summary = !!p.summary;
|
|
409
444
|
const effectiveLimit = typeof p.limit === "number" ? p.limit : 100;
|
|
410
445
|
const groups = [...groupsByPath.values()];
|
|
411
446
|
for (const group of groups) {
|
package/src/hashline.ts
CHANGED
|
@@ -71,13 +71,16 @@ interface HashlineGlobalState {
|
|
|
71
71
|
|
|
72
72
|
const HASHLINE_STATE_KEY = Symbol.for("pi-readseek.hashlineState.v1");
|
|
73
73
|
|
|
74
|
+
type HashlineGlobalSlots = typeof globalThis & Record<symbol, HashlineGlobalState | undefined>;
|
|
75
|
+
|
|
74
76
|
function getHashlineState(): HashlineGlobalState {
|
|
75
|
-
const globalObject = globalThis as
|
|
76
|
-
globalObject[HASHLINE_STATE_KEY]
|
|
77
|
+
const globalObject = globalThis as HashlineGlobalSlots;
|
|
78
|
+
const state = globalObject[HASHLINE_STATE_KEY] ?? {
|
|
77
79
|
h32Fn: null,
|
|
78
80
|
initPromise: null,
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
+
};
|
|
82
|
+
globalObject[HASHLINE_STATE_KEY] = state;
|
|
83
|
+
return state;
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
export async function ensureHashInit(): Promise<void> {
|
|
@@ -123,6 +126,7 @@ export function parseLineRef(ref: string): { line: number; hash: string; content
|
|
|
123
126
|
const match = normalized.match(new RegExp(`^(\\d+):([0-9a-fA-F]{${HASH_LEN}})$`));
|
|
124
127
|
if (!match) throw new Error(`Invalid line reference "${ref}". Expected "LINE:HASH" (e.g. "5:abc").`);
|
|
125
128
|
const line = Number.parseInt(match[1], 10);
|
|
129
|
+
if (!Number.isSafeInteger(line)) throw new Error(`Line number must be a safe integer, got ${match[1]} in "${ref}".`);
|
|
126
130
|
if (line < 1) throw new Error(`Line number must be >= 1, got ${line} in "${ref}".`);
|
|
127
131
|
return { line, hash: match[2], content: contentAfterPipe };
|
|
128
132
|
}
|
package/src/map-cache.ts
CHANGED
|
@@ -17,14 +17,17 @@ interface MapCacheGlobalState {
|
|
|
17
17
|
|
|
18
18
|
const MAP_CACHE_STATE_KEY = Symbol.for("pi-readseek.mapCacheState.v1");
|
|
19
19
|
|
|
20
|
+
type MapCacheGlobalSlots = typeof globalThis & Record<symbol, MapCacheGlobalState | undefined>;
|
|
21
|
+
|
|
20
22
|
function getMapCacheState(): MapCacheGlobalState {
|
|
21
|
-
const globalObject = globalThis as
|
|
22
|
-
globalObject[MAP_CACHE_STATE_KEY]
|
|
23
|
+
const globalObject = globalThis as MapCacheGlobalSlots;
|
|
24
|
+
const state = globalObject[MAP_CACHE_STATE_KEY] ?? {
|
|
23
25
|
cache: new Map<string, CacheEntry>(),
|
|
24
26
|
inflight: new Map<string, Promise<FileMap | null>>(),
|
|
25
27
|
maxSize: MAP_CACHE_MAX_SIZE,
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
+
};
|
|
29
|
+
globalObject[MAP_CACHE_STATE_KEY] = state;
|
|
30
|
+
return state;
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
function rememberInMemory(absPath: string, entry: CacheEntry): void {
|
package/src/read.ts
CHANGED
|
@@ -40,7 +40,7 @@ interface ReadParams {
|
|
|
40
40
|
limit?: number | string;
|
|
41
41
|
symbol?: string;
|
|
42
42
|
map?: boolean;
|
|
43
|
-
bundle?:
|
|
43
|
+
bundle?: string;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
interface ReadToolOptions {
|
|
@@ -56,6 +56,15 @@ export interface ExecuteReadOptions {
|
|
|
56
56
|
onSuccessfulRead?: FileAnchoredCallback;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
+
function hasReadAnchors(result: AgentToolResult<any>): boolean {
|
|
60
|
+
const details = (result as { details?: unknown }).details;
|
|
61
|
+
if (!details || typeof details !== "object") return false;
|
|
62
|
+
const readseekValue = (details as { readseekValue?: unknown }).readseekValue;
|
|
63
|
+
if (!readseekValue || typeof readseekValue !== "object") return false;
|
|
64
|
+
const lines = (readseekValue as { lines?: unknown }).lines;
|
|
65
|
+
return Array.isArray(lines) && lines.length > 0;
|
|
66
|
+
}
|
|
67
|
+
|
|
59
68
|
export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolResult<any>> {
|
|
60
69
|
const { toolCallId, params, signal, onUpdate, cwd, onSuccessfulRead } = opts;
|
|
61
70
|
await ensureHashInit();
|
|
@@ -76,10 +85,16 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
76
85
|
const message = `Invalid offset: expected a positive integer, received ${offset.value}.`;
|
|
77
86
|
return buildToolErrorResult("read", "invalid-offset", message, { path: rawParams.path });
|
|
78
87
|
}
|
|
88
|
+
const rawBundle = typeof rawParams.bundle === "string" ? rawParams.bundle.trim() : undefined;
|
|
89
|
+
const requestedMapViaBundle =
|
|
90
|
+
rawBundle === "map" ||
|
|
91
|
+
(rawBundle === "local" && rawParams.symbol === undefined && rawParams.map !== false);
|
|
79
92
|
const p = {
|
|
80
93
|
...rawParams,
|
|
81
94
|
offset: offset.value,
|
|
82
95
|
limit: limit.value,
|
|
96
|
+
map: rawParams.map === true || requestedMapViaBundle,
|
|
97
|
+
bundle: requestedMapViaBundle ? undefined : rawBundle,
|
|
83
98
|
};
|
|
84
99
|
if (rawParams.symbol !== undefined) {
|
|
85
100
|
const trimmedSymbol = typeof rawParams.symbol === "string" ? rawParams.symbol.trim() : "";
|
|
@@ -93,13 +108,17 @@ export async function executeRead(opts: ExecuteReadOptions): Promise<AgentToolRe
|
|
|
93
108
|
const absolutePath = resolveToCwd(rawPath, cwd);
|
|
94
109
|
const succeed = <T extends AgentToolResult<any>>(result: T): T => {
|
|
95
110
|
const isError = (result as { isError?: boolean }).isError;
|
|
96
|
-
if (!isError) {
|
|
111
|
+
if (!isError && hasReadAnchors(result)) {
|
|
97
112
|
onSuccessfulRead?.(absolutePath);
|
|
98
113
|
}
|
|
99
114
|
return result;
|
|
100
115
|
};
|
|
101
116
|
|
|
102
117
|
throwIfAborted(signal);
|
|
118
|
+
if (p.bundle !== undefined && p.bundle !== "local") {
|
|
119
|
+
const message = `Invalid bundle: expected "local", received ${JSON.stringify(p.bundle)}.`;
|
|
120
|
+
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
|
121
|
+
}
|
|
103
122
|
if (p.symbol && (p.offset !== undefined || p.limit !== undefined)) {
|
|
104
123
|
const message = "Cannot combine symbol with offset/limit. Use one or the other.";
|
|
105
124
|
return buildToolErrorResult("read", "invalid-params-combo", message, { path: rawParams.path });
|
package/src/readseek-client.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { spawn, type StdioOptions } from "node:child_process";
|
|
2
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
4
|
+
import { homedir } from "node:os";
|
|
3
5
|
import path from "node:path";
|
|
4
6
|
|
|
5
7
|
import { DetailLevel } from "./readseek/enums.js";
|
|
@@ -165,6 +167,8 @@ function symbolsFromReadseek(symbols: ReadseekSymbol[]): FileSymbol[] {
|
|
|
165
167
|
}
|
|
166
168
|
|
|
167
169
|
const require = createRequire(import.meta.url);
|
|
170
|
+
const READSEEK_REPO_PACKAGE_NAMES = new Set(["@jarkkojs/readseek", "readseek"]);
|
|
171
|
+
let defaultReadseekDirInit: Promise<string | null> | undefined;
|
|
168
172
|
|
|
169
173
|
function readseekPackageDir(): string {
|
|
170
174
|
return path.dirname(require.resolve("@jarkkojs/readseek/package.json"));
|
|
@@ -199,12 +203,39 @@ export function isReadseekAvailable(): boolean {
|
|
|
199
203
|
}
|
|
200
204
|
}
|
|
201
205
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
206
|
+
function directoryExists(dirPath: string): boolean {
|
|
207
|
+
try {
|
|
208
|
+
return statSync(dirPath).isDirectory();
|
|
209
|
+
} catch {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
205
212
|
}
|
|
206
213
|
|
|
207
|
-
|
|
214
|
+
function isOwnReadseekRepository(cwd = process.cwd()): boolean {
|
|
215
|
+
let dir = path.resolve(cwd);
|
|
216
|
+
while (true) {
|
|
217
|
+
const packageJsonPath = path.join(dir, "package.json");
|
|
218
|
+
if (existsSync(packageJsonPath)) {
|
|
219
|
+
try {
|
|
220
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { name?: unknown };
|
|
221
|
+
if (typeof packageJson.name === "string" && READSEEK_REPO_PACKAGE_NAMES.has(packageJson.name)) return true;
|
|
222
|
+
} catch {
|
|
223
|
+
// Ignore unreadable or invalid package manifests while walking up.
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const parent = path.dirname(dir);
|
|
228
|
+
if (parent === dir) return false;
|
|
229
|
+
dir = parent;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function defaultReadseekDir(): string | null {
|
|
234
|
+
const home = homedir();
|
|
235
|
+
return home ? path.join(home, ".pi", "readseek") : null;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async function spawnReadseekRaw(args: string[], options: RunReadseekOptions = {}): Promise<string> {
|
|
208
239
|
return new Promise<string>((resolve, reject) => {
|
|
209
240
|
let settled = false;
|
|
210
241
|
const fail = (error: Error): void => {
|
|
@@ -265,13 +296,43 @@ async function runReadseekRaw(args: string[], options: RunReadseekOptions = {}):
|
|
|
265
296
|
});
|
|
266
297
|
}
|
|
267
298
|
|
|
299
|
+
async function ensureDefaultReadseekDir(): Promise<string | null> {
|
|
300
|
+
const dir = defaultReadseekDir();
|
|
301
|
+
if (!dir) return null;
|
|
302
|
+
if (directoryExists(dir)) return dir;
|
|
303
|
+
|
|
304
|
+
defaultReadseekDirInit ??= spawnReadseekRaw(["--readseek-dir", dir, "init"])
|
|
305
|
+
.then(() => (directoryExists(dir) ? dir : null))
|
|
306
|
+
.catch(() => null)
|
|
307
|
+
.finally(() => {
|
|
308
|
+
defaultReadseekDirInit = undefined;
|
|
309
|
+
});
|
|
310
|
+
return defaultReadseekDirInit;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
async function readseekInvocationArgs(args: string[]): Promise<string[]> {
|
|
314
|
+
if (isOwnReadseekRepository()) return args;
|
|
315
|
+
|
|
316
|
+
const readseekDir = await ensureDefaultReadseekDir();
|
|
317
|
+
return readseekDir ? ["--readseek-dir", readseekDir, ...args] : args;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
interface RunReadseekOptions {
|
|
321
|
+
signal?: AbortSignal;
|
|
322
|
+
stdin?: string;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
async function runReadseekRaw(args: string[], options: RunReadseekOptions = {}): Promise<string> {
|
|
326
|
+
return spawnReadseekRaw(await readseekInvocationArgs(args), options);
|
|
327
|
+
}
|
|
328
|
+
|
|
268
329
|
async function runReadseek(args: string[], options: RunReadseekOptions = {}): Promise<unknown> {
|
|
269
330
|
const stdout = await runReadseekRaw(args, options);
|
|
270
331
|
return JSON.parse(stdout) as unknown;
|
|
271
332
|
}
|
|
272
333
|
|
|
273
334
|
function requireNumber(value: unknown, field: string): number {
|
|
274
|
-
if (typeof value !== "number" || !Number.
|
|
335
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) throw new Error(`invalid readseek ${field}: expected safe integer`);
|
|
275
336
|
return value;
|
|
276
337
|
}
|
|
277
338
|
|