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