pi-hashline-edit-pro 0.15.0 → 0.15.1

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/fs-write.ts +93 -95
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hashline-edit-pro",
3
- "version": "0.15.0",
3
+ "version": "0.15.1",
4
4
  "description": "Strict hashline read/replace tool for pi-coding-agent with hash-anchored edits (3-char, 18-bit, perfect hashing)",
5
5
  "main": "index.ts",
6
6
  "repository": {
package/src/fs-write.ts CHANGED
@@ -1,116 +1,114 @@
1
1
  import { randomUUID } from "crypto";
2
2
  import {
3
- lstat,
4
- mkdir,
5
- open,
6
- readlink,
7
- rename,
8
- rm,
9
- stat,
10
- writeFile,
3
+ lstat,
4
+ mkdir,
5
+ open,
6
+ readlink,
7
+ rename,
8
+ rm,
9
+ stat,
10
+ writeFile,
11
11
  } from "fs/promises";
12
12
  import { dirname, join, parse, resolve, sep } from "path";
13
13
  import { errCode } from "./validation";
14
14
 
15
15
  export async function resolveTarget(path: string): Promise<string> {
16
- const absolutePath = resolve(path);
17
- const { root } = parse(absolutePath);
18
- const parts = absolutePath
19
- .slice(root.length)
20
- .split(sep)
21
- .filter((part) => part.length > 0);
22
- const visitedSymlinks = new Set<string>();
16
+ const absolutePath = resolve(path);
17
+ const { root } = parse(absolutePath);
18
+ const parts = absolutePath
19
+ .slice(root.length)
20
+ .split(sep)
21
+ .filter((part) => part.length > 0);
22
+ const visitedSymlinks = new Set<string>();
23
23
 
24
- async function resParts(
25
- currentPath: string,
26
- remainingParts: string[],
27
- ): Promise<string> {
28
- if (remainingParts.length === 0) {
29
- return currentPath;
30
- }
24
+ async function resParts(
25
+ currentPath: string,
26
+ remainingParts: string[],
27
+ ): Promise<string> {
28
+ if (remainingParts.length === 0) {
29
+ return currentPath;
30
+ }
31
31
 
32
- const [nextPart, ...tail] = remainingParts;
33
- const candidatePath = join(currentPath, nextPart);
32
+ const [nextPart, ...tail] = remainingParts;
33
+ const candidatePath = join(currentPath, nextPart);
34
34
 
35
- try {
36
- const candidateStats = await lstat(candidatePath);
37
- if (!candidateStats.isSymbolicLink()) {
38
- return resParts(candidatePath, tail);
39
- }
35
+ try {
36
+ const candidateStats = await lstat(candidatePath);
37
+ if (!candidateStats.isSymbolicLink()) {
38
+ return resParts(candidatePath, tail);
39
+ }
40
40
 
41
- if (visitedSymlinks.has(candidatePath)) {
42
- const error = new Error(
43
- `Too many symbolic links while resolving ${path}`,
44
- ) as NodeJS.ErrnoException;
45
- error.code = "ELOOP";
46
- throw error;
47
- }
48
- visitedSymlinks.add(candidatePath);
41
+ if (visitedSymlinks.has(candidatePath)) {
42
+ const error = new Error(
43
+ `Too many symbolic links while resolving ${path}`,
44
+ ) as NodeJS.ErrnoException;
45
+ error.code = "ELOOP";
46
+ throw error;
47
+ }
48
+ visitedSymlinks.add(candidatePath);
49
49
 
50
- const linkTargetPath = resolve(
51
- dirname(candidatePath),
52
- await readlink(candidatePath),
53
- );
54
- const targetParts = linkTargetPath
55
- .slice(parse(linkTargetPath).root.length)
56
- .split(sep)
57
- .filter((part) => part.length > 0);
58
- return resParts(parse(linkTargetPath).root, [
59
- ...targetParts,
60
- ...tail,
61
- ]);
62
- } catch (error: unknown) {
63
- if (errCode(error) === "ENOENT") {
64
- return join(candidatePath, ...tail);
65
- }
66
- throw error;
67
- }
68
- }
50
+ const linkTargetPath = resolve(
51
+ dirname(candidatePath),
52
+ await readlink(candidatePath),
53
+ );
54
+ const targetParts = linkTargetPath
55
+ .slice(parse(linkTargetPath).root.length)
56
+ .split(sep)
57
+ .filter((part) => part.length > 0);
58
+ return resParts(parse(linkTargetPath).root, [
59
+ ...targetParts,
60
+ ...tail,
61
+ ]);
62
+ } catch (error: unknown) {
63
+ if (errCode(error) === "ENOENT") {
64
+ return join(candidatePath, ...tail);
65
+ }
66
+ throw error;
67
+ }
68
+ }
69
69
 
70
- return resParts(root, parts);
70
+ return resParts(root, parts);
71
71
  }
72
72
 
73
73
  export async function writeAtomic(
74
- path: string,
75
- content: string,
74
+ path: string,
75
+ content: string,
76
76
  ): Promise<void> {
77
- const targetPath = await resolveTarget(path);
77
+ const targetPath = await resolveTarget(path);
78
78
 
79
- let existingStats: Awaited<ReturnType<typeof stat>> | null = null;
80
- try {
81
- existingStats = await stat(targetPath);
82
- } catch (error: unknown) {
83
- if (errCode(error) !== "ENOENT") {
84
- throw error;
85
- }
86
- }
79
+ let existingStats: Awaited<ReturnType<typeof stat>> | null = null;
80
+ try {
81
+ existingStats = await stat(targetPath);
82
+ } catch (error: unknown) {
83
+ if (errCode(error) !== "ENOENT") {
84
+ throw error;
85
+ }
86
+ }
87
87
 
88
- if (existingStats && existingStats.nlink > 1) {
89
- await writeFile(targetPath, content, "utf-8");
90
- return;
91
- }
88
+ if (existingStats && existingStats.nlink > 1) {
89
+ await writeFile(targetPath, content, "utf-8");
90
+ return;
91
+ }
92
92
 
93
- const dir = dirname(targetPath);
94
- const tempPath = join(dir, `.tmp-${randomUUID()}`);
95
- await mkdir(dir, { recursive: true });
96
- const tempHandle = await open(tempPath, "wx", 0o600);
97
- try {
98
- await tempHandle.writeFile(content, "utf-8");
99
- if (existingStats) {
100
- await tempHandle.chmod(existingStats.mode & 0o7777);
101
- }
102
- } finally {
103
- await tempHandle.close();
104
- }
105
-
106
- try {
107
- await rename(tempPath, targetPath);
108
- } catch (error: unknown) {
109
- try {
110
- await rm(tempPath, { force: true });
111
- } catch {
112
- // best-effort cleanup
113
- }
114
- throw error;
115
- }
93
+ const dir = dirname(targetPath);
94
+ const tempPath = join(dir, `.tmp-${randomUUID()}`);
95
+ await mkdir(dir, { recursive: true });
96
+ const tempHandle = await open(tempPath, "wx", 0o600);
97
+ try {
98
+ await tempHandle.writeFile(content, "utf-8");
99
+ if (existingStats) {
100
+ await tempHandle.chmod(existingStats.mode & 0o7777);
101
+ }
102
+ } catch (error: unknown) {
103
+ await tempHandle.close();
104
+ try { await rm(tempPath, { force: true }); } catch { /* best-effort */ }
105
+ throw error;
106
+ }
107
+ try {
108
+ await tempHandle.close();
109
+ await rename(tempPath, targetPath);
110
+ } catch (error: unknown) {
111
+ try { await rm(tempPath, { force: true }); } catch { /* best-effort */ }
112
+ throw error;
113
+ }
116
114
  }