gitforest 0.1.0 → 1.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 (184) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +24 -4
  3. package/src/components/onboarding/DirectoriesStep.tsx +19 -19
  4. package/src/github/auth.ts +3 -3
  5. package/src/utils/debug.ts +4 -4
  6. package/.bunignore +0 -7
  7. package/.github/workflows/ci.yml +0 -73
  8. package/CLAUDE.md +0 -111
  9. package/CONTRIBUTING.md +0 -145
  10. package/bun.lock +0 -267
  11. package/bunfig.toml +0 -15
  12. package/cli +0 -0
  13. package/docs/ai/IMPROVEMENT_PLAN.md +0 -341
  14. package/docs/ai/VERIFICATION_REPORT.md +0 -87
  15. package/docs/ai/architecture.md +0 -169
  16. package/docs/ai/checks/check-2025-12-02-tests.md +0 -40
  17. package/docs/ai/checks/check-2025-12-02.md +0 -55
  18. package/docs/ai/checks/test-verification-report.md +0 -85
  19. package/docs/ai/implementation-guide.md +0 -776
  20. package/docs/ai/research/gitty-codebase-analysis.md +0 -221
  21. package/docs/ai/tickets/GENERAL-sitrep.md +0 -30
  22. package/docs/ai/tickets/TASK-database-tests-sitrep.md +0 -25
  23. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +0 -28
  24. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +0 -28
  25. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +0 -24
  26. package/docs/ai/tickets/TASK-github-service-sitrep.md +0 -32
  27. package/docs/ai/tickets/TASK-github-token-sitrep.md +0 -51
  28. package/docs/ai/tickets/TASK-hascommits-sitrep.md +0 -35
  29. package/docs/ai/tickets/TASK-keybindings-sitrep.md +0 -26
  30. package/docs/ai/tickets/TASK-layout-sitrep.md +0 -25
  31. package/docs/ai/tickets/TASK-markdown-sitrep.md +0 -28
  32. package/docs/ai/tickets/TASK-project-item-sitrep.md +0 -79
  33. package/docs/ai/tickets/TASK-sitrep.md +0 -28
  34. package/docs/ai/tickets/TASK-state-sitrep.md +0 -26
  35. package/docs/ai/tickets/TASK-types-sitrep.md +0 -25
  36. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +0 -26
  37. package/docs/ai/tickets/TKT-001-sitrep.md +0 -24
  38. package/docs/ai/tickets/TKT-002-sitrep.md +0 -25
  39. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +0 -46
  40. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +0 -135
  41. package/docs/ai/tickets/TKT-003-sitrep.md +0 -26
  42. package/docs/ai/tickets/TKT-004-sitrep.md +0 -27
  43. package/docs/ai/tickets/TKT-005-sitrep.md +0 -25
  44. package/docs/ai/tickets/TKT-006-sitrep.md +0 -26
  45. package/docs/ai/tickets/TKT-007-sitrep.md +0 -30
  46. package/docs/ai/tickets/TKT-008-sitrep.md +0 -32
  47. package/docs/ai/tickets/TKT-009-sitrep.md +0 -27
  48. package/docs/ai/tickets/TKT-010-sitrep.md +0 -27
  49. package/docs/ai/tickets/TKT-011-sitrep.md +0 -26
  50. package/docs/ai/tickets/TKT-012-sitrep.md +0 -25
  51. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +0 -28
  52. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +0 -25
  53. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +0 -25
  54. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +0 -24
  55. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +0 -29
  56. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +0 -29
  57. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +0 -26
  58. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +0 -30
  59. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +0 -28
  60. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +0 -26
  61. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +0 -25
  62. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +0 -30
  63. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +0 -25
  64. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +0 -29
  65. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +0 -95
  66. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +0 -61
  67. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +0 -30
  68. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +0 -27
  69. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +0 -25
  70. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +0 -27
  71. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +0 -25
  72. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +0 -28
  73. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +0 -25
  74. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +0 -26
  75. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +0 -25
  76. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +0 -26
  77. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +0 -24
  78. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +0 -25
  79. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +0 -29
  80. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +0 -25
  81. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +0 -24
  82. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +0 -24
  83. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +0 -27
  84. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +0 -25
  85. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +0 -25
  86. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +0 -27
  87. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +0 -32
  88. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +0 -27
  89. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +0 -30
  90. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +0 -25
  91. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +0 -29
  92. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +0 -25
  93. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +0 -27
  94. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +0 -25
  95. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +0 -26
  96. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +0 -25
  97. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +0 -27
  98. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +0 -75
  99. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +0 -29
  100. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +0 -29
  101. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +0 -25
  102. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +0 -25
  103. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +0 -32
  104. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +0 -64
  105. package/docs/ai/tkt-001-fix-database-error.md +0 -217
  106. package/docs/ai/ui-enhancement-plan.md +0 -562
  107. package/test/integration/app.isolated.tsx +0 -240
  108. package/test/integration/cli-commands.test.ts +0 -287
  109. package/test/integration/cli-validation.test.ts +0 -264
  110. package/test/integration/git-operations.test.ts +0 -218
  111. package/test/integration/scanner.test.ts +0 -228
  112. package/test/preload.ts +0 -18
  113. package/test/unit/cli/commands.test.ts +0 -13
  114. package/test/unit/cli/formatters.test.ts +0 -1116
  115. package/test/unit/cli/github-commands.test.ts +0 -12
  116. package/test/unit/components/CloneDialog.test.tsx +0 -240
  117. package/test/unit/components/ColumnHeader.test.tsx +0 -128
  118. package/test/unit/components/CommandPalette.test.tsx +0 -355
  119. package/test/unit/components/ConfirmDialog.test.tsx +0 -111
  120. package/test/unit/components/ErrorBoundary.test.tsx +0 -139
  121. package/test/unit/components/FilterBar.test.tsx +0 -43
  122. package/test/unit/components/FilterOptionsOverlay.test.tsx +0 -197
  123. package/test/unit/components/HelpOverlay.test.tsx +0 -90
  124. package/test/unit/components/Layout.test.tsx +0 -328
  125. package/test/unit/components/MarkdownRenderer.test.tsx +0 -45
  126. package/test/unit/components/ProgressBar.test.tsx +0 -138
  127. package/test/unit/components/ProjectItem.test.tsx +0 -182
  128. package/test/unit/components/ProjectList.test.tsx +0 -311
  129. package/test/unit/components/RepoDetailModal.test.tsx +0 -445
  130. package/test/unit/components/StatusBar.test.tsx +0 -112
  131. package/test/unit/components/UnifiedProjectItem.test.tsx +0 -618
  132. package/test/unit/components/ViewModeIndicator.test.tsx +0 -137
  133. package/test/unit/components/test-utils.tsx +0 -63
  134. package/test/unit/config/loader.test.ts +0 -692
  135. package/test/unit/db/database.test.ts +0 -978
  136. package/test/unit/db/index.test.ts +0 -314
  137. package/test/unit/fixtures/setup.ts +0 -186
  138. package/test/unit/git/commands-untested.test.ts +0 -205
  139. package/test/unit/git/commands.test.ts +0 -269
  140. package/test/unit/git/operations.test.ts +0 -322
  141. package/test/unit/git/status.test.ts +0 -219
  142. package/test/unit/github/auth.test.ts +0 -317
  143. package/test/unit/github/cache.test.ts +0 -1028
  144. package/test/unit/github/cli.test.ts +0 -135
  145. package/test/unit/github/unified.test.ts +0 -1201
  146. package/test/unit/graceful-shutdown.test.ts +0 -83
  147. package/test/unit/hooks/useBackgroundFetch.test.tsx +0 -239
  148. package/test/unit/hooks/useConfirmDialogActions.test.tsx +0 -81
  149. package/test/unit/hooks/useKeyBindings.isolated.ts +0 -715
  150. package/test/unit/hooks/useProjects.test.tsx +0 -186
  151. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +0 -115
  152. package/test/unit/hooks/useUnifiedRepos.test.tsx +0 -177
  153. package/test/unit/mocks/config.ts +0 -109
  154. package/test/unit/mocks/git-service.ts +0 -274
  155. package/test/unit/mocks/github-service.ts +0 -250
  156. package/test/unit/mocks/index.ts +0 -72
  157. package/test/unit/mocks/project.ts +0 -148
  158. package/test/unit/mocks/state-mocks.ts +0 -187
  159. package/test/unit/mocks/unified.ts +0 -169
  160. package/test/unit/operations/batch.test.ts +0 -216
  161. package/test/unit/operations/commands.test.ts +0 -550
  162. package/test/unit/scanner/errors.test.ts +0 -297
  163. package/test/unit/scanner/index.test.ts +0 -1011
  164. package/test/unit/scanner/markers.test.ts +0 -150
  165. package/test/unit/scanner/submodules.test.ts +0 -99
  166. package/test/unit/services/git-errors.test.ts +0 -190
  167. package/test/unit/services/git.test.ts +0 -442
  168. package/test/unit/services/github-errors.test.ts +0 -293
  169. package/test/unit/services/github.test.ts +0 -200
  170. package/test/unit/state/actions.test.ts +0 -217
  171. package/test/unit/state/reducer.test.ts +0 -745
  172. package/test/unit/state/store.test.tsx +0 -711
  173. package/test/unit/types/commands.test.ts +0 -220
  174. package/test/unit/types/schema.test.ts +0 -179
  175. package/test/unit/utils/array.test.ts +0 -73
  176. package/test/unit/utils/debug.test.ts +0 -23
  177. package/test/unit/utils/errors.test.ts +0 -295
  178. package/test/unit/utils/markdown.test.ts +0 -163
  179. package/test/unit/utils/project-utils.test.ts +0 -756
  180. package/test/unit/utils/rate-limiter.test.ts +0 -256
  181. package/test/unit/utils/retry.test.ts +0 -165
  182. package/test/unit/utils/strip-ansi.ts +0 -13
  183. package/test/unit/utils/timeout.test.ts +0 -93
  184. package/tsconfig.json +0 -29
@@ -1,776 +0,0 @@
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.