@howaboua/pi-codex-conversion 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/LICENSE +21 -0
- package/README.md +99 -0
- package/available-tools.png +0 -0
- package/package.json +62 -0
- package/src/adapter/codex-model.ts +22 -0
- package/src/adapter/tool-set.ts +7 -0
- package/src/index.ts +112 -0
- package/src/patch/core.ts +220 -0
- package/src/patch/parser.ts +422 -0
- package/src/patch/paths.ts +56 -0
- package/src/patch/types.ts +44 -0
- package/src/prompt/build-system-prompt.ts +111 -0
- package/src/shell/parse.ts +297 -0
- package/src/shell/summary.ts +62 -0
- package/src/shell/tokenize.ts +125 -0
- package/src/shell/types.ts +10 -0
- package/src/tools/apply-patch-tool.ts +84 -0
- package/src/tools/codex-rendering.ts +95 -0
- package/src/tools/exec-command-state.ts +43 -0
- package/src/tools/exec-command-tool.ts +107 -0
- package/src/tools/exec-session-manager.ts +478 -0
- package/src/tools/unified-exec-format.ts +28 -0
- package/src/tools/view-image-tool.ts +171 -0
- package/src/tools/write-stdin-tool.ts +145 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Umberto B.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# pi-codex-conversion
|
|
2
|
+
|
|
3
|
+
Codex-oriented adapter for [Pi](https://github.com/badlogic/pi-mono).
|
|
4
|
+
|
|
5
|
+
This package replaces Pi's default Codex/GPT experience with a narrower Codex-like surface while staying close to Pi's own runtime and prompt construction:
|
|
6
|
+
|
|
7
|
+
- swaps active tools to `exec_command`, `write_stdin`, `apply_patch`, and `view_image`
|
|
8
|
+
- preserves Pi's composed system prompt and applies a narrow Codex-oriented delta on top
|
|
9
|
+
- renders exec activity with Codex-style command and background-terminal labels
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
## Active tools in adapter mode
|
|
14
|
+
|
|
15
|
+
When the adapter is active, the LLM sees these tools:
|
|
16
|
+
|
|
17
|
+
- `exec_command` — shell execution with Codex-style `cmd` parameters and resumable sessions
|
|
18
|
+
- `write_stdin` — continue or poll a running exec session
|
|
19
|
+
- `apply_patch` — patch tool
|
|
20
|
+
- `view_image` — image-only wrapper around Pi's native image reading, enabled only for image-capable models
|
|
21
|
+
|
|
22
|
+
Notably:
|
|
23
|
+
|
|
24
|
+
- there is **no** dedicated `read`, `edit`, or `write` tool in adapter mode
|
|
25
|
+
- local text-file inspection should happen through `exec_command`
|
|
26
|
+
- file creation and edits should default to `apply_patch`
|
|
27
|
+
- Pi may still expose additional runtime tools such as `parallel`; the prompt is written to tolerate that instead of assuming a fixed four-tool universe
|
|
28
|
+
|
|
29
|
+
## Layout
|
|
30
|
+
|
|
31
|
+
- `src/index.ts` — extension entrypoint, model gating, tool-set swapping, prompt transformation
|
|
32
|
+
- `src/adapter/` — model detection and active-tool constants
|
|
33
|
+
- `src/tools/` — Pi tool wrappers, exec session management, and execution rendering
|
|
34
|
+
- `src/shell/` — shell tokenization, parsing, and exploration summaries
|
|
35
|
+
- `src/patch/` — patch parsing, path policy, and execution
|
|
36
|
+
- `src/prompt/` — Codex delta transformer over Pi's composed prompt
|
|
37
|
+
- `tests/` — deterministic unit tests
|
|
38
|
+
|
|
39
|
+
## Checks
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
npm run typecheck
|
|
43
|
+
npm test
|
|
44
|
+
npm run check
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Examples
|
|
48
|
+
|
|
49
|
+
- `rg -n foo src` -> `Explored / Search foo in src`
|
|
50
|
+
- `rg --files src | head -n 50` -> `Explored / List src`
|
|
51
|
+
- `cat README.md` -> `Explored / Read README.md`
|
|
52
|
+
- `exec_command({ cmd: "npm test", yield_time_ms: 1000 })` may return `session_id`, then continue with `write_stdin`
|
|
53
|
+
- `write_stdin({ session_id, chars: "" })` renders like `Waited for background terminal`
|
|
54
|
+
- `write_stdin({ session_id, chars: "y\\n" })` renders like `Interacted with background terminal`
|
|
55
|
+
- `view_image({ path: "/absolute/path/to/screenshot.png" })` is available on image-capable models
|
|
56
|
+
|
|
57
|
+
Raw command output is still available by expanding the tool result.
|
|
58
|
+
|
|
59
|
+
## Install
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pi install npm:@howaboua/pi-codex-conversion
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Local development:
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
pi install ./pi-codex-conversion
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Alternative Git install:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pi install git:github.com/IgorWarzocha/pi-codex-conversion
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## Prompt behavior
|
|
78
|
+
|
|
79
|
+
The adapter does not build a standalone replacement prompt anymore. Instead it:
|
|
80
|
+
|
|
81
|
+
- keeps Pi's tool descriptions, Pi docs section, AGENTS/project context, skills inventory, and date/cwd when Pi already surfaced them
|
|
82
|
+
- rewrites the top-level role framing to Codex-style wording
|
|
83
|
+
- adds a small Codex delta to the existing `Guidelines` section
|
|
84
|
+
|
|
85
|
+
That keeps the prompt much closer to `pi-mono` while still steering the model toward Codex-style tool use.
|
|
86
|
+
|
|
87
|
+
## Notes
|
|
88
|
+
|
|
89
|
+
- Adapter mode activates automatically for OpenAI `gpt*` and `codex*` models.
|
|
90
|
+
- When you switch away from those models, Pi restores the previous active tool set.
|
|
91
|
+
- `view_image` resolves paths against the active session cwd and only exposes `detail: "original"` for Codex-family image-capable models.
|
|
92
|
+
- `apply_patch` paths stay restricted to the current working directory.
|
|
93
|
+
- `exec_command` / `write_stdin` use a custom PTY-backed session manager via `node-pty` for interactive sessions.
|
|
94
|
+
- PTY output handling applies basic terminal rewrite semantics (`\r`, `\b`, erase-in-line, and common escape cleanup) so interactive redraws replay sensibly.
|
|
95
|
+
- Skills inventory is reintroduced in a Codex-style section when Pi's composed prompt already exposed the underlying Pi skills inventory.
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT
|
|
Binary file
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@howaboua/pi-codex-conversion",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Codex-oriented tool and prompt adapter for pi coding agent",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/IgorWarzocha/pi-codex-conversion.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/IgorWarzocha/pi-codex-conversion#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/IgorWarzocha/pi-codex-conversion/issues"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"pi-package",
|
|
16
|
+
"pi",
|
|
17
|
+
"pi-coding-agent",
|
|
18
|
+
"extension",
|
|
19
|
+
"codex",
|
|
20
|
+
"adapter",
|
|
21
|
+
"apply-patch"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"os": [
|
|
25
|
+
"darwin",
|
|
26
|
+
"linux"
|
|
27
|
+
],
|
|
28
|
+
"pi": {
|
|
29
|
+
"extensions": [
|
|
30
|
+
"./src/index.ts"
|
|
31
|
+
]
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"src/**/*.ts",
|
|
35
|
+
"src/**/*.md",
|
|
36
|
+
"available-tools.png",
|
|
37
|
+
"README.md",
|
|
38
|
+
"LICENSE"
|
|
39
|
+
],
|
|
40
|
+
"scripts": {
|
|
41
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
42
|
+
"test": "tsx --test tests/**/*.test.ts",
|
|
43
|
+
"check": "npm run typecheck && npm run test",
|
|
44
|
+
"prepack": "npm run check",
|
|
45
|
+
"prepublishOnly": "npm run check"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"peerDependencies": {
|
|
51
|
+
"@mariozechner/pi-coding-agent": "*",
|
|
52
|
+
"@mariozechner/pi-tui": "*",
|
|
53
|
+
"@sinclair/typebox": "*"
|
|
54
|
+
},
|
|
55
|
+
"devDependencies": {
|
|
56
|
+
"tsx": "^4.20.5",
|
|
57
|
+
"typescript": "^5.9.3"
|
|
58
|
+
},
|
|
59
|
+
"dependencies": {
|
|
60
|
+
"node-pty": "^1.1.0"
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
|
|
3
|
+
export interface CodexLikeModelDescriptor {
|
|
4
|
+
provider: string;
|
|
5
|
+
api: string;
|
|
6
|
+
id: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Keep model detection intentionally conservative. The adapter replaces the
|
|
10
|
+
// system prompt and tool surface, so false positives are worse than misses.
|
|
11
|
+
export function isCodexLikeModel(model: Partial<CodexLikeModelDescriptor> | null | undefined): boolean {
|
|
12
|
+
if (!model) return false;
|
|
13
|
+
|
|
14
|
+
const provider = (model.provider ?? "").toLowerCase();
|
|
15
|
+
const api = (model.api ?? "").toLowerCase();
|
|
16
|
+
const id = (model.id ?? "").toLowerCase();
|
|
17
|
+
return provider.includes("codex") || api.includes("codex") || id.includes("codex") || (provider.includes("openai") && id.includes("gpt"));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function isCodexLikeContext(ctx: ExtensionContext): boolean {
|
|
21
|
+
return isCodexLikeModel(ctx.model);
|
|
22
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const STATUS_KEY = "codex-adapter";
|
|
2
|
+
export const STATUS_TEXT = "\u001b[38;2;0;76;255mCodex adapter\u001b[0m";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_TOOL_NAMES = ["read", "bash", "edit", "write"];
|
|
5
|
+
|
|
6
|
+
export const CORE_ADAPTER_TOOL_NAMES = ["exec_command", "write_stdin", "apply_patch"];
|
|
7
|
+
export const VIEW_IMAGE_TOOL_NAME = "view_image";
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { CORE_ADAPTER_TOOL_NAMES, DEFAULT_TOOL_NAMES, STATUS_KEY, STATUS_TEXT, VIEW_IMAGE_TOOL_NAME } from "./adapter/tool-set.ts";
|
|
3
|
+
import { registerApplyPatchTool } from "./tools/apply-patch-tool.ts";
|
|
4
|
+
import { isCodexLikeContext } from "./adapter/codex-model.ts";
|
|
5
|
+
import { createExecCommandTracker } from "./tools/exec-command-state.ts";
|
|
6
|
+
import { registerExecCommandTool } from "./tools/exec-command-tool.ts";
|
|
7
|
+
import { createExecSessionManager } from "./tools/exec-session-manager.ts";
|
|
8
|
+
import { buildCodexSystemPrompt, extractPiPromptSkills, type PromptSkill } from "./prompt/build-system-prompt.ts";
|
|
9
|
+
import { registerViewImageTool, supportsOriginalImageDetail } from "./tools/view-image-tool.ts";
|
|
10
|
+
import { registerWriteStdinTool } from "./tools/write-stdin-tool.ts";
|
|
11
|
+
|
|
12
|
+
interface AdapterState {
|
|
13
|
+
enabled: boolean;
|
|
14
|
+
previousToolNames?: string[];
|
|
15
|
+
promptSkills: PromptSkill[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function getCommandArg(args: unknown): string | undefined {
|
|
19
|
+
if (!args || typeof args !== "object" || !("cmd" in args) || typeof args.cmd !== "string") {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
return args.cmd;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export default function codexConversion(pi: ExtensionAPI) {
|
|
26
|
+
const tracker = createExecCommandTracker();
|
|
27
|
+
const state: AdapterState = { enabled: false, promptSkills: [] };
|
|
28
|
+
const sessions = createExecSessionManager();
|
|
29
|
+
|
|
30
|
+
registerApplyPatchTool(pi);
|
|
31
|
+
registerExecCommandTool(pi, tracker, sessions);
|
|
32
|
+
registerWriteStdinTool(pi, sessions);
|
|
33
|
+
|
|
34
|
+
sessions.onSessionExit((_sessionId, command) => {
|
|
35
|
+
tracker.recordCommandFinished(command);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
39
|
+
syncAdapter(pi, ctx, state);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
pi.on("model_select", async (_event, ctx) => {
|
|
43
|
+
syncAdapter(pi, ctx, state);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
pi.on("tool_execution_start", async (event) => {
|
|
47
|
+
if (event.toolName !== "exec_command") return;
|
|
48
|
+
const command = getCommandArg(event.args);
|
|
49
|
+
if (!command) return;
|
|
50
|
+
tracker.recordStart(event.toolCallId, command);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
pi.on("tool_execution_end", async (event) => {
|
|
54
|
+
if (event.toolName !== "exec_command") return;
|
|
55
|
+
tracker.recordEnd(event.toolCallId);
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
pi.on("session_shutdown", async () => {
|
|
59
|
+
sessions.shutdown();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
pi.on("before_agent_start", async (event, ctx) => {
|
|
63
|
+
if (!isCodexLikeContext(ctx)) {
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
return { systemPrompt: buildCodexSystemPrompt(event.systemPrompt, { skills: state.promptSkills }) };
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function syncAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
|
|
71
|
+
state.promptSkills = extractPiPromptSkills(ctx.getSystemPrompt());
|
|
72
|
+
|
|
73
|
+
registerViewImageTool(pi, { allowOriginalDetail: supportsOriginalImageDetail(ctx.model) });
|
|
74
|
+
|
|
75
|
+
if (isCodexLikeContext(ctx)) {
|
|
76
|
+
enableAdapter(pi, ctx, state);
|
|
77
|
+
} else {
|
|
78
|
+
disableAdapter(pi, ctx, state);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function enableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
|
|
83
|
+
const toolNames = getAdapterToolNames(ctx);
|
|
84
|
+
if (!state.enabled) {
|
|
85
|
+
// Preserve the previous active set once so switching away from Codex-like
|
|
86
|
+
// models restores the user's existing Pi tool configuration.
|
|
87
|
+
state.previousToolNames = pi.getActiveTools();
|
|
88
|
+
state.enabled = true;
|
|
89
|
+
}
|
|
90
|
+
pi.setActiveTools(toolNames);
|
|
91
|
+
setStatus(ctx, true);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function disableAdapter(pi: ExtensionAPI, ctx: ExtensionContext, state: AdapterState): void {
|
|
95
|
+
if (state.enabled) {
|
|
96
|
+
pi.setActiveTools(state.previousToolNames && state.previousToolNames.length > 0 ? state.previousToolNames : DEFAULT_TOOL_NAMES);
|
|
97
|
+
state.enabled = false;
|
|
98
|
+
}
|
|
99
|
+
setStatus(ctx, false);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setStatus(ctx: ExtensionContext, enabled: boolean): void {
|
|
103
|
+
if (!ctx.hasUI) return;
|
|
104
|
+
ctx.ui.setStatus(STATUS_KEY, enabled ? STATUS_TEXT : undefined);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function getAdapterToolNames(ctx: ExtensionContext): string[] {
|
|
108
|
+
if (Array.isArray(ctx.model?.input) && ctx.model.input.includes("image")) {
|
|
109
|
+
return [...CORE_ADAPTER_TOOL_NAMES, VIEW_IMAGE_TOOL_NAME];
|
|
110
|
+
}
|
|
111
|
+
return [...CORE_ADAPTER_TOOL_NAMES];
|
|
112
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
import { mkdirSync, unlinkSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { parsePatchActions, parseUpdateFile } from "./parser.ts";
|
|
4
|
+
import { openFileAtPath, pathExists, removeFileAtPath, resolvePatchPath, writeFileAtPath } from "./paths.ts";
|
|
5
|
+
import { DiffError, type ExecutePatchResult, type ParsedPatchAction, type ParserState, type PatchAction } from "./types.ts";
|
|
6
|
+
|
|
7
|
+
function splitFileLines(text: string): string[] {
|
|
8
|
+
const lines = text.split("\n");
|
|
9
|
+
if (lines.at(-1) === "") {
|
|
10
|
+
lines.pop();
|
|
11
|
+
}
|
|
12
|
+
return lines;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function getUpdatedFile({ text, action, path }: { text: string; action: PatchAction; path: string }): string {
|
|
16
|
+
if (action.type !== "update") {
|
|
17
|
+
throw new DiffError(`Invalid action type for update: ${action.type}`);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const origLines = splitFileLines(text);
|
|
21
|
+
const destLines: string[] = [];
|
|
22
|
+
let origIndex = 0;
|
|
23
|
+
let destIndex = 0;
|
|
24
|
+
|
|
25
|
+
for (const chunk of action.chunks) {
|
|
26
|
+
if (chunk.origIndex > origLines.length) {
|
|
27
|
+
throw new DiffError(`_get_updated_file: ${path}: chunk.orig_index ${chunk.origIndex} > len(lines) ${origLines.length}`);
|
|
28
|
+
}
|
|
29
|
+
if (origIndex > chunk.origIndex) {
|
|
30
|
+
throw new DiffError(`_get_updated_file: ${path}: orig_index ${origIndex} > chunk.orig_index ${chunk.origIndex}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
destLines.push(...origLines.slice(origIndex, chunk.origIndex));
|
|
34
|
+
const delta = chunk.origIndex - origIndex;
|
|
35
|
+
origIndex += delta;
|
|
36
|
+
destIndex += delta;
|
|
37
|
+
|
|
38
|
+
for (const line of chunk.delLines) {
|
|
39
|
+
if (origLines[origIndex] !== line) {
|
|
40
|
+
throw new DiffError(`_get_updated_file: ${path}: Expected ${line} but got ${origLines[origIndex]} at line ${origIndex + 1}`);
|
|
41
|
+
}
|
|
42
|
+
origIndex += 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (chunk.insLines.length > 0) {
|
|
46
|
+
destLines.push(...chunk.insLines);
|
|
47
|
+
destIndex += chunk.insLines.length;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
destLines.push(...origLines.slice(origIndex));
|
|
52
|
+
const tailDelta = origLines.length - origIndex;
|
|
53
|
+
origIndex += tailDelta;
|
|
54
|
+
destIndex += tailDelta;
|
|
55
|
+
|
|
56
|
+
if (origIndex !== origLines.length) {
|
|
57
|
+
throw new DiffError(`Unexpected final orig_index for ${path}`);
|
|
58
|
+
}
|
|
59
|
+
if (destIndex !== destLines.length) {
|
|
60
|
+
throw new DiffError(`Unexpected final dest_index for ${path}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (destLines.length === 0) {
|
|
64
|
+
return "";
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return `${destLines.join("\n")}\n`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveUpdateAction({ path, text, lines }: { path: string; text: string; lines: string[] }): { action: PatchAction; fuzz: number } {
|
|
71
|
+
const state: ParserState = {
|
|
72
|
+
lines,
|
|
73
|
+
index: 0,
|
|
74
|
+
fuzz: 0,
|
|
75
|
+
};
|
|
76
|
+
const action = parseUpdateFile({ state, text, path });
|
|
77
|
+
if (action.chunks.length === 0) {
|
|
78
|
+
throw new DiffError(`Invalid patch hunk on line 2: Update file hunk for path '${path}' is empty`);
|
|
79
|
+
}
|
|
80
|
+
return { action, fuzz: state.fuzz };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function applyMove({
|
|
84
|
+
cwd,
|
|
85
|
+
path,
|
|
86
|
+
movePath,
|
|
87
|
+
content,
|
|
88
|
+
changedFiles,
|
|
89
|
+
createdFiles,
|
|
90
|
+
deletedFiles,
|
|
91
|
+
movedFiles,
|
|
92
|
+
}: {
|
|
93
|
+
cwd: string;
|
|
94
|
+
path: string;
|
|
95
|
+
movePath: string;
|
|
96
|
+
content: string;
|
|
97
|
+
changedFiles: Set<string>;
|
|
98
|
+
createdFiles: Set<string>;
|
|
99
|
+
deletedFiles: Set<string>;
|
|
100
|
+
movedFiles: Set<string>;
|
|
101
|
+
}): void {
|
|
102
|
+
const fromAbsolutePath = resolvePatchPath({ cwd, patchPath: path });
|
|
103
|
+
const toAbsolutePath = resolvePatchPath({ cwd, patchPath: movePath });
|
|
104
|
+
const destinationExisted = pathExists({ cwd, path: movePath });
|
|
105
|
+
|
|
106
|
+
mkdirSync(dirname(toAbsolutePath), { recursive: true });
|
|
107
|
+
writeFileSync(toAbsolutePath, content, "utf8");
|
|
108
|
+
if (fromAbsolutePath !== toAbsolutePath) {
|
|
109
|
+
unlinkSync(fromAbsolutePath);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
changedFiles.add(path);
|
|
113
|
+
changedFiles.add(movePath);
|
|
114
|
+
movedFiles.add(`${path} -> ${movePath}`);
|
|
115
|
+
if (!destinationExisted) {
|
|
116
|
+
createdFiles.add(movePath);
|
|
117
|
+
}
|
|
118
|
+
if (fromAbsolutePath !== toAbsolutePath) {
|
|
119
|
+
deletedFiles.add(path);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function applyAction({
|
|
124
|
+
cwd,
|
|
125
|
+
action,
|
|
126
|
+
changedFiles,
|
|
127
|
+
createdFiles,
|
|
128
|
+
deletedFiles,
|
|
129
|
+
movedFiles,
|
|
130
|
+
}: {
|
|
131
|
+
cwd: string;
|
|
132
|
+
action: ParsedPatchAction;
|
|
133
|
+
changedFiles: Set<string>;
|
|
134
|
+
createdFiles: Set<string>;
|
|
135
|
+
deletedFiles: Set<string>;
|
|
136
|
+
movedFiles: Set<string>;
|
|
137
|
+
}): number {
|
|
138
|
+
if (action.type === "delete") {
|
|
139
|
+
removeFileAtPath({ cwd, path: action.path });
|
|
140
|
+
changedFiles.add(action.path);
|
|
141
|
+
deletedFiles.add(action.path);
|
|
142
|
+
return 0;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (action.type === "add") {
|
|
146
|
+
const { created } = writeFileAtPath({
|
|
147
|
+
cwd,
|
|
148
|
+
path: action.path,
|
|
149
|
+
content: action.newFile ?? "",
|
|
150
|
+
});
|
|
151
|
+
changedFiles.add(action.path);
|
|
152
|
+
if (created) {
|
|
153
|
+
createdFiles.add(action.path);
|
|
154
|
+
}
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
if (!action.lines) {
|
|
159
|
+
throw new DiffError(`Update File Error: Missing patch lines for ${action.path}`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const originalText = openFileAtPath({ cwd, path: action.path });
|
|
163
|
+
const { action: resolvedAction, fuzz } = resolveUpdateAction({
|
|
164
|
+
path: action.path,
|
|
165
|
+
text: originalText,
|
|
166
|
+
lines: action.lines,
|
|
167
|
+
});
|
|
168
|
+
resolvedAction.movePath = action.movePath;
|
|
169
|
+
const newContent = getUpdatedFile({ text: originalText, action: resolvedAction, path: action.path });
|
|
170
|
+
|
|
171
|
+
if (action.movePath) {
|
|
172
|
+
applyMove({
|
|
173
|
+
cwd,
|
|
174
|
+
path: action.path,
|
|
175
|
+
movePath: action.movePath,
|
|
176
|
+
content: newContent,
|
|
177
|
+
changedFiles,
|
|
178
|
+
createdFiles,
|
|
179
|
+
deletedFiles,
|
|
180
|
+
movedFiles,
|
|
181
|
+
});
|
|
182
|
+
return fuzz;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
writeFileAtPath({ cwd, path: action.path, content: newContent });
|
|
186
|
+
changedFiles.add(action.path);
|
|
187
|
+
return fuzz;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function executePatch({ cwd, patchText }: { cwd: string; patchText: string }): ExecutePatchResult {
|
|
191
|
+
if (!patchText.startsWith("*** Begin Patch")) {
|
|
192
|
+
throw new DiffError("Patch must start with '*** Begin Patch'");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const actions = parsePatchActions({ text: patchText });
|
|
196
|
+
const changedFiles = new Set<string>();
|
|
197
|
+
const createdFiles = new Set<string>();
|
|
198
|
+
const deletedFiles = new Set<string>();
|
|
199
|
+
const movedFiles = new Set<string>();
|
|
200
|
+
let fuzz = 0;
|
|
201
|
+
|
|
202
|
+
for (const action of actions) {
|
|
203
|
+
fuzz += applyAction({
|
|
204
|
+
cwd,
|
|
205
|
+
action,
|
|
206
|
+
changedFiles,
|
|
207
|
+
createdFiles,
|
|
208
|
+
deletedFiles,
|
|
209
|
+
movedFiles,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
changedFiles: [...changedFiles],
|
|
215
|
+
createdFiles: [...createdFiles],
|
|
216
|
+
deletedFiles: [...deletedFiles],
|
|
217
|
+
movedFiles: [...movedFiles],
|
|
218
|
+
fuzz,
|
|
219
|
+
};
|
|
220
|
+
}
|