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,216 @@
1
+ import React, { createContext, useContext, useReducer, type ReactNode } from "react";
2
+ import type {
3
+ AppState,
4
+ QuickFilter,
5
+ UnifiedAppState,
6
+ UnifiedAppAction,
7
+ UnifiedRepo
8
+ } from "../types/index.ts";
9
+ import { filterProjects, sortProjects } from "../utils/project-utils.ts";
10
+ import { filterByViewMode, filterUnifiedRepos, sortUnifiedRepos } from "../github/unified.ts";
11
+ import { appReducer, initialState } from "./reducer.ts";
12
+
13
+ /**
14
+ * Store context
15
+ */
16
+ interface StoreContextValue {
17
+ state: UnifiedAppState;
18
+ dispatch: React.Dispatch<UnifiedAppAction>;
19
+ }
20
+
21
+ export const StoreContext = createContext<StoreContextValue | null>(null);
22
+
23
+ /**
24
+ * Store provider component
25
+ */
26
+ export function StoreProvider({ children }: { children: ReactNode }) {
27
+ const [state, dispatch] = useReducer(appReducer, initialState);
28
+
29
+ return (
30
+ <StoreContext.Provider value={{ state, dispatch }}>
31
+ {children}
32
+ </StoreContext.Provider>
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Test store provider - accepts initial state and optional mock dispatch
38
+ * Used for testing components without mock.module()
39
+ */
40
+ export function TestStoreProvider({
41
+ children,
42
+ initialState: testInitialState,
43
+ dispatch: mockDispatch,
44
+ }: {
45
+ children: ReactNode;
46
+ initialState: UnifiedAppState;
47
+ dispatch?: React.Dispatch<UnifiedAppAction>;
48
+ }) {
49
+ const [state, realDispatch] = useReducer(appReducer, testInitialState);
50
+ const dispatch = mockDispatch ?? realDispatch;
51
+
52
+ return (
53
+ <StoreContext.Provider value={{ state, dispatch }}>
54
+ {children}
55
+ </StoreContext.Provider>
56
+ );
57
+ }
58
+
59
+ /**
60
+ * Hook to access the store
61
+ */
62
+ export function useStore(): StoreContextValue {
63
+ const context = useContext(StoreContext);
64
+ if (!context) {
65
+ throw new Error("useStore must be used within a StoreProvider");
66
+ }
67
+ return context;
68
+ }
69
+
70
+ /**
71
+ * Apply quick filter to projects
72
+ */
73
+ function applyQuickFilter(projects: AppState["projects"], quickFilter: QuickFilter) {
74
+ if (quickFilter === "all") return projects;
75
+
76
+ return projects.filter((p) => {
77
+ switch (quickFilter) {
78
+ case "dirty":
79
+ return p.status?.isDirty;
80
+ case "unpushed":
81
+ return p.status?.isAhead && p.status?.unpushedCommits > 0;
82
+ case "no-remote":
83
+ return p.type === "git" && !p.status?.hasRemote;
84
+ default:
85
+ return true;
86
+ }
87
+ });
88
+ }
89
+
90
+ /**
91
+ * Hook to get filtered and sorted projects
92
+ */
93
+ export function useFilteredProjects() {
94
+ const { state } = useStore();
95
+ const { projects, filterText, quickFilter, sortBy, sortDirection } = state;
96
+
97
+ let result = projects;
98
+
99
+ // Apply quick filter first
100
+ result = applyQuickFilter(result, quickFilter);
101
+
102
+ // Apply text filter
103
+ if (filterText) {
104
+ result = filterProjects(result, filterText);
105
+ }
106
+
107
+ // Apply sort
108
+ result = sortProjects(result, sortBy, sortDirection);
109
+
110
+ return result;
111
+ }
112
+
113
+ /**
114
+ * Hook to get selected projects
115
+ */
116
+ export function useSelectedProjects() {
117
+ const { state } = useStore();
118
+ const filteredProjects = useFilteredProjects();
119
+ const { selectedIndices, cursorIndex } = state;
120
+
121
+ // If nothing selected, return the project at cursor
122
+ if (selectedIndices.size === 0) {
123
+ const current = filteredProjects[cursorIndex];
124
+ return current ? [current] : [];
125
+ }
126
+
127
+ // Return selected projects (filter out undefined and properly type)
128
+ return [...selectedIndices]
129
+ .map((i) => filteredProjects[i])
130
+ .filter((p): p is NonNullable<typeof p> => p !== undefined);
131
+ }
132
+
133
+ /**
134
+ * Apply quick filter to unified repos
135
+ */
136
+ function applyQuickFilterToUnifiedRepos(repos: UnifiedRepo[], quickFilter: QuickFilter) {
137
+ if (quickFilter === "all") return repos;
138
+
139
+ return repos.filter((r) => {
140
+ switch (quickFilter) {
141
+ case "dirty":
142
+ return r.local?.status?.isDirty;
143
+ case "unpushed":
144
+ return r.local?.status?.isAhead && r.local?.status?.unpushedCommits > 0;
145
+ case "no-remote":
146
+ return r.local?.type === "git" && !r.local?.status?.hasRemote;
147
+ case "github-only":
148
+ return r.source === "github";
149
+ case "local-only":
150
+ return r.source === "local" || r.source === "both";
151
+ case "private":
152
+ return r.github?.isPrivate === true;
153
+ case "public":
154
+ return r.github?.isPrivate === false;
155
+ case "archived":
156
+ return r.github?.isArchived === true;
157
+ case "forks":
158
+ return r.github?.isFork === true;
159
+ default:
160
+ return true;
161
+ }
162
+ });
163
+ }
164
+
165
+ /**
166
+ * Hook to get filtered and sorted unified repos
167
+ */
168
+ export function useFilteredUnifiedRepos() {
169
+ const { state } = useStore();
170
+ const { unifiedRepos, viewMode, filterText, quickFilter, sortBy, sortDirection, languageFilter } = state;
171
+
172
+ let result = unifiedRepos;
173
+
174
+ // Apply view mode filter first
175
+ result = filterByViewMode(result, viewMode);
176
+
177
+ // Apply quick filter
178
+ result = applyQuickFilterToUnifiedRepos(result, quickFilter);
179
+
180
+ // Apply text filter
181
+ if (filterText) {
182
+ result = filterUnifiedRepos(result, filterText);
183
+ }
184
+
185
+ // Apply language filter
186
+ if (languageFilter) {
187
+ result = result.filter(r =>
188
+ r.github?.language?.toLowerCase() === languageFilter.toLowerCase()
189
+ );
190
+ }
191
+
192
+ // Apply sort
193
+ result = sortUnifiedRepos(result, sortBy, sortDirection);
194
+
195
+ return result;
196
+ }
197
+
198
+ /**
199
+ * Hook to get selected unified repos
200
+ */
201
+ export function useSelectedUnifiedRepos() {
202
+ const { state } = useStore();
203
+ const filteredRepos = useFilteredUnifiedRepos();
204
+ const { selectedIndices, cursorIndex } = state;
205
+
206
+ // If nothing selected, return the repo at cursor
207
+ if (selectedIndices.size === 0) {
208
+ const current = filteredRepos[cursorIndex];
209
+ return current ? [current] : [];
210
+ }
211
+
212
+ // Return selected repos (filter out undefined and properly type)
213
+ return [...selectedIndices]
214
+ .map((i) => filteredRepos[i])
215
+ .filter((r): r is NonNullable<typeof r> => r !== undefined);
216
+ }
@@ -0,0 +1,8 @@
1
+ // Re-export state types from main types file
2
+ export type {
3
+ AppState,
4
+ AppAction,
5
+ AppMode,
6
+ SortField,
7
+ SortDirection,
8
+ } from "../types/index.ts";
@@ -0,0 +1,383 @@
1
+ import { z } from "zod";
2
+
3
+ // ============================================================================
4
+ // Configuration Types
5
+ // ============================================================================
6
+
7
+ export const DirectoryConfigSchema = z.object({
8
+ path: z.string().min(1),
9
+ maxDepth: z.number().int().min(0).max(10).default(2),
10
+ label: z.string().optional(),
11
+ editor: z.string().optional(),
12
+ });
13
+
14
+ // Reserved keys that cannot be used for custom commands
15
+ export const RESERVED_KEYS = new Set([
16
+ // Navigation
17
+ "j", "k", "g", "G",
18
+ // Selection
19
+ " ", "a",
20
+ // View/Filter
21
+ "/", "s", "S", "F",
22
+ "0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
23
+ // Git operations
24
+ "p", "P", "f", "i",
25
+ // GitHub operations
26
+ "c", "C", "A", "D",
27
+ // General
28
+ "r", "?", "q",
29
+ // Modal actions (used in detail modal)
30
+ "o", "d",
31
+ // Command palette trigger
32
+ "x",
33
+ ]);
34
+
35
+ export const CommandConfigSchema = z.object({
36
+ name: z.string().min(1),
37
+ key: z.string().length(1).refine(
38
+ (key) => !RESERVED_KEYS.has(key),
39
+ (key) => ({ message: `Key '${key}' is reserved and cannot be used for custom commands` })
40
+ ),
41
+ command: z.string().min(1),
42
+ confirm: z.boolean().default(false),
43
+ background: z.boolean().default(false),
44
+ });
45
+
46
+ export type CommandConfig = z.infer<typeof CommandConfigSchema>;
47
+
48
+ export const GitforestConfigSchema = z.object({
49
+ directories: z.array(DirectoryConfigSchema).min(1),
50
+ editor: z.string().optional(), // Global editor setting
51
+ scan: z
52
+ .object({
53
+ ignore: z
54
+ .array(z.string())
55
+ .default(["node_modules", ".git", "vendor", "__pycache__", "target", "dist", "build"]),
56
+ includeHidden: z.boolean().default(false),
57
+ concurrency: z.number().int().min(1).max(20).default(5),
58
+ })
59
+ .default({}),
60
+ github: z
61
+ .object({
62
+ defaultVisibility: z.enum(["private", "public"]).default("private"),
63
+ })
64
+ .default({}),
65
+ display: z
66
+ .object({
67
+ showSubmodules: z.boolean().default(true),
68
+ showNonGitProjects: z.boolean().default(true), // Show non-git projects (folders with project markers but no .git)
69
+ sortBy: z.enum(["status", "name", "branch", "sync", "language", "stars", "forks", "lastActivity", "size"]).default("status"),
70
+ sortDirection: z.enum(["asc", "desc"]).default("desc"),
71
+ })
72
+ .default({}),
73
+ cache: z
74
+ .object({
75
+ ttlSeconds: z.number().int().positive().default(300),
76
+ githubTtlSeconds: z.number().int().positive().default(600),
77
+ enableBackgroundRefresh: z.boolean().default(true),
78
+ backgroundRefreshIntervalSeconds: z.number().int().positive().default(300),
79
+ })
80
+ .default({}),
81
+ commands: z
82
+ .array(CommandConfigSchema)
83
+ .default([])
84
+ .refine(
85
+ (commands) => {
86
+ const keys = commands.map((c) => c.key);
87
+ return new Set(keys).size === keys.length;
88
+ },
89
+ { message: "Duplicate command keys are not allowed" }
90
+ ),
91
+ });
92
+
93
+ export type DirectoryConfig = z.infer<typeof DirectoryConfigSchema>;
94
+ export type GitforestConfig = z.infer<typeof GitforestConfigSchema>;
95
+
96
+ // ============================================================================
97
+ // Project Types
98
+ // ============================================================================
99
+
100
+ export type ProjectType = "git" | "git-submodule" | "non-git";
101
+
102
+ export interface SubmoduleInfo {
103
+ parentPath: string;
104
+ relativePath: string;
105
+ configuredCommit: string;
106
+ currentCommit: string;
107
+ isInitialized: boolean;
108
+ }
109
+
110
+ export interface GitStatus {
111
+ // Working tree status
112
+ hasUnstagedChanges: boolean;
113
+ hasStagedChanges: boolean;
114
+ hasUntrackedFiles: boolean;
115
+ modifiedCount: number;
116
+ stagedCount: number;
117
+ untrackedCount: number;
118
+
119
+ // Sync status
120
+ currentBranch: string;
121
+ trackingBranch: string | null;
122
+ unpushedCommits: number;
123
+ unpulledCommits: number;
124
+
125
+ // Remote info
126
+ hasRemote: boolean;
127
+ remoteUrl: string | null;
128
+
129
+ // Activity timestamps
130
+ lastLocalCommit: Date | null;
131
+ lastRemoteActivity: Date | null;
132
+
133
+ // Repository state
134
+ hasCommits: boolean;
135
+
136
+ // Computed flags
137
+ isDirty: boolean;
138
+ isAhead: boolean;
139
+ isBehind: boolean;
140
+ isOutOfSync: boolean;
141
+ }
142
+
143
+ export interface Project {
144
+ id: string;
145
+ name: string;
146
+ path: string;
147
+ type: ProjectType;
148
+ projectMarker: string | null; // e.g., "package.json", "Cargo.toml"
149
+ status: GitStatus | null;
150
+ submodule: SubmoduleInfo | null;
151
+ lastScanned: Date;
152
+ lastModified: Date | null; // For non-git: most recent file modification time
153
+ }
154
+
155
+ // ============================================================================
156
+ // Project Markers
157
+ // ============================================================================
158
+
159
+ export const PROJECT_MARKERS: Record<string, string> = {
160
+ "package.json": "Node.js",
161
+ "Cargo.toml": "Rust",
162
+ "pyproject.toml": "Python",
163
+ "setup.py": "Python",
164
+ "go.mod": "Go",
165
+ "Gemfile": "Ruby",
166
+ "pom.xml": "Java (Maven)",
167
+ "build.gradle": "Java (Gradle)",
168
+ "composer.json": "PHP",
169
+ "mix.exs": "Elixir",
170
+ "pubspec.yaml": "Dart/Flutter",
171
+ "CMakeLists.txt": "C/C++",
172
+ "Makefile": "Make",
173
+ "flake.nix": "Nix",
174
+ "deno.json": "Deno",
175
+ "bun.lockb": "Bun",
176
+ };
177
+
178
+ // ============================================================================
179
+ // App State Types
180
+ // ============================================================================
181
+
182
+ export type SortField =
183
+ | "status"
184
+ | "name"
185
+ | "branch"
186
+ | "sync"
187
+ | "language"
188
+ | "stars"
189
+ | "forks"
190
+ | "lastActivity"
191
+ | "size";
192
+ export type SortDirection = "asc" | "desc";
193
+ export type AppMode = "normal" | "filter" | "action" | "help" | "confirm" | "clone" | "detail" | "filter-options" | "command-palette";
194
+
195
+ // Quick filter for status-based filtering (1=dirty, 2=unpushed, 3=no-remote, 0=all)
196
+ export type QuickFilter =
197
+ | "all" | "dirty" | "unpushed" | "no-remote"
198
+ | "github-only" | "local-only"
199
+ | "private" | "public" | "archived" | "forks";
200
+
201
+ export interface ConfirmDialogState {
202
+ operation: "setup" | "create" | "archive";
203
+ title: string;
204
+ message: string;
205
+ items: string[];
206
+ projectPaths: string[];
207
+ showVisibilityToggle: boolean;
208
+ }
209
+
210
+ export interface CloneDialogState {
211
+ repos: UnifiedRepo[];
212
+ directories: DirectoryConfig[];
213
+ selectedDirIndex: number;
214
+ useSSH: boolean;
215
+ }
216
+
217
+ export interface DetailModalState {
218
+ repo: UnifiedRepo;
219
+ readmeContent: string | null;
220
+ readmeLoading: boolean;
221
+ readmeError: string | null;
222
+ readmeScrollOffset: number;
223
+ }
224
+
225
+ export interface AppState {
226
+ // Data
227
+ projects: Project[];
228
+ isLoading: boolean;
229
+ error: string | null;
230
+ message: string | null;
231
+
232
+ // Selection
233
+ cursorIndex: number;
234
+ selectedIndices: Set<number>;
235
+ scrollOffset: number;
236
+
237
+ // Filtering & Sorting
238
+ filterText: string;
239
+ quickFilter: QuickFilter;
240
+ sortBy: SortField;
241
+ sortDirection: SortDirection;
242
+
243
+ // UI state
244
+ mode: AppMode;
245
+ actionInProgress: string | null;
246
+ actionProgress: { current: number; total: number } | null;
247
+ confirmDialog: ConfirmDialogState | null;
248
+ }
249
+
250
+ export type AppAction =
251
+ | { type: "SET_PROJECTS"; payload: Project[] }
252
+ | { type: "SET_LOADING"; payload: boolean }
253
+ | { type: "SET_ERROR"; payload: string | null }
254
+ | { type: "SET_MESSAGE"; payload: string | null }
255
+ | { type: "MOVE_CURSOR"; payload: number }
256
+ | { type: "TOGGLE_SELECTION"; payload: number }
257
+ | { type: "SELECT_ALL" }
258
+ | { type: "DESELECT_ALL" }
259
+ | { type: "SET_FILTER"; payload: string }
260
+ | { type: "SET_QUICK_FILTER"; payload: QuickFilter }
261
+ | { type: "SET_SORT"; payload: { by: SortField; direction: SortDirection } }
262
+ | { type: "CYCLE_SORT" }
263
+ | { type: "SET_MODE"; payload: AppMode }
264
+ | { type: "START_ACTION"; payload: string }
265
+ | { type: "END_ACTION" }
266
+ | { type: "UPDATE_PROGRESS"; payload: { current: number; total: number } }
267
+ | { type: "SET_SCROLL_OFFSET"; payload: number }
268
+ | { type: "UPDATE_PROJECT"; payload: { id: string; updates: Partial<Project> } }
269
+ | { type: "SHOW_CONFIRM_DIALOG"; payload: ConfirmDialogState }
270
+ | { type: "HIDE_CONFIRM_DIALOG" };
271
+
272
+ // ============================================================================
273
+ // Operation Results
274
+ // ============================================================================
275
+
276
+ export interface OperationResult {
277
+ success: boolean;
278
+ projectPath: string;
279
+ operation: string;
280
+ message?: string;
281
+ error?: string;
282
+ duration: number;
283
+ }
284
+
285
+ export interface BatchResult {
286
+ total: number;
287
+ successful: number;
288
+ failed: number;
289
+ results: OperationResult[];
290
+ duration: number;
291
+ }
292
+
293
+ // ============================================================================
294
+ // GitHub Types
295
+ // ============================================================================
296
+
297
+ export interface GitHubRepoInfo {
298
+ name: string;
299
+ fullName: string;
300
+ owner: string;
301
+ description: string | null;
302
+ htmlUrl: string;
303
+ sshUrl: string;
304
+ cloneUrl: string;
305
+ isPrivate: boolean;
306
+ isArchived: boolean;
307
+ isFork: boolean;
308
+ pushedAt: Date | null;
309
+ updatedAt: Date | null;
310
+ defaultBranch: string;
311
+ language: string | null;
312
+ size: number;
313
+ stargazersCount?: number;
314
+ forksCount?: number;
315
+ openIssuesCount?: number;
316
+ watchersCount?: number;
317
+ topics?: string[];
318
+ license?: string | null;
319
+ hasIssues?: boolean;
320
+ hasWiki?: boolean;
321
+ hasDiscussions?: boolean;
322
+ }
323
+
324
+ // ============================================================================
325
+ // Unified View Types (Local + GitHub)
326
+ // ============================================================================
327
+
328
+ export type ViewMode = "local" | "github" | "combined";
329
+ export type RepoSource = "local" | "github" | "both";
330
+
331
+ /**
332
+ * Unified item that can represent:
333
+ * - A local project (may or may not be on GitHub)
334
+ * - A GitHub repo (may or may not be cloned locally)
335
+ */
336
+ export interface UnifiedRepo {
337
+ id: string;
338
+ name: string;
339
+ source: RepoSource;
340
+
341
+ // Local project info (if exists locally)
342
+ local: Project | null;
343
+
344
+ // GitHub repo info (if exists on GitHub)
345
+ github: GitHubRepoInfo | null;
346
+
347
+ // Sync status between local and GitHub
348
+ isCloned: boolean;
349
+ isOnGitHub: boolean;
350
+ localPath: string | null;
351
+ }
352
+
353
+ // Extended AppState for unified view
354
+ export interface UnifiedAppState extends AppState {
355
+ viewMode: ViewMode;
356
+ githubRepos: GitHubRepoInfo[];
357
+ unifiedRepos: UnifiedRepo[];
358
+ isLoadingGitHub: boolean;
359
+ githubError: string | null;
360
+ isRefreshing: boolean; // Background refresh indicator
361
+ cloneDialog: CloneDialogState | null;
362
+ detailModal: DetailModalState | null;
363
+ languageFilter: string | null;
364
+ }
365
+
366
+ export type UnifiedAppAction =
367
+ | AppAction
368
+ | { type: "SET_VIEW_MODE"; payload: ViewMode }
369
+ | { type: "SET_GITHUB_REPOS"; payload: GitHubRepoInfo[] }
370
+ | { type: "SET_UNIFIED_REPOS"; payload: UnifiedRepo[] }
371
+ | { type: "SET_GITHUB_LOADING"; payload: boolean }
372
+ | { type: "SET_GITHUB_ERROR"; payload: string | null }
373
+ | { type: "SET_REFRESHING"; payload: boolean }
374
+ | { type: "SHOW_CLONE_DIALOG"; payload: CloneDialogState }
375
+ | { type: "HIDE_CLONE_DIALOG" }
376
+ | { type: "UPDATE_CLONE_DIALOG"; payload: Partial<CloneDialogState> }
377
+ | { type: "CLONE_REPO_START"; payload: string }
378
+ | { type: "CLONE_REPO_COMPLETE"; payload: { id: string; localPath: string } }
379
+ | { type: "CLONE_REPO_FAILED"; payload: { id: string; error: string } }
380
+ | { type: "SHOW_DETAIL_MODAL"; payload: DetailModalState }
381
+ | { type: "HIDE_DETAIL_MODAL" }
382
+ | { type: "UPDATE_DETAIL_MODAL"; payload: Partial<DetailModalState> }
383
+ | { type: "SET_LANGUAGE_FILTER"; payload: string | null };
@@ -0,0 +1,44 @@
1
+ export const palette = {
2
+ bg: {
3
+ base: "black",
4
+ surface: "black",
5
+ muted: "gray",
6
+ accent: "blue",
7
+ caution: "yellow",
8
+ success: "green",
9
+ danger: "red",
10
+ info: "cyan",
11
+ },
12
+ text: {
13
+ primary: "white",
14
+ muted: "gray",
15
+ subtle: "brightBlack",
16
+ info: "cyan",
17
+ warning: "yellow",
18
+ success: "green",
19
+ danger: "red",
20
+ },
21
+ badge: {
22
+ dirty: { fg: "yellow", bg: undefined },
23
+ push: { fg: "cyan", bg: undefined },
24
+ pull: { fg: "magenta", bg: undefined },
25
+ local: { fg: "yellow", bg: undefined },
26
+ ok: { fg: "green", bg: undefined },
27
+ github: { fg: "white", bg: undefined },
28
+ },
29
+ } as const;
30
+
31
+ export const spacing = {
32
+ xxs: 0,
33
+ xs: 1,
34
+ sm: 2,
35
+ md: 3,
36
+ } as const;
37
+
38
+ export const typography = {
39
+ header: { color: palette.text.info, bold: true },
40
+ label: { color: palette.text.muted, bold: true },
41
+ value: { color: palette.text.primary },
42
+ muted: { color: palette.text.subtle },
43
+ } as const;
44
+
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Array utility functions
3
+ */
4
+
5
+ /**
6
+ * Split an array into chunks of a given size
7
+ */
8
+ export function chunk<T>(arr: T[], size: number): T[][] {
9
+ const chunks: T[][] = [];
10
+ for (let i = 0; i < arr.length; i += size) {
11
+ chunks.push(arr.slice(i, i + size));
12
+ }
13
+ return chunks;
14
+ }
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Debug logging utility
3
+ * Enable debug output by setting GITTY_DEBUG=1 environment variable
4
+ */
5
+
6
+ const DEBUG = process.env.GITTY_DEBUG === "1" || process.env.GITTY_DEBUG === "true";
7
+
8
+ /**
9
+ * Log a debug message (only when GITTY_DEBUG is enabled)
10
+ */
11
+ export function debug(context: string, message: string, data?: unknown): void {
12
+ if (!DEBUG) return;
13
+
14
+ const timestamp = new Date().toISOString();
15
+ if (data !== undefined) {
16
+ console.error(`[gitforest ${timestamp}] [${context}] ${message}`, data);
17
+ } else {
18
+ console.error(`[gitforest ${timestamp}] [${context}] ${message}`);
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Log a debug error (only when GITTY_DEBUG is enabled)
24
+ */
25
+ export function debugError(context: string, message: string, error: unknown): void {
26
+ if (!DEBUG) return;
27
+
28
+ const timestamp = new Date().toISOString();
29
+ const errorMessage = error instanceof Error ? error.message : String(error);
30
+ console.error(`[gitforest ${timestamp}] [${context}] ERROR: ${message} - ${errorMessage}`);
31
+ }
32
+
33
+ /**
34
+ * Check if debug mode is enabled
35
+ */
36
+ export function isDebugEnabled(): boolean {
37
+ return DEBUG;
38
+ }