blockpatch 0.1.0 → 1.1.0

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 (60) hide show
  1. package/CONTRIBUTING.md +26 -0
  2. package/README.md +131 -74
  3. package/conformance/README.md +29 -0
  4. package/conformance/cases/ambiguous-source/case.json +8 -0
  5. package/conformance/cases/ambiguous-target/case.json +8 -0
  6. package/conformance/cases/create-file/case.json +8 -0
  7. package/conformance/cases/crlf/case.json +10 -0
  8. package/conformance/cases/cross-file-relocation/case.json +12 -0
  9. package/conformance/cases/delete-existing-file/case.json +10 -0
  10. package/conformance/cases/insert-existing-file/case.json +10 -0
  11. package/conformance/cases/no-trailing-newline/case.json +10 -0
  12. package/conformance/cases/remove-file/case.json +9 -0
  13. package/conformance/cases/same-file-relocation/case.json +10 -0
  14. package/conformance/runner.mjs +221 -0
  15. package/dist/cli.js +2908 -244
  16. package/docs/behavior.md +207 -0
  17. package/docs/commands.md +84 -0
  18. package/docs/spec.md +356 -0
  19. package/examples/README.md +15 -0
  20. package/examples/create-file/README.md +9 -0
  21. package/examples/create-file/expected/file.txt +1 -0
  22. package/examples/create-file/patch.blockpatch +8 -0
  23. package/examples/create-file/work/.gitkeep +1 -0
  24. package/examples/cross-file-relocation/README.md +10 -0
  25. package/examples/cross-file-relocation/expected/source.txt +2 -0
  26. package/examples/cross-file-relocation/expected/target.txt +6 -0
  27. package/examples/cross-file-relocation/patch.blockpatch +26 -0
  28. package/examples/cross-file-relocation/work/source.txt +5 -0
  29. package/examples/cross-file-relocation/work/target.txt +3 -0
  30. package/examples/delete-existing-file/README.md +9 -0
  31. package/examples/delete-existing-file/expected/file.txt +2 -0
  32. package/examples/delete-existing-file/patch.blockpatch +10 -0
  33. package/examples/delete-existing-file/work/file.txt +3 -0
  34. package/examples/failure-ambiguous-target/README.md +9 -0
  35. package/examples/failure-ambiguous-target/patch.blockpatch +14 -0
  36. package/examples/failure-ambiguous-target/work/file.txt +5 -0
  37. package/examples/insert-existing-file/README.md +9 -0
  38. package/examples/insert-existing-file/expected/file.txt +3 -0
  39. package/examples/insert-existing-file/patch.blockpatch +10 -0
  40. package/examples/insert-existing-file/work/file.txt +2 -0
  41. package/examples/remove-file/README.md +9 -0
  42. package/examples/remove-file/expected/.gitkeep +1 -0
  43. package/examples/remove-file/patch.blockpatch +8 -0
  44. package/examples/remove-file/work/file.txt +1 -0
  45. package/examples/reverse/README.md +9 -0
  46. package/examples/reverse/expected/file.txt +4 -0
  47. package/examples/reverse/patch.blockpatch +14 -0
  48. package/examples/reverse/work/file.txt +4 -0
  49. package/examples/same-file-relocation/README.md +9 -0
  50. package/examples/same-file-relocation/expected/file.txt +4 -0
  51. package/examples/same-file-relocation/patch.blockpatch +14 -0
  52. package/examples/same-file-relocation/work/file.txt +4 -0
  53. package/package.json +33 -13
  54. package/dist/cli.d.ts +0 -2
  55. package/dist/engine.d.ts +0 -19
  56. package/dist/errors.d.ts +0 -5
  57. package/dist/index.d.ts +0 -4
  58. package/dist/index.js +0 -263
  59. package/dist/parser.d.ts +0 -2
  60. package/dist/types.d.ts +0 -20
@@ -0,0 +1,8 @@
1
+ diff --blockpatch /dev/null b/file.txt
2
+ blockpatch version 1
3
+ blockpatch move id=move-1 payload-sha256=072a398e62ad50e40613b98e57f3e3236a471dcbd7c2b9f190440c22adc16a0f
4
+ --- /dev/null
5
+ +++ b/file.txt
6
+
7
+ @@ -0,0 +1,1 @@ blockpatch-target id=move-1
8
+ +created file
@@ -0,0 +1 @@
1
+ This directory is intentionally empty before applying the patch.
@@ -0,0 +1,10 @@
1
+ # Cross-File Relocation
2
+
3
+ Moves `movedThing` from `source.txt` to `target.txt`. Cross-file relocations use two sections tied by the same move id and payload hash.
4
+
5
+ ```sh
6
+ blockpatch check patch.blockpatch -d work
7
+ blockpatch apply patch.blockpatch -d work
8
+ diff -u expected/source.txt work/source.txt
9
+ diff -u expected/target.txt work/target.txt
10
+ ```
@@ -0,0 +1,2 @@
1
+ alpha()
2
+ omega()
@@ -0,0 +1,6 @@
1
+ class Target {
2
+ method() {}
3
+ function movedThing() {
4
+ return 42;
5
+ }
6
+ }
@@ -0,0 +1,26 @@
1
+ diff --blockpatch a/source.txt b/source.txt
2
+ blockpatch version 1
3
+ blockpatch move id=move-1 role=source payload-sha256=a990c0da5571138b0e2363af883a399fe214a137ad809c67f6530c618967a4e6
4
+ --- a/source.txt
5
+ +++ b/source.txt
6
+
7
+ @@ -1,5 +1,2 @@ blockpatch-source id=move-1
8
+ alpha()
9
+ -function movedThing() {
10
+ - return 42;
11
+ -}
12
+ omega()
13
+
14
+ diff --blockpatch a/target.txt b/target.txt
15
+ blockpatch version 1
16
+ blockpatch move id=move-1 role=target payload-sha256=a990c0da5571138b0e2363af883a399fe214a137ad809c67f6530c618967a4e6
17
+ --- a/target.txt
18
+ +++ b/target.txt
19
+
20
+ @@ -1,3 +1,6 @@ blockpatch-target id=move-1
21
+ class Target {
22
+ method() {}
23
+ +function movedThing() {
24
+ + return 42;
25
+ +}
26
+ }
@@ -0,0 +1,5 @@
1
+ alpha()
2
+ function movedThing() {
3
+ return 42;
4
+ }
5
+ omega()
@@ -0,0 +1,3 @@
1
+ class Target {
2
+ method() {}
3
+ }
@@ -0,0 +1,9 @@
1
+ # Delete Existing File
2
+
3
+ Deletes a patch-carried payload from an existing file with a source-only hunk.
4
+
5
+ ```sh
6
+ blockpatch check patch.blockpatch -d work
7
+ blockpatch apply patch.blockpatch -d work
8
+ diff -u expected/file.txt work/file.txt
9
+ ```
@@ -0,0 +1,2 @@
1
+ alpha
2
+ omega
@@ -0,0 +1,10 @@
1
+ diff --blockpatch a/file.txt b/file.txt
2
+ blockpatch version 1
3
+ blockpatch move id=move-1 payload-sha256=5dbe6af8d3a2a31f6696ebdae9e590977a19b827ec8e0a22ea1e3decf1a79b77
4
+ --- a/file.txt
5
+ +++ b/file.txt
6
+
7
+ @@ -1,3 +1,2 @@ blockpatch-source id=move-1
8
+ alpha
9
+ -remove me
10
+ omega
@@ -0,0 +1,3 @@
1
+ alpha
2
+ remove me
3
+ omega
@@ -0,0 +1,9 @@
1
+ # Failure: Ambiguous Target
2
+
3
+ Demonstrates strict target matching. The target anchor appears twice, so `blockpatch` refuses to guess.
4
+
5
+ ```sh
6
+ blockpatch check patch.blockpatch -d work
7
+ ```
8
+
9
+ Expected error: `Target anchor is ambiguous`.
@@ -0,0 +1,14 @@
1
+ diff --blockpatch a/file.txt b/file.txt
2
+ blockpatch version 1
3
+ blockpatch move id=move-1 payload-sha256=f721166071c491fd38ac82a8432ecc349f39f537a969054ab2c8d3175c731e7e
4
+ --- a/file.txt
5
+ +++ b/file.txt
6
+
7
+ @@ -1,3 +1,2 @@ blockpatch-source id=move-1
8
+ alpha
9
+ -move me
10
+ omega
11
+
12
+ @@ -4,1 +4,2 @@ blockpatch-target id=move-1
13
+ target
14
+ +move me
@@ -0,0 +1,5 @@
1
+ alpha
2
+ move me
3
+ omega
4
+ target
5
+ target
@@ -0,0 +1,9 @@
1
+ # Insert Existing File
2
+
3
+ Inserts a patch-carried payload into an existing file with a target-only hunk.
4
+
5
+ ```sh
6
+ blockpatch check patch.blockpatch -d work
7
+ blockpatch apply patch.blockpatch -d work
8
+ diff -u expected/file.txt work/file.txt
9
+ ```
@@ -0,0 +1,3 @@
1
+ alpha
2
+ inserted line
3
+ omega
@@ -0,0 +1,10 @@
1
+ diff --blockpatch a/file.txt b/file.txt
2
+ blockpatch version 1
3
+ blockpatch move id=move-1 payload-sha256=cbfeef0cd60ab2c01e036d23082a1f19ea3f5153d9cffde8790ad0152a3830f5
4
+ --- a/file.txt
5
+ +++ b/file.txt
6
+
7
+ @@ -1,2 +1,3 @@ blockpatch-target id=move-1
8
+ alpha
9
+ +inserted line
10
+ omega
@@ -0,0 +1,2 @@
1
+ alpha
2
+ omega
@@ -0,0 +1,9 @@
1
+ # Remove File
2
+
3
+ Removes `file.txt` with a whole-file `file -> /dev/null` patch.
4
+
5
+ ```sh
6
+ blockpatch check patch.blockpatch -d work
7
+ blockpatch apply patch.blockpatch -d work
8
+ test ! -e work/file.txt
9
+ ```
@@ -0,0 +1 @@
1
+ After applying the patch, file.txt should not exist.
@@ -0,0 +1,8 @@
1
+ diff --blockpatch a/file.txt /dev/null
2
+ blockpatch version 1
3
+ blockpatch move id=move-1 payload-sha256=4da5a83256114db88b111676cf3b2f4a04f202d3d7fc99417cac1ba5145713e2
4
+ --- a/file.txt
5
+ +++ /dev/null
6
+
7
+ @@ -1,1 +0,0 @@ blockpatch-source id=move-1
8
+ -obsolete file
@@ -0,0 +1 @@
1
+ obsolete file
@@ -0,0 +1,9 @@
1
+ # Reverse
2
+
3
+ Applies the same relocation patch in reverse. The `work/` tree starts in the already-applied state and `-R` restores the original ordering.
4
+
5
+ ```sh
6
+ blockpatch check -R patch.blockpatch -d work
7
+ blockpatch apply -R patch.blockpatch -d work
8
+ diff -u expected/file.txt work/file.txt
9
+ ```
@@ -0,0 +1,4 @@
1
+ alpha
2
+ move me
3
+ omega
4
+ target
@@ -0,0 +1,14 @@
1
+ diff --blockpatch a/file.txt b/file.txt
2
+ blockpatch version 1
3
+ blockpatch move id=move-1 payload-sha256=f721166071c491fd38ac82a8432ecc349f39f537a969054ab2c8d3175c731e7e
4
+ --- a/file.txt
5
+ +++ b/file.txt
6
+
7
+ @@ -1,3 +1,2 @@ blockpatch-source id=move-1
8
+ alpha
9
+ -move me
10
+ omega
11
+
12
+ @@ -4,1 +4,2 @@ blockpatch-target id=move-1
13
+ target
14
+ +move me
@@ -0,0 +1,4 @@
1
+ alpha
2
+ omega
3
+ target
4
+ move me
@@ -0,0 +1,9 @@
1
+ # Same-File Relocation
2
+
3
+ Moves `move me` from between `alpha` and `omega` to after `target` in the same file.
4
+
5
+ ```sh
6
+ blockpatch check patch.blockpatch -d work
7
+ blockpatch apply patch.blockpatch -d work
8
+ diff -u expected/file.txt work/file.txt
9
+ ```
@@ -0,0 +1,4 @@
1
+ alpha
2
+ omega
3
+ target
4
+ move me
@@ -0,0 +1,14 @@
1
+ diff --blockpatch a/file.txt b/file.txt
2
+ blockpatch version 1
3
+ blockpatch move id=move-1 payload-sha256=f721166071c491fd38ac82a8432ecc349f39f537a969054ab2c8d3175c731e7e
4
+ --- a/file.txt
5
+ +++ b/file.txt
6
+
7
+ @@ -1,3 +1,2 @@ blockpatch-source id=move-1
8
+ alpha
9
+ -move me
10
+ omega
11
+
12
+ @@ -4,1 +4,2 @@ blockpatch-target id=move-1
13
+ target
14
+ +move me
@@ -0,0 +1,4 @@
1
+ alpha
2
+ move me
3
+ omega
4
+ target
package/package.json CHANGED
@@ -1,33 +1,53 @@
1
1
  {
2
2
  "name": "blockpatch",
3
- "version": "0.1.0",
4
- "description": "Anchored text block relocation patches",
3
+ "version": "1.1.0",
4
+ "description": "Hash-verified, reviewable block-move patches for coding agents",
5
+ "packageManager": "bun@1.3.14",
5
6
  "type": "module",
6
7
  "license": "MIT",
7
- "main": "./dist/index.js",
8
- "types": "./dist/index.d.ts",
9
- "exports": {
10
- ".": {
11
- "types": "./dist/index.d.ts",
12
- "import": "./dist/index.js"
13
- }
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/cheese-melted/blockpatch.git"
14
11
  },
12
+ "bugs": {
13
+ "url": "https://github.com/cheese-melted/blockpatch/issues"
14
+ },
15
+ "homepage": "https://github.com/cheese-melted/blockpatch#readme",
16
+ "keywords": [
17
+ "patch",
18
+ "diff",
19
+ "refactor",
20
+ "codemod",
21
+ "text",
22
+ "agent"
23
+ ],
15
24
  "bin": {
16
- "blockpatch": "dist/cli.js"
25
+ "blockpatch": "dist/cli.js",
26
+ "blockpatch-conformance": "conformance/runner.mjs"
17
27
  },
18
28
  "files": [
19
29
  "dist",
30
+ "docs",
31
+ "examples",
32
+ "conformance",
20
33
  "README.md",
34
+ "CONTRIBUTING.md",
21
35
  "LICENSE"
22
36
  ],
23
37
  "scripts": {
24
38
  "test": "bun test",
25
- "build": "bun build ./src/cli.ts ./src/index.ts --target=node --outdir=dist && tsc -p tsconfig.build.json",
26
- "prepublishOnly": "bun test && bun run build",
39
+ "typecheck": "tsc -p tsconfig.json --noEmit",
40
+ "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && bun build ./src/cli.ts --target=node --outdir=dist",
41
+ "smoke:dist": "node test/built-cli-smoke.mjs",
42
+ "smoke:pack": "node test/packed-cli-smoke.mjs",
43
+ "conformance": "node conformance/runner.mjs node dist/cli.js",
44
+ "prepublishOnly": "bun run typecheck && bun test && bun run build && npm run smoke:dist && npm run smoke:pack",
45
+ "pack:dry": "npm pack --dry-run",
27
46
  "publish:dry": "npm publish --dry-run"
28
47
  },
29
48
  "devDependencies": {
30
- "@types/bun": "latest",
49
+ "@types/bun": "^1.3.14",
50
+ "@types/node": "^20",
31
51
  "typescript": "^5.8.3"
32
52
  },
33
53
  "engines": {
package/dist/cli.d.ts DELETED
@@ -1,2 +0,0 @@
1
- #!/usr/bin/env node
2
- export {};
package/dist/engine.d.ts DELETED
@@ -1,19 +0,0 @@
1
- import type { ApplyOptions, ApplyResult, BlockPatch } from "./types";
2
- interface ByteRange {
3
- start: number;
4
- end: number;
5
- }
6
- interface TargetSelection {
7
- range: ByteRange;
8
- insertIndex: number;
9
- }
10
- interface MoveSelection {
11
- source: ByteRange;
12
- target: TargetSelection;
13
- payload: Buffer;
14
- }
15
- export declare function checkPatchFile(patchPath: string, options?: ApplyOptions): Promise<ApplyResult>;
16
- export declare function applyPatchFile(patchPath: string, options?: ApplyOptions): Promise<ApplyResult>;
17
- export declare function selectMove(file: Buffer, patch: BlockPatch): MoveSelection;
18
- export declare function applyMove(file: Buffer, selection: MoveSelection): Buffer;
19
- export {};
package/dist/errors.d.ts DELETED
@@ -1,5 +0,0 @@
1
- export declare class BlockPatchError extends Error {
2
- readonly code: string;
3
- constructor(code: string, message: string);
4
- }
5
- export declare function fail(code: string, message: string): never;
package/dist/index.d.ts DELETED
@@ -1,4 +0,0 @@
1
- export { applyMove, applyPatchFile, checkPatchFile, selectMove } from "./engine";
2
- export { BlockPatchError } from "./errors";
3
- export { parseBlockPatch } from "./parser";
4
- export type { ApplyOptions, ApplyResult, BlockPatch, TargetAnchor, TargetKind } from "./types";
package/dist/index.js DELETED
@@ -1,263 +0,0 @@
1
- // src/engine.ts
2
- import { randomBytes } from "node:crypto";
3
- import { chmod, readFile, rename, stat, unlink, writeFile } from "node:fs/promises";
4
- import { basename, dirname, join, resolve } from "node:path";
5
-
6
- // src/errors.ts
7
- class BlockPatchError extends Error {
8
- code;
9
- constructor(code, message) {
10
- super(message);
11
- this.name = "BlockPatchError";
12
- this.code = code;
13
- }
14
- }
15
- function fail(code, message) {
16
- throw new BlockPatchError(code, message);
17
- }
18
-
19
- // src/parser.ts
20
- var beginLine = "*** Begin BlockPatch";
21
- var sourceBeforeLine = "*** Source Before";
22
- var sourcePayloadLine = "*** Source Payload";
23
- var sourceAfterLine = "*** Source After";
24
- var targetBeforeLine = "*** Target Before";
25
- var targetAfterLine = "*** Target After";
26
- var endLine = "*** End BlockPatch";
27
- var moveFilePrefix = "*** Move File: ";
28
- function parseBlockPatch(input) {
29
- const markers = collectMarkers(input);
30
- const sequence = markers.map((marker) => marker.name);
31
- const targetIndex = sequence.findIndex((name) => name === "targetBefore" || name === "targetAfter");
32
- if (targetIndex === -1) {
33
- fail("parse_error", "Patch must contain *** Target Before or *** Target After");
34
- }
35
- const expectedPrefix = ["begin", "moveFile", "sourceBefore", "sourcePayload", "sourceAfter"];
36
- const expectedSuffix = ["end"];
37
- const expected = [...expectedPrefix, sequence[targetIndex], ...expectedSuffix];
38
- if (sequence.length !== expected.length || sequence.some((name, index) => name !== expected[index])) {
39
- fail("parse_error", "Patch markers must appear in the v0 order");
40
- }
41
- const moveFile = markers[1];
42
- const path = moveFile.value?.trim();
43
- if (!path) {
44
- fail("parse_error", "*** Move File must include a path");
45
- }
46
- const sourceBefore = section(input, markers[2], markers[3]);
47
- const sourcePayload = section(input, markers[3], markers[4]);
48
- const sourceAfter = section(input, markers[4], markers[5]);
49
- const targetAnchor = section(input, markers[5], markers[6]);
50
- const target = {
51
- kind: markers[5].name === "targetBefore" ? "before" : "after",
52
- anchor: targetAnchor
53
- };
54
- if (sourceBefore.length === 0) {
55
- fail("parse_error", "*** Source Before must not be empty");
56
- }
57
- if (sourcePayload.length === 0) {
58
- fail("parse_error", "*** Source Payload must not be empty");
59
- }
60
- if (sourceAfter.length === 0) {
61
- fail("parse_error", "*** Source After must not be empty");
62
- }
63
- if (target.anchor.length === 0) {
64
- fail("parse_error", "*** Target Before/After must not be empty");
65
- }
66
- return {
67
- type: "move",
68
- path,
69
- sourceBefore,
70
- sourcePayload,
71
- sourceAfter,
72
- target
73
- };
74
- }
75
- function collectMarkers(input) {
76
- const markers = [];
77
- let position = 0;
78
- while (position <= input.length) {
79
- const lineStart = position;
80
- const lf = input.indexOf(10, position);
81
- const lineEnd = lf === -1 ? input.length : lf;
82
- const contentStart = lf === -1 ? input.length : lf + 1;
83
- const textEnd = lineEnd > lineStart && input[lineEnd - 1] === 13 ? lineEnd - 1 : lineEnd;
84
- const line = input.subarray(lineStart, textEnd).toString("utf8");
85
- const marker = markerForLine(line, lineStart, contentStart);
86
- if (marker !== undefined) {
87
- markers.push(marker);
88
- }
89
- if (lf === -1) {
90
- break;
91
- }
92
- position = lf + 1;
93
- }
94
- if (markers.length === 0) {
95
- fail("parse_error", "Patch does not contain BlockPatch markers");
96
- }
97
- return markers;
98
- }
99
- function markerForLine(line, lineStart, contentStart) {
100
- if (line === beginLine)
101
- return { name: "begin", lineStart, contentStart };
102
- if (line.startsWith(moveFilePrefix)) {
103
- return { name: "moveFile", value: line.slice(moveFilePrefix.length), lineStart, contentStart };
104
- }
105
- if (line === sourceBeforeLine)
106
- return { name: "sourceBefore", lineStart, contentStart };
107
- if (line === sourcePayloadLine)
108
- return { name: "sourcePayload", lineStart, contentStart };
109
- if (line === sourceAfterLine)
110
- return { name: "sourceAfter", lineStart, contentStart };
111
- if (line === targetBeforeLine)
112
- return { name: "targetBefore", lineStart, contentStart };
113
- if (line === targetAfterLine)
114
- return { name: "targetAfter", lineStart, contentStart };
115
- if (line === endLine)
116
- return { name: "end", lineStart, contentStart };
117
- return;
118
- }
119
- function section(input, marker, nextMarker) {
120
- let end = nextMarker.lineStart;
121
- if (end > marker.contentStart && input[end - 1] === 10) {
122
- end -= 1;
123
- if (end > marker.contentStart && input[end - 1] === 13) {
124
- end -= 1;
125
- }
126
- }
127
- return Buffer.from(input.subarray(marker.contentStart, end));
128
- }
129
-
130
- // src/engine.ts
131
- async function checkPatchFile(patchPath, options = {}) {
132
- return runPatchFile(patchPath, { ...options, dryRun: true });
133
- }
134
- async function applyPatchFile(patchPath, options = {}) {
135
- return runPatchFile(patchPath, options);
136
- }
137
- async function runPatchFile(patchPath, options) {
138
- const cwd = options.cwd ?? process.cwd();
139
- const patchBytes = await readFile(resolve(cwd, patchPath));
140
- const patch = parseBlockPatch(patchBytes);
141
- const changedPath = await applyMovePatch(patch, cwd, options.dryRun ?? false);
142
- return { changed: [changedPath] };
143
- }
144
- async function applyMovePatch(patch, cwd, dryRun) {
145
- const absolutePath = resolve(cwd, patch.path);
146
- const original = await readFile(absolutePath);
147
- const selection = selectMove(original, patch);
148
- const next = applyMove(original, selection);
149
- if (!dryRun && !next.equals(original)) {
150
- await writeAtomic(absolutePath, next);
151
- }
152
- return patch.path;
153
- }
154
- function selectMove(file, patch) {
155
- const source = findSourceRange(file, patch);
156
- const target = findTarget(file, patch);
157
- if (rangesOverlap(source, target.range)) {
158
- fail("target_overlaps_source", `Target anchor for ${patch.path} overlaps the source block`);
159
- }
160
- return {
161
- source,
162
- target,
163
- payload: Buffer.from(file.subarray(source.start, source.end))
164
- };
165
- }
166
- function applyMove(file, selection) {
167
- const withoutSource = Buffer.concat([
168
- file.subarray(0, selection.source.start),
169
- file.subarray(selection.source.end)
170
- ]);
171
- const targetIndex = selection.target.insertIndex > selection.source.end ? selection.target.insertIndex - selection.payload.length : selection.target.insertIndex;
172
- return Buffer.concat([
173
- withoutSource.subarray(0, targetIndex),
174
- selection.payload,
175
- withoutSource.subarray(targetIndex)
176
- ]);
177
- }
178
- function findSourceRange(file, patch) {
179
- const fullSource = Buffer.concat([patch.sourceBefore, patch.sourcePayload, patch.sourceAfter]);
180
- const fullMatches = indexesOf(file, fullSource);
181
- if (fullMatches.length === 1) {
182
- const start = fullMatches[0] + patch.sourceBefore.length;
183
- return { start, end: start + patch.sourcePayload.length };
184
- }
185
- if (fullMatches.length > 1) {
186
- fail("source_ambiguous", `Source block is ambiguous in ${patch.path}; matched ${fullMatches.length} locations`);
187
- }
188
- const envelopes = findSourceEnvelopes(file, patch);
189
- if (envelopes.length === 1) {
190
- fail("payload_mismatch", `Source payload does not match located source anchors in ${patch.path}`);
191
- }
192
- if (envelopes.length > 1) {
193
- fail("source_ambiguous", `Source anchors are ambiguous in ${patch.path}; matched ${envelopes.length} locations`);
194
- }
195
- fail("source_not_found", `Source anchors were not found in ${patch.path}`);
196
- }
197
- function findSourceEnvelopes(file, patch) {
198
- const beforeMatches = indexesOf(file, patch.sourceBefore);
199
- const afterMatches = indexesOf(file, patch.sourceAfter);
200
- const ranges = [];
201
- for (const beforeStart of beforeMatches) {
202
- const payloadStart = beforeStart + patch.sourceBefore.length;
203
- const afterStart = afterMatches.find((candidate) => candidate >= payloadStart);
204
- if (afterStart !== undefined) {
205
- ranges.push({ start: payloadStart, end: afterStart });
206
- }
207
- }
208
- return ranges;
209
- }
210
- function findTarget(file, patch) {
211
- const matches = indexesOf(file, patch.target.anchor);
212
- if (matches.length === 0) {
213
- fail("target_not_found", `Target anchor was not found in ${patch.path}`);
214
- }
215
- if (matches.length > 1) {
216
- fail("target_ambiguous", `Target anchor is ambiguous in ${patch.path}; matched ${matches.length} locations`);
217
- }
218
- const start = matches[0];
219
- const end = start + patch.target.anchor.length;
220
- return {
221
- range: { start, end },
222
- insertIndex: patch.target.kind === "before" ? start : end
223
- };
224
- }
225
- function indexesOf(haystack, needle) {
226
- if (needle.length === 0) {
227
- return [];
228
- }
229
- const indexes = [];
230
- let index = haystack.indexOf(needle);
231
- while (index !== -1) {
232
- indexes.push(index);
233
- index = haystack.indexOf(needle, index + 1);
234
- }
235
- return indexes;
236
- }
237
- function rangesOverlap(left, right) {
238
- return left.start < right.end && right.start < left.end;
239
- }
240
- async function writeAtomic(path, bytes) {
241
- const info = await stat(path);
242
- const dir = dirname(path);
243
- const base = basename(path);
244
- const temp = join(dir, `.${base}.${process.pid}.${randomBytes(8).toString("hex")}.tmp`);
245
- try {
246
- await writeFile(temp, bytes, { flag: "wx" });
247
- await chmod(temp, info.mode);
248
- await rename(temp, path);
249
- } catch (error) {
250
- await unlink(temp).catch(() => {
251
- return;
252
- });
253
- throw error;
254
- }
255
- }
256
- export {
257
- selectMove,
258
- parseBlockPatch,
259
- checkPatchFile,
260
- applyPatchFile,
261
- applyMove,
262
- BlockPatchError
263
- };
package/dist/parser.d.ts DELETED
@@ -1,2 +0,0 @@
1
- import type { BlockPatch } from "./types";
2
- export declare function parseBlockPatch(input: Buffer): BlockPatch;
package/dist/types.d.ts DELETED
@@ -1,20 +0,0 @@
1
- export type TargetKind = "before" | "after";
2
- export interface BlockPatch {
3
- type: "move";
4
- path: string;
5
- sourceBefore: Buffer;
6
- sourcePayload: Buffer;
7
- sourceAfter: Buffer;
8
- target: TargetAnchor;
9
- }
10
- export interface TargetAnchor {
11
- kind: TargetKind;
12
- anchor: Buffer;
13
- }
14
- export interface ApplyOptions {
15
- cwd?: string;
16
- dryRun?: boolean;
17
- }
18
- export interface ApplyResult {
19
- changed: string[];
20
- }