chore: initialize beadboard baseline
This commit is contained in:
commit
292a72f861
30 changed files with 2983 additions and 0 deletions
15
src/app/layout.tsx
Normal file
15
src/app/layout.tsx
Normal 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
7
src/app/page.tsx
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
export default function Page() {
|
||||
return (
|
||||
<main>
|
||||
<h1>BeadBoard</h1>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
85
src/lib/parser.ts
Normal file
85
src/lib/parser.ts
Normal 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
36
src/lib/pathing.ts
Normal 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
61
src/lib/types.ts
Normal 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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue