blockpatch 0.1.0 → 1.0.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.
package/package.json CHANGED
@@ -1,33 +1,45 @@
1
1
  {
2
2
  "name": "blockpatch",
3
- "version": "0.1.0",
3
+ "version": "1.0.0",
4
4
  "description": "Anchored text block relocation patches",
5
5
  "type": "module",
6
6
  "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
- }
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/cheese-melted/blockpatch.git"
14
10
  },
11
+ "bugs": {
12
+ "url": "https://github.com/cheese-melted/blockpatch/issues"
13
+ },
14
+ "homepage": "https://github.com/cheese-melted/blockpatch#readme",
15
+ "keywords": [
16
+ "patch",
17
+ "diff",
18
+ "refactor",
19
+ "codemod",
20
+ "text",
21
+ "agent"
22
+ ],
15
23
  "bin": {
16
24
  "blockpatch": "dist/cli.js"
17
25
  },
18
26
  "files": [
19
27
  "dist",
20
28
  "README.md",
29
+ "CONTRIBUTING.md",
21
30
  "LICENSE"
22
31
  ],
23
32
  "scripts": {
24
33
  "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",
34
+ "typecheck": "tsc -p tsconfig.json --noEmit",
35
+ "build": "node -e \"fs.rmSync('dist', { recursive: true, force: true })\" && bun build ./src/cli.ts --target=node --outdir=dist",
36
+ "smoke:dist": "node test/built-cli-smoke.mjs",
37
+ "prepublishOnly": "bun run typecheck && bun test && bun run build",
27
38
  "publish:dry": "npm publish --dry-run"
28
39
  },
29
40
  "devDependencies": {
30
- "@types/bun": "latest",
41
+ "@types/bun": "^1.3.14",
42
+ "@types/node": "^26.1.0",
31
43
  "typescript": "^5.8.3"
32
44
  },
33
45
  "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
- }