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,345 @@
1
+ import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
+ import { mkdirSync, rmSync, existsSync } from "fs";
3
+ import * as fs from "fs";
4
+ import { join } from "path";
5
+ import { tmpdir } from "os";
6
+ import { readDirectory, validateDirectoryPath, getCompletions, formatDisplayPath } from "./DirectoriesStep.tsx";
7
+
8
+ // Import the functions we want to test
9
+ // Note: We'll need to export these from DirectoriesStep.tsx first
10
+
11
+ describe("Directory Browser Utility Functions", () => {
12
+ let testDir: string;
13
+
14
+ function createTestDir() {
15
+ testDir = join(tmpdir(), `gitforest-utils-test-${Date.now()}`);
16
+ mkdirSync(testDir, { recursive: true });
17
+
18
+ // Create test structure
19
+ mkdirSync(join(testDir, "projects"));
20
+ mkdirSync(join(testDir, "code"));
21
+ mkdirSync(join(testDir, "documents"));
22
+ mkdirSync(join(testDir, "test-folder"));
23
+ mkdirSync(join(testDir, ".hidden"));
24
+
25
+ // Create files (should not appear in directory listing)
26
+ Bun.write(join(testDir, "file.txt"), "test");
27
+ }
28
+
29
+ function cleanupTestDir() {
30
+ if (existsSync(testDir)) {
31
+ rmSync(testDir, { recursive: true, force: true });
32
+ }
33
+ }
34
+
35
+ beforeEach(() => {
36
+ createTestDir();
37
+ });
38
+
39
+ afterEach(() => {
40
+ cleanupTestDir();
41
+ });
42
+
43
+ describe("readDirectory", () => {
44
+ test("should return only directories, excluding files", () => {
45
+ const dirs = readDirectory(testDir);
46
+ const dirNames = dirs.map(d => d.name);
47
+
48
+ // Should contain directories
49
+ expect(dirNames).toContain("projects");
50
+ expect(dirNames).toContain("code");
51
+ expect(dirNames).toContain("documents");
52
+ expect(dirNames).toContain("test-folder");
53
+
54
+ // Should NOT contain files
55
+ expect(dirNames).not.toContain("file.txt");
56
+ });
57
+
58
+ test("should exclude hidden directories starting with .", () => {
59
+ const dirs = readDirectory(testDir);
60
+ const dirNames = dirs.map(d => d.name);
61
+
62
+ // Should not include .hidden
63
+ expect(dirNames).not.toContain(".hidden");
64
+ });
65
+
66
+ test("should sort results alphabetically", () => {
67
+ const dirs = readDirectory(testDir);
68
+ const dirNames = dirs.map(d => d.name);
69
+
70
+ // Check if sorted (code, documents, projects, test-folder)
71
+ expect(dirNames[0]).toBe("code");
72
+ expect(dirNames[1]).toBe("documents");
73
+ expect(dirNames[2]).toBe("projects");
74
+ expect(dirNames[3]).toBe("test-folder");
75
+ });
76
+
77
+ test("should return empty array for non-existent directory", () => {
78
+ const dirs = readDirectory("/non-existent-path-12345");
79
+ expect(dirs).toEqual([]);
80
+ });
81
+ });
82
+
83
+ describe("validateDirectoryPath", () => {
84
+ test("should return valid: true for existing directory", () => {
85
+ const result = validateDirectoryPath(testDir);
86
+ expect(result.valid).toBe(true);
87
+ expect(result.error).toBeUndefined();
88
+ });
89
+
90
+ test("should return valid: false for non-existent path", () => {
91
+ const result = validateDirectoryPath("/fake-path-12345");
92
+ expect(result.valid).toBe(false);
93
+ expect(result.error).toBe("Path does not exist");
94
+ });
95
+
96
+ test("should return valid: false for file instead of directory", () => {
97
+ const result = validateDirectoryPath(join(testDir, "file.txt"));
98
+ expect(result.valid).toBe(false);
99
+ expect(result.error).toBe("Not a directory");
100
+ });
101
+
102
+ test("should return valid: false for empty path", () => {
103
+ const result = validateDirectoryPath("");
104
+ expect(result.valid).toBe(false);
105
+ expect(result.error).toBe("Path is required");
106
+ });
107
+
108
+ test("should expand tilde before validating", () => {
109
+ const homeDir = require("os").homedir();
110
+ // Create a test directory in home
111
+ // For now just test the expansion logic
112
+ const result = validateDirectoryPath("~/");
113
+ // After expansion, ~/ becomes /home/user, which exists
114
+ // The validation should handle the expansion
115
+ expect(result).toBeDefined();
116
+ });
117
+ });
118
+
119
+ describe("getCompletions", () => {
120
+ test("should complete absolute paths starting with /", () => {
121
+ const completions = getCompletions("/proj", testDir);
122
+ // Should return nothing since we're not in root
123
+ expect(Array.isArray(completions)).toBe(true);
124
+ });
125
+
126
+ test("should complete relative paths from current directory", () => {
127
+ const completions = getCompletions("proj", testDir);
128
+ expect(completions.length).toBeGreaterThan(0);
129
+ expect(completions[0]).toContain("projects");
130
+ });
131
+
132
+ test("should be case-insensitive for matching", () => {
133
+ const completions = getCompletions("PROJ", testDir);
134
+ expect(completions.length).toBeGreaterThan(0);
135
+ expect(completions[0]).toContain("projects");
136
+ });
137
+
138
+ test("should strip trailing slash before processing", () => {
139
+ // Test that /proj and /proj/ give same results
140
+ const c1 = getCompletions("/proj", testDir);
141
+ const c2 = getCompletions("/proj/", testDir);
142
+ expect(c1).toEqual(c2);
143
+ });
144
+
145
+ test("should return empty array for invalid base directory", () => {
146
+ const completions = getCompletions("test", "/non-existent-12345");
147
+ expect(completions).toEqual([]);
148
+ });
149
+ });
150
+
151
+ describe("formatDisplayPath", () => {
152
+ test("should replace home directory prefix with ~", () => {
153
+ const homeDir = require("os").homedir();
154
+ const testPath = join(homeDir, "projects");
155
+ const formatted = formatDisplayPath(testPath);
156
+ expect(formatted).toBe("~/projects");
157
+ });
158
+
159
+ test("should not modify paths not in home directory", () => {
160
+ const formatted = formatDisplayPath("/Volumes/Storage");
161
+ expect(formatted).toBe("/Volumes/Storage");
162
+ });
163
+
164
+ test("should handle paths that exactly equal home directory", () => {
165
+ const homeDir = require("os").homedir();
166
+ const formatted = formatDisplayPath(homeDir);
167
+ expect(formatted).toBe("~");
168
+ });
169
+ });
170
+
171
+ describe("Edge Cases", () => {
172
+ test("should handle paths with trailing slash", () => {
173
+ const result1 = validateDirectoryPath(join(testDir, "projects"));
174
+ const result2 = validateDirectoryPath(join(testDir, "projects/"));
175
+
176
+ // Both should be valid
177
+ expect(result1.valid).toBe(true);
178
+ expect(result2.valid).toBe(true);
179
+ });
180
+
181
+ test("should handle case-insensitive folder filtering", () => {
182
+ const completions = getCompletions("PROJ", testDir);
183
+ expect(completions.length).toBeGreaterThan(0);
184
+ expect(completions.some(c => c.includes("projects"))).toBe(true);
185
+ });
186
+
187
+ test("should handle empty input for completions", () => {
188
+ // Empty input from testDir should list all entries
189
+ const completions = getCompletions("", testDir);
190
+ expect(Array.isArray(completions)).toBe(true);
191
+ });
192
+
193
+ test("should handle paths with spaces", () => {
194
+ // Create a directory with spaces in the name
195
+ const spacedDir = join(testDir, "folder with spaces");
196
+ mkdirSync(spacedDir, { recursive: true });
197
+
198
+ const dirs = readDirectory(testDir);
199
+ const dirNames = dirs.map(d => d.name);
200
+
201
+ expect(dirNames).toContain("folder with spaces");
202
+
203
+ // Validate the path with spaces
204
+ const result = validateDirectoryPath(spacedDir);
205
+ expect(result.valid).toBe(true);
206
+
207
+ // Format display path should preserve spaces
208
+ const formatted = formatDisplayPath(spacedDir);
209
+ expect(formatted).toContain("folder with spaces");
210
+ });
211
+
212
+ test("should handle non-ascii characters in folder names", () => {
213
+ // Create directories with non-ASCII characters
214
+ const unicodeDir1 = join(testDir, "café");
215
+ const unicodeDir2 = join(testDir, "проект");
216
+ mkdirSync(unicodeDir1, { recursive: true });
217
+ mkdirSync(unicodeDir2, { recursive: true });
218
+
219
+ const dirs = readDirectory(testDir);
220
+ const dirNames = dirs.map(d => d.name);
221
+
222
+ expect(dirNames).toContain("café");
223
+ expect(dirNames).toContain("проект");
224
+ });
225
+
226
+ test("should handle symlinks to directories", () => {
227
+ // Skip on Windows if symlinks require admin privileges
228
+ if (process.platform === "win32") {
229
+ // Symlink behavior varies on Windows
230
+ expect(true).toBe(true);
231
+ return;
232
+ }
233
+
234
+ // Create a target directory
235
+ const targetDir = join(testDir, "target");
236
+ mkdirSync(targetDir, { recursive: true });
237
+
238
+ // Create a symlink to the directory
239
+ const symlinkPath = join(testDir, "symlink-to-target");
240
+ try {
241
+ fs.symlinkSync(targetDir, symlinkPath, "dir");
242
+ } catch (e) {
243
+ // Symlink creation might fail due to permissions
244
+ // Skip test if we can't create symlinks
245
+ expect(true).toBe(true);
246
+ return;
247
+ }
248
+
249
+ const dirs = readDirectory(testDir);
250
+ const dirNames = dirs.map(d => d.name);
251
+
252
+ // Symlink should appear in listing (it's a directory entry)
253
+ expect(dirNames).toContain("symlink-to-target");
254
+ });
255
+
256
+ // These tests require specific platform or permission setups
257
+ // Mark them as todo since they're environment-dependent
258
+ test.todo("should handle very long paths gracefully");
259
+ test.todo("should handle paths with special characters");
260
+ test.todo("should handle permission denied errors gracefully");
261
+ });
262
+ });
263
+
264
+ describe("Browser State Management", () => {
265
+ describe("input buffer changes", () => {
266
+ test.todo("should reset selection when input changes");
267
+ test.todo("should reset scroll offset when input changes");
268
+ test.todo("should not reset when typing same character");
269
+ });
270
+
271
+ describe("currentPath changes", () => {
272
+ test.todo("should reset scroll offset when navigating to new directory");
273
+ test.todo("should reset selection when navigating to new directory");
274
+ test.todo("should reload directory entries for new path");
275
+ });
276
+
277
+ describe("tab completion", () => {
278
+ test.todo("should cycle through completions when pressing Tab repeatedly");
279
+ test.todo("should add trailing slash after completing directory");
280
+ test.todo("should update currentPath to completed directory");
281
+ test.todo("should update folder list to show completed directory's contents");
282
+ });
283
+
284
+ describe("backspace handling", () => {
285
+ test.todo("should allow backspacing initial tilde");
286
+ test.todo("should allow backspacing to empty input");
287
+ test.todo("should reset selection when backspacing");
288
+ test.todo("should reset scroll offset when backspacing");
289
+ });
290
+
291
+ describe("escape key handling", () => {
292
+ test.todo("should go to parent directory when input ends with /");
293
+ test.todo("should clear input when input has text beyond ~");
294
+ test.todo("should cancel when input is just ~");
295
+ test.todo("should reset all state when going back");
296
+ });
297
+
298
+ describe("Ctrl+U handling", () => {
299
+ test.todo("should reset input to ~");
300
+ test.todo("should reset selection");
301
+ test.todo("should reset scroll offset");
302
+ test.todo("should clear errors");
303
+ });
304
+
305
+ describe("folder filtering logic", () => {
306
+ test.todo("should filter by name when input has no slashes");
307
+ test.todo("should NOT filter when input starts with /");
308
+ test.todo("should NOT filter when input contains /");
309
+ test.todo("should show all folders when typing /Volumes");
310
+ test.todo("should show all folders when typing ~/code/proj");
311
+ });
312
+
313
+ describe("dynamic currentPath updates", () => {
314
+ test.todo("should update currentPath when input is valid directory");
315
+ test.todo("should expand ~ before validating");
316
+ test.todo("should reload entries when currentPath changes");
317
+ test.todo("should default to startingPath when input is empty");
318
+ });
319
+ });
320
+
321
+ describe("Integration Scenarios", () => {
322
+ test.todo("scenario: User types /Volumes/Storage/code and selects it", () => {
323
+ // Full workflow test
324
+ });
325
+
326
+ test.todo("scenario: User filters by typing 'proj' and navigates", () => {
327
+ // Filter workflow
328
+ });
329
+
330
+ test.todo("scenario: User starts with ~, backspaces, types absolute path", () => {
331
+ // Home to absolute path workflow
332
+ });
333
+
334
+ test.todo("scenario: User navigates deep into hierarchy, uses Esc to go back", () => {
335
+ // Navigation and back workflow
336
+ });
337
+
338
+ test.todo("scenario: User tabs through completions, cycles correctly", () => {
339
+ // Tab completion workflow
340
+ });
341
+
342
+ test.todo("scenario: Many folders, user scrolls through list", () => {
343
+ // Scrolling workflow
344
+ });
345
+ });
@@ -0,0 +1,268 @@
1
+ import { useState, useEffect } from "react";
2
+ import { Box, Text, useInput } from "ink";
3
+ import { getAuthStatus, login, isGhInstalled } from "../../github/auth.ts";
4
+
5
+ export interface GitHubAuthStepProps {
6
+ onComplete: (auth: { authenticated: boolean; user?: string; skipped: boolean }) => void;
7
+ onBack: () => void;
8
+ onCancel: () => void;
9
+ }
10
+
11
+ type AuthStatus = "checking" | "authenticated" | "not_authenticated" | "gh_not_installed" | "login_failed";
12
+
13
+ export function GitHubAuthStep({ onComplete, onBack, onCancel }: GitHubAuthStepProps) {
14
+ const [status, setStatus] = useState<AuthStatus>("checking");
15
+ const [user, setUser] = useState<string | undefined>();
16
+ const [loading, setLoading] = useState(false);
17
+ const [error, setError] = useState<string | null>(null);
18
+
19
+ useEffect(() => {
20
+ checkAuth();
21
+ }, []);
22
+
23
+ const checkAuth = async () => {
24
+ setStatus("checking");
25
+ setError(null);
26
+
27
+ const ghInstalled = await isGhInstalled();
28
+
29
+ if (!ghInstalled) {
30
+ setStatus("gh_not_installed");
31
+ return;
32
+ }
33
+
34
+ try {
35
+ const authStatus = await getAuthStatus();
36
+ if (authStatus.authenticated) {
37
+ setStatus("authenticated");
38
+ setUser(authStatus.user);
39
+ } else {
40
+ setStatus("not_authenticated");
41
+ }
42
+ } catch {
43
+ setStatus("not_authenticated");
44
+ }
45
+ };
46
+
47
+ const handleLogin = async () => {
48
+ setLoading(true);
49
+ setError(null);
50
+
51
+ try {
52
+ const result = await login();
53
+ if (result.success) {
54
+ setStatus("authenticated");
55
+ setUser(result.user);
56
+ } else {
57
+ setStatus("login_failed");
58
+ setError(result.error || "Login failed");
59
+ }
60
+ } catch {
61
+ setStatus("login_failed");
62
+ setError("An unexpected error occurred");
63
+ } finally {
64
+ setLoading(false);
65
+ }
66
+ };
67
+
68
+ useInput((input, key) => {
69
+ if (loading) return;
70
+
71
+ if (key.escape || input === "q" || input === "Q") {
72
+ onCancel();
73
+ return;
74
+ }
75
+
76
+ if (key.backspace || key.delete) {
77
+ onBack();
78
+ return;
79
+ }
80
+
81
+ if (key.return) {
82
+ // Complete with current auth state
83
+ onComplete({
84
+ authenticated: status === "authenticated",
85
+ user,
86
+ skipped: false,
87
+ });
88
+ return;
89
+ }
90
+
91
+ // Login
92
+ if ((input === "l" || input === "L") && (status === "not_authenticated" || status === "login_failed")) {
93
+ handleLogin();
94
+ return;
95
+ }
96
+
97
+ // Re-login
98
+ if ((input === "r" || input === "R") && status === "authenticated") {
99
+ handleLogin();
100
+ return;
101
+ }
102
+
103
+ // Skip
104
+ if (input === "s" || input === "S") {
105
+ onComplete({
106
+ authenticated: false,
107
+ user: undefined,
108
+ skipped: true,
109
+ });
110
+ return;
111
+ }
112
+ });
113
+
114
+ return (
115
+ <Box
116
+ flexDirection="column"
117
+ borderStyle="round"
118
+ borderColor="cyan"
119
+ paddingX={2}
120
+ paddingY={1}
121
+ width={80}
122
+ >
123
+ {/* Title */}
124
+ <Box marginBottom={1}>
125
+ <Text bold color="cyan">
126
+ GitHub Authentication
127
+ </Text>
128
+ </Box>
129
+
130
+ {/* Description */}
131
+ <Box marginBottom={1}>
132
+ <Text>GitHub integration is optional but recommended for full functionality.</Text>
133
+ </Box>
134
+
135
+ {/* Checking status */}
136
+ {status === "checking" && (
137
+ <Box marginBottom={1}>
138
+ <Text dimColor>Checking authentication status...</Text>
139
+ </Box>
140
+ )}
141
+
142
+ {/* Authenticated */}
143
+ {status === "authenticated" && (
144
+ <Box flexDirection="column" marginBottom={1}>
145
+ <Box marginBottom={1}>
146
+ <Text color="green">✓ </Text>
147
+ <Text color="green" bold>
148
+ Authenticated{user ? ` as ${user}` : ""}
149
+ </Text>
150
+ </Box>
151
+ <Text dimColor>You're all set! GitHub features are ready to use.</Text>
152
+ </Box>
153
+ )}
154
+
155
+ {/* Not authenticated */}
156
+ {status === "not_authenticated" && !loading && (
157
+ <Box flexDirection="column" marginBottom={1}>
158
+ <Box marginBottom={1}>
159
+ <Text color="yellow">○ </Text>
160
+ <Text color="yellow">Not authenticated</Text>
161
+ </Box>
162
+ <Text dimColor>GitHub CLI (gh) is installed and ready.</Text>
163
+ </Box>
164
+ )}
165
+
166
+ {/* gh not installed */}
167
+ {status === "gh_not_installed" && (
168
+ <Box flexDirection="column" marginBottom={1}>
169
+ <Box marginBottom={1}>
170
+ <Text color="red">✗ </Text>
171
+ <Text color="red">GitHub CLI (gh) is not installed</Text>
172
+ </Box>
173
+ <Box flexDirection="column" marginBottom={1}>
174
+ <Text dimColor>To enable GitHub features:</Text>
175
+ <Text dimColor> 1. Install gh CLI from https://cli.github.com</Text>
176
+ <Text dimColor> 2. Run 'gitforest login' to authenticate</Text>
177
+ <Text dimColor> 3. Or set GITHUB_TOKEN environment variable</Text>
178
+ </Box>
179
+ </Box>
180
+ )}
181
+
182
+ {/* Login failed */}
183
+ {status === "login_failed" && !loading && (
184
+ <Box flexDirection="column" marginBottom={1}>
185
+ <Box marginBottom={1}>
186
+ <Text color="red">✗ </Text>
187
+ <Text color="red">Authentication failed</Text>
188
+ </Box>
189
+ {error && (
190
+ <Box marginBottom={1}>
191
+ <Text color="red">{error}</Text>
192
+ </Box>
193
+ )}
194
+ <Text dimColor>You can try again or skip for now.</Text>
195
+ </Box>
196
+ )}
197
+
198
+ {/* Loading */}
199
+ {loading && (
200
+ <Box marginBottom={1}>
201
+ <Text dimColor>Opening browser for authentication...</Text>
202
+ </Box>
203
+ )}
204
+
205
+ {/* Actions */}
206
+ {!loading && status !== "checking" && (
207
+ <Box flexDirection="column" marginTop={1}>
208
+ {status === "not_authenticated" || status === "login_failed" ? (
209
+ <Box gap={2}>
210
+ <Text dimColor>Press </Text>
211
+ <Text color="green" bold>
212
+ l
213
+ </Text>
214
+ <Text dimColor>Login</Text>
215
+ <Text color="green" bold>
216
+ s
217
+ </Text>
218
+ <Text dimColor>Skip</Text>
219
+ <Text color="green" bold>
220
+ Enter
221
+ </Text>
222
+ <Text dimColor>Continue</Text>
223
+ </Box>
224
+ ) : status === "authenticated" ? (
225
+ <Box gap={2}>
226
+ <Text dimColor>Press </Text>
227
+ <Text color="green" bold>
228
+ r
229
+ </Text>
230
+ <Text dimColor>Re-login</Text>
231
+ <Text color="green" bold>
232
+ s
233
+ </Text>
234
+ <Text dimColor>Skip</Text>
235
+ <Text color="green" bold>
236
+ Enter
237
+ </Text>
238
+ <Text dimColor>Continue</Text>
239
+ </Box>
240
+ ) : status === "gh_not_installed" ? (
241
+ <Box gap={2}>
242
+ <Text dimColor>Press </Text>
243
+ <Text color="green" bold>
244
+ s
245
+ </Text>
246
+ <Text dimColor>Skip</Text>
247
+ <Text color="green" bold>
248
+ Enter
249
+ </Text>
250
+ <Text dimColor>Continue</Text>
251
+ </Box>
252
+ ) : null}
253
+ <Box gap={2}>
254
+ <Text dimColor></Text>
255
+ <Text color="red" bold>
256
+ Backspace
257
+ </Text>
258
+ <Text dimColor>Back</Text>
259
+ <Text color="red" bold>
260
+ q
261
+ </Text>
262
+ <Text dimColor>Quit</Text>
263
+ </Box>
264
+ </Box>
265
+ )}
266
+ </Box>
267
+ );
268
+ }