@opencode-ai/util 0.0.0-next-16027 → 0.0.0-next-16040

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/dist/patch.d.ts CHANGED
@@ -9,6 +9,7 @@ export declare class BoundaryError extends BoundaryError_base {
9
9
  declare const InvalidHunkError_base: Schema.Class<InvalidHunkError, Schema.TaggedStruct<"Patch.InvalidHunkError", {
10
10
  readonly line: Schema.String;
11
11
  readonly lineNumber: Schema.Number;
12
+ readonly reason: Schema.optional<Schema.String>;
12
13
  }>, import("effect/Cause").YieldableError>;
13
14
  export declare class InvalidHunkError extends InvalidHunkError_base {
14
15
  get message(): string;
package/dist/patch.js CHANGED
@@ -10,8 +10,11 @@ export class BoundaryError extends Schema.TaggedErrorClass()("Patch.BoundaryErro
10
10
  export class InvalidHunkError extends Schema.TaggedErrorClass()("Patch.InvalidHunkError", {
11
11
  line: Schema.String,
12
12
  lineNumber: Schema.Number,
13
+ reason: Schema.optional(Schema.String),
13
14
  }) {
14
15
  get message() {
16
+ if (this.reason)
17
+ return `Invalid hunk at line ${this.lineNumber}: ${this.reason}`;
15
18
  return `Invalid hunk at line ${this.lineNumber}: '${this.line}' is not a valid hunk header. Valid hunk headers: '*** Add File: {path}', '*** Delete File: {path}', '*** Update File: {path}'`;
16
19
  }
17
20
  }
@@ -30,51 +33,49 @@ export function parse(patchText) {
30
33
  while (index < end) {
31
34
  const line = lines[index];
32
35
  const header = line.trim();
33
- if (header.startsWith("*** Add File:")) {
34
- const path = header.slice("*** Add File:".length).trim();
35
- if (!path) {
36
- index++;
37
- continue;
38
- }
36
+ if (index === begin + 1 &&
37
+ header.startsWith("*** Environment ID:") &&
38
+ header.slice("*** Environment ID:".length).trim()) {
39
+ index++;
40
+ continue;
41
+ }
42
+ if (header.startsWith("*** Add File: ")) {
43
+ const path = header.slice("*** Add File: ".length).trim();
39
44
  const parsed = parseAdd(lines, index + 1, end);
45
+ if ("error" in parsed)
46
+ return Result.fail(parsed.error);
40
47
  hunks.push({ type: "add", path, contents: parsed.content });
41
48
  index = parsed.next;
42
49
  continue;
43
50
  }
44
- if (header.startsWith("*** Delete File:")) {
45
- const path = header.slice("*** Delete File:".length).trim();
46
- if (!path) {
47
- index++;
48
- continue;
49
- }
51
+ if (header.startsWith("*** Delete File: ")) {
52
+ const path = header.slice("*** Delete File: ".length).trim();
50
53
  hunks.push({ type: "delete", path });
51
54
  index++;
52
55
  continue;
53
56
  }
54
- if (header.startsWith("*** Update File:")) {
55
- const path = header.slice("*** Update File:".length).trim();
56
- if (!path) {
57
- index++;
58
- continue;
59
- }
57
+ if (header.startsWith("*** Update File: ")) {
58
+ const path = header.slice("*** Update File: ".length).trim();
60
59
  let next = index + 1;
61
60
  let movePath;
62
- if (lines[next]?.startsWith("*** Move to:")) {
63
- movePath = lines[next].slice("*** Move to:".length).trim();
61
+ while (lines[next]?.trimEnd() === "*** End of File")
62
+ next++;
63
+ const move = lines[next]?.trimEnd();
64
+ if (move === "*** Move to:" || move?.startsWith("*** Move to: ")) {
65
+ movePath = move.slice("*** Move to: ".length).trim();
66
+ if (!movePath) {
67
+ return Result.fail(new InvalidHunkError({ line: lines[next].trim(), lineNumber: next + 1 }));
68
+ }
64
69
  next++;
65
70
  }
66
- const parsed = parseUpdate(lines, next, end);
71
+ const parsed = parseUpdate(lines, next, end, path, index);
72
+ if ("error" in parsed)
73
+ return Result.fail(parsed.error);
67
74
  hunks.push({ type: "update", path, movePath, chunks: parsed.chunks });
68
75
  index = parsed.next;
69
76
  continue;
70
77
  }
71
- index++;
72
- }
73
- if (hunks.length === 0) {
74
- const invalid = lines.findIndex((line, index) => index > begin && index < end && line.trim() !== "");
75
- if (invalid !== -1) {
76
- return Result.fail(new InvalidHunkError({ line: lines[invalid].trim(), lineNumber: invalid + 1 }));
77
- }
78
+ return Result.fail(new InvalidHunkError({ line: header, lineNumber: index + 1 }));
78
79
  }
79
80
  return Result.succeed(hunks);
80
81
  }
@@ -99,46 +100,146 @@ export function joinBom(text, bom) {
99
100
  function parseAdd(lines, start, end) {
100
101
  const content = [];
101
102
  let index = start;
102
- while (index < end && !lines[index].startsWith("***")) {
103
- if (lines[index].startsWith("+"))
104
- content.push(lines[index].slice(1));
103
+ while (index < end && !isBoundary(lines[index].trim())) {
104
+ if (!lines[index].startsWith("+")) {
105
+ return { error: new InvalidHunkError({ line: lines[index].trim(), lineNumber: index + 1 }) };
106
+ }
107
+ content.push(lines[index].slice(1));
105
108
  index++;
106
109
  }
107
110
  return { content: content.join("\n"), next: index };
108
111
  }
109
- function parseUpdate(lines, start, end) {
112
+ function parseUpdate(lines, start, end, path, hunk) {
110
113
  const chunks = [];
111
114
  let index = start;
112
- while (index < end && !lines[index].startsWith("***")) {
113
- if (!lines[index].startsWith("@@")) {
115
+ let afterEndOfFile = false;
116
+ while (index < end) {
117
+ const line = lines[index];
118
+ const updateLine = line.trimEnd();
119
+ if (afterEndOfFile) {
120
+ if (updateLine === "") {
121
+ index++;
122
+ continue;
123
+ }
124
+ if (updateLine === "@@" || updateLine.startsWith("@@ "))
125
+ afterEndOfFile = false;
126
+ else if (isBoundary(updateLine))
127
+ break;
128
+ else {
129
+ return {
130
+ error: new InvalidHunkError({
131
+ line,
132
+ lineNumber: index + 1,
133
+ reason: `Expected update hunk to start with a @@ context marker, got: '${line}'`,
134
+ }),
135
+ };
136
+ }
137
+ }
138
+ if (updateLine === "*** End of File") {
139
+ const chunk = chunks.at(-1);
140
+ if (chunk && chunk.oldLines.length === 0 && chunk.newLines.length === 0) {
141
+ return {
142
+ error: new InvalidHunkError({
143
+ line: updateLine,
144
+ lineNumber: index + 1,
145
+ reason: "Update hunk does not contain any lines",
146
+ }),
147
+ };
148
+ }
149
+ if (chunk) {
150
+ chunk.endOfFile = true;
151
+ afterEndOfFile = true;
152
+ }
114
153
  index++;
115
154
  continue;
116
155
  }
117
- const changeContext = lines[index].slice(2).trim() || undefined;
118
- const oldLines = [];
119
- const newLines = [];
120
- let endOfFile = false;
121
- index++;
122
- while (index < end && !lines[index].startsWith("@@") && !lines[index].startsWith("***")) {
123
- const line = lines[index];
124
- if (line.startsWith(" ")) {
125
- oldLines.push(line.slice(1));
126
- newLines.push(line.slice(1));
156
+ if (isBoundary(updateLine))
157
+ break;
158
+ if (updateLine === "@@" || updateLine.startsWith("@@ ")) {
159
+ const previous = chunks.at(-1);
160
+ if (previous && previous.oldLines.length === 0 && previous.newLines.length === 0) {
161
+ return {
162
+ error: new InvalidHunkError({
163
+ line,
164
+ lineNumber: index + 1,
165
+ reason: `Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`,
166
+ }),
167
+ };
127
168
  }
128
- else if (line.startsWith("-"))
129
- oldLines.push(line.slice(1));
130
- else if (line.startsWith("+"))
131
- newLines.push(line.slice(1));
169
+ chunks.push({
170
+ oldLines: [],
171
+ newLines: [],
172
+ changeContext: updateLine === "@@" ? undefined : updateLine.slice("@@ ".length),
173
+ });
174
+ index++;
175
+ continue;
176
+ }
177
+ if (chunks.length === 0)
178
+ chunks.push({ oldLines: [], newLines: [] });
179
+ const chunk = chunks.at(-1);
180
+ if (line === "") {
181
+ chunk.oldLines.push("");
182
+ chunk.newLines.push("");
183
+ index++;
184
+ continue;
185
+ }
186
+ if (line.startsWith(" ")) {
187
+ chunk.oldLines.push(line.slice(1));
188
+ chunk.newLines.push(line.slice(1));
189
+ index++;
190
+ continue;
191
+ }
192
+ if (line.startsWith("-")) {
193
+ chunk.oldLines.push(line.slice(1));
132
194
  index++;
195
+ continue;
133
196
  }
134
- if (lines[index]?.trim() === "*** End of File") {
135
- endOfFile = true;
197
+ if (line.startsWith("+")) {
198
+ chunk.newLines.push(line.slice(1));
136
199
  index++;
200
+ continue;
137
201
  }
138
- chunks.push({ oldLines, newLines, changeContext, endOfFile: endOfFile || undefined });
202
+ const populated = chunk.oldLines.length > 0 || chunk.newLines.length > 0;
203
+ return {
204
+ error: new InvalidHunkError({
205
+ line,
206
+ lineNumber: index + 1,
207
+ reason: populated
208
+ ? `Expected update hunk to start with a @@ context marker, got: '${line}'`
209
+ : `Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`,
210
+ }),
211
+ };
212
+ }
213
+ if (chunks.length === 0) {
214
+ return {
215
+ error: new InvalidHunkError({
216
+ line: lines[hunk].trim(),
217
+ lineNumber: hunk + 1,
218
+ reason: `Update file hunk for path '${path}' is empty`,
219
+ }),
220
+ };
221
+ }
222
+ const last = chunks.at(-1);
223
+ if (last.oldLines.length === 0 && last.newLines.length === 0) {
224
+ const line = lines[index].trim();
225
+ return {
226
+ error: new InvalidHunkError({
227
+ line,
228
+ lineNumber: index + 1,
229
+ reason: line === "*** End Patch"
230
+ ? "Update hunk does not contain any lines"
231
+ : `Unexpected line found in update hunk: '${line}'. Every line should start with ' ' (context line), '+' (added line), or '-' (removed line)`,
232
+ }),
233
+ };
139
234
  }
140
235
  return { chunks, next: index };
141
236
  }
237
+ function isBoundary(line) {
238
+ return (line === "*** End Patch" ||
239
+ line.startsWith("*** Add File: ") ||
240
+ line.startsWith("*** Delete File: ") ||
241
+ line.startsWith("*** Update File: "));
242
+ }
142
243
  function computeReplacements(lines, path, chunks) {
143
244
  const replacements = [];
144
245
  let lineIndex = 0;
@@ -203,4 +304,4 @@ const normalize = (value) => value
203
304
  .replace(/[‐‑‒–—―−]/g, "-")
204
305
  .replace(/[\u00A0\u2002-\u200A\u202F\u205F\u3000]/g, " ");
205
306
  const splitBom = (text) => text.startsWith("\uFEFF") ? { bom: true, text: text.slice(1) } : { bom: false, text };
206
- const stripHeredoc = (input) => input.match(/^(?:cat\s+)?<<['"]?(\w+)['"]?\s*\n([\s\S]*?)\n\1\s*$/)?.[2] ?? input;
307
+ const stripHeredoc = (input) => input.match(/^(?:cat\s+)?<<(['"]?)(\w+)\1\s*\n([\s\S]*?)\n\2\s*$/)?.[3] ?? input;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode-ai/util",
4
- "version": "0.0.0-next-16027",
4
+ "version": "0.0.0-next-16040",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {