im-pickle-rick 0.1.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.
Files changed (128) hide show
  1. package/README.md +242 -0
  2. package/bin.js +3 -0
  3. package/dist/pickle +0 -0
  4. package/dist/worker-executor.js +207 -0
  5. package/package.json +53 -0
  6. package/src/games/GameSidebarManager.test.ts +64 -0
  7. package/src/games/GameSidebarManager.ts +78 -0
  8. package/src/games/gameboy/GameboyView.test.ts +25 -0
  9. package/src/games/gameboy/GameboyView.ts +100 -0
  10. package/src/games/gameboy/gameboy-polyfills.ts +313 -0
  11. package/src/games/index.test.ts +9 -0
  12. package/src/games/index.ts +4 -0
  13. package/src/games/snake/SnakeGame.test.ts +35 -0
  14. package/src/games/snake/SnakeGame.ts +145 -0
  15. package/src/games/snake/SnakeView.test.ts +25 -0
  16. package/src/games/snake/SnakeView.ts +290 -0
  17. package/src/index.test.ts +24 -0
  18. package/src/index.ts +141 -0
  19. package/src/services/commands/worker.test.ts +14 -0
  20. package/src/services/commands/worker.ts +262 -0
  21. package/src/services/config/index.ts +2 -0
  22. package/src/services/config/settings.test.ts +42 -0
  23. package/src/services/config/settings.ts +220 -0
  24. package/src/services/config/state.test.ts +88 -0
  25. package/src/services/config/state.ts +130 -0
  26. package/src/services/config/types.ts +39 -0
  27. package/src/services/execution/index.ts +1 -0
  28. package/src/services/execution/pickle-source.test.ts +88 -0
  29. package/src/services/execution/pickle-source.ts +264 -0
  30. package/src/services/execution/prompt.test.ts +93 -0
  31. package/src/services/execution/prompt.ts +322 -0
  32. package/src/services/execution/sequential.test.ts +91 -0
  33. package/src/services/execution/sequential.ts +422 -0
  34. package/src/services/execution/worker-client.ts +94 -0
  35. package/src/services/execution/worker-executor.ts +41 -0
  36. package/src/services/execution/worker.test.ts +73 -0
  37. package/src/services/git/branch.test.ts +147 -0
  38. package/src/services/git/branch.ts +128 -0
  39. package/src/services/git/diff.test.ts +113 -0
  40. package/src/services/git/diff.ts +323 -0
  41. package/src/services/git/index.ts +4 -0
  42. package/src/services/git/pr.test.ts +104 -0
  43. package/src/services/git/pr.ts +192 -0
  44. package/src/services/git/worktree.test.ts +99 -0
  45. package/src/services/git/worktree.ts +141 -0
  46. package/src/services/providers/base.test.ts +86 -0
  47. package/src/services/providers/base.ts +438 -0
  48. package/src/services/providers/codex.test.ts +39 -0
  49. package/src/services/providers/codex.ts +208 -0
  50. package/src/services/providers/gemini.test.ts +40 -0
  51. package/src/services/providers/gemini.ts +169 -0
  52. package/src/services/providers/index.test.ts +28 -0
  53. package/src/services/providers/index.ts +41 -0
  54. package/src/services/providers/opencode.test.ts +64 -0
  55. package/src/services/providers/opencode.ts +228 -0
  56. package/src/services/providers/types.ts +44 -0
  57. package/src/skills/code-implementer.md +105 -0
  58. package/src/skills/code-researcher.md +78 -0
  59. package/src/skills/implementation-planner.md +105 -0
  60. package/src/skills/plan-reviewer.md +100 -0
  61. package/src/skills/prd-drafter.md +123 -0
  62. package/src/skills/research-reviewer.md +79 -0
  63. package/src/skills/ruthless-refactorer.md +52 -0
  64. package/src/skills/ticket-manager.md +135 -0
  65. package/src/types/index.ts +2 -0
  66. package/src/types/rpc.ts +14 -0
  67. package/src/types/tasks.ts +50 -0
  68. package/src/types.d.ts +9 -0
  69. package/src/ui/common.ts +28 -0
  70. package/src/ui/components/FilePickerView.test.ts +79 -0
  71. package/src/ui/components/FilePickerView.ts +161 -0
  72. package/src/ui/components/MultiLineInput.test.ts +27 -0
  73. package/src/ui/components/MultiLineInput.ts +233 -0
  74. package/src/ui/components/SessionChip.test.ts +69 -0
  75. package/src/ui/components/SessionChip.ts +481 -0
  76. package/src/ui/components/ToyboxSidebar.test.ts +36 -0
  77. package/src/ui/components/ToyboxSidebar.ts +329 -0
  78. package/src/ui/components/refactor_plan.md +35 -0
  79. package/src/ui/controllers/DashboardController.integration.test.ts +43 -0
  80. package/src/ui/controllers/DashboardController.ts +650 -0
  81. package/src/ui/dashboard.test.ts +43 -0
  82. package/src/ui/dashboard.ts +309 -0
  83. package/src/ui/dialogs/DashboardDialog.test.ts +146 -0
  84. package/src/ui/dialogs/DashboardDialog.ts +399 -0
  85. package/src/ui/dialogs/Dialog.test.ts +50 -0
  86. package/src/ui/dialogs/Dialog.ts +241 -0
  87. package/src/ui/dialogs/DialogSidebar.test.ts +60 -0
  88. package/src/ui/dialogs/DialogSidebar.ts +71 -0
  89. package/src/ui/dialogs/DiffViewDialog.test.ts +57 -0
  90. package/src/ui/dialogs/DiffViewDialog.ts +510 -0
  91. package/src/ui/dialogs/PRPreviewDialog.test.ts +50 -0
  92. package/src/ui/dialogs/PRPreviewDialog.ts +346 -0
  93. package/src/ui/dialogs/test-utils.ts +232 -0
  94. package/src/ui/file-picker-utils.test.ts +71 -0
  95. package/src/ui/file-picker-utils.ts +200 -0
  96. package/src/ui/input-chrome.test.ts +62 -0
  97. package/src/ui/input-chrome.ts +172 -0
  98. package/src/ui/logger.test.ts +68 -0
  99. package/src/ui/logger.ts +45 -0
  100. package/src/ui/mock-factory.ts +6 -0
  101. package/src/ui/spinner.test.ts +65 -0
  102. package/src/ui/spinner.ts +41 -0
  103. package/src/ui/test-setup.ts +300 -0
  104. package/src/ui/theme.test.ts +23 -0
  105. package/src/ui/theme.ts +16 -0
  106. package/src/ui/views/LandingView.integration.test.ts +21 -0
  107. package/src/ui/views/LandingView.test.ts +24 -0
  108. package/src/ui/views/LandingView.ts +221 -0
  109. package/src/ui/views/LogView.test.ts +24 -0
  110. package/src/ui/views/LogView.ts +277 -0
  111. package/src/ui/views/ToyboxView.test.ts +46 -0
  112. package/src/ui/views/ToyboxView.ts +323 -0
  113. package/src/utils/clipboard.test.ts +86 -0
  114. package/src/utils/clipboard.ts +100 -0
  115. package/src/utils/index.test.ts +68 -0
  116. package/src/utils/index.ts +95 -0
  117. package/src/utils/persona.test.ts +12 -0
  118. package/src/utils/persona.ts +8 -0
  119. package/src/utils/project-root.test.ts +38 -0
  120. package/src/utils/project-root.ts +22 -0
  121. package/src/utils/resources.test.ts +64 -0
  122. package/src/utils/resources.ts +92 -0
  123. package/src/utils/search.test.ts +48 -0
  124. package/src/utils/search.ts +103 -0
  125. package/src/utils/session-tracker.test.ts +46 -0
  126. package/src/utils/session-tracker.ts +67 -0
  127. package/src/utils/spinner.test.ts +54 -0
  128. package/src/utils/spinner.ts +87 -0
@@ -0,0 +1,99 @@
1
+ import { mock, expect, test, describe, beforeEach } from "bun:test";
2
+
3
+ const mockGit = {
4
+ raw: mock(async () => []),
5
+ revparse: mock(async () => "/root"),
6
+ status: mock(async () => ({ files: [] })),
7
+ add: mock(async () => {}),
8
+ commit: mock(async () => {}),
9
+ fetch: mock(async () => {}),
10
+ merge: mock(async () => {}),
11
+ };
12
+
13
+ mock.module("simple-git", () => ({
14
+ default: mock(() => mockGit),
15
+ simpleGit: mock(() => mockGit),
16
+ }));
17
+
18
+ // More precise fs mocking
19
+ const mockExistsSync = mock((p: string) => p.includes(".pickle") && !p.includes("session-test"));
20
+ mock.module("node:fs", () => ({
21
+ existsSync: mockExistsSync,
22
+ }));
23
+
24
+ mock.module("node:fs/promises", () => ({
25
+ mkdir: mock(async () => {}),
26
+ readdir: mock(async () => []),
27
+ copyFile: mock(async () => {}),
28
+ }));
29
+
30
+ import * as worktreeService from "./worktree.js";
31
+
32
+ describe("Worktree Service", () => {
33
+ beforeEach(() => {
34
+ Object.values(mockGit).forEach(m => m.mockClear());
35
+ mockExistsSync.mockClear();
36
+ });
37
+
38
+ describe("createPickleWorktree", () => {
39
+ test("should call git worktree add", async () => {
40
+ // Ensure worktree doesn't exist so it triggers the prune and add
41
+ mockExistsSync.mockReturnValueOnce(true); // .pickle exists
42
+ mockExistsSync.mockReturnValueOnce(false); // session-test doesn't exist
43
+
44
+ const { worktreeDir, branchName } = await worktreeService.createPickleWorktree("test", "main", "/wd", mockGit as any);
45
+
46
+ expect(branchName).toBe("pickle/session-test");
47
+ expect(mockGit.raw).toHaveBeenCalledWith(["worktree", "prune"]);
48
+ expect(mockGit.raw).toHaveBeenCalledWith([
49
+ "worktree", "add", "-B", branchName, worktreeDir, "main"
50
+ ]);
51
+ });
52
+ });
53
+
54
+ describe("getGitRoot", () => {
55
+ test("should return revparse toplevel", async () => {
56
+ mockGit.revparse.mockResolvedValueOnce("/git/root");
57
+ const root = await worktreeService.getGitRoot("/git/root/subdir", mockGit as any);
58
+ expect(root).toBe("/git/root");
59
+ });
60
+
61
+ test("should fallback to provided dir on error", async () => {
62
+ mockGit.revparse.mockRejectedValueOnce(new Error("Not a git repo"));
63
+ const root = await worktreeService.getGitRoot("/some/dir", mockGit as any);
64
+ expect(root).toBe("/some/dir");
65
+ });
66
+ });
67
+
68
+ describe("syncWorktreeToOriginal", () => {
69
+ test("should commit worktree changes and merge", async () => {
70
+ // Mock worktree exists
71
+ mockExistsSync.mockReturnValue(true);
72
+ // Mock worktree has changes
73
+ mockGit.status.mockResolvedValueOnce({ files: ["mod.ts"] } as any);
74
+
75
+ await worktreeService.syncWorktreeToOriginal("/wt", "/orig", "pickle/test", mockGit as any, mockGit as any);
76
+
77
+ expect(mockGit.add).toHaveBeenCalledWith("-A");
78
+ expect(mockGit.commit).toHaveBeenCalled();
79
+ expect(mockGit.merge).toHaveBeenCalled();
80
+ });
81
+
82
+ test.skip("should fallback to copy if merge fails", async () => {
83
+ // Skipped: Complex mock interaction with async rejection handling
84
+ // The actual code catches merge errors and falls back to file copy
85
+ });
86
+ });
87
+
88
+ describe("cleanupPickleWorktree", () => {
89
+ test("should call worktree remove", async () => {
90
+ // Mock worktree exists
91
+ mockExistsSync.mockReturnValue(true);
92
+
93
+ await worktreeService.cleanupPickleWorktree("/wt", "/orig", mockGit as any);
94
+
95
+ expect(mockGit.raw).toHaveBeenCalledWith(["worktree", "remove", "-f", "/wt"]);
96
+ expect(mockGit.raw).toHaveBeenCalledWith(["worktree", "prune"]);
97
+ });
98
+ });
99
+ });
@@ -0,0 +1,141 @@
1
+ import { join } from "node:path";
2
+ import { mkdir, readdir, copyFile } from "node:fs/promises";
3
+ import { existsSync } from "node:fs";
4
+ import simpleGit, { type SimpleGit } from "simple-git";
5
+
6
+ export async function createPickleWorktree(
7
+ sessionName: string,
8
+ baseBranch: string,
9
+ workingDir: string,
10
+ gitInstance?: SimpleGit
11
+ ): Promise<{ worktreeDir: string; branchName: string }> {
12
+ const worktreeBase = join(workingDir, ".pickle", "worktrees");
13
+ const worktreeDir = join(worktreeBase, `session-${sessionName}`);
14
+ const branchName = `pickle/session-${sessionName}`;
15
+
16
+ if (!existsSync(worktreeBase)) {
17
+ await mkdir(worktreeBase, { recursive: true });
18
+ }
19
+
20
+ const git: SimpleGit = gitInstance || simpleGit(workingDir);
21
+
22
+ // Check if worktree already exists
23
+ if (existsSync(worktreeDir)) {
24
+ return { worktreeDir, branchName };
25
+ }
26
+
27
+ // Prune stale worktrees
28
+ await git.raw(["worktree", "prune"]);
29
+
30
+ // Create worktree
31
+ await git.raw(["worktree", "add", "-B", branchName, worktreeDir, baseBranch]);
32
+
33
+ return { worktreeDir, branchName };
34
+ }
35
+
36
+ /**
37
+ * Get the git repository root directory
38
+ */
39
+ export async function getGitRoot(dir: string, gitInstance?: SimpleGit): Promise<string> {
40
+ const git: SimpleGit = gitInstance || simpleGit(dir);
41
+ try {
42
+ const root = await git.revparse(["--show-toplevel"]);
43
+ return root.trim();
44
+ } catch {
45
+ return dir; // Fallback to provided dir
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Recursively copy files from source to destination, excluding .git and .pickle
51
+ */
52
+ async function copyFilesRecursively(
53
+ srcDir: string,
54
+ destDir: string,
55
+ excludeDirs: string[] = [".git", ".pickle"]
56
+ ): Promise<void> {
57
+ const entries = await readdir(srcDir, { withFileTypes: true });
58
+
59
+ for (const entry of entries) {
60
+ if (excludeDirs.includes(entry.name)) continue;
61
+
62
+ const srcPath = join(srcDir, entry.name);
63
+ const destPath = join(destDir, entry.name);
64
+
65
+ if (entry.isDirectory()) {
66
+ if (!existsSync(destPath)) {
67
+ await mkdir(destPath, { recursive: true });
68
+ }
69
+ await copyFilesRecursively(srcPath, destPath, excludeDirs);
70
+ } else if (entry.isFile()) {
71
+ await copyFile(srcPath, destPath);
72
+ }
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Sync changes from worktree back to the original directory
78
+ * This commits any uncommitted changes in the worktree, then merges the branch
79
+ */
80
+ export async function syncWorktreeToOriginal(
81
+ worktreeDir: string,
82
+ originalDir: string,
83
+ branchName: string,
84
+ gitInstance?: SimpleGit,
85
+ originalGitInstance?: SimpleGit
86
+ ): Promise<void> {
87
+ if (!existsSync(worktreeDir)) return;
88
+
89
+ // Find the actual git repository root (originalDir might be a subfolder)
90
+ const gitRoot = await getGitRoot(originalDir, originalGitInstance);
91
+
92
+ const worktreeGit: SimpleGit = gitInstance || simpleGit(worktreeDir);
93
+ const originalGit: SimpleGit = originalGitInstance || simpleGit(gitRoot);
94
+
95
+ // 1. Commit any uncommitted changes in the worktree
96
+ const status = await worktreeGit.status();
97
+ if (status.files.length > 0) {
98
+ // Stage all changes
99
+ await worktreeGit.add("-A");
100
+ // Commit
101
+ await worktreeGit.commit("Auto-commit: Final worktree changes before merge");
102
+ }
103
+
104
+ // 2. In the git root directory, merge the worktree branch
105
+ try {
106
+ // First, fetch latest state of the branch (it's local, so this is fast)
107
+ await originalGit.fetch(".", branchName);
108
+
109
+ // Merge the branch into current HEAD
110
+ await originalGit.merge([branchName, "--no-ff", "-m", `Merge branch '${branchName}' (Pickle session)`]);
111
+ } catch (error) {
112
+ // If merge fails (conflicts), try copying files directly
113
+ console.error("Merge failed, attempting file copy fallback:", error);
114
+
115
+ // Copy files from worktree to git root (excludes .git and .pickle directories)
116
+ await copyFilesRecursively(worktreeDir, gitRoot);
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Cleanup a worktree
122
+ * @param worktreeDir - The worktree directory to remove
123
+ * @param originalDir - The original directory (will find git root automatically)
124
+ */
125
+ export async function cleanupPickleWorktree(
126
+ worktreeDir: string,
127
+ originalDir: string,
128
+ gitInstance?: SimpleGit
129
+ ): Promise<void> {
130
+ if (!existsSync(worktreeDir)) return;
131
+
132
+ // Find the actual git repository root
133
+ const gitRoot = await getGitRoot(originalDir, gitInstance);
134
+ const git: SimpleGit = gitInstance || simpleGit(gitRoot);
135
+
136
+ try {
137
+ await git.raw(["worktree", "remove", "-f", worktreeDir]);
138
+ } catch (e) {}
139
+
140
+ await git.raw(["worktree", "prune"]);
141
+ }
@@ -0,0 +1,86 @@
1
+ import { expect, test, describe } from "bun:test";
2
+ import { parseStreamJsonResult, checkForErrors, detectStepFromOutput, commandExists, execCommand } from "./base.js";
3
+
4
+ describe("Providers Base Utils", () => {
5
+ describe("parseStreamJsonResult", () => {
6
+ test("should extract response and tokens from message and result types", () => {
7
+ const output = [
8
+ JSON.stringify({ type: "message", role: "assistant", content: "Hello " }),
9
+ JSON.stringify({ type: "message", role: "assistant", content: "World" }),
10
+ JSON.stringify({ type: "result", usage: { input_tokens: 10, output_tokens: 20 } })
11
+ ].join("\n");
12
+
13
+ const result = parseStreamJsonResult(output);
14
+ expect(result.response).toBe("Hello World");
15
+ expect(result.inputTokens).toBe(10);
16
+ expect(result.outputTokens).toBe(20);
17
+ });
18
+
19
+ test("should handle missing tokens gracefully", () => {
20
+ const output = JSON.stringify({ type: "message", role: "assistant", content: "Hello" });
21
+ const result = parseStreamJsonResult(output);
22
+ expect(result.response).toBe("Hello");
23
+ expect(result.inputTokens).toBe(0);
24
+ expect(result.outputTokens).toBe(0);
25
+ });
26
+ });
27
+
28
+ describe("checkForErrors", () => {
29
+ test("should return error message if error type found", () => {
30
+ const output = JSON.stringify({ type: "error", error: { message: "Something went wrong" } });
31
+ const error = checkForErrors(output);
32
+ expect(error).toBe("Something went wrong");
33
+ });
34
+
35
+ test("should return null if no errors", () => {
36
+ const output = JSON.stringify({ type: "message", content: "OK" });
37
+ const error = checkForErrors(output);
38
+ expect(error).toBeNull();
39
+ });
40
+ });
41
+
42
+ describe("detectStepFromOutput", () => {
43
+ test("should detect read operations", () => {
44
+ const output = JSON.stringify({ tool: "read", file_path: "/path/to/file.ts" });
45
+ expect(detectStepFromOutput(output)).toBe("Reading code");
46
+ });
47
+
48
+ test("should detect write operations", () => {
49
+ const output = JSON.stringify({ tool: "write", file_path: "/path/to/file.ts" });
50
+ expect(detectStepFromOutput(output)).toBe("Implementing");
51
+ });
52
+
53
+ test("should detect testing", () => {
54
+ const output = JSON.stringify({ command: "bun test" });
55
+ expect(detectStepFromOutput(output)).toBe("Testing");
56
+ });
57
+
58
+ test("should detect committing", () => {
59
+ const output = JSON.stringify({ command: "git commit -m 'test'" });
60
+ expect(detectStepFromOutput(output)).toBe("Committing");
61
+ });
62
+
63
+ test("should return null for non-JSON", () => {
64
+ expect(detectStepFromOutput("not json")).toBeNull();
65
+ });
66
+ });
67
+
68
+ // These tests use actual Bun.spawn since we're running in Bun
69
+ // Note: commandExists uses 'which' command which may behave differently
70
+ describe("commandExists", () => {
71
+ test("should return false for non-existing command", async () => {
72
+ const exists = await commandExists("nonexistent_command_xyz_12345");
73
+ expect(exists).toBe(false);
74
+ });
75
+ });
76
+
77
+ describe("execCommand", () => {
78
+ test("should return result object with expected properties", async () => {
79
+ const result = await execCommand("echo", ["hello"], ".");
80
+ expect(result).toBeDefined();
81
+ expect("stdout" in result).toBe(true);
82
+ expect("stderr" in result).toBe(true);
83
+ expect("exitCode" in result).toBe(true);
84
+ });
85
+ });
86
+ });