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.
- package/.bunignore +7 -0
- package/.github/workflows/ci.yml +73 -0
- package/CLAUDE.md +111 -0
- package/CONTRIBUTING.md +145 -0
- package/README.md +168 -0
- package/bun.lock +267 -0
- package/bunfig.toml +15 -0
- package/cli +0 -0
- package/config/gitforest.example.yaml +94 -0
- package/docs/ai/IMPROVEMENT_PLAN.md +341 -0
- package/docs/ai/VERIFICATION_REPORT.md +87 -0
- package/docs/ai/architecture.md +169 -0
- package/docs/ai/checks/check-2025-12-02-tests.md +40 -0
- package/docs/ai/checks/check-2025-12-02.md +55 -0
- package/docs/ai/checks/test-verification-report.md +85 -0
- package/docs/ai/implementation-guide.md +776 -0
- package/docs/ai/research/gitty-codebase-analysis.md +221 -0
- package/docs/ai/tickets/GENERAL-sitrep.md +30 -0
- package/docs/ai/tickets/TASK-database-tests-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-deprecated-functions-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-detail-modal-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-filter-overlay-sitrep.md +24 -0
- package/docs/ai/tickets/TASK-github-service-sitrep.md +32 -0
- package/docs/ai/tickets/TASK-github-token-sitrep.md +51 -0
- package/docs/ai/tickets/TASK-hascommits-sitrep.md +35 -0
- package/docs/ai/tickets/TASK-keybindings-sitrep.md +26 -0
- package/docs/ai/tickets/TASK-layout-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-markdown-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-project-item-sitrep.md +79 -0
- package/docs/ai/tickets/TASK-sitrep.md +28 -0
- package/docs/ai/tickets/TASK-state-sitrep.md +26 -0
- package/docs/ai/tickets/TASK-types-sitrep.md +25 -0
- package/docs/ai/tickets/TASK-unified-item-fix-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-001-sitrep.md +24 -0
- package/docs/ai/tickets/TKT-002-sitrep.md +25 -0
- package/docs/ai/tickets/TKT-003-git-service-refactoring-complete.md +46 -0
- package/docs/ai/tickets/TKT-003-git-service-refactoring-plan.md +135 -0
- package/docs/ai/tickets/TKT-003-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-004-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-005-sitrep.md +25 -0
- package/docs/ai/tickets/TKT-006-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-007-sitrep.md +30 -0
- package/docs/ai/tickets/TKT-008-sitrep.md +32 -0
- package/docs/ai/tickets/TKT-009-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-010-sitrep.md +27 -0
- package/docs/ai/tickets/TKT-011-sitrep.md +26 -0
- package/docs/ai/tickets/TKT-012-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-actions-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-actions-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-app-integration-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-background-fetch-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-background-fetch-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-batch-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-bun-test-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-cache-tests-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-cli-tests-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-clone-error-handling-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-commands-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-component-tests-1-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-configloader-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-confirm-dialog-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-coverage-sitrep.md +95 -0
- package/docs/ai/tickets/sitreps/TASK-database-tests-summary.md +61 -0
- package/docs/ai/tickets/sitreps/TASK-error-boundary-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-error-tests-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-errors-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-extract-reducer-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-filter-overlay-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-final-verification-sitrep.md +28 -0
- package/docs/ai/tickets/sitreps/TASK-fix-all-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-fix-hooks-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-fix-remaining-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-fix-test-failures-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-fix-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-formatters-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-git-timeouts-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-github-cache-test-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-githubcli-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-gitstatus-tests-sitrep.md +24 -0
- package/docs/ai/tickets/sitreps/TASK-hooks-isolation-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-keybindings-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-layout-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-mock-factories-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-modal-tests-sitrep.md +32 -0
- package/docs/ai/tickets/sitreps/TASK-processbatch-fix-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-projectlist-tests-sitrep.md +30 -0
- package/docs/ai/tickets/sitreps/TASK-projectutils-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-scanner-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-select-all-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-shell-error-handling-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-store-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-test-fixes-sitrep.md +26 -0
- package/docs/ai/tickets/sitreps/TASK-test-summary-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-test-verification-sitrep.md +27 -0
- package/docs/ai/tickets/sitreps/TASK-testsuite-sitrep.md +75 -0
- package/docs/ai/tickets/sitreps/TASK-unified-reducer-tests-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-unified-repos-test-sitrep.md +29 -0
- package/docs/ai/tickets/sitreps/TASK-unified-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-useprojects-tests-sitrep.md +25 -0
- package/docs/ai/tickets/sitreps/TASK-utility-tests-sitrep.md +32 -0
- package/docs/ai/tickets/sitreps/TKT-003-git-service-refactoring-sitrep.md +64 -0
- package/docs/ai/tkt-001-fix-database-error.md +217 -0
- package/docs/ai/ui-enhancement-plan.md +562 -0
- package/package.json +50 -0
- package/src/app.tsx +43 -0
- package/src/cli/config.ts +94 -0
- package/src/cli/formatters.ts +632 -0
- package/src/cli/index.ts +583 -0
- package/src/components/CloneDialog.tsx +137 -0
- package/src/components/ColumnHeader.tsx +128 -0
- package/src/components/CommandPalette.tsx +120 -0
- package/src/components/ConfirmDialog.tsx +105 -0
- package/src/components/ErrorBoundary.tsx +128 -0
- package/src/components/FilterBar.tsx +71 -0
- package/src/components/FilterOptionsOverlay.tsx +131 -0
- package/src/components/HelpOverlay.tsx +120 -0
- package/src/components/Layout.tsx +379 -0
- package/src/components/MarkdownRenderer.tsx +127 -0
- package/src/components/ProgressBar.tsx +53 -0
- package/src/components/ProjectItem.tsx +143 -0
- package/src/components/ProjectList.tsx +90 -0
- package/src/components/RepoDetailModal.tsx +367 -0
- package/src/components/StatusBar.tsx +188 -0
- package/src/components/UnifiedProjectItem.tsx +436 -0
- package/src/components/ViewModeIndicator.tsx +37 -0
- package/src/components/onboarding/CompleteStep.tsx +82 -0
- package/src/components/onboarding/DirectoriesStep.test.tsx +52 -0
- package/src/components/onboarding/DirectoriesStep.tsx +847 -0
- package/src/components/onboarding/DirectoriesStep.unit.test.ts +345 -0
- package/src/components/onboarding/GitHubAuthStep.tsx +268 -0
- package/src/components/onboarding/OnboardingWizard.tsx +130 -0
- package/src/components/onboarding/WelcomeStep.tsx +69 -0
- package/src/config/loader.ts +263 -0
- package/src/config/onboarding.ts +67 -0
- package/src/constants.ts +96 -0
- package/src/db/index.ts +147 -0
- package/src/db/schema.ts +70 -0
- package/src/git/commands.ts +283 -0
- package/src/git/index.ts +2 -0
- package/src/git/operations.ts +93 -0
- package/src/git/service.ts +539 -0
- package/src/git/status.ts +84 -0
- package/src/git/types.ts +5 -0
- package/src/github/auth.ts +311 -0
- package/src/github/cache.ts +231 -0
- package/src/github/cli.ts +22 -0
- package/src/github/unified.ts +415 -0
- package/src/hooks/useBackgroundFetch.ts +76 -0
- package/src/hooks/useConfirmDialogActions.ts +120 -0
- package/src/hooks/useKeyBindings.ts +656 -0
- package/src/hooks/useProjects.ts +47 -0
- package/src/hooks/useUnifiedRepos.ts +317 -0
- package/src/index.tsx +494 -0
- package/src/operations/batch.ts +280 -0
- package/src/operations/commands.ts +140 -0
- package/src/operations/index.ts +37 -0
- package/src/scanner/index.ts +424 -0
- package/src/scanner/markers.ts +43 -0
- package/src/scanner/submodules.ts +61 -0
- package/src/services/git.ts +484 -0
- package/src/services/github.ts +676 -0
- package/src/services/index.ts +28 -0
- package/src/services/types.ts +99 -0
- package/src/state/actions.ts +175 -0
- package/src/state/reducer.ts +294 -0
- package/src/state/store.tsx +216 -0
- package/src/state/types.ts +8 -0
- package/src/types/index.ts +383 -0
- package/src/ui/theme.ts +44 -0
- package/src/utils/array.ts +14 -0
- package/src/utils/debug.ts +38 -0
- package/src/utils/errors.ts +17 -0
- package/src/utils/index.ts +8 -0
- package/src/utils/markdown.ts +230 -0
- package/src/utils/project-utils.ts +129 -0
- package/src/utils/rate-limiter.ts +134 -0
- package/src/utils/retry.ts +147 -0
- package/src/utils/timeout.ts +56 -0
- package/test/integration/app.isolated.tsx +240 -0
- package/test/integration/cli-commands.test.ts +287 -0
- package/test/integration/cli-validation.test.ts +264 -0
- package/test/integration/git-operations.test.ts +218 -0
- package/test/integration/scanner.test.ts +228 -0
- package/test/preload.ts +18 -0
- package/test/unit/cli/commands.test.ts +13 -0
- package/test/unit/cli/formatters.test.ts +1116 -0
- package/test/unit/cli/github-commands.test.ts +12 -0
- package/test/unit/components/CloneDialog.test.tsx +240 -0
- package/test/unit/components/ColumnHeader.test.tsx +128 -0
- package/test/unit/components/CommandPalette.test.tsx +355 -0
- package/test/unit/components/ConfirmDialog.test.tsx +111 -0
- package/test/unit/components/ErrorBoundary.test.tsx +139 -0
- package/test/unit/components/FilterBar.test.tsx +43 -0
- package/test/unit/components/FilterOptionsOverlay.test.tsx +197 -0
- package/test/unit/components/HelpOverlay.test.tsx +90 -0
- package/test/unit/components/Layout.test.tsx +328 -0
- package/test/unit/components/MarkdownRenderer.test.tsx +45 -0
- package/test/unit/components/ProgressBar.test.tsx +138 -0
- package/test/unit/components/ProjectItem.test.tsx +182 -0
- package/test/unit/components/ProjectList.test.tsx +311 -0
- package/test/unit/components/RepoDetailModal.test.tsx +445 -0
- package/test/unit/components/StatusBar.test.tsx +112 -0
- package/test/unit/components/UnifiedProjectItem.test.tsx +618 -0
- package/test/unit/components/ViewModeIndicator.test.tsx +137 -0
- package/test/unit/components/test-utils.tsx +63 -0
- package/test/unit/config/loader.test.ts +692 -0
- package/test/unit/db/database.test.ts +978 -0
- package/test/unit/db/index.test.ts +314 -0
- package/test/unit/fixtures/setup.ts +186 -0
- package/test/unit/git/commands-untested.test.ts +205 -0
- package/test/unit/git/commands.test.ts +269 -0
- package/test/unit/git/operations.test.ts +322 -0
- package/test/unit/git/status.test.ts +219 -0
- package/test/unit/github/auth.test.ts +317 -0
- package/test/unit/github/cache.test.ts +1028 -0
- package/test/unit/github/cli.test.ts +135 -0
- package/test/unit/github/unified.test.ts +1201 -0
- package/test/unit/graceful-shutdown.test.ts +83 -0
- package/test/unit/hooks/useBackgroundFetch.test.tsx +239 -0
- package/test/unit/hooks/useConfirmDialogActions.test.tsx +81 -0
- package/test/unit/hooks/useKeyBindings.isolated.ts +715 -0
- package/test/unit/hooks/useProjects.test.tsx +186 -0
- package/test/unit/hooks/useUnifiedRepos-simple.test.tsx +115 -0
- package/test/unit/hooks/useUnifiedRepos.test.tsx +177 -0
- package/test/unit/mocks/config.ts +109 -0
- package/test/unit/mocks/git-service.ts +274 -0
- package/test/unit/mocks/github-service.ts +250 -0
- package/test/unit/mocks/index.ts +72 -0
- package/test/unit/mocks/project.ts +148 -0
- package/test/unit/mocks/state-mocks.ts +187 -0
- package/test/unit/mocks/unified.ts +169 -0
- package/test/unit/operations/batch.test.ts +216 -0
- package/test/unit/operations/commands.test.ts +550 -0
- package/test/unit/scanner/errors.test.ts +297 -0
- package/test/unit/scanner/index.test.ts +1011 -0
- package/test/unit/scanner/markers.test.ts +150 -0
- package/test/unit/scanner/submodules.test.ts +99 -0
- package/test/unit/services/git-errors.test.ts +190 -0
- package/test/unit/services/git.test.ts +442 -0
- package/test/unit/services/github-errors.test.ts +293 -0
- package/test/unit/services/github.test.ts +200 -0
- package/test/unit/state/actions.test.ts +217 -0
- package/test/unit/state/reducer.test.ts +745 -0
- package/test/unit/state/store.test.tsx +711 -0
- package/test/unit/types/commands.test.ts +220 -0
- package/test/unit/types/schema.test.ts +179 -0
- package/test/unit/utils/array.test.ts +73 -0
- package/test/unit/utils/debug.test.ts +23 -0
- package/test/unit/utils/errors.test.ts +295 -0
- package/test/unit/utils/markdown.test.ts +163 -0
- package/test/unit/utils/project-utils.test.ts +756 -0
- package/test/unit/utils/rate-limiter.test.ts +256 -0
- package/test/unit/utils/retry.test.ts +165 -0
- package/test/unit/utils/strip-ansi.ts +13 -0
- package/test/unit/utils/timeout.test.ts +93 -0
- 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
|
+
}
|