lazyreview 0.1.4

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 (78) hide show
  1. package/.github/workflows/publish-npm.yml +51 -0
  2. package/.prettierrc +7 -0
  3. package/CLAUDE.md +50 -0
  4. package/README.md +119 -0
  5. package/dist/cli.js +3830 -0
  6. package/dist/cli.js.map +1 -0
  7. package/package.json +64 -0
  8. package/pnpm-workspace.yaml +5 -0
  9. package/src/app.tsx +235 -0
  10. package/src/cli.tsx +78 -0
  11. package/src/components/common/BorderedBox.tsx +41 -0
  12. package/src/components/common/Divider.tsx +31 -0
  13. package/src/components/common/EmptyState.tsx +35 -0
  14. package/src/components/common/FilterModal.tsx +117 -0
  15. package/src/components/common/LoadingIndicator.tsx +31 -0
  16. package/src/components/common/Modal.tsx +24 -0
  17. package/src/components/common/PaginationBar.tsx +56 -0
  18. package/src/components/common/SortModal.tsx +91 -0
  19. package/src/components/common/Spinner.tsx +28 -0
  20. package/src/components/common/index.ts +9 -0
  21. package/src/components/layout/HelpModal.tsx +61 -0
  22. package/src/components/layout/MainPanel.tsx +26 -0
  23. package/src/components/layout/Sidebar.tsx +71 -0
  24. package/src/components/layout/StatusBar.tsx +42 -0
  25. package/src/components/layout/TokenInputModal.tsx +92 -0
  26. package/src/components/layout/TopBar.tsx +44 -0
  27. package/src/components/layout/index.ts +6 -0
  28. package/src/components/pr/CommentsTab.tsx +92 -0
  29. package/src/components/pr/CommitsTab.tsx +142 -0
  30. package/src/components/pr/ConversationsTab.tsx +273 -0
  31. package/src/components/pr/FilesTab.tsx +532 -0
  32. package/src/components/pr/PRHeader.tsx +69 -0
  33. package/src/components/pr/PRListItem.tsx +92 -0
  34. package/src/components/pr/PRTabs.tsx +50 -0
  35. package/src/components/pr/index.ts +8 -0
  36. package/src/hooks/index.ts +12 -0
  37. package/src/hooks/useActivePanel.ts +54 -0
  38. package/src/hooks/useAppKeymap.ts +22 -0
  39. package/src/hooks/useAuth.ts +131 -0
  40. package/src/hooks/useConfig.ts +52 -0
  41. package/src/hooks/useFilter.ts +192 -0
  42. package/src/hooks/useGitHub.ts +260 -0
  43. package/src/hooks/useInputFocus.tsx +35 -0
  44. package/src/hooks/useListNavigation.ts +121 -0
  45. package/src/hooks/useLoading.ts +25 -0
  46. package/src/hooks/usePagination.ts +87 -0
  47. package/src/models/comment.ts +15 -0
  48. package/src/models/commit.ts +20 -0
  49. package/src/models/diff.ts +93 -0
  50. package/src/models/errors.ts +24 -0
  51. package/src/models/file-change.ts +22 -0
  52. package/src/models/index.ts +12 -0
  53. package/src/models/pull-request.ts +40 -0
  54. package/src/models/review.ts +17 -0
  55. package/src/models/user.ts +9 -0
  56. package/src/screens/InvolvedScreen.tsx +161 -0
  57. package/src/screens/MyPRsScreen.tsx +161 -0
  58. package/src/screens/PRDetailScreen.tsx +88 -0
  59. package/src/screens/PRListScreen.tsx +96 -0
  60. package/src/screens/ReviewRequestsScreen.tsx +161 -0
  61. package/src/screens/SettingsScreen.tsx +284 -0
  62. package/src/screens/ThisRepoScreen.tsx +175 -0
  63. package/src/screens/index.ts +7 -0
  64. package/src/services/Auth.ts +314 -0
  65. package/src/services/Config.ts +81 -0
  66. package/src/services/GitHubApi.ts +262 -0
  67. package/src/services/Loading.ts +54 -0
  68. package/src/services/index.ts +27 -0
  69. package/src/theme/index.ts +28 -0
  70. package/src/theme/themes.ts +84 -0
  71. package/src/theme/types.ts +28 -0
  72. package/src/utils/date.ts +28 -0
  73. package/src/utils/git.ts +67 -0
  74. package/src/utils/index.ts +2 -0
  75. package/src/utils/terminal.ts +25 -0
  76. package/tsconfig.json +24 -0
  77. package/tsup.config.ts +13 -0
  78. package/vitest.config.ts +26 -0
@@ -0,0 +1,27 @@
1
+ import { Layer } from 'effect'
2
+ import { ConfigLive } from './Config'
3
+ import { AuthLive } from './Auth'
4
+ import { GitHubApiLive } from './GitHubApi'
5
+ import { LoadingLive } from './Loading'
6
+
7
+ export { Config, type AppConfig, type ConfigService } from './Config'
8
+ export { Auth, type AuthService } from './Auth'
9
+ export {
10
+ GitHubApi,
11
+ type GitHubApiService,
12
+ type ListPRsOptions,
13
+ } from './GitHubApi'
14
+ export {
15
+ Loading,
16
+ type LoadingService,
17
+ type LoadingState,
18
+ } from './Loading'
19
+
20
+ const GitHubApiFullLive = GitHubApiLive.pipe(Layer.provide(AuthLive))
21
+
22
+ export const AppLayer = Layer.mergeAll(
23
+ ConfigLive,
24
+ AuthLive,
25
+ LoadingLive,
26
+ GitHubApiFullLive,
27
+ )
@@ -0,0 +1,28 @@
1
+ import React, { createContext, useContext } from 'react'
2
+ import type { Theme, ThemeName } from './types'
3
+ import { themes, defaultTheme } from './themes'
4
+
5
+ export type { Theme, ThemeColors, ThemeName } from './types'
6
+ export { themes, defaultTheme } from './themes'
7
+
8
+ const ThemeContext = createContext<Theme>(defaultTheme)
9
+
10
+ export interface ThemeProviderProps {
11
+ readonly theme?: Theme
12
+ readonly children: React.ReactNode
13
+ }
14
+
15
+ export function ThemeProvider({
16
+ theme = defaultTheme,
17
+ children,
18
+ }: ThemeProviderProps): React.ReactElement {
19
+ return React.createElement(ThemeContext.Provider, { value: theme }, children)
20
+ }
21
+
22
+ export function useTheme(): Theme {
23
+ return useContext(ThemeContext)
24
+ }
25
+
26
+ export function getThemeByName(name: ThemeName): Theme {
27
+ return themes[name] ?? defaultTheme
28
+ }
@@ -0,0 +1,84 @@
1
+ import type { Theme, ThemeName } from './types'
2
+
3
+ const tokyoNight: Theme = {
4
+ name: 'tokyo-night',
5
+ colors: {
6
+ bg: '#1a1b26',
7
+ text: '#c0caf5',
8
+ accent: '#7aa2f7',
9
+ muted: '#565f89',
10
+ border: '#3b4261',
11
+ primary: '#7aa2f7',
12
+ secondary: '#bb9af7',
13
+
14
+ success: '#9ece6a',
15
+ error: '#f7768e',
16
+ warning: '#e0af68',
17
+ info: '#7dcfff',
18
+
19
+ diffAdd: '#9ece6a',
20
+ diffDel: '#f7768e',
21
+
22
+ selection: '#283457',
23
+ listSelectedFg: '#c0caf5',
24
+ listSelectedBg: '#283457',
25
+ },
26
+ }
27
+
28
+ const dracula: Theme = {
29
+ name: 'dracula',
30
+ colors: {
31
+ bg: '#282a36',
32
+ text: '#f8f8f2',
33
+ accent: '#bd93f9',
34
+ muted: '#6272a4',
35
+ border: '#44475a',
36
+ primary: '#bd93f9',
37
+ secondary: '#ff79c6',
38
+
39
+ success: '#50fa7b',
40
+ error: '#ff5555',
41
+ warning: '#f1fa8c',
42
+ info: '#8be9fd',
43
+
44
+ diffAdd: '#50fa7b',
45
+ diffDel: '#ff5555',
46
+
47
+ selection: '#44475a',
48
+ listSelectedFg: '#f8f8f2',
49
+ listSelectedBg: '#44475a',
50
+ },
51
+ }
52
+
53
+ const catppuccinMocha: Theme = {
54
+ name: 'catppuccin-mocha',
55
+ colors: {
56
+ bg: '#1e1e2e',
57
+ text: '#cdd6f4',
58
+ accent: '#89b4fa',
59
+ muted: '#6c7086',
60
+ border: '#313244',
61
+ primary: '#89b4fa',
62
+ secondary: '#cba6f7',
63
+
64
+ success: '#a6e3a1',
65
+ error: '#f38ba8',
66
+ warning: '#f9e2af',
67
+ info: '#89dceb',
68
+
69
+ diffAdd: '#a6e3a1',
70
+ diffDel: '#f38ba8',
71
+
72
+ selection: '#313244',
73
+ listSelectedFg: '#cdd6f4',
74
+ listSelectedBg: '#313244',
75
+ },
76
+ }
77
+
78
+ export const themes: Record<ThemeName, Theme> = {
79
+ 'tokyo-night': tokyoNight,
80
+ dracula,
81
+ 'catppuccin-mocha': catppuccinMocha,
82
+ }
83
+
84
+ export const defaultTheme: Theme = tokyoNight
@@ -0,0 +1,28 @@
1
+ export interface Theme {
2
+ readonly name: string
3
+ readonly colors: ThemeColors
4
+ }
5
+
6
+ export interface ThemeColors {
7
+ readonly bg: string
8
+ readonly text: string
9
+ readonly accent: string
10
+ readonly muted: string
11
+ readonly border: string
12
+ readonly primary: string
13
+ readonly secondary: string
14
+
15
+ readonly success: string
16
+ readonly error: string
17
+ readonly warning: string
18
+ readonly info: string
19
+
20
+ readonly diffAdd: string
21
+ readonly diffDel: string
22
+
23
+ readonly selection: string
24
+ readonly listSelectedFg: string
25
+ readonly listSelectedBg: string
26
+ }
27
+
28
+ export type ThemeName = 'tokyo-night' | 'dracula' | 'catppuccin-mocha'
@@ -0,0 +1,28 @@
1
+ import { formatDistanceToNow, parseISO, format } from 'date-fns'
2
+
3
+ export function timeAgo(dateString: string): string {
4
+ try {
5
+ const date = parseISO(dateString)
6
+ return formatDistanceToNow(date, { addSuffix: true })
7
+ } catch {
8
+ return dateString
9
+ }
10
+ }
11
+
12
+ export function formatDate(dateString: string): string {
13
+ try {
14
+ const date = parseISO(dateString)
15
+ return format(date, 'MMM d, yyyy')
16
+ } catch {
17
+ return dateString
18
+ }
19
+ }
20
+
21
+ export function formatDateTime(dateString: string): string {
22
+ try {
23
+ const date = parseISO(dateString)
24
+ return format(date, 'MMM d, yyyy h:mm a')
25
+ } catch {
26
+ return dateString
27
+ }
28
+ }
@@ -0,0 +1,67 @@
1
+ import { execFile } from 'node:child_process'
2
+ import { promisify } from 'node:util'
3
+
4
+ const execFileAsync = promisify(execFile)
5
+
6
+ export interface GitRepoInfo {
7
+ readonly isGitRepo: boolean
8
+ readonly owner: string | null
9
+ readonly repo: string | null
10
+ readonly remoteUrl: string | null
11
+ }
12
+
13
+ /**
14
+ * Detect if current directory is a git repo and extract owner/repo from remote
15
+ */
16
+ export async function detectGitRepo(): Promise<GitRepoInfo> {
17
+ try {
18
+ // Check if we're in a git repo
19
+ await execFileAsync('git', ['rev-parse', '--git-dir'])
20
+
21
+ // Get the remote URL
22
+ const { stdout } = await execFileAsync('git', [
23
+ 'remote',
24
+ 'get-url',
25
+ 'origin',
26
+ ])
27
+ const remoteUrl = stdout.trim()
28
+
29
+ // Parse owner/repo from URL
30
+ // Supports: git@github.com:owner/repo.git, https://github.com/owner/repo.git
31
+ const parsed = parseGitHubUrl(remoteUrl)
32
+
33
+ return {
34
+ isGitRepo: true,
35
+ owner: parsed?.owner ?? null,
36
+ repo: parsed?.repo ?? null,
37
+ remoteUrl,
38
+ }
39
+ } catch {
40
+ return {
41
+ isGitRepo: false,
42
+ owner: null,
43
+ repo: null,
44
+ remoteUrl: null,
45
+ }
46
+ }
47
+ }
48
+
49
+ function parseGitHubUrl(
50
+ url: string,
51
+ ): { owner: string; repo: string } | null {
52
+ // SSH format: git@github.com:owner/repo.git
53
+ const sshMatch = url.match(/git@github\.com:([^/]+)\/([^/.]+)(?:\.git)?/)
54
+ if (sshMatch) {
55
+ return { owner: sshMatch[1]!, repo: sshMatch[2]! }
56
+ }
57
+
58
+ // HTTPS format: https://github.com/owner/repo.git
59
+ const httpsMatch = url.match(
60
+ /https?:\/\/github\.com\/([^/]+)\/([^/.]+)(?:\.git)?/,
61
+ )
62
+ if (httpsMatch) {
63
+ return { owner: httpsMatch[1]!, repo: httpsMatch[2]! }
64
+ }
65
+
66
+ return null
67
+ }
@@ -0,0 +1,2 @@
1
+ export { timeAgo, formatDate, formatDateTime } from './date'
2
+ export { truncate, padRight, pluralize, formatCount } from './terminal'
@@ -0,0 +1,25 @@
1
+ export function truncate(text: string, maxWidth: number): string {
2
+ if (text.length <= maxWidth) return text
3
+ if (maxWidth <= 3) return text.slice(0, maxWidth)
4
+ return text.slice(0, maxWidth - 1) + '\u2026'
5
+ }
6
+
7
+ export function padRight(text: string, width: number): string {
8
+ if (text.length >= width) return text
9
+ return text + ' '.repeat(width - text.length)
10
+ }
11
+
12
+ export function pluralize(
13
+ count: number,
14
+ singular: string,
15
+ plural?: string,
16
+ ): string {
17
+ return count === 1 ? singular : (plural ?? `${singular}s`)
18
+ }
19
+
20
+ export function formatCount(count: number): string {
21
+ if (count >= 1000) {
22
+ return `${(count / 1000).toFixed(1)}k`
23
+ }
24
+ return String(count)
25
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ES2022",
5
+ "moduleResolution": "Bundler",
6
+ "jsx": "react-jsx",
7
+ "strict": true,
8
+ "esModuleInterop": true,
9
+ "skipLibCheck": true,
10
+ "forceConsistentCasingInFileNames": true,
11
+ "resolveJsonModule": true,
12
+ "declaration": true,
13
+ "declarationMap": true,
14
+ "sourceMap": true,
15
+ "outDir": "dist",
16
+ "rootDir": "src",
17
+ "baseUrl": ".",
18
+ "paths": {
19
+ "@/*": ["src/*"]
20
+ }
21
+ },
22
+ "include": ["src/**/*"],
23
+ "exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.test.tsx"]
24
+ }
package/tsup.config.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'tsup'
2
+
3
+ export default defineConfig({
4
+ entry: ['src/cli.tsx'],
5
+ format: ['esm'],
6
+ target: 'node20',
7
+ outDir: 'dist',
8
+ clean: true,
9
+ sourcemap: true,
10
+ banner: {
11
+ js: '#!/usr/bin/env node',
12
+ },
13
+ })
@@ -0,0 +1,26 @@
1
+ import { defineConfig } from 'vitest/config'
2
+
3
+ export default defineConfig({
4
+ test: {
5
+ globals: true,
6
+ environment: 'node',
7
+ include: ['src/**/*.test.{ts,tsx}'],
8
+ environmentMatchGlobs: [['src/**/*.test.tsx', 'jsdom']],
9
+ coverage: {
10
+ provider: 'v8',
11
+ include: ['src/**/*.{ts,tsx}'],
12
+ exclude: ['src/**/*.test.{ts,tsx}', 'src/cli.tsx'],
13
+ thresholds: {
14
+ lines: 80,
15
+ branches: 80,
16
+ functions: 80,
17
+ statements: 80,
18
+ },
19
+ },
20
+ },
21
+ resolve: {
22
+ alias: {
23
+ '@': '/src',
24
+ },
25
+ },
26
+ })