pi-thread-engine 0.4.6 → 0.4.9
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/PLAN.md +25 -28
- package/README.md +221 -214
- package/_lib/contract.ts +116 -0
- package/docs/HELLO_PYTHON.md +68 -0
- package/extensions/index.ts +332 -74
- package/package.json +15 -6
- package/src/core/executor.ts +284 -220
- package/src/core/registry.test.ts +32 -0
- package/src/core/registry.ts +290 -290
- package/src/core/types.ts +107 -107
- package/src/core/worktree.ts +309 -0
- package/src/dashboard.ts +426 -421
- package/src/hello-world.test.ts +30 -0
- package/src/hello-world.ts +25 -0
- package/src/worktree-lifecycle.test.ts +124 -0
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { helloWorld, DEFAULT_GREETING } from "./hello-world.js";
|
|
3
|
+
|
|
4
|
+
describe("helloWorld", () => {
|
|
5
|
+
it("returns 'Hello, World!' when called with no arguments", () => {
|
|
6
|
+
expect(helloWorld()).toBe("Hello, World!");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("returns 'Hello, World!' when called with undefined", () => {
|
|
10
|
+
expect(helloWorld(undefined)).toBe("Hello, World!");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("returns 'Hello, Alice!' when called with 'Alice'", () => {
|
|
14
|
+
expect(helloWorld("Alice")).toBe("Hello, Alice!");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("trims whitespace from the name", () => {
|
|
18
|
+
expect(helloWorld(" Charlie ")).toBe("Hello, Charlie!");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("returns 'Hello, World!' for empty string", () => {
|
|
22
|
+
expect(helloWorld("")).toBe("Hello, World!");
|
|
23
|
+
});
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
describe("DEFAULT_GREETING", () => {
|
|
27
|
+
it("is defined and equals 'Hello, World!'", () => {
|
|
28
|
+
expect(DEFAULT_GREETING).toBe("Hello, World!");
|
|
29
|
+
});
|
|
30
|
+
});
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default greeting string.
|
|
3
|
+
*/
|
|
4
|
+
export const DEFAULT_GREETING = "Hello, World!";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Returns a greeting string.
|
|
8
|
+
*
|
|
9
|
+
* @param name - Optional name to include in the greeting.
|
|
10
|
+
* `undefined`, empty string, or whitespace-only values
|
|
11
|
+
* produce the default greeting.
|
|
12
|
+
* @returns The greeting string.
|
|
13
|
+
*/
|
|
14
|
+
export function helloWorld(name?: string): string {
|
|
15
|
+
if (name === undefined || name === null) {
|
|
16
|
+
return DEFAULT_GREETING;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const trimmed = name.trim();
|
|
20
|
+
if (trimmed === "") {
|
|
21
|
+
return DEFAULT_GREETING;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return `Hello, ${trimmed}!`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { mkdtempSync, mkdirSync, rmSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { tmpdir } from "os";
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { ThreadExecutor } from "./core/executor.js";
|
|
6
|
+
|
|
7
|
+
const worktreeMock = vi.hoisted(() => ({
|
|
8
|
+
createWorktree: vi.fn(),
|
|
9
|
+
findRepoRoot: vi.fn(() => "/repo"),
|
|
10
|
+
removeWorktree: vi.fn(() => true),
|
|
11
|
+
listWorktrees: vi.fn(() => []),
|
|
12
|
+
cleanupAll: vi.fn(() => ({ removed: 0, failed: 0 })),
|
|
13
|
+
pushWorktreeChanges: vi.fn(() => true),
|
|
14
|
+
isGitRepo: vi.fn(() => true),
|
|
15
|
+
}));
|
|
16
|
+
|
|
17
|
+
vi.mock("./core/worktree.js", () => worktreeMock);
|
|
18
|
+
vi.mock("../extensions/index.js", async () => {
|
|
19
|
+
const actual = await vi.importActual("../extensions/index.js");
|
|
20
|
+
return actual;
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const tempDirs: string[] = [];
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
for (const dir of tempDirs.splice(0)) rmSync(dir, { recursive: true, force: true });
|
|
26
|
+
vi.clearAllMocks();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("worktree lifecycle", () => {
|
|
30
|
+
it("keeps worktree after exec so user can explicitly push/discard", async () => {
|
|
31
|
+
const dir = mkdtempSync(join(tmpdir(), "pi-threads-"));
|
|
32
|
+
mkdirSync(join(dir, ".git"), { recursive: true });
|
|
33
|
+
tempDirs.push(dir);
|
|
34
|
+
|
|
35
|
+
worktreeMock.createWorktree.mockReturnValue({
|
|
36
|
+
path: dir,
|
|
37
|
+
branch: "pi-thread/t-1-abc",
|
|
38
|
+
threadId: "t-1",
|
|
39
|
+
createdAt: 0,
|
|
40
|
+
ahead: 0,
|
|
41
|
+
behind: 0,
|
|
42
|
+
dirty: false,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const pi = {
|
|
46
|
+
exec: vi.fn().mockResolvedValue({ code: 0, stdout: "ok", stderr: "" }),
|
|
47
|
+
sendUserMessage: vi.fn(),
|
|
48
|
+
} as any;
|
|
49
|
+
const registry = {
|
|
50
|
+
startThread: vi.fn(),
|
|
51
|
+
failTask: vi.fn(),
|
|
52
|
+
completeTask: vi.fn(),
|
|
53
|
+
startTask: vi.fn(),
|
|
54
|
+
get: vi.fn(),
|
|
55
|
+
kill: vi.fn(),
|
|
56
|
+
} as any;
|
|
57
|
+
|
|
58
|
+
const executor = new ThreadExecutor(pi, registry);
|
|
59
|
+
const thread = {
|
|
60
|
+
id: "t-1",
|
|
61
|
+
createdAt: 0,
|
|
62
|
+
config: { cwd: dir, backend: "native" },
|
|
63
|
+
tasks: [{ id: "t-1.1", prompt: "hi", state: "pending" }],
|
|
64
|
+
} as any;
|
|
65
|
+
|
|
66
|
+
await (executor as any).execWorktree(thread);
|
|
67
|
+
|
|
68
|
+
expect(worktreeMock.createWorktree).toHaveBeenCalledWith(dir, "t-1");
|
|
69
|
+
expect(worktreeMock.removeWorktree).not.toHaveBeenCalled();
|
|
70
|
+
expect(registry.completeTask).toHaveBeenCalledWith("t-1", "t-1.1", "ok");
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
it("does not create worktree tasks for lifecycle subcommands missing an id", async () => {
|
|
76
|
+
const { default: registerExtension } = await import("../extensions/index.js");
|
|
77
|
+
const handlers: Record<string, any> = {};
|
|
78
|
+
const notifications: string[] = [];
|
|
79
|
+
const pi = {
|
|
80
|
+
on: vi.fn(),
|
|
81
|
+
registerCommand: vi.fn((name: string, config: any) => { handlers[name] = config.handler; }),
|
|
82
|
+
registerTool: vi.fn(),
|
|
83
|
+
registerShortcut: vi.fn(),
|
|
84
|
+
sendUserMessage: vi.fn(),
|
|
85
|
+
} as any;
|
|
86
|
+
registerExtension(pi);
|
|
87
|
+
|
|
88
|
+
const ctx = {
|
|
89
|
+
cwd: "/repo",
|
|
90
|
+
ui: { notify: vi.fn((message: string) => notifications.push(message)), custom: vi.fn(), confirm: vi.fn() },
|
|
91
|
+
sessionManager: { getEntries: () => [] },
|
|
92
|
+
} as any;
|
|
93
|
+
|
|
94
|
+
await handlers.wthread("push", ctx);
|
|
95
|
+
await handlers.wthread("discard", ctx);
|
|
96
|
+
|
|
97
|
+
expect(notifications).toContain("Usage: /wthread push <id> [message]");
|
|
98
|
+
expect(notifications).toContain("Usage: /wthread discard <id>");
|
|
99
|
+
expect(worktreeMock.createWorktree).not.toHaveBeenCalled();
|
|
100
|
+
expect(worktreeMock.pushWorktreeChanges).not.toHaveBeenCalled();
|
|
101
|
+
expect(worktreeMock.removeWorktree).not.toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("removes worktrees only on explicit discard", async () => {
|
|
105
|
+
const { default: registerExtension } = await import("../extensions/index.js");
|
|
106
|
+
const handlers: Record<string, any> = {};
|
|
107
|
+
const pi = {
|
|
108
|
+
on: vi.fn(),
|
|
109
|
+
registerCommand: vi.fn((name: string, config: any) => { handlers[name] = config.handler; }),
|
|
110
|
+
registerTool: vi.fn(),
|
|
111
|
+
registerShortcut: vi.fn(),
|
|
112
|
+
sendUserMessage: vi.fn(),
|
|
113
|
+
} as any;
|
|
114
|
+
registerExtension(pi);
|
|
115
|
+
|
|
116
|
+
await handlers.wthread("discard t-1", {
|
|
117
|
+
cwd: "/repo",
|
|
118
|
+
ui: { notify: vi.fn(), custom: vi.fn(), confirm: vi.fn() },
|
|
119
|
+
sessionManager: { getEntries: () => [] },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
expect(worktreeMock.removeWorktree).toHaveBeenCalledWith("/repo", "t-1");
|
|
123
|
+
});
|
|
124
|
+
});
|