@howaboua/pi-codex-conversion 1.0.16 → 1.0.17

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,6 +1,6 @@
1
1
  {
2
2
  "name": "@howaboua/pi-codex-conversion",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "description": "Codex-oriented tool and prompt adapter for pi coding agent",
5
5
  "type": "module",
6
6
  "repository": {
package/src/patch/core.ts CHANGED
@@ -3,7 +3,7 @@ import { dirname } from "node:path";
3
3
  import { linesMatch } from "./matching.ts";
4
4
  import { parsePatchActions, parseUpdateFile } from "./parser.ts";
5
5
  import { openFileAtPath, pathExists, removeFileAtPath, resolvePatchPath, writeFileAtPath } from "./paths.ts";
6
- import { DiffError, ExecutePatchError, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
6
+ import { DiffError, ExecutePatchError, type ExecutePatchFailure, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
7
7
 
8
8
  export const patchFsOps = {
9
9
  mkdirSync: fs.mkdirSync,
@@ -218,6 +218,14 @@ function applyAction({
218
218
  return fuzz;
219
219
  }
220
220
 
221
+ function getActionPaths(action: ParsedPatchAction): string[] {
222
+ return [action.path, action.type === "update" ? action.movePath : undefined].filter((path): path is string => typeof path === "string");
223
+ }
224
+
225
+ function getCanonicalActionPaths({ cwd, action }: { cwd: string; action: ParsedPatchAction }): string[] {
226
+ return getActionPaths(action).map((path) => resolvePatchPath({ cwd, patchPath: path }));
227
+ }
228
+
221
229
  export function executePatch({ cwd, patchText }: { cwd: string; patchText: string }): ExecutePatchResult {
222
230
  if (!patchText.startsWith("*** Begin Patch")) {
223
231
  throw new DiffError("Patch must start with '*** Begin Patch'");
@@ -228,9 +236,22 @@ export function executePatch({ cwd, patchText }: { cwd: string; patchText: strin
228
236
  const createdFiles = new Set<string>();
229
237
  const deletedFiles = new Set<string>();
230
238
  const movedFiles = new Set<string>();
239
+ const blockedPaths = new Set<string>();
240
+ const failures: ExecutePatchFailure[] = [];
231
241
  let fuzz = 0;
232
242
 
233
243
  for (const action of actions) {
244
+ const actionPaths = getActionPaths(action);
245
+ const canonicalActionPaths = getCanonicalActionPaths({ cwd, action });
246
+ const overlappingPaths = canonicalActionPaths.filter((path) => blockedPaths.has(path));
247
+ if (overlappingPaths.length > 0) {
248
+ failures.push({
249
+ action,
250
+ message: `Skipped because an earlier failed action affected ${actionPaths.filter((_, index) => overlappingPaths.includes(canonicalActionPaths[index])).join(", ")}`,
251
+ });
252
+ continue;
253
+ }
254
+
234
255
  try {
235
256
  fuzz += applyAction({
236
257
  cwd,
@@ -242,20 +263,31 @@ export function executePatch({ cwd, patchText }: { cwd: string; patchText: strin
242
263
  });
243
264
  } catch (error) {
244
265
  const message = error instanceof Error ? error.message : String(error);
245
- throw new ExecutePatchError(
246
- message,
247
- buildExecutePatchResult({
248
- changedFiles,
249
- createdFiles,
250
- deletedFiles,
251
- movedFiles,
252
- fuzz,
253
- }),
254
- action,
255
- );
266
+ for (const path of canonicalActionPaths) {
267
+ blockedPaths.add(path);
268
+ }
269
+ failures.push({ action, message });
256
270
  }
257
271
  }
258
272
 
273
+ if (failures.length > 0) {
274
+ const message =
275
+ failures.length === 1
276
+ ? failures[0].message
277
+ : failures.map(({ action, message: failureMessage }) => `${action.path}: ${failureMessage}`).join("\n");
278
+ throw new ExecutePatchError(
279
+ message,
280
+ buildExecutePatchResult({
281
+ changedFiles,
282
+ createdFiles,
283
+ deletedFiles,
284
+ movedFiles,
285
+ fuzz,
286
+ }),
287
+ failures,
288
+ );
289
+ }
290
+
259
291
  return buildExecutePatchResult({
260
292
  changedFiles,
261
293
  createdFiles,
@@ -36,6 +36,11 @@ export interface ExecutePatchResult {
36
36
  fuzz: number;
37
37
  }
38
38
 
39
+ export interface ExecutePatchFailure {
40
+ action: ParsedPatchAction;
41
+ message: string;
42
+ }
43
+
39
44
  export class DiffError extends Error {
40
45
  constructor(message: string) {
41
46
  super(message);
@@ -46,12 +51,14 @@ export class DiffError extends Error {
46
51
  export class ExecutePatchError extends DiffError {
47
52
  result: ExecutePatchResult;
48
53
  failedAction?: ParsedPatchAction;
54
+ failures: ExecutePatchFailure[];
49
55
 
50
- constructor(message: string, result: ExecutePatchResult, failedAction?: ParsedPatchAction) {
56
+ constructor(message: string, result: ExecutePatchResult, failures: ExecutePatchFailure[] = []) {
51
57
  super(message);
52
58
  this.name = "ExecutePatchError";
53
59
  this.result = result;
54
- this.failedAction = failedAction;
60
+ this.failures = failures;
61
+ this.failedAction = failures[0]?.action;
55
62
  }
56
63
 
57
64
  hasPartialSuccess(): boolean {
@@ -17,7 +17,7 @@ interface ApplyPatchRenderState {
17
17
  collapsed: string;
18
18
  expanded: string;
19
19
  status: "pending" | "partial_failure" | "failed";
20
- failedTarget?: string;
20
+ failedTargets?: string[];
21
21
  }
22
22
 
23
23
  interface ApplyPatchSuccessDetails {
@@ -29,7 +29,7 @@ interface ApplyPatchPartialFailureDetails {
29
29
  status: "partial_failure";
30
30
  result: ExecutePatchResult;
31
31
  error: string;
32
- failedTarget?: string;
32
+ failedTargets?: string[];
33
33
  appliedFiles: string[];
34
34
  failedFiles: string[];
35
35
  recoveryInstructions: {
@@ -65,7 +65,7 @@ function setApplyPatchRenderState(
65
65
  patchText: string,
66
66
  cwd: string,
67
67
  status: "pending" | "partial_failure" | "failed" = "pending",
68
- failedTarget?: string,
68
+ failedTargets?: string[],
69
69
  ): void {
70
70
  const collapsed = formatApplyPatchSummary(patchText, cwd);
71
71
  const expanded = renderApplyPatchCall(patchText, cwd);
@@ -75,15 +75,15 @@ function setApplyPatchRenderState(
75
75
  collapsed,
76
76
  expanded,
77
77
  status,
78
- failedTarget,
78
+ failedTargets,
79
79
  });
80
80
  }
81
81
 
82
- function markApplyPatchPartialFailure(toolCallId: string, failedTarget?: string): void {
83
- markApplyPatchFailure(toolCallId, "partial_failure", failedTarget);
82
+ function markApplyPatchPartialFailure(toolCallId: string, failedTargets?: string[]): void {
83
+ markApplyPatchFailure(toolCallId, "partial_failure", failedTargets);
84
84
  }
85
85
 
86
- function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "failed", failedTarget?: string): void {
86
+ function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "failed", failedTargets?: string[]): void {
87
87
  const existing = applyPatchRenderStates.get(toolCallId);
88
88
  if (!existing) {
89
89
  return;
@@ -91,14 +91,14 @@ function markApplyPatchFailure(toolCallId: string, status: "partial_failure" | "
91
91
  applyPatchRenderStates.set(toolCallId, {
92
92
  ...existing,
93
93
  status,
94
- failedTarget,
94
+ failedTargets,
95
95
  });
96
96
  }
97
97
 
98
98
  function renderPartialFailureCall(
99
99
  text: string,
100
100
  theme: { fg(role: string, text: string): string },
101
- failedTarget?: string,
101
+ failedTargets?: string[],
102
102
  ): string {
103
103
  const lines = text.split("\n");
104
104
  if (lines.length === 0) {
@@ -106,12 +106,15 @@ function renderPartialFailureCall(
106
106
  }
107
107
  lines[0] = lines[0].replace(/^• (Added|Edited|Deleted)\b/, "• Edit partially failed");
108
108
  const failedLineIndexes = new Set<number>();
109
- if (failedTarget) {
109
+ if (failedTargets) {
110
110
  for (let i = 0; i < lines.length; i += 1) {
111
- const failedLine = markFailedTargetLine(lines[i], failedTarget);
112
- if (failedLine) {
113
- lines[i] = failedLine;
114
- failedLineIndexes.add(i);
111
+ for (const failedTarget of failedTargets) {
112
+ const failedLine = markFailedTargetLine(lines[i], failedTarget);
113
+ if (failedLine) {
114
+ lines[i] = failedLine;
115
+ failedLineIndexes.add(i);
116
+ break;
117
+ }
115
118
  }
116
119
  }
117
120
  }
@@ -131,7 +134,7 @@ function renderPartialFailureCall(
131
134
  function renderFailedCall(
132
135
  text: string,
133
136
  theme: { fg(role: string, text: string): string },
134
- failedTarget?: string,
137
+ failedTargets?: string[],
135
138
  ): string {
136
139
  const lines = text.split("\n");
137
140
  if (lines.length === 0) {
@@ -139,12 +142,15 @@ function renderFailedCall(
139
142
  }
140
143
  lines[0] = lines[0].replace(/^• (Added|Edited|Deleted)\b/, "• Edit failed");
141
144
  const failedLineIndexes = new Set<number>();
142
- if (failedTarget) {
145
+ if (failedTargets) {
143
146
  for (let i = 0; i < lines.length; i += 1) {
144
- const failedLine = markFailedTargetLine(lines[i], failedTarget);
145
- if (failedLine) {
146
- lines[i] = failedLine;
147
- failedLineIndexes.add(i);
147
+ for (const failedTarget of failedTargets) {
148
+ const failedLine = markFailedTargetLine(lines[i], failedTarget);
149
+ if (failedLine) {
150
+ lines[i] = failedLine;
151
+ failedLineIndexes.add(i);
152
+ break;
153
+ }
148
154
  }
149
155
  }
150
156
  }
@@ -188,13 +194,9 @@ function uniqueStrings(values: Array<string | undefined>): string[] {
188
194
  }
189
195
 
190
196
  function getFailedPaths(error: ExecutePatchError): string[] {
191
- if (!error.failedAction) {
192
- return [];
193
- }
194
- return uniqueStrings([
195
- error.failedAction.path,
196
- error.failedAction.type === "update" ? error.failedAction.movePath : undefined,
197
- ]);
197
+ return uniqueStrings(
198
+ error.failures.flatMap(({ action }) => [action.path, action.type === "update" ? action.movePath : undefined]),
199
+ );
198
200
  }
199
201
 
200
202
  function getAppliedPaths(result: ExecutePatchResult, failedFiles: string[]): string[] {
@@ -214,11 +216,10 @@ function buildPartialFailureMessage(message: string, failedFiles: string[], appl
214
216
  return lines.join("\n");
215
217
  }
216
218
 
217
- function describeFailedAction(error: ExecutePatchError, cwd: string): string | undefined {
218
- if (!error.failedAction) {
219
- return undefined;
220
- }
221
- return formatPatchTarget(error.failedAction.path, error.failedAction.type === "update" ? error.failedAction.movePath : undefined, cwd);
219
+ function describeFailedActions(error: ExecutePatchError, cwd: string): string[] {
220
+ return uniqueStrings(
221
+ error.failures.map(({ action }) => formatPatchTarget(action.path, action.type === "update" ? action.movePath : undefined, cwd)),
222
+ );
222
223
  }
223
224
 
224
225
  export type { ExecutePatchResult } from "../patch/types.ts";
@@ -253,9 +254,9 @@ const renderApplyPatchCallWithOptionalContext: any = (
253
254
  }
254
255
  const text =
255
256
  cached?.status === "partial_failure"
256
- ? renderPartialFailureCall(baseText, theme, cached.failedTarget)
257
+ ? renderPartialFailureCall(baseText, theme, cached.failedTargets)
257
258
  : cached?.status === "failed"
258
- ? renderFailedCall(baseText, theme, cached.failedTarget)
259
+ ? renderFailedCall(baseText, theme, cached.failedTargets)
259
260
  : baseText;
260
261
  return new Text(text, 0, 0);
261
262
  };
@@ -284,23 +285,24 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
284
285
  } catch (error) {
285
286
  if (error instanceof ExecutePatchError) {
286
287
  const partial = error.hasPartialSuccess();
287
- const failedTarget = describeFailedAction(error, ctx.cwd);
288
+ const failedTargets = describeFailedActions(error, ctx.cwd);
289
+ const failedTargetSummary = failedTargets.join(", ");
288
290
  const prefix = partial
289
291
  ? `apply_patch partially failed after ${summarizePatchCounts(error.result)}`
290
292
  : "apply_patch failed";
291
- const message = failedTarget ? `${prefix} while patching ${failedTarget}: ${error.message}` : `${prefix}: ${error.message}`;
293
+ const message = failedTargetSummary ? `${prefix} while patching ${failedTargetSummary}: ${error.message}` : `${prefix}: ${error.message}`;
292
294
  if (partial) {
293
295
  const failedFiles = getFailedPaths(error);
294
296
  const appliedFiles = getAppliedPaths(error.result, failedFiles);
295
297
  const recoveryMessage = buildPartialFailureMessage(message, failedFiles, appliedFiles);
296
- markApplyPatchPartialFailure(toolCallId, failedTarget);
298
+ markApplyPatchPartialFailure(toolCallId, failedTargets);
297
299
  return {
298
300
  content: [{ type: "text", text: recoveryMessage }],
299
301
  details: {
300
302
  status: "partial_failure",
301
303
  result: error.result,
302
304
  error: recoveryMessage,
303
- failedTarget,
305
+ failedTargets,
304
306
  appliedFiles,
305
307
  failedFiles,
306
308
  recoveryInstructions: {
@@ -310,7 +312,7 @@ export function registerApplyPatchTool(pi: ExtensionAPI): void {
310
312
  } satisfies ApplyPatchPartialFailureDetails,
311
313
  };
312
314
  }
313
- markApplyPatchFailure(toolCallId, "failed", failedTarget);
315
+ markApplyPatchFailure(toolCallId, "failed", failedTargets);
314
316
  throw new Error(message);
315
317
  }
316
318
  markApplyPatchFailure(toolCallId, "failed");