@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.
- package/bin/overview +10 -0
- package/dist/overview.js +10361 -0
- package/package.json +22 -15
- package/bunfig.toml +0 -7
- package/packages/core/__tests__/concurrency.test.ts +0 -111
- package/packages/core/__tests__/helpers.ts +0 -60
- package/packages/core/__tests__/integration/git-status.test.ts +0 -62
- package/packages/core/__tests__/integration/scanner.test.ts +0 -140
- package/packages/core/__tests__/ocn.test.ts +0 -164
- package/packages/core/package.json +0 -13
- package/packages/core/src/cache.ts +0 -31
- package/packages/core/src/concurrency.ts +0 -44
- package/packages/core/src/devpad.ts +0 -61
- package/packages/core/src/git-graph.ts +0 -54
- package/packages/core/src/git-stats.ts +0 -201
- package/packages/core/src/git-status.ts +0 -316
- package/packages/core/src/github.ts +0 -286
- package/packages/core/src/index.ts +0 -58
- package/packages/core/src/ocn.ts +0 -74
- package/packages/core/src/scanner.ts +0 -118
- package/packages/core/src/types.ts +0 -199
- package/packages/core/src/watcher.ts +0 -128
- package/packages/core/src/worktree.ts +0 -80
- package/packages/core/tsconfig.json +0 -5
- package/packages/render/bunfig.toml +0 -8
- package/packages/render/jsx-runtime.d.ts +0 -3
- package/packages/render/package.json +0 -18
- package/packages/render/src/components/__tests__/scrollbox-height.test.tsx +0 -780
- package/packages/render/src/components/__tests__/widget-container.integration.test.tsx +0 -304
- package/packages/render/src/components/git-graph.tsx +0 -127
- package/packages/render/src/components/help-overlay.tsx +0 -108
- package/packages/render/src/components/index.ts +0 -7
- package/packages/render/src/components/repo-list.tsx +0 -127
- package/packages/render/src/components/stats-panel.tsx +0 -116
- package/packages/render/src/components/status-badge.tsx +0 -70
- package/packages/render/src/components/status-bar.tsx +0 -56
- package/packages/render/src/components/widget-container.tsx +0 -286
- package/packages/render/src/components/widgets/__tests__/widget-rendering.test.tsx +0 -326
- package/packages/render/src/components/widgets/branch-list.tsx +0 -93
- package/packages/render/src/components/widgets/commit-activity.tsx +0 -112
- package/packages/render/src/components/widgets/devpad-milestones.tsx +0 -88
- package/packages/render/src/components/widgets/devpad-tasks.tsx +0 -81
- package/packages/render/src/components/widgets/file-changes.tsx +0 -78
- package/packages/render/src/components/widgets/git-status.tsx +0 -125
- package/packages/render/src/components/widgets/github-ci.tsx +0 -98
- package/packages/render/src/components/widgets/github-issues.tsx +0 -101
- package/packages/render/src/components/widgets/github-prs.tsx +0 -119
- package/packages/render/src/components/widgets/github-release.tsx +0 -73
- package/packages/render/src/components/widgets/index.ts +0 -12
- package/packages/render/src/components/widgets/recent-commits.tsx +0 -64
- package/packages/render/src/components/widgets/registry.ts +0 -23
- package/packages/render/src/components/widgets/repo-meta.tsx +0 -80
- package/packages/render/src/config/index.ts +0 -104
- package/packages/render/src/lib/__tests__/fetch-context.test.ts +0 -200
- package/packages/render/src/lib/__tests__/widget-grid.test.ts +0 -665
- package/packages/render/src/lib/actions.ts +0 -68
- package/packages/render/src/lib/fetch-context.ts +0 -102
- package/packages/render/src/lib/filter.ts +0 -94
- package/packages/render/src/lib/format.ts +0 -36
- package/packages/render/src/lib/use-devpad.ts +0 -167
- package/packages/render/src/lib/use-github.ts +0 -75
- package/packages/render/src/lib/widget-grid.ts +0 -204
- package/packages/render/src/lib/widget-state.ts +0 -96
- package/packages/render/src/overview.tsx +0 -16
- package/packages/render/src/screens/index.ts +0 -1
- package/packages/render/src/screens/main-screen.tsx +0 -410
- package/packages/render/src/theme/index.ts +0 -37
- package/packages/render/tsconfig.json +0 -9
- package/tsconfig.json +0 -23
package/package.json
CHANGED
|
@@ -1,42 +1,49 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@f0rbit/overview",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Terminal UI dashboard for multi-repo git health",
|
|
5
|
-
"workspaces": [
|
|
5
|
+
"workspaces": [
|
|
6
|
+
"packages/*"
|
|
7
|
+
],
|
|
6
8
|
"bin": {
|
|
7
|
-
"overview": "
|
|
9
|
+
"overview": "bin/overview"
|
|
8
10
|
},
|
|
9
11
|
"files": [
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"tsconfig.json"
|
|
12
|
+
"dist/",
|
|
13
|
+
"bin/"
|
|
13
14
|
],
|
|
14
15
|
"scripts": {
|
|
15
16
|
"dev": "bun run --filter @overview/render dev",
|
|
16
17
|
"typecheck": "bun run --filter '*' typecheck",
|
|
17
18
|
"lint": "biome check .",
|
|
18
19
|
"test": "bun test",
|
|
19
|
-
"build": "bun build packages/render/src/overview.tsx --compile --outfile overview"
|
|
20
|
+
"build": "bun build packages/render/src/overview.tsx --compile --outfile overview",
|
|
21
|
+
"build:bundle": "rm -rf dist && bun build packages/render/src/overview.tsx --outdir dist --target=bun --format=esm --external '@opentui/*' --external 'solid-js' --external 'solid-js/*'"
|
|
20
22
|
},
|
|
21
23
|
"repository": {
|
|
22
24
|
"type": "git",
|
|
23
25
|
"url": "https://github.com/f0rbit/overview.git"
|
|
24
26
|
},
|
|
25
27
|
"license": "MIT",
|
|
26
|
-
"keywords": [
|
|
28
|
+
"keywords": [
|
|
29
|
+
"git",
|
|
30
|
+
"tui",
|
|
31
|
+
"terminal",
|
|
32
|
+
"dashboard",
|
|
33
|
+
"bun",
|
|
34
|
+
"solidjs"
|
|
35
|
+
],
|
|
27
36
|
"engines": {
|
|
28
37
|
"bun": ">=1.0.0"
|
|
29
38
|
},
|
|
30
|
-
"devDependencies": {
|
|
31
|
-
"typescript": "^5.7.0",
|
|
32
|
-
"@types/bun": "latest",
|
|
33
|
-
"@biomejs/biome": "^1.9.0"
|
|
34
|
-
},
|
|
35
39
|
"dependencies": {
|
|
36
|
-
"@devpad/api": "^2.0.4",
|
|
37
|
-
"@f0rbit/corpus": "^0.3.5",
|
|
38
40
|
"@opentui/core": "^0.1.80",
|
|
39
41
|
"@opentui/solid": "^0.1.80",
|
|
40
42
|
"solid-js": "^1.9.11"
|
|
43
|
+
},
|
|
44
|
+
"devDependencies": {
|
|
45
|
+
"typescript": "^5.7.0",
|
|
46
|
+
"@types/bun": "latest",
|
|
47
|
+
"@biomejs/biome": "^1.9.0"
|
|
41
48
|
}
|
|
42
49
|
}
|
package/bunfig.toml
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect } from "bun:test";
|
|
2
|
-
import { createPool } from "../src/concurrency";
|
|
3
|
-
|
|
4
|
-
describe("createPool", () => {
|
|
5
|
-
test("respects concurrency limit", async () => {
|
|
6
|
-
const pool = createPool(2);
|
|
7
|
-
let max_concurrent = 0;
|
|
8
|
-
let current = 0;
|
|
9
|
-
|
|
10
|
-
const task = async () => {
|
|
11
|
-
current++;
|
|
12
|
-
max_concurrent = Math.max(max_concurrent, current);
|
|
13
|
-
await Bun.sleep(30);
|
|
14
|
-
current--;
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
await Promise.all([
|
|
18
|
-
pool.run(task),
|
|
19
|
-
pool.run(task),
|
|
20
|
-
pool.run(task),
|
|
21
|
-
pool.run(task),
|
|
22
|
-
pool.run(task),
|
|
23
|
-
]);
|
|
24
|
-
|
|
25
|
-
expect(max_concurrent).toBe(2);
|
|
26
|
-
expect(current).toBe(0);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test("all tasks complete", async () => {
|
|
30
|
-
const pool = createPool(3);
|
|
31
|
-
const results: number[] = [];
|
|
32
|
-
|
|
33
|
-
const tasks = Array.from({ length: 10 }, (_, i) =>
|
|
34
|
-
pool.run(async () => {
|
|
35
|
-
await Bun.sleep(10);
|
|
36
|
-
results.push(i);
|
|
37
|
-
return i;
|
|
38
|
-
}),
|
|
39
|
-
);
|
|
40
|
-
|
|
41
|
-
const returned = await Promise.all(tasks);
|
|
42
|
-
|
|
43
|
-
expect(returned).toEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
|
44
|
-
expect(results).toHaveLength(10);
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
test("errors propagate correctly", async () => {
|
|
48
|
-
const pool = createPool(2);
|
|
49
|
-
|
|
50
|
-
const failing = pool.run(async () => {
|
|
51
|
-
throw new Error("boom");
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
await expect(failing).rejects.toThrow("boom");
|
|
55
|
-
|
|
56
|
-
// Pool should still work after error
|
|
57
|
-
const result = await pool.run(async () => "ok");
|
|
58
|
-
expect(result).toBe("ok");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
test("queued tasks run as active tasks complete", async () => {
|
|
62
|
-
const pool = createPool(1);
|
|
63
|
-
const order: string[] = [];
|
|
64
|
-
|
|
65
|
-
const p1 = pool.run(async () => {
|
|
66
|
-
await Bun.sleep(30);
|
|
67
|
-
order.push("first");
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
const p2 = pool.run(async () => {
|
|
71
|
-
order.push("second");
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
await Promise.all([p1, p2]);
|
|
75
|
-
expect(order).toEqual(["first", "second"]);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test("active_count and queue_length track state", async () => {
|
|
79
|
-
const pool = createPool(2);
|
|
80
|
-
const started: Array<() => void> = [];
|
|
81
|
-
|
|
82
|
-
// Create tasks that block until we release them
|
|
83
|
-
const make_blocking = () =>
|
|
84
|
-
pool.run(() => new Promise<void>((resolve) => { started.push(resolve); }));
|
|
85
|
-
|
|
86
|
-
const p1 = make_blocking();
|
|
87
|
-
const p2 = make_blocking();
|
|
88
|
-
const p3 = make_blocking();
|
|
89
|
-
|
|
90
|
-
// Wait for first two to start
|
|
91
|
-
await Bun.sleep(10);
|
|
92
|
-
|
|
93
|
-
expect(pool.active_count).toBe(2);
|
|
94
|
-
expect(pool.queue_length).toBe(1);
|
|
95
|
-
|
|
96
|
-
// Release first task
|
|
97
|
-
started[0]!();
|
|
98
|
-
await Bun.sleep(10);
|
|
99
|
-
|
|
100
|
-
expect(pool.active_count).toBe(2); // third task started
|
|
101
|
-
expect(pool.queue_length).toBe(0);
|
|
102
|
-
|
|
103
|
-
// Release remaining
|
|
104
|
-
started[1]!();
|
|
105
|
-
started[2]!();
|
|
106
|
-
await Promise.all([p1, p2, p3]);
|
|
107
|
-
|
|
108
|
-
expect(pool.active_count).toBe(0);
|
|
109
|
-
expect(pool.queue_length).toBe(0);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { mkdtemp, rm } from "node:fs/promises";
|
|
2
|
-
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
|
|
5
|
-
async function run(args: string[], cwd: string): Promise<void> {
|
|
6
|
-
const proc = Bun.spawn(args, { cwd, stdout: "pipe", stderr: "pipe" });
|
|
7
|
-
const code = await proc.exited;
|
|
8
|
-
if (code !== 0) {
|
|
9
|
-
const stderr = await new Response(proc.stderr).text();
|
|
10
|
-
throw new Error(`Command failed: ${args.join(" ")}\n${stderr}`);
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export async function createTempDir(): Promise<string> {
|
|
15
|
-
return mkdtemp(join(tmpdir(), "overview-test-"));
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export async function cleanupTempDir(dir: string): Promise<void> {
|
|
19
|
-
await rm(dir, { recursive: true, force: true });
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function initRepo(dir: string, name: string): Promise<string> {
|
|
23
|
-
const repo_path = join(dir, name);
|
|
24
|
-
await Bun.spawn(["mkdir", "-p", repo_path]).exited;
|
|
25
|
-
await run(["git", "init"], repo_path);
|
|
26
|
-
await run(["git", "config", "user.email", "test@test.com"], repo_path);
|
|
27
|
-
await run(["git", "config", "user.name", "Test User"], repo_path);
|
|
28
|
-
await Bun.write(join(repo_path, "README.md"), "# " + name);
|
|
29
|
-
await run(["git", "add", "."], repo_path);
|
|
30
|
-
await run(["git", "commit", "-m", "initial commit"], repo_path);
|
|
31
|
-
return repo_path;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function addCommit(
|
|
35
|
-
repoPath: string,
|
|
36
|
-
filename: string,
|
|
37
|
-
content: string,
|
|
38
|
-
message: string,
|
|
39
|
-
): Promise<void> {
|
|
40
|
-
await Bun.write(join(repoPath, filename), content);
|
|
41
|
-
await run(["git", "add", filename], repoPath);
|
|
42
|
-
await run(["git", "commit", "-m", message], repoPath);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function createBranch(repoPath: string, branchName: string): Promise<void> {
|
|
46
|
-
await run(["git", "checkout", "-b", branchName], repoPath);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export async function modifyFile(repoPath: string, filename: string, content: string): Promise<void> {
|
|
50
|
-
await Bun.write(join(repoPath, filename), content);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export async function addUntracked(repoPath: string, filename: string, content: string): Promise<void> {
|
|
54
|
-
await Bun.write(join(repoPath, filename), content);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
export async function stashChanges(repoPath: string): Promise<void> {
|
|
58
|
-
await Bun.write(join(repoPath, "README.md"), "stash content");
|
|
59
|
-
await run(["git", "stash"], repoPath);
|
|
60
|
-
}
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { collectStatus } from "../../src/git-status";
|
|
5
|
-
import {
|
|
6
|
-
createTempDir,
|
|
7
|
-
cleanupTempDir,
|
|
8
|
-
initRepo,
|
|
9
|
-
stashChanges,
|
|
10
|
-
} from "../helpers";
|
|
11
|
-
|
|
12
|
-
describe("git-status integration", () => {
|
|
13
|
-
let temp_dir: string;
|
|
14
|
-
|
|
15
|
-
beforeAll(async () => {
|
|
16
|
-
temp_dir = await createTempDir();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
afterAll(async () => {
|
|
20
|
-
await cleanupTempDir(temp_dir);
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
test("parses branch info", async () => {
|
|
24
|
-
const dir = join(temp_dir, "branch-test");
|
|
25
|
-
await mkdir(dir, { recursive: true });
|
|
26
|
-
const repo_path = await initRepo(dir, "branch-repo");
|
|
27
|
-
|
|
28
|
-
const result = await collectStatus(repo_path, dir);
|
|
29
|
-
expect(result.ok).toBe(true);
|
|
30
|
-
if (!result.ok) return;
|
|
31
|
-
|
|
32
|
-
// git init may default to "main" or "master"
|
|
33
|
-
expect(["main", "master"]).toContain(result.value.current_branch);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
test("detects stashes", async () => {
|
|
37
|
-
const dir = join(temp_dir, "stash-test");
|
|
38
|
-
await mkdir(dir, { recursive: true });
|
|
39
|
-
const repo_path = await initRepo(dir, "stash-repo");
|
|
40
|
-
await stashChanges(repo_path);
|
|
41
|
-
|
|
42
|
-
const result = await collectStatus(repo_path, dir);
|
|
43
|
-
expect(result.ok).toBe(true);
|
|
44
|
-
if (!result.ok) return;
|
|
45
|
-
|
|
46
|
-
expect(result.value.stash_count).toBeGreaterThan(0);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("handles repo with no remote", async () => {
|
|
50
|
-
const dir = join(temp_dir, "no-remote-test");
|
|
51
|
-
await mkdir(dir, { recursive: true });
|
|
52
|
-
const repo_path = await initRepo(dir, "no-remote-repo");
|
|
53
|
-
|
|
54
|
-
const result = await collectStatus(repo_path, dir);
|
|
55
|
-
expect(result.ok).toBe(true);
|
|
56
|
-
if (!result.ok) return;
|
|
57
|
-
|
|
58
|
-
expect(result.value.remote_url).toBeNull();
|
|
59
|
-
expect(result.value.ahead).toBe(0);
|
|
60
|
-
expect(result.value.behind).toBe(0);
|
|
61
|
-
});
|
|
62
|
-
});
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeAll, afterAll } from "bun:test";
|
|
2
|
-
import { mkdir } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { scanDirectory, scanAndCollect } from "../../src/index";
|
|
5
|
-
import {
|
|
6
|
-
createTempDir,
|
|
7
|
-
cleanupTempDir,
|
|
8
|
-
initRepo,
|
|
9
|
-
addCommit,
|
|
10
|
-
modifyFile,
|
|
11
|
-
addUntracked,
|
|
12
|
-
} from "../helpers";
|
|
13
|
-
|
|
14
|
-
describe("scanner integration", () => {
|
|
15
|
-
let temp_dir: string;
|
|
16
|
-
|
|
17
|
-
beforeAll(async () => {
|
|
18
|
-
temp_dir = await createTempDir();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
afterAll(async () => {
|
|
22
|
-
await cleanupTempDir(temp_dir);
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("discovers repos at root level", async () => {
|
|
26
|
-
const dir = join(temp_dir, "root-level");
|
|
27
|
-
await mkdir(dir, { recursive: true });
|
|
28
|
-
await initRepo(dir, "repo-a");
|
|
29
|
-
await initRepo(dir, "repo-b");
|
|
30
|
-
await initRepo(dir, "repo-c");
|
|
31
|
-
|
|
32
|
-
const result = await scanDirectory(dir, { depth: 1, ignore: [] });
|
|
33
|
-
expect(result.ok).toBe(true);
|
|
34
|
-
if (!result.ok) return;
|
|
35
|
-
|
|
36
|
-
const repos = result.value.filter((n) => n.type === "repo");
|
|
37
|
-
expect(repos.length).toBe(3);
|
|
38
|
-
expect(repos.map((r) => r.name).sort()).toEqual(["repo-a", "repo-b", "repo-c"]);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
test("discovers nested repos", async () => {
|
|
42
|
-
const dir = join(temp_dir, "nested");
|
|
43
|
-
await mkdir(dir, { recursive: true });
|
|
44
|
-
const nested_path = join(dir, "a", "b");
|
|
45
|
-
await mkdir(nested_path, { recursive: true });
|
|
46
|
-
await initRepo(nested_path, "repo1");
|
|
47
|
-
|
|
48
|
-
const result = await scanDirectory(dir, { depth: 4, ignore: [] });
|
|
49
|
-
expect(result.ok).toBe(true);
|
|
50
|
-
if (!result.ok) return;
|
|
51
|
-
|
|
52
|
-
const find_repo = (nodes: typeof result.value): boolean =>
|
|
53
|
-
nodes.some((n) => (n.type === "repo" && n.name === "repo1") || find_repo(n.children));
|
|
54
|
-
|
|
55
|
-
expect(find_repo(result.value)).toBe(true);
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
test("ignores directories matching ignore patterns", async () => {
|
|
59
|
-
const dir = join(temp_dir, "ignore-test");
|
|
60
|
-
await mkdir(dir, { recursive: true });
|
|
61
|
-
await initRepo(dir, "good-repo");
|
|
62
|
-
const nm_dir = join(dir, "node_modules");
|
|
63
|
-
await mkdir(nm_dir, { recursive: true });
|
|
64
|
-
await initRepo(nm_dir, "hidden-repo");
|
|
65
|
-
|
|
66
|
-
const result = await scanDirectory(dir, { depth: 2, ignore: ["node_modules"] });
|
|
67
|
-
expect(result.ok).toBe(true);
|
|
68
|
-
if (!result.ok) return;
|
|
69
|
-
|
|
70
|
-
const all_names = flatNames(result.value);
|
|
71
|
-
expect(all_names).toContain("good-repo");
|
|
72
|
-
expect(all_names).not.toContain("node_modules");
|
|
73
|
-
expect(all_names).not.toContain("hidden-repo");
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test("collects status for clean repo", async () => {
|
|
77
|
-
const dir = join(temp_dir, "clean-test");
|
|
78
|
-
await mkdir(dir, { recursive: true });
|
|
79
|
-
await initRepo(dir, "clean-repo");
|
|
80
|
-
await addCommit(join(dir, "clean-repo"), "file.txt", "content", "add file");
|
|
81
|
-
|
|
82
|
-
const result = await scanAndCollect(dir, { depth: 1, ignore: [] });
|
|
83
|
-
expect(result.ok).toBe(true);
|
|
84
|
-
if (!result.ok) return;
|
|
85
|
-
|
|
86
|
-
const repo = result.value.find((n) => n.name === "clean-repo");
|
|
87
|
-
expect(repo).toBeDefined();
|
|
88
|
-
expect(repo!.status).not.toBeNull();
|
|
89
|
-
expect(repo!.status!.is_clean).toBe(true);
|
|
90
|
-
expect(repo!.status!.health).toBe("clean");
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
test("detects dirty state", async () => {
|
|
94
|
-
const dir = join(temp_dir, "dirty-test");
|
|
95
|
-
await mkdir(dir, { recursive: true });
|
|
96
|
-
const repo_path = await initRepo(dir, "dirty-repo");
|
|
97
|
-
await addCommit(repo_path, "file.txt", "original", "add file");
|
|
98
|
-
await modifyFile(repo_path, "file.txt", "modified content");
|
|
99
|
-
|
|
100
|
-
const result = await scanAndCollect(dir, { depth: 1, ignore: [] });
|
|
101
|
-
expect(result.ok).toBe(true);
|
|
102
|
-
if (!result.ok) return;
|
|
103
|
-
|
|
104
|
-
const repo = result.value.find((n) => n.name === "dirty-repo");
|
|
105
|
-
expect(repo).toBeDefined();
|
|
106
|
-
expect(repo!.status).not.toBeNull();
|
|
107
|
-
expect(repo!.status!.modified_count).toBeGreaterThan(0);
|
|
108
|
-
expect(repo!.status!.health).toBe("dirty");
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
test("detects untracked files", async () => {
|
|
112
|
-
const dir = join(temp_dir, "untracked-test");
|
|
113
|
-
await mkdir(dir, { recursive: true });
|
|
114
|
-
const repo_path = await initRepo(dir, "untracked-repo");
|
|
115
|
-
await addUntracked(repo_path, "new-file.txt", "untracked content");
|
|
116
|
-
|
|
117
|
-
const result = await scanAndCollect(dir, { depth: 1, ignore: [] });
|
|
118
|
-
expect(result.ok).toBe(true);
|
|
119
|
-
if (!result.ok) return;
|
|
120
|
-
|
|
121
|
-
const repo = result.value.find((n) => n.name === "untracked-repo");
|
|
122
|
-
expect(repo).toBeDefined();
|
|
123
|
-
expect(repo!.status).not.toBeNull();
|
|
124
|
-
expect(repo!.status!.untracked_count).toBeGreaterThan(0);
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
test("returns empty array for empty directory", async () => {
|
|
128
|
-
const dir = join(temp_dir, "empty-test");
|
|
129
|
-
await mkdir(dir, { recursive: true });
|
|
130
|
-
|
|
131
|
-
const result = await scanDirectory(dir, { depth: 1, ignore: [] });
|
|
132
|
-
expect(result.ok).toBe(true);
|
|
133
|
-
if (!result.ok) return;
|
|
134
|
-
expect(result.value).toEqual([]);
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
function flatNames(nodes: { name: string; children: typeof nodes }[]): string[] {
|
|
139
|
-
return nodes.flatMap((n) => [n.name, ...flatNames(n.children)]);
|
|
140
|
-
}
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
import { describe, test, expect, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { mkdtemp, rm, writeFile, mkdir } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { readOcnStates } from "../src/ocn";
|
|
6
|
-
|
|
7
|
-
describe("readOcnStates", () => {
|
|
8
|
-
let temp_dir: string;
|
|
9
|
-
const original_env = process.env.OCN_STATE_DIR;
|
|
10
|
-
|
|
11
|
-
beforeEach(async () => {
|
|
12
|
-
temp_dir = await mkdtemp(join(tmpdir(), "ocn-test-"));
|
|
13
|
-
process.env.OCN_STATE_DIR = temp_dir;
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
afterEach(async () => {
|
|
17
|
-
if (original_env !== undefined) {
|
|
18
|
-
process.env.OCN_STATE_DIR = original_env;
|
|
19
|
-
} else {
|
|
20
|
-
delete process.env.OCN_STATE_DIR;
|
|
21
|
-
}
|
|
22
|
-
await rm(temp_dir, { recursive: true, force: true });
|
|
23
|
-
});
|
|
24
|
-
|
|
25
|
-
test("returns empty map when state dir is empty", async () => {
|
|
26
|
-
const result = await readOcnStates();
|
|
27
|
-
expect(result.ok).toBe(true);
|
|
28
|
-
if (result.ok) {
|
|
29
|
-
expect(result.value.size).toBe(0);
|
|
30
|
-
}
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
test("returns empty map when state dir does not exist", async () => {
|
|
34
|
-
process.env.OCN_STATE_DIR = join(temp_dir, "nonexistent");
|
|
35
|
-
const result = await readOcnStates();
|
|
36
|
-
expect(result.ok).toBe(true);
|
|
37
|
-
if (result.ok) {
|
|
38
|
-
expect(result.value.size).toBe(0);
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test("reads valid state file with alive PID", async () => {
|
|
43
|
-
const pid = process.pid; // current process is always alive
|
|
44
|
-
const state = {
|
|
45
|
-
pid,
|
|
46
|
-
directory: "/Users/tom/dev/test-repo",
|
|
47
|
-
project: "test-repo",
|
|
48
|
-
status: "busy",
|
|
49
|
-
last_transition: "2026-01-01T00:00:00Z",
|
|
50
|
-
session_id: "ses_test123",
|
|
51
|
-
};
|
|
52
|
-
await writeFile(join(temp_dir, `${pid}.json`), JSON.stringify(state));
|
|
53
|
-
|
|
54
|
-
const result = await readOcnStates();
|
|
55
|
-
expect(result.ok).toBe(true);
|
|
56
|
-
if (result.ok) {
|
|
57
|
-
expect(result.value.size).toBe(1);
|
|
58
|
-
const entry = result.value.get("/Users/tom/dev/test-repo");
|
|
59
|
-
expect(entry).toBeDefined();
|
|
60
|
-
expect(entry!.pid).toBe(pid);
|
|
61
|
-
expect(entry!.status).toBe("busy");
|
|
62
|
-
expect(entry!.session_id).toBe("ses_test123");
|
|
63
|
-
}
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
test("filters out stale PIDs", async () => {
|
|
67
|
-
const state = {
|
|
68
|
-
pid: 99999999, // very unlikely to be a running process
|
|
69
|
-
directory: "/Users/tom/dev/stale-repo",
|
|
70
|
-
project: "stale-repo",
|
|
71
|
-
status: "busy",
|
|
72
|
-
last_transition: "2026-01-01T00:00:00Z",
|
|
73
|
-
session_id: "ses_stale",
|
|
74
|
-
};
|
|
75
|
-
await writeFile(join(temp_dir, "99999999.json"), JSON.stringify(state));
|
|
76
|
-
|
|
77
|
-
const result = await readOcnStates();
|
|
78
|
-
expect(result.ok).toBe(true);
|
|
79
|
-
if (result.ok) {
|
|
80
|
-
expect(result.value.size).toBe(0);
|
|
81
|
-
}
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
test("skips malformed JSON files", async () => {
|
|
85
|
-
await writeFile(join(temp_dir, "bad.json"), "not json {{{");
|
|
86
|
-
// Also write a valid one with alive PID
|
|
87
|
-
const pid = process.pid;
|
|
88
|
-
const state = {
|
|
89
|
-
pid,
|
|
90
|
-
directory: "/Users/tom/dev/good-repo",
|
|
91
|
-
project: "good-repo",
|
|
92
|
-
status: "idle",
|
|
93
|
-
last_transition: "2026-01-01T00:00:00Z",
|
|
94
|
-
session_id: "ses_good",
|
|
95
|
-
};
|
|
96
|
-
await writeFile(join(temp_dir, `${pid}.json`), JSON.stringify(state));
|
|
97
|
-
|
|
98
|
-
const result = await readOcnStates();
|
|
99
|
-
expect(result.ok).toBe(true);
|
|
100
|
-
if (result.ok) {
|
|
101
|
-
expect(result.value.size).toBe(1);
|
|
102
|
-
expect(result.value.has("/Users/tom/dev/good-repo")).toBe(true);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
test("skips files with invalid status values", async () => {
|
|
107
|
-
const pid = process.pid;
|
|
108
|
-
const state = {
|
|
109
|
-
pid,
|
|
110
|
-
directory: "/Users/tom/dev/invalid",
|
|
111
|
-
project: "invalid",
|
|
112
|
-
status: "unknown_status",
|
|
113
|
-
last_transition: "2026-01-01T00:00:00Z",
|
|
114
|
-
session_id: "ses_invalid",
|
|
115
|
-
};
|
|
116
|
-
await writeFile(join(temp_dir, `${pid}.json`), JSON.stringify(state));
|
|
117
|
-
|
|
118
|
-
const result = await readOcnStates();
|
|
119
|
-
expect(result.ok).toBe(true);
|
|
120
|
-
if (result.ok) {
|
|
121
|
-
expect(result.value.size).toBe(0);
|
|
122
|
-
}
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test("skips non-json files", async () => {
|
|
126
|
-
await writeFile(join(temp_dir, "readme.txt"), "not a state file");
|
|
127
|
-
const result = await readOcnStates();
|
|
128
|
-
expect(result.ok).toBe(true);
|
|
129
|
-
if (result.ok) {
|
|
130
|
-
expect(result.value.size).toBe(0);
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test("reads multiple state files", async () => {
|
|
135
|
-
const pid = process.pid;
|
|
136
|
-
const state1 = {
|
|
137
|
-
pid,
|
|
138
|
-
directory: "/Users/tom/dev/repo-a",
|
|
139
|
-
project: "repo-a",
|
|
140
|
-
status: "busy",
|
|
141
|
-
last_transition: "2026-01-01T00:00:00Z",
|
|
142
|
-
session_id: "ses_a",
|
|
143
|
-
};
|
|
144
|
-
const state2 = {
|
|
145
|
-
pid,
|
|
146
|
-
directory: "/Users/tom/dev/repo-b",
|
|
147
|
-
project: "repo-b",
|
|
148
|
-
status: "prompting",
|
|
149
|
-
last_transition: "2026-01-01T00:00:00Z",
|
|
150
|
-
session_id: "ses_b",
|
|
151
|
-
};
|
|
152
|
-
// Use different filenames (same pid but different entries)
|
|
153
|
-
await writeFile(join(temp_dir, `${pid}.json`), JSON.stringify(state1));
|
|
154
|
-
await writeFile(join(temp_dir, `${pid}_2.json`), JSON.stringify(state2));
|
|
155
|
-
|
|
156
|
-
const result = await readOcnStates();
|
|
157
|
-
expect(result.ok).toBe(true);
|
|
158
|
-
if (result.ok) {
|
|
159
|
-
expect(result.value.size).toBe(2);
|
|
160
|
-
expect(result.value.get("/Users/tom/dev/repo-a")?.status).toBe("busy");
|
|
161
|
-
expect(result.value.get("/Users/tom/dev/repo-b")?.status).toBe("prompting");
|
|
162
|
-
}
|
|
163
|
-
});
|
|
164
|
-
});
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@overview/core",
|
|
3
|
-
"version": "0.1.0",
|
|
4
|
-
"main": "src/index.ts",
|
|
5
|
-
"types": "src/index.ts",
|
|
6
|
-
"scripts": {
|
|
7
|
-
"typecheck": "tsc --noEmit"
|
|
8
|
-
},
|
|
9
|
-
"dependencies": {
|
|
10
|
-
"@f0rbit/corpus": "link:@f0rbit/corpus",
|
|
11
|
-
"@devpad/api": "link:@devpad/api"
|
|
12
|
-
}
|
|
13
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
export interface CacheEntry<T> {
|
|
2
|
-
data: T;
|
|
3
|
-
fetched_at: number;
|
|
4
|
-
ttl_ms: number;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export class DataCache<T> {
|
|
8
|
-
private entries = new Map<string, CacheEntry<T>>();
|
|
9
|
-
|
|
10
|
-
get(key: string): T | null {
|
|
11
|
-
const entry = this.entries.get(key);
|
|
12
|
-
if (!entry) return null;
|
|
13
|
-
if (Date.now() - entry.fetched_at > entry.ttl_ms) {
|
|
14
|
-
this.entries.delete(key);
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
return entry.data;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
set(key: string, data: T, ttl_ms: number): void {
|
|
21
|
-
this.entries.set(key, { data, fetched_at: Date.now(), ttl_ms });
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
invalidate(key: string): void {
|
|
25
|
-
this.entries.delete(key);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
clear(): void {
|
|
29
|
-
this.entries.clear();
|
|
30
|
-
}
|
|
31
|
-
}
|