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,280 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Batch operations orchestrator
|
|
3
|
+
* Uses service abstractions for testability
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { Project, BatchResult, OperationResult } from "../types/index.ts";
|
|
7
|
+
import type { GitService } from "../services/git.ts";
|
|
8
|
+
import { bunGitService } from "../services/git.ts";
|
|
9
|
+
import { chunk } from "../utils/array.ts";
|
|
10
|
+
import { processBatch } from "../utils/rate-limiter.ts";
|
|
11
|
+
import { GIT } from "../constants.ts";
|
|
12
|
+
|
|
13
|
+
export type ProgressCallback = (completed: number, total: number) => void;
|
|
14
|
+
|
|
15
|
+
export interface BatchOptions {
|
|
16
|
+
concurrency?: number;
|
|
17
|
+
minDelay?: number;
|
|
18
|
+
onProgress?: ProgressCallback;
|
|
19
|
+
gitService?: GitService;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Filter projects that can be pulled (git with remote)
|
|
24
|
+
*/
|
|
25
|
+
function getPullableProjects(projects: Project[]): Project[] {
|
|
26
|
+
return projects.filter((p) => p.type === "git" && p.status?.hasRemote);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Filter projects that need pushing (git with remote and ahead)
|
|
31
|
+
*/
|
|
32
|
+
function getPushableProjects(projects: Project[]): Project[] {
|
|
33
|
+
return projects.filter(
|
|
34
|
+
(p) => p.type === "git" && p.status?.hasRemote && p.status?.isAhead
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Filter projects that can be fetched (git with remote)
|
|
40
|
+
*/
|
|
41
|
+
function getFetchableProjects(projects: Project[]): Project[] {
|
|
42
|
+
return projects.filter((p) => p.type === "git" && p.status?.hasRemote);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pull changes for multiple projects with rate limiting
|
|
47
|
+
*
|
|
48
|
+
* Filters projects to only those that are git repos with remotes,
|
|
49
|
+
* then pulls changes in parallel with configurable concurrency.
|
|
50
|
+
*
|
|
51
|
+
* @param projects - Array of projects to pull
|
|
52
|
+
* @param options - Batch operation options
|
|
53
|
+
* @param options.concurrency - Maximum concurrent operations (default: 5)
|
|
54
|
+
* @param options.onProgress - Progress callback (completed, total)
|
|
55
|
+
* @param options.gitService - Git service implementation (for testing)
|
|
56
|
+
* @returns Promise resolving to batch result with success/failure counts
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* ```typescript
|
|
60
|
+
* const result = await batchPull(projects, {
|
|
61
|
+
* concurrency: 3,
|
|
62
|
+
* onProgress: (done, total) => console.log(`${done}/${total}`)
|
|
63
|
+
* });
|
|
64
|
+
* console.log(`Pulled ${result.successful}/${result.total} repos`);
|
|
65
|
+
* ```
|
|
66
|
+
*/
|
|
67
|
+
export async function batchPull(
|
|
68
|
+
projects: Project[],
|
|
69
|
+
options: BatchOptions = {}
|
|
70
|
+
): Promise<BatchResult> {
|
|
71
|
+
const { concurrency = GIT.DEFAULT_CONCURRENCY, minDelay = 0, onProgress, gitService = bunGitService } = options;
|
|
72
|
+
const start = Date.now();
|
|
73
|
+
const pullable = getPullableProjects(projects);
|
|
74
|
+
|
|
75
|
+
const batchResults = await processBatch(
|
|
76
|
+
pullable,
|
|
77
|
+
(p) => gitService.pull(p.path),
|
|
78
|
+
{
|
|
79
|
+
concurrency,
|
|
80
|
+
minDelay,
|
|
81
|
+
onProgress,
|
|
82
|
+
}
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Transform BatchItemResult<OperationResult>[] to OperationResult[]
|
|
86
|
+
const results: OperationResult[] = batchResults.map((batchResult, index) => {
|
|
87
|
+
if (batchResult.success) {
|
|
88
|
+
return batchResult.result!;
|
|
89
|
+
} else {
|
|
90
|
+
// Create a failed OperationResult for this project
|
|
91
|
+
const project = pullable[index]!;
|
|
92
|
+
return {
|
|
93
|
+
success: false,
|
|
94
|
+
projectPath: project.path,
|
|
95
|
+
operation: "pull",
|
|
96
|
+
error: batchResult.error?.message,
|
|
97
|
+
duration: 0,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
const successful = results.filter((r) => r.success).length;
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
total: pullable.length,
|
|
106
|
+
successful,
|
|
107
|
+
failed: pullable.length - successful,
|
|
108
|
+
results,
|
|
109
|
+
duration: Date.now() - start,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Push changes for multiple projects with rate limiting
|
|
115
|
+
*
|
|
116
|
+
* Filters projects to only those that are git repos with remotes and have unpushed commits,
|
|
117
|
+
* then pushes changes in parallel with configurable concurrency.
|
|
118
|
+
*
|
|
119
|
+
* @param projects - Array of projects to push
|
|
120
|
+
* @param options - Batch operation options
|
|
121
|
+
* @param options.concurrency - Maximum concurrent operations (default: 5)
|
|
122
|
+
* @param options.onProgress - Progress callback (completed, total)
|
|
123
|
+
* @param options.gitService - Git service implementation (for testing)
|
|
124
|
+
* @returns Promise resolving to batch result with success/failure counts
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```typescript
|
|
128
|
+
* const result = await batchPush(projects, {
|
|
129
|
+
* concurrency: 2,
|
|
130
|
+
* onProgress: (done, total) => console.log(`${done}/${total}`)
|
|
131
|
+
* });
|
|
132
|
+
* console.log(`Pushed ${result.successful}/${result.total} repos`);
|
|
133
|
+
* ```
|
|
134
|
+
*/
|
|
135
|
+
export async function batchPush(
|
|
136
|
+
projects: Project[],
|
|
137
|
+
options: BatchOptions = {}
|
|
138
|
+
): Promise<BatchResult> {
|
|
139
|
+
const { concurrency = GIT.DEFAULT_CONCURRENCY, minDelay = 0, onProgress, gitService = bunGitService } = options;
|
|
140
|
+
const start = Date.now();
|
|
141
|
+
const pushable = getPushableProjects(projects);
|
|
142
|
+
|
|
143
|
+
const batchResults = await processBatch(
|
|
144
|
+
pushable,
|
|
145
|
+
(p) => gitService.push(p.path),
|
|
146
|
+
{
|
|
147
|
+
concurrency,
|
|
148
|
+
minDelay,
|
|
149
|
+
onProgress,
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
// Transform BatchItemResult<OperationResult>[] to OperationResult[]
|
|
154
|
+
const results: OperationResult[] = batchResults.map((batchResult, index) => {
|
|
155
|
+
if (batchResult.success) {
|
|
156
|
+
return batchResult.result!;
|
|
157
|
+
} else {
|
|
158
|
+
// Create a failed OperationResult for this project
|
|
159
|
+
const project = pushable[index]!;
|
|
160
|
+
return {
|
|
161
|
+
success: false,
|
|
162
|
+
projectPath: project.path,
|
|
163
|
+
operation: "push",
|
|
164
|
+
error: batchResult.error?.message,
|
|
165
|
+
duration: 0,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
const successful = results.filter((r) => r.success).length;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
total: pushable.length,
|
|
174
|
+
successful,
|
|
175
|
+
failed: pushable.length - successful,
|
|
176
|
+
results,
|
|
177
|
+
duration: Date.now() - start,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Fetch all remotes for multiple projects with rate limiting
|
|
183
|
+
*
|
|
184
|
+
* Filters projects to only those that are git repos with remotes,
|
|
185
|
+
* then fetches updates in parallel with configurable concurrency.
|
|
186
|
+
*
|
|
187
|
+
* @param projects - Array of projects to fetch
|
|
188
|
+
* @param options - Batch operation options
|
|
189
|
+
* @param options.concurrency - Maximum concurrent operations (default: 5)
|
|
190
|
+
* @param options.onProgress - Progress callback (completed, total)
|
|
191
|
+
* @param options.gitService - Git service implementation (for testing)
|
|
192
|
+
* @returns Promise resolving to batch result with success/failure counts
|
|
193
|
+
*
|
|
194
|
+
* @example
|
|
195
|
+
* ```typescript
|
|
196
|
+
* const result = await batchFetch(projects, {
|
|
197
|
+
* concurrency: 4,
|
|
198
|
+
* onProgress: (done, total) => console.log(`${done}/${total}`)
|
|
199
|
+
* });
|
|
200
|
+
* console.log(`Fetched ${result.successful}/${result.total} repos`);
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export async function batchFetch(
|
|
204
|
+
projects: Project[],
|
|
205
|
+
options: BatchOptions = {}
|
|
206
|
+
): Promise<BatchResult> {
|
|
207
|
+
const { concurrency = GIT.DEFAULT_CONCURRENCY, minDelay = 0, onProgress, gitService = bunGitService } = options;
|
|
208
|
+
const start = Date.now();
|
|
209
|
+
const fetchable = getFetchableProjects(projects);
|
|
210
|
+
|
|
211
|
+
const batchResults = await processBatch(
|
|
212
|
+
fetchable,
|
|
213
|
+
(p) => gitService.fetchAll(p.path),
|
|
214
|
+
{
|
|
215
|
+
concurrency,
|
|
216
|
+
minDelay,
|
|
217
|
+
onProgress,
|
|
218
|
+
}
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
// Transform BatchItemResult<OperationResult>[] to OperationResult[]
|
|
222
|
+
const results: OperationResult[] = batchResults.map((batchResult, index) => {
|
|
223
|
+
if (batchResult.success) {
|
|
224
|
+
return batchResult.result!;
|
|
225
|
+
} else {
|
|
226
|
+
// Create a failed OperationResult for this project
|
|
227
|
+
const project = fetchable[index]!;
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
projectPath: project.path,
|
|
231
|
+
operation: "fetch",
|
|
232
|
+
error: batchResult.error?.message,
|
|
233
|
+
duration: 0,
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
const successful = results.filter((r) => r.success).length;
|
|
239
|
+
|
|
240
|
+
return {
|
|
241
|
+
total: fetchable.length,
|
|
242
|
+
successful,
|
|
243
|
+
failed: fetchable.length - successful,
|
|
244
|
+
results,
|
|
245
|
+
duration: Date.now() - start,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Refresh git status for multiple projects
|
|
251
|
+
*/
|
|
252
|
+
export async function batchRefreshStatus(
|
|
253
|
+
projects: Project[],
|
|
254
|
+
options: BatchOptions = {}
|
|
255
|
+
): Promise<Map<string, Project>> {
|
|
256
|
+
const { concurrency = GIT.STATUS_REFRESH_CONCURRENCY, gitService = bunGitService } = options;
|
|
257
|
+
const gitProjects = projects.filter((p) => p.type !== "non-git");
|
|
258
|
+
const results = new Map<string, Project>();
|
|
259
|
+
|
|
260
|
+
const batches = chunk(gitProjects, concurrency);
|
|
261
|
+
|
|
262
|
+
for (const batch of batches) {
|
|
263
|
+
const updates = await Promise.all(
|
|
264
|
+
batch.map(async (p) => {
|
|
265
|
+
try {
|
|
266
|
+
const status = await gitService.getStatus(p.path);
|
|
267
|
+
return { ...p, status, lastScanned: new Date() };
|
|
268
|
+
} catch {
|
|
269
|
+
return p;
|
|
270
|
+
}
|
|
271
|
+
})
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
for (const p of updates) {
|
|
275
|
+
results.set(p.id, p);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
return results;
|
|
280
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom command execution module
|
|
3
|
+
*
|
|
4
|
+
* Executes user-configured commands in project directories
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { CommandConfig } from "../types/index.ts";
|
|
8
|
+
|
|
9
|
+
export interface CommandResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
command: string;
|
|
12
|
+
projectPath: string;
|
|
13
|
+
output: string;
|
|
14
|
+
error?: string;
|
|
15
|
+
duration: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Execute a custom command in a project directory
|
|
20
|
+
*/
|
|
21
|
+
export async function executeCommand(
|
|
22
|
+
command: CommandConfig,
|
|
23
|
+
projectPath: string,
|
|
24
|
+
options: { background?: boolean } = {}
|
|
25
|
+
): Promise<CommandResult> {
|
|
26
|
+
const startTime = Date.now();
|
|
27
|
+
const { background = command.background } = options;
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
// Expand environment variables in the command
|
|
31
|
+
const expandedCommand = expandEnvVars(command.command, projectPath);
|
|
32
|
+
|
|
33
|
+
if (background) {
|
|
34
|
+
// Run in background - don't wait for completion
|
|
35
|
+
Bun.spawn(["sh", "-c", expandedCommand], {
|
|
36
|
+
cwd: projectPath,
|
|
37
|
+
stdout: "ignore",
|
|
38
|
+
stderr: "ignore",
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
success: true,
|
|
43
|
+
command: command.name,
|
|
44
|
+
projectPath,
|
|
45
|
+
output: "Started in background",
|
|
46
|
+
duration: Date.now() - startTime,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Run and wait for completion
|
|
51
|
+
const result = await Bun.$`sh -c ${expandedCommand}`.cwd(projectPath).quiet().nothrow();
|
|
52
|
+
|
|
53
|
+
const output = result.stdout.toString().trim();
|
|
54
|
+
const stderr = result.stderr.toString().trim();
|
|
55
|
+
|
|
56
|
+
if (result.exitCode !== 0) {
|
|
57
|
+
return {
|
|
58
|
+
success: false,
|
|
59
|
+
command: command.name,
|
|
60
|
+
projectPath,
|
|
61
|
+
output,
|
|
62
|
+
error: stderr || `Command exited with code ${result.exitCode}`,
|
|
63
|
+
duration: Date.now() - startTime,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
success: true,
|
|
69
|
+
command: command.name,
|
|
70
|
+
projectPath,
|
|
71
|
+
output,
|
|
72
|
+
duration: Date.now() - startTime,
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
success: false,
|
|
77
|
+
command: command.name,
|
|
78
|
+
projectPath,
|
|
79
|
+
output: "",
|
|
80
|
+
error: error instanceof Error ? error.message : String(error),
|
|
81
|
+
duration: Date.now() - startTime,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Expand environment variables in a command string
|
|
88
|
+
* Supports $VAR and ${VAR} syntax
|
|
89
|
+
* Special handling for $PWD which gets replaced with projectPath
|
|
90
|
+
*/
|
|
91
|
+
function expandEnvVars(command: string, projectPath: string): string {
|
|
92
|
+
return command.replace(/\$\{?(\w+)\}?/g, (match, varName) => {
|
|
93
|
+
// Special case: $PWD should be the project path, not the current process's PWD
|
|
94
|
+
if (varName === "PWD") {
|
|
95
|
+
return projectPath;
|
|
96
|
+
}
|
|
97
|
+
// For other env vars, use process.env or leave unchanged if not found
|
|
98
|
+
return process.env[varName] ?? match;
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Find a command by its key from the config
|
|
104
|
+
*/
|
|
105
|
+
export function findCommandByKey(
|
|
106
|
+
commands: CommandConfig[],
|
|
107
|
+
key: string
|
|
108
|
+
): CommandConfig | undefined {
|
|
109
|
+
if (!commands || !Array.isArray(commands)) {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
return commands.find((cmd) => cmd.key === key);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Execute commands on multiple projects
|
|
117
|
+
*/
|
|
118
|
+
export async function batchExecuteCommand(
|
|
119
|
+
command: CommandConfig,
|
|
120
|
+
projectPaths: string[],
|
|
121
|
+
options: {
|
|
122
|
+
concurrency?: number;
|
|
123
|
+
onProgress?: (current: number, total: number) => void;
|
|
124
|
+
} = {}
|
|
125
|
+
): Promise<CommandResult[]> {
|
|
126
|
+
const { concurrency = 5, onProgress } = options;
|
|
127
|
+
const results: CommandResult[] = [];
|
|
128
|
+
|
|
129
|
+
// Process in batches
|
|
130
|
+
for (let i = 0; i < projectPaths.length; i += concurrency) {
|
|
131
|
+
const batch = projectPaths.slice(i, i + concurrency);
|
|
132
|
+
const batchResults = await Promise.all(
|
|
133
|
+
batch.map((path) => executeCommand(command, path))
|
|
134
|
+
);
|
|
135
|
+
results.push(...batchResults);
|
|
136
|
+
onProgress?.(Math.min(i + concurrency, projectPaths.length), projectPaths.length);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return results;
|
|
140
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operations module - orchestrators for git and batch operations
|
|
3
|
+
*
|
|
4
|
+
* This module provides both low-level operations (single project)
|
|
5
|
+
* and batch orchestrators (multiple projects with progress).
|
|
6
|
+
*
|
|
7
|
+
* For testability, batch operations accept an optional GitService
|
|
8
|
+
* that can be mocked in tests.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// Re-export single-project operations from git module
|
|
12
|
+
export {
|
|
13
|
+
initGitInProject,
|
|
14
|
+
pullProject,
|
|
15
|
+
pushProject,
|
|
16
|
+
fetchProject,
|
|
17
|
+
addRemoteToProject,
|
|
18
|
+
refreshProjectStatuses,
|
|
19
|
+
} from "../git/operations.ts";
|
|
20
|
+
|
|
21
|
+
// Export new service-based batch operations
|
|
22
|
+
export {
|
|
23
|
+
batchPull,
|
|
24
|
+
batchPush,
|
|
25
|
+
batchFetch,
|
|
26
|
+
batchRefreshStatus,
|
|
27
|
+
type BatchOptions,
|
|
28
|
+
type ProgressCallback,
|
|
29
|
+
} from "./batch.ts";
|
|
30
|
+
|
|
31
|
+
// Export custom command operations
|
|
32
|
+
export {
|
|
33
|
+
executeCommand,
|
|
34
|
+
findCommandByKey,
|
|
35
|
+
batchExecuteCommand,
|
|
36
|
+
type CommandResult,
|
|
37
|
+
} from "./commands.ts";
|