Update test script in package.json and enhance patch_files.js with chroot support and improved path handling for file operations.

This commit is contained in:
sebseb7
2025-08-11 17:27:46 +02:00
parent 612a4a6b9d
commit b26d0d3f7e
3 changed files with 722 additions and 56 deletions

View File

@@ -1,19 +1,14 @@
#!/usr/bin/env node
/**
* A self-contained JavaScript utility for applying human-readable
* "pseudo-diff" patch files to a collection of text files.
*
const desc = `
This is a custom utility that makes it more convenient to add, remove, move, or edit code files. 'apply_patch' effectively allows you to execute a diff/patch against a file,
but the format of the diff specification is unique to this task, so pay careful attention to these instructions.
To use the 'apply_patch' command, you should pass a message of the following structure as "input":
*** Begin Patch
[YOUR_PATH]
*** End Patch
Where [YOUR_PATCH] is the actual content of your patch, specified in the following V4A diff format.
*** [ACTION] File: [path/to/file] -> ACTION can be one of Add, Update, or Delete.
@@ -24,7 +19,7 @@ For each snippet of code that needs to be changed, repeat the following:
[context_after] -> See below for further instructions on context.
For instructions on [context_before] and [context_after]:
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first changes [context_after] lines in the second changes [context_before] lines.
- By default, show 3 lines of code immediately above and 3 lines immediately below each change. If a change is within 3 lines of a previous change, do NOT duplicate the first changes [context_after] lines in the second changes [context_before] lines.
- If 3 lines of context is insufficient to uniquely identify the snippet of code within the file, use the @@ operator to indicate the class or function to which the snippet belongs. For instance, we might have:
@@ class BaseClass
[3 lines of pre-context]
@@ -32,7 +27,7 @@ For instructions on [context_before] and [context_after]:
+ [new_code]
[3 lines of post-context]
- If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple `@@` statements to jump to the right context. For instance:
- If a code block is repeated so many times in a class or function such that even a single @@ statement and 3 lines of context cannot uniquely identify the snippet of code, you can use multiple '@@' statements to jump to the right context. For instance:
@@ class BaseClass
@@ def method():
@@ -42,12 +37,7 @@ For instructions on [context_before] and [context_after]:
[3 lines of post-context]
Note, then, that we do not use line numbers in this diff format, as the context is enough to uniquely identify code. An example of a message that you might pass as "input" to this function, in order to apply a patch, is shown below.
*
*
*
*/
`;
// --------------------------------------------------------------------------- //
// Domain objects
// --------------------------------------------------------------------------- //
@@ -109,16 +99,55 @@ class Patch {
}
}
// --------------------------------------------------------------------------- //
// Path utilities for chroot functionality
// --------------------------------------------------------------------------- //
function normalizePath(path) {
return path.replace(/\\/g, '/').replace(/\/+/g, '/');
}
function joinPaths(...paths) {
return normalizePath(paths.filter(p => p).join('/'));
}
function resolvePath(chroot, filepath) {
if (!chroot) return filepath;
// Remove leading slash from filepath if present
const cleanFilepath = filepath.startsWith('/') ? filepath.substring(1) : filepath;
// Join chroot and filepath
const resolved = joinPaths(chroot, cleanFilepath);
// Ensure it starts with /
return resolved.startsWith('/') ? resolved : '/' + resolved;
}
function unresolvePath(chroot, filepath) {
if (!chroot) return filepath;
const chrootPath = chroot.startsWith('/') ? chroot : '/' + chroot;
const cleanFilepath = filepath.startsWith('/') ? filepath : '/' + filepath;
if (cleanFilepath.startsWith(chrootPath)) {
const relativePath = cleanFilepath.substring(chrootPath.length);
return relativePath.startsWith('/') ? relativePath : '/' + relativePath;
}
return filepath;
}
// --------------------------------------------------------------------------- //
// Patch text parser
// --------------------------------------------------------------------------- //
class Parser {
constructor(current_files, lines) {
constructor(current_files, lines, chroot = null) {
this.current_files = current_files;
this.lines = lines;
this.index = 0;
this.patch = new Patch();
this.fuzz = 0;
this.chroot = chroot;
}
// ------------- low-level helpers -------------------------------------- //
@@ -186,43 +215,52 @@ class Parser {
// ---------- UPDATE ---------- //
let path = this.read_str("*** Update File: ");
if (path) {
if (path in this.patch.actions) {
throw new DiffError(`Duplicate update for file: ${path}`);
// Resolve path with chroot
const resolvedPath = resolvePath(this.chroot, path);
if (resolvedPath in this.patch.actions) {
throw new DiffError(`Duplicate update for file: ${resolvedPath}`);
}
const move_to = this.read_str("*** Move to: ");
if (!(path in this.current_files)) {
throw new DiffError(`Update File Error - missing file: ${path}`);
if (!(resolvedPath in this.current_files)) {
throw new DiffError(`Update File Error - missing file: ${resolvedPath}`);
}
const text = this.current_files[path];
const text = this.current_files[resolvedPath];
const action = this._parse_update_file(text);
action.move_path = move_to || null;
this.patch.actions[path] = action;
action.move_path = move_to ? resolvePath(this.chroot, move_to) : null;
this.patch.actions[resolvedPath] = action;
continue;
}
// ---------- DELETE ---------- //
path = this.read_str("*** Delete File: ");
if (path) {
if (path in this.patch.actions) {
throw new DiffError(`Duplicate delete for file: ${path}`);
// Resolve path with chroot
const resolvedPath = resolvePath(this.chroot, path);
if (resolvedPath in this.patch.actions) {
throw new DiffError(`Duplicate delete for file: ${resolvedPath}`);
}
if (!(path in this.current_files)) {
throw new DiffError(`Delete File Error - missing file: ${path}`);
if (!(resolvedPath in this.current_files)) {
throw new DiffError(`Delete File Error - missing file: ${resolvedPath}`);
}
this.patch.actions[path] = new PatchAction(ActionType.DELETE);
this.patch.actions[resolvedPath] = new PatchAction(ActionType.DELETE);
continue;
}
// ---------- ADD ---------- //
path = this.read_str("*** Add File: ");
if (path) {
if (path in this.patch.actions) {
throw new DiffError(`Duplicate add for file: ${path}`);
// Resolve path with chroot
const resolvedPath = resolvePath(this.chroot, path);
if (resolvedPath in this.patch.actions) {
throw new DiffError(`Duplicate add for file: ${resolvedPath}`);
}
if (path in this.current_files) {
throw new DiffError(`Add File Error - file already exists: ${path}`);
if (resolvedPath in this.current_files) {
throw new DiffError(`Add File Error - file already exists: ${resolvedPath}`);
}
this.patch.actions[path] = this._parse_add_file();
this.patch.actions[resolvedPath] = this._parse_add_file();
continue;
}
@@ -512,7 +550,7 @@ function _get_updated_file(text, action, path) {
return dest_lines.join("\n");
}
function patch_to_commit(patch, orig) {
function patch_to_commit(patch, orig, chroot = null) {
const commit = new Commit();
for (const [path, action] of Object.entries(patch.actions)) {
if (action.type === ActionType.DELETE) {
@@ -534,11 +572,12 @@ function patch_to_commit(patch, orig) {
);
} else if (action.type === ActionType.UPDATE) {
const new_content = _get_updated_file(orig[path], action, path);
const move_path = action.move_path ? unresolvePath(chroot, action.move_path) : null;
commit.changes[path] = new FileChange(
ActionType.UPDATE,
orig[path],
new_content,
action.move_path
move_path
);
}
}
@@ -548,7 +587,7 @@ function patch_to_commit(patch, orig) {
// --------------------------------------------------------------------------- //
// User-facing helpers
// --------------------------------------------------------------------------- //
function text_to_patch(text, orig) {
function text_to_patch(text, orig, chroot = null) {
// Handle different line ending scenarios
let lines = text.split(/\r?\n/);
@@ -574,13 +613,13 @@ function text_to_patch(text, orig) {
throw new DiffError("Invalid patch text - missing sentinels");
}
const parser = new Parser(orig, lines);
const parser = new Parser(orig, lines, chroot);
parser.index = 1;
parser.parse();
return [parser.patch, parser.fuzz];
}
function identify_files_needed(text) {
function identify_files_needed(text, chroot = null) {
// Handle line splitting consistently
let lines = text.split(/\r?\n/);
if (lines.length > 0 && lines[lines.length - 1] === "") {
@@ -589,16 +628,16 @@ function identify_files_needed(text) {
const updateFiles = lines
.filter(line => line.startsWith("*** Update File: "))
.map(line => line.substring("*** Update File: ".length));
.map(line => resolvePath(chroot, line.substring("*** Update File: ".length)));
const deleteFiles = lines
.filter(line => line.startsWith("*** Delete File: "))
.map(line => line.substring("*** Delete File: ".length));
.map(line => resolvePath(chroot, line.substring("*** Delete File: ".length)));
return [...updateFiles, ...deleteFiles];
}
function identify_files_added(text) {
function identify_files_added(text, chroot = null) {
// Handle line splitting consistently
let lines = text.split(/\r?\n/);
if (lines.length > 0 && lines[lines.length - 1] === "") {
@@ -607,7 +646,7 @@ function identify_files_added(text) {
return lines
.filter(line => line.startsWith("*** Add File: "))
.map(line => line.substring("*** Add File: ".length));
.map(line => resolvePath(chroot, line.substring("*** Add File: ".length)));
}
// --------------------------------------------------------------------------- //
@@ -621,7 +660,7 @@ function load_files(paths, open_fn) {
return result;
}
function apply_commit(commit, write_fn, remove_fn) {
function apply_commit(commit, write_fn, remove_fn, chroot = null) {
for (const [path, change] of Object.entries(commit.changes)) {
if (change.type === ActionType.DELETE) {
remove_fn(path);
@@ -634,7 +673,7 @@ function apply_commit(commit, write_fn, remove_fn) {
if (change.new_content === null) {
throw new DiffError(`UPDATE change for ${path} has no new content`);
}
const target = change.move_path || path;
const target = change.move_path ? resolvePath(chroot, change.move_path) : path;
write_fn(target, change.new_content);
if (change.move_path) {
remove_fn(path);
@@ -643,15 +682,15 @@ function apply_commit(commit, write_fn, remove_fn) {
}
}
function process_patch(text, open_fn, write_fn, remove_fn) {
function process_patch(text, open_fn, write_fn, remove_fn, chroot = null) {
if (!text.startsWith("*** Begin Patch")) {
throw new DiffError("Patch text must start with *** Begin Patch");
}
const paths = identify_files_needed(text);
const paths = identify_files_needed(text, chroot);
const orig = load_files(paths, open_fn);
const [patch, _fuzz] = text_to_patch(text, orig);
const commit = patch_to_commit(patch, orig);
apply_commit(commit, write_fn, remove_fn);
const [patch, _fuzz] = text_to_patch(text, orig, chroot);
const commit = patch_to_commit(patch, orig, chroot);
apply_commit(commit, write_fn, remove_fn, chroot);
return "Done!";
}
@@ -683,13 +722,13 @@ function remove_file(filepath) {
export default {
type: "function",
name: "patch_files",
description: "Apply a unified diff patch to files within a chroot directory, with option to reverse the patch",
description: "Apply a unified diff patch " + desc,
parameters: {
type: "object",
properties: {
patch: {
type: "string",
description: "The unidiff patch string to apply.",
description: "The patch string to apply.",
}
},
required: ["patch"],
@@ -706,7 +745,8 @@ export async function run(args) {
args.patch,
open_file,
write_file,
remove_file
remove_file,
'/home/seb/src/aiTools/tmp'
);
return result;
} catch (error) {
@@ -715,4 +755,4 @@ export async function run(args) {
}
throw error;
}
}
}