gitforest 0.1.0 → 1.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 (184) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +24 -4
  3. package/src/components/onboarding/DirectoriesStep.tsx +19 -19
  4. package/src/github/auth.ts +3 -3
  5. package/src/utils/debug.ts +4 -4
  6. package/.bunignore +0 -7
  7. package/.github/workflows/ci.yml +0 -73
  8. package/CLAUDE.md +0 -111
  9. package/CONTRIBUTING.md +0 -145
  10. package/bun.lock +0 -267
  11. package/bunfig.toml +0 -15
  12. package/cli +0 -0
  13. package/docs/ai/IMPROVEMENT_PLAN.md +0 -341
  14. package/docs/ai/VERIFICATION_REPORT.md +0 -87
  15. package/docs/ai/architecture.md +0 -169
  16. package/docs/ai/checks/check-2025-12-02-tests.md +0 -40
  17. package/docs/ai/checks/check-2025-12-02.md +0 -55
  18. package/docs/ai/checks/test-verification-report.md +0 -85
  19. package/docs/ai/implementation-guide.md +0 -776
  20. package/docs/ai/research/gitty-codebase-analysis.md +0 -221
  21. package/docs/ai/tickets/GENERAL-sitrep.md +0 -30
  22. package/docs/ai/tickets/TASK-database-tests-sitrep.md +0 -25
  23. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +0 -28
  24. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +0 -28
  25. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +0 -24
  26. package/docs/ai/tickets/TASK-github-service-sitrep.md +0 -32
  27. package/docs/ai/tickets/TASK-github-token-sitrep.md +0 -51
  28. package/docs/ai/tickets/TASK-hascommits-sitrep.md +0 -35
  29. package/docs/ai/tickets/TASK-keybindings-sitrep.md +0 -26
  30. package/docs/ai/tickets/TASK-layout-sitrep.md +0 -25
  31. package/docs/ai/tickets/TASK-markdown-sitrep.md +0 -28
  32. package/docs/ai/tickets/TASK-project-item-sitrep.md +0 -79
  33. package/docs/ai/tickets/TASK-sitrep.md +0 -28
  34. package/docs/ai/tickets/TASK-state-sitrep.md +0 -26
  35. package/docs/ai/tickets/TASK-types-sitrep.md +0 -25
  36. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +0 -26
  37. package/docs/ai/tickets/TKT-001-sitrep.md +0 -24
  38. package/docs/ai/tickets/TKT-002-sitrep.md +0 -25
  39. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +0 -46
  40. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +0 -135
  41. package/docs/ai/tickets/TKT-003-sitrep.md +0 -26
  42. package/docs/ai/tickets/TKT-004-sitrep.md +0 -27
  43. package/docs/ai/tickets/TKT-005-sitrep.md +0 -25
  44. package/docs/ai/tickets/TKT-006-sitrep.md +0 -26
  45. package/docs/ai/tickets/TKT-007-sitrep.md +0 -30
  46. package/docs/ai/tickets/TKT-008-sitrep.md +0 -32
  47. package/docs/ai/tickets/TKT-009-sitrep.md +0 -27
  48. package/docs/ai/tickets/TKT-010-sitrep.md +0 -27
  49. package/docs/ai/tickets/TKT-011-sitrep.md +0 -26
  50. package/docs/ai/tickets/TKT-012-sitrep.md +0 -25
  51. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +0 -28
  52. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +0 -25
  53. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +0 -25
  54. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +0 -24
  55. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +0 -29
  56. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +0 -29
  57. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +0 -26
  58. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +0 -30
  59. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +0 -28
  60. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +0 -26
  61. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +0 -25
  62. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +0 -30
  63. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +0 -25
  64. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +0 -29
  65. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +0 -95
  66. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +0 -61
  67. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +0 -30
  68. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +0 -27
  69. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +0 -25
  70. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +0 -27
  71. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +0 -25
  72. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +0 -28
  73. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +0 -25
  74. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +0 -26
  75. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +0 -25
  76. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +0 -26
  77. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +0 -24
  78. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +0 -25
  79. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +0 -29
  80. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +0 -25
  81. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +0 -24
  82. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +0 -24
  83. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +0 -27
  84. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +0 -25
  85. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +0 -25
  86. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +0 -27
  87. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +0 -32
  88. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +0 -27
  89. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +0 -30
  90. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +0 -25
  91. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +0 -29
  92. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +0 -25
  93. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +0 -27
  94. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +0 -25
  95. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +0 -26
  96. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +0 -25
  97. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +0 -27
  98. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +0 -75
  99. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +0 -29
  100. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +0 -29
  101. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +0 -25
  102. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +0 -25
  103. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +0 -32
  104. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +0 -64
  105. package/docs/ai/tkt-001-fix-database-error.md +0 -217
  106. package/docs/ai/ui-enhancement-plan.md +0 -562
  107. package/test/integration/app.isolated.tsx +0 -240
  108. package/test/integration/cli-commands.test.ts +0 -287
  109. package/test/integration/cli-validation.test.ts +0 -264
  110. package/test/integration/git-operations.test.ts +0 -218
  111. package/test/integration/scanner.test.ts +0 -228
  112. package/test/preload.ts +0 -18
  113. package/test/unit/cli/commands.test.ts +0 -13
  114. package/test/unit/cli/formatters.test.ts +0 -1116
  115. package/test/unit/cli/github-commands.test.ts +0 -12
  116. package/test/unit/components/CloneDialog.test.tsx +0 -240
  117. package/test/unit/components/ColumnHeader.test.tsx +0 -128
  118. package/test/unit/components/CommandPalette.test.tsx +0 -355
  119. package/test/unit/components/ConfirmDialog.test.tsx +0 -111
  120. package/test/unit/components/ErrorBoundary.test.tsx +0 -139
  121. package/test/unit/components/FilterBar.test.tsx +0 -43
  122. package/test/unit/components/FilterOptionsOverlay.test.tsx +0 -197
  123. package/test/unit/components/HelpOverlay.test.tsx +0 -90
  124. package/test/unit/components/Layout.test.tsx +0 -328
  125. package/test/unit/components/MarkdownRenderer.test.tsx +0 -45
  126. package/test/unit/components/ProgressBar.test.tsx +0 -138
  127. package/test/unit/components/ProjectItem.test.tsx +0 -182
  128. package/test/unit/components/ProjectList.test.tsx +0 -311
  129. package/test/unit/components/RepoDetailModal.test.tsx +0 -445
  130. package/test/unit/components/StatusBar.test.tsx +0 -112
  131. package/test/unit/components/UnifiedProjectItem.test.tsx +0 -618
  132. package/test/unit/components/ViewModeIndicator.test.tsx +0 -137
  133. package/test/unit/components/test-utils.tsx +0 -63
  134. package/test/unit/config/loader.test.ts +0 -692
  135. package/test/unit/db/database.test.ts +0 -978
  136. package/test/unit/db/index.test.ts +0 -314
  137. package/test/unit/fixtures/setup.ts +0 -186
  138. package/test/unit/git/commands-untested.test.ts +0 -205
  139. package/test/unit/git/commands.test.ts +0 -269
  140. package/test/unit/git/operations.test.ts +0 -322
  141. package/test/unit/git/status.test.ts +0 -219
  142. package/test/unit/github/auth.test.ts +0 -317
  143. package/test/unit/github/cache.test.ts +0 -1028
  144. package/test/unit/github/cli.test.ts +0 -135
  145. package/test/unit/github/unified.test.ts +0 -1201
  146. package/test/unit/graceful-shutdown.test.ts +0 -83
  147. package/test/unit/hooks/useBackgroundFetch.test.tsx +0 -239
  148. package/test/unit/hooks/useConfirmDialogActions.test.tsx +0 -81
  149. package/test/unit/hooks/useKeyBindings.isolated.ts +0 -715
  150. package/test/unit/hooks/useProjects.test.tsx +0 -186
  151. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +0 -115
  152. package/test/unit/hooks/useUnifiedRepos.test.tsx +0 -177
  153. package/test/unit/mocks/config.ts +0 -109
  154. package/test/unit/mocks/git-service.ts +0 -274
  155. package/test/unit/mocks/github-service.ts +0 -250
  156. package/test/unit/mocks/index.ts +0 -72
  157. package/test/unit/mocks/project.ts +0 -148
  158. package/test/unit/mocks/state-mocks.ts +0 -187
  159. package/test/unit/mocks/unified.ts +0 -169
  160. package/test/unit/operations/batch.test.ts +0 -216
  161. package/test/unit/operations/commands.test.ts +0 -550
  162. package/test/unit/scanner/errors.test.ts +0 -297
  163. package/test/unit/scanner/index.test.ts +0 -1011
  164. package/test/unit/scanner/markers.test.ts +0 -150
  165. package/test/unit/scanner/submodules.test.ts +0 -99
  166. package/test/unit/services/git-errors.test.ts +0 -190
  167. package/test/unit/services/git.test.ts +0 -442
  168. package/test/unit/services/github-errors.test.ts +0 -293
  169. package/test/unit/services/github.test.ts +0 -200
  170. package/test/unit/state/actions.test.ts +0 -217
  171. package/test/unit/state/reducer.test.ts +0 -745
  172. package/test/unit/state/store.test.tsx +0 -711
  173. package/test/unit/types/commands.test.ts +0 -220
  174. package/test/unit/types/schema.test.ts +0 -179
  175. package/test/unit/utils/array.test.ts +0 -73
  176. package/test/unit/utils/debug.test.ts +0 -23
  177. package/test/unit/utils/errors.test.ts +0 -295
  178. package/test/unit/utils/markdown.test.ts +0 -163
  179. package/test/unit/utils/project-utils.test.ts +0 -756
  180. package/test/unit/utils/rate-limiter.test.ts +0 -256
  181. package/test/unit/utils/retry.test.ts +0 -165
  182. package/test/unit/utils/strip-ansi.ts +0 -13
  183. package/test/unit/utils/timeout.test.ts +0 -93
  184. package/tsconfig.json +0 -29
@@ -1,692 +0,0 @@
1
- import { describe, test, expect, beforeEach, afterEach } from "bun:test";
2
- import { join } from "path";
3
- import { mkdtemp, rm, writeFile } from "fs/promises";
4
- import { tmpdir } from "os";
5
- import { stringify } from "yaml";
6
- import { loadConfig, findConfigPath, applyEnvOverrides, getSupportedEnvVars } from "../../../src/config/loader.ts";
7
- import type { GitforestConfig } from "../../../src/types/index.ts";
8
- import {
9
- createTempDir,
10
- cleanupTempDir,
11
- createMockConfig,
12
- createMockYamlConfig,
13
- } from "../fixtures/setup.ts";
14
-
15
- describe("findConfigPath", () => {
16
- let tempDir: string;
17
-
18
- beforeEach(() => {
19
- tempDir = createTempDir("config-test");
20
- });
21
-
22
- afterEach(() => {
23
- cleanupTempDir(tempDir);
24
- });
25
-
26
- test("finds gitforest.config.yaml in current directory first", () => {
27
- createMockYamlConfig(tempDir, "directories:\n - path: ~/test");
28
- const result = findConfigPath(tempDir);
29
- // Should find the config in specified directory (highest priority)
30
- expect(result).toBe(join(tempDir, "gitforest.config.yaml"));
31
- });
32
-
33
- test("finds gitforest.config.json in current directory", () => {
34
- createMockConfig(tempDir, { directories: [{ path: "~/test" }] });
35
- const result = findConfigPath(tempDir);
36
- expect(result).toBe(join(tempDir, "gitforest.config.json"));
37
- });
38
-
39
- test("prefers yaml over json in current directory", () => {
40
- createMockYamlConfig(tempDir, "directories:\n - path: ~/yaml");
41
- createMockConfig(tempDir, { directories: [{ path: "~/json" }] });
42
- const result = findConfigPath(tempDir);
43
- // YAML should be found first per priority order
44
- expect(result).toBe(join(tempDir, "gitforest.config.yaml"));
45
- });
46
- });
47
-
48
- describe("loadConfig", () => {
49
- let tempDir: string;
50
-
51
- beforeEach(() => {
52
- tempDir = createTempDir("config-load-test");
53
- });
54
-
55
- afterEach(() => {
56
- cleanupTempDir(tempDir);
57
- });
58
-
59
- test("loads valid JSON config", async () => {
60
- const configPath = createMockConfig(tempDir, {
61
- directories: [{ path: "~/projects", maxDepth: 2 }],
62
- });
63
-
64
- const config = await loadConfig(configPath);
65
-
66
- expect(config.directories).toHaveLength(1);
67
- expect(config.directories[0]?.maxDepth).toBe(2);
68
- });
69
-
70
- test("loads valid YAML config", async () => {
71
- const configPath = createMockYamlConfig(
72
- tempDir,
73
- `
74
- directories:
75
- - path: ~/projects
76
- maxDepth: 3
77
- label: Projects
78
- - path: ~/work
79
- maxDepth: 2
80
-
81
- scan:
82
- ignore:
83
- - node_modules
84
- - vendor
85
- includeHidden: false
86
-
87
- github:
88
- defaultVisibility: private
89
- `
90
- );
91
-
92
- const config = await loadConfig(configPath);
93
-
94
- expect(config.directories).toHaveLength(2);
95
- expect(config.directories[0]?.maxDepth).toBe(3);
96
- expect(config.directories[0]?.label).toBe("Projects");
97
- expect(config.scan.ignore).toContain("vendor");
98
- expect(config.github.defaultVisibility).toBe("private");
99
- });
100
-
101
- test("expands tilde in paths", async () => {
102
- const configPath = createMockConfig(tempDir, {
103
- directories: [{ path: "~/projects" }],
104
- });
105
-
106
- const config = await loadConfig(configPath);
107
-
108
- expect(config.directories[0]?.path).not.toContain("~");
109
- expect(config.directories[0]?.path).toContain(process.env.HOME || "");
110
- });
111
-
112
- test("throws error for invalid config", async () => {
113
- const configPath = createMockConfig(tempDir, {
114
- directories: [], // Invalid: empty array
115
- });
116
-
117
- await expect(loadConfig(configPath)).rejects.toThrow();
118
- });
119
-
120
- test("throws error for missing required field", async () => {
121
- const configPath = createMockConfig(tempDir, {
122
- // Missing directories field
123
- scan: { ignore: [] },
124
- });
125
-
126
- await expect(loadConfig(configPath)).rejects.toThrow();
127
- });
128
-
129
- test("throws error when config file does not exist", async () => {
130
- const nonExistentPath = join(tempDir, "non-existent.yaml");
131
- await expect(loadConfig(nonExistentPath)).rejects.toThrow();
132
- });
133
-
134
- test("applies default values for optional fields", async () => {
135
- const configPath = createMockConfig(tempDir, {
136
- directories: [{ path: "~/projects" }],
137
- });
138
-
139
- const config = await loadConfig(configPath);
140
-
141
- expect(config.scan.concurrency).toBe(5);
142
- expect(config.display.sortBy).toBe("status");
143
- expect(config.cache.ttlSeconds).toBe(300);
144
- });
145
- });
146
-
147
- describe("applyEnvOverrides", () => {
148
- // Store original env values
149
- const originalEnv: Record<string, string | undefined> = {};
150
- const envVarsToClean = [
151
- "GITTY_GITHUB_VISIBILITY",
152
- "GITTY_CONCURRENCY",
153
- "GITTY_INCLUDE_HIDDEN",
154
- "GITTY_SORT_BY",
155
- "GITTY_SORT_DIR",
156
- "GITTY_SHOW_SUBMODULES",
157
- "GITTY_CACHE_TTL",
158
- ];
159
-
160
- const createBaseConfig = (): GitforestConfig => ({
161
- directories: [{ path: "/home/user/projects", maxDepth: 2 }],
162
- scan: {
163
- concurrency: 5,
164
- ignore: ["node_modules"],
165
- includeHidden: false,
166
- },
167
- display: {
168
- showSubmodules: true,
169
- showNonGitProjects: true,
170
- sortBy: "status",
171
- sortDirection: "desc",
172
- },
173
- github: {
174
- defaultVisibility: "private",
175
- },
176
- cache: {
177
- ttlSeconds: 300,
178
- githubTtlSeconds: 600,
179
- enableBackgroundRefresh: true,
180
- backgroundRefreshIntervalSeconds: 300,
181
- },
182
- commands: [],
183
- });
184
-
185
- beforeEach(() => {
186
- // Save original values
187
- for (const key of envVarsToClean) {
188
- originalEnv[key] = process.env[key];
189
- delete process.env[key];
190
- }
191
- });
192
-
193
- afterEach(() => {
194
- // Restore original values
195
- for (const key of envVarsToClean) {
196
- if (originalEnv[key] !== undefined) {
197
- process.env[key] = originalEnv[key];
198
- } else {
199
- delete process.env[key];
200
- }
201
- }
202
- });
203
-
204
- test("returns config unchanged when no env vars set", () => {
205
- const config = createBaseConfig();
206
- const result = applyEnvOverrides(config);
207
-
208
- expect(result).toEqual(config);
209
- });
210
-
211
- test("overrides github.defaultVisibility from GITTY_GITHUB_VISIBILITY", () => {
212
- process.env.GITTY_GITHUB_VISIBILITY = "public";
213
- const config = createBaseConfig();
214
-
215
- const result = applyEnvOverrides(config);
216
-
217
- expect(result.github?.defaultVisibility).toBe("public");
218
- });
219
-
220
- test("overrides scan.concurrency from GITTY_CONCURRENCY", () => {
221
- process.env.GITTY_CONCURRENCY = "10";
222
- const config = createBaseConfig();
223
-
224
- const result = applyEnvOverrides(config);
225
-
226
- expect(result.scan?.concurrency).toBe(10);
227
- });
228
-
229
- test("overrides scan.includeHidden from GITTY_INCLUDE_HIDDEN", () => {
230
- process.env.GITTY_INCLUDE_HIDDEN = "true";
231
- const config = createBaseConfig();
232
-
233
- const result = applyEnvOverrides(config);
234
-
235
- expect(result.scan?.includeHidden).toBe(true);
236
- });
237
-
238
- test("overrides display.sortBy from GITTY_SORT_BY", () => {
239
- process.env.GITTY_SORT_BY = "name";
240
- const config = createBaseConfig();
241
-
242
- const result = applyEnvOverrides(config);
243
-
244
- expect(result.display?.sortBy).toBe("name");
245
- });
246
-
247
- test("overrides display.sortDirection from GITTY_SORT_DIR", () => {
248
- process.env.GITTY_SORT_DIR = "asc";
249
- const config = createBaseConfig();
250
-
251
- const result = applyEnvOverrides(config);
252
-
253
- expect(result.display?.sortDirection).toBe("asc");
254
- });
255
-
256
- test("overrides display.showSubmodules from GITTY_SHOW_SUBMODULES", () => {
257
- process.env.GITTY_SHOW_SUBMODULES = "false";
258
- const config = createBaseConfig();
259
-
260
- const result = applyEnvOverrides(config);
261
-
262
- expect(result.display?.showSubmodules).toBe(false);
263
- });
264
-
265
- test("overrides cache.ttlSeconds from GITTY_CACHE_TTL", () => {
266
- process.env.GITTY_CACHE_TTL = "600";
267
- const config = createBaseConfig();
268
-
269
- const result = applyEnvOverrides(config);
270
-
271
- expect(result.cache?.ttlSeconds).toBe(600);
272
- });
273
-
274
- test("applies multiple env overrides at once", () => {
275
- process.env.GITTY_GITHUB_VISIBILITY = "public";
276
- process.env.GITTY_CONCURRENCY = "20";
277
- process.env.GITTY_SORT_BY = "lastActivity";
278
- process.env.GITTY_CACHE_TTL = "1200";
279
- const config = createBaseConfig();
280
-
281
- const result = applyEnvOverrides(config);
282
-
283
- expect(result.github?.defaultVisibility).toBe("public");
284
- expect(result.scan?.concurrency).toBe(20);
285
- expect(result.display?.sortBy).toBe("lastActivity");
286
- expect(result.cache?.ttlSeconds).toBe(1200);
287
- });
288
-
289
- test("does not modify original config", () => {
290
- process.env.GITTY_CONCURRENCY = "15";
291
- const config = createBaseConfig();
292
- const originalConcurrency = config.scan?.concurrency;
293
-
294
- applyEnvOverrides(config);
295
-
296
- expect(config.scan?.concurrency).toBe(originalConcurrency);
297
- });
298
-
299
- test("ignores empty string env values", () => {
300
- process.env.GITTY_GITHUB_VISIBILITY = "";
301
- const config = createBaseConfig();
302
-
303
- const result = applyEnvOverrides(config);
304
-
305
- // Empty env values should be ignored, keeping the original value
306
- expect(result.github?.defaultVisibility).toBe("private");
307
- });
308
-
309
- test("creates missing parent objects", () => {
310
- process.env.GITTY_CACHE_TTL = "900";
311
- // Start with a minimal but complete config
312
- const config: GitforestConfig = {
313
- directories: [{ path: "/test", maxDepth: 1 }],
314
- scan: { ignore: [], includeHidden: false, concurrency: 4 },
315
- github: { defaultVisibility: "private" },
316
- display: { showSubmodules: true, showNonGitProjects: true, sortBy: "status", sortDirection: "desc" },
317
- cache: {
318
- ttlSeconds: 300,
319
- githubTtlSeconds: 600,
320
- enableBackgroundRefresh: true,
321
- backgroundRefreshIntervalSeconds: 300,
322
- },
323
- commands: [],
324
- };
325
-
326
- const result = applyEnvOverrides(config);
327
-
328
- expect(result.cache?.ttlSeconds).toBe(900);
329
- });
330
- });
331
-
332
- describe("getSupportedEnvVars", () => {
333
- test("returns list of supported env vars", () => {
334
- const envVars = getSupportedEnvVars();
335
-
336
- expect(envVars.length).toBeGreaterThan(0);
337
- expect(envVars.some((v) => v.env === "GITTY_GITHUB_VISIBILITY")).toBe(true);
338
- expect(envVars.some((v) => v.env === "GITTY_CONCURRENCY")).toBe(true);
339
- });
340
-
341
- test("each env var has required properties", () => {
342
- const envVars = getSupportedEnvVars();
343
-
344
- for (const envVar of envVars) {
345
- expect(envVar.env).toBeDefined();
346
- expect(envVar.path).toBeDefined();
347
- expect(envVar.description).toBeDefined();
348
- expect(envVar.env.length).toBeGreaterThan(0);
349
- expect(envVar.path.length).toBeGreaterThan(0);
350
- }
351
- });
352
-
353
- test("path format is dot-separated", () => {
354
- const envVars = getSupportedEnvVars();
355
- const visibility = envVars.find((v) => v.env === "GITTY_GITHUB_VISIBILITY");
356
-
357
- expect(visibility?.path).toBe("github.defaultVisibility");
358
- });
359
- });
360
-
361
- describe("loadConfig - comprehensive tests", () => {
362
- let tempDir: string;
363
-
364
- beforeEach(async () => {
365
- tempDir = await mkdtemp(join(tmpdir(), "gitforest-config-test-"));
366
- });
367
-
368
- afterEach(async () => {
369
- await rm(tempDir, { recursive: true, force: true });
370
- });
371
-
372
- describe("loadConfig from default location", () => {
373
- test("loads config from default location when no path provided", async () => {
374
- const configContent = {
375
- directories: [{ path: "~/projects", maxDepth: 2 }]
376
- };
377
- await writeFile(join(tempDir, "gitforest.config.yaml"), stringify(configContent));
378
-
379
- const config = await loadConfig(undefined, tempDir);
380
- expect(config.directories).toHaveLength(1);
381
- expect(config.directories[0]?.path).toContain(process.env.HOME || "");
382
- });
383
-
384
- test("loads config from custom path", async () => {
385
- const configPath = join(tempDir, "custom-config.yaml");
386
- const configContent = {
387
- directories: [{ path: "/test/path", maxDepth: 3 }]
388
- };
389
- await writeFile(configPath, stringify(configContent));
390
-
391
- const config = await loadConfig(configPath);
392
- expect(config.directories[0]?.path).toBe("/test/path");
393
- expect(config.directories[0]?.maxDepth).toBe(3);
394
- });
395
-
396
- test("throws error when specified config file does not exist", async () => {
397
- // Test with a specific path that doesn't exist
398
- const nonExistentPath = join(tempDir, "does-not-exist.yaml");
399
- await expect(loadConfig(nonExistentPath)).rejects.toThrow();
400
- });
401
-
402
- test("handles YAML parse errors gracefully", async () => {
403
- const configPath = join(tempDir, "invalid.yaml");
404
- await writeFile(configPath, "invalid: yaml: content: [");
405
-
406
- await expect(loadConfig(configPath)).rejects.toThrow();
407
- });
408
-
409
- test("handles invalid config structure", async () => {
410
- const configPath = join(tempDir, "invalid-structure.yaml");
411
- const invalidConfig = {
412
- directories: [{ path: 123 }], // path should be string
413
- };
414
- await writeFile(configPath, stringify(invalidConfig));
415
-
416
- await expect(loadConfig(configPath)).rejects.toThrow(/Invalid config file/);
417
- });
418
-
419
- test("expands ~ in directory paths", async () => {
420
- const configPath = join(tempDir, "tilde-config.yaml");
421
- const configContent = {
422
- directories: [
423
- { path: "~/projects" },
424
- { path: "~/work/test" },
425
- { path: "/absolute/path" }
426
- ]
427
- };
428
- await writeFile(configPath, stringify(configContent));
429
-
430
- const config = await loadConfig(configPath);
431
- expect(config.directories[0]?.path).toBe(join(process.env.HOME || "", "projects"));
432
- expect(config.directories[1]?.path).toBe(join(process.env.HOME || "", "work/test"));
433
- expect(config.directories[2]?.path).toBe("/absolute/path");
434
- });
435
-
436
- test("expands environment variables in paths", async () => {
437
- process.env.TEST_PROJECTS_DIR = "/test/projects";
438
- const configPath = join(tempDir, "env-var-config.yaml");
439
- const configContent = {
440
- directories: [
441
- { path: "${TEST_PROJECTS_DIR}/frontend" },
442
- { path: "~/backend" }
443
- ]
444
- };
445
- await writeFile(configPath, stringify(configContent));
446
-
447
- const config = await loadConfig(configPath);
448
- // Note: The current implementation doesn't expand env vars, this is expected behavior
449
- expect(config.directories[0]?.path).toBe("${TEST_PROJECTS_DIR}/frontend");
450
- delete process.env.TEST_PROJECTS_DIR;
451
- });
452
- });
453
-
454
- describe("Validation tests", () => {
455
- test("validates required directories field exists", async () => {
456
- const configPath = join(tempDir, "no-directories.yaml");
457
- const configContent = {
458
- scan: { ignore: ["node_modules"] }
459
- };
460
- await writeFile(configPath, stringify(configContent));
461
-
462
- await expect(loadConfig(configPath)).rejects.toThrow(/Invalid config file/);
463
- });
464
-
465
- test("validates directory path is non-empty", async () => {
466
- const configPath = join(tempDir, "empty-path.yaml");
467
- const configContent = {
468
- directories: [{ path: "" }]
469
- };
470
- await writeFile(configPath, stringify(configContent));
471
-
472
- await expect(loadConfig(configPath)).rejects.toThrow(/Invalid config file/);
473
- });
474
-
475
- test("validates maxDepth is within range (0-10)", async () => {
476
- const configPath = join(tempDir, "invalid-depth.yaml");
477
- const configContent = {
478
- directories: [{ path: "~/test", maxDepth: 15 }]
479
- };
480
- await writeFile(configPath, stringify(configContent));
481
-
482
- await expect(loadConfig(configPath)).rejects.toThrow(/Invalid config file/);
483
- });
484
-
485
- test("validates scan.concurrency is within range (1-20)", async () => {
486
- const configPath = join(tempDir, "invalid-concurrency.yaml");
487
- const configContent = {
488
- directories: [{ path: "~/test" }],
489
- scan: { concurrency: 25 }
490
- };
491
- await writeFile(configPath, stringify(configContent));
492
-
493
- await expect(loadConfig(configPath)).rejects.toThrow(/Invalid config file/);
494
- });
495
-
496
- test("validates command keys are not reserved", async () => {
497
- const configPath = join(tempDir, "reserved-key.yaml");
498
- const configContent = {
499
- directories: [{ path: "~/test" }],
500
- commands: [
501
- { name: "test", key: "j", command: "echo test" } // 'j' is reserved
502
- ]
503
- };
504
- await writeFile(configPath, stringify(configContent));
505
-
506
- await expect(loadConfig(configPath)).rejects.toThrow(/reserved/);
507
- });
508
-
509
- test("validates no duplicate command keys", async () => {
510
- const configPath = join(tempDir, "duplicate-keys.yaml");
511
- const configContent = {
512
- directories: [{ path: "~/test" }],
513
- commands: [
514
- { name: "test1", key: "t", command: "echo test1" },
515
- { name: "test2", key: "t", command: "echo test2" }
516
- ]
517
- };
518
- await writeFile(configPath, stringify(configContent));
519
-
520
- await expect(loadConfig(configPath)).rejects.toThrow(/Duplicate command keys/);
521
- });
522
-
523
- test("validates command key is single character", async () => {
524
- const configPath = join(tempDir, "multi-char-key.yaml");
525
- const configContent = {
526
- directories: [{ path: "~/test" }],
527
- commands: [
528
- { name: "test", key: "abc", command: "echo test" }
529
- ]
530
- };
531
- await writeFile(configPath, stringify(configContent));
532
-
533
- await expect(loadConfig(configPath)).rejects.toThrow(/Invalid config file/);
534
- });
535
- });
536
-
537
- describe("Default values tests", () => {
538
- test("uses default ignore patterns", async () => {
539
- const configPath = join(tempDir, "default-ignore.yaml");
540
- const configContent = {
541
- directories: [{ path: "~/test" }]
542
- };
543
- await writeFile(configPath, stringify(configContent));
544
-
545
- const config = await loadConfig(configPath);
546
- expect(config.scan.ignore).toContain("node_modules");
547
- expect(config.scan.ignore).toContain(".git");
548
- expect(config.scan.ignore).toContain("vendor");
549
- expect(config.scan.ignore).toContain("__pycache__");
550
- expect(config.scan.ignore).toContain("target");
551
- expect(config.scan.ignore).toContain("dist");
552
- expect(config.scan.ignore).toContain("build");
553
- });
554
-
555
- test("uses default cache TTL", async () => {
556
- const configPath = join(tempDir, "default-ttl.yaml");
557
- const configContent = {
558
- directories: [{ path: "~/test" }]
559
- };
560
- await writeFile(configPath, stringify(configContent));
561
-
562
- const config = await loadConfig(configPath);
563
- expect(config.cache.ttlSeconds).toBe(300);
564
- expect(config.cache.githubTtlSeconds).toBe(600);
565
- });
566
-
567
- test("uses default concurrency", async () => {
568
- const configPath = join(tempDir, "default-concurrency.yaml");
569
- const configContent = {
570
- directories: [{ path: "~/test" }]
571
- };
572
- await writeFile(configPath, stringify(configContent));
573
-
574
- const config = await loadConfig(configPath);
575
- expect(config.scan.concurrency).toBe(5);
576
- });
577
-
578
- test("uses default visibility", async () => {
579
- const configPath = join(tempDir, "default-visibility.yaml");
580
- const configContent = {
581
- directories: [{ path: "~/test" }]
582
- };
583
- await writeFile(configPath, stringify(configContent));
584
-
585
- const config = await loadConfig(configPath);
586
- expect(config.github.defaultVisibility).toBe("private");
587
- });
588
-
589
- test("uses default display settings", async () => {
590
- const configPath = join(tempDir, "default-display.yaml");
591
- const configContent = {
592
- directories: [{ path: "~/test" }]
593
- };
594
- await writeFile(configPath, stringify(configContent));
595
-
596
- const config = await loadConfig(configPath);
597
- expect(config.display.showSubmodules).toBe(true);
598
- expect(config.display.showNonGitProjects).toBe(true);
599
- expect(config.display.sortBy).toBe("status");
600
- expect(config.display.sortDirection).toBe("desc");
601
- });
602
- });
603
-
604
- describe("Edge cases", () => {
605
- test("handles empty config file", async () => {
606
- const configPath = join(tempDir, "empty.yaml");
607
- await writeFile(configPath, "");
608
-
609
- await expect(loadConfig(configPath)).rejects.toThrow();
610
- });
611
-
612
- test("handles config with only directories", async () => {
613
- const configPath = join(tempDir, "minimal-config.yaml");
614
- const configContent = {
615
- directories: [{ path: "~/projects" }]
616
- };
617
- await writeFile(configPath, stringify(configContent));
618
-
619
- const config = await loadConfig(configPath);
620
- expect(config.directories).toHaveLength(1);
621
- expect(config.directories[0]?.path).toContain("projects");
622
- // All other fields should have defaults
623
- expect(config.scan).toBeDefined();
624
- expect(config.display).toBeDefined();
625
- expect(config.cache).toBeDefined();
626
- expect(config.github).toBeDefined();
627
- });
628
-
629
- test("handles config with all optional fields", async () => {
630
- const configPath = join(tempDir, "full-config.yaml");
631
- const configContent = {
632
- directories: [
633
- { path: "~/projects", maxDepth: 3, label: "My Projects" },
634
- { path: "~/work", maxDepth: 2, label: "Work Stuff" }
635
- ],
636
- scan: {
637
- ignore: ["custom_ignore"],
638
- includeHidden: true,
639
- concurrency: 10
640
- },
641
- github: {
642
- defaultVisibility: "public" as const
643
- },
644
- display: {
645
- showSubmodules: false,
646
- showNonGitProjects: false,
647
- sortBy: "name" as const,
648
- sortDirection: "asc" as const
649
- },
650
- cache: {
651
- ttlSeconds: 600,
652
- githubTtlSeconds: 1200,
653
- enableBackgroundRefresh: false,
654
- backgroundRefreshIntervalSeconds: 600
655
- },
656
- commands: [
657
- { name: "Custom Command", key: "z", command: "echo 'Custom!'", confirm: true, background: false }
658
- ]
659
- };
660
- await writeFile(configPath, stringify(configContent));
661
-
662
- const config = await loadConfig(configPath);
663
- expect(config.directories).toHaveLength(2);
664
- expect(config.scan.concurrency).toBe(10);
665
- expect(config.github.defaultVisibility).toBe("public");
666
- expect(config.display.sortBy).toBe("name");
667
- expect(config.display.sortDirection).toBe("asc");
668
- expect(config.cache.ttlSeconds).toBe(600);
669
- expect(config.commands).toHaveLength(1);
670
- expect(config.commands[0]?.key).toBe("z");
671
- });
672
-
673
- test("handles JSON config files", async () => {
674
- const configPath = join(tempDir, "config.json");
675
- const configContent = {
676
- directories: [{ path: "~/projects" }]
677
- };
678
- await writeFile(configPath, JSON.stringify(configContent));
679
-
680
- const config = await loadConfig(configPath);
681
- expect(config.directories).toHaveLength(1);
682
- expect(config.directories[0]?.path).toContain("projects");
683
- });
684
-
685
- test("throws error for unknown file format", async () => {
686
- const configPath = join(tempDir, "config.txt");
687
- await writeFile(configPath, "directories: []");
688
-
689
- await expect(loadConfig(configPath)).rejects.toThrow(/Unknown config file format/);
690
- });
691
- });
692
- });