@f0rbit/overview 0.1.0 → 0.2.1

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 (69) hide show
  1. package/bin/overview +10 -0
  2. package/dist/overview.js +10361 -0
  3. package/package.json +22 -15
  4. package/bunfig.toml +0 -7
  5. package/packages/core/__tests__/concurrency.test.ts +0 -111
  6. package/packages/core/__tests__/helpers.ts +0 -60
  7. package/packages/core/__tests__/integration/git-status.test.ts +0 -62
  8. package/packages/core/__tests__/integration/scanner.test.ts +0 -140
  9. package/packages/core/__tests__/ocn.test.ts +0 -164
  10. package/packages/core/package.json +0 -13
  11. package/packages/core/src/cache.ts +0 -31
  12. package/packages/core/src/concurrency.ts +0 -44
  13. package/packages/core/src/devpad.ts +0 -61
  14. package/packages/core/src/git-graph.ts +0 -54
  15. package/packages/core/src/git-stats.ts +0 -201
  16. package/packages/core/src/git-status.ts +0 -316
  17. package/packages/core/src/github.ts +0 -286
  18. package/packages/core/src/index.ts +0 -58
  19. package/packages/core/src/ocn.ts +0 -74
  20. package/packages/core/src/scanner.ts +0 -118
  21. package/packages/core/src/types.ts +0 -199
  22. package/packages/core/src/watcher.ts +0 -128
  23. package/packages/core/src/worktree.ts +0 -80
  24. package/packages/core/tsconfig.json +0 -5
  25. package/packages/render/bunfig.toml +0 -8
  26. package/packages/render/jsx-runtime.d.ts +0 -3
  27. package/packages/render/package.json +0 -18
  28. package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +0 -780
  29. package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +0 -304
  30. package/packages/render/src/components/git-graph.tsx +0 -127
  31. package/packages/render/src/components/help-overlay.tsx +0 -108
  32. package/packages/render/src/components/index.ts +0 -7
  33. package/packages/render/src/components/repo-list.tsx +0 -127
  34. package/packages/render/src/components/stats-panel.tsx +0 -116
  35. package/packages/render/src/components/status-badge.tsx +0 -70
  36. package/packages/render/src/components/status-bar.tsx +0 -56
  37. package/packages/render/src/components/widget-container.tsx +0 -286
  38. package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +0 -326
  39. package/packages/render/src/components/widgets/branch-list.tsx +0 -93
  40. package/packages/render/src/components/widgets/commit-activity.tsx +0 -112
  41. package/packages/render/src/components/widgets/devpad-milestones.tsx +0 -88
  42. package/packages/render/src/components/widgets/devpad-tasks.tsx +0 -81
  43. package/packages/render/src/components/widgets/file-changes.tsx +0 -78
  44. package/packages/render/src/components/widgets/git-status.tsx +0 -125
  45. package/packages/render/src/components/widgets/github-ci.tsx +0 -98
  46. package/packages/render/src/components/widgets/github-issues.tsx +0 -101
  47. package/packages/render/src/components/widgets/github-prs.tsx +0 -119
  48. package/packages/render/src/components/widgets/github-release.tsx +0 -73
  49. package/packages/render/src/components/widgets/index.ts +0 -12
  50. package/packages/render/src/components/widgets/recent-commits.tsx +0 -64
  51. package/packages/render/src/components/widgets/registry.ts +0 -23
  52. package/packages/render/src/components/widgets/repo-meta.tsx +0 -80
  53. package/packages/render/src/config/index.ts +0 -104
  54. package/packages/render/src/lib/__tests__/fetch-context.test.ts +0 -200
  55. package/packages/render/src/lib/__tests__/widget-grid.test.ts +0 -665
  56. package/packages/render/src/lib/actions.ts +0 -68
  57. package/packages/render/src/lib/fetch-context.ts +0 -102
  58. package/packages/render/src/lib/filter.ts +0 -94
  59. package/packages/render/src/lib/format.ts +0 -36
  60. package/packages/render/src/lib/use-devpad.ts +0 -167
  61. package/packages/render/src/lib/use-github.ts +0 -75
  62. package/packages/render/src/lib/widget-grid.ts +0 -204
  63. package/packages/render/src/lib/widget-state.ts +0 -96
  64. package/packages/render/src/overview.tsx +0 -16
  65. package/packages/render/src/screens/index.ts +0 -1
  66. package/packages/render/src/screens/main-screen.tsx +0 -410
  67. package/packages/render/src/theme/index.ts +0 -37
  68. package/packages/render/tsconfig.json +0 -9
  69. package/tsconfig.json +0 -23
@@ -1,410 +0,0 @@
1
- import { createSignal, createEffect, createMemo, onMount, onCleanup, Show } from "solid-js";
2
- import { useKeyboard, useTerminalDimensions, useRenderer } from "@opentui/solid";
3
- import type { RepoNode, GitGraphOutput, OverviewConfig, HealthStatus, WidgetConfig } from "@overview/core";
4
- import { scanAndCollect, captureGraph, collectStats, collectStatus, createRepoWatcher, collectCommitActivity } from "@overview/core";
5
- import { RepoList, GitGraph, WidgetContainer, StatusBar, HelpOverlay, type AppMode } from "../components";
6
- import { filterTree, sortTree, nextFilter, nextSort, type SortMode, type FilterMode } from "../lib/filter";
7
- import { createFetchContext } from "../lib/fetch-context";
8
- import { launchGgi, launchEditor, launchSessionizer } from "../lib/actions";
9
- import { loadWidgetState, saveWidgetState, defaultWidgetConfig, getWidgetState, updateWidgetState } from "../lib/widget-state";
10
- import { theme } from "../theme";
11
-
12
- interface MainScreenProps {
13
- config: OverviewConfig;
14
- }
15
-
16
- function countNodes(nodes: RepoNode[]): number {
17
- return nodes.reduce(
18
- (acc, n) => acc + (n.type === "directory" ? countNodes(n.children) : 1),
19
- 0,
20
- );
21
- }
22
-
23
- function countByHealth(nodes: RepoNode[], pred: (h: HealthStatus) => boolean): number {
24
- return nodes.reduce((acc, n) => {
25
- if (n.type === "directory") return acc + countByHealth(n.children, pred);
26
- return acc + (n.status && pred(n.status.health) ? 1 : 0);
27
- }, 0);
28
- }
29
-
30
- function collectRepoPaths(nodes: RepoNode[]): string[] {
31
- return nodes.flatMap((n) =>
32
- n.type === "directory" ? collectRepoPaths(n.children) : [n.path],
33
- );
34
- }
35
-
36
- function updateRepoStatus(nodes: RepoNode[], repoPath: string, status: RepoNode["status"]): void {
37
- for (const n of nodes) {
38
- if (n.type === "directory") {
39
- updateRepoStatus(n.children, repoPath, status);
40
- } else if (n.path === repoPath) {
41
- n.status = status;
42
- }
43
- }
44
- }
45
-
46
- export function MainScreen(props: MainScreenProps) {
47
- const [repos, setRepos] = createSignal<RepoNode[]>([]);
48
- const [selectedNode, setSelectedNode] = createSignal<RepoNode | null>(null);
49
- const [graph, setGraph] = createSignal<GitGraphOutput | null>(null);
50
- const [graphLoading, setGraphLoading] = createSignal(false);
51
- const [statsLoading, setStatsLoading] = createSignal(false);
52
- const [scanning, setScanning] = createSignal(true);
53
- const [mode, setMode] = createSignal<AppMode>("NORMAL");
54
- const [focusPanel, setFocusPanel] = createSignal<"list" | "graph" | "stats">("list");
55
- const [message, setMessage] = createSignal<string | null>(null);
56
- const [sortMode, setSortMode] = createSignal<SortMode>(props.config.sort);
57
- const [filterMode, setFilterMode] = createSignal<FilterMode>(props.config.filter);
58
- const [showHelp, setShowHelp] = createSignal(false);
59
- const [widgetConfigs, setWidgetConfigs] = createSignal<WidgetConfig[]>(defaultWidgetConfig());
60
- const [repoVersion, setRepoVersion] = createSignal(0);
61
-
62
- const renderer = useRenderer();
63
-
64
- const dimensions = useTerminalDimensions();
65
- const leftWidth = () => 40;
66
-
67
- const rightPanelWidth = createMemo(() => {
68
- const w = dimensions().width - leftWidth() - 2; // subtract left panel border
69
- return Math.max(10, w);
70
- });
71
-
72
- const processedRepos = createMemo(() => {
73
- repoVersion(); // track version for reactivity
74
- let result = repos();
75
- result = filterTree(result, filterMode());
76
- result = sortTree(result, sortMode());
77
- return result;
78
- });
79
-
80
- const statusMessage = createMemo(() => {
81
- const parts: string[] = [];
82
- if (filterMode() !== "all") parts.push(`filter: ${filterMode()}`);
83
- if (sortMode() !== "name") parts.push(`sort: ${sortMode()}`);
84
- return parts.length > 0 ? parts.join(" ") : message();
85
- });
86
-
87
- const repoCount = createMemo(() => countNodes(repos()));
88
- const dirtyCount = createMemo(() => countByHealth(repos(), (h) => h !== "clean"));
89
- const aheadCount = createMemo(() => countByHealth(repos(), (h) => h === "ahead" || h === "diverged"));
90
-
91
- const widgetSummary = createMemo(() => {
92
- const configs = widgetConfigs();
93
- const enabled = configs.filter((c) => c.enabled).length;
94
- return `${enabled}/${configs.length} widgets`;
95
- });
96
-
97
- async function handleWidgetConfigChange(configs: WidgetConfig[]) {
98
- setWidgetConfigs(configs);
99
- const state = { ...getWidgetState(), widgets: configs };
100
- updateWidgetState(state);
101
- await saveWidgetState(state);
102
- }
103
-
104
- let _details_request_id = 0;
105
- let _details_timer: ReturnType<typeof setTimeout> | undefined;
106
-
107
- async function fetchDetails(node: RepoNode | null) {
108
- if (!node || node.type === "directory") {
109
- setGraph(null);
110
- setGraphLoading(false);
111
- setStatsLoading(false);
112
- return;
113
- }
114
-
115
- const my_request_id = _details_request_id;
116
-
117
- setGraphLoading(true);
118
- setStatsLoading(true);
119
-
120
- const [graphResult, statsResult, activityResult] = await Promise.all([
121
- captureGraph(node.path),
122
- collectStats(node.path),
123
- collectCommitActivity(node.path),
124
- ]);
125
-
126
- // Stale check — a newer request was issued while we were awaiting
127
- if (my_request_id !== _details_request_id) return;
128
-
129
- if (graphResult.ok) setGraph(graphResult.value);
130
- else setGraph(null);
131
-
132
- if (statsResult.ok && node.status) {
133
- node.status.tags = statsResult.value.tags;
134
- node.status.total_commits = statsResult.value.total_commits;
135
- node.status.repo_size_bytes = statsResult.value.repo_size_bytes;
136
- node.status.contributor_count = statsResult.value.contributor_count;
137
- node.status.recent_commits = statsResult.value.recent_commits;
138
- }
139
-
140
- if (activityResult.ok && node.status) {
141
- node.status.commit_activity = activityResult.value;
142
- }
143
-
144
- setGraphLoading(false);
145
- setStatsLoading(false);
146
- }
147
-
148
- createEffect(() => {
149
- const node = selectedNode();
150
- clearTimeout(_details_timer);
151
- if (!node || node.type === "directory") {
152
- _details_request_id++;
153
- setGraph(null);
154
- setGraphLoading(false);
155
- setStatsLoading(false);
156
- return;
157
- }
158
- // Show loading state immediately
159
- setGraphLoading(true);
160
- setStatsLoading(true);
161
- // Debounce the actual fetch
162
- _details_request_id++;
163
- _details_timer = setTimeout(() => {
164
- fetchDetails(node);
165
- }, 250);
166
- });
167
-
168
- const watcher = createRepoWatcher({
169
- debounce_ms: 500,
170
- on_change: (repoPath) => {
171
- collectStatus(repoPath, props.config.scan_dirs[0]!).then((result) => {
172
- if (result.ok) {
173
- updateRepoStatus(repos(), repoPath, result.value);
174
- setRepoVersion(v => v + 1);
175
- }
176
- });
177
- },
178
- });
179
-
180
- async function performScan() {
181
- setScanning(true);
182
- for (const dir of props.config.scan_dirs) {
183
- const result = await scanAndCollect(dir, {
184
- depth: props.config.depth,
185
- ignore: props.config.ignore,
186
- });
187
- if (result.ok) {
188
- setRepos(result.value);
189
- watcher.watch(collectRepoPaths(result.value));
190
- }
191
- }
192
- setScanning(false);
193
- }
194
-
195
- onMount(() => {
196
- performScan();
197
- loadWidgetState().then((result) => {
198
- if (result.ok) {
199
- setWidgetConfigs(result.value.widgets);
200
- updateWidgetState(result.value);
201
- }
202
- });
203
- });
204
-
205
- onCleanup(() => {
206
- clearTimeout(_details_timer);
207
- watcher.close();
208
- });
209
-
210
- const FOCUS_ORDER = ["list", "graph", "stats"] as const;
211
-
212
- function cycleFocus() {
213
- const current = focusPanel();
214
- const idx = FOCUS_ORDER.indexOf(current);
215
- setFocusPanel(FOCUS_ORDER[(idx + 1) % FOCUS_ORDER.length]!);
216
- }
217
-
218
- function handleSelect(node: RepoNode) {
219
- setSelectedNode(node);
220
- }
221
-
222
- useKeyboard((key) => {
223
- const m = mode();
224
-
225
- if (key.name === "tab") {
226
- cycleFocus();
227
- return;
228
- }
229
-
230
- if (m === "NORMAL") {
231
- switch (key.name) {
232
- case "q":
233
- case "escape":
234
- process.exit(0);
235
- break;
236
- case "return": {
237
- const node = selectedNode();
238
- if (node && node.type !== "directory") {
239
- setMode("DETAIL");
240
- setFocusPanel("graph");
241
- }
242
- break;
243
- }
244
- case "l": {
245
- const node = selectedNode();
246
- if (node && node.type !== "directory") {
247
- setMode("DETAIL");
248
- setFocusPanel("graph");
249
- }
250
- break;
251
- }
252
- case "r":
253
- _details_request_id++;
254
- clearTimeout(_details_timer);
255
- fetchDetails(selectedNode());
256
- break;
257
- case "R":
258
- performScan();
259
- break;
260
- case "f":
261
- setFilterMode(nextFilter(filterMode()));
262
- break;
263
- case "s":
264
- setSortMode(nextSort(sortMode()));
265
- break;
266
- case "o": {
267
- const node = selectedNode();
268
- if (node?.type === "repo" || node?.type === "worktree") {
269
- launchEditor(node.path, props.config.actions.editor, {
270
- onSuspend: () => renderer.suspend(),
271
- onResume: () => renderer.resume(),
272
- });
273
- }
274
- break;
275
- }
276
- case "t": {
277
- const node = selectedNode();
278
- if ((node?.type === "repo" || node?.type === "worktree") && props.config.actions.sessionizer) {
279
- launchSessionizer(node.path, props.config.actions.sessionizer, {
280
- onSuspend: () => renderer.suspend(),
281
- onResume: () => renderer.resume(),
282
- });
283
- }
284
- break;
285
- }
286
- }
287
-
288
- if (key.raw === "?") {
289
- setShowHelp(!showHelp());
290
- }
291
- return;
292
- }
293
-
294
- if (m === "DETAIL") {
295
- switch (key.name) {
296
- case "q":
297
- case "escape":
298
- setMode("NORMAL");
299
- setFocusPanel("list");
300
- break;
301
- case "h":
302
- if (focusPanel() === "graph") {
303
- setMode("NORMAL");
304
- setFocusPanel("list");
305
- } else {
306
- setFocusPanel("graph");
307
- }
308
- break;
309
- case "l":
310
- setFocusPanel("stats");
311
- break;
312
- case "g": {
313
- const node = selectedNode();
314
- if (node?.type === "repo" || node?.type === "worktree") {
315
- launchGgi(node.path, props.config.actions.ggi, {
316
- onSuspend: () => renderer.suspend(),
317
- onResume: () => renderer.resume(),
318
- });
319
- }
320
- break;
321
- }
322
- case "o": {
323
- const node = selectedNode();
324
- if (node?.type === "repo" || node?.type === "worktree") {
325
- launchEditor(node.path, props.config.actions.editor, {
326
- onSuspend: () => renderer.suspend(),
327
- onResume: () => renderer.resume(),
328
- });
329
- }
330
- break;
331
- }
332
- case "t": {
333
- const node = selectedNode();
334
- if ((node?.type === "repo" || node?.type === "worktree") && props.config.actions.sessionizer) {
335
- launchSessionizer(node.path, props.config.actions.sessionizer, {
336
- onSuspend: () => renderer.suspend(),
337
- onResume: () => renderer.resume(),
338
- });
339
- }
340
- break;
341
- }
342
- case "r":
343
- _details_request_id++;
344
- clearTimeout(_details_timer);
345
- fetchDetails(selectedNode());
346
- break;
347
- }
348
- }
349
- });
350
-
351
- return (
352
- <box flexDirection="column" width="100%" height="100%" backgroundColor={theme.bg}>
353
- {/* Header */}
354
- <box height={1} width="100%" backgroundColor={theme.bg_dark} paddingLeft={1}>
355
- <text fg={theme.blue}>overview</text>
356
- <box flexGrow={1} />
357
- <text fg={theme.fg_dim} content={`${props.config.scan_dirs[0]} — ${repoCount()} repos`} />
358
- <Show when={scanning()}>
359
- <text fg={theme.yellow}> scanning...</text>
360
- </Show>
361
- </box>
362
-
363
- {/* Main content */}
364
- <box flexDirection="row" flexGrow={1}>
365
- {/* Left panel */}
366
- <box width={leftWidth()} flexDirection="column" borderStyle="rounded" borderColor={focusPanel() === "list" ? theme.border_highlight : theme.border}>
367
- <RepoList
368
- repos={processedRepos()}
369
- focused={focusPanel() === "list"}
370
- onSelect={handleSelect}
371
- />
372
- </box>
373
-
374
- {/* Right panels */}
375
- <box flexDirection="column" flexGrow={1}>
376
- <GitGraph
377
- graph={graph()}
378
- repoName={selectedNode()?.name ?? ""}
379
- loading={graphLoading()}
380
- focused={focusPanel() === "graph"}
381
- height="50%"
382
- />
383
- <WidgetContainer
384
- status={selectedNode()?.status ?? null}
385
- repoName={selectedNode()?.name ?? ""}
386
- loading={statsLoading()}
387
- focused={focusPanel() === "stats"}
388
- height="50%"
389
- availableWidth={rightPanelWidth()}
390
- widgetConfigs={widgetConfigs()}
391
- onWidgetConfigChange={handleWidgetConfigChange}
392
- />
393
- </box>
394
- </box>
395
-
396
- {/* Status bar */}
397
- <StatusBar
398
- mode={mode()}
399
- repoCount={repoCount()}
400
- dirtyCount={dirtyCount()}
401
- aheadCount={aheadCount()}
402
- scanning={scanning()}
403
- message={statusMessage()}
404
- widgetSummary={widgetSummary()}
405
- />
406
-
407
- <HelpOverlay visible={showHelp()} onClose={() => setShowHelp(false)} />
408
- </box>
409
- );
410
- }
@@ -1,37 +0,0 @@
1
- export const theme = {
2
- // Base
3
- bg: "#1a1b26",
4
- bg_dark: "#16161e",
5
- bg_highlight: "#283457",
6
- fg: "#c0caf5",
7
- fg_dark: "#a9b1d6",
8
- fg_dim: "#565f89",
9
-
10
- // Accents
11
- blue: "#7aa2f7",
12
- cyan: "#7dcfff",
13
- green: "#9ece6a",
14
- yellow: "#e0af68",
15
- red: "#f7768e",
16
- magenta: "#bb9af7",
17
- orange: "#ff9e64",
18
-
19
- // UI
20
- border: "#3b4261",
21
- border_highlight: "#7aa2f7",
22
- selection: "#283457",
23
- comment: "#565f89",
24
-
25
- // Status colors
26
- status: {
27
- clean: "#9ece6a",
28
- ahead: "#e0af68",
29
- behind: "#7dcfff",
30
- modified: "#7aa2f7",
31
- conflict: "#f7768e",
32
- untracked: "#565f89",
33
- stash: "#bb9af7",
34
- },
35
- } as const;
36
-
37
- export type Theme = typeof theme;
@@ -1,9 +0,0 @@
1
- {
2
- "extends": "../../tsconfig.json",
3
- "compilerOptions": {
4
- "jsx": "preserve",
5
- "jsxImportSource": "@opentui/solid"
6
- },
7
- "include": ["src/**/*", "jsx-runtime.d.ts"],
8
- "exclude": ["node_modules", "dist"]
9
- }
package/tsconfig.json DELETED
@@ -1,23 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "strict": true,
4
- "moduleResolution": "bundler",
5
- "target": "esnext",
6
- "module": "esnext",
7
- "jsx": "preserve",
8
- "jsxImportSource": "solid-js",
9
- "esModuleInterop": true,
10
- "skipLibCheck": true,
11
- "forceConsistentCasingInFileNames": true,
12
- "resolveJsonModule": true,
13
- "declaration": true,
14
- "declarationMap": true,
15
- "sourceMap": true,
16
- "noUncheckedIndexedAccess": true,
17
- "paths": {
18
- "@overview/core": ["./packages/core/src/index.ts"],
19
- "@overview/core/*": ["./packages/core/src/*"]
20
- }
21
- },
22
- "include": ["packages/core/src/**/*", "packages/render/src/**/*"]
23
- }