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,656 @@
|
|
|
1
|
+
import { useInput, useApp } from "ink";
|
|
2
|
+
import { useStore, useFilteredProjects, useSelectedProjects, useFilteredUnifiedRepos, useSelectedUnifiedRepos } from "../state/store.tsx";
|
|
3
|
+
import {
|
|
4
|
+
moveCursor,
|
|
5
|
+
toggleSelection,
|
|
6
|
+
selectAll,
|
|
7
|
+
deselectAll,
|
|
8
|
+
setMode,
|
|
9
|
+
setFilter,
|
|
10
|
+
cycleSort,
|
|
11
|
+
setSort,
|
|
12
|
+
setMessage,
|
|
13
|
+
startAction,
|
|
14
|
+
endAction,
|
|
15
|
+
updateProgress,
|
|
16
|
+
} from "../state/actions.ts";
|
|
17
|
+
import { batchPull as defaultBatchPull, batchPush as defaultBatchPush, batchFetch as defaultBatchFetch } from "../operations/batch.ts";
|
|
18
|
+
import { initGitInProject as defaultInitGitInProject } from "../git/operations.ts";
|
|
19
|
+
import { executeCommand as defaultExecuteCommand, findCommandByKey as defaultFindCommandByKey } from "../operations/commands.ts";
|
|
20
|
+
import { errorToString } from "../utils/errors.ts";
|
|
21
|
+
import type { GitforestConfig, ConfirmDialogState, QuickFilter, ViewMode, DetailModalState, UnifiedRepo, CommandConfig, Project, BatchResult, OperationResult } from "../types/index.ts";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Dependencies that can be injected for testing
|
|
25
|
+
*/
|
|
26
|
+
export interface KeyBindingDeps {
|
|
27
|
+
batchPull: (projects: Project[], options?: { concurrency?: number; onProgress?: (current: number, total: number) => void }) => Promise<BatchResult>;
|
|
28
|
+
batchPush: (projects: Project[], options?: { concurrency?: number; onProgress?: (current: number, total: number) => void }) => Promise<BatchResult>;
|
|
29
|
+
batchFetch: (projects: Project[], options?: { concurrency?: number; onProgress?: (current: number, total: number) => void }) => Promise<BatchResult>;
|
|
30
|
+
initGitInProject: (path: string) => Promise<OperationResult>;
|
|
31
|
+
executeCommand: (command: CommandConfig, projectPath: string) => Promise<{ success: boolean; output?: string; error?: string }>;
|
|
32
|
+
findCommandByKey: (commands: CommandConfig[], key: string) => CommandConfig | undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface UseKeyBindingsOptions {
|
|
36
|
+
config: GitforestConfig;
|
|
37
|
+
onRefresh: () => Promise<void>;
|
|
38
|
+
deps?: Partial<KeyBindingDeps>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function useKeyBindings({ config, onRefresh, deps }: UseKeyBindingsOptions) {
|
|
42
|
+
const { state, dispatch } = useStore();
|
|
43
|
+
const { exit } = useApp();
|
|
44
|
+
const filteredProjects = useFilteredProjects();
|
|
45
|
+
const selectedProjects = useSelectedProjects();
|
|
46
|
+
const filteredUnifiedRepos = useFilteredUnifiedRepos();
|
|
47
|
+
const selectedUnifiedRepos = useSelectedUnifiedRepos();
|
|
48
|
+
|
|
49
|
+
// Use injected dependencies or defaults
|
|
50
|
+
const batchPull = deps?.batchPull ?? defaultBatchPull;
|
|
51
|
+
const batchPush = deps?.batchPush ?? defaultBatchPush;
|
|
52
|
+
const batchFetch = deps?.batchFetch ?? defaultBatchFetch;
|
|
53
|
+
const initGitInProject = deps?.initGitInProject ?? defaultInitGitInProject;
|
|
54
|
+
const executeCommand = deps?.executeCommand ?? defaultExecuteCommand;
|
|
55
|
+
const findCommandByKey = deps?.findCommandByKey ?? defaultFindCommandByKey;
|
|
56
|
+
|
|
57
|
+
// Helper function to execute a custom command on selected repos
|
|
58
|
+
async function handleCommandExecution(command: CommandConfig, repos: UnifiedRepo[]) {
|
|
59
|
+
// If no repos explicitly selected, use the current cursor position
|
|
60
|
+
let targetRepos = repos;
|
|
61
|
+
if (targetRepos.length === 0) {
|
|
62
|
+
const currentRepo = filteredUnifiedRepos[state.cursorIndex];
|
|
63
|
+
if (currentRepo) {
|
|
64
|
+
targetRepos = [currentRepo];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Filter to repos with local paths
|
|
69
|
+
const localRepos = targetRepos.filter((r) => r.localPath || r.local?.path);
|
|
70
|
+
|
|
71
|
+
if (localRepos.length === 0) {
|
|
72
|
+
dispatch(setMessage("No local projects selected"));
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const projectPath = localRepos[0]!.localPath || localRepos[0]!.local!.path;
|
|
77
|
+
|
|
78
|
+
dispatch(startAction(`Running: ${command.name}`));
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
const result = await executeCommand(command, projectPath);
|
|
82
|
+
dispatch(endAction());
|
|
83
|
+
|
|
84
|
+
if (result.success) {
|
|
85
|
+
if (result.output) {
|
|
86
|
+
// Show truncated output
|
|
87
|
+
const shortOutput = result.output.length > 50
|
|
88
|
+
? result.output.slice(0, 50) + "..."
|
|
89
|
+
: result.output;
|
|
90
|
+
dispatch(setMessage(`${command.name}: ${shortOutput}`));
|
|
91
|
+
} else {
|
|
92
|
+
dispatch(setMessage(`${command.name}: Done`));
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
dispatch(setMessage(`${command.name} failed: ${result.error}`));
|
|
96
|
+
}
|
|
97
|
+
} catch (error) {
|
|
98
|
+
dispatch(endAction());
|
|
99
|
+
dispatch(setMessage(`${command.name} failed: ${error instanceof Error ? error.message : String(error)}`));
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Helper function to fetch README content
|
|
104
|
+
async function fetchReadme(repo: UnifiedRepo): Promise<{ content: string | null; error: string | null }> {
|
|
105
|
+
// Try local README first
|
|
106
|
+
if (repo.localPath) {
|
|
107
|
+
try {
|
|
108
|
+
const readmePath = `${repo.localPath}/README.md`;
|
|
109
|
+
const file = Bun.file(readmePath);
|
|
110
|
+
if (await file.exists()) {
|
|
111
|
+
const content = await file.text();
|
|
112
|
+
return { content, error: null };
|
|
113
|
+
}
|
|
114
|
+
} catch {}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Try GitHub API
|
|
118
|
+
if (repo.github) {
|
|
119
|
+
try {
|
|
120
|
+
const response = await fetch(
|
|
121
|
+
`https://api.github.com/repos/${repo.github.fullName}/readme`,
|
|
122
|
+
{
|
|
123
|
+
headers: {
|
|
124
|
+
'Accept': 'application/vnd.github.raw',
|
|
125
|
+
'Authorization': `Bearer ${process.env.GITHUB_TOKEN}`,
|
|
126
|
+
},
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
if (response.ok) {
|
|
130
|
+
const content = await response.text();
|
|
131
|
+
return { content, error: null };
|
|
132
|
+
}
|
|
133
|
+
} catch {}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return { content: null, error: "README not found" };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
useInput(async (input, key) => {
|
|
140
|
+
const { mode, cursorIndex, selectedIndices } = state;
|
|
141
|
+
|
|
142
|
+
// Handle filter mode separately
|
|
143
|
+
if (mode === "filter") {
|
|
144
|
+
if (key.escape) {
|
|
145
|
+
dispatch(setMode("normal"));
|
|
146
|
+
dispatch(setFilter(""));
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (key.return) {
|
|
150
|
+
dispatch(setMode("normal"));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Let TextInput handle other keys
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Handle detail modal mode
|
|
158
|
+
if (mode === "detail") {
|
|
159
|
+
if (key.escape) {
|
|
160
|
+
dispatch({ type: "HIDE_DETAIL_MODAL" });
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
// j/k to scroll README
|
|
164
|
+
if (input === "j") {
|
|
165
|
+
dispatch({ type: "UPDATE_DETAIL_MODAL", payload: {
|
|
166
|
+
readmeScrollOffset: (state.detailModal?.readmeScrollOffset ?? 0) + 1
|
|
167
|
+
}});
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (input === "k") {
|
|
171
|
+
dispatch({ type: "UPDATE_DETAIL_MODAL", payload: {
|
|
172
|
+
readmeScrollOffset: Math.max(0, (state.detailModal?.readmeScrollOffset ?? 0) - 1)
|
|
173
|
+
}});
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
// Actions in modal - these should trigger the onAction callback
|
|
177
|
+
// For now, just close modal and perform action
|
|
178
|
+
if (input === "p" || input === "P" || input === "f") {
|
|
179
|
+
// Let the existing handlers work, but close modal first
|
|
180
|
+
dispatch({ type: "HIDE_DETAIL_MODAL" });
|
|
181
|
+
// Don't return - let it fall through to normal handling
|
|
182
|
+
}
|
|
183
|
+
if (input === "o") {
|
|
184
|
+
// Open in browser - need to implement
|
|
185
|
+
const repo = state.detailModal?.repo;
|
|
186
|
+
if (repo?.github?.htmlUrl) {
|
|
187
|
+
// Use Bun to open URL
|
|
188
|
+
try {
|
|
189
|
+
await Bun.$`open ${repo.github.htmlUrl}`.quiet();
|
|
190
|
+
} catch (error) {
|
|
191
|
+
dispatch(setMessage(`Failed to open browser: ${errorToString(error)}`));
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (input === "d") {
|
|
197
|
+
// Open in editor
|
|
198
|
+
const repo = state.detailModal?.repo;
|
|
199
|
+
if (repo?.localPath) {
|
|
200
|
+
const editor = process.env.EDITOR || "code";
|
|
201
|
+
try {
|
|
202
|
+
await Bun.$`${editor} ${repo.localPath}`.quiet();
|
|
203
|
+
} catch (error) {
|
|
204
|
+
dispatch(setMessage(`Failed to open editor: ${errorToString(error)}`));
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
return; // Don't process other keys in detail mode
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Handle filter options mode
|
|
213
|
+
if (mode === "filter-options") {
|
|
214
|
+
// Any key closes the overlay
|
|
215
|
+
dispatch(setMode("normal"));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Handle help mode
|
|
220
|
+
if (mode === "help") {
|
|
221
|
+
dispatch(setMode("normal"));
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle clone dialog mode
|
|
226
|
+
if (mode === "clone") {
|
|
227
|
+
if (key.escape) {
|
|
228
|
+
dispatch({ type: "HIDE_CLONE_DIALOG" });
|
|
229
|
+
dispatch(setMode("normal"));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
// Let CloneDialog handle other keys
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Handle command palette mode
|
|
237
|
+
if (mode === "command-palette") {
|
|
238
|
+
if (key.escape) {
|
|
239
|
+
dispatch(setMode("normal"));
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Find and execute command by key
|
|
244
|
+
const command = findCommandByKey(config.commands, input);
|
|
245
|
+
if (command) {
|
|
246
|
+
await handleCommandExecution(command, selectedUnifiedRepos);
|
|
247
|
+
dispatch(setMode("normal"));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// Global keys that work in any mode
|
|
254
|
+
if (input === "q" || (key.ctrl && input === "c")) {
|
|
255
|
+
exit();
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (input === "?") {
|
|
260
|
+
dispatch(setMode("help"));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// F key for filter options overlay
|
|
265
|
+
if (input === "F") {
|
|
266
|
+
dispatch(setMode("filter-options"));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// x key for command palette
|
|
271
|
+
if (input === "x") {
|
|
272
|
+
if (config.commands.length === 0) {
|
|
273
|
+
dispatch(setMessage("No commands configured"));
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
dispatch(setMode("command-palette"));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Tab - cycle view mode
|
|
281
|
+
if (key.tab) {
|
|
282
|
+
const modes: ViewMode[] = ["local", "github", "combined"];
|
|
283
|
+
const unifiedState = state as any;
|
|
284
|
+
const currentMode: ViewMode = unifiedState.viewMode || "local";
|
|
285
|
+
const currentIdx = modes.indexOf(currentMode);
|
|
286
|
+
if (currentIdx === -1) return; // Safety check
|
|
287
|
+
const nextIdx = (currentIdx + 1) % modes.length;
|
|
288
|
+
const nextMode = modes[nextIdx]!;
|
|
289
|
+
dispatch({ type: "SET_VIEW_MODE", payload: nextMode });
|
|
290
|
+
const modeLabels: Record<ViewMode, string> = { local: "Local only", github: "GitHub only", combined: "All repos" };
|
|
291
|
+
dispatch(setMessage(`View: ${modeLabels[nextMode]}`));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Navigation
|
|
296
|
+
if (input === "j" || key.downArrow) {
|
|
297
|
+
dispatch(moveCursor(cursorIndex + 1));
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (input === "k" || key.upArrow) {
|
|
302
|
+
dispatch(moveCursor(cursorIndex - 1));
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Enter - open detail modal
|
|
307
|
+
if (key.return) {
|
|
308
|
+
const currentRepo = filteredUnifiedRepos[cursorIndex];
|
|
309
|
+
if (currentRepo) {
|
|
310
|
+
// Fetch README content (async)
|
|
311
|
+
const detailState: DetailModalState = {
|
|
312
|
+
repo: currentRepo,
|
|
313
|
+
readmeContent: null,
|
|
314
|
+
readmeLoading: true,
|
|
315
|
+
readmeError: null,
|
|
316
|
+
readmeScrollOffset: 0,
|
|
317
|
+
};
|
|
318
|
+
dispatch({ type: "SHOW_DETAIL_MODAL", payload: detailState });
|
|
319
|
+
|
|
320
|
+
// Fetch README in background
|
|
321
|
+
fetchReadme(currentRepo).then(({ content, error }) => {
|
|
322
|
+
dispatch({ type: "UPDATE_DETAIL_MODAL", payload: {
|
|
323
|
+
readmeContent: content,
|
|
324
|
+
readmeLoading: false,
|
|
325
|
+
readmeError: error,
|
|
326
|
+
}});
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (input === "g") {
|
|
333
|
+
dispatch(moveCursor(0));
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (input === "G") {
|
|
338
|
+
const unifiedState = state as any;
|
|
339
|
+
const currentViewMode = unifiedState.viewMode || "combined";
|
|
340
|
+
const itemCount = currentViewMode === "local" ? filteredProjects.length : filteredUnifiedRepos.length;
|
|
341
|
+
dispatch(moveCursor(itemCount - 1));
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Selection
|
|
346
|
+
if (input === " ") {
|
|
347
|
+
dispatch(toggleSelection(cursorIndex));
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (input === "a") {
|
|
352
|
+
const unifiedState = state as any;
|
|
353
|
+
const currentViewMode = unifiedState.viewMode || "combined";
|
|
354
|
+
const itemCount = currentViewMode === "local" ? filteredProjects.length : filteredUnifiedRepos.length;
|
|
355
|
+
if (selectedIndices.size === itemCount) {
|
|
356
|
+
dispatch(deselectAll());
|
|
357
|
+
} else {
|
|
358
|
+
dispatch(selectAll());
|
|
359
|
+
}
|
|
360
|
+
return;
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Filter
|
|
364
|
+
if (input === "/") {
|
|
365
|
+
dispatch(setMode("filter"));
|
|
366
|
+
return;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Sort - 's' cycles field, 'S' reverses direction
|
|
370
|
+
if (input === "s") {
|
|
371
|
+
dispatch(cycleSort());
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (input === "S") {
|
|
376
|
+
const newDirection = state.sortDirection === 'desc' ? 'asc' : 'desc';
|
|
377
|
+
dispatch(setSort(state.sortBy, newDirection));
|
|
378
|
+
dispatch(setMessage(`Sort: ${state.sortBy} ${newDirection === 'desc' ? '↓' : '↑'}`));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
// Quick filters (1=dirty, 2=unpushed, 3=no-remote, 4=github-only, 5=local-only, 6=private, 7=public, 8=archived, 9=forks, 0=all)
|
|
383
|
+
const quickFilterMap: Record<string, QuickFilter> = {
|
|
384
|
+
"0": "all",
|
|
385
|
+
"1": "dirty",
|
|
386
|
+
"2": "unpushed",
|
|
387
|
+
"3": "no-remote",
|
|
388
|
+
"4": "github-only",
|
|
389
|
+
"5": "local-only",
|
|
390
|
+
"6": "private",
|
|
391
|
+
"7": "public",
|
|
392
|
+
"8": "archived",
|
|
393
|
+
"9": "forks",
|
|
394
|
+
};
|
|
395
|
+
|
|
396
|
+
if (input in quickFilterMap) {
|
|
397
|
+
const newFilter = quickFilterMap[input]!;
|
|
398
|
+
dispatch({ type: "SET_QUICK_FILTER", payload: newFilter });
|
|
399
|
+
const filterNames: Record<QuickFilter, string> = {
|
|
400
|
+
all: "All projects",
|
|
401
|
+
dirty: "Dirty projects",
|
|
402
|
+
unpushed: "Unpushed commits",
|
|
403
|
+
"no-remote": "No remote",
|
|
404
|
+
"github-only": "GitHub only",
|
|
405
|
+
"local-only": "Local only",
|
|
406
|
+
private: "Private repos",
|
|
407
|
+
public: "Public repos",
|
|
408
|
+
archived: "Archived repos",
|
|
409
|
+
forks: "Forked repos",
|
|
410
|
+
};
|
|
411
|
+
dispatch(setMessage(`Filter: ${filterNames[newFilter]}`));
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Refresh
|
|
416
|
+
if (input === "r") {
|
|
417
|
+
dispatch({ type: "SET_REFRESHING", payload: true });
|
|
418
|
+
await onRefresh();
|
|
419
|
+
dispatch({ type: "SET_REFRESHING", payload: false });
|
|
420
|
+
dispatch(setMessage("Refresh complete"));
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
// Git operations
|
|
425
|
+
if (input === "p") {
|
|
426
|
+
// Push selected
|
|
427
|
+
const gitProjects = selectedProjects.filter(
|
|
428
|
+
(p) => p.type === "git" && p.status?.hasRemote && p.status?.isAhead
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
if (gitProjects.length === 0) {
|
|
432
|
+
dispatch(setMessage("No projects to push"));
|
|
433
|
+
return;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
dispatch(startAction("Pushing"));
|
|
437
|
+
|
|
438
|
+
const result = await batchPush(gitProjects, { concurrency: config.scan.concurrency, onProgress: (current, total) => {
|
|
439
|
+
dispatch(updateProgress(current, total));
|
|
440
|
+
}});
|
|
441
|
+
|
|
442
|
+
dispatch(endAction());
|
|
443
|
+
|
|
444
|
+
// Show detailed push result
|
|
445
|
+
if (result.failed > 0) {
|
|
446
|
+
const failedNames = result.results
|
|
447
|
+
.filter(r => !r.success)
|
|
448
|
+
.map((_, i) => gitProjects[i]?.name)
|
|
449
|
+
.filter(Boolean)
|
|
450
|
+
.slice(0, 3)
|
|
451
|
+
.join(", ");
|
|
452
|
+
const moreCount = result.failed > 3 ? ` +${result.failed - 3} more` : "";
|
|
453
|
+
dispatch(setMessage(`Pushed ${result.successful}/${result.total} (failed: ${failedNames}${moreCount})`));
|
|
454
|
+
} else {
|
|
455
|
+
const pushedNames = gitProjects.slice(0, 3).map(p => p.name).join(", ");
|
|
456
|
+
const moreCount = gitProjects.length > 3 ? ` +${gitProjects.length - 3} more` : "";
|
|
457
|
+
dispatch(setMessage(`Pushed: ${pushedNames}${moreCount}`));
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Refresh after success to reflect latest state
|
|
461
|
+
await onRefresh();
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (input === "P") {
|
|
466
|
+
// Pull all
|
|
467
|
+
const gitProjects = filteredProjects.filter(
|
|
468
|
+
(p) => p.type === "git" && p.status?.hasRemote
|
|
469
|
+
);
|
|
470
|
+
|
|
471
|
+
if (gitProjects.length === 0) {
|
|
472
|
+
dispatch(setMessage("No projects with remotes"));
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
dispatch(startAction("Pulling"));
|
|
477
|
+
|
|
478
|
+
const result = await batchPull(gitProjects, { concurrency: config.scan.concurrency, onProgress: (current, total) => {
|
|
479
|
+
dispatch(updateProgress(current, total));
|
|
480
|
+
}});
|
|
481
|
+
|
|
482
|
+
dispatch(endAction());
|
|
483
|
+
|
|
484
|
+
// Show detailed pull result
|
|
485
|
+
if (result.failed > 0) {
|
|
486
|
+
dispatch(setMessage(`Pulled ${result.successful}/${result.total} (${result.failed} failed)`));
|
|
487
|
+
} else {
|
|
488
|
+
dispatch(setMessage(`Pulled ${result.successful} projects successfully`));
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
await onRefresh();
|
|
492
|
+
return;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (input === "f") {
|
|
496
|
+
// Fetch all
|
|
497
|
+
const gitProjects = filteredProjects.filter(
|
|
498
|
+
(p) => p.type === "git" && p.status?.hasRemote
|
|
499
|
+
);
|
|
500
|
+
|
|
501
|
+
if (gitProjects.length === 0) {
|
|
502
|
+
dispatch(setMessage("No projects with remotes"));
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
dispatch(startAction("Fetching"));
|
|
507
|
+
|
|
508
|
+
const result = await batchFetch(gitProjects, { concurrency: config.scan.concurrency, onProgress: (current, total) => {
|
|
509
|
+
dispatch(updateProgress(current, total));
|
|
510
|
+
}});
|
|
511
|
+
|
|
512
|
+
dispatch(endAction());
|
|
513
|
+
|
|
514
|
+
// Show detailed fetch result
|
|
515
|
+
if (result.failed > 0) {
|
|
516
|
+
dispatch(setMessage(`Fetched ${result.successful}/${result.total} (${result.failed} failed)`));
|
|
517
|
+
} else {
|
|
518
|
+
dispatch(setMessage(`Fetched ${result.successful} projects`));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
await onRefresh();
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
if (input === "i") {
|
|
526
|
+
// Init git in selected non-git projects
|
|
527
|
+
const nonGitProjects = selectedProjects.filter((p) => p.type === "non-git");
|
|
528
|
+
|
|
529
|
+
if (nonGitProjects.length === 0) {
|
|
530
|
+
dispatch(setMessage("No non-git projects selected"));
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
dispatch(startAction(`Initializing ${nonGitProjects.length} projects`));
|
|
535
|
+
|
|
536
|
+
let success = 0;
|
|
537
|
+
for (const project of nonGitProjects) {
|
|
538
|
+
const result = await initGitInProject(project.path);
|
|
539
|
+
if (result.success) success++;
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
dispatch(endAction());
|
|
543
|
+
dispatch(
|
|
544
|
+
setMessage(`Initialized ${success}/${nonGitProjects.length} projects`)
|
|
545
|
+
);
|
|
546
|
+
|
|
547
|
+
// Refresh after init
|
|
548
|
+
await onRefresh();
|
|
549
|
+
return;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// GitHub operations
|
|
553
|
+
if (input === "c") {
|
|
554
|
+
// Create GitHub repo - show confirmation dialog
|
|
555
|
+
const gitProjects = selectedProjects.filter(
|
|
556
|
+
(p) => p.type === "git" && !p.status?.hasRemote
|
|
557
|
+
);
|
|
558
|
+
|
|
559
|
+
if (gitProjects.length === 0) {
|
|
560
|
+
dispatch(setMessage("No git projects without remotes selected"));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const dialogState: ConfirmDialogState = {
|
|
565
|
+
operation: "create",
|
|
566
|
+
title: "Create GitHub Repos",
|
|
567
|
+
message: "Create GitHub repositories for:",
|
|
568
|
+
items: gitProjects.map((p) => p.name),
|
|
569
|
+
projectPaths: gitProjects.map((p) => p.path),
|
|
570
|
+
showVisibilityToggle: true,
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
dispatch({ type: "SHOW_CONFIRM_DIALOG", payload: dialogState });
|
|
574
|
+
return;
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (input === "C") {
|
|
578
|
+
// Setup: init git (if needed) + create GitHub repo + push
|
|
579
|
+
const eligibleProjects = selectedProjects.filter(
|
|
580
|
+
(p) => p.type === "non-git" || (p.type === "git" && !p.status?.hasRemote)
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
if (eligibleProjects.length === 0) {
|
|
584
|
+
dispatch(setMessage("No projects need setup (all have remotes)"));
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
const dialogState: ConfirmDialogState = {
|
|
589
|
+
operation: "setup",
|
|
590
|
+
title: "Setup Projects",
|
|
591
|
+
message: "Init git (if needed) and create GitHub repos for:",
|
|
592
|
+
items: eligibleProjects.map((p) => `${p.name}${p.type === "non-git" ? " (will init)" : ""}`),
|
|
593
|
+
projectPaths: eligibleProjects.map((p) => p.path),
|
|
594
|
+
showVisibilityToggle: true,
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
dispatch({ type: "SHOW_CONFIRM_DIALOG", payload: dialogState });
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (input === "A") {
|
|
602
|
+
// Archive GitHub repos - show confirmation dialog
|
|
603
|
+
const gitProjects = selectedProjects.filter(
|
|
604
|
+
(p) => p.type === "git" && p.status?.hasRemote
|
|
605
|
+
);
|
|
606
|
+
|
|
607
|
+
if (gitProjects.length === 0) {
|
|
608
|
+
dispatch(setMessage("No git projects with remotes selected"));
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const dialogState: ConfirmDialogState = {
|
|
613
|
+
operation: "archive",
|
|
614
|
+
title: "Archive GitHub Repos",
|
|
615
|
+
message: "Archive these repositories on GitHub:",
|
|
616
|
+
items: gitProjects.map((p) => p.name),
|
|
617
|
+
projectPaths: gitProjects.map((p) => p.path),
|
|
618
|
+
showVisibilityToggle: false,
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
dispatch({ type: "SHOW_CONFIRM_DIALOG", payload: dialogState });
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
// D - Clone GitHub repos
|
|
626
|
+
if (input === "D") {
|
|
627
|
+
const githubOnlyRepos = selectedUnifiedRepos.filter(r => r.source === "github");
|
|
628
|
+
|
|
629
|
+
if (githubOnlyRepos.length === 0) {
|
|
630
|
+
dispatch(setMessage("No GitHub-only repos selected to clone"));
|
|
631
|
+
return;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
dispatch({
|
|
635
|
+
type: "SHOW_CLONE_DIALOG",
|
|
636
|
+
payload: {
|
|
637
|
+
repos: githubOnlyRepos,
|
|
638
|
+
directories: config.directories,
|
|
639
|
+
selectedDirIndex: 0,
|
|
640
|
+
useSSH: true,
|
|
641
|
+
}
|
|
642
|
+
});
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
// Custom commands - check if input matches a configured command key
|
|
647
|
+
// This allows executing commands directly from main list without command palette
|
|
648
|
+
if (config.commands.length > 0) {
|
|
649
|
+
const command = findCommandByKey(config.commands, input);
|
|
650
|
+
if (command) {
|
|
651
|
+
await handleCommandExecution(command, selectedUnifiedRepos);
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useCallback } from "react";
|
|
2
|
+
import { useStore } from "../state/store.tsx";
|
|
3
|
+
import {
|
|
4
|
+
setProjects,
|
|
5
|
+
setLoading,
|
|
6
|
+
setError,
|
|
7
|
+
setMessage,
|
|
8
|
+
} from "../state/actions.ts";
|
|
9
|
+
import { scanAllDirectories, sortProjects } from "../scanner/index.ts";
|
|
10
|
+
import type { GitforestConfig } from "../types/index.ts";
|
|
11
|
+
import { errorToString } from "../utils/errors.ts";
|
|
12
|
+
|
|
13
|
+
export function useProjects(config: GitforestConfig) {
|
|
14
|
+
const { state, dispatch } = useStore();
|
|
15
|
+
const { sortBy, sortDirection } = state;
|
|
16
|
+
|
|
17
|
+
const loadProjects = useCallback(async () => {
|
|
18
|
+
dispatch(setLoading(true));
|
|
19
|
+
dispatch(setError(null));
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const projects = await scanAllDirectories(config);
|
|
23
|
+
const sorted = sortProjects(projects, sortBy, sortDirection);
|
|
24
|
+
dispatch(setProjects(sorted));
|
|
25
|
+
dispatch(setMessage(`Found ${projects.length} projects`));
|
|
26
|
+
} catch (error) {
|
|
27
|
+
dispatch(setError(errorToString(error)));
|
|
28
|
+
} finally {
|
|
29
|
+
dispatch(setLoading(false));
|
|
30
|
+
}
|
|
31
|
+
}, [config, dispatch, sortBy, sortDirection]);
|
|
32
|
+
|
|
33
|
+
// Load on mount
|
|
34
|
+
useEffect(() => {
|
|
35
|
+
loadProjects();
|
|
36
|
+
}, []);
|
|
37
|
+
|
|
38
|
+
// Re-sort when sort changes
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
if (state.projects.length > 0 && !state.isLoading) {
|
|
41
|
+
const sorted = sortProjects(state.projects, sortBy, sortDirection);
|
|
42
|
+
dispatch(setProjects(sorted));
|
|
43
|
+
}
|
|
44
|
+
}, [sortBy, sortDirection]);
|
|
45
|
+
|
|
46
|
+
return { loadProjects };
|
|
47
|
+
}
|