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.
- package/.bunignore +7 -0
- package/.github/workflows/ci.yml +73 -0
- package/CLAUDE.md +111 -0
- package/CONTRIBUTING.md +145 -0
- package/README.md +168 -0
- package/bun.lock +267 -0
- package/bunfig.toml +15 -0
- package/cli +0 -0
- package/config/gitforest.example.yaml +94 -0
- package/docs/ai/IMPROVEMENT_PLAN.md +341 -0
- package/docs/ai/VERIFICATION_REPORT.md +87 -0
- package/docs/ai/architecture.md +169 -0
- package/docs/ai/checks/check-2025-12-02-tests.md +40 -0
- package/docs/ai/checks/check-2025-12-02.md +55 -0
- package/docs/ai/checks/test-verification-report.md +85 -0
- package/docs/ai/implementation-guide.md +776 -0
- package/docs/ai/research/gitty-codebase-analysis.md +221 -0
- package/docs/ai/tickets/GENERAL-sitrep.md +30 -0
- package/docs/ai/tickets/TASK-database-tests-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-detail-modal-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +24 -0
- package/docs/ai/tickets/TASK-github-service-sitrep.md +32 -0
- package/docs/ai/tickets/TASK-github-token-sitrep.md +51 -0
- package/docs/ai/tickets/TASK-hascommits-sitrep.md +35 -0
- package/docs/ai/tickets/TASK-keybindings-sitrep.md +26 -0
- package/docs/ai/tickets/TASK-layout-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-markdown-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-project-item-sitrep.md +79 -0
- package/docs/ai/tickets/TASK-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-state-sitrep.md +26 -0
- package/docs/ai/tickets/TASK-types-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-001-sitrep.md +24 -0
- package/docs/ai/tickets/TKT-002-sitrep.md +25 -0
- package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +46 -0
- package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +135 -0
- package/docs/ai/tickets/TKT-003-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-004-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-005-sitrep.md +25 -0
- package/docs/ai/tickets/TKT-006-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-007-sitrep.md +30 -0
- package/docs/ai/tickets/TKT-008-sitrep.md +32 -0
- package/docs/ai/tickets/TKT-009-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-010-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-011-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-012-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +95 -0
- package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +61 -0
- package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +32 -0
- package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +75 -0
- package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +32 -0
- package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +64 -0
- package/docs/ai/tkt-001-fix-database-error.md +217 -0
- package/docs/ai/ui-enhancement-plan.md +562 -0
- package/package.json +50 -0
- package/src/app.tsx +43 -0
- package/src/cli/config.ts +94 -0
- package/src/cli/formatters.ts +632 -0
- package/src/cli/index.ts +583 -0
- package/src/components/CloneDialog.tsx +137 -0
- package/src/components/ColumnHeader.tsx +128 -0
- package/src/components/CommandPalette.tsx +120 -0
- package/src/components/ConfirmDialog.tsx +105 -0
- package/src/components/ErrorBoundary.tsx +128 -0
- package/src/components/FilterBar.tsx +71 -0
- package/src/components/FilterOptionsOverlay.tsx +131 -0
- package/src/components/HelpOverlay.tsx +120 -0
- package/src/components/Layout.tsx +379 -0
- package/src/components/MarkdownRenderer.tsx +127 -0
- package/src/components/ProgressBar.tsx +53 -0
- package/src/components/ProjectItem.tsx +143 -0
- package/src/components/ProjectList.tsx +90 -0
- package/src/components/RepoDetailModal.tsx +367 -0
- package/src/components/StatusBar.tsx +188 -0
- package/src/components/UnifiedProjectItem.tsx +436 -0
- package/src/components/ViewModeIndicator.tsx +37 -0
- package/src/components/onboarding/CompleteStep.tsx +82 -0
- package/src/components/onboarding/DirectoriesStep.test.tsx +52 -0
- package/src/components/onboarding/DirectoriesStep.tsx +847 -0
- package/src/components/onboarding/DirectoriesStep.unit.test.ts +345 -0
- package/src/components/onboarding/GitHubAuthStep.tsx +268 -0
- package/src/components/onboarding/OnboardingWizard.tsx +130 -0
- package/src/components/onboarding/WelcomeStep.tsx +69 -0
- package/src/config/loader.ts +263 -0
- package/src/config/onboarding.ts +67 -0
- package/src/constants.ts +96 -0
- package/src/db/index.ts +147 -0
- package/src/db/schema.ts +70 -0
- package/src/git/commands.ts +283 -0
- package/src/git/index.ts +2 -0
- package/src/git/operations.ts +93 -0
- package/src/git/service.ts +539 -0
- package/src/git/status.ts +84 -0
- package/src/git/types.ts +5 -0
- package/src/github/auth.ts +311 -0
- package/src/github/cache.ts +231 -0
- package/src/github/cli.ts +22 -0
- package/src/github/unified.ts +415 -0
- package/src/hooks/useBackgroundFetch.ts +76 -0
- package/src/hooks/useConfirmDialogActions.ts +120 -0
- package/src/hooks/useKeyBindings.ts +656 -0
- package/src/hooks/useProjects.ts +47 -0
- package/src/hooks/useUnifiedRepos.ts +317 -0
- package/src/index.tsx +494 -0
- package/src/operations/batch.ts +280 -0
- package/src/operations/commands.ts +140 -0
- package/src/operations/index.ts +37 -0
- package/src/scanner/index.ts +424 -0
- package/src/scanner/markers.ts +43 -0
- package/src/scanner/submodules.ts +61 -0
- package/src/services/git.ts +484 -0
- package/src/services/github.ts +676 -0
- package/src/services/index.ts +28 -0
- package/src/services/types.ts +99 -0
- package/src/state/actions.ts +175 -0
- package/src/state/reducer.ts +294 -0
- package/src/state/store.tsx +216 -0
- package/src/state/types.ts +8 -0
- package/src/types/index.ts +383 -0
- package/src/ui/theme.ts +44 -0
- package/src/utils/array.ts +14 -0
- package/src/utils/debug.ts +38 -0
- package/src/utils/errors.ts +17 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/markdown.ts +230 -0
- package/src/utils/project-utils.ts +129 -0
- package/src/utils/rate-limiter.ts +134 -0
- package/src/utils/retry.ts +147 -0
- package/src/utils/timeout.ts +56 -0
- package/test/integration/app.isolated.tsx +240 -0
- package/test/integration/cli-commands.test.ts +287 -0
- package/test/integration/cli-validation.test.ts +264 -0
- package/test/integration/git-operations.test.ts +218 -0
- package/test/integration/scanner.test.ts +228 -0
- package/test/preload.ts +18 -0
- package/test/unit/cli/commands.test.ts +13 -0
- package/test/unit/cli/formatters.test.ts +1116 -0
- package/test/unit/cli/github-commands.test.ts +12 -0
- package/test/unit/components/CloneDialog.test.tsx +240 -0
- package/test/unit/components/ColumnHeader.test.tsx +128 -0
- package/test/unit/components/CommandPalette.test.tsx +355 -0
- package/test/unit/components/ConfirmDialog.test.tsx +111 -0
- package/test/unit/components/ErrorBoundary.test.tsx +139 -0
- package/test/unit/components/FilterBar.test.tsx +43 -0
- package/test/unit/components/FilterOptionsOverlay.test.tsx +197 -0
- package/test/unit/components/HelpOverlay.test.tsx +90 -0
- package/test/unit/components/Layout.test.tsx +328 -0
- package/test/unit/components/MarkdownRenderer.test.tsx +45 -0
- package/test/unit/components/ProgressBar.test.tsx +138 -0
- package/test/unit/components/ProjectItem.test.tsx +182 -0
- package/test/unit/components/ProjectList.test.tsx +311 -0
- package/test/unit/components/RepoDetailModal.test.tsx +445 -0
- package/test/unit/components/StatusBar.test.tsx +112 -0
- package/test/unit/components/UnifiedProjectItem.test.tsx +618 -0
- package/test/unit/components/ViewModeIndicator.test.tsx +137 -0
- package/test/unit/components/test-utils.tsx +63 -0
- package/test/unit/config/loader.test.ts +692 -0
- package/test/unit/db/database.test.ts +978 -0
- package/test/unit/db/index.test.ts +314 -0
- package/test/unit/fixtures/setup.ts +186 -0
- package/test/unit/git/commands-untested.test.ts +205 -0
- package/test/unit/git/commands.test.ts +269 -0
- package/test/unit/git/operations.test.ts +322 -0
- package/test/unit/git/status.test.ts +219 -0
- package/test/unit/github/auth.test.ts +317 -0
- package/test/unit/github/cache.test.ts +1028 -0
- package/test/unit/github/cli.test.ts +135 -0
- package/test/unit/github/unified.test.ts +1201 -0
- package/test/unit/graceful-shutdown.test.ts +83 -0
- package/test/unit/hooks/useBackgroundFetch.test.tsx +239 -0
- package/test/unit/hooks/useConfirmDialogActions.test.tsx +81 -0
- package/test/unit/hooks/useKeyBindings.isolated.ts +715 -0
- package/test/unit/hooks/useProjects.test.tsx +186 -0
- package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +115 -0
- package/test/unit/hooks/useUnifiedRepos.test.tsx +177 -0
- package/test/unit/mocks/config.ts +109 -0
- package/test/unit/mocks/git-service.ts +274 -0
- package/test/unit/mocks/github-service.ts +250 -0
- package/test/unit/mocks/index.ts +72 -0
- package/test/unit/mocks/project.ts +148 -0
- package/test/unit/mocks/state-mocks.ts +187 -0
- package/test/unit/mocks/unified.ts +169 -0
- package/test/unit/operations/batch.test.ts +216 -0
- package/test/unit/operations/commands.test.ts +550 -0
- package/test/unit/scanner/errors.test.ts +297 -0
- package/test/unit/scanner/index.test.ts +1011 -0
- package/test/unit/scanner/markers.test.ts +150 -0
- package/test/unit/scanner/submodules.test.ts +99 -0
- package/test/unit/services/git-errors.test.ts +190 -0
- package/test/unit/services/git.test.ts +442 -0
- package/test/unit/services/github-errors.test.ts +293 -0
- package/test/unit/services/github.test.ts +200 -0
- package/test/unit/state/actions.test.ts +217 -0
- package/test/unit/state/reducer.test.ts +745 -0
- package/test/unit/state/store.test.tsx +711 -0
- package/test/unit/types/commands.test.ts +220 -0
- package/test/unit/types/schema.test.ts +179 -0
- package/test/unit/utils/array.test.ts +73 -0
- package/test/unit/utils/debug.test.ts +23 -0
- package/test/unit/utils/errors.test.ts +295 -0
- package/test/unit/utils/markdown.test.ts +163 -0
- package/test/unit/utils/project-utils.test.ts +756 -0
- package/test/unit/utils/rate-limiter.test.ts +256 -0
- package/test/unit/utils/retry.test.ts +165 -0
- package/test/unit/utils/strip-ansi.ts +13 -0
- package/test/unit/utils/timeout.test.ts +93 -0
- 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.
|