newpr 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 (82) hide show
  1. package/README.md +189 -0
  2. package/package.json +78 -0
  3. package/src/analyzer/errors.ts +22 -0
  4. package/src/analyzer/pipeline.ts +299 -0
  5. package/src/analyzer/progress.ts +69 -0
  6. package/src/cli/args.ts +192 -0
  7. package/src/cli/auth.ts +82 -0
  8. package/src/cli/history-cmd.ts +64 -0
  9. package/src/cli/index.ts +115 -0
  10. package/src/cli/pretty.ts +79 -0
  11. package/src/config/index.ts +103 -0
  12. package/src/config/store.ts +50 -0
  13. package/src/diff/chunker.ts +30 -0
  14. package/src/diff/parser.ts +116 -0
  15. package/src/diff/stats.ts +37 -0
  16. package/src/github/auth.ts +16 -0
  17. package/src/github/fetch-diff.ts +24 -0
  18. package/src/github/fetch-pr.ts +90 -0
  19. package/src/github/parse-pr.ts +39 -0
  20. package/src/history/store.ts +96 -0
  21. package/src/history/types.ts +15 -0
  22. package/src/llm/claude-code-client.ts +134 -0
  23. package/src/llm/client.ts +240 -0
  24. package/src/llm/prompts.ts +176 -0
  25. package/src/llm/response-parser.ts +71 -0
  26. package/src/tui/App.tsx +97 -0
  27. package/src/tui/Footer.tsx +34 -0
  28. package/src/tui/Header.tsx +27 -0
  29. package/src/tui/HelpOverlay.tsx +46 -0
  30. package/src/tui/InputBar.tsx +65 -0
  31. package/src/tui/Loading.tsx +192 -0
  32. package/src/tui/Shell.tsx +384 -0
  33. package/src/tui/TabBar.tsx +31 -0
  34. package/src/tui/commands.ts +75 -0
  35. package/src/tui/narrative-parser.ts +143 -0
  36. package/src/tui/panels/FilesPanel.tsx +134 -0
  37. package/src/tui/panels/GroupsPanel.tsx +140 -0
  38. package/src/tui/panels/NarrativePanel.tsx +102 -0
  39. package/src/tui/panels/StoryPanel.tsx +296 -0
  40. package/src/tui/panels/SummaryPanel.tsx +59 -0
  41. package/src/tui/panels/WalkthroughPanel.tsx +149 -0
  42. package/src/tui/render.tsx +62 -0
  43. package/src/tui/theme.ts +44 -0
  44. package/src/types/config.ts +19 -0
  45. package/src/types/diff.ts +36 -0
  46. package/src/types/github.ts +28 -0
  47. package/src/types/output.ts +59 -0
  48. package/src/web/client/App.tsx +121 -0
  49. package/src/web/client/components/AppShell.tsx +203 -0
  50. package/src/web/client/components/DetailPane.tsx +141 -0
  51. package/src/web/client/components/ErrorScreen.tsx +119 -0
  52. package/src/web/client/components/InputScreen.tsx +41 -0
  53. package/src/web/client/components/LoadingTimeline.tsx +179 -0
  54. package/src/web/client/components/Markdown.tsx +109 -0
  55. package/src/web/client/components/ResizeHandle.tsx +45 -0
  56. package/src/web/client/components/ResultsScreen.tsx +185 -0
  57. package/src/web/client/components/SettingsPanel.tsx +299 -0
  58. package/src/web/client/hooks/useAnalysis.ts +153 -0
  59. package/src/web/client/hooks/useGithubUser.ts +24 -0
  60. package/src/web/client/hooks/useSessions.ts +17 -0
  61. package/src/web/client/hooks/useTheme.ts +34 -0
  62. package/src/web/client/main.tsx +12 -0
  63. package/src/web/client/panels/FilesPanel.tsx +85 -0
  64. package/src/web/client/panels/GroupsPanel.tsx +62 -0
  65. package/src/web/client/panels/NarrativePanel.tsx +9 -0
  66. package/src/web/client/panels/StoryPanel.tsx +54 -0
  67. package/src/web/client/panels/SummaryPanel.tsx +20 -0
  68. package/src/web/components/ui/button.tsx +46 -0
  69. package/src/web/components/ui/card.tsx +37 -0
  70. package/src/web/components/ui/scroll-area.tsx +39 -0
  71. package/src/web/components/ui/tabs.tsx +52 -0
  72. package/src/web/index.html +14 -0
  73. package/src/web/lib/utils.ts +6 -0
  74. package/src/web/server/routes.ts +202 -0
  75. package/src/web/server/session-manager.ts +147 -0
  76. package/src/web/server.ts +96 -0
  77. package/src/web/styles/globals.css +91 -0
  78. package/src/workspace/agent.ts +317 -0
  79. package/src/workspace/explore.ts +82 -0
  80. package/src/workspace/repo-cache.ts +69 -0
  81. package/src/workspace/types.ts +30 -0
  82. package/src/workspace/worktree.ts +129 -0
@@ -0,0 +1,129 @@
1
+ import { tmpdir } from "node:os";
2
+ import { join } from "node:path";
3
+ import { existsSync, rmSync } from "node:fs";
4
+ import type { WorktreeSet } from "./types.ts";
5
+
6
+ const WORKTREE_ROOT = join(tmpdir(), "newpr-workspaces");
7
+
8
+ function worktreeDir(owner: string, repo: string, prNumber: number): string {
9
+ return join(WORKTREE_ROOT, `${owner}-${repo}-pr-${prNumber}`);
10
+ }
11
+
12
+ async function pruneAndCleanAll(bareRepoPath: string): Promise<void> {
13
+ await Bun.$`git -C ${bareRepoPath} worktree prune`.quiet().nothrow();
14
+
15
+ const listResult = await Bun.$`git -C ${bareRepoPath} worktree list --porcelain`.quiet().nothrow();
16
+ if (listResult.exitCode !== 0) return;
17
+
18
+ const lines = listResult.stdout.toString().split("\n");
19
+ for (const line of lines) {
20
+ if (!line.startsWith("worktree ")) continue;
21
+ const wtPath = line.slice("worktree ".length).trim();
22
+ if (wtPath === bareRepoPath) continue;
23
+ if (wtPath.startsWith(WORKTREE_ROOT) && !existsSync(wtPath)) {
24
+ await Bun.$`git -C ${bareRepoPath} worktree remove ${wtPath} --force`.quiet().nothrow();
25
+ }
26
+ }
27
+
28
+ await Bun.$`git -C ${bareRepoPath} worktree prune`.quiet().nothrow();
29
+ }
30
+
31
+ async function forceRemoveWorktree(bareRepoPath: string, wtPath: string): Promise<void> {
32
+ await Bun.$`git -C ${bareRepoPath} worktree remove ${wtPath} --force`.quiet().nothrow();
33
+ if (existsSync(wtPath)) {
34
+ rmSync(wtPath, { recursive: true });
35
+ }
36
+ await Bun.$`git -C ${bareRepoPath} worktree prune`.quiet().nothrow();
37
+ }
38
+
39
+ export async function createWorktrees(
40
+ bareRepoPath: string,
41
+ baseBranch: string,
42
+ prNumber: number,
43
+ owner: string,
44
+ repo: string,
45
+ onProgress?: (msg: string) => void,
46
+ ): Promise<WorktreeSet> {
47
+ const dir = worktreeDir(owner, repo, prNumber);
48
+ const basePath = join(dir, "base");
49
+ const headPath = join(dir, "head");
50
+
51
+ await pruneAndCleanAll(bareRepoPath);
52
+
53
+ await forceRemoveWorktree(bareRepoPath, basePath);
54
+ await forceRemoveWorktree(bareRepoPath, headPath);
55
+ if (existsSync(dir)) {
56
+ rmSync(dir, { recursive: true });
57
+ }
58
+
59
+ onProgress?.(`Fetching PR #${prNumber} ref...`);
60
+ await Bun.$`git -C ${bareRepoPath} fetch origin pull/${prNumber}/head:pr-${prNumber}`.quiet().nothrow();
61
+
62
+ onProgress?.(`Checking out base branch (${baseBranch})...`);
63
+ let baseResult = await Bun.$`git -C ${bareRepoPath} worktree add ${basePath} ${baseBranch}`.quiet().nothrow();
64
+
65
+ if (baseResult.exitCode !== 0) {
66
+ const stderr = baseResult.stderr.toString();
67
+ if (stderr.includes("already checked out") || stderr.includes("is already used by")) {
68
+ onProgress?.(`Branch '${baseBranch}' locked by another worktree, cleaning up...`);
69
+ await pruneAndCleanAll(bareRepoPath);
70
+ const lockResult = await Bun.$`git -C ${bareRepoPath} worktree list --porcelain`.quiet().nothrow();
71
+ const lockLines = lockResult.stdout.toString().split("\n");
72
+ for (let i = 0; i < lockLines.length; i++) {
73
+ const branchLine = lockLines[i];
74
+ if (branchLine && branchLine.includes(`branch refs/heads/${baseBranch}`)) {
75
+ const wtLine = lockLines.slice(0, i).reverse().find((l) => l.startsWith("worktree "));
76
+ if (wtLine) {
77
+ const conflictPath = wtLine.slice("worktree ".length).trim();
78
+ if (conflictPath !== bareRepoPath) {
79
+ await forceRemoveWorktree(bareRepoPath, conflictPath);
80
+ }
81
+ }
82
+ }
83
+ }
84
+ baseResult = await Bun.$`git -C ${bareRepoPath} worktree add ${basePath} ${baseBranch}`.quiet().nothrow();
85
+ }
86
+
87
+ if (baseResult.exitCode !== 0) {
88
+ onProgress?.(`Falling back to detached HEAD for ${baseBranch}...`);
89
+ baseResult = await Bun.$`git -C ${bareRepoPath} worktree add --detach ${basePath} ${baseBranch}`.quiet().nothrow();
90
+ }
91
+
92
+ if (baseResult.exitCode !== 0) {
93
+ const errMsg = baseResult.stderr.toString().trim();
94
+ throw new Error(`worktree add base failed (exit ${baseResult.exitCode}): ${errMsg}`);
95
+ }
96
+ }
97
+
98
+ onProgress?.(`Checking out PR head...`);
99
+ let headResult = await Bun.$`git -C ${bareRepoPath} worktree add ${headPath} pr-${prNumber}`.quiet().nothrow();
100
+ if (headResult.exitCode !== 0) {
101
+ headResult = await Bun.$`git -C ${bareRepoPath} worktree add --detach ${headPath} pr-${prNumber}`.quiet().nothrow();
102
+ }
103
+ if (headResult.exitCode !== 0) {
104
+ const errMsg = headResult.stderr.toString().trim();
105
+ throw new Error(`worktree add head failed (exit ${headResult.exitCode}): ${errMsg}`);
106
+ }
107
+
108
+ return { basePath, headPath };
109
+ }
110
+
111
+ export async function cleanupWorktrees(
112
+ bareRepoPath: string,
113
+ prNumber: number,
114
+ owner: string,
115
+ repo: string,
116
+ ): Promise<void> {
117
+ const dir = worktreeDir(owner, repo, prNumber);
118
+ const basePath = join(dir, "base");
119
+ const headPath = join(dir, "head");
120
+
121
+ await forceRemoveWorktree(bareRepoPath, basePath);
122
+ await forceRemoveWorktree(bareRepoPath, headPath);
123
+
124
+ if (existsSync(dir)) {
125
+ rmSync(dir, { recursive: true });
126
+ }
127
+
128
+ await Bun.$`git -C ${bareRepoPath} worktree prune`.quiet().nothrow();
129
+ }