@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
|
@@ -1,286 +0,0 @@
|
|
|
1
|
-
import { ok, err, type Result } from "@f0rbit/corpus";
|
|
2
|
-
|
|
3
|
-
export type GithubError =
|
|
4
|
-
| { kind: "not_github_repo" }
|
|
5
|
-
| { kind: "gh_cli_not_found" }
|
|
6
|
-
| { kind: "gh_auth_required" }
|
|
7
|
-
| { kind: "api_error"; cause: string }
|
|
8
|
-
| { kind: "rate_limited" };
|
|
9
|
-
|
|
10
|
-
export interface GithubPR {
|
|
11
|
-
number: number;
|
|
12
|
-
title: string;
|
|
13
|
-
state: string;
|
|
14
|
-
review_decision: string | null;
|
|
15
|
-
ci_status: "success" | "failure" | "pending" | "none";
|
|
16
|
-
is_draft: boolean;
|
|
17
|
-
author: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface GithubIssue {
|
|
21
|
-
number: number;
|
|
22
|
-
title: string;
|
|
23
|
-
labels: string[];
|
|
24
|
-
created_at: string;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export interface GithubWorkflowRun {
|
|
28
|
-
name: string;
|
|
29
|
-
status: string;
|
|
30
|
-
conclusion: string | null;
|
|
31
|
-
head_branch: string;
|
|
32
|
-
duration_seconds: number | null;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface GithubRelease {
|
|
36
|
-
tag_name: string;
|
|
37
|
-
name: string;
|
|
38
|
-
published_at: string;
|
|
39
|
-
commits_since: number;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
export interface GithubRepoData {
|
|
43
|
-
prs: GithubPR[];
|
|
44
|
-
issues: GithubIssue[];
|
|
45
|
-
ci_runs: GithubWorkflowRun[];
|
|
46
|
-
latest_release: GithubRelease | null;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
let gh_available: boolean | null = null;
|
|
50
|
-
|
|
51
|
-
export function checkGhAvailable(): boolean {
|
|
52
|
-
if (gh_available === null) {
|
|
53
|
-
gh_available = Bun.which("gh") !== null;
|
|
54
|
-
}
|
|
55
|
-
return gh_available;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function isGithubRemote(remote_url: string | null): boolean {
|
|
59
|
-
if (!remote_url) return false;
|
|
60
|
-
return remote_url.includes("github.com");
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function parseGhOwnerRepo(remote_url: string): { owner: string; repo: string } | null {
|
|
64
|
-
const match = remote_url.match(/github\.com[:/]([^/]+)\/([^/.]+)/);
|
|
65
|
-
if (match) return { owner: match[1]!, repo: match[2]! };
|
|
66
|
-
return null;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
function safeJsonParse<T>(text: string): Result<T, GithubError> {
|
|
70
|
-
try {
|
|
71
|
-
return ok(JSON.parse(text) as T);
|
|
72
|
-
} catch {
|
|
73
|
-
return err({ kind: "api_error", cause: "invalid JSON response from gh CLI" });
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
async function gh(args: string[], cwd: string): Promise<Result<string, GithubError>> {
|
|
78
|
-
if (!checkGhAvailable()) return err({ kind: "gh_cli_not_found" });
|
|
79
|
-
|
|
80
|
-
const proc = Bun.spawn(["gh", ...args], {
|
|
81
|
-
cwd,
|
|
82
|
-
stdout: "pipe",
|
|
83
|
-
stderr: "pipe",
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
const stdout = await new Response(proc.stdout).text();
|
|
87
|
-
const stderr = await new Response(proc.stderr).text();
|
|
88
|
-
const exit_code = await proc.exited;
|
|
89
|
-
|
|
90
|
-
if (exit_code !== 0) {
|
|
91
|
-
if (stderr.includes("auth login")) return err({ kind: "gh_auth_required" });
|
|
92
|
-
if (stderr.includes("rate limit")) return err({ kind: "rate_limited" });
|
|
93
|
-
return err({ kind: "api_error", cause: stderr.trim() });
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
return ok(stdout);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
interface RawPR {
|
|
100
|
-
number: number;
|
|
101
|
-
title: string;
|
|
102
|
-
state: string;
|
|
103
|
-
reviewDecision: string | null;
|
|
104
|
-
statusCheckRollup: Array<{ state: string; status: string; conclusion: string }> | null;
|
|
105
|
-
isDraft: boolean;
|
|
106
|
-
author: { login: string };
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
function deriveCiStatus(checks: RawPR["statusCheckRollup"]): GithubPR["ci_status"] {
|
|
110
|
-
if (!checks || checks.length === 0) return "none";
|
|
111
|
-
const has_failure = checks.some(
|
|
112
|
-
(c) => c.conclusion === "FAILURE" || c.conclusion === "failure",
|
|
113
|
-
);
|
|
114
|
-
if (has_failure) return "failure";
|
|
115
|
-
const all_success = checks.every(
|
|
116
|
-
(c) => c.conclusion === "SUCCESS" || c.conclusion === "success",
|
|
117
|
-
);
|
|
118
|
-
if (all_success) return "success";
|
|
119
|
-
return "pending";
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
function mapPR(raw: RawPR): GithubPR {
|
|
123
|
-
return {
|
|
124
|
-
number: raw.number,
|
|
125
|
-
title: raw.title,
|
|
126
|
-
state: raw.state,
|
|
127
|
-
review_decision: raw.reviewDecision ?? null,
|
|
128
|
-
ci_status: deriveCiStatus(raw.statusCheckRollup),
|
|
129
|
-
is_draft: raw.isDraft,
|
|
130
|
-
author: raw.author?.login ?? "unknown",
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
export async function collectPRs(cwd: string): Promise<Result<GithubPR[], GithubError>> {
|
|
135
|
-
const result = await gh(
|
|
136
|
-
["pr", "list", "--json", "number,title,state,reviewDecision,statusCheckRollup,isDraft,author", "--limit", "20"],
|
|
137
|
-
cwd,
|
|
138
|
-
);
|
|
139
|
-
if (!result.ok) return result;
|
|
140
|
-
|
|
141
|
-
const parsed = safeJsonParse<RawPR[]>(result.value);
|
|
142
|
-
if (!parsed.ok) return parsed;
|
|
143
|
-
|
|
144
|
-
return ok(parsed.value.map(mapPR));
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
interface RawIssue {
|
|
148
|
-
number: number;
|
|
149
|
-
title: string;
|
|
150
|
-
labels: Array<{ name: string }>;
|
|
151
|
-
createdAt: string;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export async function collectIssues(cwd: string): Promise<Result<GithubIssue[], GithubError>> {
|
|
155
|
-
const result = await gh(
|
|
156
|
-
["issue", "list", "--json", "number,title,labels,createdAt", "--limit", "10"],
|
|
157
|
-
cwd,
|
|
158
|
-
);
|
|
159
|
-
if (!result.ok) return result;
|
|
160
|
-
|
|
161
|
-
const parsed = safeJsonParse<RawIssue[]>(result.value);
|
|
162
|
-
if (!parsed.ok) return parsed;
|
|
163
|
-
|
|
164
|
-
return ok(
|
|
165
|
-
parsed.value.map((raw) => ({
|
|
166
|
-
number: raw.number,
|
|
167
|
-
title: raw.title,
|
|
168
|
-
labels: raw.labels.map((l) => l.name),
|
|
169
|
-
created_at: raw.createdAt,
|
|
170
|
-
})),
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
interface RawWorkflowRun {
|
|
175
|
-
name: string;
|
|
176
|
-
status: string;
|
|
177
|
-
conclusion: string | null;
|
|
178
|
-
headBranch: string;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
export async function collectCIRuns(cwd: string): Promise<Result<GithubWorkflowRun[], GithubError>> {
|
|
182
|
-
const result = await gh(
|
|
183
|
-
["run", "list", "--json", "name,status,conclusion,headBranch", "--limit", "10"],
|
|
184
|
-
cwd,
|
|
185
|
-
);
|
|
186
|
-
if (!result.ok) return result;
|
|
187
|
-
|
|
188
|
-
const parsed = safeJsonParse<RawWorkflowRun[]>(result.value);
|
|
189
|
-
if (!parsed.ok) return parsed;
|
|
190
|
-
|
|
191
|
-
return ok(
|
|
192
|
-
parsed.value.map((raw) => ({
|
|
193
|
-
name: raw.name,
|
|
194
|
-
status: raw.status,
|
|
195
|
-
conclusion: raw.conclusion ?? null,
|
|
196
|
-
head_branch: raw.headBranch,
|
|
197
|
-
duration_seconds: null,
|
|
198
|
-
})),
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
interface RawRelease {
|
|
203
|
-
tagName: string;
|
|
204
|
-
name: string;
|
|
205
|
-
publishedAt: string;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
async function countCommitsSince(tag: string, cwd: string): Promise<number> {
|
|
209
|
-
// Ensure tags are fetched — the release tag may not exist locally
|
|
210
|
-
const fetch_proc = Bun.spawn(["git", "fetch", "--tags", "--quiet"], {
|
|
211
|
-
cwd,
|
|
212
|
-
stdout: "pipe",
|
|
213
|
-
stderr: "pipe",
|
|
214
|
-
});
|
|
215
|
-
await fetch_proc.exited;
|
|
216
|
-
|
|
217
|
-
const proc = Bun.spawn(["git", "rev-list", `${tag}..HEAD`, "--count"], {
|
|
218
|
-
cwd,
|
|
219
|
-
stdout: "pipe",
|
|
220
|
-
stderr: "pipe",
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
const stdout = await new Response(proc.stdout).text();
|
|
224
|
-
const exit_code = await proc.exited;
|
|
225
|
-
|
|
226
|
-
if (exit_code !== 0) return 0;
|
|
227
|
-
|
|
228
|
-
const count = parseInt(stdout.trim(), 10);
|
|
229
|
-
return Number.isNaN(count) ? 0 : count;
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
export async function collectRelease(cwd: string): Promise<Result<GithubRelease | null, GithubError>> {
|
|
233
|
-
const result = await gh(
|
|
234
|
-
["release", "view", "--json", "tagName,publishedAt,name"],
|
|
235
|
-
cwd,
|
|
236
|
-
);
|
|
237
|
-
if (!result.ok) {
|
|
238
|
-
if (result.error.kind === "api_error" && result.error.cause.includes("no releases")) {
|
|
239
|
-
return ok(null);
|
|
240
|
-
}
|
|
241
|
-
return result;
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const parsed = safeJsonParse<RawRelease>(result.value);
|
|
245
|
-
if (!parsed.ok) return parsed;
|
|
246
|
-
|
|
247
|
-
const raw = parsed.value;
|
|
248
|
-
const commits_since = await countCommitsSince(raw.tagName, cwd);
|
|
249
|
-
|
|
250
|
-
return ok({
|
|
251
|
-
tag_name: raw.tagName,
|
|
252
|
-
name: raw.name,
|
|
253
|
-
published_at: raw.publishedAt,
|
|
254
|
-
commits_since,
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function isFatalError(error: GithubError): boolean {
|
|
259
|
-
return error.kind === "gh_auth_required" || error.kind === "rate_limited" || error.kind === "gh_cli_not_found";
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
export async function collectGithubData(
|
|
263
|
-
repo_path: string,
|
|
264
|
-
remote_url: string | null,
|
|
265
|
-
): Promise<Result<GithubRepoData, GithubError>> {
|
|
266
|
-
if (!isGithubRemote(remote_url)) return err({ kind: "not_github_repo" });
|
|
267
|
-
if (!checkGhAvailable()) return err({ kind: "gh_cli_not_found" });
|
|
268
|
-
|
|
269
|
-
const [prs_result, issues_result, ci_result, release_result] = await Promise.all([
|
|
270
|
-
collectPRs(repo_path),
|
|
271
|
-
collectIssues(repo_path),
|
|
272
|
-
collectCIRuns(repo_path),
|
|
273
|
-
collectRelease(repo_path),
|
|
274
|
-
]);
|
|
275
|
-
|
|
276
|
-
const results = [prs_result, issues_result, ci_result, release_result];
|
|
277
|
-
const fatal = results.find((r) => !r.ok && isFatalError(r.error));
|
|
278
|
-
if (fatal && !fatal.ok) return fatal as Result<never, GithubError>;
|
|
279
|
-
|
|
280
|
-
return ok({
|
|
281
|
-
prs: prs_result.ok ? prs_result.value : [],
|
|
282
|
-
issues: issues_result.ok ? issues_result.value : [],
|
|
283
|
-
ci_runs: ci_result.ok ? ci_result.value : [],
|
|
284
|
-
latest_release: release_result.ok ? release_result.value : null,
|
|
285
|
-
});
|
|
286
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
import { ok, type Result } from "@f0rbit/corpus";
|
|
2
|
-
import type { RepoNode, OcnStatus } from "./types";
|
|
3
|
-
import { scanDirectory, type ScanError } from "./scanner";
|
|
4
|
-
import { collectStatus } from "./git-status";
|
|
5
|
-
import { detectWorktrees } from "./worktree";
|
|
6
|
-
import { createPool } from "./concurrency";
|
|
7
|
-
import { readOcnStates } from "./ocn";
|
|
8
|
-
|
|
9
|
-
export * from "./types";
|
|
10
|
-
export * from "./scanner";
|
|
11
|
-
export * from "./worktree";
|
|
12
|
-
export * from "./git-status";
|
|
13
|
-
export * from "./git-graph";
|
|
14
|
-
export * from "./git-stats";
|
|
15
|
-
export * from "./watcher";
|
|
16
|
-
export * from "./cache";
|
|
17
|
-
export * from "./github";
|
|
18
|
-
export * from "./devpad";
|
|
19
|
-
export * from "./concurrency";
|
|
20
|
-
export * from "./ocn";
|
|
21
|
-
|
|
22
|
-
export type ScanAndCollectError = ScanError;
|
|
23
|
-
|
|
24
|
-
const pool = createPool(8);
|
|
25
|
-
|
|
26
|
-
async function populateNode(node: RepoNode, scanRoot: string, ocn_map: Map<string, OcnStatus>): Promise<void> {
|
|
27
|
-
if (node.type === "repo" || node.type === "worktree") {
|
|
28
|
-
await pool.run(async () => {
|
|
29
|
-
const [status_result, worktree_result] = await Promise.all([
|
|
30
|
-
collectStatus(node.path, scanRoot),
|
|
31
|
-
detectWorktrees(node.path),
|
|
32
|
-
]);
|
|
33
|
-
node.status = status_result.ok ? status_result.value : null;
|
|
34
|
-
if (node.status) {
|
|
35
|
-
node.status.ocn_status = ocn_map.get(node.path) ?? null;
|
|
36
|
-
}
|
|
37
|
-
node.worktrees = worktree_result.ok ? worktree_result.value : [];
|
|
38
|
-
});
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
await Promise.all(node.children.map((child) => populateNode(child, scanRoot, ocn_map)));
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function scanAndCollect(
|
|
45
|
-
root: string,
|
|
46
|
-
options: { depth: number; ignore: string[] },
|
|
47
|
-
): Promise<Result<RepoNode[], ScanAndCollectError>> {
|
|
48
|
-
const scan_result = await scanDirectory(root, options);
|
|
49
|
-
if (!scan_result.ok) return scan_result;
|
|
50
|
-
|
|
51
|
-
const ocn_result = await readOcnStates();
|
|
52
|
-
const ocn_map = ocn_result.ok ? ocn_result.value : new Map<string, OcnStatus>();
|
|
53
|
-
|
|
54
|
-
const nodes = scan_result.value;
|
|
55
|
-
await Promise.all(nodes.map((node) => populateNode(node, root, ocn_map)));
|
|
56
|
-
|
|
57
|
-
return ok(nodes);
|
|
58
|
-
}
|
package/packages/core/src/ocn.ts
DELETED
|
@@ -1,74 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,118 +0,0 @@
|
|
|
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
|
-
}
|