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
@@ -0,0 +1,130 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Box, Text } from "ink";
3
+ import type { GitforestConfig } from "../../types/index.ts";
4
+ import { WelcomeStep } from "./WelcomeStep.tsx";
5
+ import { DirectoriesStep } from "./DirectoriesStep.tsx";
6
+ import { GitHubAuthStep } from "./GitHubAuthStep.tsx";
7
+ import { CompleteStep } from "./CompleteStep.tsx";
8
+
9
+ type Step = "welcome" | "directories" | "github" | "complete";
10
+
11
+ interface DirectoryConfig {
12
+ path: string;
13
+ maxDepth: number;
14
+ label?: string;
15
+ }
16
+
17
+ interface GitHubAuthState {
18
+ authenticated: boolean;
19
+ user?: string;
20
+ skipped: boolean;
21
+ }
22
+
23
+ export interface OnboardingWizardProps {
24
+ onComplete: (config: GitforestConfig) => void;
25
+ onCancel: () => void;
26
+ }
27
+
28
+ export function OnboardingWizard({ onComplete, onCancel }: OnboardingWizardProps) {
29
+ const [currentStep, setCurrentStep] = useState<Step>("welcome");
30
+ const [directories, setDirectories] = useState<DirectoryConfig[]>([]);
31
+ const [githubAuth, setGithubAuth] = useState<GitHubAuthState>({
32
+ authenticated: false,
33
+ skipped: false,
34
+ });
35
+ const [isCreatingConfig, setIsCreatingConfig] = useState(false);
36
+ const [configError, setConfigError] = useState<string | null>(null);
37
+
38
+ const handleWelcomeComplete = () => {
39
+ setCurrentStep("directories");
40
+ };
41
+
42
+ const handleDirectoriesComplete = (dirs: DirectoryConfig[]) => {
43
+ setDirectories(dirs);
44
+ setCurrentStep("github");
45
+ };
46
+
47
+ const handleGitHubComplete = (auth: GitHubAuthState) => {
48
+ setGithubAuth(auth);
49
+ // Immediately create config, then show complete step
50
+ setIsCreatingConfig(true);
51
+ };
52
+
53
+ // Create config when GitHub step completes
54
+ useEffect(() => {
55
+ if (isCreatingConfig && currentStep === "github" && !configError) {
56
+ void (async () => {
57
+ try {
58
+ const { createOnboardingConfig } = await import("../../config/onboarding.ts");
59
+ const config = await createOnboardingConfig({
60
+ directories,
61
+ githubAuth: {
62
+ authenticated: githubAuth.authenticated,
63
+ user: githubAuth.user,
64
+ },
65
+ });
66
+ // Config created successfully, show complete step
67
+ setCurrentStep("complete");
68
+ setIsCreatingConfig(false);
69
+ // Auto-complete after showing success
70
+ setTimeout(() => {
71
+ onComplete(config);
72
+ }, 2000);
73
+ } catch (error) {
74
+ setConfigError(error instanceof Error ? error.message : "Failed to create config");
75
+ setIsCreatingConfig(false);
76
+ }
77
+ })();
78
+ }
79
+ }, [isCreatingConfig, currentStep, directories, githubAuth, onComplete, configError]);
80
+
81
+ const handleCancel = () => {
82
+ onCancel();
83
+ };
84
+
85
+ const handleBack = () => {
86
+ switch (currentStep) {
87
+ case "directories":
88
+ setCurrentStep("welcome");
89
+ break;
90
+ case "github":
91
+ setCurrentStep("directories");
92
+ break;
93
+ case "complete":
94
+ setCurrentStep("github");
95
+ break;
96
+ }
97
+ };
98
+
99
+ return (
100
+ <Box flexDirection="column">
101
+ {currentStep === "welcome" && (
102
+ <WelcomeStep onComplete={handleWelcomeComplete} onCancel={handleCancel} />
103
+ )}
104
+
105
+ {currentStep === "directories" && (
106
+ <DirectoriesStep
107
+ directories={directories}
108
+ onComplete={handleDirectoriesComplete}
109
+ onBack={handleBack}
110
+ onCancel={handleCancel}
111
+ />
112
+ )}
113
+
114
+ {currentStep === "github" && (
115
+ <GitHubAuthStep
116
+ onComplete={handleGitHubComplete}
117
+ onBack={handleBack}
118
+ onCancel={handleCancel}
119
+ />
120
+ )}
121
+
122
+ {currentStep === "complete" && (
123
+ <CompleteStep
124
+ directories={directories}
125
+ githubAuth={githubAuth}
126
+ />
127
+ )}
128
+ </Box>
129
+ );
130
+ }
@@ -0,0 +1,69 @@
1
+ import { Box, Text, useInput } from "ink";
2
+
3
+ export interface WelcomeStepProps {
4
+ onComplete: () => void;
5
+ onCancel: () => void;
6
+ }
7
+
8
+ export function WelcomeStep({ onComplete, onCancel }: WelcomeStepProps) {
9
+ useInput((input, key) => {
10
+ if (key.return || input === " ") {
11
+ onComplete();
12
+ return;
13
+ }
14
+
15
+ if (input === "q" || input === "Q" || key.escape) {
16
+ onCancel();
17
+ return;
18
+ }
19
+ });
20
+
21
+ return (
22
+ <Box
23
+ flexDirection="column"
24
+ borderStyle="round"
25
+ borderColor="cyan"
26
+ paddingX={3}
27
+ paddingY={2}
28
+ width={70}
29
+ >
30
+ {/* Title */}
31
+ <Box marginBottom={1}>
32
+ <Text bold color="cyan" underline>
33
+ Welcome to Gitforest!
34
+ </Text>
35
+ </Box>
36
+
37
+ <Box marginBottom={1}>
38
+ <Text>Git Repository Manager for the Terminal</Text>
39
+ </Box>
40
+
41
+ {/* Description */}
42
+ <Box flexDirection="column" marginBottom={1}>
43
+ <Text dimColor>
44
+ This wizard will help you set up Gitforest for the first time.
45
+ </Text>
46
+ </Box>
47
+
48
+ {/* What we'll configure */}
49
+ <Box flexDirection="column" marginBottom={1}>
50
+ <Text>We'll configure:</Text>
51
+ <Text> • Project directories to scan for Git repositories</Text>
52
+ <Text> • GitHub authentication (optional)</Text>
53
+ </Box>
54
+
55
+ {/* Instructions */}
56
+ <Box marginTop={1} gap={1}>
57
+ <Text dimColor>Press </Text>
58
+ <Text color="green" bold>
59
+ Enter
60
+ </Text>
61
+ <Text dimColor> to continue, </Text>
62
+ <Text color="red" bold>
63
+ q
64
+ </Text>
65
+ <Text dimColor> to quit</Text>
66
+ </Box>
67
+ </Box>
68
+ );
69
+ }
@@ -0,0 +1,263 @@
1
+ import { existsSync } from "fs";
2
+ import { join } from "path";
3
+ import { mkdir } from "fs/promises";
4
+ import { homedir } from "os";
5
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
6
+ import { GitforestConfigSchema, type GitforestConfig } from "../types/index.ts";
7
+
8
+ /**
9
+ * Environment variable prefixes and mappings
10
+ */
11
+ const ENV_PREFIX = "GITFOREST_";
12
+
13
+ interface EnvMapping {
14
+ env: string;
15
+ path: string[];
16
+ transform?: (value: string) => unknown;
17
+ }
18
+
19
+ const ENV_MAPPINGS: EnvMapping[] = [
20
+ // GitHub settings
21
+ { env: "GITFOREST_GITHUB_VISIBILITY", path: ["github", "defaultVisibility"], transform: (v) => v },
22
+
23
+ // Scan settings
24
+ { env: "GITFOREST_CONCURRENCY", path: ["scan", "concurrency"], transform: (v) => parseInt(v, 10) },
25
+ { env: "GITFOREST_INCLUDE_HIDDEN", path: ["scan", "includeHidden"], transform: (v) => v === "true" },
26
+
27
+ // Display settings
28
+ { env: "GITFOREST_SORT_BY", path: ["display", "sortBy"], transform: (v) => v },
29
+ { env: "GITFOREST_SORT_DIR", path: ["display", "sortDirection"], transform: (v) => v },
30
+ { env: "GITFOREST_SHOW_SUBMODULES", path: ["display", "showSubmodules"], transform: (v) => v === "true" },
31
+
32
+ // Cache settings
33
+ { env: "GITFOREST_CACHE_TTL", path: ["cache", "ttlSeconds"], transform: (v) => parseInt(v, 10) },
34
+ { env: "GITFOREST_GITHUB_CACHE_TTL", path: ["cache", "githubTtlSeconds"], transform: (v) => parseInt(v, 10) },
35
+ { env: "GITFOREST_ENABLE_BACKGROUND_REFRESH", path: ["cache", "enableBackgroundRefresh"], transform: (v) => v === "true" },
36
+ { env: "GITFOREST_BACKGROUND_REFRESH_INTERVAL", path: ["cache", "backgroundRefreshIntervalSeconds"], transform: (v) => parseInt(v, 10) },
37
+ ];
38
+
39
+ /**
40
+ * Set a nested value in an object using a path array
41
+ */
42
+ function setNestedValue(obj: Record<string, unknown>, path: string[], value: unknown): void {
43
+ let current = obj;
44
+ for (let i = 0; i < path.length - 1; i++) {
45
+ const key = path[i]!;
46
+ if (!(key in current) || typeof current[key] !== "object" || current[key] === null) {
47
+ current[key] = {};
48
+ }
49
+ current = current[key] as Record<string, unknown>;
50
+ }
51
+ const lastKey = path[path.length - 1]!;
52
+ current[lastKey] = value;
53
+ }
54
+
55
+ /**
56
+ * Apply environment variable overrides to config
57
+ * Environment variables take precedence over file-based config
58
+ */
59
+ export function applyEnvOverrides(config: GitforestConfig): GitforestConfig {
60
+ const result = JSON.parse(JSON.stringify(config)) as GitforestConfig;
61
+
62
+ for (const mapping of ENV_MAPPINGS) {
63
+ const envValue = process.env[mapping.env];
64
+ if (envValue !== undefined && envValue !== "") {
65
+ const transformedValue = mapping.transform ? mapping.transform(envValue) : envValue;
66
+ setNestedValue(result as unknown as Record<string, unknown>, mapping.path, transformedValue);
67
+ }
68
+ }
69
+
70
+ return result;
71
+ }
72
+
73
+ /**
74
+ * Get list of supported environment variables
75
+ */
76
+ export function getSupportedEnvVars(): { env: string; path: string; description: string }[] {
77
+ return ENV_MAPPINGS.map((m) => ({
78
+ env: m.env,
79
+ path: m.path.join("."),
80
+ description: `Override ${m.path.join(".")} config value`,
81
+ }));
82
+ }
83
+
84
+ /**
85
+ * Get possible config file locations in order of priority
86
+ */
87
+ function getConfigPaths(cwd?: string): string[] {
88
+ const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
89
+ const home = homedir();
90
+ const workingDir = cwd ?? process.cwd();
91
+
92
+ return [
93
+ // Current directory
94
+ join(workingDir, "gitforest.config.yaml"),
95
+ join(workingDir, "gitforest.config.yml"),
96
+ join(workingDir, "gitforest.config.json"),
97
+ join(workingDir, ".gitforest.yaml"),
98
+ join(workingDir, ".gitforest.yml"),
99
+ join(workingDir, ".gitforest.json"),
100
+ // XDG config directory
101
+ join(xdg, "gitforest", "config.yaml"),
102
+ join(xdg, "gitforest", "config.yml"),
103
+ join(xdg, "gitforest", "config.json"),
104
+ // Home directory
105
+ join(home, ".gitforest.yaml"),
106
+ join(home, ".gitforest.yml"),
107
+ join(home, ".gitforest.json"),
108
+ ];
109
+ }
110
+
111
+ /**
112
+ * Find the first existing config file
113
+ */
114
+ export function findConfigPath(cwd?: string): string | null {
115
+ for (const path of getConfigPaths(cwd)) {
116
+ if (existsSync(path)) {
117
+ return path;
118
+ }
119
+ }
120
+ return null;
121
+ }
122
+
123
+ /**
124
+ * Load and validate configuration from file
125
+ */
126
+ export async function loadConfig(configPath?: string, cwd?: string): Promise<GitforestConfig> {
127
+ const path = configPath ?? findConfigPath(cwd);
128
+
129
+ if (!path) {
130
+ const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
131
+ throw new Error(
132
+ `No config file found. Create one at:\n` +
133
+ ` - ${join(xdg, "gitforest", "config.yaml")}\n\n` +
134
+ `Example:\n` +
135
+ `directories:\n` +
136
+ ` - path: ~/projects\n` +
137
+ ` maxDepth: 2\n` +
138
+ ` - path: ~/.dotfiles\n` +
139
+ ` maxDepth: 3\n`
140
+ );
141
+ }
142
+
143
+ const file = Bun.file(path);
144
+ const content = await file.text();
145
+
146
+ let parsed: unknown;
147
+
148
+ if (path.endsWith(".yaml") || path.endsWith(".yml")) {
149
+ parsed = parseYaml(content);
150
+ } else if (path.endsWith(".json")) {
151
+ parsed = JSON.parse(content);
152
+ } else {
153
+ throw new Error(`Unknown config file format: ${path}`);
154
+ }
155
+
156
+ const result = GitforestConfigSchema.safeParse(parsed);
157
+
158
+ if (!result.success) {
159
+ const issues = result.error.issues
160
+ .map((issue) => ` - ${issue.path.join(".")}: ${issue.message}`)
161
+ .join("\n");
162
+ throw new Error(`Invalid config file at ${path}:\n${issues}`);
163
+ }
164
+
165
+ // Expand ~ in directory paths
166
+ let config = result.data;
167
+ config.directories = config.directories.map((dir) => ({
168
+ ...dir,
169
+ path: dir.path.replace(/^~/, homedir()),
170
+ }));
171
+
172
+ // Apply environment variable overrides
173
+ config = applyEnvOverrides(config);
174
+
175
+ return config;
176
+ }
177
+
178
+ /**
179
+ * Get the default config path for creating a new config
180
+ */
181
+ export function getDefaultConfigPath(): string {
182
+ const xdg = process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config");
183
+ return join(xdg, "gitforest", "config.yaml");
184
+ }
185
+
186
+ /**
187
+ * Create a default config file
188
+ */
189
+ export async function createDefaultConfig(): Promise<string> {
190
+ const configPath = getDefaultConfigPath();
191
+ const configDir = join(configPath, "..");
192
+
193
+ // Ensure directory exists
194
+ await mkdir(configDir, { recursive: true });
195
+
196
+ const defaultConfig = `# Gitforest Configuration
197
+ # Directories to scan for projects
198
+ directories:
199
+ - path: ~/projects
200
+ maxDepth: 2
201
+ label: Projects
202
+
203
+ # Optional: Add more directories
204
+ # - path: ~/.dotfiles
205
+ # maxDepth: 3
206
+ # label: Dotfiles
207
+
208
+ # Scan settings
209
+ scan:
210
+ ignore:
211
+ - node_modules
212
+ - .git
213
+ - vendor
214
+ - __pycache__
215
+ - target
216
+ - dist
217
+ - build
218
+ includeHidden: false
219
+ concurrency: 5
220
+
221
+ # GitHub settings
222
+ github:
223
+ defaultVisibility: private
224
+
225
+ # Display settings
226
+ display:
227
+ showSubmodules: true
228
+ sortBy: status # name | status | lastActivity
229
+ sortDirection: desc
230
+
231
+ # Cache settings
232
+ cache:
233
+ ttlSeconds: 300 # 5 minutes for local projects
234
+ githubTtlSeconds: 600 # 10 minutes for GitHub repos
235
+ enableBackgroundRefresh: true # Refresh data in background
236
+ backgroundRefreshIntervalSeconds: 300 # 5 minutes
237
+ `;
238
+
239
+ await Bun.write(configPath, defaultConfig);
240
+ return configPath;
241
+ }
242
+
243
+ /**
244
+ * Save configuration to file
245
+ */
246
+ export async function saveConfig(config: GitforestConfig, path?: string): Promise<void> {
247
+ const configPath = path ?? findConfigPath() ?? getDefaultConfigPath();
248
+
249
+ // Create backup of existing config if it exists
250
+ if (existsSync(configPath)) {
251
+ const backupPath = `${configPath}.bak`;
252
+ await Bun.write(backupPath, await Bun.file(configPath).text());
253
+ }
254
+
255
+ let content: string;
256
+ if (configPath.endsWith(".json")) {
257
+ content = JSON.stringify(config, null, 2);
258
+ } else {
259
+ content = stringifyYaml(config);
260
+ }
261
+
262
+ await Bun.write(configPath, content);
263
+ }
@@ -0,0 +1,67 @@
1
+ import { mkdir } from "fs/promises";
2
+ import { join } from "path";
3
+ import { homedir } from "os";
4
+ import type { GitforestConfig } from "../types/index.ts";
5
+ import { saveConfig, getDefaultConfigPath } from "./loader.ts";
6
+
7
+ export interface CreateOnboardingConfigOptions {
8
+ directories: Array<{ path: string; maxDepth: number; label?: string }>;
9
+ githubAuth?: {
10
+ authenticated: boolean;
11
+ user?: string;
12
+ };
13
+ }
14
+
15
+ /**
16
+ * Create a config file from onboarding wizard state
17
+ */
18
+ export async function createOnboardingConfig(
19
+ options: CreateOnboardingConfigOptions
20
+ ): Promise<GitforestConfig> {
21
+ const { directories, githubAuth } = options;
22
+
23
+ // Validate at least one directory
24
+ if (directories.length === 0) {
25
+ throw new Error("At least one directory is required");
26
+ }
27
+
28
+ // Create config object with defaults
29
+ const config: GitforestConfig = {
30
+ directories: directories.map((dir) => ({
31
+ path: dir.path,
32
+ maxDepth: dir.maxDepth,
33
+ label: dir.label,
34
+ })),
35
+ scan: {
36
+ ignore: ["node_modules", ".git", "vendor", "__pycache__", "target", "dist", "build"],
37
+ includeHidden: false,
38
+ concurrency: 5,
39
+ },
40
+ github: {
41
+ defaultVisibility: "private",
42
+ },
43
+ display: {
44
+ showSubmodules: true,
45
+ showNonGitProjects: true,
46
+ sortBy: "status",
47
+ sortDirection: "desc",
48
+ },
49
+ cache: {
50
+ ttlSeconds: 300,
51
+ githubTtlSeconds: 600,
52
+ enableBackgroundRefresh: true,
53
+ backgroundRefreshIntervalSeconds: 300,
54
+ },
55
+ commands: [],
56
+ };
57
+
58
+ // Ensure config directory exists
59
+ const configPath = getDefaultConfigPath();
60
+ const configDir = join(configPath, "..");
61
+ await mkdir(configDir, { recursive: true });
62
+
63
+ // Save config
64
+ await saveConfig(config, configPath);
65
+
66
+ return config;
67
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * Application constants
3
+ * Centralized configuration values to avoid magic numbers
4
+ */
5
+
6
+ // ============================================================================
7
+ // UI Constants
8
+ // ============================================================================
9
+
10
+ export const UI = {
11
+ /** Height reserved for header, filter bar, status bar, and padding */
12
+ LAYOUT_OVERHEAD: 6,
13
+ /** Minimum terminal height for proper display */
14
+ MIN_TERMINAL_HEIGHT: 24,
15
+ /** Default terminal height if detection fails */
16
+ DEFAULT_TERMINAL_HEIGHT: 24,
17
+ } as const;
18
+
19
+ // ============================================================================
20
+ // Scanner Constants
21
+ // ============================================================================
22
+
23
+ export const SCANNER = {
24
+ /** Default maximum directory depth for scanning */
25
+ DEFAULT_MAX_DEPTH: 2,
26
+ /** Default concurrency for scanning operations */
27
+ DEFAULT_CONCURRENCY: 5,
28
+ /** Length of generated project IDs (MD5 hash truncation) */
29
+ PROJECT_ID_LENGTH: 12,
30
+ /** Default cache TTL in seconds */
31
+ DEFAULT_CACHE_TTL_SECONDS: 300,
32
+ } as const;
33
+
34
+ // ============================================================================
35
+ // Git Constants
36
+ // ============================================================================
37
+
38
+ export const GIT = {
39
+ /** Default remote name */
40
+ DEFAULT_REMOTE: "origin",
41
+ /** Default branch name */
42
+ DEFAULT_BRANCH: "main",
43
+ /** Default concurrency for git operations */
44
+ DEFAULT_CONCURRENCY: 5,
45
+ /** Higher concurrency for status refresh (read-only) */
46
+ STATUS_REFRESH_CONCURRENCY: 10,
47
+ } as const;
48
+
49
+ export const GIT_TIMEOUTS = {
50
+ CLONE: 120000, // 2 minutes for clone
51
+ PULL: 60000, // 1 minute for pull
52
+ PUSH: 60000, // 1 minute for push
53
+ FETCH: 60000, // 1 minute for fetch
54
+ STATUS: 10000, // 10 seconds for status
55
+ } as const;
56
+
57
+ // ============================================================================
58
+ // GitHub API Constants
59
+ // ============================================================================
60
+
61
+ export const GITHUB_API = {
62
+ /** Base URL for GitHub API */
63
+ BASE_URL: "https://api.github.com",
64
+ /** API version header */
65
+ API_VERSION: "2022-11-28",
66
+ /** Default page size for pagination */
67
+ PAGE_SIZE: 100,
68
+ /** Maximum retry attempts */
69
+ MAX_RETRIES: 3,
70
+ /** Initial retry delay in milliseconds */
71
+ INITIAL_RETRY_DELAY: 1000,
72
+ /** Maximum retry delay in milliseconds */
73
+ MAX_RETRY_DELAY: 30000,
74
+ /** Backoff multiplier for retries */
75
+ RETRY_BACKOFF_FACTOR: 2,
76
+ } as const;
77
+
78
+ // ============================================================================
79
+ // Background Fetch Constants
80
+ // ============================================================================
81
+
82
+ export const BACKGROUND_FETCH = {
83
+ /** Interval between background fetches in milliseconds (5 minutes) */
84
+ INTERVAL_MS: 5 * 60 * 1000,
85
+ } as const;
86
+
87
+ // ============================================================================
88
+ // Type Exports
89
+ // ============================================================================
90
+
91
+ export type UIConstants = typeof UI;
92
+ export type ScannerConstants = typeof SCANNER;
93
+ export type GitConstants = typeof GIT;
94
+ export type GitTimeouts = typeof GIT_TIMEOUTS;
95
+ export type GitHubAPIConstants = typeof GITHUB_API;
96
+ export type BackgroundFetchConstants = typeof BACKGROUND_FETCH;