@claudiu-ceia/spatch 0.3.0 → 0.3.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 (64) hide show
  1. package/README.md +187 -114
  2. package/dist/app.d.ts +1 -0
  3. package/dist/app.js +28 -0
  4. package/dist/cli.js +3 -119
  5. package/dist/command/flags.d.ts +64 -0
  6. package/dist/command/flags.js +73 -0
  7. package/dist/command/interactive/path-resolution.d.ts +5 -0
  8. package/dist/command/interactive/path-resolution.js +55 -0
  9. package/dist/command/interactive/run.d.ts +3 -0
  10. package/dist/command/interactive/run.js +166 -0
  11. package/dist/command/interactive/terminal.d.ts +32 -0
  12. package/dist/command/interactive/terminal.js +79 -0
  13. package/dist/command/interactive/types.d.ts +13 -0
  14. package/dist/command/interactive/types.js +0 -0
  15. package/dist/command/interactive/validation.d.ts +2 -0
  16. package/dist/command/interactive/validation.js +19 -0
  17. package/dist/command/interactive.d.ts +1 -0
  18. package/dist/command/interactive.js +1 -0
  19. package/dist/command/output.d.ts +11 -0
  20. package/dist/command/output.js +82 -0
  21. package/dist/command.d.ts +22 -25
  22. package/dist/command.js +36 -334
  23. package/dist/file-write.d.ts +24 -0
  24. package/dist/file-write.js +50 -0
  25. package/dist/index.d.ts +3 -5
  26. package/dist/index.js +2 -3
  27. package/dist/internal/command.d.ts +1 -0
  28. package/dist/internal/command.js +1 -0
  29. package/dist/phases/output.d.ts +2 -1
  30. package/dist/phases/parse.d.ts +2 -2
  31. package/dist/phases/parse.js +6 -6
  32. package/dist/phases/patch-document.d.ts +6 -0
  33. package/dist/{patch-document.js → phases/patch-document.js} +6 -15
  34. package/dist/phases/rewrite.js +128 -33
  35. package/dist/replacement-spans.d.ts +7 -0
  36. package/dist/replacement-spans.js +26 -0
  37. package/dist/spatch.d.ts +0 -1
  38. package/dist/spatch.js +1 -2
  39. package/dist/tsconfig.tsbuildinfo +1 -1
  40. package/package.json +19 -13
  41. package/src/app.ts +34 -0
  42. package/src/cli.ts +3 -143
  43. package/src/command/flags.ts +85 -0
  44. package/src/command/interactive/path-resolution.ts +72 -0
  45. package/src/command/interactive/run.ts +207 -0
  46. package/src/command/interactive/terminal.ts +134 -0
  47. package/src/command/interactive/types.ts +20 -0
  48. package/src/command/interactive/validation.ts +36 -0
  49. package/src/command/interactive.ts +1 -0
  50. package/src/command/output.ts +109 -0
  51. package/src/command.ts +82 -484
  52. package/src/file-write.ts +80 -0
  53. package/src/index.ts +3 -21
  54. package/src/internal/command.ts +1 -0
  55. package/src/phases/output.ts +1 -1
  56. package/src/phases/parse.ts +7 -7
  57. package/src/{patch-document.ts → phases/patch-document.ts} +16 -30
  58. package/src/phases/rewrite.ts +177 -53
  59. package/src/replacement-spans.ts +37 -0
  60. package/src/spatch.ts +1 -6
  61. package/dist/patch-document.d.ts +0 -9
  62. package/dist/template.d.ts +0 -2
  63. package/dist/template.js +0 -1
  64. package/src/template.ts +0 -2
package/dist/command.js CHANGED
@@ -1,143 +1,58 @@
1
- import { readFile, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { stderr as processStderr, stdin as processStdin, stdout as processStdout, } from "node:process";
4
- import { createInterface } from "node:readline/promises";
1
+ import { stderr as processStderr, stdin as processStdin } from "node:process";
5
2
  import { buildCommand } from "@stricli/core";
6
- import chalk, { Chalk } from "chalk";
7
3
  import { resolveTextInput } from "@claudiu-ceia/astkit-core";
4
+ import { patchCommandFlagParameters, validatePatchCommandFlags, } from "./command/flags.js";
5
+ import { runInteractivePatchCommand } from "./command/interactive.js";
6
+ import { formatPatchOutput } from "./command/output.js";
8
7
  import { patchProject } from "./spatch.js";
9
8
  export async function runPatchCommand(patchInput, scope, flags, options = {}) {
9
+ validatePatchCommandFlags(flags);
10
10
  const patchScope = scope ?? ".";
11
11
  const patchCwd = flags.cwd;
12
+ const logger = options.logger ??
13
+ (flags.verbose ? (line) => processStderr.write(`${line}\n`) : undefined);
12
14
  const resolvedPatchInput = await resolvePatchInput(patchInput, {
13
15
  cwd: patchCwd,
14
16
  encoding: "utf8",
15
17
  readStdin: options.readStdin,
18
+ stdinStream: options.stdinStream,
16
19
  });
17
- if (flags.interactive ?? false) {
18
- if (flags["dry-run"] ?? false) {
19
- throw new Error("Cannot combine --interactive with --dry-run.");
20
- }
21
- return runInteractivePatchCommand(resolvedPatchInput, patchScope, patchCwd, flags["no-color"] ?? false, options.interactiveDecider);
22
- }
23
- return patchProject(resolvedPatchInput, {
20
+ const patchOptions = {
24
21
  concurrency: flags.concurrency,
25
22
  cwd: patchCwd,
26
- dryRun: flags["dry-run"] ?? false,
23
+ encoding: options.encoding,
24
+ logger,
27
25
  scope: patchScope,
28
26
  verbose: flags.verbose,
29
- logger: flags.verbose ? (line) => processStderr.write(`${line}\n`) : undefined,
30
- });
31
- }
32
- export function formatPatchOutput(result, options = {}) {
33
- const chalkInstance = buildChalk(options);
34
- const useColor = chalkInstance.level > 0;
35
- const lines = [];
36
- const changedFiles = result.files.filter((file) => file.replacementCount > 0);
37
- for (const file of changedFiles) {
38
- const headerPrefix = useColor ? chalkInstance.bold : (value) => value;
39
- lines.push(headerPrefix(`diff --git a/${file.file} b/${file.file}`));
40
- lines.push(useColor ? chalkInstance.gray(`--- a/${file.file}`) : `--- a/${file.file}`);
41
- lines.push(useColor ? chalkInstance.gray(`+++ b/${file.file}`) : `+++ b/${file.file}`);
42
- for (const occurrence of file.occurrences) {
43
- if (occurrence.matched === occurrence.replacement) {
44
- continue;
45
- }
46
- const oldCount = countLines(occurrence.matched);
47
- const newCount = countLines(occurrence.replacement);
48
- const hunkHeader = `@@ -${occurrence.line},${oldCount} +${occurrence.line},${newCount} @@`;
49
- lines.push(useColor ? chalkInstance.cyan(hunkHeader) : hunkHeader);
50
- for (const oldLine of splitDiffLines(occurrence.matched)) {
51
- const line = `-${oldLine}`;
52
- lines.push(useColor ? chalkInstance.red(line) : line);
53
- }
54
- for (const newLine of splitDiffLines(occurrence.replacement)) {
55
- const line = `+${newLine}`;
56
- lines.push(useColor ? chalkInstance.green(line) : line);
57
- }
58
- }
59
- }
60
- if (changedFiles.length === 0) {
61
- lines.push(useColor ? chalkInstance.gray("No changes.") : "No changes.");
27
+ };
28
+ if (flags.interactive ?? false) {
29
+ return runInteractivePatchCommand(resolvedPatchInput, {
30
+ ...patchOptions,
31
+ noColor: flags["no-color"] ?? false,
32
+ interactiveDecider: options.interactiveDecider,
33
+ });
62
34
  }
63
- const summary = [
64
- `${result.filesChanged} ${pluralize("file", result.filesChanged)} changed`,
65
- `${result.totalReplacements} ${pluralize("replacement", result.totalReplacements)}`,
66
- result.dryRun ? "(dry-run)" : null,
67
- ]
68
- .filter((part) => part !== null)
69
- .join(", ");
70
- lines.push(useColor ? chalkInstance.gray(summary) : summary);
71
- return lines.join("\n");
35
+ return patchProject(resolvedPatchInput, {
36
+ ...patchOptions,
37
+ dryRun: (flags["dry-run"] ?? false) || (flags.check ?? false),
38
+ });
72
39
  }
73
40
  export const patchCommand = buildCommand({
74
41
  async func(flags, patchInput, scope) {
75
42
  const result = await runPatchCommand(patchInput, scope, flags);
76
43
  if (flags.json ?? false) {
77
44
  this.process.stdout.write(JSON.stringify(result, null, 2) + "\n");
45
+ enforceCheckMode(flags, result);
78
46
  return;
79
47
  }
80
48
  const output = formatPatchOutput(result, {
81
- color: Boolean(processStdout.isTTY) && !(flags["no-color"] ?? false),
49
+ color: Boolean(this.process.stdout.isTTY) && !(flags["no-color"] ?? false),
82
50
  });
83
51
  this.process.stdout.write(`${output}\n`);
52
+ enforceCheckMode(flags, result);
84
53
  },
85
54
  parameters: {
86
- flags: {
87
- concurrency: {
88
- kind: "parsed",
89
- optional: true,
90
- brief: "Max files processed concurrently (default: 8)",
91
- placeholder: "n",
92
- parse: (input) => {
93
- const value = Number(input);
94
- if (!Number.isFinite(value) || value <= 0) {
95
- throw new Error("--concurrency must be a positive number");
96
- }
97
- return Math.floor(value);
98
- },
99
- },
100
- verbose: {
101
- kind: "parsed",
102
- optional: true,
103
- brief: "Print perf tracing (1=summary, 2=includes slow files)",
104
- placeholder: "level",
105
- parse: (input) => {
106
- const value = Number(input);
107
- if (!Number.isFinite(value) || value < 0) {
108
- throw new Error("--verbose must be a non-negative number");
109
- }
110
- return Math.floor(value);
111
- },
112
- },
113
- interactive: {
114
- kind: "boolean",
115
- optional: true,
116
- brief: "Interactively select which matches to apply",
117
- },
118
- json: {
119
- kind: "boolean",
120
- optional: true,
121
- brief: "Output structured JSON instead of compact diff-style text",
122
- },
123
- "no-color": {
124
- kind: "boolean",
125
- optional: true,
126
- brief: "Disable colored output",
127
- },
128
- "dry-run": {
129
- kind: "boolean",
130
- optional: true,
131
- brief: "Preview changes without writing files",
132
- },
133
- cwd: {
134
- kind: "parsed",
135
- optional: true,
136
- brief: "Working directory for resolving patch file and scope",
137
- placeholder: "path",
138
- parse: (input) => input,
139
- },
140
- },
55
+ flags: patchCommandFlagParameters,
141
56
  positional: {
142
57
  kind: "tuple",
143
58
  parameters: [
@@ -159,244 +74,31 @@ export const patchCommand = buildCommand({
159
74
  brief: "Apply structural rewrite from a patch document",
160
75
  },
161
76
  });
162
- function buildChalk(options) {
163
- if (options.chalkInstance) {
164
- return options.chalkInstance;
165
- }
166
- const shouldColor = options.color ?? false;
167
- if (!shouldColor) {
168
- return new Chalk({ level: 0 });
169
- }
170
- const level = chalk.level > 0 ? chalk.level : 1;
171
- return new Chalk({ level });
172
- }
173
- function splitDiffLines(text) {
174
- const normalized = text.replaceAll("\r\n", "\n");
175
- if (normalized.length === 0) {
176
- return [""];
77
+ function enforceCheckMode(flags, result) {
78
+ if (!(flags.check ?? false)) {
79
+ return;
177
80
  }
178
- return normalized.split("\n");
179
- }
180
- function countLines(text) {
181
- const normalized = text.replaceAll("\r\n", "\n");
182
- if (normalized.length === 0) {
183
- return 0;
81
+ if (result.totalReplacements > 0) {
82
+ throw new Error(`Check failed: ${result.totalReplacements} replacements would be applied in ${result.filesChanged} files.`);
184
83
  }
185
- return normalized.split("\n").length;
186
- }
187
- function pluralize(word, count) {
188
- return count === 1 ? word : `${word}s`;
189
- }
190
- async function runInteractivePatchCommand(patchInput, scope, cwd, noColor, interactiveDecider) {
191
- if (!interactiveDecider && (!processStdin.isTTY || !processStdout.isTTY)) {
192
- throw new Error("Interactive mode requires a TTY stdin/stdout.");
193
- }
194
- const startedAt = Date.now();
195
- const dryResult = await patchProject(patchInput, {
196
- cwd,
197
- dryRun: true,
198
- scope,
199
- });
200
- const totalChanges = dryResult.files.reduce((count, file) => count +
201
- file.occurrences.filter((occurrence) => occurrence.matched !== occurrence.replacement).length, 0);
202
- let interactivePrompt = null;
203
- const decider = interactiveDecider ??
204
- ((interactivePrompt = await createTerminalInteractiveDecider(noColor)),
205
- interactivePrompt.decider);
206
- const selectedByFile = new Map();
207
- let applyAll = false;
208
- let stop = false;
209
- let changeNumber = 0;
210
- try {
211
- for (const file of dryResult.files) {
212
- const selected = [];
213
- for (const occurrence of file.occurrences) {
214
- if (occurrence.matched === occurrence.replacement) {
215
- continue;
216
- }
217
- changeNumber += 1;
218
- if (applyAll) {
219
- selected.push(occurrence);
220
- continue;
221
- }
222
- const choice = await decider({
223
- file: file.file,
224
- occurrence,
225
- changeNumber,
226
- totalChanges,
227
- });
228
- if (choice === "yes") {
229
- selected.push(occurrence);
230
- continue;
231
- }
232
- if (choice === "all") {
233
- applyAll = true;
234
- selected.push(occurrence);
235
- continue;
236
- }
237
- if (choice === "quit") {
238
- stop = true;
239
- break;
240
- }
241
- }
242
- selectedByFile.set(file.file, selected);
243
- if (stop) {
244
- break;
245
- }
246
- }
247
- }
248
- finally {
249
- interactivePrompt?.close();
250
- }
251
- const fileResults = [];
252
- let filesChanged = 0;
253
- let totalReplacements = 0;
254
- for (const file of dryResult.files) {
255
- const selected = selectedByFile.get(file.file) ?? [];
256
- if (selected.length === 0) {
257
- fileResults.push({
258
- ...file,
259
- replacementCount: 0,
260
- changed: false,
261
- byteDelta: 0,
262
- occurrences: [],
263
- });
264
- continue;
265
- }
266
- const absolutePath = path.resolve(cwd ?? process.cwd(), file.file);
267
- const originalText = await readFile(absolutePath, "utf8");
268
- const rewrittenText = applySelectedOccurrences(originalText, selected);
269
- const changed = rewrittenText !== originalText;
270
- if (changed) {
271
- await writeFile(absolutePath, rewrittenText, "utf8");
272
- }
273
- const replacementCount = selected.filter((occurrence) => occurrence.matched !== occurrence.replacement).length;
274
- totalReplacements += replacementCount;
275
- if (changed) {
276
- filesChanged += 1;
277
- }
278
- fileResults.push({
279
- ...file,
280
- replacementCount,
281
- changed,
282
- byteDelta: changed
283
- ? Buffer.byteLength(rewrittenText, "utf8") -
284
- Buffer.byteLength(originalText, "utf8")
285
- : 0,
286
- occurrences: selected,
287
- });
288
- }
289
- return {
290
- ...dryResult,
291
- dryRun: false,
292
- filesChanged,
293
- totalReplacements,
294
- elapsedMs: Date.now() - startedAt,
295
- files: fileResults,
296
- };
297
84
  }
298
85
  async function resolvePatchInput(patchInput, options) {
299
86
  if (patchInput !== "-") {
300
87
  return await resolveTextInput(patchInput, { cwd: options.cwd, encoding: options.encoding });
301
88
  }
302
- const reader = options.readStdin ?? (() => readAllFromStdin(options.encoding));
89
+ const reader = options.readStdin ??
90
+ (() => readAllFromStream(options.stdinStream ?? processStdin, options.encoding));
303
91
  const text = await reader();
304
92
  if (text.length === 0) {
305
93
  throw new Error("Patch document read from stdin was empty.");
306
94
  }
307
95
  return text;
308
96
  }
309
- async function readAllFromStdin(encoding) {
310
- // Read raw patch document from stdin (e.g. `cat rule.spatch | spatch - src`).
311
- // `node:process` stdin is a stream in both Node and Bun.
312
- const stdin = processStdin;
313
- stdin.setEncoding(encoding);
97
+ async function readAllFromStream(stream, encoding) {
98
+ stream.setEncoding?.(encoding);
314
99
  let text = "";
315
- for await (const chunk of stdin) {
100
+ for await (const chunk of stream) {
316
101
  text += String(chunk);
317
102
  }
318
103
  return text;
319
104
  }
320
- function applySelectedOccurrences(source, occurrences) {
321
- if (occurrences.length === 0) {
322
- return source;
323
- }
324
- const sorted = [...occurrences].sort((left, right) => left.start - right.start);
325
- const parts = [];
326
- let cursor = 0;
327
- for (const occurrence of sorted) {
328
- parts.push(source.slice(cursor, occurrence.start));
329
- parts.push(occurrence.replacement);
330
- cursor = occurrence.end;
331
- }
332
- parts.push(source.slice(cursor));
333
- return parts.join("");
334
- }
335
- async function createTerminalInteractiveDecider(noColor) {
336
- const chalkInstance = buildChalk({
337
- color: processStdout.isTTY && !noColor,
338
- });
339
- const useColor = chalkInstance.level > 0;
340
- const rl = createInterface({
341
- input: processStdin,
342
- output: processStdout,
343
- });
344
- return {
345
- decider: async ({ file, occurrence, changeNumber, totalChanges }) => {
346
- processStdout.write(`\n${formatInteractiveChangeBlock({ file, occurrence, changeNumber, totalChanges }, {
347
- chalkInstance,
348
- color: useColor,
349
- })}\n`);
350
- while (true) {
351
- const answer = await rl.question(useColor
352
- ? chalkInstance.bold("Choice [y/n/a/q] (default: n): ")
353
- : "Choice [y/n/a/q] (default: n): ");
354
- const parsed = parseInteractiveChoice(answer);
355
- if (parsed) {
356
- return parsed;
357
- }
358
- processStdout.write(useColor
359
- ? `${chalkInstance.yellow("Invalid choice.")} Use y, n, a, or q.\n`
360
- : "Invalid choice. Use y, n, a, or q.\n");
361
- }
362
- },
363
- close: () => rl.close(),
364
- };
365
- }
366
- function formatInteractiveChangeBlock(ctx, options = {}) {
367
- const chalkInstance = buildChalk(options);
368
- const useColor = chalkInstance.level > 0;
369
- const divider = "─".repeat(72);
370
- const oldCount = countLines(ctx.occurrence.matched);
371
- const newCount = countLines(ctx.occurrence.replacement);
372
- const hunkHeader = `@@ -${ctx.occurrence.line},${oldCount} +${ctx.occurrence.line},${newCount} @@`;
373
- const lines = [
374
- useColor ? chalkInstance.gray(divider) : divider,
375
- useColor
376
- ? chalkInstance.bold(`Change ${ctx.changeNumber}/${ctx.totalChanges} · ${ctx.file}:${ctx.occurrence.line}:${ctx.occurrence.character}`)
377
- : `Change ${ctx.changeNumber}/${ctx.totalChanges} · ${ctx.file}:${ctx.occurrence.line}:${ctx.occurrence.character}`,
378
- useColor ? chalkInstance.cyan(hunkHeader) : hunkHeader,
379
- ...splitDiffLines(ctx.occurrence.matched).map((line) => useColor ? chalkInstance.red(`-${line}`) : `-${line}`),
380
- ...splitDiffLines(ctx.occurrence.replacement).map((line) => useColor ? chalkInstance.green(`+${line}`) : `+${line}`),
381
- useColor
382
- ? chalkInstance.gray("Actions: [y] apply · [n] skip · [a] apply remaining · [q] quit")
383
- : "Actions: [y] apply · [n] skip · [a] apply remaining · [q] quit",
384
- ];
385
- return lines.join("\n");
386
- }
387
- function parseInteractiveChoice(answer) {
388
- const normalized = answer.trim().toLowerCase();
389
- if (normalized.length === 0 || normalized === "n" || normalized === "no") {
390
- return "no";
391
- }
392
- if (normalized === "y" || normalized === "yes") {
393
- return "yes";
394
- }
395
- if (normalized === "a" || normalized === "all") {
396
- return "all";
397
- }
398
- if (normalized === "q" || normalized === "quit") {
399
- return "quit";
400
- }
401
- return null;
402
- }
@@ -0,0 +1,24 @@
1
+ type WriteFileIfUnchangedAtomicallyInput = {
2
+ filePath: string;
3
+ originalText: string;
4
+ rewrittenText: string;
5
+ encoding: BufferEncoding;
6
+ operationName: string;
7
+ fs?: FileWriteFs;
8
+ };
9
+ type FileWriteFs = {
10
+ readFile: (path: string, encoding: BufferEncoding) => Promise<string>;
11
+ stat: (path: string) => Promise<{
12
+ mode: number;
13
+ }>;
14
+ writeFile: (path: string, data: string, options: {
15
+ encoding: BufferEncoding;
16
+ mode: number;
17
+ }) => Promise<void>;
18
+ rename: (oldPath: string, newPath: string) => Promise<void>;
19
+ rm: (path: string, options: {
20
+ force: boolean;
21
+ }) => Promise<void>;
22
+ };
23
+ export declare function writeFileIfUnchangedAtomically(input: WriteFileIfUnchangedAtomicallyInput): Promise<void>;
24
+ export {};
@@ -0,0 +1,50 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { readFile, rename, rm, stat, writeFile } from "node:fs/promises";
3
+ import path from "node:path";
4
+ const defaultFs = {
5
+ readFile,
6
+ stat,
7
+ writeFile,
8
+ rename,
9
+ rm,
10
+ };
11
+ export async function writeFileIfUnchangedAtomically(input) {
12
+ const fs = input.fs ?? defaultFs;
13
+ let currentText;
14
+ try {
15
+ currentText = await fs.readFile(input.filePath, input.encoding);
16
+ }
17
+ catch {
18
+ throw buildStaleApplyError(input.filePath, input.operationName);
19
+ }
20
+ if (currentText !== input.originalText) {
21
+ throw buildStaleApplyError(input.filePath, input.operationName);
22
+ }
23
+ let fileStats;
24
+ try {
25
+ fileStats = await fs.stat(input.filePath);
26
+ }
27
+ catch {
28
+ throw buildStaleApplyError(input.filePath, input.operationName);
29
+ }
30
+ const tempPath = buildAtomicTempPath(input.filePath);
31
+ await fs.writeFile(tempPath, input.rewrittenText, {
32
+ encoding: input.encoding,
33
+ mode: fileStats.mode,
34
+ });
35
+ try {
36
+ await fs.rename(tempPath, input.filePath);
37
+ }
38
+ catch (error) {
39
+ await fs.rm(tempPath, { force: true }).catch(() => undefined);
40
+ throw error;
41
+ }
42
+ }
43
+ function buildAtomicTempPath(filePath) {
44
+ const directory = path.dirname(filePath);
45
+ const fileName = path.basename(filePath);
46
+ return path.join(directory, `.${fileName}.spatch-${process.pid}-${randomUUID()}.tmp`);
47
+ }
48
+ function buildStaleApplyError(filePath, operationName) {
49
+ return new Error(`File changed during ${operationName}: ${filePath}. Re-run spatch to avoid overwriting concurrent edits.`);
50
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,3 @@
1
- export { patchProject, spatch, } from "./spatch.ts";
2
- export type { SpatchFileResult, SpatchOccurrence, SpatchOptions, SpatchResult, } from "./types.ts";
3
- export { DEFAULT_EXCLUDED_DIRECTORIES, DEFAULT_PATCHABLE_EXTENSIONS, } from "./types.ts";
4
- export { formatPatchOutput, patchCommand, runPatchCommand, } from "./command.ts";
5
- export type { PatchCommandFlags } from "./command.ts";
1
+ export { patchProject } from "./spatch.ts";
2
+ export type { SpatchFileResult, SpatchOccurrence, SpatchOptions, SpatchResult } from "./types.ts";
3
+ export { DEFAULT_EXCLUDED_DIRECTORIES, DEFAULT_PATCHABLE_EXTENSIONS } from "./types.ts";
package/dist/index.js CHANGED
@@ -1,3 +1,2 @@
1
- export { patchProject, spatch, } from "./spatch.js";
2
- export { DEFAULT_EXCLUDED_DIRECTORIES, DEFAULT_PATCHABLE_EXTENSIONS, } from "./types.js";
3
- export { formatPatchOutput, patchCommand, runPatchCommand, } from "./command.js";
1
+ export { patchProject } from "./spatch.js";
2
+ export { DEFAULT_EXCLUDED_DIRECTORIES, DEFAULT_PATCHABLE_EXTENSIONS } from "./types.js";
@@ -0,0 +1 @@
1
+ export { patchCommand } from "../command.ts";
@@ -0,0 +1 @@
1
+ export { patchCommand } from "../command.js";
@@ -1,9 +1,10 @@
1
1
  import type { SpatchResult } from "../types.ts";
2
2
  import type { ParsedPatchSpec } from "./parse.ts";
3
3
  import type { RewritePhaseResult } from "./rewrite.ts";
4
- export type OutputPhaseInput = {
4
+ type OutputPhaseInput = {
5
5
  patch: ParsedPatchSpec;
6
6
  rewrite: RewritePhaseResult;
7
7
  elapsedMs: number;
8
8
  };
9
9
  export declare function buildSpatchResult(input: OutputPhaseInput): SpatchResult;
10
+ export {};
@@ -3,9 +3,9 @@ export type ParsedPatchSpec = {
3
3
  pattern: string;
4
4
  replacement: string;
5
5
  };
6
- export type ParsedPatchInvocation = {
6
+ type ParsedPatchInvocation = {
7
7
  patch: ParsedPatchSpec;
8
8
  options: SpatchOptions;
9
9
  };
10
- export declare function parsePatchSpec(patchDocument: string): ParsedPatchSpec;
11
10
  export declare function parsePatchInvocation(patchInput: string, options?: SpatchOptions): Promise<ParsedPatchInvocation>;
11
+ export {};
@@ -1,6 +1,6 @@
1
- import { resolveTextInput } from "@claudiu-ceia/astkit-core";
2
- import { parsePatchDocument } from "../patch-document.js";
3
- export function parsePatchSpec(patchDocument) {
1
+ import { parseTextInvocation } from "@claudiu-ceia/astkit-core";
2
+ import { parsePatchDocument } from "./patch-document.js";
3
+ function parsePatchSpec(patchDocument) {
4
4
  const parsed = parsePatchDocument(patchDocument);
5
5
  return {
6
6
  pattern: parsed.pattern,
@@ -8,9 +8,9 @@ export function parsePatchSpec(patchDocument) {
8
8
  };
9
9
  }
10
10
  export async function parsePatchInvocation(patchInput, options = {}) {
11
- const patchDocument = await resolveTextInput(patchInput, options);
11
+ const invocation = await parseTextInvocation(patchInput, options, parsePatchSpec);
12
12
  return {
13
- patch: parsePatchSpec(patchDocument),
14
- options,
13
+ patch: invocation.spec,
14
+ options: invocation.options,
15
15
  };
16
16
  }
@@ -0,0 +1,6 @@
1
+ type ParsedPatchDocument = {
2
+ pattern: string;
3
+ replacement: string;
4
+ };
5
+ export declare function parsePatchDocument(source: string): ParsedPatchDocument;
6
+ export {};
@@ -8,10 +8,7 @@ const additionLineParser = map(seq(str("+"), lineContentParser), ([, content]) =
8
8
  const deletionLineParser = map(seq(str("-"), lineContentParser), ([, content]) => ({ kind: "deletion", value: content }));
9
9
  const contextLineParser = map(lineContentParser, (content) => ({ kind: "context", value: content }));
10
10
  const patchLineParser = any(escapedMarkerLineParser, additionLineParser, deletionLineParser, contextLineParser);
11
- const patchDocumentParser = map(seq(patchLineParser, many(map(seq(str("\n"), patchLineParser), ([, line]) => line)), optional(str("\n")), eof()), ([firstLine, remainingLines, trailingNewline]) => ({
12
- lines: [firstLine, ...remainingLines],
13
- trailingNewline: trailingNewline !== null,
14
- }));
11
+ const patchDocumentParser = map(seq(patchLineParser, many(map(seq(str("\n"), patchLineParser), ([, line]) => line)), optional(str("\n")), eof()), ([firstLine, remainingLines]) => [firstLine, ...remainingLines]);
15
12
  export function parsePatchDocument(source) {
16
13
  if (source.length === 0) {
17
14
  throw new Error("Patch document cannot be empty.");
@@ -23,21 +20,19 @@ export function parsePatchDocument(source) {
23
20
  throw new Error(`Invalid patch document: ${formatErrorCompact(parsed)}`);
24
21
  }
25
22
  const lines = trailingNewline &&
26
- parsed.value.lines.length > 0 &&
27
- parsed.value.lines[parsed.value.lines.length - 1]?.kind === "context" &&
28
- parsed.value.lines[parsed.value.lines.length - 1]?.value === ""
29
- ? parsed.value.lines.slice(0, -1)
30
- : parsed.value.lines;
23
+ parsed.value.length > 0 &&
24
+ parsed.value[parsed.value.length - 1]?.kind === "context" &&
25
+ parsed.value[parsed.value.length - 1]?.value === ""
26
+ ? parsed.value.slice(0, -1)
27
+ : parsed.value;
31
28
  const patternLines = [];
32
29
  const replacementLines = [];
33
30
  let additions = 0;
34
31
  let deletions = 0;
35
- let contextLines = 0;
36
32
  for (const line of lines) {
37
33
  if (line.kind === "context") {
38
34
  patternLines.push(line.value);
39
35
  replacementLines.push(line.value);
40
- contextLines += 1;
41
36
  continue;
42
37
  }
43
38
  if (line.kind === "addition") {
@@ -56,9 +51,5 @@ export function parsePatchDocument(source) {
56
51
  return {
57
52
  pattern: trailingNewline ? `${pattern}\n` : pattern,
58
53
  replacement: trailingNewline ? `${replacement}\n` : replacement,
59
- additions,
60
- deletions,
61
- contextLines,
62
- trailingNewline,
63
54
  };
64
55
  }