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.
- package/.github/workflows/publish-npm.yml +51 -0
- package/.prettierrc +7 -0
- package/CLAUDE.md +50 -0
- package/README.md +119 -0
- package/dist/cli.js +3830 -0
- package/dist/cli.js.map +1 -0
- package/package.json +64 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/app.tsx +235 -0
- package/src/cli.tsx +78 -0
- package/src/components/common/BorderedBox.tsx +41 -0
- package/src/components/common/Divider.tsx +31 -0
- package/src/components/common/EmptyState.tsx +35 -0
- package/src/components/common/FilterModal.tsx +117 -0
- package/src/components/common/LoadingIndicator.tsx +31 -0
- package/src/components/common/Modal.tsx +24 -0
- package/src/components/common/PaginationBar.tsx +56 -0
- package/src/components/common/SortModal.tsx +91 -0
- package/src/components/common/Spinner.tsx +28 -0
- package/src/components/common/index.ts +9 -0
- package/src/components/layout/HelpModal.tsx +61 -0
- package/src/components/layout/MainPanel.tsx +26 -0
- package/src/components/layout/Sidebar.tsx +71 -0
- package/src/components/layout/StatusBar.tsx +42 -0
- package/src/components/layout/TokenInputModal.tsx +92 -0
- package/src/components/layout/TopBar.tsx +44 -0
- package/src/components/layout/index.ts +6 -0
- package/src/components/pr/CommentsTab.tsx +92 -0
- package/src/components/pr/CommitsTab.tsx +142 -0
- package/src/components/pr/ConversationsTab.tsx +273 -0
- package/src/components/pr/FilesTab.tsx +532 -0
- package/src/components/pr/PRHeader.tsx +69 -0
- package/src/components/pr/PRListItem.tsx +92 -0
- package/src/components/pr/PRTabs.tsx +50 -0
- package/src/components/pr/index.ts +8 -0
- package/src/hooks/index.ts +12 -0
- package/src/hooks/useActivePanel.ts +54 -0
- package/src/hooks/useAppKeymap.ts +22 -0
- package/src/hooks/useAuth.ts +131 -0
- package/src/hooks/useConfig.ts +52 -0
- package/src/hooks/useFilter.ts +192 -0
- package/src/hooks/useGitHub.ts +260 -0
- package/src/hooks/useInputFocus.tsx +35 -0
- package/src/hooks/useListNavigation.ts +121 -0
- package/src/hooks/useLoading.ts +25 -0
- package/src/hooks/usePagination.ts +87 -0
- package/src/models/comment.ts +15 -0
- package/src/models/commit.ts +20 -0
- package/src/models/diff.ts +93 -0
- package/src/models/errors.ts +24 -0
- package/src/models/file-change.ts +22 -0
- package/src/models/index.ts +12 -0
- package/src/models/pull-request.ts +40 -0
- package/src/models/review.ts +17 -0
- package/src/models/user.ts +9 -0
- package/src/screens/InvolvedScreen.tsx +161 -0
- package/src/screens/MyPRsScreen.tsx +161 -0
- package/src/screens/PRDetailScreen.tsx +88 -0
- package/src/screens/PRListScreen.tsx +96 -0
- package/src/screens/ReviewRequestsScreen.tsx +161 -0
- package/src/screens/SettingsScreen.tsx +284 -0
- package/src/screens/ThisRepoScreen.tsx +175 -0
- package/src/screens/index.ts +7 -0
- package/src/services/Auth.ts +314 -0
- package/src/services/Config.ts +81 -0
- package/src/services/GitHubApi.ts +262 -0
- package/src/services/Loading.ts +54 -0
- package/src/services/index.ts +27 -0
- package/src/theme/index.ts +28 -0
- package/src/theme/themes.ts +84 -0
- package/src/theme/types.ts +28 -0
- package/src/utils/date.ts +28 -0
- package/src/utils/git.ts +67 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/terminal.ts +25 -0
- package/tsconfig.json +24 -0
- package/tsup.config.ts +13 -0
- 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
|
+
}
|
package/src/utils/git.ts
ADDED
|
@@ -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,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
package/vitest.config.ts
ADDED
|
@@ -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
|
+
})
|