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,147 @@
1
+ import { mock, expect, test, describe, beforeEach } from "bun:test";
2
+
3
+ const mockGit = {
4
+ status: mock(async () => ({ files: [], current: "main" })),
5
+ stash: mock(async () => {}),
6
+ checkout: mock(async () => {}),
7
+ pull: mock(async () => ({ catch: () => ({}) })),
8
+ checkoutLocalBranch: mock(async () => {}),
9
+ branchLocal: mock(async () => ({ all: ["main"], current: "main" })),
10
+ };
11
+
12
+ mock.module("simple-git", () => ({
13
+ default: mock(() => mockGit),
14
+ simpleGit: mock(() => mockGit),
15
+ }));
16
+
17
+ import * as branchService from "./branch.js";
18
+
19
+ describe("Branch Service", () => {
20
+ beforeEach(() => {
21
+ // Reset mockGit manual mocks
22
+ Object.values(mockGit).forEach(m => m.mockClear());
23
+
24
+ // Setup default behaviors
25
+ mockGit.status.mockResolvedValue({ files: [], current: "main" } as any);
26
+ mockGit.branchLocal.mockResolvedValue({ all: ["main", "master"], current: "main" } as any);
27
+ mockGit.pull.mockResolvedValue({ catch: () => ({}) } as any);
28
+ });
29
+
30
+ describe("slugify", () => {
31
+ test("should convert text to kebab-case", () => {
32
+ expect(branchService.slugify("Feature: Add tests")).toBe("feature-add-tests");
33
+ });
34
+
35
+ test("should remove special characters", () => {
36
+ expect(branchService.slugify("Fix! @bug #123")).toBe("fix-bug-123");
37
+ });
38
+
39
+ test("should limit length to 50 characters", () => {
40
+ const long = "a".repeat(100);
41
+ expect(branchService.slugify(long)).toHaveLength(50);
42
+ });
43
+
44
+ test("should trim dashes from ends", () => {
45
+ expect(branchService.slugify("-start-and-end-")).toBe("start-and-end");
46
+ });
47
+ });
48
+
49
+ describe("createTaskBranch", () => {
50
+ test("should create a branch with slugified task name", async () => {
51
+ const branchName = await branchService.createTaskBranch("New Task", "main");
52
+ expect(branchName).toBe("pickle/new-task");
53
+ expect(mockGit.checkoutLocalBranch).toHaveBeenCalledWith("pickle/new-task");
54
+ });
55
+
56
+ test("should stash if there are uncommitted changes", async () => {
57
+ mockGit.status.mockResolvedValue({ files: ["file1.ts"], current: "main" } as any);
58
+
59
+ await branchService.createTaskBranch("New Task", "main");
60
+
61
+ expect(mockGit.stash).toHaveBeenCalledWith(["push", "-m", "pickle-autostash"]);
62
+ expect(mockGit.stash).toHaveBeenCalledWith(["pop"]);
63
+ });
64
+
65
+ test("should checkout and pull base branch", async () => {
66
+ await branchService.createTaskBranch("New Task", "main");
67
+
68
+ expect(mockGit.checkout).toHaveBeenCalledWith("main");
69
+ expect(mockGit.pull).toHaveBeenCalledWith("origin", "main");
70
+ });
71
+
72
+ test("should fallback to checkout if checkoutLocalBranch fails", async () => {
73
+ mockGit.checkoutLocalBranch.mockRejectedValue(new Error("Already exists"));
74
+
75
+ await branchService.createTaskBranch("New Task", "main");
76
+
77
+ expect(mockGit.checkout).toHaveBeenCalledWith("pickle/new-task");
78
+ });
79
+ });
80
+
81
+ describe("getCurrentBranch", () => {
82
+ test("should return current branch from status", async () => {
83
+ mockGit.status.mockResolvedValue({ current: "feature-branch" } as any);
84
+ const branch = await branchService.getCurrentBranch(undefined, mockGit as any);
85
+ expect(branch).toBe("feature-branch");
86
+ });
87
+ });
88
+
89
+ describe("getDefaultBaseBranch", () => {
90
+ test("should prefer main if available", async () => {
91
+ mockGit.branchLocal.mockResolvedValue({ all: ["main", "master"], current: "main" } as any);
92
+ const base = await branchService.getDefaultBaseBranch();
93
+ expect(base).toBe("main");
94
+ });
95
+
96
+ test("should use master if main is missing", async () => {
97
+ mockGit.branchLocal.mockResolvedValue({ all: ["master", "other"], current: "master" } as any);
98
+ const base = await branchService.getDefaultBaseBranch();
99
+ expect(base).toBe("master");
100
+ });
101
+
102
+ test("should fallback to current branch if neither main nor master exists", async () => {
103
+ mockGit.branchLocal.mockResolvedValue({ all: ["dev"], current: "dev" } as any);
104
+ const base = await branchService.getDefaultBaseBranch();
105
+ expect(base).toBe("dev");
106
+ });
107
+ });
108
+
109
+ describe("hasUncommittedChanges", () => {
110
+ test("should return true if status has files", async () => {
111
+ mockGit.status.mockResolvedValue({ files: ["one"] } as any);
112
+ expect(await branchService.hasUncommittedChanges()).toBe(true);
113
+ });
114
+
115
+ test("should return false if status has no files", async () => {
116
+ mockGit.status.mockResolvedValue({ files: [] } as any);
117
+ expect(await branchService.hasUncommittedChanges()).toBe(false);
118
+ });
119
+ });
120
+
121
+ describe("getGitStatusInfo", () => {
122
+ test("should return mapped status info", async () => {
123
+ mockGit.status.mockResolvedValue({
124
+ current: "main",
125
+ ahead: 2,
126
+ behind: 1,
127
+ files: ["file.ts"]
128
+ } as any);
129
+
130
+ const info = await branchService.getGitStatusInfo();
131
+ expect(info).toEqual({
132
+ branch: "main",
133
+ ahead: 2,
134
+ behind: 1,
135
+ modified: 1,
136
+ isClean: false
137
+ });
138
+ });
139
+
140
+ test("should return default info on error", async () => {
141
+ mockGit.status.mockRejectedValue(new Error("Git error"));
142
+ const info = await branchService.getGitStatusInfo();
143
+ expect(info.branch).toBe("unknown");
144
+ expect(info.isClean).toBe(true);
145
+ });
146
+ });
147
+ });
@@ -0,0 +1,128 @@
1
+ import simpleGit, { type SimpleGit } from "simple-git";
2
+ import type { GitStatusInfo } from "../../types/tasks.js";
3
+
4
+ /**
5
+ * Slugify text for branch names
6
+ */
7
+ export function slugify(text: string): string {
8
+ return text
9
+ .toLowerCase()
10
+ .replace(/[^a-z0-9]+/g, "-")
11
+ .replace(/^-|-$/g, "")
12
+ .slice(0, 50);
13
+ }
14
+
15
+ /**
16
+ * Create a task branch
17
+ */
18
+ export async function createTaskBranch(
19
+ task: string,
20
+ baseBranch: string,
21
+ workDir = process.cwd(),
22
+ ): Promise<string> {
23
+ const git: SimpleGit = simpleGit(workDir);
24
+ const branchName = `pickle/${slugify(task)}`;
25
+
26
+ // Stash any changes
27
+ let stashed = false;
28
+ const status = await git.status();
29
+ if (status.files.length > 0) {
30
+ await git.stash(["push", "-m", "pickle-autostash"]);
31
+ stashed = true;
32
+ }
33
+
34
+ try {
35
+ // Checkout base branch and pull
36
+ await git.checkout(baseBranch);
37
+ await git.pull("origin", baseBranch).catch(() => {
38
+ // Ignore pull errors
39
+ });
40
+
41
+ // Create new branch (or checkout if exists)
42
+ try {
43
+ await git.checkoutLocalBranch(branchName);
44
+ } catch {
45
+ await git.checkout(branchName);
46
+ }
47
+ } finally {
48
+ // Pop stash if we stashed
49
+ if (stashed) {
50
+ await git.stash(["pop"]).catch(() => {
51
+ // Ignore stash pop errors
52
+ });
53
+ }
54
+ }
55
+
56
+ return branchName;
57
+ }
58
+
59
+ /**
60
+ * Return to the base branch
61
+ */
62
+ export async function returnToBaseBranch(
63
+ baseBranch: string,
64
+ workDir = process.cwd(),
65
+ ): Promise<void> {
66
+ const git: SimpleGit = simpleGit(workDir);
67
+ await git.checkout(baseBranch).catch(() => {
68
+ // Ignore checkout errors
69
+ });
70
+ }
71
+
72
+ /**
73
+ * Get the current branch name
74
+ */
75
+ export async function getCurrentBranch(workDir = process.cwd(), gitInstance?: SimpleGit): Promise<string> {
76
+ const git: SimpleGit = gitInstance || simpleGit(workDir);
77
+ const status = await git.status();
78
+ return status.current || "";
79
+ }
80
+ /**
81
+ * Get the default base branch (main or master)
82
+ */
83
+ export async function getDefaultBaseBranch(workDir = process.cwd()): Promise<string> {
84
+ const git: SimpleGit = simpleGit(workDir);
85
+
86
+ // Try main first, then master
87
+ const branches = await git.branchLocal();
88
+ if (branches.all.includes("main")) return "main";
89
+ if (branches.all.includes("master")) return "master";
90
+
91
+ // Fall back to current branch
92
+ return branches.current;
93
+ }
94
+
95
+ /**
96
+ * Check if there are uncommitted changes
97
+ */
98
+ export async function hasUncommittedChanges(workDir = process.cwd()): Promise<boolean> {
99
+ const git: SimpleGit = simpleGit(workDir);
100
+ const status = await git.status();
101
+ return status.files.length > 0;
102
+ }
103
+
104
+ /**
105
+ * Get comprehensive git status info for display
106
+ */
107
+ export async function getGitStatusInfo(workDir = process.cwd()): Promise<GitStatusInfo> {
108
+ const git: SimpleGit = simpleGit(workDir);
109
+
110
+ try {
111
+ const status = await git.status();
112
+ return {
113
+ branch: status.current || "unknown",
114
+ ahead: status.ahead || 0,
115
+ behind: status.behind || 0,
116
+ modified: status.files.length,
117
+ isClean: status.files.length === 0,
118
+ };
119
+ } catch {
120
+ return {
121
+ branch: "unknown",
122
+ ahead: 0,
123
+ behind: 0,
124
+ modified: 0,
125
+ isClean: true,
126
+ };
127
+ }
128
+ }
@@ -0,0 +1,113 @@
1
+ import { mock, expect, test, describe, beforeEach } from "bun:test";
2
+
3
+ const mockGit = {
4
+ diffSummary: mock(async () => ({ files: [] })),
5
+ raw: mock(async () => ""),
6
+ status: mock(async () => ({ not_added: [], created: [], deleted: [] })),
7
+ diff: mock(async () => ""),
8
+ show: mock(async () => ""),
9
+ };
10
+
11
+ mock.module("simple-git", () => ({
12
+ default: mock(() => mockGit),
13
+ simpleGit: mock(() => mockGit),
14
+ }));
15
+
16
+ mock.module("node:fs/promises", () => ({
17
+ readFile: mock(async () => "line1\nline2"),
18
+ }));
19
+
20
+ import * as diffService from "./diff.js";
21
+
22
+ describe("Diff Service", () => {
23
+ beforeEach(() => {
24
+ Object.values(mockGit).forEach(m => m.mockClear());
25
+ });
26
+
27
+ describe("getChangedFiles", () => {
28
+ test("should combine committed and uncommitted changes", async () => {
29
+ mockGit.diffSummary.mockResolvedValueOnce({
30
+ files: [{ file: "file1.ts", insertions: 10, deletions: 5 }]
31
+ } as any);
32
+ mockGit.raw.mockResolvedValueOnce("A\tfile1.ts");
33
+
34
+ const files = await diffService.getChangedFiles("/wt", "main", mockGit as any);
35
+ expect(files).toHaveLength(1);
36
+ expect(files[0].path).toBe("file1.ts");
37
+ expect(files[0].status).toBe("added");
38
+ });
39
+
40
+ test("should handle renamed files in committed diff", async () => {
41
+ mockGit.diffSummary.mockResolvedValueOnce({
42
+ files: [{ file: "old.ts => new.ts" }]
43
+ } as any);
44
+
45
+ const files = await diffService.getChangedFiles("/wt", "main", mockGit as any);
46
+ expect(files[0].status).toBe("renamed");
47
+ expect(files[0].oldPath).toBe("old.ts");
48
+ expect(files[0].path).toBe("new.ts");
49
+ });
50
+
51
+ test("should handle complex renamed files (with braces)", async () => {
52
+ mockGit.diffSummary.mockResolvedValueOnce({
53
+ files: [{ file: "src/{old.ts => new.ts}" }]
54
+ } as any);
55
+
56
+ const files = await diffService.getChangedFiles("/wt", "main", mockGit as any);
57
+ expect(files[0].status).toBe("renamed");
58
+ expect(files[0].oldPath).toBe("src/old.ts");
59
+ expect(files[0].path).toBe("src/new.ts");
60
+ });
61
+
62
+ test("should include untracked and deleted files from status", async () => {
63
+ mockGit.status.mockResolvedValueOnce({
64
+ not_added: ["untracked.ts"],
65
+ created: [],
66
+ deleted: ["gone.ts"]
67
+ } as any);
68
+
69
+ const files = await diffService.getChangedFiles("/wt", "main", mockGit as any);
70
+ expect(files.find(f => f.path === "untracked.ts")?.status).toBe("added");
71
+ expect(files.find(f => f.path === "gone.ts")?.status).toBe("deleted");
72
+ });
73
+ });
74
+
75
+ describe("getFileDiff", () => {
76
+ test("should return diff for existing file", async () => {
77
+ mockGit.diff.mockResolvedValueOnce("some diff");
78
+ const diff = await diffService.getFileDiff("/wt", "main", "file.ts", "modified", mockGit as any);
79
+ expect(diff).toBe("some diff");
80
+ });
81
+
82
+ test("should create synthetic diff for new files", async () => {
83
+ mockGit.diff.mockResolvedValueOnce("");
84
+ mockGit.show.mockRejectedValueOnce(new Error("No such file in base"));
85
+
86
+ const diff = await diffService.getFileDiff("/wt", "main", "new.ts", "added", mockGit as any);
87
+ expect(diff).toContain("new file mode 100644");
88
+ expect(diff).toContain("+line1");
89
+ });
90
+ });
91
+
92
+ describe("getFileType", () => {
93
+ test("should return correct language for extensions", () => {
94
+ expect(diffService.getFileType("test.ts")).toBe("typescript");
95
+ expect(diffService.getFileType("app.jsx")).toBe("javascript");
96
+ expect(diffService.getFileType("README.md")).toBe("markdown");
97
+ });
98
+
99
+ test("should return text for unknown extensions", () => {
100
+ expect(diffService.getFileType("config.unknown")).toBe("text");
101
+ expect(diffService.getFileType("no-ext")).toBe("text");
102
+ });
103
+ });
104
+
105
+ describe("getStatusIndicator", () => {
106
+ test("should return correct character", () => {
107
+ expect(diffService.getStatusIndicator("added")).toBe("A");
108
+ expect(diffService.getStatusIndicator("modified")).toBe("M");
109
+ expect(diffService.getStatusIndicator("deleted")).toBe("D");
110
+ expect(diffService.getStatusIndicator("renamed")).toBe("R");
111
+ });
112
+ });
113
+ });