pi-hashline-edit-pro 4.2.1 → 4.2.2

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/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
- export async function readConfig(): Promise<Config> {
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
- const content = await readFile(configPath(), "utf-8");
73
- return parseConfig(content);
79
+ return { config: parseConfig(content), corrupted: false };
74
80
  } catch (error: unknown) {
75
- if (errCode(error) !== "ENOENT") {
76
- console.error("Config file corrupted, using defaults:", error);
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
- return { ...DEFAULT_CONFIG };
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 readConfig();
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 readConfig();
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 readConfig();
102
- config.requirePath = !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 readConfig();
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
- const config = await readConfig();
116
- const current = config.boundaryDedupMode ?? "on";
117
- const next = BOUNDARY_DEDUP_MODES[(BOUNDARY_DEDUP_MODES.indexOf(current) + 1) % BOUNDARY_DEDUP_MODES.length] ?? "on";
118
- config.boundaryDedupMode = next;
119
- await writeConfig(config);
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
- const config = await readConfig();
129
- const next = normalizeDiffContextLines(normalizeDiffContextLines(config.diffContextLines) + delta);
130
- config.diffContextLines = next;
131
- await writeConfig(config);
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
@@ -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);
@@ -451,7 +457,7 @@ const grepToolSchema = Type.Object(
451
457
  }),
452
458
  ),
453
459
  },
454
- { additionalProperties: false },
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 readGrepFile = async (absPath: string) => {
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: "real", signal });
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 readGrepFile(absPath);
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);
@@ -1,11 +1,11 @@
1
1
  import anchorData from "./anchor-table.json";
2
-
3
- const TABLE: string = anchorData.anchors;
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}]+$`);