@f0rbit/overview 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.
Files changed (68) hide show
  1. package/README.md +242 -0
  2. package/bunfig.toml +7 -0
  3. package/package.json +42 -0
  4. package/packages/core/__tests__/concurrency.test.ts +111 -0
  5. package/packages/core/__tests__/helpers.ts +60 -0
  6. package/packages/core/__tests__/integration/git-status.test.ts +62 -0
  7. package/packages/core/__tests__/integration/scanner.test.ts +140 -0
  8. package/packages/core/__tests__/ocn.test.ts +164 -0
  9. package/packages/core/package.json +13 -0
  10. package/packages/core/src/cache.ts +31 -0
  11. package/packages/core/src/concurrency.ts +44 -0
  12. package/packages/core/src/devpad.ts +61 -0
  13. package/packages/core/src/git-graph.ts +54 -0
  14. package/packages/core/src/git-stats.ts +201 -0
  15. package/packages/core/src/git-status.ts +316 -0
  16. package/packages/core/src/github.ts +286 -0
  17. package/packages/core/src/index.ts +58 -0
  18. package/packages/core/src/ocn.ts +74 -0
  19. package/packages/core/src/scanner.ts +118 -0
  20. package/packages/core/src/types.ts +199 -0
  21. package/packages/core/src/watcher.ts +128 -0
  22. package/packages/core/src/worktree.ts +80 -0
  23. package/packages/core/tsconfig.json +5 -0
  24. package/packages/render/bunfig.toml +8 -0
  25. package/packages/render/jsx-runtime.d.ts +3 -0
  26. package/packages/render/package.json +18 -0
  27. package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +780 -0
  28. package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +304 -0
  29. package/packages/render/src/components/git-graph.tsx +127 -0
  30. package/packages/render/src/components/help-overlay.tsx +108 -0
  31. package/packages/render/src/components/index.ts +7 -0
  32. package/packages/render/src/components/repo-list.tsx +127 -0
  33. package/packages/render/src/components/stats-panel.tsx +116 -0
  34. package/packages/render/src/components/status-badge.tsx +70 -0
  35. package/packages/render/src/components/status-bar.tsx +56 -0
  36. package/packages/render/src/components/widget-container.tsx +286 -0
  37. package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +326 -0
  38. package/packages/render/src/components/widgets/branch-list.tsx +93 -0
  39. package/packages/render/src/components/widgets/commit-activity.tsx +112 -0
  40. package/packages/render/src/components/widgets/devpad-milestones.tsx +88 -0
  41. package/packages/render/src/components/widgets/devpad-tasks.tsx +81 -0
  42. package/packages/render/src/components/widgets/file-changes.tsx +78 -0
  43. package/packages/render/src/components/widgets/git-status.tsx +125 -0
  44. package/packages/render/src/components/widgets/github-ci.tsx +98 -0
  45. package/packages/render/src/components/widgets/github-issues.tsx +101 -0
  46. package/packages/render/src/components/widgets/github-prs.tsx +119 -0
  47. package/packages/render/src/components/widgets/github-release.tsx +73 -0
  48. package/packages/render/src/components/widgets/index.ts +12 -0
  49. package/packages/render/src/components/widgets/recent-commits.tsx +64 -0
  50. package/packages/render/src/components/widgets/registry.ts +23 -0
  51. package/packages/render/src/components/widgets/repo-meta.tsx +80 -0
  52. package/packages/render/src/config/index.ts +104 -0
  53. package/packages/render/src/lib/__tests__/fetch-context.test.ts +200 -0
  54. package/packages/render/src/lib/__tests__/widget-grid.test.ts +665 -0
  55. package/packages/render/src/lib/actions.ts +68 -0
  56. package/packages/render/src/lib/fetch-context.ts +102 -0
  57. package/packages/render/src/lib/filter.ts +94 -0
  58. package/packages/render/src/lib/format.ts +36 -0
  59. package/packages/render/src/lib/use-devpad.ts +167 -0
  60. package/packages/render/src/lib/use-github.ts +75 -0
  61. package/packages/render/src/lib/widget-grid.ts +204 -0
  62. package/packages/render/src/lib/widget-state.ts +96 -0
  63. package/packages/render/src/overview.tsx +16 -0
  64. package/packages/render/src/screens/index.ts +1 -0
  65. package/packages/render/src/screens/main-screen.tsx +410 -0
  66. package/packages/render/src/theme/index.ts +37 -0
  67. package/packages/render/tsconfig.json +9 -0
  68. package/tsconfig.json +23 -0
@@ -0,0 +1,74 @@
1
+ import { ok, err, type Result } from "@f0rbit/corpus";
2
+ import { join } from "node:path";
3
+ import { readdir } from "node:fs/promises";
4
+ import type { OcnStatus, OcnSessionStatus } from "./types";
5
+
6
+ export type OcnError =
7
+ | { kind: "state_dir_not_found" }
8
+ | { kind: "read_failed"; path: string; cause: string };
9
+
10
+ interface OcnStateFile {
11
+ pid: number;
12
+ directory: string;
13
+ project: string;
14
+ status: OcnSessionStatus;
15
+ last_transition: string;
16
+ session_id: string;
17
+ }
18
+
19
+ const VALID_STATUSES: Set<string> = new Set(["idle", "busy", "prompting", "error"]);
20
+
21
+ function isAlive(pid: number): boolean {
22
+ try {
23
+ process.kill(pid, 0);
24
+ return true;
25
+ } catch {
26
+ return false;
27
+ }
28
+ }
29
+
30
+ function isValidState(data: unknown): data is OcnStateFile {
31
+ if (typeof data !== "object" || data === null) return false;
32
+ const obj = data as Record<string, unknown>;
33
+ return (
34
+ typeof obj.pid === "number" &&
35
+ typeof obj.directory === "string" &&
36
+ typeof obj.status === "string" &&
37
+ VALID_STATUSES.has(obj.status) &&
38
+ typeof obj.session_id === "string"
39
+ );
40
+ }
41
+
42
+ export async function readOcnStates(): Promise<Result<Map<string, OcnStatus>, OcnError>> {
43
+ const state_dir = process.env.OCN_STATE_DIR ?? join(process.env.HOME ?? "~", ".local", "state", "ocn");
44
+ const map = new Map<string, OcnStatus>();
45
+
46
+ let entries: string[];
47
+ try {
48
+ const dir_entries = await readdir(state_dir);
49
+ entries = dir_entries.filter((e) => e.endsWith(".json"));
50
+ } catch {
51
+ // State dir doesn't exist — ocn not installed or no sessions. Not an error.
52
+ return ok(map);
53
+ }
54
+
55
+ for (const entry of entries) {
56
+ const file_path = join(state_dir, entry);
57
+ try {
58
+ const data = await Bun.file(file_path).json();
59
+ if (!isValidState(data)) continue;
60
+ if (!isAlive(data.pid)) continue;
61
+
62
+ map.set(data.directory, {
63
+ pid: data.pid,
64
+ status: data.status,
65
+ session_id: data.session_id,
66
+ });
67
+ } catch {
68
+ // Malformed JSON or read error — skip silently
69
+ continue;
70
+ }
71
+ }
72
+
73
+ return ok(map);
74
+ }
@@ -0,0 +1,118 @@
1
+ import { readdir, stat, lstat } from "node:fs/promises";
2
+ import { join, basename } from "node:path";
3
+ import { ok, err, try_catch_async, format_error, type Result } from "@f0rbit/corpus";
4
+ import type { RepoNode } from "./types";
5
+
6
+ export type ScanError =
7
+ | { kind: "invalid_path"; path: string; message: string }
8
+ | { kind: "permission_denied"; path: string }
9
+ | { kind: "scan_failed"; path: string; cause: string };
10
+
11
+ async function isGitRepo(dirPath: string): Promise<boolean> {
12
+ const proc = Bun.spawn(["git", "rev-parse", "--git-dir"], {
13
+ cwd: dirPath,
14
+ stdout: "pipe",
15
+ stderr: "pipe",
16
+ });
17
+ const code = await proc.exited;
18
+ return code === 0;
19
+ }
20
+
21
+ async function detectType(dirPath: string): Promise<"repo" | "worktree" | null> {
22
+ const git_path = join(dirPath, ".git");
23
+ const result = await try_catch_async(
24
+ () => lstat(git_path),
25
+ () => null,
26
+ );
27
+ if (!result.ok || result.value === null) return null;
28
+
29
+ const stats = result.value;
30
+ if (stats.isFile()) return "worktree";
31
+ if (stats.isDirectory()) return "repo";
32
+ return null;
33
+ }
34
+
35
+ async function walkDirectory(
36
+ dirPath: string,
37
+ current_depth: number,
38
+ max_depth: number,
39
+ ignore: string[],
40
+ ): Promise<Result<RepoNode[], ScanError>> {
41
+ if (current_depth > max_depth) return ok([]);
42
+
43
+ const entries_result = await try_catch_async(
44
+ () => readdir(dirPath, { withFileTypes: true }),
45
+ (e) => {
46
+ const msg = format_error(e);
47
+ if (msg.includes("EACCES") || msg.includes("permission"))
48
+ return { kind: "permission_denied" as const, path: dirPath };
49
+ return { kind: "scan_failed" as const, path: dirPath, cause: msg };
50
+ },
51
+ );
52
+
53
+ if (!entries_result.ok) return entries_result;
54
+
55
+ const dirs = entries_result.value
56
+ .filter((e) => e.isDirectory() && !ignore.some((pattern) => e.name.includes(pattern)))
57
+ .sort((a, b) => a.name.localeCompare(b.name));
58
+
59
+ const results = await Promise.all(
60
+ dirs.map(async (entry) => {
61
+ const full_path = join(dirPath, entry.name);
62
+ const repo_type = await detectType(full_path);
63
+ const is_repo = repo_type !== null && (await isGitRepo(full_path));
64
+
65
+ const children_result = await walkDirectory(full_path, current_depth + 1, max_depth, ignore);
66
+ if (!children_result.ok) return null;
67
+
68
+ const children = children_result.value;
69
+
70
+ if (is_repo) {
71
+ return {
72
+ name: entry.name,
73
+ path: full_path,
74
+ type: repo_type,
75
+ status: null,
76
+ worktrees: [],
77
+ children,
78
+ depth: current_depth,
79
+ expanded: current_depth <= 1,
80
+ } as RepoNode;
81
+ } else if (children.length > 0) {
82
+ return {
83
+ name: entry.name,
84
+ path: full_path,
85
+ type: "directory" as const,
86
+ status: null,
87
+ worktrees: [],
88
+ children,
89
+ depth: current_depth,
90
+ expanded: current_depth <= 1,
91
+ } as RepoNode;
92
+ }
93
+ return null;
94
+ }),
95
+ );
96
+
97
+ return ok(results.filter((n): n is RepoNode => n !== null));
98
+ }
99
+
100
+ export async function scanDirectory(
101
+ root: string,
102
+ options: { depth: number; ignore: string[] },
103
+ ): Promise<Result<RepoNode[], ScanError>> {
104
+ const root_stat = await try_catch_async(
105
+ () => stat(root),
106
+ (e) => ({
107
+ kind: "invalid_path" as const,
108
+ path: root,
109
+ message: format_error(e),
110
+ }),
111
+ );
112
+
113
+ if (!root_stat.ok) return root_stat;
114
+ if (!root_stat.value.isDirectory())
115
+ return err({ kind: "invalid_path", path: root, message: "Not a directory" });
116
+
117
+ return walkDirectory(root, 0, options.depth, options.ignore);
118
+ }
@@ -0,0 +1,199 @@
1
+ // Git file change
2
+ export interface GitFileChange {
3
+ path: string;
4
+ status: "modified" | "added" | "deleted" | "renamed" | "copied" | "untracked" | "ignored" | "conflicted";
5
+ staged: boolean;
6
+ }
7
+
8
+ // Branch info
9
+ export interface BranchInfo {
10
+ name: string;
11
+ is_current: boolean;
12
+ upstream: string | null;
13
+ ahead: number;
14
+ behind: number;
15
+ last_commit_time: number; // unix timestamp
16
+ }
17
+
18
+ // Stash entry
19
+ export interface StashEntry {
20
+ index: number;
21
+ message: string;
22
+ date: string;
23
+ }
24
+
25
+ // Recent commit
26
+ export interface RecentCommit {
27
+ hash: string;
28
+ message: string;
29
+ author: string;
30
+ time: number; // unix timestamp
31
+ }
32
+
33
+ // OpenCode session status (from ocn)
34
+ export type OcnSessionStatus = "idle" | "busy" | "prompting" | "error";
35
+
36
+ export interface OcnStatus {
37
+ pid: number;
38
+ status: OcnSessionStatus;
39
+ session_id: string;
40
+ }
41
+
42
+ // Health status
43
+ export type HealthStatus = "clean" | "dirty" | "ahead" | "behind" | "diverged" | "conflict";
44
+
45
+ // Full repo status
46
+ export interface RepoStatus {
47
+ // Identity
48
+ path: string;
49
+ name: string;
50
+ display_path: string; // relative to scan root
51
+
52
+ // Current state
53
+ current_branch: string;
54
+ head_commit: string;
55
+ head_message: string;
56
+ head_time: number;
57
+
58
+ // Tracking
59
+ remote_url: string | null;
60
+ ahead: number;
61
+ behind: number;
62
+
63
+ // Working tree
64
+ modified_count: number;
65
+ staged_count: number;
66
+ untracked_count: number;
67
+ conflict_count: number;
68
+ changes: GitFileChange[];
69
+
70
+ // Stash
71
+ stash_count: number;
72
+ stashes: StashEntry[];
73
+
74
+ // Branches
75
+ branches: BranchInfo[];
76
+ local_branch_count: number;
77
+ remote_branch_count: number;
78
+
79
+ // Metadata
80
+ tags: string[];
81
+ total_commits: number;
82
+ repo_size_bytes: number;
83
+ contributor_count: number;
84
+
85
+ // Recent activity
86
+ recent_commits: RecentCommit[];
87
+
88
+ // Commit activity (populated by fetchDetails, not initial scan)
89
+ commit_activity: { daily_counts: number[]; total_this_week: number; total_last_week: number } | null;
90
+
91
+ // OpenCode session status (from ocn)
92
+ ocn_status: OcnStatus | null;
93
+
94
+ // Derived
95
+ is_clean: boolean;
96
+ health: HealthStatus;
97
+ }
98
+
99
+ // Worktree info
100
+ export interface WorktreeInfo {
101
+ path: string;
102
+ branch: string;
103
+ head: string;
104
+ is_bare: boolean;
105
+ is_main: boolean;
106
+ }
107
+
108
+ // Git graph output
109
+ export interface GitGraphOutput {
110
+ lines: string[];
111
+ total_lines: number;
112
+ repo_path: string;
113
+ }
114
+
115
+ // Repo tree node
116
+ export interface RepoNode {
117
+ name: string;
118
+ path: string;
119
+ type: "directory" | "repo" | "worktree";
120
+ status: RepoStatus | null;
121
+ worktrees: WorktreeInfo[];
122
+ children: RepoNode[];
123
+ depth: number;
124
+ expanded: boolean;
125
+ }
126
+
127
+ // Widget system
128
+ export type WidgetId =
129
+ | "git-status"
130
+ | "recent-commits"
131
+ | "branch-list"
132
+ | "github-prs"
133
+ | "github-issues"
134
+ | "github-ci"
135
+ | "devpad-tasks"
136
+ | "devpad-milestones"
137
+ | "repo-meta"
138
+ | "file-changes"
139
+ | "commit-activity"
140
+ | "github-release";
141
+
142
+ export interface WidgetConfig {
143
+ id: WidgetId;
144
+ enabled: boolean;
145
+ priority: number;
146
+ collapsed: boolean;
147
+ }
148
+
149
+ export type WidgetSpan = "full" | "half" | "third" | "auto";
150
+
151
+ export interface WidgetSizeHint {
152
+ span: WidgetSpan;
153
+ min_height: number;
154
+ }
155
+
156
+ export interface WidgetRenderProps {
157
+ width: number;
158
+ focused: boolean;
159
+ }
160
+
161
+ // Config
162
+ export interface OverviewConfig {
163
+ scan_dirs: string[];
164
+ depth: number;
165
+ refresh_interval: number;
166
+ layout: {
167
+ left_width_pct: number;
168
+ graph_height_pct: number;
169
+ };
170
+ sort: "name" | "status" | "last-commit";
171
+ filter: "all" | "dirty" | "clean" | "ahead" | "behind";
172
+ ignore: string[];
173
+ actions: {
174
+ ggi: string;
175
+ editor: string;
176
+ sessionizer: string | null;
177
+ };
178
+ }
179
+
180
+ // Default config factory
181
+ export function defaultConfig(): OverviewConfig {
182
+ return {
183
+ scan_dirs: ["~/dev"],
184
+ depth: 3,
185
+ refresh_interval: 30,
186
+ layout: {
187
+ left_width_pct: 35,
188
+ graph_height_pct: 45,
189
+ },
190
+ sort: "name",
191
+ filter: "all",
192
+ ignore: ["node_modules", ".git"],
193
+ actions: {
194
+ ggi: "ggi",
195
+ editor: "$EDITOR",
196
+ sessionizer: null,
197
+ },
198
+ };
199
+ }
@@ -0,0 +1,128 @@
1
+ import { watch, type FSWatcher } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { readFile, lstat } from "node:fs/promises";
4
+
5
+ export interface WatcherOptions {
6
+ /** Debounce interval in ms (default 500) */
7
+ debounce_ms?: number;
8
+ /** Callback when a repo changes */
9
+ on_change: (repoPath: string) => void;
10
+ }
11
+
12
+ export interface RepoWatcher {
13
+ /** Start watching a list of repo paths */
14
+ watch(repoPaths: string[]): void;
15
+ /** Stop watching all repos */
16
+ close(): void;
17
+ /** Add a single repo to watch */
18
+ add(repoPath: string): void;
19
+ /** Remove a single repo from watching */
20
+ remove(repoPath: string): void;
21
+ }
22
+
23
+ async function resolveGitDir(repo_path: string): Promise<string | null> {
24
+ const git_path = join(repo_path, ".git");
25
+ try {
26
+ const stats = await lstat(git_path);
27
+ if (stats.isDirectory()) return git_path;
28
+ if (stats.isFile()) {
29
+ const content = await readFile(git_path, "utf-8");
30
+ const match = content.match(/^gitdir:\s*(.+)$/m);
31
+ const target = match?.[1]?.trim();
32
+ if (!target) return null;
33
+ return target.startsWith("/") ? target : join(repo_path, target);
34
+ }
35
+ return null;
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ function tryWatch(
42
+ target: string,
43
+ options: { recursive?: boolean },
44
+ callback: () => void,
45
+ ): FSWatcher | null {
46
+ try {
47
+ const watcher = watch(target, options, callback);
48
+ watcher.on("error", () => {});
49
+ return watcher;
50
+ } catch {
51
+ return null;
52
+ }
53
+ }
54
+
55
+ export function createRepoWatcher(options: WatcherOptions): RepoWatcher {
56
+ const debounce_ms = options.debounce_ms ?? 500;
57
+ const watchers = new Map<string, FSWatcher[]>();
58
+ const timers = new Map<string, ReturnType<typeof setTimeout>>();
59
+
60
+ function debouncedChange(repo_path: string) {
61
+ const existing = timers.get(repo_path);
62
+ if (existing) clearTimeout(existing);
63
+ timers.set(
64
+ repo_path,
65
+ setTimeout(() => {
66
+ timers.delete(repo_path);
67
+ options.on_change(repo_path);
68
+ }, debounce_ms),
69
+ );
70
+ }
71
+
72
+ async function addRepo(repo_path: string) {
73
+ if (watchers.has(repo_path)) return;
74
+
75
+ const git_dir = await resolveGitDir(repo_path);
76
+ if (!git_dir) {
77
+ console.warn(`[watcher] skipping ${repo_path}: could not resolve .git directory`);
78
+ return;
79
+ }
80
+
81
+ const repo_watchers: FSWatcher[] = [];
82
+ const on_event = () => debouncedChange(repo_path);
83
+
84
+ const index_watcher = tryWatch(join(git_dir, "index"), {}, on_event);
85
+ if (index_watcher) repo_watchers.push(index_watcher);
86
+
87
+ const refs_watcher = tryWatch(join(git_dir, "refs"), { recursive: true }, on_event);
88
+ if (refs_watcher) repo_watchers.push(refs_watcher);
89
+
90
+ if (repo_watchers.length === 0) {
91
+ console.warn(`[watcher] skipping ${repo_path}: no watchable targets`);
92
+ return;
93
+ }
94
+
95
+ watchers.set(repo_path, repo_watchers);
96
+ }
97
+
98
+ function removeRepo(repo_path: string) {
99
+ const repo_watchers = watchers.get(repo_path);
100
+ if (repo_watchers) {
101
+ repo_watchers.forEach((w) => w.close());
102
+ watchers.delete(repo_path);
103
+ }
104
+ const timer = timers.get(repo_path);
105
+ if (timer) {
106
+ clearTimeout(timer);
107
+ timers.delete(repo_path);
108
+ }
109
+ }
110
+
111
+ return {
112
+ watch(repo_paths: string[]) {
113
+ repo_paths.forEach((p) => addRepo(p));
114
+ },
115
+ close() {
116
+ watchers.forEach((ws) => ws.forEach((w) => w.close()));
117
+ watchers.clear();
118
+ timers.forEach((t) => clearTimeout(t));
119
+ timers.clear();
120
+ },
121
+ add(repo_path: string) {
122
+ addRepo(repo_path);
123
+ },
124
+ remove(repo_path: string) {
125
+ removeRepo(repo_path);
126
+ },
127
+ };
128
+ }
@@ -0,0 +1,80 @@
1
+ import { ok, err, type Result } from "@f0rbit/corpus";
2
+ import type { WorktreeInfo } from "./types";
3
+
4
+ export type WorktreeError =
5
+ | { kind: "not_a_repo"; path: string }
6
+ | { kind: "worktree_failed"; path: string; cause: string };
7
+
8
+ async function gitCommand(
9
+ args: string[],
10
+ cwd: string,
11
+ ): Promise<Result<string, WorktreeError>> {
12
+ const proc = Bun.spawn(["git", ...args], {
13
+ cwd,
14
+ stdout: "pipe",
15
+ stderr: "pipe",
16
+ });
17
+
18
+ await proc.exited;
19
+
20
+ if (proc.exitCode !== 0) {
21
+ const stderr = await new Response(proc.stderr).text();
22
+ const is_not_repo =
23
+ stderr.includes("not a git repository") ||
24
+ stderr.includes("not a git repo");
25
+ if (is_not_repo) {
26
+ return err({ kind: "not_a_repo", path: cwd });
27
+ }
28
+ return err({
29
+ kind: "worktree_failed",
30
+ path: cwd,
31
+ cause: stderr.trim(),
32
+ });
33
+ }
34
+
35
+ const stdout = await new Response(proc.stdout).text();
36
+ return ok(stdout);
37
+ }
38
+
39
+ function parseWorktreeBlock(
40
+ lines: string[],
41
+ is_main: boolean,
42
+ ): WorktreeInfo | null {
43
+ const path = lines
44
+ .find((l) => l.startsWith("worktree "))
45
+ ?.slice("worktree ".length);
46
+ if (!path) return null;
47
+
48
+ const head_line = lines.find((l) => l.startsWith("HEAD "));
49
+ const head = head_line ? head_line.slice("HEAD ".length, "HEAD ".length + 7) : "0000000";
50
+
51
+ const branch_line = lines.find((l) => l.startsWith("branch "));
52
+ const is_bare = lines.some((l) => l === "bare");
53
+ const branch = branch_line
54
+ ? branch_line.slice("branch refs/heads/".length)
55
+ : is_bare
56
+ ? "bare"
57
+ : "detached";
58
+
59
+ return { path, branch, head, is_bare, is_main };
60
+ }
61
+
62
+ export async function detectWorktrees(
63
+ repoPath: string,
64
+ ): Promise<Result<WorktreeInfo[], WorktreeError>> {
65
+ const result = await gitCommand(["worktree", "list", "--porcelain"], repoPath);
66
+ if (!result.ok) return result;
67
+
68
+ const blocks = result.value
69
+ .split("\n\n")
70
+ .map((b) => b.trim())
71
+ .filter((b) => b.length > 0);
72
+
73
+ const worktrees = blocks
74
+ .map((block, i) => parseWorktreeBlock(block.split("\n"), i === 0))
75
+ .filter((w): w is WorktreeInfo => w !== null);
76
+
77
+ if (worktrees.length <= 1) return ok([]);
78
+
79
+ return ok(worktrees);
80
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "../../tsconfig.json",
3
+ "include": ["src/**/*"],
4
+ "exclude": ["node_modules", "dist"]
5
+ }
@@ -0,0 +1,8 @@
1
+ preload = ["@opentui/solid/preload"]
2
+
3
+ [test]
4
+ preload = ["@opentui/solid/preload"]
5
+
6
+ [compilerOptions]
7
+ jsxImportSource = "solid-js"
8
+ jsx = "preserve"
@@ -0,0 +1,3 @@
1
+ // JSX types provided by @opentui/solid/jsx-runtime via jsxImportSource config.
2
+ // This file is included in tsconfig for any future type augmentations.
3
+ export {}
@@ -0,0 +1,18 @@
1
+ {
2
+ "name": "@overview/render",
3
+ "version": "0.1.0",
4
+ "main": "src/overview.tsx",
5
+ "scripts": {
6
+ "dev": "bun run src/overview.tsx",
7
+ "test": "bun test",
8
+ "typecheck": "tsc --noEmit"
9
+ },
10
+ "dependencies": {
11
+ "@opentui/core": "^0.1.80",
12
+ "@opentui/solid": "^0.1.80",
13
+ "solid-js": "^1.9.11",
14
+ "@overview/core": "workspace:*",
15
+ "@f0rbit/corpus": "link:@f0rbit/corpus",
16
+ "@devpad/api": "link:@devpad/api"
17
+ }
18
+ }