gitforest 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 (256) hide show
  1. package/.bunignore +7 -0
  2. package/.github/workflows/ci.yml +73 -0
  3. package/CLAUDE.md +111 -0
  4. package/CONTRIBUTING.md +145 -0
  5. package/README.md +168 -0
  6. package/bun.lock +267 -0
  7. package/bunfig.toml +15 -0
  8. package/cli +0 -0
  9. package/config/gitforest.example.yaml +94 -0
  10. package/docs/ai/IMPROVEMENT_PLAN.md +341 -0
  11. package/docs/ai/VERIFICATION_REPORT.md +87 -0
  12. package/docs/ai/architecture.md +169 -0
  13. package/docs/ai/checks/check-2025-12-02-tests.md +40 -0
  14. package/docs/ai/checks/check-2025-12-02.md +55 -0
  15. package/docs/ai/checks/test-verification-report.md +85 -0
  16. package/docs/ai/implementation-guide.md +776 -0
  17. package/docs/ai/research/gitty-codebase-analysis.md +221 -0
  18. package/docs/ai/tickets/GENERAL-sitrep.md +30 -0
  19. package/docs/ai/tickets/TASK-database-tests-sitrep.md +25 -0
  20. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +28 -0
  21. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +28 -0
  22. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +24 -0
  23. package/docs/ai/tickets/TASK-github-service-sitrep.md +32 -0
  24. package/docs/ai/tickets/TASK-github-token-sitrep.md +51 -0
  25. package/docs/ai/tickets/TASK-hascommits-sitrep.md +35 -0
  26. package/docs/ai/tickets/TASK-keybindings-sitrep.md +26 -0
  27. package/docs/ai/tickets/TASK-layout-sitrep.md +25 -0
  28. package/docs/ai/tickets/TASK-markdown-sitrep.md +28 -0
  29. package/docs/ai/tickets/TASK-project-item-sitrep.md +79 -0
  30. package/docs/ai/tickets/TASK-sitrep.md +28 -0
  31. package/docs/ai/tickets/TASK-state-sitrep.md +26 -0
  32. package/docs/ai/tickets/TASK-types-sitrep.md +25 -0
  33. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +26 -0
  34. package/docs/ai/tickets/TKT-001-sitrep.md +24 -0
  35. package/docs/ai/tickets/TKT-002-sitrep.md +25 -0
  36. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +46 -0
  37. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +135 -0
  38. package/docs/ai/tickets/TKT-003-sitrep.md +26 -0
  39. package/docs/ai/tickets/TKT-004-sitrep.md +27 -0
  40. package/docs/ai/tickets/TKT-005-sitrep.md +25 -0
  41. package/docs/ai/tickets/TKT-006-sitrep.md +26 -0
  42. package/docs/ai/tickets/TKT-007-sitrep.md +30 -0
  43. package/docs/ai/tickets/TKT-008-sitrep.md +32 -0
  44. package/docs/ai/tickets/TKT-009-sitrep.md +27 -0
  45. package/docs/ai/tickets/TKT-010-sitrep.md +27 -0
  46. package/docs/ai/tickets/TKT-011-sitrep.md +26 -0
  47. package/docs/ai/tickets/TKT-012-sitrep.md +25 -0
  48. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +28 -0
  49. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +25 -0
  50. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +25 -0
  51. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +24 -0
  52. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +29 -0
  53. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +29 -0
  54. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +26 -0
  55. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +30 -0
  56. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +28 -0
  57. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +26 -0
  58. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +25 -0
  59. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +30 -0
  60. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +25 -0
  61. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +29 -0
  62. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +95 -0
  63. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +61 -0
  64. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +30 -0
  65. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +27 -0
  66. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +25 -0
  67. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +27 -0
  68. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +25 -0
  69. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +28 -0
  70. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +25 -0
  71. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +26 -0
  72. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +25 -0
  73. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +26 -0
  74. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +24 -0
  75. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +25 -0
  76. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +29 -0
  77. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +25 -0
  78. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +24 -0
  79. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +24 -0
  80. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +27 -0
  81. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +25 -0
  82. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +25 -0
  83. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +27 -0
  84. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +32 -0
  85. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +27 -0
  86. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +30 -0
  87. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +25 -0
  88. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +29 -0
  89. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +25 -0
  90. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +27 -0
  91. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +25 -0
  92. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +26 -0
  93. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +25 -0
  94. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +27 -0
  95. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +75 -0
  96. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +29 -0
  97. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +29 -0
  98. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +25 -0
  99. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +25 -0
  100. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +32 -0
  101. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +64 -0
  102. package/docs/ai/tkt-001-fix-database-error.md +217 -0
  103. package/docs/ai/ui-enhancement-plan.md +562 -0
  104. package/package.json +50 -0
  105. package/src/app.tsx +43 -0
  106. package/src/cli/config.ts +94 -0
  107. package/src/cli/formatters.ts +632 -0
  108. package/src/cli/index.ts +583 -0
  109. package/src/components/CloneDialog.tsx +137 -0
  110. package/src/components/ColumnHeader.tsx +128 -0
  111. package/src/components/CommandPalette.tsx +120 -0
  112. package/src/components/ConfirmDialog.tsx +105 -0
  113. package/src/components/ErrorBoundary.tsx +128 -0
  114. package/src/components/FilterBar.tsx +71 -0
  115. package/src/components/FilterOptionsOverlay.tsx +131 -0
  116. package/src/components/HelpOverlay.tsx +120 -0
  117. package/src/components/Layout.tsx +379 -0
  118. package/src/components/MarkdownRenderer.tsx +127 -0
  119. package/src/components/ProgressBar.tsx +53 -0
  120. package/src/components/ProjectItem.tsx +143 -0
  121. package/src/components/ProjectList.tsx +90 -0
  122. package/src/components/RepoDetailModal.tsx +367 -0
  123. package/src/components/StatusBar.tsx +188 -0
  124. package/src/components/UnifiedProjectItem.tsx +436 -0
  125. package/src/components/ViewModeIndicator.tsx +37 -0
  126. package/src/components/onboarding/CompleteStep.tsx +82 -0
  127. package/src/components/onboarding/DirectoriesStep.test.tsx +52 -0
  128. package/src/components/onboarding/DirectoriesStep.tsx +847 -0
  129. package/src/components/onboarding/DirectoriesStep.unit.test.ts +345 -0
  130. package/src/components/onboarding/GitHubAuthStep.tsx +268 -0
  131. package/src/components/onboarding/OnboardingWizard.tsx +130 -0
  132. package/src/components/onboarding/WelcomeStep.tsx +69 -0
  133. package/src/config/loader.ts +263 -0
  134. package/src/config/onboarding.ts +67 -0
  135. package/src/constants.ts +96 -0
  136. package/src/db/index.ts +147 -0
  137. package/src/db/schema.ts +70 -0
  138. package/src/git/commands.ts +283 -0
  139. package/src/git/index.ts +2 -0
  140. package/src/git/operations.ts +93 -0
  141. package/src/git/service.ts +539 -0
  142. package/src/git/status.ts +84 -0
  143. package/src/git/types.ts +5 -0
  144. package/src/github/auth.ts +311 -0
  145. package/src/github/cache.ts +231 -0
  146. package/src/github/cli.ts +22 -0
  147. package/src/github/unified.ts +415 -0
  148. package/src/hooks/useBackgroundFetch.ts +76 -0
  149. package/src/hooks/useConfirmDialogActions.ts +120 -0
  150. package/src/hooks/useKeyBindings.ts +656 -0
  151. package/src/hooks/useProjects.ts +47 -0
  152. package/src/hooks/useUnifiedRepos.ts +317 -0
  153. package/src/index.tsx +494 -0
  154. package/src/operations/batch.ts +280 -0
  155. package/src/operations/commands.ts +140 -0
  156. package/src/operations/index.ts +37 -0
  157. package/src/scanner/index.ts +424 -0
  158. package/src/scanner/markers.ts +43 -0
  159. package/src/scanner/submodules.ts +61 -0
  160. package/src/services/git.ts +484 -0
  161. package/src/services/github.ts +676 -0
  162. package/src/services/index.ts +28 -0
  163. package/src/services/types.ts +99 -0
  164. package/src/state/actions.ts +175 -0
  165. package/src/state/reducer.ts +294 -0
  166. package/src/state/store.tsx +216 -0
  167. package/src/state/types.ts +8 -0
  168. package/src/types/index.ts +383 -0
  169. package/src/ui/theme.ts +44 -0
  170. package/src/utils/array.ts +14 -0
  171. package/src/utils/debug.ts +38 -0
  172. package/src/utils/errors.ts +17 -0
  173. package/src/utils/index.ts +8 -0
  174. package/src/utils/markdown.ts +230 -0
  175. package/src/utils/project-utils.ts +129 -0
  176. package/src/utils/rate-limiter.ts +134 -0
  177. package/src/utils/retry.ts +147 -0
  178. package/src/utils/timeout.ts +56 -0
  179. package/test/integration/app.isolated.tsx +240 -0
  180. package/test/integration/cli-commands.test.ts +287 -0
  181. package/test/integration/cli-validation.test.ts +264 -0
  182. package/test/integration/git-operations.test.ts +218 -0
  183. package/test/integration/scanner.test.ts +228 -0
  184. package/test/preload.ts +18 -0
  185. package/test/unit/cli/commands.test.ts +13 -0
  186. package/test/unit/cli/formatters.test.ts +1116 -0
  187. package/test/unit/cli/github-commands.test.ts +12 -0
  188. package/test/unit/components/CloneDialog.test.tsx +240 -0
  189. package/test/unit/components/ColumnHeader.test.tsx +128 -0
  190. package/test/unit/components/CommandPalette.test.tsx +355 -0
  191. package/test/unit/components/ConfirmDialog.test.tsx +111 -0
  192. package/test/unit/components/ErrorBoundary.test.tsx +139 -0
  193. package/test/unit/components/FilterBar.test.tsx +43 -0
  194. package/test/unit/components/FilterOptionsOverlay.test.tsx +197 -0
  195. package/test/unit/components/HelpOverlay.test.tsx +90 -0
  196. package/test/unit/components/Layout.test.tsx +328 -0
  197. package/test/unit/components/MarkdownRenderer.test.tsx +45 -0
  198. package/test/unit/components/ProgressBar.test.tsx +138 -0
  199. package/test/unit/components/ProjectItem.test.tsx +182 -0
  200. package/test/unit/components/ProjectList.test.tsx +311 -0
  201. package/test/unit/components/RepoDetailModal.test.tsx +445 -0
  202. package/test/unit/components/StatusBar.test.tsx +112 -0
  203. package/test/unit/components/UnifiedProjectItem.test.tsx +618 -0
  204. package/test/unit/components/ViewModeIndicator.test.tsx +137 -0
  205. package/test/unit/components/test-utils.tsx +63 -0
  206. package/test/unit/config/loader.test.ts +692 -0
  207. package/test/unit/db/database.test.ts +978 -0
  208. package/test/unit/db/index.test.ts +314 -0
  209. package/test/unit/fixtures/setup.ts +186 -0
  210. package/test/unit/git/commands-untested.test.ts +205 -0
  211. package/test/unit/git/commands.test.ts +269 -0
  212. package/test/unit/git/operations.test.ts +322 -0
  213. package/test/unit/git/status.test.ts +219 -0
  214. package/test/unit/github/auth.test.ts +317 -0
  215. package/test/unit/github/cache.test.ts +1028 -0
  216. package/test/unit/github/cli.test.ts +135 -0
  217. package/test/unit/github/unified.test.ts +1201 -0
  218. package/test/unit/graceful-shutdown.test.ts +83 -0
  219. package/test/unit/hooks/useBackgroundFetch.test.tsx +239 -0
  220. package/test/unit/hooks/useConfirmDialogActions.test.tsx +81 -0
  221. package/test/unit/hooks/useKeyBindings.isolated.ts +715 -0
  222. package/test/unit/hooks/useProjects.test.tsx +186 -0
  223. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +115 -0
  224. package/test/unit/hooks/useUnifiedRepos.test.tsx +177 -0
  225. package/test/unit/mocks/config.ts +109 -0
  226. package/test/unit/mocks/git-service.ts +274 -0
  227. package/test/unit/mocks/github-service.ts +250 -0
  228. package/test/unit/mocks/index.ts +72 -0
  229. package/test/unit/mocks/project.ts +148 -0
  230. package/test/unit/mocks/state-mocks.ts +187 -0
  231. package/test/unit/mocks/unified.ts +169 -0
  232. package/test/unit/operations/batch.test.ts +216 -0
  233. package/test/unit/operations/commands.test.ts +550 -0
  234. package/test/unit/scanner/errors.test.ts +297 -0
  235. package/test/unit/scanner/index.test.ts +1011 -0
  236. package/test/unit/scanner/markers.test.ts +150 -0
  237. package/test/unit/scanner/submodules.test.ts +99 -0
  238. package/test/unit/services/git-errors.test.ts +190 -0
  239. package/test/unit/services/git.test.ts +442 -0
  240. package/test/unit/services/github-errors.test.ts +293 -0
  241. package/test/unit/services/github.test.ts +200 -0
  242. package/test/unit/state/actions.test.ts +217 -0
  243. package/test/unit/state/reducer.test.ts +745 -0
  244. package/test/unit/state/store.test.tsx +711 -0
  245. package/test/unit/types/commands.test.ts +220 -0
  246. package/test/unit/types/schema.test.ts +179 -0
  247. package/test/unit/utils/array.test.ts +73 -0
  248. package/test/unit/utils/debug.test.ts +23 -0
  249. package/test/unit/utils/errors.test.ts +295 -0
  250. package/test/unit/utils/markdown.test.ts +163 -0
  251. package/test/unit/utils/project-utils.test.ts +756 -0
  252. package/test/unit/utils/rate-limiter.test.ts +256 -0
  253. package/test/unit/utils/retry.test.ts +165 -0
  254. package/test/unit/utils/strip-ansi.ts +13 -0
  255. package/test/unit/utils/timeout.test.ts +93 -0
  256. package/tsconfig.json +29 -0
@@ -0,0 +1,99 @@
1
+ /**
2
+ * Common types for service abstractions
3
+ */
4
+
5
+ import type { GitStatus, OperationResult, BatchResult, Project } from "../types/index.ts";
6
+
7
+ // ============================================================================
8
+ // Git Service Types
9
+ // ============================================================================
10
+
11
+ export interface GitCommandResult {
12
+ success: boolean;
13
+ output?: string;
14
+ error?: string;
15
+ }
16
+
17
+ export interface GitStatusResult extends GitStatus {}
18
+
19
+ export interface SubmoduleEntry {
20
+ path: string;
21
+ commit: string;
22
+ status: "-" | "+" | " " | "U";
23
+ }
24
+
25
+ // ============================================================================
26
+ // GitHub Service Types
27
+ // ============================================================================
28
+
29
+ export interface GitHubUser {
30
+ login: string;
31
+ name: string | null;
32
+ type: "User" | "Organization";
33
+ }
34
+
35
+ export interface GitHubOrg {
36
+ login: string;
37
+ description: string | null;
38
+ }
39
+
40
+ export interface GitHubRepoData {
41
+ id: number;
42
+ name: string;
43
+ fullName: string;
44
+ description: string | null;
45
+ htmlUrl: string;
46
+ sshUrl: string;
47
+ cloneUrl: string;
48
+ isPrivate: boolean;
49
+ isArchived: boolean;
50
+ isFork: boolean;
51
+ createdAt: string;
52
+ updatedAt: string;
53
+ pushedAt: string | null;
54
+ size: number;
55
+ stargazersCount: number;
56
+ forksCount: number;
57
+ openIssuesCount: number;
58
+ watchersCount: number;
59
+ topics: string[];
60
+ license: {
61
+ name: string;
62
+ } | null;
63
+ hasIssues: boolean;
64
+ hasWiki: boolean;
65
+ hasDiscussions: boolean;
66
+ language: string | null;
67
+ defaultBranch: string;
68
+ owner: {
69
+ login: string;
70
+ type: "User" | "Organization";
71
+ };
72
+ }
73
+
74
+ export interface GetReposOptions {
75
+ type?: "all" | "owner" | "member";
76
+ sort?: "created" | "updated" | "pushed" | "full_name";
77
+ direction?: "asc" | "desc";
78
+ includeArchived?: boolean;
79
+ includeForks?: boolean;
80
+ includeOrgs?: boolean;
81
+ }
82
+
83
+ export interface CreateRepoOptions {
84
+ name: string;
85
+ description?: string;
86
+ isPrivate?: boolean;
87
+ localPath?: string;
88
+ }
89
+
90
+ export interface CloneOptions {
91
+ useSSH?: boolean;
92
+ targetDir: string;
93
+ }
94
+
95
+ // ============================================================================
96
+ // Progress Callback
97
+ // ============================================================================
98
+
99
+ export type ProgressCallback = (completed: number, total: number) => void;
@@ -0,0 +1,175 @@
1
+ import type { AppMode, SortField, SortDirection, Project, ViewMode, GitHubRepoInfo, UnifiedRepo, QuickFilter, DetailModalState } from "../types/index.ts";
2
+
3
+ /**
4
+ * Action creators for app state
5
+ * Each returns a specific action type for better type inference in tests
6
+ */
7
+
8
+ export const setProjects = (projects: Project[]) => ({
9
+ type: "SET_PROJECTS" as const,
10
+ payload: projects,
11
+ });
12
+
13
+ export const setLoading = (loading: boolean) => ({
14
+ type: "SET_LOADING" as const,
15
+ payload: loading,
16
+ });
17
+
18
+ export const setError = (error: string | null) => ({
19
+ type: "SET_ERROR" as const,
20
+ payload: error,
21
+ });
22
+
23
+ export const setMessage = (message: string | null) => ({
24
+ type: "SET_MESSAGE" as const,
25
+ payload: message,
26
+ });
27
+
28
+ export const moveCursor = (index: number) => ({
29
+ type: "MOVE_CURSOR" as const,
30
+ payload: index,
31
+ });
32
+
33
+ export const toggleSelection = (index: number) => ({
34
+ type: "TOGGLE_SELECTION" as const,
35
+ payload: index,
36
+ });
37
+
38
+ export const selectAll = () => ({
39
+ type: "SELECT_ALL" as const,
40
+ });
41
+
42
+ export const deselectAll = () => ({
43
+ type: "DESELECT_ALL" as const,
44
+ });
45
+
46
+ export const setFilter = (text: string) => ({
47
+ type: "SET_FILTER" as const,
48
+ payload: text,
49
+ });
50
+
51
+ export const setSort = (by: SortField, direction: SortDirection) => ({
52
+ type: "SET_SORT" as const,
53
+ payload: { by, direction },
54
+ });
55
+
56
+ export const cycleSort = () => ({
57
+ type: "CYCLE_SORT" as const,
58
+ });
59
+
60
+ export const setMode = (mode: AppMode) => ({
61
+ type: "SET_MODE" as const,
62
+ payload: mode,
63
+ });
64
+
65
+ export const startAction = (action: string) => ({
66
+ type: "START_ACTION" as const,
67
+ payload: action,
68
+ });
69
+
70
+ export const endAction = () => ({
71
+ type: "END_ACTION" as const,
72
+ });
73
+
74
+ export const updateProgress = (current: number, total: number) => ({
75
+ type: "UPDATE_PROGRESS" as const,
76
+ payload: { current, total },
77
+ });
78
+
79
+ export const setScrollOffset = (offset: number) => ({
80
+ type: "SET_SCROLL_OFFSET" as const,
81
+ payload: offset,
82
+ });
83
+
84
+ export const updateProject = (id: string, updates: Partial<Project>) => ({
85
+ type: "UPDATE_PROJECT" as const,
86
+ payload: { id, updates },
87
+ });
88
+
89
+ // Unified view action creators
90
+ export const setViewMode = (mode: ViewMode) => ({
91
+ type: "SET_VIEW_MODE" as const,
92
+ payload: mode,
93
+ });
94
+
95
+ export const setGitHubRepos = (repos: GitHubRepoInfo[]) => ({
96
+ type: "SET_GITHUB_REPOS" as const,
97
+ payload: repos,
98
+ });
99
+
100
+ export const setUnifiedRepos = (repos: UnifiedRepo[]) => ({
101
+ type: "SET_UNIFIED_REPOS" as const,
102
+ payload: repos,
103
+ });
104
+
105
+ export const setGitHubLoading = (loading: boolean) => ({
106
+ type: "SET_GITHUB_LOADING" as const,
107
+ payload: loading,
108
+ });
109
+
110
+ export const setGitHubError = (error: string | null) => ({
111
+ type: "SET_GITHUB_ERROR" as const,
112
+ payload: error,
113
+ });
114
+
115
+ export const showCloneDialog = (repos: UnifiedRepo[], directories: any[], selectedDirIndex: number, useSSH: boolean) => ({
116
+ type: "SHOW_CLONE_DIALOG" as const,
117
+ payload: { repos, directories, selectedDirIndex, useSSH },
118
+ });
119
+
120
+ export const hideCloneDialog = () => ({
121
+ type: "HIDE_CLONE_DIALOG" as const,
122
+ });
123
+
124
+ export const updateCloneDialog = (updates: Partial<{ selectedDirIndex: number; useSSH: boolean }>) => ({
125
+ type: "UPDATE_CLONE_DIALOG" as const,
126
+ payload: updates,
127
+ });
128
+
129
+ export const cloneRepoStart = (repoId: string) => ({
130
+ type: "CLONE_REPO_START" as const,
131
+ payload: repoId,
132
+ });
133
+
134
+ export const cloneRepoComplete = (id: string, localPath: string) => ({
135
+ type: "CLONE_REPO_COMPLETE" as const,
136
+ payload: { id, localPath },
137
+ });
138
+
139
+ export const cloneRepoFailed = (id: string, error: string) => ({
140
+ type: "CLONE_REPO_FAILED" as const,
141
+ payload: { id, error },
142
+ });
143
+
144
+ // Quick filter actions
145
+ export const setQuickFilter = (filter: QuickFilter) => ({
146
+ type: "SET_QUICK_FILTER" as const,
147
+ payload: filter,
148
+ });
149
+
150
+ // Refreshing state actions
151
+ export const setRefreshing = (refreshing: boolean) => ({
152
+ type: "SET_REFRESHING" as const,
153
+ payload: refreshing,
154
+ });
155
+
156
+ // Detail modal actions
157
+ export const showDetailModal = (modal: DetailModalState) => ({
158
+ type: "SHOW_DETAIL_MODAL" as const,
159
+ payload: modal,
160
+ });
161
+
162
+ export const hideDetailModal = () => ({
163
+ type: "HIDE_DETAIL_MODAL" as const,
164
+ });
165
+
166
+ export const updateDetailModal = (updates: Partial<DetailModalState>) => ({
167
+ type: "UPDATE_DETAIL_MODAL" as const,
168
+ payload: updates,
169
+ });
170
+
171
+ // Language filter action
172
+ export const setLanguageFilter = (language: string | null) => ({
173
+ type: "SET_LANGUAGE_FILTER" as const,
174
+ payload: language,
175
+ });
@@ -0,0 +1,294 @@
1
+ import type {
2
+ UnifiedAppState,
3
+ UnifiedAppAction,
4
+ SortField
5
+ } from "../types/index.ts";
6
+
7
+ /**
8
+ * Initial app state
9
+ */
10
+ export const initialState: UnifiedAppState = {
11
+ projects: [],
12
+ isLoading: true,
13
+ error: null,
14
+ message: null,
15
+ cursorIndex: 0,
16
+ selectedIndices: new Set(),
17
+ scrollOffset: 0,
18
+ filterText: "",
19
+ quickFilter: "all",
20
+ sortBy: "status",
21
+ sortDirection: "desc",
22
+ mode: "normal",
23
+ actionInProgress: null,
24
+ actionProgress: null,
25
+ confirmDialog: null,
26
+ // NEW unified view fields:
27
+ viewMode: "combined",
28
+ githubRepos: [],
29
+ unifiedRepos: [],
30
+ isLoadingGitHub: false,
31
+ githubError: null,
32
+ isRefreshing: false,
33
+ cloneDialog: null,
34
+ detailModal: null,
35
+ languageFilter: null,
36
+ };
37
+
38
+ /**
39
+ * Sort field cycle order (matches column order)
40
+ */
41
+ export const SORT_FIELDS: SortField[] = [
42
+ "status",
43
+ "name",
44
+ "branch",
45
+ "sync",
46
+ "language",
47
+ "stars",
48
+ "forks",
49
+ "lastActivity",
50
+ "size",
51
+ ];
52
+
53
+ /**
54
+ * App state reducer
55
+ */
56
+ export function appReducer(state: UnifiedAppState, action: UnifiedAppAction): UnifiedAppState {
57
+ switch (action.type) {
58
+ case "SET_PROJECTS":
59
+ return {
60
+ ...state,
61
+ projects: action.payload,
62
+ // Reset cursor if needed
63
+ cursorIndex: Math.min(state.cursorIndex, Math.max(0, action.payload.length - 1)),
64
+ };
65
+
66
+ case "SET_LOADING":
67
+ return { ...state, isLoading: action.payload };
68
+
69
+ case "SET_ERROR":
70
+ return { ...state, error: action.payload };
71
+
72
+ case "SET_MESSAGE":
73
+ return { ...state, message: action.payload };
74
+
75
+ case "MOVE_CURSOR": {
76
+ // Use unifiedRepos length for cursor bounds (unified view is now primary)
77
+ const maxIndex = state.unifiedRepos.length - 1;
78
+ return {
79
+ ...state,
80
+ cursorIndex: Math.max(0, Math.min(action.payload, maxIndex)),
81
+ };
82
+ }
83
+
84
+ case "TOGGLE_SELECTION": {
85
+ const newSelected = new Set(state.selectedIndices);
86
+ if (newSelected.has(action.payload)) {
87
+ newSelected.delete(action.payload);
88
+ } else {
89
+ newSelected.add(action.payload);
90
+ }
91
+ return { ...state, selectedIndices: newSelected };
92
+ }
93
+
94
+ case "SELECT_ALL":
95
+ return {
96
+ ...state,
97
+ selectedIndices: new Set(state.unifiedRepos.map((_, i) => i)),
98
+ };
99
+
100
+ case "DESELECT_ALL":
101
+ return { ...state, selectedIndices: new Set() };
102
+
103
+ case "SET_FILTER":
104
+ return {
105
+ ...state,
106
+ filterText: action.payload,
107
+ cursorIndex: 0, // Reset cursor when filtering
108
+ };
109
+
110
+ case "SET_QUICK_FILTER":
111
+ return {
112
+ ...state,
113
+ quickFilter: action.payload,
114
+ cursorIndex: 0, // Reset cursor when filtering
115
+ selectedIndices: new Set(), // Clear selection
116
+ };
117
+
118
+ case "SET_SORT":
119
+ return {
120
+ ...state,
121
+ sortBy: action.payload.by,
122
+ sortDirection: action.payload.direction,
123
+ };
124
+
125
+ case "CYCLE_SORT": {
126
+ const currentIndex = SORT_FIELDS.indexOf(state.sortBy);
127
+ const nextIndex = (currentIndex + 1) % SORT_FIELDS.length;
128
+ return {
129
+ ...state,
130
+ sortBy: SORT_FIELDS[nextIndex]!,
131
+ };
132
+ }
133
+
134
+ case "SET_MODE":
135
+ return { ...state, mode: action.payload };
136
+
137
+ case "START_ACTION":
138
+ return { ...state, actionInProgress: action.payload, actionProgress: null };
139
+
140
+ case "END_ACTION":
141
+ return { ...state, actionInProgress: null, actionProgress: null };
142
+
143
+ case "UPDATE_PROGRESS":
144
+ return { ...state, actionProgress: action.payload };
145
+
146
+ case "SET_SCROLL_OFFSET":
147
+ return { ...state, scrollOffset: action.payload };
148
+
149
+ case "UPDATE_PROJECT": {
150
+ const { id, updates } = action.payload;
151
+ return {
152
+ ...state,
153
+ projects: state.projects.map((p) =>
154
+ p.id === id ? { ...p, ...updates } : p
155
+ ),
156
+ };
157
+ }
158
+
159
+ case "SHOW_CONFIRM_DIALOG":
160
+ return {
161
+ ...state,
162
+ mode: "confirm",
163
+ confirmDialog: action.payload,
164
+ };
165
+
166
+ case "HIDE_CONFIRM_DIALOG":
167
+ return {
168
+ ...state,
169
+ mode: "normal",
170
+ confirmDialog: null,
171
+ };
172
+
173
+ // New unified view actions
174
+ case "SET_VIEW_MODE":
175
+ return {
176
+ ...state,
177
+ viewMode: action.payload,
178
+ cursorIndex: 0, // Reset cursor when changing view mode
179
+ };
180
+
181
+ case "SET_GITHUB_REPOS":
182
+ return {
183
+ ...state,
184
+ githubRepos: action.payload,
185
+ };
186
+
187
+ case "SET_UNIFIED_REPOS":
188
+ return {
189
+ ...state,
190
+ unifiedRepos: action.payload,
191
+ };
192
+
193
+ case "SET_GITHUB_LOADING":
194
+ return {
195
+ ...state,
196
+ isLoadingGitHub: action.payload,
197
+ };
198
+
199
+ case "SET_GITHUB_ERROR":
200
+ return {
201
+ ...state,
202
+ githubError: action.payload,
203
+ };
204
+
205
+ case "SET_REFRESHING":
206
+ return {
207
+ ...state,
208
+ isRefreshing: action.payload,
209
+ };
210
+
211
+ case "SHOW_CLONE_DIALOG":
212
+ return {
213
+ ...state,
214
+ mode: "clone",
215
+ cloneDialog: action.payload,
216
+ };
217
+
218
+ case "HIDE_CLONE_DIALOG":
219
+ return {
220
+ ...state,
221
+ mode: "normal",
222
+ cloneDialog: null,
223
+ };
224
+
225
+ case "UPDATE_CLONE_DIALOG":
226
+ return {
227
+ ...state,
228
+ cloneDialog: state.cloneDialog
229
+ ? { ...state.cloneDialog, ...action.payload }
230
+ : null,
231
+ };
232
+
233
+ case "CLONE_REPO_START":
234
+ return {
235
+ ...state,
236
+ actionInProgress: `Cloning ${action.payload}...`,
237
+ actionProgress: null,
238
+ };
239
+
240
+ case "CLONE_REPO_COMPLETE": {
241
+ const { id, localPath } = action.payload;
242
+ return {
243
+ ...state,
244
+ unifiedRepos: state.unifiedRepos.map((repo) =>
245
+ repo.id === id
246
+ ? { ...repo, isCloned: true, localPath }
247
+ : repo
248
+ ),
249
+ actionInProgress: null,
250
+ actionProgress: null,
251
+ };
252
+ }
253
+
254
+ case "CLONE_REPO_FAILED":
255
+ return {
256
+ ...state,
257
+ error: `Failed to clone ${action.payload.id}: ${action.payload.error}`,
258
+ actionInProgress: null,
259
+ actionProgress: null,
260
+ };
261
+
262
+ case "SHOW_DETAIL_MODAL":
263
+ return {
264
+ ...state,
265
+ mode: "detail",
266
+ detailModal: action.payload,
267
+ };
268
+
269
+ case "HIDE_DETAIL_MODAL":
270
+ return {
271
+ ...state,
272
+ mode: "normal",
273
+ detailModal: null,
274
+ };
275
+
276
+ case "UPDATE_DETAIL_MODAL":
277
+ return {
278
+ ...state,
279
+ detailModal: state.detailModal
280
+ ? { ...state.detailModal, ...action.payload }
281
+ : null,
282
+ };
283
+
284
+ case "SET_LANGUAGE_FILTER":
285
+ return {
286
+ ...state,
287
+ languageFilter: action.payload,
288
+ cursorIndex: 0,
289
+ };
290
+
291
+ default:
292
+ return state;
293
+ }
294
+ }