Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { evaluate } from "./commands/evaluate.js";
import { list } from "./commands/list.js";
import { run } from "./commands/run.js";
import { stats } from "./commands/stats.js";
import { undo } from "./commands/undo.js";
import { loadConfig } from "./utils/config.js";
import { resolvePrompt } from "./utils/prompt.js";

Expand Down Expand Up @@ -113,6 +114,13 @@ program
});
});

program
.command("undo")
.description("Reverse the last applied diff (from `thinktank apply`)")
.action(async () => {
await undo();
});

program
.command("compare <agentA> <agentB>")
.description("Compare two agents' results side by side")
Expand Down
8 changes: 7 additions & 1 deletion src/commands/apply.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { execFile } from "node:child_process";
import { readFile } from "node:fs/promises";
import { mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import pc from "picocolors";
Expand Down Expand Up @@ -104,6 +104,12 @@ export async function apply(opts: ApplyOptions): Promise<void> {
child.child.stdin?.write(agent.diff);
child.child.stdin?.end();
await child;

// Save patch for undo
const patchDir = join(repoRoot, ".thinktank");
await mkdir(patchDir, { recursive: true });
await writeFile(join(patchDir, "last-applied.patch"), agent.diff, "utf-8");

console.log(" Changes applied successfully.");
} catch (err: unknown) {
const e = err as { stderr?: string };
Expand Down
85 changes: 85 additions & 0 deletions src/commands/undo.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { describe, it } from "node:test";
import { promisify } from "node:util";

const exec = promisify(execFile);

describe("undo", () => {
it("is exported and callable", async () => {
const { undo } = await import("./undo.js");
assert.equal(typeof undo, "function");
});
});

describe("undo patch file lifecycle", () => {
it("apply saves patch and undo reverses it in a temp repo", async () => {
// Set up a temporary git repo
const { mkdtemp } = await import("node:fs/promises");
const { tmpdir } = await import("node:os");
const tempDir = await mkdtemp(join(tmpdir(), "thinktank-undo-test-"));

try {
await exec("git", ["init", tempDir]);
await exec("git", ["config", "user.email", "test@test.com"], { cwd: tempDir });
await exec("git", ["config", "user.name", "Test"], { cwd: tempDir });

// Create initial file and commit
const filePath = join(tempDir, "hello.txt");
await writeFile(filePath, "hello\n", "utf-8");
await exec("git", ["add", "."], { cwd: tempDir });
await exec("git", ["commit", "-m", "initial"], { cwd: tempDir });

// Create a patch that adds a line
const patch = [
"diff --git a/hello.txt b/hello.txt",
"--- a/hello.txt",
"+++ b/hello.txt",
"@@ -1 +1,2 @@",
" hello",
"+world",
"",
].join("\n");

// Apply the patch via git
const applyChild = exec("git", ["apply", "-"], { cwd: tempDir });
applyChild.child.stdin?.write(patch);
applyChild.child.stdin?.end();
await applyChild;

// Save the patch (simulating what apply.ts does)
const patchDir = join(tempDir, ".thinktank");
await mkdir(patchDir, { recursive: true });
const patchPath = join(patchDir, "last-applied.patch");
await writeFile(patchPath, patch, "utf-8");

// Verify the file was changed
const afterApply = (await readFile(filePath, "utf-8")).replace(/\r\n/g, "\n");
assert.equal(afterApply, "hello\nworld\n");

// Reverse the patch
const savedPatch = await readFile(patchPath, "utf-8");
const reverseChild = exec("git", ["apply", "--reverse", "-"], { cwd: tempDir });
reverseChild.child.stdin?.write(savedPatch);
reverseChild.child.stdin?.end();
await reverseChild;

// Verify the file is back to original
const afterUndo = (await readFile(filePath, "utf-8")).replace(/\r\n/g, "\n");
assert.equal(afterUndo, "hello\n");

// Clean up patch
await rm(patchPath);
} finally {
await rm(tempDir, { recursive: true, force: true });
}
});

it("errors when no patch file exists", async () => {
const { readFile: rf } = await import("node:fs/promises");
const fakePath = join("/tmp", "nonexistent-thinktank-dir", "last-applied.patch");
await assert.rejects(rf(fakePath, "utf-8"), "Should throw when patch file does not exist");
});
});
46 changes: 46 additions & 0 deletions src/commands/undo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { execFile } from "node:child_process";
import { readFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import pc from "picocolors";
import { getRepoRoot } from "../utils/git.js";

const exec = promisify(execFile);

export async function undo(): Promise<void> {
const repoRoot = await getRepoRoot();
const patchPath = join(repoRoot, ".thinktank", "last-applied.patch");

let patch: string;
try {
patch = await readFile(patchPath, "utf-8");
} catch {
console.error(pc.red(" No applied patch to undo."));
console.error();
console.error(" Run `thinktank apply` first to apply an agent's changes.");
process.exit(1);
}

console.log(" Reversing last applied patch...");

try {
const child = exec("git", ["apply", "--reverse", "-"], { cwd: repoRoot });
child.child.stdin?.write(patch);
child.child.stdin?.end();
await child;
} catch (err: unknown) {
const e = err as { stderr?: string };
console.error(pc.red(" Failed to reverse the patch."));
if (e.stderr) console.error(` ${e.stderr}`);
console.error(" The patch file is at: .thinktank/last-applied.patch");
console.error(" You may need to reverse changes manually.");
process.exit(1);
}

await rm(patchPath).catch(() => {});

console.log(pc.green(" Undo complete — last applied patch has been reversed."));
console.log();
console.log(" Review with: git diff");
console.log();
}
Loading