chore: initialize beadboard baseline

This commit is contained in:
zenchantlive 2026-02-11 17:42:51 -08:00
commit 292a72f861
30 changed files with 2983 additions and 0 deletions

15
src/app/layout.tsx Normal file
View file

@ -0,0 +1,15 @@
import type { Metadata } from 'next';
import type { ReactNode } from 'react';
export const metadata: Metadata = {
title: 'BeadBoard',
description: 'Windows-native Beads dashboard',
};
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>{children}</body>
</html>
);
}

7
src/app/page.tsx Normal file
View file

@ -0,0 +1,7 @@
export default function Page() {
return (
<main>
<h1>BeadBoard</h1>
</main>
);
}

85
src/lib/parser.ts Normal file
View file

@ -0,0 +1,85 @@
import type { BeadDependency, BeadIssue, ParseableBeadIssue } from './types';
export interface ParseIssuesOptions {
includeTombstones?: boolean;
}
function normalizeDependencies(value: unknown): BeadDependency[] {
if (!Array.isArray(value)) {
return [];
}
return value
.map((item) => {
if (!item || typeof item !== 'object') {
return null;
}
const dep = item as { type?: unknown; target?: unknown };
if (typeof dep.type !== 'string' || typeof dep.target !== 'string') {
return null;
}
return {
type: dep.type as BeadDependency['type'],
target: dep.target,
};
})
.filter((dep): dep is BeadDependency => dep !== null);
}
function normalizeIssue(raw: ParseableBeadIssue): BeadIssue {
return {
id: raw.id,
title: raw.title,
description: typeof raw.description === 'string' ? raw.description : null,
status: (raw.status ?? 'open') as BeadIssue['status'],
priority: typeof raw.priority === 'number' ? raw.priority : 2,
issue_type: (raw.issue_type ?? 'task') as BeadIssue['issue_type'],
assignee: typeof raw.assignee === 'string' ? raw.assignee : null,
owner: typeof raw.owner === 'string' ? raw.owner : null,
labels: Array.isArray(raw.labels) ? raw.labels.filter((x): x is string => typeof x === 'string') : [],
dependencies: normalizeDependencies(raw.dependencies),
created_at: typeof raw.created_at === 'string' ? raw.created_at : '',
updated_at: typeof raw.updated_at === 'string' ? raw.updated_at : '',
closed_at: typeof raw.closed_at === 'string' ? raw.closed_at : null,
close_reason: typeof raw.close_reason === 'string' ? raw.close_reason : null,
closed_by_session: typeof raw.closed_by_session === 'string' ? raw.closed_by_session : null,
created_by: typeof raw.created_by === 'string' ? raw.created_by : null,
due_at: typeof raw.due_at === 'string' ? raw.due_at : null,
estimated_minutes: typeof raw.estimated_minutes === 'number' ? raw.estimated_minutes : null,
external_ref: typeof raw.external_ref === 'string' ? raw.external_ref : null,
metadata: typeof raw.metadata === 'object' && raw.metadata !== null ? (raw.metadata as Record<string, unknown>) : {},
};
}
export function parseIssuesJsonl(text: string, options: ParseIssuesOptions = {}): BeadIssue[] {
const includeTombstones = options.includeTombstones ?? false;
const issues: BeadIssue[] = [];
for (const line of text.split(/\r?\n/)) {
const trimmed = line.trim();
if (!trimmed) {
continue;
}
try {
const parsed = JSON.parse(trimmed) as ParseableBeadIssue;
if (!parsed.id || !parsed.title) {
continue;
}
const normalized = normalizeIssue(parsed);
if (!includeTombstones && normalized.status === 'tombstone') {
continue;
}
issues.push(normalized);
} catch {
// Skip malformed lines to keep parser resilient against partial writes.
continue;
}
}
return issues;
}

36
src/lib/pathing.ts Normal file
View file

@ -0,0 +1,36 @@
import path from 'node:path';
function normalizeDriveLetter(input: string): string {
if (/^[a-z]:/.test(input)) {
return `${input[0].toUpperCase()}${input.slice(1)}`;
}
return input;
}
function trimTrailingSeparator(input: string): string {
if (/^[A-Za-z]:\\$/.test(input)) {
return input;
}
return input.replace(/[\\/]+$/, '');
}
export function canonicalizeWindowsPath(input: string): string {
const withBackslashes = input.replaceAll('/', '\\');
const normalized = path.win32.normalize(withBackslashes);
const withDriveCase = normalizeDriveLetter(normalized);
return trimTrailingSeparator(withDriveCase);
}
export function windowsPathKey(input: string): string {
return canonicalizeWindowsPath(input).toLowerCase();
}
export function toDisplayPath(input: string): string {
return canonicalizeWindowsPath(input).replaceAll('\\', '/');
}
export function sameWindowsPath(a: string, b: string): boolean {
return windowsPathKey(a) === windowsPathKey(b);
}

61
src/lib/types.ts Normal file
View file

@ -0,0 +1,61 @@
export const BEAD_STATUSES = [
'open',
'in_progress',
'blocked',
'deferred',
'closed',
'tombstone',
'pinned',
'hooked',
] as const;
export type BeadStatus = (typeof BEAD_STATUSES)[number];
export const BEAD_DEPENDENCY_TYPES = [
'blocks',
'parent',
'relates_to',
'duplicates',
'supersedes',
'replies_to',
] as const;
export type BeadDependencyType = (typeof BEAD_DEPENDENCY_TYPES)[number];
export const CORE_ISSUE_TYPES = ['task', 'bug', 'feature', 'epic', 'chore'] as const;
export type CoreIssueType = (typeof CORE_ISSUE_TYPES)[number];
export type BeadIssueType = CoreIssueType | (string & {});
export interface BeadDependency {
type: BeadDependencyType;
target: string;
}
export interface BeadIssue {
id: string;
title: string;
description: string | null;
status: BeadStatus;
priority: number;
issue_type: BeadIssueType;
assignee: string | null;
owner: string | null;
labels: string[];
dependencies: BeadDependency[];
created_at: string;
updated_at: string;
closed_at: string | null;
close_reason: string | null;
closed_by_session: string | null;
created_by: string | null;
due_at: string | null;
estimated_minutes: number | null;
external_ref: string | null;
metadata: Record<string, unknown>;
}
export interface ParseableBeadIssue extends Partial<BeadIssue> {
id: string;
title: string;
}