@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,64 +0,0 @@
1
- import { For, Show } from "solid-js";
2
- import type { WidgetRenderProps, RepoStatus } from "@overview/core";
3
- import { registerWidget } from "./registry";
4
- import { theme } from "../../theme";
5
- import { truncate, formatRelativeTime } from "../../lib/format";
6
-
7
- const size_hint = { span: "half" as const, min_height: 2 };
8
- const MAX_VISIBLE = 8;
9
-
10
- function RecentCommitsWidget(props: WidgetRenderProps & { status: RepoStatus | null }) {
11
- const all_commits = () => props.status?.recent_commits ?? [];
12
- const visible_commits = () => all_commits().slice(0, MAX_VISIBLE);
13
- const overflow = () => Math.max(0, all_commits().length - MAX_VISIBLE);
14
-
15
- const format_line = (commit: { hash: string; message: string; time: number }) => {
16
- const hash_short = commit.hash.slice(0, 7);
17
- const time_str = " " + formatRelativeTime(commit.time);
18
- const available = props.width - hash_short.length - 1 - time_str.length;
19
- const msg = truncate(commit.message, Math.max(1, available));
20
- return { hash_short, msg, time_str };
21
- };
22
-
23
- return (
24
- <box flexDirection="column">
25
- <box height={1}>
26
- <text fg={theme.fg_dark} content="Recent Commits" />
27
- </box>
28
- <Show
29
- when={(props.status?.recent_commits?.length ?? 0) > 0}
30
- fallback={
31
- <box height={1}>
32
- <text fg={theme.fg_dim} content="(no commits)" />
33
- </box>
34
- }
35
- >
36
- <For each={visible_commits()}>
37
- {(commit) => {
38
- const line = () => format_line(commit);
39
- return (
40
- <box flexDirection="row" height={1}>
41
- <text fg={theme.yellow} content={line().hash_short} />
42
- <text content={" "} />
43
- <text content={line().msg} />
44
- <text fg={theme.fg_dim} content={line().time_str} />
45
- </box>
46
- );
47
- }}
48
- </For>
49
- <Show when={overflow() > 0}>
50
- <text fg={theme.fg_dim} content={`+${overflow()} more`} />
51
- </Show>
52
- </Show>
53
- </box>
54
- );
55
- }
56
-
57
- registerWidget({
58
- id: "recent-commits",
59
- label: "Recent Commits",
60
- size_hint,
61
- component: RecentCommitsWidget,
62
- });
63
-
64
- export { RecentCommitsWidget };
@@ -1,23 +0,0 @@
1
- import type { Component } from "solid-js";
2
- import type { WidgetId, WidgetSizeHint, WidgetRenderProps, RepoStatus } from "@overview/core";
3
-
4
- export interface WidgetDefinition {
5
- id: WidgetId;
6
- label: string;
7
- size_hint: WidgetSizeHint;
8
- component: Component<WidgetRenderProps & { status: RepoStatus | null }>;
9
- }
10
-
11
- const registry = new Map<WidgetId, WidgetDefinition>();
12
-
13
- export function registerWidget(def: WidgetDefinition): void {
14
- registry.set(def.id, def);
15
- }
16
-
17
- export function getWidget(id: WidgetId): WidgetDefinition | undefined {
18
- return registry.get(id);
19
- }
20
-
21
- export function getAllWidgets(): WidgetDefinition[] {
22
- return Array.from(registry.values());
23
- }
@@ -1,80 +0,0 @@
1
- import { Show } from "solid-js";
2
- import type { WidgetRenderProps, RepoStatus } from "@overview/core";
3
- import { registerWidget } from "./registry";
4
- import { theme } from "../../theme";
5
- import { formatBytes } from "../../lib/format";
6
-
7
- const size_hint = { span: "third" as const, min_height: 2 };
8
-
9
- const SEMVER_RE = /^v?\d+\.\d+/;
10
-
11
- function RepoMetaWidget(props: WidgetRenderProps & { status: RepoStatus | null }) {
12
- const s = () => props.status;
13
-
14
- return (
15
- <box flexDirection="column">
16
- <Show
17
- when={s()}
18
- fallback={
19
- <text fg={theme.fg_dim} content="(no data)" />
20
- }
21
- >
22
- {(status) => {
23
- const latest_tag = () => status().tags[0] ?? null;
24
- const tag_is_semver = () => {
25
- const tag = latest_tag();
26
- return tag !== null && SEMVER_RE.test(tag);
27
- };
28
-
29
- return (
30
- <>
31
- {/* Row 1: commits + contributors */}
32
- <box flexDirection="row" height={1} gap={2}>
33
- <box flexDirection="row" gap={1}>
34
- <text fg={theme.yellow} content={`${status().total_commits}`} />
35
- <text fg={theme.fg_dim} content="commits" />
36
- </box>
37
- <box flexDirection="row" gap={1}>
38
- <text fg={theme.yellow} content={`${status().contributor_count}`} />
39
- <text fg={theme.fg_dim} content="contributors" />
40
- </box>
41
- </box>
42
-
43
- {/* Row 2: repo size + tag count */}
44
- <box flexDirection="row" height={1} gap={2}>
45
- <box flexDirection="row" gap={1}>
46
- <text fg={theme.yellow} content={formatBytes(status().repo_size_bytes)} />
47
- <text fg={theme.fg_dim} content="on disk" />
48
- </box>
49
- <box flexDirection="row" gap={1}>
50
- <text fg={theme.yellow} content={`${status().tags.length}`} />
51
- <text fg={theme.fg_dim} content="tags" />
52
- </box>
53
- </box>
54
-
55
- {/* Row 3: latest tag */}
56
- <Show when={latest_tag()}>
57
- <box flexDirection="row" height={1} gap={1}>
58
- <text fg={theme.fg_dim} content="latest:" />
59
- <text
60
- fg={tag_is_semver() ? theme.green : theme.fg}
61
- content={latest_tag()!}
62
- />
63
- </box>
64
- </Show>
65
- </>
66
- );
67
- }}
68
- </Show>
69
- </box>
70
- );
71
- }
72
-
73
- registerWidget({
74
- id: "repo-meta",
75
- label: "Repo Meta",
76
- size_hint,
77
- component: RepoMetaWidget,
78
- });
79
-
80
- export { RepoMetaWidget };
@@ -1,104 +0,0 @@
1
- import { ok, err, merge_deep, type Result, type DeepPartial } from "@f0rbit/corpus";
2
- import { type OverviewConfig, defaultConfig } from "@overview/core";
3
- import { readFile, writeFile, mkdir } from "node:fs/promises";
4
- import { join } from "node:path";
5
- import { homedir } from "node:os";
6
-
7
- export type ConfigError =
8
- | { kind: "parse_error"; path: string; cause: string }
9
- | { kind: "write_error"; path: string; cause: string };
10
-
11
- const CONFIG_DIR = join(homedir(), ".config", "overview");
12
- const CONFIG_PATH = join(CONFIG_DIR, "config.json");
13
-
14
- const expandTilde = (p: string): string =>
15
- p.startsWith("~/") ? join(homedir(), p.slice(2)) : p;
16
-
17
- const expandPaths = (config: OverviewConfig): OverviewConfig => ({
18
- ...config,
19
- scan_dirs: config.scan_dirs.map(expandTilde),
20
- });
21
-
22
- const isEnoent = (e: unknown): boolean =>
23
- e instanceof Error && "code" in e && (e as NodeJS.ErrnoException).code === "ENOENT";
24
-
25
- export async function loadConfig(): Promise<Result<OverviewConfig, ConfigError>> {
26
- let raw: string;
27
- try {
28
- raw = await readFile(CONFIG_PATH, "utf-8");
29
- } catch (e) {
30
- if (isEnoent(e)) return ok(expandPaths(defaultConfig()));
31
- return err({ kind: "parse_error", path: CONFIG_PATH, cause: String(e) });
32
- }
33
-
34
- let parsed: DeepPartial<OverviewConfig>;
35
- try {
36
- parsed = JSON.parse(raw);
37
- } catch (e) {
38
- return err({ kind: "parse_error", path: CONFIG_PATH, cause: `Invalid JSON: ${e}` });
39
- }
40
-
41
- const merged = merge_deep(defaultConfig() as unknown as Record<string, unknown>, parsed as unknown as Record<string, unknown>) as unknown as OverviewConfig;
42
- return ok(expandPaths(merged));
43
- }
44
-
45
- export async function writeDefaultConfig(): Promise<Result<void, ConfigError>> {
46
- try {
47
- await readFile(CONFIG_PATH, "utf-8");
48
- return ok(undefined);
49
- } catch (e) {
50
- if (!isEnoent(e)) return err({ kind: "write_error", path: CONFIG_PATH, cause: String(e) });
51
- }
52
-
53
- try {
54
- await mkdir(CONFIG_DIR, { recursive: true });
55
- await writeFile(CONFIG_PATH, JSON.stringify(defaultConfig(), null, 2) + "\n", "utf-8");
56
- return ok(undefined);
57
- } catch (e) {
58
- return err({ kind: "write_error", path: CONFIG_PATH, cause: String(e) });
59
- }
60
- }
61
-
62
- export interface CliArgs {
63
- dir?: string;
64
- depth?: number;
65
- sort?: "name" | "status" | "last-commit";
66
- filter?: "all" | "dirty" | "clean" | "ahead" | "behind";
67
- }
68
-
69
- export function parseCliArgs(argv: string[]): CliArgs {
70
- const args = argv.slice(2);
71
- const result: CliArgs = {};
72
-
73
- for (let i = 0; i < args.length; i++) {
74
- const flag = args[i];
75
- const next = args[i + 1];
76
-
77
- if ((flag === "--dir" || flag === "-d") && next) {
78
- result.dir = next;
79
- i++;
80
- } else if (flag === "--depth" && next) {
81
- result.depth = parseInt(next, 10);
82
- i++;
83
- } else if (flag === "--sort" && next) {
84
- result.sort = next as CliArgs["sort"];
85
- i++;
86
- } else if (flag === "--filter" && next) {
87
- result.filter = next as CliArgs["filter"];
88
- i++;
89
- }
90
- }
91
-
92
- return result;
93
- }
94
-
95
- export function mergeCliArgs(config: OverviewConfig, args: CliArgs): OverviewConfig {
96
- const result = { ...config };
97
-
98
- if (args.dir !== undefined) result.scan_dirs = [expandTilde(args.dir)];
99
- if (args.depth !== undefined) result.depth = args.depth;
100
- if (args.sort !== undefined) result.sort = args.sort;
101
- if (args.filter !== undefined) result.filter = args.filter;
102
-
103
- return result;
104
- }
@@ -1,200 +0,0 @@
1
- import { describe, test, expect } from "bun:test";
2
- import { createFetchContext, InFlightDedup } from "../fetch-context";
3
-
4
- describe("createFetchContext", () => {
5
- test("trigger fires after delay", async () => {
6
- let result: string | null = null;
7
- const ctx = createFetchContext<string>(50, (value) => {
8
- result = value;
9
- });
10
-
11
- ctx.trigger(() => Promise.resolve("hello"));
12
-
13
- // Not yet fired
14
- expect(result).toBeNull();
15
-
16
- // Wait for debounce
17
- await Bun.sleep(80);
18
-
19
- expect(result).toBe("hello");
20
- ctx.dispose();
21
- });
22
-
23
- test("rapid triggers only fire the last one", async () => {
24
- const results: string[] = [];
25
- const ctx = createFetchContext<string>(50, (value) => {
26
- results.push(value);
27
- });
28
-
29
- ctx.trigger(() => Promise.resolve("first"));
30
- ctx.trigger(() => Promise.resolve("second"));
31
- ctx.trigger(() => Promise.resolve("third"));
32
-
33
- await Bun.sleep(80);
34
-
35
- expect(results).toEqual(["third"]);
36
- ctx.dispose();
37
- });
38
-
39
- test("immediate bypasses debounce", async () => {
40
- let result: string | null = null;
41
- const ctx = createFetchContext<string>(500, (value) => {
42
- result = value;
43
- });
44
-
45
- ctx.immediate(() => Promise.resolve("now"));
46
-
47
- await Bun.sleep(10);
48
-
49
- expect(result).toBe("now");
50
- ctx.dispose();
51
- });
52
-
53
- test("immediate cancels pending debounced trigger", async () => {
54
- const results: string[] = [];
55
- const ctx = createFetchContext<string>(50, (value) => {
56
- results.push(value);
57
- });
58
-
59
- ctx.trigger(() => Promise.resolve("debounced"));
60
- ctx.immediate(() => Promise.resolve("immediate"));
61
-
62
- await Bun.sleep(80);
63
-
64
- // Only "immediate" should have fired, "debounced" was cancelled
65
- expect(results).toEqual(["immediate"]);
66
- ctx.dispose();
67
- });
68
-
69
- test("stale results are discarded", async () => {
70
- const results: string[] = [];
71
- const ctx = createFetchContext<string>(10, (value) => {
72
- results.push(value);
73
- });
74
-
75
- // Start a slow fetch
76
- ctx.immediate(async () => {
77
- await Bun.sleep(100);
78
- return "slow";
79
- });
80
-
81
- // Before the slow fetch completes, trigger a fast one
82
- await Bun.sleep(20);
83
- ctx.immediate(() => Promise.resolve("fast"));
84
-
85
- // Wait for both to complete
86
- await Bun.sleep(150);
87
-
88
- // Only "fast" should be in results — "slow" was stale
89
- expect(results).toEqual(["fast"]);
90
- ctx.dispose();
91
- });
92
-
93
- test("cancel prevents pending execution", async () => {
94
- const results: string[] = [];
95
- const ctx = createFetchContext<string>(50, (value) => {
96
- results.push(value);
97
- });
98
-
99
- ctx.trigger(() => Promise.resolve("cancelled"));
100
- ctx.cancel();
101
-
102
- await Bun.sleep(80);
103
-
104
- expect(results).toEqual([]);
105
- ctx.dispose();
106
- });
107
-
108
- test("request_id increments on each trigger/cancel/immediate", () => {
109
- const ctx = createFetchContext<string>(50, () => {});
110
-
111
- const id1 = ctx.request_id;
112
- ctx.trigger(() => Promise.resolve("a"));
113
- expect(ctx.request_id).toBe(id1 + 1);
114
-
115
- ctx.cancel();
116
- expect(ctx.request_id).toBe(id1 + 2);
117
-
118
- ctx.immediate(() => Promise.resolve("b"));
119
- expect(ctx.request_id).toBe(id1 + 3);
120
-
121
- ctx.dispose();
122
- });
123
- });
124
-
125
- describe("InFlightDedup", () => {
126
- test("deduplicates concurrent calls for the same key", async () => {
127
- const dedup = new InFlightDedup<string>();
128
- let call_count = 0;
129
-
130
- const fn = async () => {
131
- call_count++;
132
- await Bun.sleep(50);
133
- return "result";
134
- };
135
-
136
- // Fire 3 concurrent requests for the same key
137
- const [r1, r2, r3] = await Promise.all([
138
- dedup.run("key1", fn),
139
- dedup.run("key1", fn),
140
- dedup.run("key1", fn),
141
- ]);
142
-
143
- // All get the same result
144
- expect(r1).toBe("result");
145
- expect(r2).toBe("result");
146
- expect(r3).toBe("result");
147
-
148
- // But fn was only called once
149
- expect(call_count).toBe(1);
150
- });
151
-
152
- test("different keys run independently", async () => {
153
- const dedup = new InFlightDedup<string>();
154
- let call_count = 0;
155
-
156
- const fn = async (val: string) => {
157
- call_count++;
158
- await Bun.sleep(20);
159
- return val;
160
- };
161
-
162
- const [r1, r2] = await Promise.all([
163
- dedup.run("a", () => fn("alpha")),
164
- dedup.run("b", () => fn("beta")),
165
- ]);
166
-
167
- expect(r1).toBe("alpha");
168
- expect(r2).toBe("beta");
169
- expect(call_count).toBe(2);
170
- });
171
-
172
- test("after completion, next call starts a new fetch", async () => {
173
- const dedup = new InFlightDedup<number>();
174
- let call_count = 0;
175
-
176
- const fn = async () => {
177
- call_count++;
178
- return call_count;
179
- };
180
-
181
- const r1 = await dedup.run("k", fn);
182
- expect(r1).toBe(1);
183
- expect(dedup.has("k")).toBe(false);
184
-
185
- const r2 = await dedup.run("k", fn);
186
- expect(r2).toBe(2);
187
- expect(call_count).toBe(2);
188
- });
189
-
190
- test("error propagates and cleans up in-flight entry", async () => {
191
- const dedup = new InFlightDedup<string>();
192
-
193
- const fn = async () => {
194
- throw new Error("boom");
195
- };
196
-
197
- await expect(dedup.run("k", fn)).rejects.toThrow("boom");
198
- expect(dedup.has("k")).toBe(false);
199
- });
200
- });