gitforest 0.1.0 → 1.0.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 (183) hide show
  1. package/LICENSE +21 -0
  2. package/package.json +24 -4
  3. package/src/github/auth.ts +3 -3
  4. package/src/utils/debug.ts +4 -4
  5. package/.bunignore +0 -7
  6. package/.github/workflows/ci.yml +0 -73
  7. package/CLAUDE.md +0 -111
  8. package/CONTRIBUTING.md +0 -145
  9. package/bun.lock +0 -267
  10. package/bunfig.toml +0 -15
  11. package/cli +0 -0
  12. package/docs/ai/IMPROVEMENT_PLAN.md +0 -341
  13. package/docs/ai/VERIFICATION_REPORT.md +0 -87
  14. package/docs/ai/architecture.md +0 -169
  15. package/docs/ai/checks/check-2025-12-02-tests.md +0 -40
  16. package/docs/ai/checks/check-2025-12-02.md +0 -55
  17. package/docs/ai/checks/test-verification-report.md +0 -85
  18. package/docs/ai/implementation-guide.md +0 -776
  19. package/docs/ai/research/gitty-codebase-analysis.md +0 -221
  20. package/docs/ai/tickets/GENERAL-sitrep.md +0 -30
  21. package/docs/ai/tickets/TASK-database-tests-sitrep.md +0 -25
  22. package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +0 -28
  23. package/docs/ai/tickets/TASK-detail-modal-sitrep.md +0 -28
  24. package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +0 -24
  25. package/docs/ai/tickets/TASK-github-service-sitrep.md +0 -32
  26. package/docs/ai/tickets/TASK-github-token-sitrep.md +0 -51
  27. package/docs/ai/tickets/TASK-hascommits-sitrep.md +0 -35
  28. package/docs/ai/tickets/TASK-keybindings-sitrep.md +0 -26
  29. package/docs/ai/tickets/TASK-layout-sitrep.md +0 -25
  30. package/docs/ai/tickets/TASK-markdown-sitrep.md +0 -28
  31. package/docs/ai/tickets/TASK-project-item-sitrep.md +0 -79
  32. package/docs/ai/tickets/TASK-sitrep.md +0 -28
  33. package/docs/ai/tickets/TASK-state-sitrep.md +0 -26
  34. package/docs/ai/tickets/TASK-types-sitrep.md +0 -25
  35. package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +0 -26
  36. package/docs/ai/tickets/TKT-001-sitrep.md +0 -24
  37. package/docs/ai/tickets/TKT-002-sitrep.md +0 -25
  38. package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +0 -46
  39. package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +0 -135
  40. package/docs/ai/tickets/TKT-003-sitrep.md +0 -26
  41. package/docs/ai/tickets/TKT-004-sitrep.md +0 -27
  42. package/docs/ai/tickets/TKT-005-sitrep.md +0 -25
  43. package/docs/ai/tickets/TKT-006-sitrep.md +0 -26
  44. package/docs/ai/tickets/TKT-007-sitrep.md +0 -30
  45. package/docs/ai/tickets/TKT-008-sitrep.md +0 -32
  46. package/docs/ai/tickets/TKT-009-sitrep.md +0 -27
  47. package/docs/ai/tickets/TKT-010-sitrep.md +0 -27
  48. package/docs/ai/tickets/TKT-011-sitrep.md +0 -26
  49. package/docs/ai/tickets/TKT-012-sitrep.md +0 -25
  50. package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +0 -28
  51. package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +0 -25
  52. package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +0 -25
  53. package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +0 -24
  54. package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +0 -29
  55. package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +0 -29
  56. package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +0 -26
  57. package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +0 -30
  58. package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +0 -28
  59. package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +0 -26
  60. package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +0 -25
  61. package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +0 -30
  62. package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +0 -25
  63. package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +0 -29
  64. package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +0 -95
  65. package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +0 -61
  66. package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +0 -30
  67. package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +0 -27
  68. package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +0 -25
  69. package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +0 -27
  70. package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +0 -25
  71. package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +0 -28
  72. package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +0 -25
  73. package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +0 -26
  74. package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +0 -25
  75. package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +0 -26
  76. package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +0 -24
  77. package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +0 -25
  78. package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +0 -29
  79. package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +0 -25
  80. package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +0 -24
  81. package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +0 -24
  82. package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +0 -27
  83. package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +0 -25
  84. package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +0 -25
  85. package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +0 -27
  86. package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +0 -32
  87. package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +0 -27
  88. package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +0 -30
  89. package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +0 -25
  90. package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +0 -29
  91. package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +0 -25
  92. package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +0 -27
  93. package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +0 -25
  94. package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +0 -26
  95. package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +0 -25
  96. package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +0 -27
  97. package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +0 -75
  98. package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +0 -29
  99. package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +0 -29
  100. package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +0 -25
  101. package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +0 -25
  102. package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +0 -32
  103. package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +0 -64
  104. package/docs/ai/tkt-001-fix-database-error.md +0 -217
  105. package/docs/ai/ui-enhancement-plan.md +0 -562
  106. package/test/integration/app.isolated.tsx +0 -240
  107. package/test/integration/cli-commands.test.ts +0 -287
  108. package/test/integration/cli-validation.test.ts +0 -264
  109. package/test/integration/git-operations.test.ts +0 -218
  110. package/test/integration/scanner.test.ts +0 -228
  111. package/test/preload.ts +0 -18
  112. package/test/unit/cli/commands.test.ts +0 -13
  113. package/test/unit/cli/formatters.test.ts +0 -1116
  114. package/test/unit/cli/github-commands.test.ts +0 -12
  115. package/test/unit/components/CloneDialog.test.tsx +0 -240
  116. package/test/unit/components/ColumnHeader.test.tsx +0 -128
  117. package/test/unit/components/CommandPalette.test.tsx +0 -355
  118. package/test/unit/components/ConfirmDialog.test.tsx +0 -111
  119. package/test/unit/components/ErrorBoundary.test.tsx +0 -139
  120. package/test/unit/components/FilterBar.test.tsx +0 -43
  121. package/test/unit/components/FilterOptionsOverlay.test.tsx +0 -197
  122. package/test/unit/components/HelpOverlay.test.tsx +0 -90
  123. package/test/unit/components/Layout.test.tsx +0 -328
  124. package/test/unit/components/MarkdownRenderer.test.tsx +0 -45
  125. package/test/unit/components/ProgressBar.test.tsx +0 -138
  126. package/test/unit/components/ProjectItem.test.tsx +0 -182
  127. package/test/unit/components/ProjectList.test.tsx +0 -311
  128. package/test/unit/components/RepoDetailModal.test.tsx +0 -445
  129. package/test/unit/components/StatusBar.test.tsx +0 -112
  130. package/test/unit/components/UnifiedProjectItem.test.tsx +0 -618
  131. package/test/unit/components/ViewModeIndicator.test.tsx +0 -137
  132. package/test/unit/components/test-utils.tsx +0 -63
  133. package/test/unit/config/loader.test.ts +0 -692
  134. package/test/unit/db/database.test.ts +0 -978
  135. package/test/unit/db/index.test.ts +0 -314
  136. package/test/unit/fixtures/setup.ts +0 -186
  137. package/test/unit/git/commands-untested.test.ts +0 -205
  138. package/test/unit/git/commands.test.ts +0 -269
  139. package/test/unit/git/operations.test.ts +0 -322
  140. package/test/unit/git/status.test.ts +0 -219
  141. package/test/unit/github/auth.test.ts +0 -317
  142. package/test/unit/github/cache.test.ts +0 -1028
  143. package/test/unit/github/cli.test.ts +0 -135
  144. package/test/unit/github/unified.test.ts +0 -1201
  145. package/test/unit/graceful-shutdown.test.ts +0 -83
  146. package/test/unit/hooks/useBackgroundFetch.test.tsx +0 -239
  147. package/test/unit/hooks/useConfirmDialogActions.test.tsx +0 -81
  148. package/test/unit/hooks/useKeyBindings.isolated.ts +0 -715
  149. package/test/unit/hooks/useProjects.test.tsx +0 -186
  150. package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +0 -115
  151. package/test/unit/hooks/useUnifiedRepos.test.tsx +0 -177
  152. package/test/unit/mocks/config.ts +0 -109
  153. package/test/unit/mocks/git-service.ts +0 -274
  154. package/test/unit/mocks/github-service.ts +0 -250
  155. package/test/unit/mocks/index.ts +0 -72
  156. package/test/unit/mocks/project.ts +0 -148
  157. package/test/unit/mocks/state-mocks.ts +0 -187
  158. package/test/unit/mocks/unified.ts +0 -169
  159. package/test/unit/operations/batch.test.ts +0 -216
  160. package/test/unit/operations/commands.test.ts +0 -550
  161. package/test/unit/scanner/errors.test.ts +0 -297
  162. package/test/unit/scanner/index.test.ts +0 -1011
  163. package/test/unit/scanner/markers.test.ts +0 -150
  164. package/test/unit/scanner/submodules.test.ts +0 -99
  165. package/test/unit/services/git-errors.test.ts +0 -190
  166. package/test/unit/services/git.test.ts +0 -442
  167. package/test/unit/services/github-errors.test.ts +0 -293
  168. package/test/unit/services/github.test.ts +0 -200
  169. package/test/unit/state/actions.test.ts +0 -217
  170. package/test/unit/state/reducer.test.ts +0 -745
  171. package/test/unit/state/store.test.tsx +0 -711
  172. package/test/unit/types/commands.test.ts +0 -220
  173. package/test/unit/types/schema.test.ts +0 -179
  174. package/test/unit/utils/array.test.ts +0 -73
  175. package/test/unit/utils/debug.test.ts +0 -23
  176. package/test/unit/utils/errors.test.ts +0 -295
  177. package/test/unit/utils/markdown.test.ts +0 -163
  178. package/test/unit/utils/project-utils.test.ts +0 -756
  179. package/test/unit/utils/rate-limiter.test.ts +0 -256
  180. package/test/unit/utils/retry.test.ts +0 -165
  181. package/test/unit/utils/strip-ansi.ts +0 -13
  182. package/test/unit/utils/timeout.test.ts +0 -93
  183. package/tsconfig.json +0 -29
@@ -1,1116 +0,0 @@
1
- /**
2
- * Tests for CLI formatters
3
- */
4
-
5
- import { describe, test, expect } from "bun:test";
6
- import {
7
- getProjectTypeIcon,
8
- formatProjectStatus,
9
- formatProject,
10
- formatBatchResult,
11
- formatProgress,
12
- formatWarning,
13
- formatError,
14
- formatInfo,
15
- formatSuccess,
16
- formatScanning,
17
- formatOperationItem,
18
- formatOperationSummary,
19
- calculateStatusSummary,
20
- calculateProjectStats,
21
- formatProjectStats,
22
- formatProjectList,
23
- formatStatusSummary,
24
- formatStatusSummaryJson,
25
- formatDirtyRepos,
26
- getSourceIcon,
27
- formatUnifiedRepoStatus,
28
- formatUnifiedRepo,
29
- formatUnifiedStats,
30
- formatAuthSuccess,
31
- formatAuthFailure,
32
- formatNoToken,
33
- formatCloneItem,
34
- } from "../../../src/cli/formatters.ts";
35
- import {
36
- createMockProject,
37
- createDirtyProject,
38
- createAheadProject,
39
- createNonGitProject,
40
- createSubmoduleProject,
41
- defaultMockStatus,
42
- createDirtyStatus,
43
- createBehindStatus,
44
- createNoRemoteStatus,
45
- } from "../mocks/index.ts";
46
-
47
- describe("CLI formatters", () => {
48
- describe("getProjectTypeIcon", () => {
49
- test("returns green dot for git", () => {
50
- const icon = getProjectTypeIcon("git");
51
- expect(icon).toContain("●");
52
- });
53
-
54
- test("returns magenta circle for submodule", () => {
55
- const icon = getProjectTypeIcon("git-submodule");
56
- expect(icon).toContain("○");
57
- });
58
-
59
- test("returns gray dash for non-git", () => {
60
- const icon = getProjectTypeIcon("non-git");
61
- expect(icon).toContain("-");
62
- });
63
- });
64
-
65
- describe("formatProjectStatus", () => {
66
- test("returns 'clean' for clean project", () => {
67
- const project = createMockProject();
68
- const status = formatProjectStatus(project);
69
-
70
- expect(status).toContain("clean");
71
- });
72
-
73
- test("includes modified count for dirty project", () => {
74
- const project = createDirtyProject();
75
- const status = formatProjectStatus(project);
76
-
77
- expect(status).toContain("M");
78
- });
79
-
80
- test("includes unpushed indicator for ahead project", () => {
81
- const project = createAheadProject("test", 3);
82
- const status = formatProjectStatus(project);
83
-
84
- expect(status).toContain("↑");
85
- expect(status).toContain("3");
86
- });
87
-
88
- test("includes unpulled indicator for behind project", () => {
89
- const project = createMockProject({
90
- status: createBehindStatus(2),
91
- });
92
- const status = formatProjectStatus(project);
93
-
94
- expect(status).toContain("↓");
95
- expect(status).toContain("2");
96
- });
97
-
98
- test("includes no-remote for project without remote", () => {
99
- const project = createMockProject({
100
- status: createNoRemoteStatus(),
101
- });
102
- const status = formatProjectStatus(project);
103
-
104
- expect(status).toContain("no-remote");
105
- });
106
-
107
- test("handles non-git project", () => {
108
- const project = createNonGitProject("npm-project");
109
- const status = formatProjectStatus(project);
110
-
111
- expect(status).toContain("clean");
112
- });
113
-
114
- test("includes untracked files count", () => {
115
- const project = createMockProject({
116
- status: {
117
- ...defaultMockStatus,
118
- untrackedCount: 5,
119
- hasUntrackedFiles: true,
120
- },
121
- });
122
- const status = formatProjectStatus(project);
123
-
124
- expect(status).toContain("5?");
125
- });
126
-
127
- test("combines multiple status indicators", () => {
128
- const project = createMockProject({
129
- status: {
130
- ...createDirtyStatus(),
131
- unpushedCommits: 2,
132
- isAhead: true,
133
- unpulledCommits: 1,
134
- isBehind: true,
135
- untrackedCount: 3,
136
- hasUntrackedFiles: true,
137
- },
138
- });
139
- const status = formatProjectStatus(project);
140
-
141
- expect(status).toContain("2M");
142
- expect(status).toContain("3?");
143
- expect(status).toContain("↑2");
144
- expect(status).toContain("↓1");
145
- });
146
- });
147
-
148
- describe("formatProject", () => {
149
- test("includes project name", () => {
150
- const project = createMockProject({ name: "my-project" });
151
- const output = formatProject(project);
152
-
153
- expect(output).toContain("my-project");
154
- });
155
-
156
- test("includes type icon", () => {
157
- const project = createMockProject();
158
- const output = formatProject(project);
159
-
160
- expect(output).toContain("●");
161
- });
162
-
163
- test("includes path in verbose mode", () => {
164
- const project = createMockProject({
165
- name: "test",
166
- path: "/home/user/test",
167
- });
168
- const output = formatProject(project, true);
169
-
170
- expect(output).toContain("/home/user/test");
171
- });
172
-
173
- test("includes branch in verbose mode", () => {
174
- const project = createMockProject({
175
- name: "test",
176
- status: {
177
- ...defaultMockStatus,
178
- currentBranch: "feature-branch",
179
- },
180
- });
181
- const output = formatProject(project, true);
182
-
183
- expect(output).toContain("feature-branch");
184
- });
185
-
186
- test("handles missing branch in verbose mode", () => {
187
- const project = createNonGitProject("test");
188
- const output = formatProject(project, true);
189
-
190
- expect(output).toContain("N/A");
191
- });
192
- });
193
-
194
- describe("formatBatchResult", () => {
195
- test("formats successful batch", () => {
196
- const result = {
197
- total: 5,
198
- successful: 5,
199
- failed: 0,
200
- results: [],
201
- duration: 1000,
202
- };
203
-
204
- const output = formatBatchResult(result, "Pull");
205
-
206
- expect(output).toContain("Pull");
207
- expect(output).toContain("5/5");
208
- expect(output).toContain("1000ms");
209
- });
210
-
211
- test("formats batch with failures", () => {
212
- const result = {
213
- total: 5,
214
- successful: 3,
215
- failed: 2,
216
- results: [],
217
- duration: 2000,
218
- };
219
-
220
- const output = formatBatchResult(result, "Push");
221
-
222
- expect(output).toContain("Push");
223
- expect(output).toContain("3/5");
224
- expect(output).toContain("2 failed");
225
- expect(output).toContain("2000ms");
226
- });
227
-
228
- test("formats all failed batch", () => {
229
- const result = {
230
- total: 3,
231
- successful: 0,
232
- failed: 3,
233
- results: [],
234
- duration: 500,
235
- };
236
-
237
- const output = formatBatchResult(result, "Fetch");
238
-
239
- expect(output).toContain("Fetch");
240
- expect(output).toContain("0/3");
241
- expect(output).toContain("3 failed");
242
- expect(output).toContain("500ms");
243
- });
244
-
245
- test("includes error details for failed operations", () => {
246
- const result = {
247
- total: 2,
248
- successful: 1,
249
- failed: 1,
250
- results: [
251
- { projectPath: "/project1", success: true, operation: "pull", message: "Success", duration: 100 },
252
- { projectPath: "/project2", success: false, operation: "pull", error: "Permission denied", duration: 50 },
253
- ],
254
- duration: 150,
255
- };
256
-
257
- const output = formatBatchResult(result, "Pull");
258
-
259
- expect(output).toContain("Permission denied");
260
- expect(output).toContain("/project2");
261
- });
262
- });
263
-
264
- describe("formatProgress", () => {
265
- test("formats progress with completed and total", () => {
266
- const output = formatProgress(50, 100);
267
-
268
- expect(output).toContain("50");
269
- expect(output).toContain("100");
270
- expect(output).toContain("Progress:");
271
- });
272
-
273
- test("handles zero total", () => {
274
- const output = formatProgress(0, 0);
275
-
276
- expect(output).toContain("0/0");
277
- });
278
-
279
- test("handles completion", () => {
280
- const output = formatProgress(100, 100);
281
-
282
- expect(output).toContain("100/100");
283
- });
284
- });
285
-
286
- describe("message formatters", () => {
287
- test("formatWarning includes warning text", () => {
288
- const output = formatWarning("This is a warning");
289
-
290
- expect(output).toContain("This is a warning");
291
- });
292
-
293
- test("formatError includes error text", () => {
294
- const output = formatError("Something went wrong");
295
-
296
- expect(output).toContain("Something went wrong");
297
- });
298
-
299
- test("formatInfo includes info text", () => {
300
- const output = formatInfo("Information message");
301
-
302
- expect(output).toContain("Information message");
303
- });
304
-
305
- test("formatSuccess includes success text", () => {
306
- const output = formatSuccess("Operation completed");
307
-
308
- expect(output).toContain("Operation completed");
309
- });
310
-
311
- test("formatScanning includes scanning text", () => {
312
- const output = formatScanning("Scanning repositories...");
313
-
314
- expect(output).toContain("Scanning repositories...");
315
- });
316
-
317
- test("formatScanning uses default message", () => {
318
- const output = formatScanning();
319
-
320
- expect(output).toContain("Scanning directories...");
321
- });
322
- });
323
-
324
- describe("formatOperationItem", () => {
325
- test("formats successful operation", () => {
326
- const output = formatOperationItem("Deploy", true);
327
-
328
- expect(output).toContain("✓");
329
- expect(output).toContain("Deploy");
330
- });
331
-
332
- test("formats failed operation", () => {
333
- const output = formatOperationItem("Deploy", false, "Connection timeout");
334
-
335
- expect(output).toContain("✗");
336
- expect(output).toContain("Deploy");
337
- expect(output).toContain("Connection timeout");
338
- });
339
-
340
- test("formats failed operation without error", () => {
341
- const output = formatOperationItem("Deploy", false);
342
-
343
- expect(output).toContain("✗");
344
- expect(output).toContain("Deploy");
345
- expect(output).not.toContain(":");
346
- });
347
- });
348
-
349
- describe("formatOperationSummary", () => {
350
- test("formats summary with single item", () => {
351
- const output = formatOperationSummary("Deploy", 1, 1);
352
-
353
- expect(output).toContain("Deploy");
354
- expect(output).toContain("1/1");
355
- expect(output).toContain("item");
356
- });
357
-
358
- test("formats summary with multiple items", () => {
359
- const output = formatOperationSummary("Deploy", 5, 5);
360
-
361
- expect(output).toContain("Deploy");
362
- expect(output).toContain("5/5");
363
- expect(output).toContain("items");
364
- });
365
- });
366
-
367
- describe("calculateStatusSummary", () => {
368
- test("calculates summary for mixed projects", () => {
369
- const projects = [
370
- createMockProject({ name: "clean" }),
371
- createDirtyProject("dirty"),
372
- createAheadProject("ahead", 2),
373
- createNonGitProject("npm"),
374
- ];
375
-
376
- const summary = calculateStatusSummary(projects);
377
-
378
- expect(summary.total).toBe(4);
379
- expect(summary.dirty).toHaveLength(1);
380
- expect(summary.unpushed).toHaveLength(1);
381
- expect(summary.nonGit).toHaveLength(1);
382
- });
383
-
384
- test("identifies no-remote projects", () => {
385
- const projects = [
386
- createMockProject({ status: createNoRemoteStatus() }),
387
- createMockProject(), // Has remote
388
- ];
389
-
390
- const summary = calculateStatusSummary(projects);
391
-
392
- expect(summary.noRemote).toHaveLength(1);
393
- });
394
-
395
- test("identifies unpulled projects", () => {
396
- const projects = [
397
- createMockProject({ status: createBehindStatus(3) }),
398
- createMockProject(), // Up to date
399
- ];
400
-
401
- const summary = calculateStatusSummary(projects);
402
-
403
- expect(summary.unpulled).toHaveLength(1);
404
- });
405
-
406
- test("handles empty project list", () => {
407
- const summary = calculateStatusSummary([]);
408
-
409
- expect(summary.total).toBe(0);
410
- expect(summary.dirty).toHaveLength(0);
411
- expect(summary.unpushed).toHaveLength(0);
412
- expect(summary.unpulled).toHaveLength(0);
413
- expect(summary.noRemote).toHaveLength(0);
414
- expect(summary.nonGit).toHaveLength(0);
415
- });
416
- });
417
-
418
- describe("calculateProjectStats", () => {
419
- test("counts project types correctly", () => {
420
- const projects = [
421
- createMockProject(), // git
422
- createMockProject(), // git
423
- createSubmoduleProject("sub"),
424
- createNonGitProject("npm"),
425
- ];
426
-
427
- const stats = calculateProjectStats(projects);
428
-
429
- expect(stats.total).toBe(4);
430
- expect(stats.gitCount).toBe(2);
431
- expect(stats.submoduleCount).toBe(1);
432
- expect(stats.nonGitCount).toBe(1);
433
- });
434
-
435
- test("counts dirty and sync status", () => {
436
- const projects = [
437
- createDirtyProject(),
438
- createAheadProject("ahead"),
439
- createMockProject({ status: createBehindStatus() }),
440
- ];
441
-
442
- const stats = calculateProjectStats(projects);
443
-
444
- expect(stats.dirtyCount).toBe(1);
445
- expect(stats.unpushedCount).toBe(1);
446
- expect(stats.unpulledCount).toBe(1);
447
- });
448
-
449
- test("handles empty project list", () => {
450
- const stats = calculateProjectStats([]);
451
-
452
- expect(stats.total).toBe(0);
453
- expect(stats.gitCount).toBe(0);
454
- expect(stats.submoduleCount).toBe(0);
455
- expect(stats.nonGitCount).toBe(0);
456
- expect(stats.dirtyCount).toBe(0);
457
- expect(stats.unpushedCount).toBe(0);
458
- expect(stats.unpulledCount).toBe(0);
459
- });
460
- });
461
-
462
- describe("formatProjectStats", () => {
463
- test("formats stats with all counts", () => {
464
- const stats = {
465
- total: 10,
466
- gitCount: 6,
467
- submoduleCount: 2,
468
- nonGitCount: 2,
469
- dirtyCount: 3,
470
- unpushedCount: 1,
471
- unpulledCount: 2,
472
- };
473
-
474
- const output = formatProjectStats(stats);
475
-
476
- expect(output).toContain("6 git");
477
- expect(output).toContain("2 submodules");
478
- expect(output).toContain("2 non-git");
479
- expect(output).toContain("3 dirty");
480
- expect(output).toContain("1 unpushed");
481
- });
482
-
483
- test("formats stats with zero counts", () => {
484
- const stats = {
485
- total: 0,
486
- gitCount: 0,
487
- submoduleCount: 0,
488
- nonGitCount: 0,
489
- dirtyCount: 0,
490
- unpushedCount: 0,
491
- unpulledCount: 0,
492
- };
493
-
494
- const output = formatProjectStats(stats);
495
-
496
- expect(output).toContain("0 git");
497
- expect(output).toContain("0 submodules");
498
- expect(output).toContain("0 non-git");
499
- expect(output).toContain("0 dirty");
500
- expect(output).toContain("0 unpushed");
501
- });
502
- });
503
-
504
- describe("formatProjectList", () => {
505
- test("formats project list in default mode", () => {
506
- const projects = [
507
- createMockProject({ name: "project1" }),
508
- createDirtyProject("project2"),
509
- createNonGitProject("project3"),
510
- ];
511
-
512
- const output = formatProjectList(projects);
513
-
514
- expect(output).toContain("Found 3 projects");
515
- expect(output).toContain("project1");
516
- expect(output).toContain("project2");
517
- expect(output).toContain("project3");
518
- expect(output).toContain("---");
519
- expect(output).toContain("2 git");
520
- expect(output).toContain("1 non-git");
521
- expect(output).toContain("1 dirty");
522
- });
523
-
524
- test("formats project list in verbose mode", () => {
525
- const projects = [
526
- createMockProject({ name: "project1", path: "/path/to/project1" }),
527
- ];
528
-
529
- const output = formatProjectList(projects, { verbose: true });
530
-
531
- expect(output).toContain("Path: /path/to/project1");
532
- expect(output).toContain("Status:");
533
- expect(output).toContain("Branch:");
534
- });
535
-
536
- test("formats project list as JSON", () => {
537
- const projects = [
538
- createMockProject({ name: "project1" }),
539
- createMockProject({ name: "project2" }),
540
- ];
541
-
542
- const output = formatProjectList(projects, { json: true });
543
-
544
- const parsed = JSON.parse(output);
545
- expect(parsed).toHaveLength(2);
546
- expect(parsed[0].name).toBe("project1");
547
- expect(parsed[1].name).toBe("project2");
548
- });
549
- });
550
-
551
- describe("formatStatusSummary", () => {
552
- test("formats summary with all categories", () => {
553
- const summary = calculateStatusSummary([
554
- createMockProject({ name: "clean" }),
555
- createDirtyProject("dirty1"),
556
- createDirtyProject("dirty2"),
557
- createAheadProject("ahead1", 2),
558
- createAheadProject("ahead2", 5),
559
- createMockProject({ name: "behind", status: createBehindStatus(3) }),
560
- createMockProject({ name: "no-remote", status: createNoRemoteStatus() }),
561
- createNonGitProject("npm"),
562
- ]);
563
-
564
- const output = formatStatusSummary(summary);
565
-
566
- expect(output).toContain("Status Summary (8 projects)");
567
- expect(output).toContain("Dirty (2)");
568
- expect(output).toContain("dirty1");
569
- expect(output).toContain("dirty2");
570
- expect(output).toContain("Unpushed (2)");
571
- expect(output).toContain("ahead1 (↑2)");
572
- expect(output).toContain("Unpulled (1)");
573
- expect(output).toContain("behind (↓3)");
574
- expect(output).toContain("No Remote (1)");
575
- expect(output).toContain("no-remote");
576
- });
577
-
578
- test("shows clean message when all repos are clean", () => {
579
- const summary = calculateStatusSummary([
580
- createMockProject({ name: "clean1" }),
581
- createMockProject({ name: "clean2" }),
582
- ]);
583
-
584
- const output = formatStatusSummary(summary);
585
-
586
- expect(output).toContain("Status Summary (2 projects)");
587
- expect(output).toContain("All repositories are clean and in sync!");
588
- });
589
-
590
- test("handles empty summary", () => {
591
- const summary = calculateStatusSummary([]);
592
-
593
- const output = formatStatusSummary(summary);
594
-
595
- expect(output).toContain("Status Summary (0 projects)");
596
- });
597
- });
598
-
599
- describe("formatStatusSummaryJson", () => {
600
- test("formats summary as JSON", () => {
601
- const summary = calculateStatusSummary([
602
- createMockProject({ name: "clean" }),
603
- createDirtyProject("dirty"),
604
- createAheadProject("ahead", 2),
605
- createNonGitProject("npm"),
606
- ]);
607
-
608
- const output = formatStatusSummaryJson(summary);
609
- const parsed = JSON.parse(output);
610
-
611
- expect(parsed.total).toBe(4);
612
- expect(parsed.dirty).toEqual(["dirty"]);
613
- expect(parsed.unpushed).toEqual(["ahead"]);
614
- expect(parsed.nonGit).toEqual(["npm"]);
615
- });
616
- });
617
-
618
- describe("formatDirtyRepos", () => {
619
- test("formats empty dirty list", () => {
620
- const output = formatDirtyRepos([]);
621
-
622
- expect(output).toContain("All repositories are clean!");
623
- });
624
-
625
- test("formats dirty repos list", () => {
626
- const dirty = [
627
- createDirtyProject("project1"),
628
- createMockProject({
629
- name: "project2",
630
- status: {
631
- ...createDirtyStatus(),
632
- stagedCount: 2,
633
- untrackedCount: 1,
634
- },
635
- }),
636
- ];
637
-
638
- const output = formatDirtyRepos(dirty);
639
-
640
- expect(output).toContain("2 dirty repositories");
641
- expect(output).toContain("project1");
642
- expect(output).toContain("project2");
643
- expect(output).toContain("2 modified");
644
- expect(output).toContain("2 staged");
645
- expect(output).toContain("1 untracked");
646
- });
647
-
648
- test("formats as JSON", () => {
649
- const dirty = [
650
- createDirtyProject("project1"),
651
- createDirtyProject("project2"),
652
- ];
653
-
654
- const output = formatDirtyRepos(dirty, true);
655
- const parsed = JSON.parse(output);
656
-
657
- expect(parsed).toHaveLength(2);
658
- expect(parsed[0].name).toBe("project1");
659
- expect(parsed[1].name).toBe("project2");
660
- });
661
- });
662
-
663
- describe("getSourceIcon", () => {
664
- test("returns L for local", () => {
665
- const icon = getSourceIcon("local");
666
- expect(icon).toContain("L");
667
- });
668
-
669
- test("returns G for github", () => {
670
- const icon = getSourceIcon("github");
671
- expect(icon).toContain("G");
672
- });
673
-
674
- test("returns checkmark for both", () => {
675
- const icon = getSourceIcon("both");
676
- expect(icon).toContain("✓");
677
- });
678
- });
679
-
680
- describe("formatUnifiedRepoStatus", () => {
681
- test("shows not cloned for github-only repo", () => {
682
- const repo = {
683
- id: "test-repo",
684
- name: "test-repo",
685
- source: "github" as const,
686
- github: {
687
- name: "test-repo",
688
- fullName: "user/test-repo",
689
- owner: "user",
690
- description: null,
691
- htmlUrl: "https://github.com/user/test-repo",
692
- sshUrl: "git@github.com:user/test-repo.git",
693
- cloneUrl: "https://github.com/user/test-repo.git",
694
- isPrivate: false,
695
- isArchived: false,
696
- isFork: false,
697
- pushedAt: new Date(),
698
- updatedAt: new Date(),
699
- defaultBranch: "main",
700
- language: "TypeScript",
701
- size: 1024,
702
- },
703
- local: null,
704
- isCloned: false,
705
- isOnGitHub: true,
706
- localPath: null,
707
- };
708
-
709
- const status = formatUnifiedRepoStatus(repo);
710
-
711
- expect(status).toContain("not cloned");
712
- });
713
-
714
- test("shows local-only for repo not on GitHub", () => {
715
- const repo = {
716
- id: "test-repo",
717
- name: "test-repo",
718
- source: "local" as const,
719
- github: null,
720
- local: createMockProject({ name: "test-repo" }),
721
- isCloned: true,
722
- isOnGitHub: false,
723
- localPath: "/path/to/repo",
724
- };
725
-
726
- const status = formatUnifiedRepoStatus(repo);
727
-
728
- expect(status).toContain("local-only");
729
- });
730
-
731
- test("shows synced for clean synced repo", () => {
732
- const repo = {
733
- id: "test-repo",
734
- name: "test-repo",
735
- source: "both" as const,
736
- github: {
737
- name: "test-repo",
738
- fullName: "user/test-repo",
739
- owner: "user",
740
- description: null,
741
- htmlUrl: "https://github.com/user/test-repo",
742
- sshUrl: "git@github.com:user/test-repo.git",
743
- cloneUrl: "https://github.com/user/test-repo.git",
744
- isPrivate: false,
745
- isArchived: false,
746
- isFork: false,
747
- pushedAt: new Date(),
748
- updatedAt: new Date(),
749
- defaultBranch: "main",
750
- language: "TypeScript",
751
- size: 1024,
752
- },
753
- local: createMockProject({ name: "test-repo" }),
754
- isCloned: true,
755
- isOnGitHub: true,
756
- localPath: "/path/to/repo",
757
- };
758
-
759
- const status = formatUnifiedRepoStatus(repo);
760
-
761
- expect(status).toContain("synced");
762
- });
763
-
764
- test("shows dirty status", () => {
765
- const repo = {
766
- id: "test-repo",
767
- name: "test-repo",
768
- source: "both" as const,
769
- github: {
770
- name: "test-repo",
771
- fullName: "user/test-repo",
772
- owner: "user",
773
- description: null,
774
- htmlUrl: "https://github.com/user/test-repo",
775
- sshUrl: "git@github.com:user/test-repo.git",
776
- cloneUrl: "https://github.com/user/test-repo.git",
777
- isPrivate: false,
778
- isArchived: false,
779
- isFork: false,
780
- pushedAt: new Date(),
781
- updatedAt: new Date(),
782
- defaultBranch: "main",
783
- language: "TypeScript",
784
- size: 1024,
785
- },
786
- local: createDirtyProject("test-repo"),
787
- isCloned: true,
788
- isOnGitHub: true,
789
- localPath: "/path/to/repo",
790
- };
791
-
792
- const status = formatUnifiedRepoStatus(repo);
793
-
794
- expect(status).toContain("2M");
795
- });
796
-
797
- test("shows ahead status", () => {
798
- const repo = {
799
- id: "test-repo",
800
- name: "test-repo",
801
- source: "both" as const,
802
- github: {
803
- name: "test-repo",
804
- fullName: "user/test-repo",
805
- owner: "user",
806
- description: null,
807
- htmlUrl: "https://github.com/user/test-repo",
808
- sshUrl: "git@github.com:user/test-repo.git",
809
- cloneUrl: "https://github.com/user/test-repo.git",
810
- isPrivate: false,
811
- isArchived: false,
812
- isFork: false,
813
- pushedAt: new Date(),
814
- updatedAt: new Date(),
815
- defaultBranch: "main",
816
- language: "TypeScript",
817
- size: 1024,
818
- },
819
- local: createAheadProject("test-repo", 3),
820
- isCloned: true,
821
- isOnGitHub: true,
822
- localPath: "/path/to/repo",
823
- };
824
-
825
- const status = formatUnifiedRepoStatus(repo);
826
-
827
- expect(status).toContain("↑3");
828
- });
829
-
830
- test("shows behind status", () => {
831
- const repo = {
832
- id: "test-repo",
833
- name: "test-repo",
834
- source: "both" as const,
835
- github: {
836
- name: "test-repo",
837
- fullName: "user/test-repo",
838
- owner: "user",
839
- description: null,
840
- htmlUrl: "https://github.com/user/test-repo",
841
- sshUrl: "git@github.com:user/test-repo.git",
842
- cloneUrl: "https://github.com/user/test-repo.git",
843
- isPrivate: false,
844
- isArchived: false,
845
- isFork: false,
846
- pushedAt: new Date(),
847
- updatedAt: new Date(),
848
- defaultBranch: "main",
849
- language: "TypeScript",
850
- size: 1024,
851
- },
852
- local: createMockProject({
853
- name: "test-repo",
854
- status: createBehindStatus(2)
855
- }),
856
- isCloned: true,
857
- isOnGitHub: true,
858
- localPath: "/path/to/repo",
859
- };
860
-
861
- const status = formatUnifiedRepoStatus(repo);
862
-
863
- expect(status).toContain("↓2");
864
- });
865
- });
866
-
867
- describe("formatUnifiedRepo", () => {
868
- test("formats basic repo", () => {
869
- const repo = {
870
- id: "test-repo",
871
- name: "test-repo",
872
- source: "both" as const,
873
- github: {
874
- name: "test-repo",
875
- fullName: "user/test-repo",
876
- owner: "user",
877
- description: null,
878
- htmlUrl: "https://github.com/user/test-repo",
879
- sshUrl: "git@github.com:user/test-repo.git",
880
- cloneUrl: "https://github.com/user/test-repo.git",
881
- isPrivate: false,
882
- isArchived: false,
883
- isFork: false,
884
- pushedAt: new Date(),
885
- updatedAt: new Date(),
886
- defaultBranch: "main",
887
- language: "TypeScript",
888
- size: 1024,
889
- },
890
- local: createMockProject({ name: "test-repo" }),
891
- isCloned: true,
892
- isOnGitHub: true,
893
- localPath: "/path/to/repo",
894
- };
895
-
896
- const output = formatUnifiedRepo(repo);
897
-
898
- expect(output).toContain("test-repo");
899
- expect(output).toContain("synced");
900
- });
901
-
902
- test("formats with private indicator", () => {
903
- const repo = {
904
- id: "test-repo",
905
- name: "test-repo",
906
- source: "both" as const,
907
- github: {
908
- name: "test-repo",
909
- fullName: "user/test-repo",
910
- owner: "user",
911
- description: null,
912
- htmlUrl: "https://github.com/user/test-repo",
913
- sshUrl: "git@github.com:user/test-repo.git",
914
- cloneUrl: "https://github.com/user/test-repo.git",
915
- isPrivate: true,
916
- isArchived: false,
917
- isFork: false,
918
- pushedAt: new Date(),
919
- updatedAt: new Date(),
920
- defaultBranch: "main",
921
- language: "TypeScript",
922
- size: 1024,
923
- },
924
- local: createMockProject({ name: "test-repo" }),
925
- isCloned: true,
926
- isOnGitHub: true,
927
- localPath: "/path/to/repo",
928
- };
929
-
930
- const output = formatUnifiedRepo(repo);
931
-
932
- expect(output).toContain("test-repo");
933
- expect(output).toContain("(private)");
934
- });
935
-
936
- test("formats in verbose mode", () => {
937
- const repo = {
938
- id: "test-repo",
939
- name: "test-repo",
940
- source: "both" as const,
941
- github: {
942
- name: "test-repo",
943
- fullName: "user/test-repo",
944
- owner: "user",
945
- description: "A test repository for formatting",
946
- htmlUrl: "https://github.com/user/test-repo",
947
- sshUrl: "git@github.com:user/test-repo.git",
948
- cloneUrl: "https://github.com/user/test-repo.git",
949
- isPrivate: false,
950
- isArchived: false,
951
- isFork: false,
952
- pushedAt: new Date(),
953
- updatedAt: new Date(),
954
- defaultBranch: "main",
955
- language: "TypeScript",
956
- size: 1024,
957
- },
958
- local: createMockProject({ name: "test-repo" }),
959
- isCloned: true,
960
- isOnGitHub: true,
961
- localPath: "/path/to/repo",
962
- };
963
-
964
- const output = formatUnifiedRepo(repo, true);
965
-
966
- expect(output).toContain("test-repo");
967
- expect(output).toContain("Local: /path/to/repo");
968
- expect(output).toContain("GitHub: user/test-repo");
969
- expect(output).toContain("Desc: A test repository for formatting");
970
- expect(output).toContain("Status: synced");
971
- });
972
-
973
- test("handles github-only repo in verbose mode", () => {
974
- const repo = {
975
- id: "test-repo",
976
- name: "test-repo",
977
- source: "github" as const,
978
- github: {
979
- name: "test-repo",
980
- fullName: "user/test-repo",
981
- owner: "user",
982
- description: "Not cloned yet",
983
- htmlUrl: "https://github.com/user/test-repo",
984
- sshUrl: "git@github.com:user/test-repo.git",
985
- cloneUrl: "https://github.com/user/test-repo.git",
986
- isPrivate: false,
987
- isArchived: false,
988
- isFork: false,
989
- pushedAt: new Date(),
990
- updatedAt: new Date(),
991
- defaultBranch: "main",
992
- language: "TypeScript",
993
- size: 1024,
994
- },
995
- local: null,
996
- isCloned: false,
997
- isOnGitHub: true,
998
- localPath: null,
999
- };
1000
-
1001
- const output = formatUnifiedRepo(repo, true);
1002
-
1003
- expect(output).toContain("test-repo");
1004
- expect(output).not.toContain("Local:");
1005
- expect(output).toContain("GitHub: user/test-repo");
1006
- expect(output).toContain("Desc: Not cloned yet");
1007
- });
1008
- });
1009
-
1010
- describe("formatUnifiedStats", () => {
1011
- test("formats stats with all counts", () => {
1012
- const stats = {
1013
- total: 10,
1014
- both: 5,
1015
- localOnly: 2,
1016
- githubOnly: 3,
1017
- dirty: 1,
1018
- unpushed: 2,
1019
- unpulled: 1,
1020
- };
1021
-
1022
- const output = formatUnifiedStats(stats);
1023
-
1024
- expect(output).toContain("5 synced");
1025
- expect(output).toContain("2 local-only");
1026
- expect(output).toContain("3 github-only");
1027
- expect(output).toContain("1 dirty");
1028
- expect(output).toContain("2 unpushed");
1029
- });
1030
-
1031
- test("formats stats with zero counts", () => {
1032
- const stats = {
1033
- total: 0,
1034
- both: 0,
1035
- localOnly: 0,
1036
- githubOnly: 0,
1037
- dirty: 0,
1038
- unpushed: 0,
1039
- unpulled: 0,
1040
- };
1041
-
1042
- const output = formatUnifiedStats(stats);
1043
-
1044
- expect(output).toContain("0 synced");
1045
- expect(output).toContain("0 local-only");
1046
- expect(output).toContain("0 github-only");
1047
- expect(output).toContain("0 dirty");
1048
- expect(output).toContain("0 unpushed");
1049
- });
1050
- });
1051
-
1052
- describe("formatAuthSuccess", () => {
1053
- test("formats success with login only", () => {
1054
- const output = formatAuthSuccess("johndoe");
1055
-
1056
- expect(output).toContain("✓");
1057
- expect(output).toContain("Authenticated as johndoe");
1058
- expect(output).not.toContain("Name:");
1059
- });
1060
-
1061
- test("formats success with login and name", () => {
1062
- const output = formatAuthSuccess("johndoe", "John Doe");
1063
-
1064
- expect(output).toContain("✓");
1065
- expect(output).toContain("Authenticated as johndoe");
1066
- expect(output).toContain("Name: John Doe");
1067
- });
1068
- });
1069
-
1070
- describe("formatAuthFailure", () => {
1071
- test("formats failure without error", () => {
1072
- const output = formatAuthFailure();
1073
-
1074
- expect(output).toContain("✗");
1075
- expect(output).toContain("Authentication failed");
1076
- expect(output).not.toContain(":");
1077
- });
1078
-
1079
- test("formats failure with error", () => {
1080
- const output = formatAuthFailure("Invalid token");
1081
-
1082
- expect(output).toContain("✗");
1083
- expect(output).toContain("Authentication failed: Invalid token");
1084
- });
1085
- });
1086
-
1087
- describe("formatNoToken", () => {
1088
- test("formats no token message", () => {
1089
- const output = formatNoToken();
1090
-
1091
- expect(output).toContain("✗");
1092
- expect(output).toContain("GITHUB_TOKEN not set");
1093
- expect(output).toContain("gitforest login");
1094
- expect(output).toContain("export GITHUB_TOKEN=your_token");
1095
- });
1096
- });
1097
-
1098
- describe("formatCloneItem", () => {
1099
- test("formats successful clone", () => {
1100
- const output = formatCloneItem("user/repo", true, "/path/to/repo");
1101
-
1102
- expect(output).toContain("✓");
1103
- expect(output).toContain("user/repo");
1104
- expect(output).toContain("→ /path/to/repo");
1105
- });
1106
-
1107
- test("formats failed clone", () => {
1108
- const output = formatCloneItem("user/repo", false, undefined, "Already exists");
1109
-
1110
- expect(output).toContain("✗");
1111
- expect(output).toContain("user/repo");
1112
- expect(output).toContain("Already exists");
1113
- expect(output).not.toContain("→");
1114
- });
1115
- });
1116
- });