mirror of
https://bitbucket.org/siakitem/my-pi.git
synced 2026-10-26 10:45:22 +00:00
feat(pi-worktree): 新增斜杠命令 git worktree 管理扩展
提供 /wt(别名 /worktree)一键拆分/合并: - /wt new 建 worktree 后经 SessionManager.forkFrom + switchSession 迁移会话 cwd,历史上下文保留;合并/切换同理 - /wt merge 先确认再 rebase 到来源分支、默认 --ff-only(可选 --squash),来源改动用 stash 兜底,冲突可中止或保留后用 /wt merge --continue / --abort 处理,永不 push - 来源 checkout 的 .pi/worktree.json 声明环境复刻(copy/link/env/ postCreate),确认框先展示计划再执行;postCreate 仅在项目已信任 时以 argv 形式 execFile,失败可 /wt reopen 增量重跑 - 登记文件 <agentDir>/pi-worktree/state.json 原子写入并加锁 串行化;会话切换后向模型注入 cwd 变更提示;未被 .gitignore 覆盖的软链/环境文件写入 .git/info/exclude 防止误提交 - 仅注册用户显式触发的斜杠命令,不暴露模型可调用工具 Generated with [Devin](https://devin.ai) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
co-authored by
Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
parent
f237025cf9
commit
97c2e75dfe
@@ -0,0 +1,147 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { CURRENT_SESSION_VERSION, SessionManager } from "@earendil-works/pi-coding-agent";
|
||||
import { existsSync } from "node:fs";
|
||||
import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { prepareSessionForCwd, switchToCwd } from "../src/session.ts";
|
||||
|
||||
interface FakeContext {
|
||||
sessionManager: { getSessionFile(): string | undefined };
|
||||
}
|
||||
|
||||
function fakeCtx(getSessionFile: () => string | undefined): FakeContext {
|
||||
return { sessionManager: { getSessionFile } };
|
||||
}
|
||||
|
||||
test("prepareSessionForCwd forks an existing session file into the target cwd", async (t) => {
|
||||
const agentDir = await mkdtemp(join(tmpdir(), "pi-worktree-agent-"));
|
||||
const previous = process.env.PI_CODING_AGENT_DIR;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
t.after(() => {
|
||||
if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previous;
|
||||
});
|
||||
|
||||
const root = await mkdtemp(join(tmpdir(), "pi-worktree-session-"));
|
||||
const sourceCwd = join(root, "source");
|
||||
const targetCwd = join(root, "target");
|
||||
await mkdir(sourceCwd, { recursive: true });
|
||||
await mkdir(targetCwd, { recursive: true });
|
||||
|
||||
// Hand-written session file: header plus one user message entry.
|
||||
const sessionDir = join(root, "sessions");
|
||||
await mkdir(sessionDir, { recursive: true });
|
||||
const file = join(sessionDir, "source-session.jsonl");
|
||||
const header = {
|
||||
type: "session",
|
||||
version: CURRENT_SESSION_VERSION,
|
||||
id: "11111111-1111-4111-8111-111111111111",
|
||||
timestamp: new Date().toISOString(),
|
||||
cwd: sourceCwd,
|
||||
};
|
||||
const message = {
|
||||
type: "message",
|
||||
id: "22222222-2222-4222-8222-222222222222",
|
||||
parentId: null,
|
||||
timestamp: new Date().toISOString(),
|
||||
message: {
|
||||
role: "user",
|
||||
content: [{ type: "text", text: "hello" }],
|
||||
timestamp: Date.now(),
|
||||
},
|
||||
};
|
||||
await writeFile(file, `${JSON.stringify(header)}\n${JSON.stringify(message)}\n`, "utf8");
|
||||
|
||||
const newFile = prepareSessionForCwd(fakeCtx(() => file), targetCwd);
|
||||
assert.ok(existsSync(newFile));
|
||||
assert.notEqual(newFile, file);
|
||||
|
||||
const opened = SessionManager.open(newFile);
|
||||
assert.equal(opened.getCwd(), resolve(targetCwd));
|
||||
const entries = opened.getEntries();
|
||||
assert.ok(entries.some((e) => e.type === "message"), "forked session keeps the history");
|
||||
});
|
||||
|
||||
test("prepareSessionForCwd writes a minimal session file when none exists yet", async (t) => {
|
||||
const agentDir = await mkdtemp(join(tmpdir(), "pi-worktree-agent-"));
|
||||
const previous = process.env.PI_CODING_AGENT_DIR;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
t.after(() => {
|
||||
if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previous;
|
||||
});
|
||||
|
||||
const targetCwd = await mkdtemp(join(tmpdir(), "pi-worktree-target-"));
|
||||
const newFile = prepareSessionForCwd(fakeCtx(() => undefined), targetCwd);
|
||||
assert.ok(existsSync(newFile));
|
||||
assert.equal(SessionManager.open(newFile).getCwd(), resolve(targetCwd));
|
||||
});
|
||||
|
||||
test("switchToCwd appends a cwd-change notice before running the callback", async (t) => {
|
||||
const agentDir = await mkdtemp(join(tmpdir(), "pi-worktree-agent-"));
|
||||
const previous = process.env.PI_CODING_AGENT_DIR;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
t.after(() => {
|
||||
if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previous;
|
||||
});
|
||||
|
||||
const targetCwd = await mkdtemp(join(tmpdir(), "pi-worktree-target-"));
|
||||
const order: string[] = [];
|
||||
const sent: { message: unknown; options: unknown }[] = [];
|
||||
const replacement = {
|
||||
sendMessage: async (message: unknown, options: unknown) => {
|
||||
order.push("sendMessage");
|
||||
sent.push({ message, options });
|
||||
},
|
||||
};
|
||||
const ctx = {
|
||||
cwd: "/a",
|
||||
waitForIdle: async () => undefined,
|
||||
sessionManager: { getSessionFile: () => undefined },
|
||||
switchSession: async (_file: string, opts: { withSession: (c: unknown) => Promise<void> }) => {
|
||||
await opts.withSession(replacement);
|
||||
return { cancelled: false };
|
||||
},
|
||||
};
|
||||
|
||||
const switched = await switchToCwd(ctx as never, targetCwd, async () => {
|
||||
order.push("withSession");
|
||||
});
|
||||
assert.equal(switched.cancelled, false);
|
||||
assert.deepEqual(order, ["sendMessage", "withSession"]);
|
||||
assert.equal(sent.length, 1);
|
||||
const { message, options } = sent[0] as { message: { customType?: string; content?: string; display?: boolean }; options: unknown };
|
||||
assert.equal(message.customType, "pi-worktree");
|
||||
assert.equal(message.display, true);
|
||||
assert.ok(message.content?.includes("/a"), "notice names the previous cwd");
|
||||
assert.ok(message.content?.includes(targetCwd), "notice names the new cwd");
|
||||
assert.equal(options, undefined);
|
||||
});
|
||||
|
||||
test("switchToCwd skips the notice when the cwd did not change", async (t) => {
|
||||
const agentDir = await mkdtemp(join(tmpdir(), "pi-worktree-agent-"));
|
||||
const previous = process.env.PI_CODING_AGENT_DIR;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
t.after(() => {
|
||||
if (previous === undefined) delete process.env.PI_CODING_AGENT_DIR;
|
||||
else process.env.PI_CODING_AGENT_DIR = previous;
|
||||
});
|
||||
|
||||
const targetCwd = await mkdtemp(join(tmpdir(), "pi-worktree-target-"));
|
||||
let sent = 0;
|
||||
const ctx = {
|
||||
cwd: targetCwd,
|
||||
waitForIdle: async () => undefined,
|
||||
sessionManager: { getSessionFile: () => undefined },
|
||||
switchSession: async (_file: string, opts: { withSession: (c: unknown) => Promise<void> }) => {
|
||||
await opts.withSession({ sendMessage: async () => sent++ });
|
||||
return { cancelled: false };
|
||||
},
|
||||
};
|
||||
await switchToCwd(ctx as never, targetCwd, async () => undefined);
|
||||
assert.equal(sent, 0);
|
||||
});
|
||||
Reference in New Issue
Block a user