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,776 @@
1
+ # Gitforest Implementation Guide - All Improvements
2
+
3
+ ## Phase 1: Critical Fixes (Week 1)
4
+
5
+ ### 1. TKT-001: Fix Database Error
6
+ **File:** `src/db/index.ts`
7
+ **Issue:** Missing `await` in clearCache function
8
+
9
+ ```typescript
10
+ // Current (broken)
11
+ export async function clearCache(): Promise<void> {
12
+ const database = await initDb();
13
+ database.delete(schema.projects).all(); // Missing await
14
+ database.delete(schema.remoteStatus).all(); // Missing await
15
+ }
16
+
17
+ // Fixed
18
+ export async function clearCache(): Promise<void> {
19
+ try {
20
+ const database = await initDb();
21
+
22
+ // Run in parallel for better performance
23
+ await Promise.all([
24
+ database.delete(schema.projects).all(),
25
+ database.delete(schema.remoteStatus).all()
26
+ ]);
27
+ } catch (error) {
28
+ console.error('Failed to clear cache:', error);
29
+ throw new Error(`Cache clear failed: ${error instanceof Error ? error.message : String(error)}`);
30
+ }
31
+ }
32
+ ```
33
+
34
+ ### 2. TKT-002: Add Graceful Shutdown
35
+ **File:** `index.tsx`
36
+
37
+ ```typescript
38
+ // Add at top level
39
+ let isShuttingDown = false;
40
+
41
+ async function gracefulShutdown(signal: string): Promise<void> {
42
+ if (isShuttingDown) return;
43
+ isShuttingDown = true;
44
+
45
+ console.log(`\nReceived ${signal}, shutting down gracefully...`);
46
+
47
+ try {
48
+ // Close database
49
+ closeDb();
50
+
51
+ // Exit cleanly
52
+ process.exit(0);
53
+ } catch (error) {
54
+ console.error('Error during shutdown:', error);
55
+ process.exit(1);
56
+ }
57
+ }
58
+
59
+ // In main function, add before try-catch
60
+ process.on('SIGINT', () => gracefulShutdown('SIGINT'));
61
+ process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
62
+
63
+ // Also handle uncaught exceptions
64
+ process.on('uncaughtException', (error) => {
65
+ console.error('Uncaught Exception:', error);
66
+ gracefulShutdown('uncaughtException');
67
+ });
68
+
69
+ process.on('unhandledRejection', (reason, promise) => {
70
+ console.error('Unhandled Rejection at:', promise, 'reason:', reason);
71
+ gracefulShutdown('unhandledRejection');
72
+ });
73
+ ```
74
+
75
+ ### 3. TKT-012: Add GitHub Token Security
76
+ **File:** `src/services/github.ts`
77
+
78
+ ```typescript
79
+ // Add validation function
80
+ function validateToken(token: string): boolean {
81
+ // GitHub tokens are typically 40 characters (classic) or start with 'ghp_', 'github_pat_' (fine-grained)
82
+ const patterns = [
83
+ /^ghp_[a-zA-Z0-9]{36}$/, // Personal access tokens
84
+ /^github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}$/, // Fine-grained tokens
85
+ /^[a-zA-Z0-9]{40}$/ // Classic tokens
86
+ ];
87
+
88
+ return patterns.some(pattern => pattern.test(token));
89
+ }
90
+
91
+ // Update getToken method
92
+ getToken(): string | null {
93
+ const token = process.env.GITHUB_TOKEN ?? process.env.GH_TOKEN ?? null;
94
+
95
+ if (token && !validateToken(token)) {
96
+ console.warn('Warning: GITHUB_TOKEN format appears invalid');
97
+ }
98
+
99
+ return token;
100
+ }
101
+
102
+ // Add token permission check (optional, requires additional API call)
103
+ async function checkTokenPermissions(token: string): Promise<void> {
104
+ try {
105
+ const response = await fetch('https://api.github.com/user', {
106
+ headers: {
107
+ 'Authorization': `Bearer ${token}`,
108
+ 'Accept': 'application/vnd.github+json'
109
+ }
110
+ });
111
+
112
+ if (response.ok) {
113
+ const scopes = response.headers.get('X-OAuth-Scopes');
114
+ if (scopes && scopes.includes('delete_repo')) {
115
+ console.warn('Warning: Token has delete_repo permission which is potentially dangerous');
116
+ }
117
+ }
118
+ } catch {
119
+ // Silently fail - this is just a warning
120
+ }
121
+ }
122
+ ```
123
+
124
+ ## Phase 2: Core Reliability (Week 2)
125
+
126
+ ### 4. TKT-003: Remove Deprecated Functions
127
+ **Check if batch operations exist:**
128
+
129
+ ```bash
130
+ # Check if operations/batch.ts exists
131
+ ls -la src/operations/
132
+ ```
133
+
134
+ **If batch operations exist:**
135
+ ```typescript
136
+ // Update imports in useKeyBindings.ts
137
+ // Replace:
138
+ import { pullAllProjects, pushProjects, fetchAllProjects } from "../git/operations.ts";
139
+
140
+ // With:
141
+ import { batchPull, batchPush, batchFetch } from "../operations/batch.ts";
142
+ ```
143
+
144
+ **If not, remove deprecation warnings:**
145
+ ```typescript
146
+ // In git/operations.ts, remove @deprecated comments
147
+ export async function pullAllProjects(...) {
148
+ // Remove: @deprecated Use batchPull from operations/batch.ts instead
149
+ }
150
+ ```
151
+
152
+ ### 5. TKT-004: Add Retry Logic
153
+ **Create new file:** `src/utils/retry.ts`
154
+
155
+ ```typescript
156
+ export class RetryError extends Error {
157
+ constructor(message: string, public attempts: number, public lastError: Error) {
158
+ super(message);
159
+ this.name = 'RetryError';
160
+ }
161
+ }
162
+
163
+ export interface RetryOptions {
164
+ maxAttempts?: number;
165
+ initialDelay?: number;
166
+ maxDelay?: number;
167
+ backoffFactor?: number;
168
+ shouldRetry?: (error: Error, attempt: number) => boolean;
169
+ onRetry?: (error: Error, attempt: number, nextDelay: number) => void;
170
+ }
171
+
172
+ export async function withRetry<T>(
173
+ fn: () => Promise<T>,
174
+ options: RetryOptions = {}
175
+ ): Promise<T> {
176
+ const {
177
+ maxAttempts = 3,
178
+ initialDelay = 1000,
179
+ maxDelay = 30000,
180
+ backoffFactor = 2,
181
+ shouldRetry = () => true,
182
+ onRetry
183
+ } = options;
184
+
185
+ let lastError: Error;
186
+
187
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
188
+ try {
189
+ return await fn();
190
+ } catch (error) {
191
+ lastError = error as Error;
192
+
193
+ if (attempt === maxAttempts || !shouldRetry(lastError, attempt)) {
194
+ throw new RetryError(
195
+ `Failed after ${attempt} attempts: ${lastError.message}`,
196
+ attempt,
197
+ lastError
198
+ );
199
+ }
200
+
201
+ const delay = Math.min(
202
+ initialDelay * Math.pow(backoffFactor, attempt - 1),
203
+ maxDelay
204
+ );
205
+
206
+ onRetry?.(lastError, attempt, delay);
207
+
208
+ await new Promise(resolve => setTimeout(resolve, delay));
209
+ }
210
+ }
211
+
212
+ throw lastError!;
213
+ }
214
+
215
+ // Specific retry logic for GitHub API
216
+ export function shouldRetryGitHubAPI(error: Error, attempt: number): boolean {
217
+ // Don't retry client errors (4xx) except 429 (rate limit)
218
+ if (error instanceof GitHubAPIError) {
219
+ if (error.status === 429) return true; // Rate limit - retry
220
+ if (error.status >= 400 && error.status < 500) return false; // Client error - don't retry
221
+ if (error.status >= 500) return true; // Server error - retry
222
+ }
223
+
224
+ // Network errors - retry
225
+ if (error.message.includes('network') || error.message.includes('fetch')) {
226
+ return true;
227
+ }
228
+
229
+ return attempt < 3; // Default retry for unknown errors
230
+ }
231
+ ```
232
+
233
+ **Update GitHub service to use retry:**
234
+ ```typescript
235
+ // In services/github.ts
236
+ async function githubFetch<T>(endpoint: string, token: string): Promise<T> {
237
+ return withRetry(
238
+ async () => {
239
+ if (!token) {
240
+ throw new GitHubAPIError("GITHUB_TOKEN not set", 401);
241
+ }
242
+
243
+ const url = endpoint.startsWith("https://")
244
+ ? endpoint
245
+ : `https://api.github.com${endpoint}`;
246
+
247
+ const response = await fetch(url, {
248
+ headers: {
249
+ Accept: "application/vnd.github+json",
250
+ Authorization: `Bearer ${token}`,
251
+ "X-GitHub-Api-Version": "2022-11-28",
252
+ },
253
+ });
254
+
255
+ if (!response.ok) {
256
+ const error = await response.json().catch(() => ({}));
257
+ throw new GitHubAPIError(
258
+ `GitHub API error: ${response.statusText}`,
259
+ response.status,
260
+ error
261
+ );
262
+ }
263
+
264
+ return response.json() as Promise<T>;
265
+ },
266
+ {
267
+ shouldRetry: shouldRetryGitHubAPI,
268
+ onRetry: (error, attempt, delay) => {
269
+ console.log(`GitHub API retry attempt ${attempt} after ${delay}ms: ${error.message}`);
270
+ }
271
+ }
272
+ );
273
+ }
274
+ ```
275
+
276
+ ### 6. TKT-005: Convert to Async Operations
277
+ **File:** `src/scanner/index.ts`
278
+
279
+ ```typescript
280
+ // Change imports at top
281
+ import { readdir, stat } from 'fs/promises';
282
+
283
+ // Update scanDirectory function
284
+ async function scanDirectory(
285
+ dirPath: string,
286
+ config: GitforestConfig,
287
+ depth: number,
288
+ maxDepth: number,
289
+ foundProjects: Project[],
290
+ processedPaths: Set<string>
291
+ ): Promise<void> {
292
+ if (depth > maxDepth) return;
293
+ if (processedPaths.has(dirPath)) return;
294
+ processedPaths.add(dirPath);
295
+ if (!existsSync(dirPath)) return;
296
+
297
+ const isGit = await isGitRepo(dirPath);
298
+
299
+ if (isGit) {
300
+ const project = await createProject(dirPath, "git");
301
+ foundProjects.push(project);
302
+
303
+ if (config.display.showSubmodules) {
304
+ const submodulePaths = await findSubmodules(dirPath);
305
+ for (const subPath of submodulePaths) {
306
+ if (!processedPaths.has(subPath)) {
307
+ const subProject = await createProject(subPath, "git-submodule");
308
+ foundProjects.push(subProject);
309
+ processedPaths.add(subPath);
310
+ }
311
+ }
312
+ }
313
+ return;
314
+ }
315
+
316
+ const marker = await detectProjectMarker(dirPath);
317
+ if (marker) {
318
+ const project = await createProject(dirPath, "non-git", marker);
319
+ foundProjects.push(project);
320
+ return;
321
+ }
322
+
323
+ // Use async readdir
324
+ try {
325
+ const entries = await readdir(dirPath);
326
+
327
+ for (const entry of entries) {
328
+ if (shouldIgnore(entry, config.scan.ignore, config.scan.includeHidden)) {
329
+ continue;
330
+ }
331
+
332
+ const entryPath = join(dirPath, entry);
333
+
334
+ try {
335
+ const entryStat = await stat(entryPath);
336
+ if (entryStat.isDirectory()) {
337
+ await scanDirectory(
338
+ entryPath,
339
+ config,
340
+ depth + 1,
341
+ maxDepth,
342
+ foundProjects,
343
+ processedPaths
344
+ );
345
+ }
346
+ } catch {
347
+ // Skip entries we can't stat
348
+ }
349
+ }
350
+ } catch {
351
+ // Skip directories we can't read
352
+ }
353
+ }
354
+ ```
355
+
356
+ ## Phase 3: Performance & Testing (Week 3)
357
+
358
+ ### 7. TKT-006: Add Rate Limiting
359
+ **Create:** `src/utils/rate-limiter.ts`
360
+
361
+ ```typescript
362
+ export class RateLimiter {
363
+ private queue: Array<() => void> = [];
364
+ private running = 0;
365
+
366
+ constructor(private maxConcurrent: number) {}
367
+
368
+ async acquire<T>(fn: () => Promise<T>): Promise<T> {
369
+ return new Promise<T>((resolve, reject) => {
370
+ this.queue.push(async () => {
371
+ try {
372
+ this.running++;
373
+ const result = await fn();
374
+ resolve(result);
375
+ } catch (error) {
376
+ reject(error);
377
+ } finally {
378
+ this.running--;
379
+ this.process();
380
+ }
381
+ });
382
+
383
+ this.process();
384
+ });
385
+ }
386
+
387
+ private process(): void {
388
+ if (this.running < this.maxConcurrent && this.queue.length > 0) {
389
+ const next = this.queue.shift();
390
+ if (next) next();
391
+ }
392
+ }
393
+ }
394
+
395
+ // Batch processor with rate limiting
396
+ export async function processInBatches<T, R>(
397
+ items: T[],
398
+ processor: (item: T) => Promise<R>,
399
+ options: {
400
+ batchSize?: number;
401
+ maxConcurrent?: number;
402
+ onProgress?: (completed: number, total: number) => void;
403
+ } = {}
404
+ ): Promise<R[]> {
405
+ const { batchSize = 10, maxConcurrent = 5, onProgress } = options;
406
+
407
+ const limiter = new RateLimiter(maxConcurrent);
408
+ const results: R[] = [];
409
+ let completed = 0;
410
+
411
+ // Process in batches
412
+ for (let i = 0; i < items.length; i += batchSize) {
413
+ const batch = items.slice(i, i + batchSize);
414
+
415
+ const batchResults = await Promise.all(
416
+ batch.map(item =>
417
+ limiter.acquire(async () => {
418
+ const result = await processor(item);
419
+ completed++;
420
+ onProgress?.(completed, items.length);
421
+ return result;
422
+ })
423
+ )
424
+ );
425
+
426
+ results.push(...batchResults);
427
+ }
428
+
429
+ return results;
430
+ }
431
+ ```
432
+
433
+ **Update git operations:**
434
+ ```typescript
435
+ // In git/operations.ts
436
+ import { processInBatches } from '../utils/rate-limiter';
437
+
438
+ export async function pullAllProjects(
439
+ projects: Project[],
440
+ concurrency = 5,
441
+ onProgress?: (completed: number, total: number) => void
442
+ ): Promise<BatchResult> {
443
+ const start = Date.now();
444
+ const gitProjects = projects.filter(p => p.type === "git" && p.status?.hasRemote);
445
+
446
+ const results = await processInBatches(
447
+ gitProjects,
448
+ p => pullProject(p.path),
449
+ {
450
+ batchSize: concurrency,
451
+ maxConcurrent: concurrency,
452
+ onProgress
453
+ }
454
+ );
455
+
456
+ const successful = results.filter(r => r.success).length;
457
+
458
+ return {
459
+ total: gitProjects.length,
460
+ successful,
461
+ failed: gitProjects.length - successful,
462
+ results,
463
+ duration: Date.now() - start,
464
+ };
465
+ }
466
+ ```
467
+
468
+ ### 8. TKT-009: Improve Test Coverage
469
+ **Create comprehensive tests:**
470
+
471
+ ```typescript
472
+ // test/hooks/useKeyBindings.test.tsx
473
+ import { describe, test, expect, beforeEach, afterEach } from 'bun:test';
474
+ import { renderHook, act } from '@testing-library/react';
475
+ import { useKeyBindings } from '../../src/hooks/useKeyBindings';
476
+ import { createMockProject } from '../mocks';
477
+
478
+ describe('useKeyBindings', () => {
479
+ let onRefresh: jest.Mock;
480
+ let mockConfig: any;
481
+
482
+ beforeEach(() => {
483
+ onRefresh = jest.fn().mockResolvedValue(undefined);
484
+ mockConfig = {
485
+ scan: { concurrency: 5 },
486
+ display: { sortBy: 'status', sortDirection: 'desc' }
487
+ };
488
+ });
489
+
490
+ afterEach(() => {
491
+ jest.clearAllMocks();
492
+ });
493
+
494
+ test('should handle push operation', async () => {
495
+ const { result } = renderHook(() =>
496
+ useKeyBindings({ config: mockConfig, onRefresh })
497
+ );
498
+
499
+ // Simulate 'p' key press
500
+ await act(async () => {
501
+ // Simulate keyboard event
502
+ const event = new KeyboardEvent('keypress', { key: 'p' });
503
+ window.dispatchEvent(event);
504
+ });
505
+
506
+ // Verify push was called
507
+ expect(onRefresh).toHaveBeenCalled();
508
+ });
509
+
510
+ test('should handle errors gracefully', async () => {
511
+ onRefresh.mockRejectedValue(new Error('Push failed'));
512
+
513
+ const { result } = renderHook(() =>
514
+ useKeyBindings({ config: mockConfig, onRefresh })
515
+ );
516
+
517
+ // Should not throw
518
+ await act(async () => {
519
+ const event = new KeyboardEvent('keypress', { key: 'p' });
520
+ window.dispatchEvent(event);
521
+ });
522
+
523
+ // Error should be handled internally
524
+ expect(onRefresh).toHaveBeenCalled();
525
+ });
526
+ });
527
+ ```
528
+
529
+ ### 9. TKT-010: Add CLI Validation
530
+ **Update argument parser:**
531
+
532
+ ```typescript
533
+ // index.tsx
534
+ function parseArgs(args: string[]): {
535
+ command: string | null;
536
+ flags: Record<string, boolean | string>;
537
+ positional: string[];
538
+ errors: string[];
539
+ } {
540
+ const flags: Record<string, boolean | string> = {};
541
+ const positional: string[] = [];
542
+ const errors: string[] = [];
543
+ let command: string | null = null;
544
+
545
+ // Track seen flags to detect duplicates
546
+ const seenFlags = new Set<string>();
547
+
548
+ for (let i = 0; i < args.length; i++) {
549
+ const arg = args[i]!;
550
+
551
+ if (arg.startsWith("--")) {
552
+ const key = arg.slice(2);
553
+
554
+ // Check for unknown flags
555
+ const knownFlags = [
556
+ 'init', 'help', 'json', 'verbose', 'filter', 'target', 'public',
557
+ 'local', 'combined', 'https', 'h', 'v', 'f', 't'
558
+ ];
559
+
560
+ if (!knownFlags.includes(key) && !knownFlags.includes(key.split('=')[0])) {
561
+ errors.push(`Unknown flag: --${key}`);
562
+ continue;
563
+ }
564
+
565
+ // Check for duplicates
566
+ if (seenFlags.has(key)) {
567
+ errors.push(`Duplicate flag: --${key}`);
568
+ continue;
569
+ }
570
+ seenFlags.add(key);
571
+
572
+ // Parse value
573
+ if (key === "filter" || key === "target") {
574
+ const nextArg = args[i + 1];
575
+ if (nextArg && !nextArg.startsWith("-")) {
576
+ flags[key] = nextArg;
577
+ i++;
578
+ } else {
579
+ errors.push(`Flag --${key} requires a value`);
580
+ }
581
+ } else {
582
+ flags[key] = true;
583
+ }
584
+ } else if (arg.startsWith("-") && arg.length === 2) {
585
+ // Handle short flags similarly...
586
+ }
587
+ }
588
+
589
+ return { command, flags, positional, errors };
590
+ }
591
+ ```
592
+
593
+ ## Phase 4: Documentation & Polish (Week 4)
594
+
595
+ ### 10. TKT-007: Extract Constants
596
+ **Create:** `src/constants.ts`
597
+
598
+ ```typescript
599
+ // UI Constants
600
+ export const UI = {
601
+ HEADER_HEIGHT: 6,
602
+ MIN_TERMINAL_HEIGHT: 24,
603
+ PROJECT_LIST_PADDING: 2,
604
+ STATUS_BAR_HEIGHT: 3,
605
+ FILTER_BAR_HEIGHT: 2,
606
+ } as const;
607
+
608
+ // API Constants
609
+ export const API = {
610
+ GITHUB_PAGE_SIZE: 100,
611
+ GITHUB_MAX_RETRIES: 3,
612
+ GITHUB_INITIAL_RETRY_DELAY: 1000,
613
+ GITHUB_MAX_RETRY_DELAY: 30000,
614
+ } as const;
615
+
616
+ // Scanner Constants
617
+ export const SCANNER = {
618
+ DEFAULT_MAX_DEPTH: 2,
619
+ DEFAULT_CONCURRENCY: 5,
620
+ PROJECT_ID_LENGTH: 12,
621
+ CACHE_TTL_SECONDS: 300,
622
+ } as const;
623
+
624
+ // Git Constants
625
+ export const GIT = {
626
+ DEFAULT_REMOTE: 'origin',
627
+ DEFAULT_BRANCH: 'main',
628
+ SUBMODULE_STATUS_REGEX: /^([-\+U ])([a-f0-9]{40}) (.+?)(?: \((.+)\))?$/,
629
+ } as const;
630
+ ```
631
+
632
+ ### 11. TKT-008: Add JSDoc
633
+ **Example for public API:**
634
+
635
+ ```typescript
636
+ /**
637
+ * Scan all configured directories for projects
638
+ * @param config - The gitforest configuration object
639
+ * @param options - Optional scanning options
640
+ * @param options.forceRefresh - Force a fresh scan even if cache is valid
641
+ * @param options.onProgress - Callback for scan progress updates
642
+ * @returns Promise resolving to array of found projects
643
+ * @throws {Error} If scanning fails due to permission errors
644
+ *
645
+ * @example
646
+ * ```typescript
647
+ * const projects = await scanWithCache(config, {
648
+ * forceRefresh: true,
649
+ * onProgress: (scanned, found) => console.log(`Found ${found} projects in ${scanned} directories`)
650
+ * });
651
+ * ```
652
+ */
653
+ export async function scanWithCache(
654
+ config: GitforestConfig,
655
+ options: {
656
+ forceRefresh?: boolean;
657
+ onProgress?: (scanned: number, found: number) => void;
658
+ } = {}
659
+ ): Promise<Project[]> {
660
+ // implementation...
661
+ }
662
+ ```
663
+
664
+ ### 12. TKT-011: Create Documentation
665
+ **Create:** `CONTRIBUTING.md`
666
+
667
+ ```markdown
668
+ # Contributing to Gitforest
669
+
670
+ ## Development Setup
671
+
672
+ 1. Install Bun runtime
673
+ 2. Clone the repository
674
+ 3. Run `bun install`
675
+ 4. Run `bun test` to verify setup
676
+
677
+ ## Architecture
678
+
679
+ ### Project Structure
680
+ - `src/cli/` - Command-line interface
681
+ - `src/components/` - React components for TUI
682
+ - `src/hooks/` - React hooks
683
+ - `src/state/` - State management
684
+ - `src/services/` - External service integrations
685
+ - `src/git/` - Git operations
686
+ - `src/scanner/` - Directory scanning
687
+ - `src/db/` - SQLite database
688
+
689
+ ### Key Patterns
690
+ - Service abstraction for testability
691
+ - Async/await for all I/O operations
692
+ - Error boundaries for graceful failure
693
+ - Rate limiting for batch operations
694
+
695
+ ## Testing
696
+ - Unit tests: `bun test`
697
+ - Integration tests: `bun test test/integration/`
698
+ - Coverage: `bun test --coverage`
699
+
700
+ ## Code Style
701
+ - TypeScript strict mode
702
+ - No unused variables or imports
703
+ - JSDoc for public APIs
704
+ - Error handling with specific error types
705
+ ```
706
+
707
+ ## Testing All Changes
708
+
709
+ Create a comprehensive test script:
710
+
711
+ ```bash
712
+ #!/bin/bash
713
+ # test-all.sh
714
+
715
+ echo "Running comprehensive tests..."
716
+
717
+ # Unit tests
718
+ echo "1. Running unit tests..."
719
+ bun test --coverage
720
+
721
+ # Integration tests
722
+ echo "2. Running integration tests..."
723
+ bun test test/integration/
724
+
725
+ # Type checking
726
+ echo "3. Type checking..."
727
+ bun run typecheck
728
+
729
+ # Linting
730
+ echo "4. Linting..."
731
+ bun run lint
732
+
733
+ # Test CLI commands
734
+ echo "5. Testing CLI commands..."
735
+ bun run index.tsx --help
736
+ bun run index.tsx cache status
737
+ bun run index.tsx list --json
738
+
739
+ # Test with sample config
740
+ echo "6. Testing with sample configuration..."
741
+ cp config/gitforest.example.yaml /tmp/test-config.yaml
742
+ bun run index.tsx --init
743
+
744
+ # Performance test
745
+ echo "7. Performance test..."
746
+ time bun run index.tsx list
747
+
748
+ echo "All tests completed!"
749
+ ```
750
+
751
+ ## Final Verification Checklist
752
+
753
+ - [ ] All database operations use proper async/await
754
+ - [ ] Graceful shutdown handles all signals
755
+ - [ ] No deprecated functions in use
756
+ - [ ] GitHub API has retry logic
757
+ - [ ] File operations are async
758
+ - [ ] Batch operations have rate limiting
759
+ - [ ] Test coverage >80%
760
+ - [ ] CLI validates all inputs
761
+ - [ ] Constants extracted from code
762
+ - [ ] Public APIs documented
763
+ - [ ] Documentation complete
764
+ - [ ] No TypeScript errors or warnings
765
+ - [ ] All tests pass
766
+ - [ ] Performance acceptable
767
+
768
+ ## Deployment Considerations
769
+
770
+ 1. **Backward Compatibility**: Ensure config format hasn't changed
771
+ 2. **Migration**: No database schema changes needed
772
+ 3. **Rollback**: Keep previous version tagged
773
+ 4. **Monitoring**: Add metrics for error rates and performance
774
+ 5. **Documentation**: Update README with new features
775
+
776
+ This completes the comprehensive improvement plan for the gitforest codebase.