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
package/src/index.tsx
ADDED
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { render } from "ink";
|
|
4
|
+
import { App } from "./app.tsx";
|
|
5
|
+
import { loadConfig, createDefaultConfig, findConfigPath } from "./config/loader.ts";
|
|
6
|
+
import { initDb, closeDb } from "./db/index.ts";
|
|
7
|
+
import {
|
|
8
|
+
listProjects,
|
|
9
|
+
showStatus,
|
|
10
|
+
pullAll,
|
|
11
|
+
pushAll,
|
|
12
|
+
fetchAll,
|
|
13
|
+
showDirty,
|
|
14
|
+
initNonGit,
|
|
15
|
+
createGitHubRepos,
|
|
16
|
+
archiveRepos,
|
|
17
|
+
listGitHubRepos,
|
|
18
|
+
showUnifiedStatus,
|
|
19
|
+
cloneGitHubRepos,
|
|
20
|
+
showGitHubAuth,
|
|
21
|
+
setupProjects,
|
|
22
|
+
loginGitHub,
|
|
23
|
+
logoutGitHub,
|
|
24
|
+
handleConfigCommand,
|
|
25
|
+
} from "./cli/index.ts";
|
|
26
|
+
import { clearCache, getCacheStats } from "./scanner/index.ts";
|
|
27
|
+
import type { ViewMode, GitforestConfig } from "./types/index.ts";
|
|
28
|
+
|
|
29
|
+
const HELP_TEXT = `
|
|
30
|
+
gitforest - Git Repository Manager
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
gitforest Start the interactive TUI
|
|
34
|
+
gitforest <command> [options]
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
list List all local projects
|
|
38
|
+
status Show status summary
|
|
39
|
+
dirty Show dirty repositories
|
|
40
|
+
pull Pull all repositories
|
|
41
|
+
push Push repositories with unpushed commits
|
|
42
|
+
fetch Fetch all remotes
|
|
43
|
+
init Initialize git in non-git projects
|
|
44
|
+
setup Setup: init git + create GitHub repo + push
|
|
45
|
+
create-repos Create GitHub repos for projects without remotes
|
|
46
|
+
archive <repos...> Archive GitHub repositories
|
|
47
|
+
|
|
48
|
+
GitHub Commands:
|
|
49
|
+
login Login to GitHub (opens browser)
|
|
50
|
+
logout Logout from GitHub
|
|
51
|
+
auth Check GitHub authentication status
|
|
52
|
+
github List repos on GitHub not cloned locally
|
|
53
|
+
github --local List local repos only
|
|
54
|
+
github --combined List all repos (local + GitHub)
|
|
55
|
+
unified-status Show unified status (local + GitHub)
|
|
56
|
+
clone [repos...] Clone GitHub repos not yet local
|
|
57
|
+
|
|
58
|
+
Cache Commands:
|
|
59
|
+
cache status Show cache statistics
|
|
60
|
+
cache clear Clear the project cache
|
|
61
|
+
|
|
62
|
+
Options:
|
|
63
|
+
--init Create default config file
|
|
64
|
+
--help, -h Show this help message
|
|
65
|
+
--json Output as JSON (for list, status, dirty)
|
|
66
|
+
--verbose, -v Verbose output
|
|
67
|
+
--filter, -f <text> Filter projects by name/path
|
|
68
|
+
--public Create public repos (default: private)
|
|
69
|
+
--local Show local repos only (github command)
|
|
70
|
+
--combined Show all repos (github command)
|
|
71
|
+
--target, -t <dir> Target directory for clone
|
|
72
|
+
|
|
73
|
+
Environment:
|
|
74
|
+
GITHUB_TOKEN GitHub personal access token for API access
|
|
75
|
+
(Optional - run 'gitforest login' for automatic auth)
|
|
76
|
+
|
|
77
|
+
Config file locations (in order of priority):
|
|
78
|
+
./gitforest.config.yaml
|
|
79
|
+
~/.config/gitforest/config.yaml
|
|
80
|
+
~/.gitforest.yaml
|
|
81
|
+
|
|
82
|
+
TUI Keyboard shortcuts:
|
|
83
|
+
j/k or arrows Navigate
|
|
84
|
+
space Toggle selection
|
|
85
|
+
a Select all / deselect all
|
|
86
|
+
p Push selected
|
|
87
|
+
P Pull all repos
|
|
88
|
+
f Fetch all remotes
|
|
89
|
+
i Init git in selected
|
|
90
|
+
c Create GitHub repo
|
|
91
|
+
C Setup (init + create + push)
|
|
92
|
+
A Archive GitHub repo
|
|
93
|
+
/ Filter by text
|
|
94
|
+
0 Show all projects
|
|
95
|
+
1 Show dirty projects
|
|
96
|
+
2 Show unpushed commits
|
|
97
|
+
3 Show no-remote projects
|
|
98
|
+
s Cycle sort
|
|
99
|
+
v Cycle view (local/github/combined)
|
|
100
|
+
r Refresh
|
|
101
|
+
? Help
|
|
102
|
+
q Quit
|
|
103
|
+
|
|
104
|
+
Examples:
|
|
105
|
+
gitforest # Start TUI
|
|
106
|
+
gitforest list # List all local projects
|
|
107
|
+
gitforest list --json # List as JSON
|
|
108
|
+
gitforest status # Show local status summary
|
|
109
|
+
gitforest unified-status # Show status with GitHub repos
|
|
110
|
+
gitforest github # List GitHub repos not cloned
|
|
111
|
+
gitforest github --combined # List all repos
|
|
112
|
+
gitforest clone # Clone all uncloned GitHub repos
|
|
113
|
+
gitforest clone my-repo # Clone specific repo
|
|
114
|
+
gitforest clone -f python # Clone repos matching filter
|
|
115
|
+
gitforest dirty # Show dirty repos
|
|
116
|
+
gitforest pull # Pull all repos
|
|
117
|
+
gitforest push # Push repos with unpushed commits
|
|
118
|
+
gitforest fetch # Fetch all remotes
|
|
119
|
+
gitforest list -f myproject # Filter by name
|
|
120
|
+
gitforest init # Init git in non-git projects
|
|
121
|
+
gitforest setup # Setup all eligible projects (private)
|
|
122
|
+
gitforest setup --public # Setup with public repos
|
|
123
|
+
gitforest setup -f myproject # Setup filtered projects
|
|
124
|
+
gitforest create-repos # Create private GitHub repos
|
|
125
|
+
gitforest create-repos --public # Create public GitHub repos
|
|
126
|
+
gitforest archive my-repo # Archive a GitHub repo
|
|
127
|
+
gitforest auth # Check GitHub auth status
|
|
128
|
+
`;
|
|
129
|
+
|
|
130
|
+
let isShuttingDown = false;
|
|
131
|
+
|
|
132
|
+
async function gracefulShutdown(signal: string): Promise<void> {
|
|
133
|
+
if (isShuttingDown) return;
|
|
134
|
+
isShuttingDown = true;
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
closeDb();
|
|
140
|
+
} catch (error) {
|
|
141
|
+
console.error('Error during shutdown:', error);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
process.exit(0);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Register signal handlers
|
|
148
|
+
process.on('SIGINT', () => gracefulShutdown('SIGINT'));
|
|
149
|
+
process.on('SIGTERM', () => gracefulShutdown('SIGTERM'));
|
|
150
|
+
process.on('uncaughtException', (error) => {
|
|
151
|
+
console.error('Uncaught Exception:', error);
|
|
152
|
+
gracefulShutdown('uncaughtException');
|
|
153
|
+
});
|
|
154
|
+
process.on('unhandledRejection', (reason) => {
|
|
155
|
+
console.error('Unhandled Rejection:', reason);
|
|
156
|
+
gracefulShutdown('unhandledRejection');
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
function parseArgs(args: string[]): {
|
|
160
|
+
command: string | null;
|
|
161
|
+
flags: Record<string, boolean | string>;
|
|
162
|
+
positional: string[];
|
|
163
|
+
errors: string[];
|
|
164
|
+
} {
|
|
165
|
+
const flags: Record<string, boolean | string> = {};
|
|
166
|
+
const positional: string[] = [];
|
|
167
|
+
const errors: string[] = [];
|
|
168
|
+
let command: string | null = null;
|
|
169
|
+
|
|
170
|
+
// Known flags
|
|
171
|
+
const knownLongFlags = new Set([
|
|
172
|
+
'init', 'help', 'json', 'verbose', 'filter', 'target',
|
|
173
|
+
'public', 'local', 'combined', 'https', 'max-depth'
|
|
174
|
+
]);
|
|
175
|
+
const flagsWithValues = new Set(['filter', 'target', 'max-depth']);
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
const shortFlagMap: Record<string, string> = {
|
|
179
|
+
'h': 'help',
|
|
180
|
+
'v': 'verbose',
|
|
181
|
+
'f': 'filter',
|
|
182
|
+
't': 'target',
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
for (let i = 0; i < args.length; i++) {
|
|
186
|
+
const arg = args[i]!;
|
|
187
|
+
|
|
188
|
+
if (arg.startsWith("--")) {
|
|
189
|
+
const key = arg.slice(2);
|
|
190
|
+
|
|
191
|
+
// Check for unknown flags
|
|
192
|
+
if (!knownLongFlags.has(key)) {
|
|
193
|
+
errors.push(`Unknown flag: --${key}`);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const nextArg = args[i + 1];
|
|
198
|
+
|
|
199
|
+
if (flagsWithValues.has(key)) {
|
|
200
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
201
|
+
flags[key] = nextArg;
|
|
202
|
+
i++;
|
|
203
|
+
} else {
|
|
204
|
+
errors.push(`Flag --${key} requires a value`);
|
|
205
|
+
}
|
|
206
|
+
} else {
|
|
207
|
+
flags[key] = true;
|
|
208
|
+
}
|
|
209
|
+
} else if (arg.startsWith("-") && arg.length === 2) {
|
|
210
|
+
const shortKey = arg.slice(1);
|
|
211
|
+
const longKey = shortFlagMap[shortKey];
|
|
212
|
+
|
|
213
|
+
if (!longKey) {
|
|
214
|
+
errors.push(`Unknown flag: -${shortKey}`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const nextArg = args[i + 1];
|
|
219
|
+
|
|
220
|
+
if (flagsWithValues.has(longKey)) {
|
|
221
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
222
|
+
flags[longKey] = nextArg;
|
|
223
|
+
i++;
|
|
224
|
+
} else {
|
|
225
|
+
errors.push(`Flag -${shortKey} requires a value`);
|
|
226
|
+
}
|
|
227
|
+
} else {
|
|
228
|
+
flags[longKey] = true;
|
|
229
|
+
}
|
|
230
|
+
} else if (!command) {
|
|
231
|
+
command = arg;
|
|
232
|
+
} else {
|
|
233
|
+
positional.push(arg);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { command, flags, positional, errors };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Run the onboarding wizard for first-time users
|
|
242
|
+
*/
|
|
243
|
+
async function runOnboardingWizard(): Promise<void> {
|
|
244
|
+
const { OnboardingWizard } = await import("./components/onboarding/OnboardingWizard.tsx");
|
|
245
|
+
let completedConfig: GitforestConfig | null = null;
|
|
246
|
+
let cancelled = false;
|
|
247
|
+
|
|
248
|
+
const handleComplete = (config: GitforestConfig) => {
|
|
249
|
+
completedConfig = config;
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
const handleCancel = () => {
|
|
253
|
+
cancelled = true;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
// Render onboarding wizard
|
|
257
|
+
const { waitUntilExit } = render(
|
|
258
|
+
<OnboardingWizard
|
|
259
|
+
onComplete={handleComplete}
|
|
260
|
+
onCancel={handleCancel}
|
|
261
|
+
/>
|
|
262
|
+
);
|
|
263
|
+
|
|
264
|
+
await waitUntilExit();
|
|
265
|
+
|
|
266
|
+
if (cancelled) {
|
|
267
|
+
console.log("\nOnboarding cancelled. Run 'gitforest --init' to create a default config.");
|
|
268
|
+
process.exit(0);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (completedConfig) {
|
|
272
|
+
// Start main TUI app
|
|
273
|
+
await initDb();
|
|
274
|
+
const { waitUntilExit: waitAppExit } = render(<App config={completedConfig} />);
|
|
275
|
+
await waitAppExit();
|
|
276
|
+
closeDb();
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
async function main() {
|
|
281
|
+
const args = process.argv.slice(2);
|
|
282
|
+
const { command, flags, positional, errors } = parseArgs(args);
|
|
283
|
+
|
|
284
|
+
// Handle validation errors
|
|
285
|
+
if (errors.length > 0) {
|
|
286
|
+
for (const error of errors) {
|
|
287
|
+
console.error(`Error: ${error}`);
|
|
288
|
+
}
|
|
289
|
+
console.log("\nRun 'gitforest --help' for usage information.");
|
|
290
|
+
process.exit(1);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Handle --init flag to create default config
|
|
294
|
+
if (flags["init"]) {
|
|
295
|
+
const existingConfig = findConfigPath();
|
|
296
|
+
if (existingConfig) {
|
|
297
|
+
console.log(`Config already exists at: ${existingConfig}`);
|
|
298
|
+
process.exit(1);
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const configPath = await createDefaultConfig();
|
|
302
|
+
console.log(`Created default config at: ${configPath}`);
|
|
303
|
+
console.log("\nEdit the config to add your project directories, then run 'gitforest' again.");
|
|
304
|
+
process.exit(0);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Handle --help flag (even when a command is provided)
|
|
308
|
+
if (flags["help"] || flags["h"]) {
|
|
309
|
+
console.log(HELP_TEXT);
|
|
310
|
+
process.exit(0);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
// Check for first-time run (no config)
|
|
314
|
+
const configPath = findConfigPath();
|
|
315
|
+
const isTUIMode = !command;
|
|
316
|
+
|
|
317
|
+
if (!configPath && isTUIMode) {
|
|
318
|
+
// No config and TUI mode - run onboarding wizard
|
|
319
|
+
await runOnboardingWizard();
|
|
320
|
+
return; // runOnboardingWizard handles everything
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
try {
|
|
324
|
+
// Load configuration
|
|
325
|
+
const config = await loadConfig();
|
|
326
|
+
const filter = typeof flags["filter"] === "string" ? flags["filter"] : undefined;
|
|
327
|
+
const json = !!flags["json"];
|
|
328
|
+
const verbose = !!flags["verbose"];
|
|
329
|
+
const maxDepth = flags["max-depth"] ? parseInt(String(flags["max-depth"]), 10) : undefined;
|
|
330
|
+
|
|
331
|
+
// CLI commands
|
|
332
|
+
if (command) {
|
|
333
|
+
// Check for help flag again after command (e.g., "list --help")
|
|
334
|
+
if (flags["help"] || flags["h"]) {
|
|
335
|
+
console.log(HELP_TEXT);
|
|
336
|
+
process.exit(0);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const cliOptions = { config, filter, json, verbose, maxDepth };
|
|
340
|
+
|
|
341
|
+
switch (command) {
|
|
342
|
+
case "list":
|
|
343
|
+
case "ls":
|
|
344
|
+
await listProjects(cliOptions);
|
|
345
|
+
break;
|
|
346
|
+
|
|
347
|
+
case "status":
|
|
348
|
+
case "st":
|
|
349
|
+
await showStatus(cliOptions);
|
|
350
|
+
break;
|
|
351
|
+
|
|
352
|
+
case "dirty":
|
|
353
|
+
await showDirty(cliOptions);
|
|
354
|
+
break;
|
|
355
|
+
|
|
356
|
+
case "pull":
|
|
357
|
+
await pullAll(cliOptions);
|
|
358
|
+
break;
|
|
359
|
+
|
|
360
|
+
case "push":
|
|
361
|
+
await pushAll(cliOptions);
|
|
362
|
+
break;
|
|
363
|
+
|
|
364
|
+
case "fetch":
|
|
365
|
+
await fetchAll(cliOptions);
|
|
366
|
+
break;
|
|
367
|
+
|
|
368
|
+
case "init":
|
|
369
|
+
await initNonGit(cliOptions);
|
|
370
|
+
break;
|
|
371
|
+
|
|
372
|
+
case "setup":
|
|
373
|
+
await setupProjects({
|
|
374
|
+
...cliOptions,
|
|
375
|
+
isPrivate: !flags["public"],
|
|
376
|
+
});
|
|
377
|
+
break;
|
|
378
|
+
|
|
379
|
+
case "create-repos":
|
|
380
|
+
case "create":
|
|
381
|
+
await createGitHubRepos({
|
|
382
|
+
...cliOptions,
|
|
383
|
+
isPrivate: !flags["public"],
|
|
384
|
+
});
|
|
385
|
+
break;
|
|
386
|
+
|
|
387
|
+
case "archive":
|
|
388
|
+
if (positional.length === 0) {
|
|
389
|
+
console.error("Error: Please specify repositories to archive");
|
|
390
|
+
console.log("Usage: gitforest archive <repo1> [repo2] ...");
|
|
391
|
+
process.exit(1);
|
|
392
|
+
}
|
|
393
|
+
await archiveRepos({ ...cliOptions, repos: positional });
|
|
394
|
+
break;
|
|
395
|
+
|
|
396
|
+
// GitHub API commands
|
|
397
|
+
case "github":
|
|
398
|
+
case "gh": {
|
|
399
|
+
const view: ViewMode = flags["local"]
|
|
400
|
+
? "local"
|
|
401
|
+
: flags["combined"]
|
|
402
|
+
? "combined"
|
|
403
|
+
: "github";
|
|
404
|
+
await listGitHubRepos({ ...cliOptions, view });
|
|
405
|
+
break;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
case "unified-status":
|
|
409
|
+
case "ustatus":
|
|
410
|
+
await showUnifiedStatus(cliOptions);
|
|
411
|
+
break;
|
|
412
|
+
|
|
413
|
+
case "clone": {
|
|
414
|
+
const targetDir = typeof flags["target"] === "string" ? flags["target"] : undefined;
|
|
415
|
+
await cloneGitHubRepos({
|
|
416
|
+
...cliOptions,
|
|
417
|
+
repos: positional.length > 0 ? positional : undefined,
|
|
418
|
+
targetDir,
|
|
419
|
+
useHTTPS: !!flags["https"],
|
|
420
|
+
});
|
|
421
|
+
break;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
case "auth":
|
|
425
|
+
await showGitHubAuth();
|
|
426
|
+
break;
|
|
427
|
+
|
|
428
|
+
case "login":
|
|
429
|
+
await loginGitHub();
|
|
430
|
+
break;
|
|
431
|
+
|
|
432
|
+
case "logout":
|
|
433
|
+
await logoutGitHub();
|
|
434
|
+
break;
|
|
435
|
+
|
|
436
|
+
case "cache":
|
|
437
|
+
if (positional[0] === "clear") {
|
|
438
|
+
await clearCache();
|
|
439
|
+
console.log("Cache cleared.");
|
|
440
|
+
} else if (positional[0] === "status" || positional.length === 0) {
|
|
441
|
+
const stats = await getCacheStats();
|
|
442
|
+
console.log("Cache Statistics:");
|
|
443
|
+
console.log(` Projects cached: ${stats.projectCount}`);
|
|
444
|
+
if (stats.oldestScan) {
|
|
445
|
+
console.log(` Oldest scan: ${stats.oldestScan.toLocaleString()}`);
|
|
446
|
+
}
|
|
447
|
+
if (stats.newestScan) {
|
|
448
|
+
console.log(` Newest scan: ${stats.newestScan.toLocaleString()}`);
|
|
449
|
+
}
|
|
450
|
+
} else {
|
|
451
|
+
console.error(`Unknown cache subcommand: ${positional[0]}`);
|
|
452
|
+
console.log("Usage: gitforest cache [status|clear]");
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
break;
|
|
456
|
+
|
|
457
|
+
case "config":
|
|
458
|
+
if (positional.length === 0) {
|
|
459
|
+
console.error("Error: Missing config subcommand");
|
|
460
|
+
console.log("Usage: gitforest config add-dir <path>");
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
await handleConfigCommand(positional[0]!, positional.slice(1), cliOptions);
|
|
464
|
+
break;
|
|
465
|
+
|
|
466
|
+
default:
|
|
467
|
+
console.error(`Unknown command: ${command}`);
|
|
468
|
+
console.error("\nRun 'gitforest --help' for usage information.");
|
|
469
|
+
process.exit(1);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
process.exit(0);
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// TUI mode (no command specified)
|
|
476
|
+
await initDb();
|
|
477
|
+
|
|
478
|
+
const { waitUntilExit } = render(<App config={config} />);
|
|
479
|
+
await waitUntilExit();
|
|
480
|
+
|
|
481
|
+
closeDb();
|
|
482
|
+
} catch (error) {
|
|
483
|
+
console.error("Error:", error instanceof Error ? error.message : error);
|
|
484
|
+
|
|
485
|
+
if (error instanceof Error && error.message.includes("No config file found")) {
|
|
486
|
+
console.log("\nRun 'gitforest' to start the onboarding wizard,");
|
|
487
|
+
console.log("or 'gitforest --init' to create a default config file.");
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
process.exit(1);
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
main();
|