@ilya-lesikov/pi-pi 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 (271) hide show
  1. package/3p/pi-ask-user/index.ts +1835 -0
  2. package/3p/pi-ask-user/package.json +50 -0
  3. package/3p/pi-ask-user/single-select-layout.ts +203 -0
  4. package/3p/pi-lsp/extensions/lsp/client.ts +542 -0
  5. package/3p/pi-lsp/extensions/lsp/config.ts +135 -0
  6. package/3p/pi-lsp/extensions/lsp/effects/command.ts +42 -0
  7. package/3p/pi-lsp/extensions/lsp/effects/filesystem.ts +50 -0
  8. package/3p/pi-lsp/extensions/lsp/effects/runtime.ts +21 -0
  9. package/3p/pi-lsp/extensions/lsp/errors.ts +113 -0
  10. package/3p/pi-lsp/extensions/lsp/formatting.ts +315 -0
  11. package/3p/pi-lsp/extensions/lsp/index.ts +231 -0
  12. package/3p/pi-lsp/extensions/lsp/protocol.ts +209 -0
  13. package/3p/pi-lsp/extensions/lsp/retry.ts +39 -0
  14. package/3p/pi-lsp/extensions/lsp/tools/programs.ts +293 -0
  15. package/3p/pi-lsp/extensions/lsp/tools.ts +89 -0
  16. package/3p/pi-lsp/extensions/lsp/types.ts +243 -0
  17. package/3p/pi-lsp/package.json +54 -0
  18. package/3p/pi-plannotator/AGENTS.md +583 -0
  19. package/3p/pi-plannotator/apps/pi-extension/README.md +228 -0
  20. package/3p/pi-plannotator/apps/pi-extension/assistant-message.ts +128 -0
  21. package/3p/pi-plannotator/apps/pi-extension/config.test.ts +166 -0
  22. package/3p/pi-plannotator/apps/pi-extension/config.ts +318 -0
  23. package/3p/pi-plannotator/apps/pi-extension/current-pi-session.ts +147 -0
  24. package/3p/pi-plannotator/apps/pi-extension/index.ts +1279 -0
  25. package/3p/pi-plannotator/apps/pi-extension/package.json +56 -0
  26. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.test.ts +13 -0
  27. package/3p/pi-plannotator/apps/pi-extension/plannotator-browser.ts +616 -0
  28. package/3p/pi-plannotator/apps/pi-extension/plannotator-events.ts +342 -0
  29. package/3p/pi-plannotator/apps/pi-extension/plannotator.json +12 -0
  30. package/3p/pi-plannotator/apps/pi-extension/server/agent-jobs.ts +515 -0
  31. package/3p/pi-plannotator/apps/pi-extension/server/ai-runtime.ts +169 -0
  32. package/3p/pi-plannotator/apps/pi-extension/server/annotations.ts +85 -0
  33. package/3p/pi-plannotator/apps/pi-extension/server/external-annotations.ts +189 -0
  34. package/3p/pi-plannotator/apps/pi-extension/server/handlers.ts +210 -0
  35. package/3p/pi-plannotator/apps/pi-extension/server/helpers.ts +78 -0
  36. package/3p/pi-plannotator/apps/pi-extension/server/ide.ts +46 -0
  37. package/3p/pi-plannotator/apps/pi-extension/server/integrations.ts +195 -0
  38. package/3p/pi-plannotator/apps/pi-extension/server/network.test.ts +158 -0
  39. package/3p/pi-plannotator/apps/pi-extension/server/network.ts +268 -0
  40. package/3p/pi-plannotator/apps/pi-extension/server/pr.ts +126 -0
  41. package/3p/pi-plannotator/apps/pi-extension/server/project.ts +64 -0
  42. package/3p/pi-plannotator/apps/pi-extension/server/reference.ts +362 -0
  43. package/3p/pi-plannotator/apps/pi-extension/server/serverAnnotate.ts +199 -0
  44. package/3p/pi-plannotator/apps/pi-extension/server/serverPlan.ts +505 -0
  45. package/3p/pi-plannotator/apps/pi-extension/server/serverReview.ts +1229 -0
  46. package/3p/pi-plannotator/apps/pi-extension/server/vcs.ts +121 -0
  47. package/3p/pi-plannotator/apps/pi-extension/server.test.ts +966 -0
  48. package/3p/pi-plannotator/apps/pi-extension/server.ts +44 -0
  49. package/3p/pi-plannotator/apps/pi-extension/tool-scope.test.ts +88 -0
  50. package/3p/pi-plannotator/apps/pi-extension/tool-scope.ts +39 -0
  51. package/3p/pi-plannotator/apps/pi-extension/tsconfig.json +16 -0
  52. package/3p/pi-plannotator/apps/pi-extension/vendor.sh +48 -0
  53. package/3p/pi-plannotator/package.json +53 -0
  54. package/3p/pi-plannotator/packages/ai/ai.test.ts +1407 -0
  55. package/3p/pi-plannotator/packages/ai/base-session.ts +94 -0
  56. package/3p/pi-plannotator/packages/ai/context.ts +250 -0
  57. package/3p/pi-plannotator/packages/ai/endpoints.ts +326 -0
  58. package/3p/pi-plannotator/packages/ai/index.ts +105 -0
  59. package/3p/pi-plannotator/packages/ai/package.json +21 -0
  60. package/3p/pi-plannotator/packages/ai/provider.ts +103 -0
  61. package/3p/pi-plannotator/packages/ai/providers/claude-agent-sdk.ts +447 -0
  62. package/3p/pi-plannotator/packages/ai/providers/codex-sdk.ts +430 -0
  63. package/3p/pi-plannotator/packages/ai/providers/command-path.ts +115 -0
  64. package/3p/pi-plannotator/packages/ai/providers/opencode-sdk.ts +546 -0
  65. package/3p/pi-plannotator/packages/ai/providers/pi-events.ts +110 -0
  66. package/3p/pi-plannotator/packages/ai/providers/pi-sdk-node.ts +425 -0
  67. package/3p/pi-plannotator/packages/ai/providers/pi-sdk.ts +469 -0
  68. package/3p/pi-plannotator/packages/ai/session-manager.ts +195 -0
  69. package/3p/pi-plannotator/packages/ai/tsconfig.json +15 -0
  70. package/3p/pi-plannotator/packages/ai/types.ts +379 -0
  71. package/3p/pi-plannotator/packages/server/agent-jobs.ts +538 -0
  72. package/3p/pi-plannotator/packages/server/agent-review-message.test.ts +135 -0
  73. package/3p/pi-plannotator/packages/server/agent-review-message.ts +243 -0
  74. package/3p/pi-plannotator/packages/server/ai-runtime.ts +108 -0
  75. package/3p/pi-plannotator/packages/server/annotate.ts +401 -0
  76. package/3p/pi-plannotator/packages/server/browser.test.ts +87 -0
  77. package/3p/pi-plannotator/packages/server/browser.ts +245 -0
  78. package/3p/pi-plannotator/packages/server/claude-review.ts +352 -0
  79. package/3p/pi-plannotator/packages/server/code-nav.ts +73 -0
  80. package/3p/pi-plannotator/packages/server/codex-review-schema.json +41 -0
  81. package/3p/pi-plannotator/packages/server/codex-review.ts +330 -0
  82. package/3p/pi-plannotator/packages/server/config.ts +8 -0
  83. package/3p/pi-plannotator/packages/server/draft.ts +1 -0
  84. package/3p/pi-plannotator/packages/server/editor-annotations.ts +76 -0
  85. package/3p/pi-plannotator/packages/server/external-annotations.test.ts +18 -0
  86. package/3p/pi-plannotator/packages/server/external-annotations.ts +207 -0
  87. package/3p/pi-plannotator/packages/server/git.ts +141 -0
  88. package/3p/pi-plannotator/packages/server/goal-setup.test.ts +55 -0
  89. package/3p/pi-plannotator/packages/server/goal-setup.ts +248 -0
  90. package/3p/pi-plannotator/packages/server/ide.ts +43 -0
  91. package/3p/pi-plannotator/packages/server/image.test.ts +63 -0
  92. package/3p/pi-plannotator/packages/server/image.ts +66 -0
  93. package/3p/pi-plannotator/packages/server/index.ts +648 -0
  94. package/3p/pi-plannotator/packages/server/integrations.test.ts +172 -0
  95. package/3p/pi-plannotator/packages/server/integrations.ts +214 -0
  96. package/3p/pi-plannotator/packages/server/jj.test.ts +69 -0
  97. package/3p/pi-plannotator/packages/server/jj.ts +86 -0
  98. package/3p/pi-plannotator/packages/server/p4.ts +417 -0
  99. package/3p/pi-plannotator/packages/server/package.json +40 -0
  100. package/3p/pi-plannotator/packages/server/path-utils.ts +18 -0
  101. package/3p/pi-plannotator/packages/server/pr.ts +146 -0
  102. package/3p/pi-plannotator/packages/server/project.test.ts +115 -0
  103. package/3p/pi-plannotator/packages/server/project.ts +45 -0
  104. package/3p/pi-plannotator/packages/server/reference-handlers.ts +404 -0
  105. package/3p/pi-plannotator/packages/server/remote.test.ts +150 -0
  106. package/3p/pi-plannotator/packages/server/remote.ts +74 -0
  107. package/3p/pi-plannotator/packages/server/repo.ts +80 -0
  108. package/3p/pi-plannotator/packages/server/resolve-file.test.ts +310 -0
  109. package/3p/pi-plannotator/packages/server/review-workspace.test.ts +1032 -0
  110. package/3p/pi-plannotator/packages/server/review-workspace.ts +48 -0
  111. package/3p/pi-plannotator/packages/server/review.ts +1318 -0
  112. package/3p/pi-plannotator/packages/server/sessions.ts +111 -0
  113. package/3p/pi-plannotator/packages/server/share-url.ts +53 -0
  114. package/3p/pi-plannotator/packages/server/shared-handlers.test.ts +43 -0
  115. package/3p/pi-plannotator/packages/server/shared-handlers.ts +177 -0
  116. package/3p/pi-plannotator/packages/server/storage.test.ts +176 -0
  117. package/3p/pi-plannotator/packages/server/storage.ts +17 -0
  118. package/3p/pi-plannotator/packages/server/tour/tour-review.test.ts +146 -0
  119. package/3p/pi-plannotator/packages/server/tour/tour-review.ts +604 -0
  120. package/3p/pi-plannotator/packages/server/tsconfig.json +15 -0
  121. package/3p/pi-plannotator/packages/server/vcs.test.ts +48 -0
  122. package/3p/pi-plannotator/packages/server/vcs.ts +80 -0
  123. package/3p/pi-plannotator/packages/shared/agent-jobs.ts +132 -0
  124. package/3p/pi-plannotator/packages/shared/agents.ts +53 -0
  125. package/3p/pi-plannotator/packages/shared/annotate-args.test.ts +386 -0
  126. package/3p/pi-plannotator/packages/shared/annotate-args.ts +107 -0
  127. package/3p/pi-plannotator/packages/shared/at-reference.test.ts +99 -0
  128. package/3p/pi-plannotator/packages/shared/at-reference.ts +52 -0
  129. package/3p/pi-plannotator/packages/shared/checklist.ts +52 -0
  130. package/3p/pi-plannotator/packages/shared/code-file.test.ts +112 -0
  131. package/3p/pi-plannotator/packages/shared/code-file.ts +41 -0
  132. package/3p/pi-plannotator/packages/shared/code-nav.test.ts +515 -0
  133. package/3p/pi-plannotator/packages/shared/code-nav.ts +436 -0
  134. package/3p/pi-plannotator/packages/shared/compress.ts +51 -0
  135. package/3p/pi-plannotator/packages/shared/config.ts +262 -0
  136. package/3p/pi-plannotator/packages/shared/crypto.test.ts +172 -0
  137. package/3p/pi-plannotator/packages/shared/crypto.ts +97 -0
  138. package/3p/pi-plannotator/packages/shared/data-dir.ts +42 -0
  139. package/3p/pi-plannotator/packages/shared/diff-paths.test.ts +30 -0
  140. package/3p/pi-plannotator/packages/shared/diff-paths.ts +137 -0
  141. package/3p/pi-plannotator/packages/shared/draft.ts +64 -0
  142. package/3p/pi-plannotator/packages/shared/external-annotation.ts +397 -0
  143. package/3p/pi-plannotator/packages/shared/extract-code-paths.test.ts +59 -0
  144. package/3p/pi-plannotator/packages/shared/extract-code-paths.ts +66 -0
  145. package/3p/pi-plannotator/packages/shared/favicon.ts +5 -0
  146. package/3p/pi-plannotator/packages/shared/feedback-templates.test.ts +65 -0
  147. package/3p/pi-plannotator/packages/shared/feedback-templates.ts +29 -0
  148. package/3p/pi-plannotator/packages/shared/goal-setup.test.ts +231 -0
  149. package/3p/pi-plannotator/packages/shared/goal-setup.ts +336 -0
  150. package/3p/pi-plannotator/packages/shared/html-to-markdown.test.ts +62 -0
  151. package/3p/pi-plannotator/packages/shared/html-to-markdown.ts +32 -0
  152. package/3p/pi-plannotator/packages/shared/improvement-hooks.test.ts +135 -0
  153. package/3p/pi-plannotator/packages/shared/improvement-hooks.ts +115 -0
  154. package/3p/pi-plannotator/packages/shared/integrations-common.ts +243 -0
  155. package/3p/pi-plannotator/packages/shared/jj-core.test.ts +236 -0
  156. package/3p/pi-plannotator/packages/shared/jj-core.ts +433 -0
  157. package/3p/pi-plannotator/packages/shared/package.json +55 -0
  158. package/3p/pi-plannotator/packages/shared/pfm-reminder.test.ts +88 -0
  159. package/3p/pi-plannotator/packages/shared/pfm-reminder.ts +80 -0
  160. package/3p/pi-plannotator/packages/shared/pr-github.ts +661 -0
  161. package/3p/pi-plannotator/packages/shared/pr-gitlab.test.ts +202 -0
  162. package/3p/pi-plannotator/packages/shared/pr-gitlab.ts +620 -0
  163. package/3p/pi-plannotator/packages/shared/pr-provider.test.ts +266 -0
  164. package/3p/pi-plannotator/packages/shared/pr-provider.ts +123 -0
  165. package/3p/pi-plannotator/packages/shared/pr-stack.test.ts +104 -0
  166. package/3p/pi-plannotator/packages/shared/pr-stack.ts +194 -0
  167. package/3p/pi-plannotator/packages/shared/pr-types.ts +326 -0
  168. package/3p/pi-plannotator/packages/shared/project.ts +71 -0
  169. package/3p/pi-plannotator/packages/shared/prompts-integration.test.ts +421 -0
  170. package/3p/pi-plannotator/packages/shared/prompts.test.ts +504 -0
  171. package/3p/pi-plannotator/packages/shared/prompts.ts +247 -0
  172. package/3p/pi-plannotator/packages/shared/reference-common.ts +87 -0
  173. package/3p/pi-plannotator/packages/shared/repo.ts +71 -0
  174. package/3p/pi-plannotator/packages/shared/resolve-file.test.ts +113 -0
  175. package/3p/pi-plannotator/packages/shared/resolve-file.ts +509 -0
  176. package/3p/pi-plannotator/packages/shared/review-args.test.ts +64 -0
  177. package/3p/pi-plannotator/packages/shared/review-args.ts +85 -0
  178. package/3p/pi-plannotator/packages/shared/review-core.test.ts +286 -0
  179. package/3p/pi-plannotator/packages/shared/review-core.ts +895 -0
  180. package/3p/pi-plannotator/packages/shared/review-workspace-node.ts +230 -0
  181. package/3p/pi-plannotator/packages/shared/review-workspace.ts +436 -0
  182. package/3p/pi-plannotator/packages/shared/semantic-diff-types.ts +76 -0
  183. package/3p/pi-plannotator/packages/shared/semantic-diff.test.ts +322 -0
  184. package/3p/pi-plannotator/packages/shared/semantic-diff.ts +520 -0
  185. package/3p/pi-plannotator/packages/shared/storage.ts +378 -0
  186. package/3p/pi-plannotator/packages/shared/tour.ts +61 -0
  187. package/3p/pi-plannotator/packages/shared/tsconfig.json +15 -0
  188. package/3p/pi-plannotator/packages/shared/types.ts +29 -0
  189. package/3p/pi-plannotator/packages/shared/url-to-markdown.test.ts +177 -0
  190. package/3p/pi-plannotator/packages/shared/url-to-markdown.ts +351 -0
  191. package/3p/pi-plannotator/packages/shared/vcs-core.test.ts +332 -0
  192. package/3p/pi-plannotator/packages/shared/vcs-core.ts +482 -0
  193. package/3p/pi-plannotator/packages/shared/worktree-pool.test.ts +162 -0
  194. package/3p/pi-plannotator/packages/shared/worktree-pool.ts +103 -0
  195. package/3p/pi-plannotator/packages/shared/worktree.ts +119 -0
  196. package/3p/pi-subagents/package.json +50 -0
  197. package/3p/pi-subagents/src/agent-manager.ts +413 -0
  198. package/3p/pi-subagents/src/agent-runner.ts +502 -0
  199. package/3p/pi-subagents/src/agent-types.ts +248 -0
  200. package/3p/pi-subagents/src/context.ts +58 -0
  201. package/3p/pi-subagents/src/cross-extension-rpc.ts +135 -0
  202. package/3p/pi-subagents/src/custom-agents.ts +137 -0
  203. package/3p/pi-subagents/src/default-agents.ts +144 -0
  204. package/3p/pi-subagents/src/env.ts +33 -0
  205. package/3p/pi-subagents/src/group-join.ts +141 -0
  206. package/3p/pi-subagents/src/index.ts +1811 -0
  207. package/3p/pi-subagents/src/invocation-config.ts +40 -0
  208. package/3p/pi-subagents/src/memory.ts +165 -0
  209. package/3p/pi-subagents/src/model-resolver.ts +81 -0
  210. package/3p/pi-subagents/src/output-file.ts +77 -0
  211. package/3p/pi-subagents/src/prompts.ts +85 -0
  212. package/3p/pi-subagents/src/skill-loader.ts +79 -0
  213. package/3p/pi-subagents/src/types.ts +111 -0
  214. package/3p/pi-subagents/src/ui/agent-widget.ts +496 -0
  215. package/3p/pi-subagents/src/ui/conversation-viewer.ts +250 -0
  216. package/3p/pi-subagents/src/worktree.ts +162 -0
  217. package/3p/pi-tasks/package.json +52 -0
  218. package/3p/pi-tasks/src/auto-clear.ts +91 -0
  219. package/3p/pi-tasks/src/index.ts +1150 -0
  220. package/3p/pi-tasks/src/process-tracker.ts +140 -0
  221. package/3p/pi-tasks/src/reminder-cadence.ts +90 -0
  222. package/3p/pi-tasks/src/task-store.ts +324 -0
  223. package/3p/pi-tasks/src/tasks-config.ts +27 -0
  224. package/3p/pi-tasks/src/types.ts +40 -0
  225. package/3p/pi-tasks/src/ui/settings-menu.ts +152 -0
  226. package/3p/pi-tasks/src/ui/task-widget.ts +296 -0
  227. package/AGENTS.md +28 -0
  228. package/LICENSE +201 -0
  229. package/extensions/orchestrator/agents/brainstorm-reviewer.ts +77 -0
  230. package/extensions/orchestrator/agents/code-reviewer.ts +89 -0
  231. package/extensions/orchestrator/agents/explore.ts +33 -0
  232. package/extensions/orchestrator/agents/librarian.ts +43 -0
  233. package/extensions/orchestrator/agents/plan-reviewer.ts +75 -0
  234. package/extensions/orchestrator/agents/planner.ts +61 -0
  235. package/extensions/orchestrator/agents/registry.ts +156 -0
  236. package/extensions/orchestrator/agents/task.ts +50 -0
  237. package/extensions/orchestrator/agents/tool-routing.ts +84 -0
  238. package/extensions/orchestrator/ast-search.ts +85 -0
  239. package/extensions/orchestrator/cbm.ts +330 -0
  240. package/extensions/orchestrator/command-handlers.test.ts +163 -0
  241. package/extensions/orchestrator/command-handlers.ts +116 -0
  242. package/extensions/orchestrator/commands.test.ts +81 -0
  243. package/extensions/orchestrator/commands.ts +75 -0
  244. package/extensions/orchestrator/config.test.ts +221 -0
  245. package/extensions/orchestrator/config.ts +230 -0
  246. package/extensions/orchestrator/context.test.ts +293 -0
  247. package/extensions/orchestrator/context.ts +206 -0
  248. package/extensions/orchestrator/event-handlers.test.ts +190 -0
  249. package/extensions/orchestrator/event-handlers.ts +1416 -0
  250. package/extensions/orchestrator/exa.ts +104 -0
  251. package/extensions/orchestrator/flant-infra.ts +486 -0
  252. package/extensions/orchestrator/index.ts +80 -0
  253. package/extensions/orchestrator/integration.test.ts +1214 -0
  254. package/extensions/orchestrator/orchestrator.test.ts +252 -0
  255. package/extensions/orchestrator/orchestrator.ts +538 -0
  256. package/extensions/orchestrator/phases/brainstorm.test.ts +15 -0
  257. package/extensions/orchestrator/phases/brainstorm.ts +273 -0
  258. package/extensions/orchestrator/phases/implementation.ts +40 -0
  259. package/extensions/orchestrator/phases/machine.test.ts +293 -0
  260. package/extensions/orchestrator/phases/machine.ts +209 -0
  261. package/extensions/orchestrator/phases/planning.ts +255 -0
  262. package/extensions/orchestrator/phases/review.ts +193 -0
  263. package/extensions/orchestrator/plannotator.ts +56 -0
  264. package/extensions/orchestrator/pp-menu.ts +866 -0
  265. package/extensions/orchestrator/state.test.ts +343 -0
  266. package/extensions/orchestrator/state.ts +237 -0
  267. package/extensions/orchestrator/validate-artifacts.test.ts +88 -0
  268. package/extensions/orchestrator/validate-artifacts.ts +272 -0
  269. package/extensions/orchestrator/vendor.d.ts +26 -0
  270. package/package.json +73 -0
  271. package/scripts/postinstall.sh +18 -0
@@ -0,0 +1,266 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import {
3
+ getCliInstallUrl,
4
+ getCliName,
5
+ getDisplayRepo,
6
+ getMRLabel,
7
+ getMRNumberLabel,
8
+ getPlatformLabel,
9
+ isSameProject,
10
+ parsePRUrl,
11
+ prRefFromMetadata,
12
+ type PRMetadata,
13
+ type PRRef,
14
+ } from "./pr-types";
15
+ import {
16
+ getPRDiffScopeOptions,
17
+ getPRStackInfo,
18
+ } from "./pr-stack";
19
+
20
+ describe("pr-provider platform helpers", () => {
21
+ test("parses GitHub PR URLs including nested suffixes", () => {
22
+ const ref = parsePRUrl("https://github.com/backnotprop/plannotator/pull/364/files");
23
+
24
+ expect(ref).toEqual({
25
+ platform: "github",
26
+ host: "github.com",
27
+ owner: "backnotprop",
28
+ repo: "plannotator",
29
+ number: 364,
30
+ });
31
+ });
32
+
33
+ test("parses GitHub Enterprise PR URLs", () => {
34
+ const ref = parsePRUrl("https://ghe.company.com/org/repo/pull/99/files");
35
+
36
+ expect(ref).toEqual({
37
+ platform: "github",
38
+ host: "ghe.company.com",
39
+ owner: "org",
40
+ repo: "repo",
41
+ number: 99,
42
+ });
43
+ });
44
+
45
+ test("does not confuse GHE URL with GitLab", () => {
46
+ const ref = parsePRUrl("https://git.internal.corp/team/app/pull/5");
47
+
48
+ expect(ref).toEqual({
49
+ platform: "github",
50
+ host: "git.internal.corp",
51
+ owner: "team",
52
+ repo: "app",
53
+ number: 5,
54
+ });
55
+ });
56
+
57
+ test("parses GitLab.com MR URLs", () => {
58
+ const ref = parsePRUrl("https://gitlab.com/group/project/-/merge_requests/42/diffs");
59
+
60
+ expect(ref).toEqual({
61
+ platform: "gitlab",
62
+ host: "gitlab.com",
63
+ projectPath: "group/project",
64
+ iid: 42,
65
+ });
66
+ });
67
+
68
+ test("parses self-hosted GitLab MR URLs with nested groups", () => {
69
+ const ref = parsePRUrl("https://gitlab.example.com/group/subgroup/project/-/merge_requests/7");
70
+
71
+ expect(ref).toEqual({
72
+ platform: "gitlab",
73
+ host: "gitlab.example.com",
74
+ projectPath: "group/subgroup/project",
75
+ iid: 7,
76
+ });
77
+ });
78
+
79
+ test("returns null for unsupported URLs", () => {
80
+ expect(parsePRUrl("https://example.com/not-a-pr/123")).toBeNull();
81
+ expect(parsePRUrl("")).toBeNull();
82
+ });
83
+
84
+ test("formats platform-aware labels for GitHub and GitLab", () => {
85
+ const githubMeta: PRMetadata = {
86
+ platform: "github",
87
+ host: "github.com",
88
+ owner: "backnotprop",
89
+ repo: "plannotator",
90
+ number: 364,
91
+ title: "GitHub PR",
92
+ author: "backnotprop",
93
+ baseBranch: "main",
94
+ headBranch: "feature/github",
95
+ baseSha: "base",
96
+ headSha: "head",
97
+ url: "https://github.com/backnotprop/plannotator/pull/364",
98
+ };
99
+
100
+ const gitlabMeta: PRMetadata = {
101
+ platform: "gitlab",
102
+ host: "gitlab.example.com",
103
+ projectPath: "group/project",
104
+ iid: 42,
105
+ title: "GitLab MR",
106
+ author: "alice",
107
+ baseBranch: "main",
108
+ headBranch: "feature/gitlab",
109
+ baseSha: "base",
110
+ headSha: "head",
111
+ url: "https://gitlab.example.com/group/project/-/merge_requests/42",
112
+ };
113
+
114
+ expect(getPlatformLabel(githubMeta)).toBe("GitHub");
115
+ expect(getMRLabel(githubMeta)).toBe("PR");
116
+ expect(getMRNumberLabel(githubMeta)).toBe("#364");
117
+ expect(getDisplayRepo(githubMeta)).toBe("backnotprop/plannotator");
118
+
119
+ expect(getPlatformLabel(gitlabMeta)).toBe("GitLab");
120
+ expect(getMRLabel(gitlabMeta)).toBe("MR");
121
+ expect(getMRNumberLabel(gitlabMeta)).toBe("!42");
122
+ expect(getDisplayRepo(gitlabMeta)).toBe("group/project");
123
+ });
124
+
125
+ test("reconstructs refs and CLI metadata for each platform", () => {
126
+ const githubMeta: PRMetadata = {
127
+ platform: "github",
128
+ host: "github.com",
129
+ owner: "backnotprop",
130
+ repo: "plannotator",
131
+ number: 1,
132
+ title: "GitHub PR",
133
+ author: "backnotprop",
134
+ baseBranch: "main",
135
+ headBranch: "feature/github",
136
+ baseSha: "base",
137
+ headSha: "head",
138
+ url: "https://github.com/backnotprop/plannotator/pull/1",
139
+ };
140
+
141
+ const gitlabMeta: PRMetadata = {
142
+ platform: "gitlab",
143
+ host: "gitlab.example.com",
144
+ projectPath: "group/project",
145
+ iid: 2,
146
+ title: "GitLab MR",
147
+ author: "alice",
148
+ baseBranch: "main",
149
+ headBranch: "feature/gitlab",
150
+ baseSha: "base",
151
+ headSha: "head",
152
+ url: "https://gitlab.example.com/group/project/-/merge_requests/2",
153
+ };
154
+
155
+ const githubRef = prRefFromMetadata(githubMeta);
156
+ const gitlabRef = prRefFromMetadata(gitlabMeta);
157
+
158
+ expect(githubRef).toEqual({
159
+ platform: "github",
160
+ host: "github.com",
161
+ owner: "backnotprop",
162
+ repo: "plannotator",
163
+ number: 1,
164
+ });
165
+ expect(gitlabRef).toEqual({
166
+ platform: "gitlab",
167
+ host: "gitlab.example.com",
168
+ projectPath: "group/project",
169
+ iid: 2,
170
+ });
171
+
172
+ expect(getCliName(githubRef)).toBe("gh");
173
+ expect(getCliInstallUrl(githubRef)).toBe("https://cli.github.com");
174
+ expect(getCliName(gitlabRef)).toBe("glab");
175
+ expect(getCliInstallUrl(gitlabRef)).toBe("https://gitlab.com/gitlab-org/cli");
176
+ });
177
+ });
178
+
179
+ describe("PR stack helpers", () => {
180
+ const stackedMeta: PRMetadata = {
181
+ platform: "github",
182
+ host: "github.com",
183
+ owner: "backnotprop",
184
+ repo: "plannotator-stack-fixture",
185
+ number: 3,
186
+ title: "Validate user id",
187
+ author: "backnotprop",
188
+ baseBranch: "stack/auth-refactor",
189
+ headBranch: "stack/validation",
190
+ defaultBranch: "main",
191
+ baseSha: "base",
192
+ headSha: "head",
193
+ url: "https://github.com/backnotprop/plannotator-stack-fixture/pull/3",
194
+ };
195
+
196
+ test("infers a stacked PR when the base branch differs from the default branch", () => {
197
+ expect(getPRStackInfo(stackedMeta)).toEqual({
198
+ isStacked: true,
199
+ baseBranch: "stack/auth-refactor",
200
+ defaultBranch: "main",
201
+ label: "stack/validation stacked on stack/auth-refactor",
202
+ source: "branch-inferred",
203
+ });
204
+ });
205
+
206
+ test("does not infer a stack for the bottom PR targeting the default branch", () => {
207
+ expect(getPRStackInfo({
208
+ ...stackedMeta,
209
+ number: 1,
210
+ baseBranch: "main",
211
+ headBranch: "stack/base-cleanup",
212
+ })).toBeNull();
213
+ });
214
+
215
+ test("only enables full-stack scope when stacked metadata has a local checkout", () => {
216
+ expect(getPRDiffScopeOptions(stackedMeta, true)).toEqual([
217
+ {
218
+ id: "layer",
219
+ label: "Layer",
220
+ description: "Only changes relative to stack/auth-refactor.",
221
+ enabled: true,
222
+ },
223
+ {
224
+ id: "full-stack",
225
+ label: "Full stack",
226
+ description: "All changes from main to HEAD in the local checkout.",
227
+ enabled: true,
228
+ },
229
+ ]);
230
+
231
+ expect(getPRDiffScopeOptions(stackedMeta, false)[1].enabled).toBe(false);
232
+ });
233
+ });
234
+
235
+ describe("isSameProject", () => {
236
+ const ghRef: PRRef = { platform: "github", host: "github.com", owner: "acme", repo: "widgets", number: 1 };
237
+ const glRef: PRRef = { platform: "gitlab", host: "gitlab.com", projectPath: "acme/widgets", iid: 1 };
238
+
239
+ test("same GitHub project", () => {
240
+ expect(isSameProject(ghRef, { ...ghRef, number: 99 })).toBe(true);
241
+ });
242
+
243
+ test("different GitHub owner", () => {
244
+ expect(isSameProject(ghRef, { ...ghRef, owner: "other" })).toBe(false);
245
+ });
246
+
247
+ test("different GitHub repo", () => {
248
+ expect(isSameProject(ghRef, { ...ghRef, repo: "gadgets" })).toBe(false);
249
+ });
250
+
251
+ test("different GitHub host", () => {
252
+ expect(isSameProject(ghRef, { ...ghRef, host: "ghe.corp.com" })).toBe(false);
253
+ });
254
+
255
+ test("same GitLab project", () => {
256
+ expect(isSameProject(glRef, { ...glRef, iid: 99 })).toBe(true);
257
+ });
258
+
259
+ test("different GitLab projectPath", () => {
260
+ expect(isSameProject(glRef, { ...glRef, projectPath: "other/repo" })).toBe(false);
261
+ });
262
+
263
+ test("GitHub vs GitLab", () => {
264
+ expect(isSameProject(ghRef, glRef)).toBe(false);
265
+ });
266
+ });
@@ -0,0 +1,123 @@
1
+ /**
2
+ * Server-only PR/MR dispatch.
3
+ *
4
+ * Picks GitHub vs GitLab from PRRef.platform and delegates to the
5
+ * platform implementation in pr-github.ts / pr-gitlab.ts. These
6
+ * implementations may use Node built-ins (fs, os, path) for things
7
+ * like persisting failed comments — they must never be imported
8
+ * from browser code.
9
+ *
10
+ * Pure types and label helpers live in pr-types.ts, which is
11
+ * browser-safe.
12
+ */
13
+
14
+ import { checkGhAuth, getGhUser, fetchGhPR, fetchGhPRContext, fetchGhPRFileContent, submitGhPRReview, fetchGhPRViewedFiles, markGhFilesViewed, fetchGhPRStack, fetchGhPRList } from "./pr-github";
15
+ import { checkGlAuth, getGlUser, fetchGlMR, fetchGlMRContext, fetchGlFileContent, submitGlMRReview } from "./pr-gitlab";
16
+ import type { PRRuntime, PRRef, PRMetadata, PRContext, PRReviewFileComment, PRStackTree, PRListItem } from "./pr-types";
17
+
18
+ // Re-export the browser-safe surface so server callers can keep using
19
+ // pr-provider as a single facade. Browser code imports from pr-types
20
+ // directly to avoid pulling pr-github / pr-gitlab into the client bundle.
21
+ export * from "./pr-types";
22
+
23
+ // --- Dispatch Functions ---
24
+
25
+ export async function checkAuth(runtime: PRRuntime, ref: PRRef): Promise<void> {
26
+ if (ref.platform === "github") return checkGhAuth(runtime, ref.host);
27
+ return checkGlAuth(runtime, ref.host);
28
+ }
29
+
30
+ export async function getUser(runtime: PRRuntime, ref: PRRef): Promise<string | null> {
31
+ if (ref.platform === "github") return getGhUser(runtime, ref.host);
32
+ return getGlUser(runtime, ref.host);
33
+ }
34
+
35
+ export async function fetchPR(
36
+ runtime: PRRuntime,
37
+ ref: PRRef,
38
+ ): Promise<{ metadata: PRMetadata; rawPatch: string }> {
39
+ if (ref.platform === "github") return fetchGhPR(runtime, ref);
40
+ return fetchGlMR(runtime, ref);
41
+ }
42
+
43
+ export async function fetchPRContext(
44
+ runtime: PRRuntime,
45
+ ref: PRRef,
46
+ ): Promise<PRContext> {
47
+ if (ref.platform === "github") return fetchGhPRContext(runtime, ref);
48
+ return fetchGlMRContext(runtime, ref);
49
+ }
50
+
51
+ export async function fetchPRFileContent(
52
+ runtime: PRRuntime,
53
+ ref: PRRef,
54
+ sha: string,
55
+ filePath: string,
56
+ ): Promise<string | null> {
57
+ if (ref.platform === "github") return fetchGhPRFileContent(runtime, ref, sha, filePath);
58
+ return fetchGlFileContent(runtime, ref, sha, filePath);
59
+ }
60
+
61
+ export async function submitPRReview(
62
+ runtime: PRRuntime,
63
+ ref: PRRef,
64
+ headSha: string,
65
+ action: "approve" | "comment",
66
+ body: string,
67
+ fileComments: PRReviewFileComment[],
68
+ ): Promise<void> {
69
+ if (ref.platform === "github") return submitGhPRReview(runtime, ref, headSha, action, body, fileComments);
70
+ return submitGlMRReview(runtime, ref, headSha, action, body, fileComments);
71
+ }
72
+
73
+ /**
74
+ * Fetch per-file "viewed" state for a PR.
75
+ * GitHub: returns { filePath: isViewed } map.
76
+ * GitLab: always returns {} (no server-side viewed state API).
77
+ */
78
+ export async function fetchPRViewedFiles(
79
+ runtime: PRRuntime,
80
+ ref: PRRef,
81
+ ): Promise<Record<string, boolean>> {
82
+ if (ref.platform === "github") return fetchGhPRViewedFiles(runtime, ref);
83
+ return {}; // GitLab has no server-side viewed state
84
+ }
85
+
86
+ /**
87
+ * Mark or unmark files as viewed in a PR.
88
+ * GitHub: fires markFileAsViewed / unmarkFileAsViewed GraphQL mutations.
89
+ * GitLab: no-op (no server-side viewed state API).
90
+ */
91
+ export async function markPRFilesViewed(
92
+ runtime: PRRuntime,
93
+ ref: PRRef,
94
+ prNodeId: string,
95
+ filePaths: string[],
96
+ viewed: boolean,
97
+ ): Promise<void> {
98
+ if (ref.platform === "github") return markGhFilesViewed(runtime, ref, prNodeId, filePaths, viewed);
99
+ // GitLab: no-op
100
+ }
101
+
102
+ /**
103
+ * Fetch the full stack tree for a stacked PR.
104
+ * Walks up from the current PR to the default branch, resolving
105
+ * PR numbers and titles for each intermediate branch.
106
+ * Returns null if the PR is not stacked or the API call fails.
107
+ */
108
+ export async function fetchPRStack(
109
+ runtime: PRRuntime,
110
+ ref: PRRef,
111
+ metadata: PRMetadata,
112
+ ): Promise<PRStackTree | null> {
113
+ if (ref.platform === "github") return fetchGhPRStack(runtime, ref, metadata);
114
+ return null; // GitLab: not yet implemented
115
+ }
116
+
117
+ export async function fetchPRList(
118
+ runtime: PRRuntime,
119
+ ref: PRRef,
120
+ ): Promise<PRListItem[]> {
121
+ if (ref.platform === "github") return fetchGhPRList(runtime, ref);
122
+ return []; // GitLab: not yet implemented
123
+ }
@@ -0,0 +1,104 @@
1
+ import { describe, expect, test } from "bun:test";
2
+ import type { PRMetadata } from "./pr-types";
3
+ import type { GitCommandResult, ReviewGitRuntime } from "./review-core";
4
+ import { runPRFullStackDiff } from "./pr-stack";
5
+
6
+ function result(stdout = "", stderr = "", exitCode = 0): GitCommandResult {
7
+ return { stdout, stderr, exitCode };
8
+ }
9
+
10
+ const metadata: PRMetadata = {
11
+ platform: "github",
12
+ host: "github.com",
13
+ owner: "backnotprop",
14
+ repo: "plannotator-stack-fixture",
15
+ number: 3,
16
+ title: "Validate user id",
17
+ author: "backnotprop",
18
+ baseBranch: "stack/auth-refactor",
19
+ headBranch: "stack/validation",
20
+ defaultBranch: "main",
21
+ baseSha: "base",
22
+ headSha: "head",
23
+ url: "https://github.com/backnotprop/plannotator-stack-fixture/pull/3",
24
+ };
25
+
26
+ describe("runPRFullStackDiff", () => {
27
+ test("uses origin default branch when it is available", async () => {
28
+ const calls: string[][] = [];
29
+ const runtime: ReviewGitRuntime = {
30
+ async runGit(args) {
31
+ calls.push(args);
32
+ if (args[0] === "show-ref" && args[3] === "refs/remotes/origin/main") {
33
+ return result();
34
+ }
35
+ if (args[0] === "diff") {
36
+ return result("diff --git a/src/auth.ts b/src/auth.ts\n");
37
+ }
38
+ return result("", "unexpected", 1);
39
+ },
40
+ async readTextFile() {
41
+ return null;
42
+ },
43
+ };
44
+
45
+ const diff = await runPRFullStackDiff(runtime, metadata, "/tmp/repo");
46
+
47
+ expect(diff).toEqual({
48
+ patch: "diff --git a/src/auth.ts b/src/auth.ts\n",
49
+ label: "Full stack diff vs origin/main",
50
+ });
51
+ expect(calls.at(-1)).toEqual([
52
+ "diff",
53
+ "--no-ext-diff",
54
+ "--src-prefix=a/",
55
+ "--dst-prefix=b/",
56
+ "--end-of-options",
57
+ "origin/main...HEAD",
58
+ ]);
59
+ });
60
+
61
+ test("falls back to a local default branch", async () => {
62
+ const runtime: ReviewGitRuntime = {
63
+ async runGit(args) {
64
+ if (args[0] === "show-ref" && args[3] === "refs/remotes/origin/main") {
65
+ return result("", "", 1);
66
+ }
67
+ if (args[0] === "show-ref" && args[3] === "refs/heads/main") {
68
+ return result();
69
+ }
70
+ if (args[0] === "diff") {
71
+ return result("local branch patch");
72
+ }
73
+ return result("", "unexpected", 1);
74
+ },
75
+ async readTextFile() {
76
+ return null;
77
+ },
78
+ };
79
+
80
+ const diff = await runPRFullStackDiff(runtime, metadata);
81
+
82
+ expect(diff).toEqual({
83
+ patch: "local branch patch",
84
+ label: "Full stack diff vs main",
85
+ });
86
+ });
87
+
88
+ test("returns an error when no default branch ref exists locally", async () => {
89
+ const runtime: ReviewGitRuntime = {
90
+ async runGit() {
91
+ return result("", "", 1);
92
+ },
93
+ async readTextFile() {
94
+ return null;
95
+ },
96
+ };
97
+
98
+ const diff = await runPRFullStackDiff(runtime, metadata);
99
+
100
+ expect(diff.patch).toBe("");
101
+ expect(diff.label).toBe("Full stack diff unavailable");
102
+ expect(diff.error).toContain("Could not find origin/main or local main");
103
+ });
104
+ });
@@ -0,0 +1,194 @@
1
+ import type { DiffResult, ReviewGitRuntime } from "./review-core";
2
+ import type {
3
+ PRDiffScopeOption,
4
+ PRMetadata,
5
+ PRStackInfo,
6
+ PRStackTree,
7
+ PRStackNode,
8
+ } from "./pr-types";
9
+ export type { PRDiffScope, PRDiffScopeOption, PRStackInfo, PRStackTree, PRStackNode } from "./pr-types";
10
+
11
+ function branchNameIsSafe(branch: string): boolean {
12
+ return branch.trim().length > 0 && !branch.startsWith("-") && !branch.includes("\0");
13
+ }
14
+
15
+ export function getPRStackInfo(metadata: PRMetadata | undefined): PRStackInfo | null {
16
+ if (!metadata?.defaultBranch) return null;
17
+ if (metadata.baseBranch === metadata.defaultBranch) return null;
18
+
19
+ return {
20
+ isStacked: true,
21
+ baseBranch: metadata.baseBranch,
22
+ defaultBranch: metadata.defaultBranch,
23
+ label: `${metadata.headBranch} stacked on ${metadata.baseBranch}`,
24
+ source: "branch-inferred",
25
+ };
26
+ }
27
+
28
+ export function resolveStackInfo(
29
+ metadata: PRMetadata,
30
+ stackTree: PRStackTree | null,
31
+ existing?: PRStackInfo | null,
32
+ ): PRStackInfo | null {
33
+ if (existing) return existing;
34
+ if (!stackTree || stackTree.nodes.filter(n => !n.isDefaultBranch).length <= 1) return null;
35
+ return getPRStackInfo(metadata) ?? {
36
+ isStacked: true,
37
+ baseBranch: metadata.baseBranch,
38
+ defaultBranch: metadata.defaultBranch!,
39
+ label: `Root of stack — ${metadata.headBranch}`,
40
+ source: "tree-discovered",
41
+ };
42
+ }
43
+
44
+ export function getPRDiffScopeOptions(
45
+ metadata: PRMetadata | undefined,
46
+ hasLocalCheckout: boolean,
47
+ ): PRDiffScopeOption[] {
48
+ const stackInfo = getPRStackInfo(metadata);
49
+
50
+ return [
51
+ {
52
+ id: "layer",
53
+ label: "Layer",
54
+ description: metadata?.baseBranch
55
+ ? `Only changes relative to ${metadata.baseBranch}.`
56
+ : "Only changes from this review.",
57
+ enabled: true,
58
+ },
59
+ {
60
+ id: "full-stack",
61
+ label: "Full stack",
62
+ description: stackInfo?.defaultBranch
63
+ ? `All changes from ${stackInfo.defaultBranch} to HEAD in the local checkout.`
64
+ : "All changes from the default branch to HEAD in the local checkout.",
65
+ enabled: Boolean(stackInfo && hasLocalCheckout),
66
+ },
67
+ ];
68
+ }
69
+
70
+ export async function resolvePRFullStackBaseRef(
71
+ runtime: ReviewGitRuntime,
72
+ defaultBranch: string,
73
+ cwd?: string,
74
+ ): Promise<string | null> {
75
+ const remoteRef = `origin/${defaultBranch}`;
76
+ const remote = await runtime.runGit(
77
+ ["show-ref", "--verify", "--quiet", `refs/remotes/${remoteRef}`],
78
+ { cwd },
79
+ );
80
+ if (remote.exitCode === 0) return remoteRef;
81
+
82
+ const local = await runtime.runGit(
83
+ ["show-ref", "--verify", "--quiet", `refs/heads/${defaultBranch}`],
84
+ { cwd },
85
+ );
86
+ if (local.exitCode === 0) return defaultBranch;
87
+
88
+ return null;
89
+ }
90
+
91
+ export async function runPRFullStackDiff(
92
+ runtime: ReviewGitRuntime,
93
+ metadata: PRMetadata,
94
+ cwd?: string,
95
+ ): Promise<DiffResult> {
96
+ const defaultBranch = metadata.defaultBranch;
97
+ if (!defaultBranch || !branchNameIsSafe(defaultBranch)) {
98
+ return {
99
+ patch: "",
100
+ label: "Full stack diff unavailable",
101
+ error: "Could not determine a safe default branch for this review.",
102
+ };
103
+ }
104
+
105
+ const baseRef = await resolvePRFullStackBaseRef(runtime, defaultBranch, cwd);
106
+ if (!baseRef) {
107
+ return {
108
+ patch: "",
109
+ label: "Full stack diff unavailable",
110
+ error: `Could not find origin/${defaultBranch} or local ${defaultBranch} in this checkout.`,
111
+ };
112
+ }
113
+
114
+ const diffArgs = [
115
+ "diff",
116
+ "--no-ext-diff",
117
+ "--src-prefix=a/",
118
+ "--dst-prefix=b/",
119
+ "--end-of-options",
120
+ `${baseRef}...HEAD`,
121
+ ];
122
+ const diff = await runtime.runGit(diffArgs, { cwd });
123
+ if (diff.exitCode !== 0) {
124
+ const message = diff.stderr.trim() || `git ${diffArgs.join(" ")} failed`;
125
+ return {
126
+ patch: "",
127
+ label: "Full stack diff unavailable",
128
+ error: message.split("\n").find((line) => line.trim().length > 0) ?? message,
129
+ };
130
+ }
131
+
132
+ return {
133
+ patch: diff.stdout,
134
+ label: `Full stack diff vs ${baseRef}`,
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Fetch and checkout a PR/MR head in a local worktree.
140
+ * Returns true if the checkout succeeded, false otherwise.
141
+ */
142
+ export async function checkoutPRHead(
143
+ runtime: ReviewGitRuntime,
144
+ metadata: PRMetadata,
145
+ cwd: string,
146
+ ): Promise<boolean> {
147
+ const refSpec = metadata.platform === "github"
148
+ ? `refs/pull/${metadata.number}/head`
149
+ : `refs/merge-requests/${metadata.iid}/head`;
150
+
151
+ const fetch = await runtime.runGit(["fetch", "origin", refSpec], { cwd });
152
+ if (fetch.exitCode !== 0) return false;
153
+
154
+ const checkout = await runtime.runGit(["checkout", "FETCH_HEAD"], { cwd });
155
+ return checkout.exitCode === 0;
156
+ }
157
+
158
+ /**
159
+ * Build a minimal stack tree from existing metadata (no API calls).
160
+ * Used as a fallback when the full stack tree hasn't loaded yet.
161
+ */
162
+ export function buildMinimalStackTree(
163
+ metadata: PRMetadata,
164
+ stackInfo: PRStackInfo,
165
+ ): PRStackTree {
166
+ const nodes: PRStackNode[] = [];
167
+
168
+ if (stackInfo.defaultBranch) {
169
+ nodes.push({
170
+ branch: stackInfo.defaultBranch,
171
+ isCurrent: false,
172
+ isDefaultBranch: true,
173
+ });
174
+ }
175
+
176
+ if (stackInfo.baseBranch !== stackInfo.defaultBranch) {
177
+ nodes.push({
178
+ branch: stackInfo.baseBranch,
179
+ isCurrent: false,
180
+ isDefaultBranch: false,
181
+ });
182
+ }
183
+
184
+ nodes.push({
185
+ branch: metadata.headBranch,
186
+ number: metadata.platform === "github" ? metadata.number : metadata.iid,
187
+ title: metadata.title,
188
+ url: metadata.url,
189
+ isCurrent: true,
190
+ isDefaultBranch: false,
191
+ });
192
+
193
+ return { nodes };
194
+ }