block-in-file 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.
Files changed (67) hide show
  1. package/- +3 -0
  2. package/.beads/README.md +85 -0
  3. package/.beads/config.yaml +67 -0
  4. package/.beads/interactions.jsonl +0 -0
  5. package/.beads/issues.jsonl +23 -0
  6. package/.beads/metadata.json +4 -0
  7. package/.git-blame-ignore-revs +2 -0
  8. package/.gitattributes +3 -0
  9. package/.prettierrc.json +5 -0
  10. package/AGENTS.md +40 -0
  11. package/README.md +122 -0
  12. package/block-in-file.ts +150 -0
  13. package/content +10 -0
  14. package/deno.json +14 -0
  15. package/deno.lock +1084 -0
  16. package/doc/PLAN-envsubst.md +200 -0
  17. package/doc/PLAN-restructure.md +114 -0
  18. package/package.json +44 -0
  19. package/src/attributes.ts +161 -0
  20. package/src/backup.ts +180 -0
  21. package/src/block-parser.ts +170 -0
  22. package/src/block-remover.ts +128 -0
  23. package/src/conflict-detection.ts +179 -0
  24. package/src/defaults.ts +23 -0
  25. package/src/envsubst.ts +59 -0
  26. package/src/file-processor.ts +378 -0
  27. package/src/index.ts +5 -0
  28. package/src/input.ts +69 -0
  29. package/src/mode-handler.ts +39 -0
  30. package/src/output.ts +107 -0
  31. package/src/plugins/.beads/.local_version +1 -0
  32. package/src/plugins/.beads/issues.jsonl +21 -0
  33. package/src/plugins/.beads/metadata.json +4 -0
  34. package/src/plugins/config.ts +282 -0
  35. package/src/plugins/diff.ts +109 -0
  36. package/src/plugins/io.ts +72 -0
  37. package/src/plugins/logger.ts +41 -0
  38. package/src/tags/tag-merger.ts +31 -0
  39. package/src/tags/tag-mode.ts +1 -0
  40. package/src/tags/tag.ts +36 -0
  41. package/src/tags/tags.ts +4 -0
  42. package/src/tags/types.ts +4 -0
  43. package/src/timestamp.ts +39 -0
  44. package/src/types.ts +32 -0
  45. package/src/validation.ts +11 -0
  46. package/test/additive-cli.test.ts +109 -0
  47. package/test/additive.test.ts +233 -0
  48. package/test/attributes-integration.test.ts +161 -0
  49. package/test/attributes.test.ts +100 -0
  50. package/test/backup.test.ts +386 -0
  51. package/test/block-in-file.test.ts +235 -0
  52. package/test/block-parser.test.ts +221 -0
  53. package/test/block-remover.test.ts +209 -0
  54. package/test/cli.test.ts +254 -0
  55. package/test/defaults.test.ts +38 -0
  56. package/test/envsubst-edge-cases.test.ts +116 -0
  57. package/test/envsubst-integration.test.ts +78 -0
  58. package/test/envsubst.test.ts +184 -0
  59. package/test/input.test.ts +86 -0
  60. package/test/mode.test.ts +193 -0
  61. package/test/output.test.ts +44 -0
  62. package/test/tag-merger.test.ts +176 -0
  63. package/test/tags.test.ts +116 -0
  64. package/test/timestamp-integration.test.ts +209 -0
  65. package/test/timestamp.test.ts +76 -0
  66. package/tsconfig.json +16 -0
  67. package/vitest.config.ts +8 -0
@@ -0,0 +1,378 @@
1
+ import type { ModeArg, EnvsubstMode } from "./plugins/config.ts";
2
+ import type { LoggerExtension } from "./plugins/logger.ts";
3
+ import type { IOExtension } from "./plugins/io.ts";
4
+ import type { DiffExtension } from "./plugins/diff.ts";
5
+ import type { BackupOptions } from "./backup.ts";
6
+ import { parseAndInsertBlock } from "./block-parser.ts";
7
+ import { formatOutputs } from "./output.ts";
8
+ import { detectBlockState, shouldSkipForMode } from "./mode-handler.ts";
9
+ import { runValidation } from "./validation.ts";
10
+ import { detectConflicts, detectConflictsWithPattern } from "./conflict-detection.ts";
11
+ import { parseAttributes, applyAttributesSafe } from "./attributes.ts";
12
+ import { removeBlocks, type RemovalStats } from "./block-remover.ts";
13
+ import { substitute } from "./envsubst.ts";
14
+ import { generateTimestampTag, parseTimestampFormat } from "./timestamp.ts";
15
+ import { stripTagsForMatching, addTags, parseTags, type Tag } from "./tags/tags.ts";
16
+ import { applyTagMode } from "./tags/tag-merger.ts";
17
+ import type { TagMode } from "./tags/tag-mode.ts";
18
+
19
+ export interface ProcessContext {
20
+ file: string;
21
+ fileExists: boolean;
22
+ fileContent: string;
23
+ inputBlock: string;
24
+ opener: string;
25
+ closer: string;
26
+ before?: RegExp | boolean;
27
+ after?: RegExp | boolean;
28
+ mode?: ModeArg;
29
+ force?: boolean;
30
+ create?: boolean;
31
+ validateCmd?: string;
32
+ debug: boolean;
33
+ logger: LoggerExtension;
34
+ io: IOExtension;
35
+ diffExt: DiffExtension;
36
+ output: string;
37
+ dos: boolean;
38
+ backupOptions?: BackupOptions;
39
+ tempExt?: string;
40
+ tempExtAtomic?: string;
41
+ tempExtPrevalidate?: string;
42
+ appendNewline?: boolean;
43
+ attributes?: string;
44
+ removeAll?: string;
45
+ removeOrphans?: boolean;
46
+ envsubst?: EnvsubstMode;
47
+ additive?: boolean;
48
+ additiveBefore?: string;
49
+ additiveAfter?: string;
50
+ timestamp?: string;
51
+ tagMode?: string;
52
+ }
53
+
54
+ export interface ProcessResult {
55
+ status: "written" | "skipped" | "validation-failed" | "removed";
56
+ reason?: string;
57
+ outputs?: string[];
58
+ originalContent?: string;
59
+ removalStats?: RemovalStats;
60
+ }
61
+
62
+ export async function processFile(ctx: ProcessContext): Promise<ProcessResult> {
63
+ const {
64
+ file,
65
+ fileContent,
66
+ opener,
67
+ closer,
68
+ inputBlock,
69
+ before,
70
+ after,
71
+ mode,
72
+ force,
73
+ validateCmd,
74
+ debug,
75
+ logger,
76
+ io,
77
+ diffExt: _diffExt,
78
+ output,
79
+ dos,
80
+ backupOptions,
81
+ create,
82
+ tempExt,
83
+ tempExtAtomic,
84
+ tempExtPrevalidate,
85
+ appendNewline,
86
+ attributes,
87
+ removeAll,
88
+ removeOrphans,
89
+ envsubst,
90
+ additive,
91
+ additiveBefore,
92
+ additiveAfter,
93
+ timestamp,
94
+ tagMode,
95
+ } = ctx;
96
+
97
+ const processedInputBlock = envsubst ? substitute(inputBlock, { mode: envsubst }) : inputBlock;
98
+
99
+ if (debug) {
100
+ logger.debug(`Processing file: ${file}`);
101
+ }
102
+
103
+ if (additiveBefore && additiveAfter) {
104
+ throw new Error("Cannot specify both --additive-before and --additive-after");
105
+ }
106
+
107
+ let parsedAdditiveBefore: RegExp | "EOB" | "EOF" | "BOF" | undefined;
108
+ let parsedAdditiveAfter: RegExp | "EOB" | "EOF" | "BOF" | undefined;
109
+
110
+ if (additiveBefore) {
111
+ if (additiveBefore === "EOB" || additiveBefore === "EOF" || additiveBefore === "BOF") {
112
+ parsedAdditiveBefore = additiveBefore;
113
+ } else {
114
+ parsedAdditiveBefore = new RegExp(additiveBefore);
115
+ }
116
+ }
117
+
118
+ if (additiveAfter) {
119
+ if (additiveAfter === "EOF" || additiveAfter === "EOB" || additiveAfter === "BOF") {
120
+ parsedAdditiveAfter = additiveAfter;
121
+ } else {
122
+ parsedAdditiveAfter = new RegExp(additiveAfter);
123
+ }
124
+ }
125
+
126
+ const timestampFormat = parseTimestampFormat(timestamp);
127
+
128
+ let parsedTagMode: TagMode = "merge";
129
+ if (tagMode) {
130
+ if (tagMode === "merge" || tagMode === "replace") {
131
+ parsedTagMode = tagMode;
132
+ }
133
+ }
134
+
135
+ let newTags: Tag[] = [];
136
+
137
+ if (timestampFormat) {
138
+ newTags.push({
139
+ name: "timestamp",
140
+ value: generateTimestampTag(timestampFormat)
141
+ .replace(/^\[timestamp:/, "")
142
+ .replace(/\]$/, ""),
143
+ });
144
+ }
145
+
146
+ let existingTags: Tag[] = [];
147
+ const lines = fileContent.split("\n");
148
+
149
+ for (const line of lines) {
150
+ const strippedLine = stripTagsForMatching(line.trim());
151
+ if (strippedLine === opener) {
152
+ existingTags = parseTags(line);
153
+ break;
154
+ }
155
+ }
156
+
157
+ const finalTags = applyTagMode(existingTags, newTags, parsedTagMode);
158
+
159
+ let actualOpener = opener;
160
+ let openerPattern: RegExp | undefined;
161
+ let closerPattern: RegExp | undefined;
162
+
163
+ if (finalTags.length > 0) {
164
+ actualOpener = addTags(opener, finalTags);
165
+
166
+ const openerBase = opener.replace(/\s+/g, "\\s+");
167
+ const closerBase = closer.replace(/\s+/g, "\\s+");
168
+ openerPattern = new RegExp(
169
+ `^\\s*${openerBase}(\\s+\\[[a-zA-Z0-9_-]+(?::[^\\[\\]]+)?\\])*\\s*$`,
170
+ );
171
+ closerPattern = new RegExp(`^\\s*${closerBase}\\s*$`);
172
+ }
173
+
174
+ const conflictResult =
175
+ finalTags.length > 0
176
+ ? detectConflictsWithPattern(
177
+ fileContent,
178
+ opener,
179
+ closer,
180
+ openerPattern!,
181
+ closerPattern!,
182
+ logger,
183
+ )
184
+ : detectConflicts(fileContent, opener, closer, logger);
185
+ if (conflictResult.hasConflicts) {
186
+ throw new Error(
187
+ `File conflicts detected:\n${conflictResult.conflicts.map((c) => c.message).join("\n")}`,
188
+ );
189
+ }
190
+
191
+ if (removeAll) {
192
+ const blockNames = removeAll
193
+ .trim()
194
+ .split(/\s+/)
195
+ .filter((n) => n.length > 0);
196
+
197
+ if (debug) {
198
+ logger.debug(`Removing blocks: ${blockNames.join(", ")}`);
199
+ }
200
+
201
+ const commentMatch = opener.match(/^(#\s*|\/\/\s*)/) || opener.match(/^(\/\/\s*)/);
202
+ const comment = commentMatch ? commentMatch[1] : "";
203
+ const openerParts = opener.split(/\s+/);
204
+ const closerParts = closer.split(/\s+/);
205
+ const markerStart = openerParts[openerParts.length - 1] || "start";
206
+ const markerEnd = closerParts[closerParts.length - 1] || "end";
207
+
208
+ const { content, stats } = removeBlocks({
209
+ fileContent,
210
+ blockNames,
211
+ comment,
212
+ markerStart,
213
+ markerEnd,
214
+ removeOrphans: removeOrphans || false,
215
+ debug,
216
+ logger,
217
+ });
218
+
219
+ let originalContent: string | undefined;
220
+ if (output === "---") {
221
+ originalContent = fileContent;
222
+ if (backupOptions && backupOptions.enabled) {
223
+ const backup = await io.backupFile(file, backupOptions, fileContent);
224
+ if (backup && debug) {
225
+ logger.debug(`Created backup: ${backup}`);
226
+ }
227
+ }
228
+ }
229
+
230
+ const outputText = formatOutputs([content], dos);
231
+
232
+ if (output === "---" || output === "--") {
233
+ let tempFile: string;
234
+
235
+ if (validateCmd && !force) {
236
+ const ext = tempExtPrevalidate || tempExt || ".prevalidate";
237
+ tempFile = `${file}${ext}`;
238
+ await io.writeFile(tempFile, outputText);
239
+ try {
240
+ await runValidation(tempFile, validateCmd);
241
+ } catch (err) {
242
+ await io.deleteFile(tempFile);
243
+ throw err;
244
+ }
245
+ } else {
246
+ const ext = tempExtAtomic || tempExt || ".atomic";
247
+ tempFile = `${file}${ext}`;
248
+ await io.writeFile(tempFile, outputText);
249
+ }
250
+
251
+ await io.rename(tempFile, file);
252
+
253
+ if (attributes) {
254
+ const changes = parseAttributes(attributes);
255
+ await applyAttributesSafe(file, changes, { debug, logger, io });
256
+ }
257
+ } else if (output === "-") {
258
+ io.writeFile(output, outputText);
259
+ } else {
260
+ await io.writeFile(output, outputText);
261
+ }
262
+
263
+ if (debug) {
264
+ logger.debug(`Removed ${stats.removed} block(s) (${stats.orphans} orphans) from ${file}`);
265
+ }
266
+
267
+ return { status: "removed", removalStats: stats, originalContent };
268
+ }
269
+
270
+ const blockExists = fileContent.includes(opener);
271
+ const wouldChange = blockWouldChange(fileContent, processedInputBlock, opener, closer);
272
+ const state = detectBlockState(ctx.fileExists, blockExists, wouldChange);
273
+
274
+ if (mode && mode !== "none") {
275
+ const decision = shouldSkipForMode(state, mode);
276
+ if (decision.skip) {
277
+ if (debug) {
278
+ logger.debug(`mode=${mode}: skipping, ${decision.reason}`);
279
+ }
280
+ return { status: "skipped", reason: decision.reason };
281
+ }
282
+ }
283
+
284
+ if (create && !ctx.fileExists) {
285
+ await io.writeFile(file, "");
286
+ } else if (before === true) {
287
+ await io.writeFile(file, "");
288
+ }
289
+
290
+ const result = parseAndInsertBlock(fileContent, {
291
+ opener: opener,
292
+ closer: closer,
293
+ inputBlock: processedInputBlock,
294
+ before: before === true ? undefined : before,
295
+ after: after === true ? undefined : after,
296
+ appendNewline,
297
+ additive,
298
+ additiveBefore: parsedAdditiveBefore,
299
+ additiveAfter: parsedAdditiveAfter,
300
+ actualOpener: finalTags.length > 0 ? actualOpener : undefined,
301
+ });
302
+
303
+ const outputText = formatOutputs(result.outputs, dos);
304
+ if (debug) {
305
+ logger.debug(`Output text length: ${outputText.length}`);
306
+ }
307
+
308
+ let originalContent: string | undefined;
309
+ if (output === "---") {
310
+ originalContent = fileContent;
311
+ if (backupOptions && backupOptions.enabled) {
312
+ const backup = await io.backupFile(file, backupOptions, fileContent);
313
+ if (backup && debug) {
314
+ logger.debug(`Created backup: ${backup}`);
315
+ }
316
+ }
317
+ }
318
+
319
+ if (output === "---" || output === "--") {
320
+ let tempFile: string;
321
+
322
+ if (validateCmd && !force) {
323
+ const ext = tempExtPrevalidate || tempExt || ".prevalidate";
324
+ tempFile = `${file}${ext}`;
325
+ await io.writeFile(tempFile, outputText);
326
+ try {
327
+ await runValidation(tempFile, validateCmd);
328
+ } catch (err) {
329
+ await io.deleteFile(tempFile);
330
+ throw err;
331
+ }
332
+ } else {
333
+ const ext = tempExtAtomic || tempExt || ".atomic";
334
+ tempFile = `${file}${ext}`;
335
+ await io.writeFile(tempFile, outputText);
336
+ }
337
+
338
+ await io.rename(tempFile, file);
339
+
340
+ if (attributes) {
341
+ const changes = parseAttributes(attributes);
342
+ await applyAttributesSafe(file, changes, { debug, logger, io });
343
+ }
344
+ } else if (output === "-") {
345
+ io.writeFile(output, outputText);
346
+ } else {
347
+ await io.writeFile(output, outputText);
348
+ }
349
+
350
+ if (debug) {
351
+ logger.debug(`Completed processing: ${file}`);
352
+ }
353
+
354
+ return { status: "written", outputs: result.outputs, originalContent };
355
+ }
356
+
357
+ function blockWouldChange(
358
+ fileContent: string,
359
+ inputBlock: string,
360
+ opener: string,
361
+ closer: string,
362
+ ): boolean {
363
+ const openerIndex = fileContent.indexOf(opener);
364
+ if (openerIndex === -1) {
365
+ return true;
366
+ }
367
+
368
+ const closerIndex = fileContent.indexOf(closer, openerIndex);
369
+ if (closerIndex === -1) {
370
+ return true;
371
+ }
372
+
373
+ const blockContent = fileContent.slice(openerIndex + opener.length, closerIndex);
374
+ const lines = blockContent.split("\n");
375
+ const content = lines.slice(1).join("\n");
376
+
377
+ return content.trim() !== inputBlock.trim();
378
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { defaultOptions, getDefaultOptions } from "./defaults.ts";
2
+ export type { BlockInFileOptions, CreateArg, InputOptions, ParseResult } from "./types.ts";
3
+ export { parseAndInsertBlock } from "./block-parser.ts";
4
+ export { formatOutputs, generateDiff, writeDiff, writeOutput } from "./output.ts";
5
+ export { createOpt, get, openInput, readInput } from "./input.ts";
package/src/input.ts ADDED
@@ -0,0 +1,69 @@
1
+ import type { CreateArg, InputOptions } from "./types.ts";
2
+ import * as fs from "node:fs/promises";
3
+
4
+ export function get<T>(source: Partial<T>, defaults: Partial<T>, ...keys: (keyof T)[]) {
5
+ if (keys.length === 1) {
6
+ const key = keys[0];
7
+ const o = source[key];
8
+ return (o !== undefined ? o : defaults?.[key]) as any;
9
+ }
10
+
11
+ const o = Array.from({ length: keys.length });
12
+ for (const i in keys) {
13
+ o[i] = get(source, defaults, keys[i]);
14
+ }
15
+ return o;
16
+ }
17
+
18
+ export function createOpt(value: CreateArg, target: CreateArg): InputOptions | undefined {
19
+ return value === 1 || value === true || value === target ? { create: true } : undefined;
20
+ }
21
+
22
+ export async function openInput(file: string, opts?: InputOptions): Promise<fs.FileHandle> {
23
+ try {
24
+ if (file === "-" || file === undefined) {
25
+ return process.stdin as any;
26
+ }
27
+ return await fs.open(file, "r");
28
+ } catch (ex) {
29
+ if (opts?.create) {
30
+ return await fs.open("/dev/null", "r");
31
+ }
32
+ throw ex;
33
+ }
34
+ }
35
+
36
+ export async function readInput(file: string, opts?: InputOptions): Promise<string> {
37
+ const inputStream = await openInput(file, opts);
38
+ if (typeof inputStream === "object" && "readable" in inputStream) {
39
+ const reader = (inputStream.readable as ReadableStream<Uint8Array>).getReader();
40
+ const chunks: Uint8Array[] = [];
41
+
42
+ let done = false;
43
+ while (!done) {
44
+ const { value, done: isDone } = await reader.read();
45
+ done = isDone;
46
+ if (value) {
47
+ chunks.push(value);
48
+ }
49
+ }
50
+
51
+ const decoder = new TextDecoder();
52
+ return decoder.decode(concat(chunks));
53
+ }
54
+
55
+ throw new Error("Invalid input stream");
56
+ }
57
+
58
+ function concat(chunks: Uint8Array[]): Uint8Array {
59
+ const totalLength = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
60
+ const result = new Uint8Array(totalLength);
61
+ let offset = 0;
62
+
63
+ for (const chunk of chunks) {
64
+ result.set(chunk, offset);
65
+ offset += chunk.length;
66
+ }
67
+
68
+ return result;
69
+ }
@@ -0,0 +1,39 @@
1
+ import type { ModeArg } from "./plugins/config.ts";
2
+
3
+ export type BlockState = "absent" | "created" | "exists" | "updated" | "unchanged";
4
+
5
+ export interface ProcessDecision {
6
+ skip: boolean;
7
+ reason?: string;
8
+ }
9
+
10
+ export function shouldSkipForMode(state: BlockState, mode: ModeArg): ProcessDecision {
11
+ if (mode === "only") {
12
+ if (state === "exists" || state === "updated") {
13
+ return { skip: true, reason: "block already exists" };
14
+ }
15
+ } else if (mode === "ensure") {
16
+ if (state === "unchanged") {
17
+ return { skip: true, reason: "no changes needed" };
18
+ }
19
+ }
20
+
21
+ return { skip: false };
22
+ }
23
+
24
+ export function detectBlockState(
25
+ fileExists: boolean,
26
+ hasBlock: boolean,
27
+ wouldChange: boolean,
28
+ ): BlockState {
29
+ if (!fileExists) {
30
+ return "absent";
31
+ }
32
+ if (!hasBlock) {
33
+ return "absent";
34
+ }
35
+ if (wouldChange) {
36
+ return "updated";
37
+ }
38
+ return "unchanged";
39
+ }
package/src/output.ts ADDED
@@ -0,0 +1,107 @@
1
+ import * as fs from "node:fs/promises";
2
+
3
+ export function formatOutputs(outputs: string[], dos: boolean): string {
4
+ if (outputs.length === 0 || outputs[outputs.length - 1] !== "") {
5
+ outputs.push("");
6
+ }
7
+ return outputs.join(dos ? "\r\n" : "\n");
8
+ }
9
+
10
+ export async function writeOutput(
11
+ outputText: string,
12
+ output: string | undefined,
13
+ filePath: string,
14
+ ): Promise<void> {
15
+ if (!output || output === "---") {
16
+ await fs.writeFile(filePath, outputText, "utf-8");
17
+ } else if (output === "--") {
18
+ // no output
19
+ } else if (output === "-") {
20
+ console.log(outputText);
21
+ } else {
22
+ await fs.writeFile(output, outputText, "utf-8");
23
+ }
24
+ }
25
+
26
+ export function generateDiff(original: string, modified: string, filePath: string): string {
27
+ const originalLines = original.split("\n");
28
+ const modifiedLines = modified.split("\n");
29
+ const output: string[] = [];
30
+
31
+ output.push(`--- ${filePath}`);
32
+ output.push(`+++ ${filePath}`);
33
+
34
+ let i = 0;
35
+ let j = 0;
36
+
37
+ while (i < originalLines.length || j < modifiedLines.length) {
38
+ if (
39
+ i < originalLines.length &&
40
+ j < modifiedLines.length &&
41
+ originalLines[i] === modifiedLines[j]
42
+ ) {
43
+ output.push(` ${originalLines[i]}`);
44
+ i++;
45
+ j++;
46
+ } else {
47
+ const origStart = i;
48
+ const modStart = j;
49
+
50
+ while (
51
+ i < originalLines.length &&
52
+ (j >= modifiedLines.length || originalLines[i] !== modifiedLines[j])
53
+ ) {
54
+ let found = false;
55
+ for (let k = j; k < Math.min(j + 10, modifiedLines.length); k++) {
56
+ if (originalLines[i] === modifiedLines[k]) {
57
+ found = true;
58
+ break;
59
+ }
60
+ }
61
+ if (found) break;
62
+ i++;
63
+ }
64
+
65
+ while (
66
+ j < modifiedLines.length &&
67
+ (i >= originalLines.length || modifiedLines[j] !== originalLines[i])
68
+ ) {
69
+ let found = false;
70
+ for (let k = i; k < Math.min(i + 10, originalLines.length); k++) {
71
+ if (modifiedLines[j] === originalLines[k]) {
72
+ found = true;
73
+ break;
74
+ }
75
+ }
76
+ if (found) break;
77
+ j++;
78
+ }
79
+
80
+ for (let k = origStart; k < i; k++) {
81
+ output.push(`-${originalLines[k]}`);
82
+ }
83
+ for (let k = modStart; k < j; k++) {
84
+ output.push(`+${modifiedLines[k]}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ return output.join("\n");
90
+ }
91
+
92
+ export async function writeDiff(
93
+ diff: string | boolean | undefined,
94
+ originalContent: string,
95
+ newContent: string,
96
+ filePath: string,
97
+ ): Promise<void> {
98
+ if (!diff) return;
99
+
100
+ const diffText = generateDiff(originalContent, newContent, filePath);
101
+
102
+ if (diff === true || diff === "-") {
103
+ console.error(diffText);
104
+ } else if (typeof diff === "string") {
105
+ await fs.writeFile(diff, diffText, "utf-8");
106
+ }
107
+ }
@@ -0,0 +1 @@
1
+ 0.49.2