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,216 @@
|
|
|
1
|
+
import React, { createContext, useContext, useReducer, type ReactNode } from "react";
|
|
2
|
+
import type {
|
|
3
|
+
AppState,
|
|
4
|
+
QuickFilter,
|
|
5
|
+
UnifiedAppState,
|
|
6
|
+
UnifiedAppAction,
|
|
7
|
+
UnifiedRepo
|
|
8
|
+
} from "../types/index.ts";
|
|
9
|
+
import { filterProjects, sortProjects } from "../utils/project-utils.ts";
|
|
10
|
+
import { filterByViewMode, filterUnifiedRepos, sortUnifiedRepos } from "../github/unified.ts";
|
|
11
|
+
import { appReducer, initialState } from "./reducer.ts";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Store context
|
|
15
|
+
*/
|
|
16
|
+
interface StoreContextValue {
|
|
17
|
+
state: UnifiedAppState;
|
|
18
|
+
dispatch: React.Dispatch<UnifiedAppAction>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export const StoreContext = createContext<StoreContextValue | null>(null);
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Store provider component
|
|
25
|
+
*/
|
|
26
|
+
export function StoreProvider({ children }: { children: ReactNode }) {
|
|
27
|
+
const [state, dispatch] = useReducer(appReducer, initialState);
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<StoreContext.Provider value={{ state, dispatch }}>
|
|
31
|
+
{children}
|
|
32
|
+
</StoreContext.Provider>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Test store provider - accepts initial state and optional mock dispatch
|
|
38
|
+
* Used for testing components without mock.module()
|
|
39
|
+
*/
|
|
40
|
+
export function TestStoreProvider({
|
|
41
|
+
children,
|
|
42
|
+
initialState: testInitialState,
|
|
43
|
+
dispatch: mockDispatch,
|
|
44
|
+
}: {
|
|
45
|
+
children: ReactNode;
|
|
46
|
+
initialState: UnifiedAppState;
|
|
47
|
+
dispatch?: React.Dispatch<UnifiedAppAction>;
|
|
48
|
+
}) {
|
|
49
|
+
const [state, realDispatch] = useReducer(appReducer, testInitialState);
|
|
50
|
+
const dispatch = mockDispatch ?? realDispatch;
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<StoreContext.Provider value={{ state, dispatch }}>
|
|
54
|
+
{children}
|
|
55
|
+
</StoreContext.Provider>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Hook to access the store
|
|
61
|
+
*/
|
|
62
|
+
export function useStore(): StoreContextValue {
|
|
63
|
+
const context = useContext(StoreContext);
|
|
64
|
+
if (!context) {
|
|
65
|
+
throw new Error("useStore must be used within a StoreProvider");
|
|
66
|
+
}
|
|
67
|
+
return context;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Apply quick filter to projects
|
|
72
|
+
*/
|
|
73
|
+
function applyQuickFilter(projects: AppState["projects"], quickFilter: QuickFilter) {
|
|
74
|
+
if (quickFilter === "all") return projects;
|
|
75
|
+
|
|
76
|
+
return projects.filter((p) => {
|
|
77
|
+
switch (quickFilter) {
|
|
78
|
+
case "dirty":
|
|
79
|
+
return p.status?.isDirty;
|
|
80
|
+
case "unpushed":
|
|
81
|
+
return p.status?.isAhead && p.status?.unpushedCommits > 0;
|
|
82
|
+
case "no-remote":
|
|
83
|
+
return p.type === "git" && !p.status?.hasRemote;
|
|
84
|
+
default:
|
|
85
|
+
return true;
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Hook to get filtered and sorted projects
|
|
92
|
+
*/
|
|
93
|
+
export function useFilteredProjects() {
|
|
94
|
+
const { state } = useStore();
|
|
95
|
+
const { projects, filterText, quickFilter, sortBy, sortDirection } = state;
|
|
96
|
+
|
|
97
|
+
let result = projects;
|
|
98
|
+
|
|
99
|
+
// Apply quick filter first
|
|
100
|
+
result = applyQuickFilter(result, quickFilter);
|
|
101
|
+
|
|
102
|
+
// Apply text filter
|
|
103
|
+
if (filterText) {
|
|
104
|
+
result = filterProjects(result, filterText);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Apply sort
|
|
108
|
+
result = sortProjects(result, sortBy, sortDirection);
|
|
109
|
+
|
|
110
|
+
return result;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Hook to get selected projects
|
|
115
|
+
*/
|
|
116
|
+
export function useSelectedProjects() {
|
|
117
|
+
const { state } = useStore();
|
|
118
|
+
const filteredProjects = useFilteredProjects();
|
|
119
|
+
const { selectedIndices, cursorIndex } = state;
|
|
120
|
+
|
|
121
|
+
// If nothing selected, return the project at cursor
|
|
122
|
+
if (selectedIndices.size === 0) {
|
|
123
|
+
const current = filteredProjects[cursorIndex];
|
|
124
|
+
return current ? [current] : [];
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Return selected projects (filter out undefined and properly type)
|
|
128
|
+
return [...selectedIndices]
|
|
129
|
+
.map((i) => filteredProjects[i])
|
|
130
|
+
.filter((p): p is NonNullable<typeof p> => p !== undefined);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Apply quick filter to unified repos
|
|
135
|
+
*/
|
|
136
|
+
function applyQuickFilterToUnifiedRepos(repos: UnifiedRepo[], quickFilter: QuickFilter) {
|
|
137
|
+
if (quickFilter === "all") return repos;
|
|
138
|
+
|
|
139
|
+
return repos.filter((r) => {
|
|
140
|
+
switch (quickFilter) {
|
|
141
|
+
case "dirty":
|
|
142
|
+
return r.local?.status?.isDirty;
|
|
143
|
+
case "unpushed":
|
|
144
|
+
return r.local?.status?.isAhead && r.local?.status?.unpushedCommits > 0;
|
|
145
|
+
case "no-remote":
|
|
146
|
+
return r.local?.type === "git" && !r.local?.status?.hasRemote;
|
|
147
|
+
case "github-only":
|
|
148
|
+
return r.source === "github";
|
|
149
|
+
case "local-only":
|
|
150
|
+
return r.source === "local" || r.source === "both";
|
|
151
|
+
case "private":
|
|
152
|
+
return r.github?.isPrivate === true;
|
|
153
|
+
case "public":
|
|
154
|
+
return r.github?.isPrivate === false;
|
|
155
|
+
case "archived":
|
|
156
|
+
return r.github?.isArchived === true;
|
|
157
|
+
case "forks":
|
|
158
|
+
return r.github?.isFork === true;
|
|
159
|
+
default:
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Hook to get filtered and sorted unified repos
|
|
167
|
+
*/
|
|
168
|
+
export function useFilteredUnifiedRepos() {
|
|
169
|
+
const { state } = useStore();
|
|
170
|
+
const { unifiedRepos, viewMode, filterText, quickFilter, sortBy, sortDirection, languageFilter } = state;
|
|
171
|
+
|
|
172
|
+
let result = unifiedRepos;
|
|
173
|
+
|
|
174
|
+
// Apply view mode filter first
|
|
175
|
+
result = filterByViewMode(result, viewMode);
|
|
176
|
+
|
|
177
|
+
// Apply quick filter
|
|
178
|
+
result = applyQuickFilterToUnifiedRepos(result, quickFilter);
|
|
179
|
+
|
|
180
|
+
// Apply text filter
|
|
181
|
+
if (filterText) {
|
|
182
|
+
result = filterUnifiedRepos(result, filterText);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Apply language filter
|
|
186
|
+
if (languageFilter) {
|
|
187
|
+
result = result.filter(r =>
|
|
188
|
+
r.github?.language?.toLowerCase() === languageFilter.toLowerCase()
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Apply sort
|
|
193
|
+
result = sortUnifiedRepos(result, sortBy, sortDirection);
|
|
194
|
+
|
|
195
|
+
return result;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Hook to get selected unified repos
|
|
200
|
+
*/
|
|
201
|
+
export function useSelectedUnifiedRepos() {
|
|
202
|
+
const { state } = useStore();
|
|
203
|
+
const filteredRepos = useFilteredUnifiedRepos();
|
|
204
|
+
const { selectedIndices, cursorIndex } = state;
|
|
205
|
+
|
|
206
|
+
// If nothing selected, return the repo at cursor
|
|
207
|
+
if (selectedIndices.size === 0) {
|
|
208
|
+
const current = filteredRepos[cursorIndex];
|
|
209
|
+
return current ? [current] : [];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Return selected repos (filter out undefined and properly type)
|
|
213
|
+
return [...selectedIndices]
|
|
214
|
+
.map((i) => filteredRepos[i])
|
|
215
|
+
.filter((r): r is NonNullable<typeof r> => r !== undefined);
|
|
216
|
+
}
|
|
@@ -0,0 +1,383 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// ============================================================================
|
|
4
|
+
// Configuration Types
|
|
5
|
+
// ============================================================================
|
|
6
|
+
|
|
7
|
+
export const DirectoryConfigSchema = z.object({
|
|
8
|
+
path: z.string().min(1),
|
|
9
|
+
maxDepth: z.number().int().min(0).max(10).default(2),
|
|
10
|
+
label: z.string().optional(),
|
|
11
|
+
editor: z.string().optional(),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
// Reserved keys that cannot be used for custom commands
|
|
15
|
+
export const RESERVED_KEYS = new Set([
|
|
16
|
+
// Navigation
|
|
17
|
+
"j", "k", "g", "G",
|
|
18
|
+
// Selection
|
|
19
|
+
" ", "a",
|
|
20
|
+
// View/Filter
|
|
21
|
+
"/", "s", "S", "F",
|
|
22
|
+
"0", "1", "2", "3", "4", "5", "6", "7", "8", "9",
|
|
23
|
+
// Git operations
|
|
24
|
+
"p", "P", "f", "i",
|
|
25
|
+
// GitHub operations
|
|
26
|
+
"c", "C", "A", "D",
|
|
27
|
+
// General
|
|
28
|
+
"r", "?", "q",
|
|
29
|
+
// Modal actions (used in detail modal)
|
|
30
|
+
"o", "d",
|
|
31
|
+
// Command palette trigger
|
|
32
|
+
"x",
|
|
33
|
+
]);
|
|
34
|
+
|
|
35
|
+
export const CommandConfigSchema = z.object({
|
|
36
|
+
name: z.string().min(1),
|
|
37
|
+
key: z.string().length(1).refine(
|
|
38
|
+
(key) => !RESERVED_KEYS.has(key),
|
|
39
|
+
(key) => ({ message: `Key '${key}' is reserved and cannot be used for custom commands` })
|
|
40
|
+
),
|
|
41
|
+
command: z.string().min(1),
|
|
42
|
+
confirm: z.boolean().default(false),
|
|
43
|
+
background: z.boolean().default(false),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export type CommandConfig = z.infer<typeof CommandConfigSchema>;
|
|
47
|
+
|
|
48
|
+
export const GitforestConfigSchema = z.object({
|
|
49
|
+
directories: z.array(DirectoryConfigSchema).min(1),
|
|
50
|
+
editor: z.string().optional(), // Global editor setting
|
|
51
|
+
scan: z
|
|
52
|
+
.object({
|
|
53
|
+
ignore: z
|
|
54
|
+
.array(z.string())
|
|
55
|
+
.default(["node_modules", ".git", "vendor", "__pycache__", "target", "dist", "build"]),
|
|
56
|
+
includeHidden: z.boolean().default(false),
|
|
57
|
+
concurrency: z.number().int().min(1).max(20).default(5),
|
|
58
|
+
})
|
|
59
|
+
.default({}),
|
|
60
|
+
github: z
|
|
61
|
+
.object({
|
|
62
|
+
defaultVisibility: z.enum(["private", "public"]).default("private"),
|
|
63
|
+
})
|
|
64
|
+
.default({}),
|
|
65
|
+
display: z
|
|
66
|
+
.object({
|
|
67
|
+
showSubmodules: z.boolean().default(true),
|
|
68
|
+
showNonGitProjects: z.boolean().default(true), // Show non-git projects (folders with project markers but no .git)
|
|
69
|
+
sortBy: z.enum(["status", "name", "branch", "sync", "language", "stars", "forks", "lastActivity", "size"]).default("status"),
|
|
70
|
+
sortDirection: z.enum(["asc", "desc"]).default("desc"),
|
|
71
|
+
})
|
|
72
|
+
.default({}),
|
|
73
|
+
cache: z
|
|
74
|
+
.object({
|
|
75
|
+
ttlSeconds: z.number().int().positive().default(300),
|
|
76
|
+
githubTtlSeconds: z.number().int().positive().default(600),
|
|
77
|
+
enableBackgroundRefresh: z.boolean().default(true),
|
|
78
|
+
backgroundRefreshIntervalSeconds: z.number().int().positive().default(300),
|
|
79
|
+
})
|
|
80
|
+
.default({}),
|
|
81
|
+
commands: z
|
|
82
|
+
.array(CommandConfigSchema)
|
|
83
|
+
.default([])
|
|
84
|
+
.refine(
|
|
85
|
+
(commands) => {
|
|
86
|
+
const keys = commands.map((c) => c.key);
|
|
87
|
+
return new Set(keys).size === keys.length;
|
|
88
|
+
},
|
|
89
|
+
{ message: "Duplicate command keys are not allowed" }
|
|
90
|
+
),
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
export type DirectoryConfig = z.infer<typeof DirectoryConfigSchema>;
|
|
94
|
+
export type GitforestConfig = z.infer<typeof GitforestConfigSchema>;
|
|
95
|
+
|
|
96
|
+
// ============================================================================
|
|
97
|
+
// Project Types
|
|
98
|
+
// ============================================================================
|
|
99
|
+
|
|
100
|
+
export type ProjectType = "git" | "git-submodule" | "non-git";
|
|
101
|
+
|
|
102
|
+
export interface SubmoduleInfo {
|
|
103
|
+
parentPath: string;
|
|
104
|
+
relativePath: string;
|
|
105
|
+
configuredCommit: string;
|
|
106
|
+
currentCommit: string;
|
|
107
|
+
isInitialized: boolean;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface GitStatus {
|
|
111
|
+
// Working tree status
|
|
112
|
+
hasUnstagedChanges: boolean;
|
|
113
|
+
hasStagedChanges: boolean;
|
|
114
|
+
hasUntrackedFiles: boolean;
|
|
115
|
+
modifiedCount: number;
|
|
116
|
+
stagedCount: number;
|
|
117
|
+
untrackedCount: number;
|
|
118
|
+
|
|
119
|
+
// Sync status
|
|
120
|
+
currentBranch: string;
|
|
121
|
+
trackingBranch: string | null;
|
|
122
|
+
unpushedCommits: number;
|
|
123
|
+
unpulledCommits: number;
|
|
124
|
+
|
|
125
|
+
// Remote info
|
|
126
|
+
hasRemote: boolean;
|
|
127
|
+
remoteUrl: string | null;
|
|
128
|
+
|
|
129
|
+
// Activity timestamps
|
|
130
|
+
lastLocalCommit: Date | null;
|
|
131
|
+
lastRemoteActivity: Date | null;
|
|
132
|
+
|
|
133
|
+
// Repository state
|
|
134
|
+
hasCommits: boolean;
|
|
135
|
+
|
|
136
|
+
// Computed flags
|
|
137
|
+
isDirty: boolean;
|
|
138
|
+
isAhead: boolean;
|
|
139
|
+
isBehind: boolean;
|
|
140
|
+
isOutOfSync: boolean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface Project {
|
|
144
|
+
id: string;
|
|
145
|
+
name: string;
|
|
146
|
+
path: string;
|
|
147
|
+
type: ProjectType;
|
|
148
|
+
projectMarker: string | null; // e.g., "package.json", "Cargo.toml"
|
|
149
|
+
status: GitStatus | null;
|
|
150
|
+
submodule: SubmoduleInfo | null;
|
|
151
|
+
lastScanned: Date;
|
|
152
|
+
lastModified: Date | null; // For non-git: most recent file modification time
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ============================================================================
|
|
156
|
+
// Project Markers
|
|
157
|
+
// ============================================================================
|
|
158
|
+
|
|
159
|
+
export const PROJECT_MARKERS: Record<string, string> = {
|
|
160
|
+
"package.json": "Node.js",
|
|
161
|
+
"Cargo.toml": "Rust",
|
|
162
|
+
"pyproject.toml": "Python",
|
|
163
|
+
"setup.py": "Python",
|
|
164
|
+
"go.mod": "Go",
|
|
165
|
+
"Gemfile": "Ruby",
|
|
166
|
+
"pom.xml": "Java (Maven)",
|
|
167
|
+
"build.gradle": "Java (Gradle)",
|
|
168
|
+
"composer.json": "PHP",
|
|
169
|
+
"mix.exs": "Elixir",
|
|
170
|
+
"pubspec.yaml": "Dart/Flutter",
|
|
171
|
+
"CMakeLists.txt": "C/C++",
|
|
172
|
+
"Makefile": "Make",
|
|
173
|
+
"flake.nix": "Nix",
|
|
174
|
+
"deno.json": "Deno",
|
|
175
|
+
"bun.lockb": "Bun",
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
// ============================================================================
|
|
179
|
+
// App State Types
|
|
180
|
+
// ============================================================================
|
|
181
|
+
|
|
182
|
+
export type SortField =
|
|
183
|
+
| "status"
|
|
184
|
+
| "name"
|
|
185
|
+
| "branch"
|
|
186
|
+
| "sync"
|
|
187
|
+
| "language"
|
|
188
|
+
| "stars"
|
|
189
|
+
| "forks"
|
|
190
|
+
| "lastActivity"
|
|
191
|
+
| "size";
|
|
192
|
+
export type SortDirection = "asc" | "desc";
|
|
193
|
+
export type AppMode = "normal" | "filter" | "action" | "help" | "confirm" | "clone" | "detail" | "filter-options" | "command-palette";
|
|
194
|
+
|
|
195
|
+
// Quick filter for status-based filtering (1=dirty, 2=unpushed, 3=no-remote, 0=all)
|
|
196
|
+
export type QuickFilter =
|
|
197
|
+
| "all" | "dirty" | "unpushed" | "no-remote"
|
|
198
|
+
| "github-only" | "local-only"
|
|
199
|
+
| "private" | "public" | "archived" | "forks";
|
|
200
|
+
|
|
201
|
+
export interface ConfirmDialogState {
|
|
202
|
+
operation: "setup" | "create" | "archive";
|
|
203
|
+
title: string;
|
|
204
|
+
message: string;
|
|
205
|
+
items: string[];
|
|
206
|
+
projectPaths: string[];
|
|
207
|
+
showVisibilityToggle: boolean;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface CloneDialogState {
|
|
211
|
+
repos: UnifiedRepo[];
|
|
212
|
+
directories: DirectoryConfig[];
|
|
213
|
+
selectedDirIndex: number;
|
|
214
|
+
useSSH: boolean;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface DetailModalState {
|
|
218
|
+
repo: UnifiedRepo;
|
|
219
|
+
readmeContent: string | null;
|
|
220
|
+
readmeLoading: boolean;
|
|
221
|
+
readmeError: string | null;
|
|
222
|
+
readmeScrollOffset: number;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export interface AppState {
|
|
226
|
+
// Data
|
|
227
|
+
projects: Project[];
|
|
228
|
+
isLoading: boolean;
|
|
229
|
+
error: string | null;
|
|
230
|
+
message: string | null;
|
|
231
|
+
|
|
232
|
+
// Selection
|
|
233
|
+
cursorIndex: number;
|
|
234
|
+
selectedIndices: Set<number>;
|
|
235
|
+
scrollOffset: number;
|
|
236
|
+
|
|
237
|
+
// Filtering & Sorting
|
|
238
|
+
filterText: string;
|
|
239
|
+
quickFilter: QuickFilter;
|
|
240
|
+
sortBy: SortField;
|
|
241
|
+
sortDirection: SortDirection;
|
|
242
|
+
|
|
243
|
+
// UI state
|
|
244
|
+
mode: AppMode;
|
|
245
|
+
actionInProgress: string | null;
|
|
246
|
+
actionProgress: { current: number; total: number } | null;
|
|
247
|
+
confirmDialog: ConfirmDialogState | null;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
export type AppAction =
|
|
251
|
+
| { type: "SET_PROJECTS"; payload: Project[] }
|
|
252
|
+
| { type: "SET_LOADING"; payload: boolean }
|
|
253
|
+
| { type: "SET_ERROR"; payload: string | null }
|
|
254
|
+
| { type: "SET_MESSAGE"; payload: string | null }
|
|
255
|
+
| { type: "MOVE_CURSOR"; payload: number }
|
|
256
|
+
| { type: "TOGGLE_SELECTION"; payload: number }
|
|
257
|
+
| { type: "SELECT_ALL" }
|
|
258
|
+
| { type: "DESELECT_ALL" }
|
|
259
|
+
| { type: "SET_FILTER"; payload: string }
|
|
260
|
+
| { type: "SET_QUICK_FILTER"; payload: QuickFilter }
|
|
261
|
+
| { type: "SET_SORT"; payload: { by: SortField; direction: SortDirection } }
|
|
262
|
+
| { type: "CYCLE_SORT" }
|
|
263
|
+
| { type: "SET_MODE"; payload: AppMode }
|
|
264
|
+
| { type: "START_ACTION"; payload: string }
|
|
265
|
+
| { type: "END_ACTION" }
|
|
266
|
+
| { type: "UPDATE_PROGRESS"; payload: { current: number; total: number } }
|
|
267
|
+
| { type: "SET_SCROLL_OFFSET"; payload: number }
|
|
268
|
+
| { type: "UPDATE_PROJECT"; payload: { id: string; updates: Partial<Project> } }
|
|
269
|
+
| { type: "SHOW_CONFIRM_DIALOG"; payload: ConfirmDialogState }
|
|
270
|
+
| { type: "HIDE_CONFIRM_DIALOG" };
|
|
271
|
+
|
|
272
|
+
// ============================================================================
|
|
273
|
+
// Operation Results
|
|
274
|
+
// ============================================================================
|
|
275
|
+
|
|
276
|
+
export interface OperationResult {
|
|
277
|
+
success: boolean;
|
|
278
|
+
projectPath: string;
|
|
279
|
+
operation: string;
|
|
280
|
+
message?: string;
|
|
281
|
+
error?: string;
|
|
282
|
+
duration: number;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
export interface BatchResult {
|
|
286
|
+
total: number;
|
|
287
|
+
successful: number;
|
|
288
|
+
failed: number;
|
|
289
|
+
results: OperationResult[];
|
|
290
|
+
duration: number;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// ============================================================================
|
|
294
|
+
// GitHub Types
|
|
295
|
+
// ============================================================================
|
|
296
|
+
|
|
297
|
+
export interface GitHubRepoInfo {
|
|
298
|
+
name: string;
|
|
299
|
+
fullName: string;
|
|
300
|
+
owner: string;
|
|
301
|
+
description: string | null;
|
|
302
|
+
htmlUrl: string;
|
|
303
|
+
sshUrl: string;
|
|
304
|
+
cloneUrl: string;
|
|
305
|
+
isPrivate: boolean;
|
|
306
|
+
isArchived: boolean;
|
|
307
|
+
isFork: boolean;
|
|
308
|
+
pushedAt: Date | null;
|
|
309
|
+
updatedAt: Date | null;
|
|
310
|
+
defaultBranch: string;
|
|
311
|
+
language: string | null;
|
|
312
|
+
size: number;
|
|
313
|
+
stargazersCount?: number;
|
|
314
|
+
forksCount?: number;
|
|
315
|
+
openIssuesCount?: number;
|
|
316
|
+
watchersCount?: number;
|
|
317
|
+
topics?: string[];
|
|
318
|
+
license?: string | null;
|
|
319
|
+
hasIssues?: boolean;
|
|
320
|
+
hasWiki?: boolean;
|
|
321
|
+
hasDiscussions?: boolean;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// ============================================================================
|
|
325
|
+
// Unified View Types (Local + GitHub)
|
|
326
|
+
// ============================================================================
|
|
327
|
+
|
|
328
|
+
export type ViewMode = "local" | "github" | "combined";
|
|
329
|
+
export type RepoSource = "local" | "github" | "both";
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Unified item that can represent:
|
|
333
|
+
* - A local project (may or may not be on GitHub)
|
|
334
|
+
* - A GitHub repo (may or may not be cloned locally)
|
|
335
|
+
*/
|
|
336
|
+
export interface UnifiedRepo {
|
|
337
|
+
id: string;
|
|
338
|
+
name: string;
|
|
339
|
+
source: RepoSource;
|
|
340
|
+
|
|
341
|
+
// Local project info (if exists locally)
|
|
342
|
+
local: Project | null;
|
|
343
|
+
|
|
344
|
+
// GitHub repo info (if exists on GitHub)
|
|
345
|
+
github: GitHubRepoInfo | null;
|
|
346
|
+
|
|
347
|
+
// Sync status between local and GitHub
|
|
348
|
+
isCloned: boolean;
|
|
349
|
+
isOnGitHub: boolean;
|
|
350
|
+
localPath: string | null;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Extended AppState for unified view
|
|
354
|
+
export interface UnifiedAppState extends AppState {
|
|
355
|
+
viewMode: ViewMode;
|
|
356
|
+
githubRepos: GitHubRepoInfo[];
|
|
357
|
+
unifiedRepos: UnifiedRepo[];
|
|
358
|
+
isLoadingGitHub: boolean;
|
|
359
|
+
githubError: string | null;
|
|
360
|
+
isRefreshing: boolean; // Background refresh indicator
|
|
361
|
+
cloneDialog: CloneDialogState | null;
|
|
362
|
+
detailModal: DetailModalState | null;
|
|
363
|
+
languageFilter: string | null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
export type UnifiedAppAction =
|
|
367
|
+
| AppAction
|
|
368
|
+
| { type: "SET_VIEW_MODE"; payload: ViewMode }
|
|
369
|
+
| { type: "SET_GITHUB_REPOS"; payload: GitHubRepoInfo[] }
|
|
370
|
+
| { type: "SET_UNIFIED_REPOS"; payload: UnifiedRepo[] }
|
|
371
|
+
| { type: "SET_GITHUB_LOADING"; payload: boolean }
|
|
372
|
+
| { type: "SET_GITHUB_ERROR"; payload: string | null }
|
|
373
|
+
| { type: "SET_REFRESHING"; payload: boolean }
|
|
374
|
+
| { type: "SHOW_CLONE_DIALOG"; payload: CloneDialogState }
|
|
375
|
+
| { type: "HIDE_CLONE_DIALOG" }
|
|
376
|
+
| { type: "UPDATE_CLONE_DIALOG"; payload: Partial<CloneDialogState> }
|
|
377
|
+
| { type: "CLONE_REPO_START"; payload: string }
|
|
378
|
+
| { type: "CLONE_REPO_COMPLETE"; payload: { id: string; localPath: string } }
|
|
379
|
+
| { type: "CLONE_REPO_FAILED"; payload: { id: string; error: string } }
|
|
380
|
+
| { type: "SHOW_DETAIL_MODAL"; payload: DetailModalState }
|
|
381
|
+
| { type: "HIDE_DETAIL_MODAL" }
|
|
382
|
+
| { type: "UPDATE_DETAIL_MODAL"; payload: Partial<DetailModalState> }
|
|
383
|
+
| { type: "SET_LANGUAGE_FILTER"; payload: string | null };
|
package/src/ui/theme.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export const palette = {
|
|
2
|
+
bg: {
|
|
3
|
+
base: "black",
|
|
4
|
+
surface: "black",
|
|
5
|
+
muted: "gray",
|
|
6
|
+
accent: "blue",
|
|
7
|
+
caution: "yellow",
|
|
8
|
+
success: "green",
|
|
9
|
+
danger: "red",
|
|
10
|
+
info: "cyan",
|
|
11
|
+
},
|
|
12
|
+
text: {
|
|
13
|
+
primary: "white",
|
|
14
|
+
muted: "gray",
|
|
15
|
+
subtle: "brightBlack",
|
|
16
|
+
info: "cyan",
|
|
17
|
+
warning: "yellow",
|
|
18
|
+
success: "green",
|
|
19
|
+
danger: "red",
|
|
20
|
+
},
|
|
21
|
+
badge: {
|
|
22
|
+
dirty: { fg: "yellow", bg: undefined },
|
|
23
|
+
push: { fg: "cyan", bg: undefined },
|
|
24
|
+
pull: { fg: "magenta", bg: undefined },
|
|
25
|
+
local: { fg: "yellow", bg: undefined },
|
|
26
|
+
ok: { fg: "green", bg: undefined },
|
|
27
|
+
github: { fg: "white", bg: undefined },
|
|
28
|
+
},
|
|
29
|
+
} as const;
|
|
30
|
+
|
|
31
|
+
export const spacing = {
|
|
32
|
+
xxs: 0,
|
|
33
|
+
xs: 1,
|
|
34
|
+
sm: 2,
|
|
35
|
+
md: 3,
|
|
36
|
+
} as const;
|
|
37
|
+
|
|
38
|
+
export const typography = {
|
|
39
|
+
header: { color: palette.text.info, bold: true },
|
|
40
|
+
label: { color: palette.text.muted, bold: true },
|
|
41
|
+
value: { color: palette.text.primary },
|
|
42
|
+
muted: { color: palette.text.subtle },
|
|
43
|
+
} as const;
|
|
44
|
+
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array utility functions
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Split an array into chunks of a given size
|
|
7
|
+
*/
|
|
8
|
+
export function chunk<T>(arr: T[], size: number): T[][] {
|
|
9
|
+
const chunks: T[][] = [];
|
|
10
|
+
for (let i = 0; i < arr.length; i += size) {
|
|
11
|
+
chunks.push(arr.slice(i, i + size));
|
|
12
|
+
}
|
|
13
|
+
return chunks;
|
|
14
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Debug logging utility
|
|
3
|
+
* Enable debug output by setting GITTY_DEBUG=1 environment variable
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const DEBUG = process.env.GITTY_DEBUG === "1" || process.env.GITTY_DEBUG === "true";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Log a debug message (only when GITTY_DEBUG is enabled)
|
|
10
|
+
*/
|
|
11
|
+
export function debug(context: string, message: string, data?: unknown): void {
|
|
12
|
+
if (!DEBUG) return;
|
|
13
|
+
|
|
14
|
+
const timestamp = new Date().toISOString();
|
|
15
|
+
if (data !== undefined) {
|
|
16
|
+
console.error(`[gitforest ${timestamp}] [${context}] ${message}`, data);
|
|
17
|
+
} else {
|
|
18
|
+
console.error(`[gitforest ${timestamp}] [${context}] ${message}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Log a debug error (only when GITTY_DEBUG is enabled)
|
|
24
|
+
*/
|
|
25
|
+
export function debugError(context: string, message: string, error: unknown): void {
|
|
26
|
+
if (!DEBUG) return;
|
|
27
|
+
|
|
28
|
+
const timestamp = new Date().toISOString();
|
|
29
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
30
|
+
console.error(`[gitforest ${timestamp}] [${context}] ERROR: ${message} - ${errorMessage}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Check if debug mode is enabled
|
|
35
|
+
*/
|
|
36
|
+
export function isDebugEnabled(): boolean {
|
|
37
|
+
return DEBUG;
|
|
38
|
+
}
|