pi-hashline-edit-pro 4.2.1 → 4.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/index.ts +3 -2
- package/package.json +1 -1
- package/src/anchor-registry.ts +65 -6
- package/src/batch.ts +126 -29
- package/src/commit.ts +29 -7
- package/src/config.ts +88 -37
- package/src/grep.ts +14 -6
- package/src/hashline/alphabet.ts +5 -5
- package/src/hashline/anchor-table.json +1 -1
- package/src/hashline/apply.ts +3 -1
- package/src/hashline/resolve.ts +1 -1
- package/src/insert.ts +42 -38
- package/src/replace.ts +29 -20
- package/src/validation.ts +2 -2
- package/src/write-hook.ts +1 -1
package/src/config.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { readFile } from "fs/promises";
|
|
1
|
+
import { mkdir, readFile, rename, rm, stat } from "fs/promises";
|
|
2
|
+
import { dirname } from "path";
|
|
2
3
|
import { configPath } from "./paths";
|
|
3
4
|
import { errCode, isRec } from "./utils";
|
|
4
5
|
import { writeAtomic } from "./fs-write";
|
|
5
|
-
|
|
6
6
|
export type BoundaryDedupMode = "on" | "off" | "strict";
|
|
7
7
|
|
|
8
8
|
export const DEFAULT_DIFF_CONTEXT_LINES = 1;
|
|
@@ -66,16 +66,79 @@ function parseConfig(content: string): Config {
|
|
|
66
66
|
};
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
69
|
+
async function loadConfigFile(): Promise<{ config: Config; corrupted: boolean }> {
|
|
70
|
+
let content: string;
|
|
71
|
+
try {
|
|
72
|
+
content = await readFile(configPath(), "utf-8");
|
|
73
|
+
} catch (error: unknown) {
|
|
74
|
+
if (errCode(error) === "ENOENT") return { config: { ...DEFAULT_CONFIG }, corrupted: false };
|
|
75
|
+
console.error("Config file unreadable, using defaults:", error);
|
|
76
|
+
return { config: { ...DEFAULT_CONFIG }, corrupted: false };
|
|
77
|
+
}
|
|
71
78
|
try {
|
|
72
|
-
|
|
73
|
-
return parseConfig(content);
|
|
79
|
+
return { config: parseConfig(content), corrupted: false };
|
|
74
80
|
} catch (error: unknown) {
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
try {
|
|
82
|
+
const badPath = configPath();
|
|
83
|
+
await rename(badPath, `${badPath}.corrupt-${Date.now()}-${process.pid}-${Math.random().toString(36).slice(2)}`);
|
|
84
|
+
} catch { }
|
|
85
|
+
console.error("Config file corrupted, quarantined, using defaults:", error);
|
|
86
|
+
return { config: { ...DEFAULT_CONFIG }, corrupted: true };
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
export async function readConfig(): Promise<Config> {
|
|
90
|
+
return (await loadConfigFile()).config;
|
|
91
|
+
}
|
|
92
|
+
export async function readConfigWithStatus(): Promise<{ config: Config; corrupted: boolean }> {
|
|
93
|
+
return loadConfigFile();
|
|
94
|
+
}
|
|
95
|
+
const CONFIG_LOCK_RETRIES = 80;
|
|
96
|
+
const CONFIG_LOCK_DELAY_MS = 25;
|
|
97
|
+
const CONFIG_LOCK_STALE_MS = 5000;
|
|
98
|
+
async function acquireConfigLock(lockPath: string): Promise<void> {
|
|
99
|
+
try {
|
|
100
|
+
await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
101
|
+
} catch { }
|
|
102
|
+
for (let attempt = 0; attempt < CONFIG_LOCK_RETRIES; attempt++) {
|
|
103
|
+
try {
|
|
104
|
+
await mkdir(lockPath, { mode: 0o700 });
|
|
105
|
+
return;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
if (errCode(error) === "ENOENT") {
|
|
108
|
+
try {
|
|
109
|
+
await mkdir(dirname(lockPath), { recursive: true, mode: 0o700 });
|
|
110
|
+
} catch { }
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (errCode(error) !== "EEXIST") throw error;
|
|
114
|
+
try {
|
|
115
|
+
const st = await stat(lockPath);
|
|
116
|
+
if (Date.now() - st.mtimeMs > CONFIG_LOCK_STALE_MS) {
|
|
117
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
} catch { }
|
|
121
|
+
await new Promise<void>((r) => setTimeout(r, CONFIG_LOCK_DELAY_MS));
|
|
77
122
|
}
|
|
78
|
-
|
|
123
|
+
}
|
|
124
|
+
throw new Error(`[E_ACCESS] Could not acquire config lock: ${lockPath}`);
|
|
125
|
+
}
|
|
126
|
+
async function releaseConfigLock(lockPath: string): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
await rm(lockPath, { recursive: true, force: true });
|
|
129
|
+
} catch { }
|
|
130
|
+
}
|
|
131
|
+
export async function updateConfig(mut: (config: Config) => void): Promise<Config> {
|
|
132
|
+
const cfgPath = configPath();
|
|
133
|
+
const lockPath = `${cfgPath}.lock`;
|
|
134
|
+
await acquireConfigLock(lockPath);
|
|
135
|
+
try {
|
|
136
|
+
const config = await readConfig();
|
|
137
|
+
mut(config);
|
|
138
|
+
await writeConfig(config);
|
|
139
|
+
return config;
|
|
140
|
+
} finally {
|
|
141
|
+
await releaseConfigLock(lockPath);
|
|
79
142
|
}
|
|
80
143
|
}
|
|
81
144
|
export async function writeConfig(config: Config): Promise<void> {
|
|
@@ -84,50 +147,38 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
84
147
|
|
|
85
148
|
|
|
86
149
|
export async function toggleAutoRead(): Promise<boolean> {
|
|
87
|
-
const config = await
|
|
88
|
-
config.autoRead = !config.autoRead;
|
|
89
|
-
await writeConfig(config);
|
|
150
|
+
const config = await updateConfig((c) => { c.autoRead = !c.autoRead; });
|
|
90
151
|
return config.autoRead;
|
|
91
152
|
}
|
|
92
|
-
|
|
93
153
|
export async function toggleAnchorGrep(): Promise<boolean> {
|
|
94
|
-
const config = await
|
|
95
|
-
config.anchorGrepEnabled = !config.anchorGrepEnabled;
|
|
96
|
-
await writeConfig(config);
|
|
154
|
+
const config = await updateConfig((c) => { c.anchorGrepEnabled = !c.anchorGrepEnabled; });
|
|
97
155
|
return config.anchorGrepEnabled;
|
|
98
156
|
}
|
|
99
|
-
|
|
100
157
|
export async function toggleRequirePath(): Promise<boolean> {
|
|
101
|
-
const config = await
|
|
102
|
-
config.requirePath
|
|
103
|
-
await writeConfig(config);
|
|
104
|
-
return config.requirePath;
|
|
158
|
+
const config = await updateConfig((c) => { c.requirePath = !c.requirePath; });
|
|
159
|
+
return config.requirePath === true;
|
|
105
160
|
}
|
|
106
|
-
|
|
107
161
|
export async function toggleStrictInput(): Promise<boolean> {
|
|
108
|
-
const config = await
|
|
109
|
-
config.strictInput = !(config.strictInput === true);
|
|
110
|
-
await writeConfig(config);
|
|
162
|
+
const config = await updateConfig((c) => { c.strictInput = !(c.strictInput === true); });
|
|
111
163
|
return config.strictInput === true;
|
|
112
164
|
}
|
|
113
|
-
|
|
114
165
|
export async function cycleBoundaryDedupMode(): Promise<BoundaryDedupMode> {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
166
|
+
let next: BoundaryDedupMode = "on";
|
|
167
|
+
await updateConfig((c) => {
|
|
168
|
+
const current = c.boundaryDedupMode ?? "on";
|
|
169
|
+
next = BOUNDARY_DEDUP_MODES[(BOUNDARY_DEDUP_MODES.indexOf(current) + 1) % BOUNDARY_DEDUP_MODES.length] ?? "on";
|
|
170
|
+
c.boundaryDedupMode = next;
|
|
171
|
+
});
|
|
120
172
|
return next;
|
|
121
173
|
}
|
|
122
|
-
|
|
123
174
|
export async function getDiffContextLines(): Promise<number> {
|
|
124
175
|
return normalizeDiffContextLines((await readConfig()).diffContextLines);
|
|
125
176
|
}
|
|
126
|
-
|
|
127
177
|
export async function adjustDiffContextLines(delta: number): Promise<number> {
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
178
|
+
let next = DEFAULT_DIFF_CONTEXT_LINES;
|
|
179
|
+
await updateConfig((c) => {
|
|
180
|
+
next = normalizeDiffContextLines(normalizeDiffContextLines(c.diffContextLines) + delta);
|
|
181
|
+
c.diffContextLines = next;
|
|
182
|
+
});
|
|
132
183
|
return next;
|
|
133
184
|
}
|
package/src/grep.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
|
2
2
|
import { formatSize, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, type TruncationResult } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { Type } from "typebox";
|
|
4
4
|
import { stat } from "fs/promises";
|
|
5
|
-
import { dirname, join, relative } from "path";
|
|
5
|
+
import { dirname, isAbsolute, join, relative, win32 } from "path";
|
|
6
6
|
import { spawn, spawnSync } from "child_process";
|
|
7
7
|
import { createInterface } from "readline";
|
|
8
8
|
import { tryReadNormFile } from "./file-reader";
|
|
@@ -326,6 +326,12 @@ async function collectRgMatches(
|
|
|
326
326
|
signal?: AbortSignal,
|
|
327
327
|
): Promise<Map<string, number[]>> {
|
|
328
328
|
const args = ["--json", "--line-number", "--color=never", "--hidden", "--glob", "!.git"];
|
|
329
|
+
const wanted = req.limit ?? 100;
|
|
330
|
+
args.push("--max-count", String(wanted + 1));
|
|
331
|
+
if (typeof req.glob === "string" && req.glob.length > 0) {
|
|
332
|
+
const stripped = req.glob.startsWith("/") ? req.glob.slice(1) : req.glob;
|
|
333
|
+
if (!stripped.includes("/")) args.push("--glob", stripped);
|
|
334
|
+
}
|
|
329
335
|
if (req.ignoreCase) args.push("--ignore-case");
|
|
330
336
|
if (req.literal) args.push("--fixed-strings");
|
|
331
337
|
args.push("--", pattern, searchPath);
|
|
@@ -366,7 +372,7 @@ async function collectRgMatches(
|
|
|
366
372
|
if (typeof filePath === "string" && typeof lineNumber === "number") {
|
|
367
373
|
let abs: string;
|
|
368
374
|
try {
|
|
369
|
-
abs = filePath
|
|
375
|
+
abs = isAbsolute(filePath) || win32.isAbsolute(filePath) ? filePath : join(searchPath, filePath);
|
|
370
376
|
} catch {
|
|
371
377
|
abs = filePath;
|
|
372
378
|
}
|
|
@@ -451,7 +457,7 @@ const grepToolSchema = Type.Object(
|
|
|
451
457
|
}),
|
|
452
458
|
),
|
|
453
459
|
},
|
|
454
|
-
{ additionalProperties:
|
|
460
|
+
{ additionalProperties: true },
|
|
455
461
|
);
|
|
456
462
|
|
|
457
463
|
|
|
@@ -579,15 +585,17 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
579
585
|
let linesReplaced = 0;
|
|
580
586
|
let countOnly = false;
|
|
581
587
|
let poolSkipped = 0;
|
|
582
|
-
const
|
|
588
|
+
const makeGrepReader = (allocation: "real" | "shadow") => async (absPath: string) => {
|
|
583
589
|
try {
|
|
584
|
-
return await tryReadNormFile(absPath, ctx.cwd, { maxLines: MAX_HASH_LINES, noPersist: true, allocation
|
|
590
|
+
return await tryReadNormFile(absPath, ctx.cwd, { maxLines: MAX_HASH_LINES, noPersist: true, allocation, signal });
|
|
585
591
|
} catch (error) {
|
|
586
592
|
if (!isPoolExhaustedError(error)) throw error;
|
|
587
593
|
poolSkipped += 1;
|
|
588
594
|
return undefined;
|
|
589
595
|
}
|
|
590
596
|
};
|
|
597
|
+
const readGrepFile = makeGrepReader("real");
|
|
598
|
+
const readGrepFileShadow = makeGrepReader("shadow");
|
|
591
599
|
const rgMatches = await collectRgMatches(rgPath, req.pattern, base, req, signal);
|
|
592
600
|
const sortedFiles = [...rgMatches.keys()].sort(cmp);
|
|
593
601
|
for (let f = 0; f < sortedFiles.length; f++) {
|
|
@@ -603,7 +611,7 @@ export function regGrep(pi: ExtensionAPI): void {
|
|
|
603
611
|
const globPath = relative(globRoot, absPath).replace(/\\/g, "/");
|
|
604
612
|
if (!globRegex.test(globPath) && !globRegex.test(displayPath)) continue;
|
|
605
613
|
}
|
|
606
|
-
const norm = await
|
|
614
|
+
const norm = await readGrepFileShadow(absPath);
|
|
607
615
|
if (!norm) continue;
|
|
608
616
|
const hit = makeHitFromIndices(norm, relative(ctx.cwd, absPath).replace(/\\/g, "/"), indices, context, validatedRegex, totalForFile, indices.length);
|
|
609
617
|
const display = displayRowsForHit(hit);
|
package/src/hashline/alphabet.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import anchorData from "./anchor-table.json";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
const rawAnchors: unknown = (anchorData as { anchors?: unknown }).anchors;
|
|
3
|
+
if (typeof rawAnchors !== "string" || rawAnchors.length === 0 || rawAnchors.length % 4 !== 0) {
|
|
4
|
+
throw new Error("[E_REGISTRY] Anchor table is missing or corrupt; reinstall pi-hashline-edit-pro.");
|
|
5
|
+
}
|
|
6
|
+
const TABLE: string = rawAnchors;
|
|
5
7
|
export const HASH_LEN = 4;
|
|
6
|
-
|
|
7
8
|
export const ANCHOR_COUNT = TABLE.length / HASH_LEN;
|
|
8
|
-
|
|
9
9
|
const ALNUM = "A-Za-z0-9";
|
|
10
10
|
|
|
11
11
|
export const ALPH_RE = new RegExp(`^[${ALNUM}]+$`);
|