mrvn-cli 0.2.9 → 0.3.0
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/dist/index.js +1061 -9
- package/dist/index.js.map +1 -1
- package/dist/marvin-serve.js +16 -3
- package/dist/marvin-serve.js.map +1 -1
- package/dist/marvin.js +1061 -9
- package/dist/marvin.js.map +1 -1
- package/package.json +1 -1
package/dist/marvin.js
CHANGED
|
@@ -6188,13 +6188,13 @@ var error16 = () => {
|
|
|
6188
6188
|
// no unit
|
|
6189
6189
|
};
|
|
6190
6190
|
const typeEntry = (t) => t ? TypeNames[t] : void 0;
|
|
6191
|
-
const
|
|
6191
|
+
const typeLabel2 = (t) => {
|
|
6192
6192
|
const e = typeEntry(t);
|
|
6193
6193
|
if (e)
|
|
6194
6194
|
return e.label;
|
|
6195
6195
|
return t ?? TypeNames.unknown.label;
|
|
6196
6196
|
};
|
|
6197
|
-
const withDefinite = (t) => `\u05D4${
|
|
6197
|
+
const withDefinite = (t) => `\u05D4${typeLabel2(t)}`;
|
|
6198
6198
|
const verbFor = (t) => {
|
|
6199
6199
|
const e = typeEntry(t);
|
|
6200
6200
|
const gender = e?.gender ?? "m";
|
|
@@ -6244,7 +6244,7 @@ var error16 = () => {
|
|
|
6244
6244
|
switch (issue2.code) {
|
|
6245
6245
|
case "invalid_type": {
|
|
6246
6246
|
const expectedKey = issue2.expected;
|
|
6247
|
-
const expected = TypeDictionary[expectedKey ?? ""] ??
|
|
6247
|
+
const expected = TypeDictionary[expectedKey ?? ""] ?? typeLabel2(expectedKey);
|
|
6248
6248
|
const receivedType = parsedType(issue2.input);
|
|
6249
6249
|
const received = TypeDictionary[receivedType] ?? TypeNames[receivedType]?.label ?? receivedType;
|
|
6250
6250
|
if (/^[A-Z]/.test(issue2.expected)) {
|
|
@@ -16730,6 +16730,11 @@ var DocumentStore = class {
|
|
|
16730
16730
|
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
16731
16731
|
const doc = parseDocument(raw, filePath);
|
|
16732
16732
|
if (doc.frontmatter.id) {
|
|
16733
|
+
if (this.index.has(doc.frontmatter.id)) {
|
|
16734
|
+
console.warn(
|
|
16735
|
+
`[marvin] Duplicate ID "${doc.frontmatter.id}" in ${file2} \u2014 conflicts with existing entry. Run ID repair to fix.`
|
|
16736
|
+
);
|
|
16737
|
+
}
|
|
16733
16738
|
this.index.set(doc.frontmatter.id, doc.frontmatter);
|
|
16734
16739
|
}
|
|
16735
16740
|
}
|
|
@@ -16850,14 +16855,22 @@ var DocumentStore = class {
|
|
|
16850
16855
|
if (!prefix) {
|
|
16851
16856
|
throw new Error(`Unknown document type: ${type}`);
|
|
16852
16857
|
}
|
|
16853
|
-
const
|
|
16858
|
+
const dirName = this.typeDirs[type];
|
|
16859
|
+
const dir = path5.join(this.docsDir, dirName);
|
|
16860
|
+
if (!fs5.existsSync(dir)) return `${prefix}-001`;
|
|
16861
|
+
const idPattern = new RegExp(`^${prefix}-(\\d+)$`);
|
|
16862
|
+
const files = fs5.readdirSync(dir).filter((f) => f.endsWith(".md"));
|
|
16854
16863
|
let maxNum = 0;
|
|
16855
|
-
for (const
|
|
16856
|
-
const
|
|
16864
|
+
for (const file2 of files) {
|
|
16865
|
+
const filePath = path5.join(dir, file2);
|
|
16866
|
+
const raw = fs5.readFileSync(filePath, "utf-8");
|
|
16867
|
+
const doc = parseDocument(raw, filePath);
|
|
16868
|
+
const match = doc.frontmatter.id?.match(idPattern);
|
|
16857
16869
|
if (match) {
|
|
16858
16870
|
maxNum = Math.max(maxNum, parseInt(match[1], 10));
|
|
16859
16871
|
}
|
|
16860
16872
|
}
|
|
16873
|
+
maxNum = Math.max(maxNum, files.length);
|
|
16861
16874
|
return `${prefix}-${String(maxNum + 1).padStart(3, "0")}`;
|
|
16862
16875
|
}
|
|
16863
16876
|
counts() {
|
|
@@ -20358,8 +20371,8 @@ function executeImportPlan(plan, store, marvinDir, options) {
|
|
|
20358
20371
|
}
|
|
20359
20372
|
function formatPlanSummary(plan) {
|
|
20360
20373
|
const lines = [];
|
|
20361
|
-
const
|
|
20362
|
-
lines.push(`Detected: ${
|
|
20374
|
+
const typeLabel2 = classificationLabel(plan.classification.type);
|
|
20375
|
+
lines.push(`Detected: ${typeLabel2}`);
|
|
20363
20376
|
lines.push(`Source: ${plan.classification.inputPath}`);
|
|
20364
20377
|
lines.push("");
|
|
20365
20378
|
const imports = plan.items.filter((i) => i.action === "import");
|
|
@@ -21392,12 +21405,1048 @@ async function garReportCommand(options) {
|
|
|
21392
21405
|
}
|
|
21393
21406
|
}
|
|
21394
21407
|
|
|
21408
|
+
// src/web/server.ts
|
|
21409
|
+
import * as http from "http";
|
|
21410
|
+
import { exec } from "child_process";
|
|
21411
|
+
|
|
21412
|
+
// src/web/data.ts
|
|
21413
|
+
function getOverviewData(store) {
|
|
21414
|
+
const types = [];
|
|
21415
|
+
const counts = store.counts();
|
|
21416
|
+
for (const type of store.registeredTypes) {
|
|
21417
|
+
const total = counts[type] ?? 0;
|
|
21418
|
+
const open = store.list({ type, status: "open" }).length;
|
|
21419
|
+
types.push({ type, total, open });
|
|
21420
|
+
}
|
|
21421
|
+
const allDocs = store.list();
|
|
21422
|
+
const sorted = allDocs.sort(
|
|
21423
|
+
(a, b) => (b.frontmatter.updated ?? b.frontmatter.created).localeCompare(
|
|
21424
|
+
a.frontmatter.updated ?? a.frontmatter.created
|
|
21425
|
+
)
|
|
21426
|
+
);
|
|
21427
|
+
return { types, recent: sorted.slice(0, 20) };
|
|
21428
|
+
}
|
|
21429
|
+
function getDocumentListData(store, type, filterStatus, filterOwner) {
|
|
21430
|
+
if (!store.registeredTypes.includes(type)) return void 0;
|
|
21431
|
+
const allOfType = store.list({ type });
|
|
21432
|
+
const statuses = [...new Set(allOfType.map((d) => d.frontmatter.status))].sort();
|
|
21433
|
+
const owners = [
|
|
21434
|
+
...new Set(allOfType.map((d) => d.frontmatter.owner).filter(Boolean))
|
|
21435
|
+
].sort();
|
|
21436
|
+
let docs = allOfType;
|
|
21437
|
+
if (filterStatus) {
|
|
21438
|
+
docs = docs.filter((d) => d.frontmatter.status === filterStatus);
|
|
21439
|
+
}
|
|
21440
|
+
if (filterOwner) {
|
|
21441
|
+
docs = docs.filter((d) => d.frontmatter.owner === filterOwner);
|
|
21442
|
+
}
|
|
21443
|
+
docs.sort((a, b) => a.frontmatter.id.localeCompare(b.frontmatter.id));
|
|
21444
|
+
return { type, docs, statuses, owners, filterStatus, filterOwner };
|
|
21445
|
+
}
|
|
21446
|
+
function getDocumentDetail(store, type, id) {
|
|
21447
|
+
if (!store.registeredTypes.includes(type)) return void 0;
|
|
21448
|
+
return store.get(id);
|
|
21449
|
+
}
|
|
21450
|
+
function getGarData(store, projectName) {
|
|
21451
|
+
const metrics = collectGarMetrics(store);
|
|
21452
|
+
return evaluateGar(projectName, metrics);
|
|
21453
|
+
}
|
|
21454
|
+
function getBoardData(store, type) {
|
|
21455
|
+
const docs = type ? store.list({ type }) : store.list();
|
|
21456
|
+
const types = store.registeredTypes;
|
|
21457
|
+
const byStatus = /* @__PURE__ */ new Map();
|
|
21458
|
+
for (const doc of docs) {
|
|
21459
|
+
const status = doc.frontmatter.status;
|
|
21460
|
+
if (!byStatus.has(status)) byStatus.set(status, []);
|
|
21461
|
+
byStatus.get(status).push(doc);
|
|
21462
|
+
}
|
|
21463
|
+
const statusOrder = ["open", "draft", "in-progress", "blocked"];
|
|
21464
|
+
const allStatuses = [...byStatus.keys()];
|
|
21465
|
+
const ordered = [];
|
|
21466
|
+
for (const s of statusOrder) {
|
|
21467
|
+
if (allStatuses.includes(s)) ordered.push(s);
|
|
21468
|
+
}
|
|
21469
|
+
for (const s of allStatuses.sort()) {
|
|
21470
|
+
if (!ordered.includes(s) && s !== "done" && s !== "closed" && s !== "resolved") {
|
|
21471
|
+
ordered.push(s);
|
|
21472
|
+
}
|
|
21473
|
+
}
|
|
21474
|
+
for (const s of ["done", "closed", "resolved"]) {
|
|
21475
|
+
if (allStatuses.includes(s)) ordered.push(s);
|
|
21476
|
+
}
|
|
21477
|
+
const columns = ordered.map((status) => ({
|
|
21478
|
+
status,
|
|
21479
|
+
docs: byStatus.get(status) ?? []
|
|
21480
|
+
}));
|
|
21481
|
+
return { columns, type, types };
|
|
21482
|
+
}
|
|
21483
|
+
|
|
21484
|
+
// src/web/templates/layout.ts
|
|
21485
|
+
function escapeHtml(str) {
|
|
21486
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
21487
|
+
}
|
|
21488
|
+
function statusBadge(status) {
|
|
21489
|
+
const cls = {
|
|
21490
|
+
open: "badge-open",
|
|
21491
|
+
done: "badge-done",
|
|
21492
|
+
closed: "badge-done",
|
|
21493
|
+
resolved: "badge-resolved",
|
|
21494
|
+
"in-progress": "badge-in-progress",
|
|
21495
|
+
"in progress": "badge-in-progress",
|
|
21496
|
+
draft: "badge-draft",
|
|
21497
|
+
blocked: "badge-blocked"
|
|
21498
|
+
}[status.toLowerCase()] ?? "badge-default";
|
|
21499
|
+
return `<span class="badge ${cls}">${escapeHtml(status)}</span>`;
|
|
21500
|
+
}
|
|
21501
|
+
function formatDate(iso) {
|
|
21502
|
+
if (!iso) return "";
|
|
21503
|
+
return iso.slice(0, 10);
|
|
21504
|
+
}
|
|
21505
|
+
function typeLabel(type) {
|
|
21506
|
+
return type.replace(/-/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
21507
|
+
}
|
|
21508
|
+
function renderMarkdown(md) {
|
|
21509
|
+
const lines = md.split("\n");
|
|
21510
|
+
const out = [];
|
|
21511
|
+
let inList = false;
|
|
21512
|
+
let listTag = "ul";
|
|
21513
|
+
for (const raw of lines) {
|
|
21514
|
+
const line = raw;
|
|
21515
|
+
if (inList && !/^\s*[-*]\s/.test(line) && !/^\s*\d+\.\s/.test(line) && line.trim() !== "") {
|
|
21516
|
+
out.push(`</${listTag}>`);
|
|
21517
|
+
inList = false;
|
|
21518
|
+
}
|
|
21519
|
+
const headingMatch = line.match(/^(#{1,3})\s+(.+)$/);
|
|
21520
|
+
if (headingMatch) {
|
|
21521
|
+
const level = headingMatch[1].length;
|
|
21522
|
+
out.push(`<h${level}>${inline(headingMatch[2])}</h${level}>`);
|
|
21523
|
+
continue;
|
|
21524
|
+
}
|
|
21525
|
+
const ulMatch = line.match(/^\s*[-*]\s+(.+)$/);
|
|
21526
|
+
if (ulMatch) {
|
|
21527
|
+
if (!inList || listTag !== "ul") {
|
|
21528
|
+
if (inList) out.push(`</${listTag}>`);
|
|
21529
|
+
out.push("<ul>");
|
|
21530
|
+
inList = true;
|
|
21531
|
+
listTag = "ul";
|
|
21532
|
+
}
|
|
21533
|
+
out.push(`<li>${inline(ulMatch[1])}</li>`);
|
|
21534
|
+
continue;
|
|
21535
|
+
}
|
|
21536
|
+
const olMatch = line.match(/^\s*\d+\.\s+(.+)$/);
|
|
21537
|
+
if (olMatch) {
|
|
21538
|
+
if (!inList || listTag !== "ol") {
|
|
21539
|
+
if (inList) out.push(`</${listTag}>`);
|
|
21540
|
+
out.push("<ol>");
|
|
21541
|
+
inList = true;
|
|
21542
|
+
listTag = "ol";
|
|
21543
|
+
}
|
|
21544
|
+
out.push(`<li>${inline(olMatch[1])}</li>`);
|
|
21545
|
+
continue;
|
|
21546
|
+
}
|
|
21547
|
+
if (line.trim() === "") {
|
|
21548
|
+
if (inList) {
|
|
21549
|
+
out.push(`</${listTag}>`);
|
|
21550
|
+
inList = false;
|
|
21551
|
+
}
|
|
21552
|
+
continue;
|
|
21553
|
+
}
|
|
21554
|
+
out.push(`<p>${inline(line)}</p>`);
|
|
21555
|
+
}
|
|
21556
|
+
if (inList) out.push(`</${listTag}>`);
|
|
21557
|
+
return out.join("\n");
|
|
21558
|
+
}
|
|
21559
|
+
function inline(text) {
|
|
21560
|
+
let s = escapeHtml(text);
|
|
21561
|
+
s = s.replace(/`([^`]+)`/g, "<code>$1</code>");
|
|
21562
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>");
|
|
21563
|
+
s = s.replace(/__([^_]+)__/g, "<strong>$1</strong>");
|
|
21564
|
+
s = s.replace(/\*([^*]+)\*/g, "<em>$1</em>");
|
|
21565
|
+
s = s.replace(/_([^_]+)_/g, "<em>$1</em>");
|
|
21566
|
+
return s;
|
|
21567
|
+
}
|
|
21568
|
+
function layout(opts, body) {
|
|
21569
|
+
const navItems = [
|
|
21570
|
+
{ href: "/", label: "Overview" },
|
|
21571
|
+
{ href: "/board", label: "Board" },
|
|
21572
|
+
{ href: "/gar", label: "GAR Report" }
|
|
21573
|
+
];
|
|
21574
|
+
const typeNavItems = opts.navTypes.map((type) => ({
|
|
21575
|
+
href: `/docs/${type}`,
|
|
21576
|
+
label: typeLabel(type) + "s"
|
|
21577
|
+
}));
|
|
21578
|
+
const isActive = (href) => opts.activePath === href || href !== "/" && opts.activePath.startsWith(href) ? " active" : "";
|
|
21579
|
+
return `<!DOCTYPE html>
|
|
21580
|
+
<html lang="en">
|
|
21581
|
+
<head>
|
|
21582
|
+
<meta charset="UTF-8">
|
|
21583
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
21584
|
+
<title>${escapeHtml(opts.title)} \u2014 Marvin</title>
|
|
21585
|
+
<link rel="stylesheet" href="/styles.css">
|
|
21586
|
+
</head>
|
|
21587
|
+
<body>
|
|
21588
|
+
<div class="shell">
|
|
21589
|
+
<aside class="sidebar">
|
|
21590
|
+
<div class="sidebar-brand">
|
|
21591
|
+
<h1>Marvin</h1>
|
|
21592
|
+
<div class="project-name">${escapeHtml(opts.projectName)}</div>
|
|
21593
|
+
</div>
|
|
21594
|
+
<nav>
|
|
21595
|
+
${navItems.map((n) => `<a href="${n.href}" class="${isActive(n.href)}">${n.label}</a>`).join("\n ")}
|
|
21596
|
+
${typeNavItems.map((n) => `<a href="${n.href}" class="${isActive(n.href)}">${n.label}</a>`).join("\n ")}
|
|
21597
|
+
</nav>
|
|
21598
|
+
</aside>
|
|
21599
|
+
<main class="main">
|
|
21600
|
+
${body}
|
|
21601
|
+
</main>
|
|
21602
|
+
</div>
|
|
21603
|
+
</body>
|
|
21604
|
+
</html>`;
|
|
21605
|
+
}
|
|
21606
|
+
|
|
21607
|
+
// src/web/templates/styles.ts
|
|
21608
|
+
function renderStyles() {
|
|
21609
|
+
return `
|
|
21610
|
+
:root {
|
|
21611
|
+
--bg: #0f1117;
|
|
21612
|
+
--bg-card: #1a1d27;
|
|
21613
|
+
--bg-hover: #222632;
|
|
21614
|
+
--border: #2a2e3a;
|
|
21615
|
+
--text: #e1e4ea;
|
|
21616
|
+
--text-dim: #8b8fa4;
|
|
21617
|
+
--accent: #6c8cff;
|
|
21618
|
+
--accent-dim: #4a6ad4;
|
|
21619
|
+
--green: #34d399;
|
|
21620
|
+
--amber: #fbbf24;
|
|
21621
|
+
--red: #f87171;
|
|
21622
|
+
--radius: 8px;
|
|
21623
|
+
--font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
21624
|
+
--mono: "SF Mono", "Fira Code", monospace;
|
|
21625
|
+
}
|
|
21626
|
+
|
|
21627
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
21628
|
+
|
|
21629
|
+
body {
|
|
21630
|
+
font-family: var(--font);
|
|
21631
|
+
background: var(--bg);
|
|
21632
|
+
color: var(--text);
|
|
21633
|
+
line-height: 1.6;
|
|
21634
|
+
min-height: 100vh;
|
|
21635
|
+
}
|
|
21636
|
+
|
|
21637
|
+
a { color: var(--accent); text-decoration: none; }
|
|
21638
|
+
a:hover { text-decoration: underline; }
|
|
21639
|
+
|
|
21640
|
+
/* Layout */
|
|
21641
|
+
.shell {
|
|
21642
|
+
display: flex;
|
|
21643
|
+
min-height: 100vh;
|
|
21644
|
+
}
|
|
21645
|
+
|
|
21646
|
+
.sidebar {
|
|
21647
|
+
width: 220px;
|
|
21648
|
+
background: var(--bg-card);
|
|
21649
|
+
border-right: 1px solid var(--border);
|
|
21650
|
+
padding: 1.5rem 0;
|
|
21651
|
+
position: fixed;
|
|
21652
|
+
top: 0;
|
|
21653
|
+
left: 0;
|
|
21654
|
+
bottom: 0;
|
|
21655
|
+
overflow-y: auto;
|
|
21656
|
+
}
|
|
21657
|
+
|
|
21658
|
+
.sidebar-brand {
|
|
21659
|
+
padding: 0 1.25rem 1.25rem;
|
|
21660
|
+
border-bottom: 1px solid var(--border);
|
|
21661
|
+
margin-bottom: 1rem;
|
|
21662
|
+
}
|
|
21663
|
+
|
|
21664
|
+
.sidebar-brand h1 {
|
|
21665
|
+
font-size: 1.1rem;
|
|
21666
|
+
font-weight: 700;
|
|
21667
|
+
color: var(--accent);
|
|
21668
|
+
letter-spacing: -0.02em;
|
|
21669
|
+
}
|
|
21670
|
+
|
|
21671
|
+
.sidebar-brand .project-name {
|
|
21672
|
+
font-size: 0.75rem;
|
|
21673
|
+
color: var(--text-dim);
|
|
21674
|
+
margin-top: 0.25rem;
|
|
21675
|
+
}
|
|
21676
|
+
|
|
21677
|
+
.sidebar nav a {
|
|
21678
|
+
display: block;
|
|
21679
|
+
padding: 0.5rem 1.25rem;
|
|
21680
|
+
color: var(--text-dim);
|
|
21681
|
+
font-size: 0.875rem;
|
|
21682
|
+
transition: background 0.15s, color 0.15s;
|
|
21683
|
+
}
|
|
21684
|
+
|
|
21685
|
+
.sidebar nav a:hover {
|
|
21686
|
+
background: var(--bg-hover);
|
|
21687
|
+
color: var(--text);
|
|
21688
|
+
text-decoration: none;
|
|
21689
|
+
}
|
|
21690
|
+
|
|
21691
|
+
.sidebar nav a.active {
|
|
21692
|
+
color: var(--accent);
|
|
21693
|
+
background: rgba(108, 140, 255, 0.08);
|
|
21694
|
+
border-right: 2px solid var(--accent);
|
|
21695
|
+
}
|
|
21696
|
+
|
|
21697
|
+
.main {
|
|
21698
|
+
margin-left: 220px;
|
|
21699
|
+
flex: 1;
|
|
21700
|
+
padding: 2rem 2.5rem;
|
|
21701
|
+
max-width: 1200px;
|
|
21702
|
+
}
|
|
21703
|
+
|
|
21704
|
+
/* Page header */
|
|
21705
|
+
.page-header {
|
|
21706
|
+
margin-bottom: 2rem;
|
|
21707
|
+
}
|
|
21708
|
+
|
|
21709
|
+
.page-header h2 {
|
|
21710
|
+
font-size: 1.5rem;
|
|
21711
|
+
font-weight: 600;
|
|
21712
|
+
}
|
|
21713
|
+
|
|
21714
|
+
.page-header .subtitle {
|
|
21715
|
+
color: var(--text-dim);
|
|
21716
|
+
font-size: 0.875rem;
|
|
21717
|
+
margin-top: 0.25rem;
|
|
21718
|
+
}
|
|
21719
|
+
|
|
21720
|
+
/* Breadcrumb */
|
|
21721
|
+
.breadcrumb {
|
|
21722
|
+
font-size: 0.8rem;
|
|
21723
|
+
color: var(--text-dim);
|
|
21724
|
+
margin-bottom: 1rem;
|
|
21725
|
+
}
|
|
21726
|
+
|
|
21727
|
+
.breadcrumb a { color: var(--text-dim); }
|
|
21728
|
+
.breadcrumb a:hover { color: var(--accent); }
|
|
21729
|
+
.breadcrumb .sep { margin: 0 0.4rem; }
|
|
21730
|
+
|
|
21731
|
+
/* Cards grid */
|
|
21732
|
+
.cards {
|
|
21733
|
+
display: grid;
|
|
21734
|
+
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
|
|
21735
|
+
gap: 1rem;
|
|
21736
|
+
margin-bottom: 2rem;
|
|
21737
|
+
}
|
|
21738
|
+
|
|
21739
|
+
.card {
|
|
21740
|
+
background: var(--bg-card);
|
|
21741
|
+
border: 1px solid var(--border);
|
|
21742
|
+
border-radius: var(--radius);
|
|
21743
|
+
padding: 1.25rem;
|
|
21744
|
+
transition: border-color 0.15s;
|
|
21745
|
+
}
|
|
21746
|
+
|
|
21747
|
+
.card:hover {
|
|
21748
|
+
border-color: var(--accent-dim);
|
|
21749
|
+
}
|
|
21750
|
+
|
|
21751
|
+
.card a { color: inherit; text-decoration: none; display: block; }
|
|
21752
|
+
|
|
21753
|
+
.card .card-label {
|
|
21754
|
+
font-size: 0.75rem;
|
|
21755
|
+
text-transform: uppercase;
|
|
21756
|
+
letter-spacing: 0.05em;
|
|
21757
|
+
color: var(--text-dim);
|
|
21758
|
+
margin-bottom: 0.5rem;
|
|
21759
|
+
}
|
|
21760
|
+
|
|
21761
|
+
.card .card-value {
|
|
21762
|
+
font-size: 1.75rem;
|
|
21763
|
+
font-weight: 700;
|
|
21764
|
+
}
|
|
21765
|
+
|
|
21766
|
+
.card .card-sub {
|
|
21767
|
+
font-size: 0.8rem;
|
|
21768
|
+
color: var(--text-dim);
|
|
21769
|
+
margin-top: 0.25rem;
|
|
21770
|
+
}
|
|
21771
|
+
|
|
21772
|
+
/* Status badge */
|
|
21773
|
+
.badge {
|
|
21774
|
+
display: inline-block;
|
|
21775
|
+
padding: 0.15rem 0.6rem;
|
|
21776
|
+
border-radius: 999px;
|
|
21777
|
+
font-size: 0.7rem;
|
|
21778
|
+
font-weight: 600;
|
|
21779
|
+
text-transform: uppercase;
|
|
21780
|
+
letter-spacing: 0.03em;
|
|
21781
|
+
}
|
|
21782
|
+
|
|
21783
|
+
.badge-open { background: rgba(108, 140, 255, 0.15); color: var(--accent); }
|
|
21784
|
+
.badge-done { background: rgba(52, 211, 153, 0.15); color: var(--green); }
|
|
21785
|
+
.badge-in-progress { background: rgba(251, 191, 36, 0.15); color: var(--amber); }
|
|
21786
|
+
.badge-draft { background: rgba(139, 143, 164, 0.15); color: var(--text-dim); }
|
|
21787
|
+
.badge-closed, .badge-resolved { background: rgba(52, 211, 153, 0.15); color: var(--green); }
|
|
21788
|
+
.badge-blocked { background: rgba(248, 113, 113, 0.15); color: var(--red); }
|
|
21789
|
+
.badge-default { background: rgba(139, 143, 164, 0.1); color: var(--text-dim); }
|
|
21790
|
+
|
|
21791
|
+
/* Table */
|
|
21792
|
+
.table-wrap {
|
|
21793
|
+
overflow-x: auto;
|
|
21794
|
+
}
|
|
21795
|
+
|
|
21796
|
+
table {
|
|
21797
|
+
width: 100%;
|
|
21798
|
+
border-collapse: collapse;
|
|
21799
|
+
}
|
|
21800
|
+
|
|
21801
|
+
th {
|
|
21802
|
+
text-align: left;
|
|
21803
|
+
padding: 0.6rem 0.75rem;
|
|
21804
|
+
font-size: 0.7rem;
|
|
21805
|
+
text-transform: uppercase;
|
|
21806
|
+
letter-spacing: 0.05em;
|
|
21807
|
+
color: var(--text-dim);
|
|
21808
|
+
border-bottom: 1px solid var(--border);
|
|
21809
|
+
}
|
|
21810
|
+
|
|
21811
|
+
td {
|
|
21812
|
+
padding: 0.6rem 0.75rem;
|
|
21813
|
+
font-size: 0.875rem;
|
|
21814
|
+
border-bottom: 1px solid var(--border);
|
|
21815
|
+
}
|
|
21816
|
+
|
|
21817
|
+
tr:hover td {
|
|
21818
|
+
background: var(--bg-hover);
|
|
21819
|
+
}
|
|
21820
|
+
|
|
21821
|
+
/* GAR */
|
|
21822
|
+
.gar-overall {
|
|
21823
|
+
text-align: center;
|
|
21824
|
+
padding: 2rem;
|
|
21825
|
+
margin-bottom: 2rem;
|
|
21826
|
+
border-radius: var(--radius);
|
|
21827
|
+
border: 1px solid var(--border);
|
|
21828
|
+
background: var(--bg-card);
|
|
21829
|
+
}
|
|
21830
|
+
|
|
21831
|
+
.gar-overall .dot {
|
|
21832
|
+
width: 60px;
|
|
21833
|
+
height: 60px;
|
|
21834
|
+
border-radius: 50%;
|
|
21835
|
+
display: inline-block;
|
|
21836
|
+
margin-bottom: 0.75rem;
|
|
21837
|
+
}
|
|
21838
|
+
|
|
21839
|
+
.gar-overall .label {
|
|
21840
|
+
font-size: 1.1rem;
|
|
21841
|
+
font-weight: 600;
|
|
21842
|
+
text-transform: uppercase;
|
|
21843
|
+
}
|
|
21844
|
+
|
|
21845
|
+
.gar-areas {
|
|
21846
|
+
display: grid;
|
|
21847
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
21848
|
+
gap: 1rem;
|
|
21849
|
+
}
|
|
21850
|
+
|
|
21851
|
+
.gar-area {
|
|
21852
|
+
background: var(--bg-card);
|
|
21853
|
+
border: 1px solid var(--border);
|
|
21854
|
+
border-radius: var(--radius);
|
|
21855
|
+
padding: 1.25rem;
|
|
21856
|
+
}
|
|
21857
|
+
|
|
21858
|
+
.gar-area .area-header {
|
|
21859
|
+
display: flex;
|
|
21860
|
+
align-items: center;
|
|
21861
|
+
gap: 0.6rem;
|
|
21862
|
+
margin-bottom: 0.75rem;
|
|
21863
|
+
}
|
|
21864
|
+
|
|
21865
|
+
.gar-area .area-dot {
|
|
21866
|
+
width: 14px;
|
|
21867
|
+
height: 14px;
|
|
21868
|
+
border-radius: 50%;
|
|
21869
|
+
flex-shrink: 0;
|
|
21870
|
+
}
|
|
21871
|
+
|
|
21872
|
+
.gar-area .area-name {
|
|
21873
|
+
font-weight: 600;
|
|
21874
|
+
font-size: 1rem;
|
|
21875
|
+
}
|
|
21876
|
+
|
|
21877
|
+
.gar-area .area-summary {
|
|
21878
|
+
font-size: 0.85rem;
|
|
21879
|
+
color: var(--text-dim);
|
|
21880
|
+
margin-bottom: 0.75rem;
|
|
21881
|
+
}
|
|
21882
|
+
|
|
21883
|
+
.gar-area ul {
|
|
21884
|
+
list-style: none;
|
|
21885
|
+
font-size: 0.8rem;
|
|
21886
|
+
}
|
|
21887
|
+
|
|
21888
|
+
.gar-area li {
|
|
21889
|
+
padding: 0.2rem 0;
|
|
21890
|
+
color: var(--text-dim);
|
|
21891
|
+
}
|
|
21892
|
+
|
|
21893
|
+
.gar-area li .ref-id {
|
|
21894
|
+
color: var(--accent);
|
|
21895
|
+
font-family: var(--mono);
|
|
21896
|
+
margin-right: 0.4rem;
|
|
21897
|
+
}
|
|
21898
|
+
|
|
21899
|
+
.dot-green { background: var(--green); }
|
|
21900
|
+
.dot-amber { background: var(--amber); }
|
|
21901
|
+
.dot-red { background: var(--red); }
|
|
21902
|
+
|
|
21903
|
+
/* Board / Kanban */
|
|
21904
|
+
.board {
|
|
21905
|
+
display: flex;
|
|
21906
|
+
gap: 1rem;
|
|
21907
|
+
overflow-x: auto;
|
|
21908
|
+
padding-bottom: 1rem;
|
|
21909
|
+
}
|
|
21910
|
+
|
|
21911
|
+
.board-column {
|
|
21912
|
+
min-width: 240px;
|
|
21913
|
+
max-width: 300px;
|
|
21914
|
+
flex: 1;
|
|
21915
|
+
}
|
|
21916
|
+
|
|
21917
|
+
.board-column-header {
|
|
21918
|
+
font-size: 0.75rem;
|
|
21919
|
+
text-transform: uppercase;
|
|
21920
|
+
letter-spacing: 0.05em;
|
|
21921
|
+
color: var(--text-dim);
|
|
21922
|
+
padding: 0.5rem 0.75rem;
|
|
21923
|
+
border-bottom: 2px solid var(--border);
|
|
21924
|
+
margin-bottom: 0.5rem;
|
|
21925
|
+
display: flex;
|
|
21926
|
+
justify-content: space-between;
|
|
21927
|
+
}
|
|
21928
|
+
|
|
21929
|
+
.board-column-header .count {
|
|
21930
|
+
background: var(--bg-hover);
|
|
21931
|
+
padding: 0 0.5rem;
|
|
21932
|
+
border-radius: 999px;
|
|
21933
|
+
font-size: 0.7rem;
|
|
21934
|
+
}
|
|
21935
|
+
|
|
21936
|
+
.board-card {
|
|
21937
|
+
background: var(--bg-card);
|
|
21938
|
+
border: 1px solid var(--border);
|
|
21939
|
+
border-radius: var(--radius);
|
|
21940
|
+
padding: 0.75rem;
|
|
21941
|
+
margin-bottom: 0.5rem;
|
|
21942
|
+
transition: border-color 0.15s;
|
|
21943
|
+
}
|
|
21944
|
+
|
|
21945
|
+
.board-card:hover {
|
|
21946
|
+
border-color: var(--accent-dim);
|
|
21947
|
+
}
|
|
21948
|
+
|
|
21949
|
+
.board-card .bc-id {
|
|
21950
|
+
font-family: var(--mono);
|
|
21951
|
+
font-size: 0.7rem;
|
|
21952
|
+
color: var(--accent);
|
|
21953
|
+
}
|
|
21954
|
+
|
|
21955
|
+
.board-card .bc-title {
|
|
21956
|
+
font-size: 0.85rem;
|
|
21957
|
+
margin: 0.25rem 0;
|
|
21958
|
+
}
|
|
21959
|
+
|
|
21960
|
+
.board-card .bc-owner {
|
|
21961
|
+
font-size: 0.7rem;
|
|
21962
|
+
color: var(--text-dim);
|
|
21963
|
+
}
|
|
21964
|
+
|
|
21965
|
+
/* Detail page */
|
|
21966
|
+
.detail-meta {
|
|
21967
|
+
background: var(--bg-card);
|
|
21968
|
+
border: 1px solid var(--border);
|
|
21969
|
+
border-radius: var(--radius);
|
|
21970
|
+
padding: 1.25rem;
|
|
21971
|
+
margin-bottom: 1.5rem;
|
|
21972
|
+
}
|
|
21973
|
+
|
|
21974
|
+
.detail-meta dl {
|
|
21975
|
+
display: grid;
|
|
21976
|
+
grid-template-columns: 120px 1fr;
|
|
21977
|
+
gap: 0.4rem 1rem;
|
|
21978
|
+
}
|
|
21979
|
+
|
|
21980
|
+
.detail-meta dt {
|
|
21981
|
+
font-size: 0.75rem;
|
|
21982
|
+
text-transform: uppercase;
|
|
21983
|
+
letter-spacing: 0.05em;
|
|
21984
|
+
color: var(--text-dim);
|
|
21985
|
+
}
|
|
21986
|
+
|
|
21987
|
+
.detail-meta dd {
|
|
21988
|
+
font-size: 0.875rem;
|
|
21989
|
+
}
|
|
21990
|
+
|
|
21991
|
+
.detail-content {
|
|
21992
|
+
background: var(--bg-card);
|
|
21993
|
+
border: 1px solid var(--border);
|
|
21994
|
+
border-radius: var(--radius);
|
|
21995
|
+
padding: 1.5rem;
|
|
21996
|
+
line-height: 1.7;
|
|
21997
|
+
}
|
|
21998
|
+
|
|
21999
|
+
.detail-content h1, .detail-content h2, .detail-content h3 {
|
|
22000
|
+
margin: 1.25rem 0 0.5rem;
|
|
22001
|
+
font-weight: 600;
|
|
22002
|
+
}
|
|
22003
|
+
|
|
22004
|
+
.detail-content h1 { font-size: 1.3rem; }
|
|
22005
|
+
.detail-content h2 { font-size: 1.15rem; }
|
|
22006
|
+
.detail-content h3 { font-size: 1rem; }
|
|
22007
|
+
.detail-content p { margin-bottom: 0.75rem; }
|
|
22008
|
+
.detail-content ul, .detail-content ol { margin: 0.5rem 0 0.75rem 1.5rem; }
|
|
22009
|
+
.detail-content li { margin-bottom: 0.25rem; }
|
|
22010
|
+
.detail-content code {
|
|
22011
|
+
background: var(--bg-hover);
|
|
22012
|
+
padding: 0.1rem 0.35rem;
|
|
22013
|
+
border-radius: 3px;
|
|
22014
|
+
font-family: var(--mono);
|
|
22015
|
+
font-size: 0.85em;
|
|
22016
|
+
}
|
|
22017
|
+
|
|
22018
|
+
/* Filters */
|
|
22019
|
+
.filters {
|
|
22020
|
+
display: flex;
|
|
22021
|
+
gap: 0.75rem;
|
|
22022
|
+
margin-bottom: 1.5rem;
|
|
22023
|
+
flex-wrap: wrap;
|
|
22024
|
+
}
|
|
22025
|
+
|
|
22026
|
+
.filters select {
|
|
22027
|
+
background: var(--bg-card);
|
|
22028
|
+
border: 1px solid var(--border);
|
|
22029
|
+
color: var(--text);
|
|
22030
|
+
padding: 0.4rem 0.75rem;
|
|
22031
|
+
border-radius: var(--radius);
|
|
22032
|
+
font-size: 0.8rem;
|
|
22033
|
+
cursor: pointer;
|
|
22034
|
+
}
|
|
22035
|
+
|
|
22036
|
+
.filters select:focus {
|
|
22037
|
+
outline: none;
|
|
22038
|
+
border-color: var(--accent);
|
|
22039
|
+
}
|
|
22040
|
+
|
|
22041
|
+
/* Empty state */
|
|
22042
|
+
.empty {
|
|
22043
|
+
text-align: center;
|
|
22044
|
+
padding: 3rem;
|
|
22045
|
+
color: var(--text-dim);
|
|
22046
|
+
}
|
|
22047
|
+
|
|
22048
|
+
.empty p { font-size: 0.9rem; }
|
|
22049
|
+
|
|
22050
|
+
/* Section heading */
|
|
22051
|
+
.section-title {
|
|
22052
|
+
font-size: 0.9rem;
|
|
22053
|
+
font-weight: 600;
|
|
22054
|
+
margin: 1.5rem 0 0.75rem;
|
|
22055
|
+
}
|
|
22056
|
+
|
|
22057
|
+
/* Priority */
|
|
22058
|
+
.priority-high { color: var(--red); }
|
|
22059
|
+
.priority-medium { color: var(--amber); }
|
|
22060
|
+
.priority-low { color: var(--green); }
|
|
22061
|
+
`;
|
|
22062
|
+
}
|
|
22063
|
+
|
|
22064
|
+
// src/web/templates/pages/overview.ts
|
|
22065
|
+
function overviewPage(data) {
|
|
22066
|
+
const cards = data.types.map(
|
|
22067
|
+
(t) => `
|
|
22068
|
+
<div class="card">
|
|
22069
|
+
<a href="/docs/${t.type}">
|
|
22070
|
+
<div class="card-label">${escapeHtml(typeLabel(t.type))}s</div>
|
|
22071
|
+
<div class="card-value">${t.total}</div>
|
|
22072
|
+
${t.open > 0 ? `<div class="card-sub">${t.open} open</div>` : `<div class="card-sub">none open</div>`}
|
|
22073
|
+
</a>
|
|
22074
|
+
</div>`
|
|
22075
|
+
).join("\n");
|
|
22076
|
+
const rows = data.recent.map(
|
|
22077
|
+
(doc) => `
|
|
22078
|
+
<tr>
|
|
22079
|
+
<td><a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
|
|
22080
|
+
<td>${escapeHtml(doc.frontmatter.title)}</td>
|
|
22081
|
+
<td>${escapeHtml(typeLabel(doc.frontmatter.type))}</td>
|
|
22082
|
+
<td>${statusBadge(doc.frontmatter.status)}</td>
|
|
22083
|
+
<td>${formatDate(doc.frontmatter.updated ?? doc.frontmatter.created)}</td>
|
|
22084
|
+
</tr>`
|
|
22085
|
+
).join("\n");
|
|
22086
|
+
return `
|
|
22087
|
+
<div class="page-header">
|
|
22088
|
+
<h2>Project Overview</h2>
|
|
22089
|
+
</div>
|
|
22090
|
+
|
|
22091
|
+
<div class="cards">
|
|
22092
|
+
${cards}
|
|
22093
|
+
</div>
|
|
22094
|
+
|
|
22095
|
+
<div class="section-title">Recent Activity</div>
|
|
22096
|
+
${data.recent.length > 0 ? `
|
|
22097
|
+
<div class="table-wrap">
|
|
22098
|
+
<table>
|
|
22099
|
+
<thead>
|
|
22100
|
+
<tr>
|
|
22101
|
+
<th>ID</th>
|
|
22102
|
+
<th>Title</th>
|
|
22103
|
+
<th>Type</th>
|
|
22104
|
+
<th>Status</th>
|
|
22105
|
+
<th>Updated</th>
|
|
22106
|
+
</tr>
|
|
22107
|
+
</thead>
|
|
22108
|
+
<tbody>
|
|
22109
|
+
${rows}
|
|
22110
|
+
</tbody>
|
|
22111
|
+
</table>
|
|
22112
|
+
</div>` : `<div class="empty"><p>No documents yet.</p></div>`}
|
|
22113
|
+
`;
|
|
22114
|
+
}
|
|
22115
|
+
|
|
22116
|
+
// src/web/templates/pages/documents.ts
|
|
22117
|
+
function documentsPage(data) {
|
|
22118
|
+
const label = typeLabel(data.type);
|
|
22119
|
+
const statusOptions = data.statuses.map(
|
|
22120
|
+
(s) => `<option value="${escapeHtml(s)}"${data.filterStatus === s ? " selected" : ""}>${escapeHtml(s)}</option>`
|
|
22121
|
+
).join("");
|
|
22122
|
+
const ownerOptions = data.owners.map(
|
|
22123
|
+
(o) => `<option value="${escapeHtml(o)}"${data.filterOwner === o ? " selected" : ""}>${escapeHtml(o)}</option>`
|
|
22124
|
+
).join("");
|
|
22125
|
+
const rows = data.docs.map(
|
|
22126
|
+
(doc) => `
|
|
22127
|
+
<tr>
|
|
22128
|
+
<td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.id)}</a></td>
|
|
22129
|
+
<td><a href="/docs/${data.type}/${doc.frontmatter.id}">${escapeHtml(doc.frontmatter.title)}</a></td>
|
|
22130
|
+
<td>${statusBadge(doc.frontmatter.status)}</td>
|
|
22131
|
+
<td>${escapeHtml(doc.frontmatter.owner ?? "\u2014")}</td>
|
|
22132
|
+
<td>${doc.frontmatter.priority ? `<span class="priority-${doc.frontmatter.priority.toLowerCase()}">${escapeHtml(doc.frontmatter.priority)}</span>` : "\u2014"}</td>
|
|
22133
|
+
<td>${formatDate(doc.frontmatter.updated ?? doc.frontmatter.created)}</td>
|
|
22134
|
+
</tr>`
|
|
22135
|
+
).join("\n");
|
|
22136
|
+
return `
|
|
22137
|
+
<div class="page-header">
|
|
22138
|
+
<h2>${escapeHtml(label)}s</h2>
|
|
22139
|
+
<div class="subtitle">${data.docs.length} document${data.docs.length !== 1 ? "s" : ""}</div>
|
|
22140
|
+
</div>
|
|
22141
|
+
|
|
22142
|
+
<div class="filters">
|
|
22143
|
+
<select onchange="filterByStatus(this.value)">
|
|
22144
|
+
<option value="">All statuses</option>
|
|
22145
|
+
${statusOptions}
|
|
22146
|
+
</select>
|
|
22147
|
+
<select onchange="filterByOwner(this.value)">
|
|
22148
|
+
<option value="">All owners</option>
|
|
22149
|
+
${ownerOptions}
|
|
22150
|
+
</select>
|
|
22151
|
+
</div>
|
|
22152
|
+
|
|
22153
|
+
${data.docs.length > 0 ? `
|
|
22154
|
+
<div class="table-wrap">
|
|
22155
|
+
<table>
|
|
22156
|
+
<thead>
|
|
22157
|
+
<tr>
|
|
22158
|
+
<th>ID</th>
|
|
22159
|
+
<th>Title</th>
|
|
22160
|
+
<th>Status</th>
|
|
22161
|
+
<th>Owner</th>
|
|
22162
|
+
<th>Priority</th>
|
|
22163
|
+
<th>Updated</th>
|
|
22164
|
+
</tr>
|
|
22165
|
+
</thead>
|
|
22166
|
+
<tbody>
|
|
22167
|
+
${rows}
|
|
22168
|
+
</tbody>
|
|
22169
|
+
</table>
|
|
22170
|
+
</div>` : `<div class="empty"><p>No ${label.toLowerCase()}s found.</p></div>`}
|
|
22171
|
+
|
|
22172
|
+
<script>
|
|
22173
|
+
function filterByStatus(status) {
|
|
22174
|
+
const url = new URL(window.location);
|
|
22175
|
+
if (status) url.searchParams.set('status', status);
|
|
22176
|
+
else url.searchParams.delete('status');
|
|
22177
|
+
window.location = url;
|
|
22178
|
+
}
|
|
22179
|
+
function filterByOwner(owner) {
|
|
22180
|
+
const url = new URL(window.location);
|
|
22181
|
+
if (owner) url.searchParams.set('owner', owner);
|
|
22182
|
+
else url.searchParams.delete('owner');
|
|
22183
|
+
window.location = url;
|
|
22184
|
+
}
|
|
22185
|
+
</script>
|
|
22186
|
+
`;
|
|
22187
|
+
}
|
|
22188
|
+
|
|
22189
|
+
// src/web/templates/pages/document-detail.ts
|
|
22190
|
+
function documentDetailPage(doc) {
|
|
22191
|
+
const fm = doc.frontmatter;
|
|
22192
|
+
const label = typeLabel(fm.type);
|
|
22193
|
+
const skipKeys = /* @__PURE__ */ new Set(["title", "type"]);
|
|
22194
|
+
const entries = Object.entries(fm).filter(
|
|
22195
|
+
([key]) => !skipKeys.has(key) && fm[key] != null
|
|
22196
|
+
);
|
|
22197
|
+
const dtDd = entries.map(([key, value]) => {
|
|
22198
|
+
let rendered;
|
|
22199
|
+
if (key === "status") {
|
|
22200
|
+
rendered = statusBadge(value);
|
|
22201
|
+
} else if (key === "tags" && Array.isArray(value)) {
|
|
22202
|
+
rendered = value.map((t) => `<span class="badge badge-default">${escapeHtml(t)}</span>`).join(" ");
|
|
22203
|
+
} else if (key === "created" || key === "updated") {
|
|
22204
|
+
rendered = formatDate(value);
|
|
22205
|
+
} else {
|
|
22206
|
+
rendered = escapeHtml(String(value));
|
|
22207
|
+
}
|
|
22208
|
+
return `<dt>${escapeHtml(key)}</dt><dd>${rendered}</dd>`;
|
|
22209
|
+
}).join("\n ");
|
|
22210
|
+
return `
|
|
22211
|
+
<div class="breadcrumb">
|
|
22212
|
+
<a href="/">Overview</a><span class="sep">/</span>
|
|
22213
|
+
<a href="/docs/${fm.type}">${escapeHtml(label)}s</a><span class="sep">/</span>
|
|
22214
|
+
${escapeHtml(fm.id)}
|
|
22215
|
+
</div>
|
|
22216
|
+
|
|
22217
|
+
<div class="page-header">
|
|
22218
|
+
<h2>${escapeHtml(fm.title)}</h2>
|
|
22219
|
+
<div class="subtitle">${escapeHtml(fm.id)} · ${escapeHtml(label)}</div>
|
|
22220
|
+
</div>
|
|
22221
|
+
|
|
22222
|
+
<div class="detail-meta">
|
|
22223
|
+
<dl>
|
|
22224
|
+
${dtDd}
|
|
22225
|
+
</dl>
|
|
22226
|
+
</div>
|
|
22227
|
+
|
|
22228
|
+
${doc.content.trim() ? `<div class="detail-content">${renderMarkdown(doc.content)}</div>` : ""}
|
|
22229
|
+
`;
|
|
22230
|
+
}
|
|
22231
|
+
|
|
22232
|
+
// src/web/templates/pages/gar.ts
|
|
22233
|
+
function garPage(report) {
|
|
22234
|
+
const dotClass = `dot-${report.overall}`;
|
|
22235
|
+
const areaCards = report.areas.map(
|
|
22236
|
+
(area) => `
|
|
22237
|
+
<div class="gar-area">
|
|
22238
|
+
<div class="area-header">
|
|
22239
|
+
<div class="area-dot dot-${area.status}"></div>
|
|
22240
|
+
<div class="area-name">${escapeHtml(area.name)}</div>
|
|
22241
|
+
</div>
|
|
22242
|
+
<div class="area-summary">${escapeHtml(area.summary)}</div>
|
|
22243
|
+
${area.items.length > 0 ? `<ul>${area.items.map((item) => `<li><span class="ref-id">${escapeHtml(item.id)}</span>${escapeHtml(item.title)}</li>`).join("")}</ul>` : ""}
|
|
22244
|
+
</div>`
|
|
22245
|
+
).join("\n");
|
|
22246
|
+
return `
|
|
22247
|
+
<div class="page-header">
|
|
22248
|
+
<h2>GAR Report</h2>
|
|
22249
|
+
<div class="subtitle">Generated ${escapeHtml(report.generatedAt)}</div>
|
|
22250
|
+
</div>
|
|
22251
|
+
|
|
22252
|
+
<div class="gar-overall">
|
|
22253
|
+
<div class="dot ${dotClass}"></div>
|
|
22254
|
+
<div class="label">Overall: ${escapeHtml(report.overall)}</div>
|
|
22255
|
+
</div>
|
|
22256
|
+
|
|
22257
|
+
<div class="gar-areas">
|
|
22258
|
+
${areaCards}
|
|
22259
|
+
</div>
|
|
22260
|
+
`;
|
|
22261
|
+
}
|
|
22262
|
+
|
|
22263
|
+
// src/web/templates/pages/board.ts
|
|
22264
|
+
function boardPage(data) {
|
|
22265
|
+
const typeOptions = data.types.map(
|
|
22266
|
+
(t) => `<option value="${escapeHtml(t)}"${data.type === t ? " selected" : ""}>${escapeHtml(typeLabel(t))}s</option>`
|
|
22267
|
+
).join("");
|
|
22268
|
+
const columns = data.columns.map(
|
|
22269
|
+
(col) => `
|
|
22270
|
+
<div class="board-column">
|
|
22271
|
+
<div class="board-column-header">
|
|
22272
|
+
<span>${escapeHtml(col.status)}</span>
|
|
22273
|
+
<span class="count">${col.docs.length}</span>
|
|
22274
|
+
</div>
|
|
22275
|
+
${col.docs.map(
|
|
22276
|
+
(doc) => `
|
|
22277
|
+
<div class="board-card">
|
|
22278
|
+
<a href="/docs/${doc.frontmatter.type}/${doc.frontmatter.id}">
|
|
22279
|
+
<div class="bc-id">${escapeHtml(doc.frontmatter.id)}</div>
|
|
22280
|
+
<div class="bc-title">${escapeHtml(doc.frontmatter.title)}</div>
|
|
22281
|
+
${doc.frontmatter.owner ? `<div class="bc-owner">${escapeHtml(doc.frontmatter.owner)}</div>` : ""}
|
|
22282
|
+
</a>
|
|
22283
|
+
</div>`
|
|
22284
|
+
).join("\n")}
|
|
22285
|
+
</div>`
|
|
22286
|
+
).join("\n");
|
|
22287
|
+
return `
|
|
22288
|
+
<div class="page-header">
|
|
22289
|
+
<h2>Status Board</h2>
|
|
22290
|
+
</div>
|
|
22291
|
+
|
|
22292
|
+
<div class="filters">
|
|
22293
|
+
<select onchange="filterByType(this.value)">
|
|
22294
|
+
<option value="">All types</option>
|
|
22295
|
+
${typeOptions}
|
|
22296
|
+
</select>
|
|
22297
|
+
</div>
|
|
22298
|
+
|
|
22299
|
+
${data.columns.length > 0 ? `<div class="board">${columns}</div>` : `<div class="empty"><p>No documents to display.</p></div>`}
|
|
22300
|
+
|
|
22301
|
+
<script>
|
|
22302
|
+
function filterByType(type) {
|
|
22303
|
+
if (type) window.location = '/board/' + type;
|
|
22304
|
+
else window.location = '/board';
|
|
22305
|
+
}
|
|
22306
|
+
</script>
|
|
22307
|
+
`;
|
|
22308
|
+
}
|
|
22309
|
+
|
|
22310
|
+
// src/web/router.ts
|
|
22311
|
+
function handleRequest(req, res, store, projectName) {
|
|
22312
|
+
const parsed = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
|
|
22313
|
+
const pathname = parsed.pathname;
|
|
22314
|
+
const navTypes = store.registeredTypes;
|
|
22315
|
+
try {
|
|
22316
|
+
if (pathname === "/styles.css") {
|
|
22317
|
+
res.writeHead(200, {
|
|
22318
|
+
"Content-Type": "text/css",
|
|
22319
|
+
"Cache-Control": "public, max-age=300"
|
|
22320
|
+
});
|
|
22321
|
+
res.end(renderStyles());
|
|
22322
|
+
return;
|
|
22323
|
+
}
|
|
22324
|
+
if (pathname === "/") {
|
|
22325
|
+
const data = getOverviewData(store);
|
|
22326
|
+
const body = overviewPage(data);
|
|
22327
|
+
respond(res, layout({ title: "Overview", activePath: "/", projectName, navTypes }, body));
|
|
22328
|
+
return;
|
|
22329
|
+
}
|
|
22330
|
+
if (pathname === "/gar") {
|
|
22331
|
+
const report = getGarData(store, projectName);
|
|
22332
|
+
const body = garPage(report);
|
|
22333
|
+
respond(res, layout({ title: "GAR Report", activePath: "/gar", projectName, navTypes }, body));
|
|
22334
|
+
return;
|
|
22335
|
+
}
|
|
22336
|
+
const boardMatch = pathname.match(/^\/board(?:\/([^/]+))?$/);
|
|
22337
|
+
if (boardMatch) {
|
|
22338
|
+
const type = boardMatch[1];
|
|
22339
|
+
if (type && !navTypes.includes(type)) {
|
|
22340
|
+
notFound(res, projectName, navTypes, pathname);
|
|
22341
|
+
return;
|
|
22342
|
+
}
|
|
22343
|
+
const data = getBoardData(store, type);
|
|
22344
|
+
const body = boardPage(data);
|
|
22345
|
+
respond(res, layout({ title: "Board", activePath: "/board", projectName, navTypes }, body));
|
|
22346
|
+
return;
|
|
22347
|
+
}
|
|
22348
|
+
const detailMatch = pathname.match(/^\/docs\/([^/]+)\/([^/]+)$/);
|
|
22349
|
+
if (detailMatch) {
|
|
22350
|
+
const [, type, id] = detailMatch;
|
|
22351
|
+
const doc = getDocumentDetail(store, type, id);
|
|
22352
|
+
if (!doc) {
|
|
22353
|
+
notFound(res, projectName, navTypes, pathname);
|
|
22354
|
+
return;
|
|
22355
|
+
}
|
|
22356
|
+
const body = documentDetailPage(doc);
|
|
22357
|
+
respond(res, layout({ title: `${id} \u2014 ${doc.frontmatter.title}`, activePath: `/docs/${type}`, projectName, navTypes }, body));
|
|
22358
|
+
return;
|
|
22359
|
+
}
|
|
22360
|
+
const listMatch = pathname.match(/^\/docs\/([^/]+)$/);
|
|
22361
|
+
if (listMatch) {
|
|
22362
|
+
const type = listMatch[1];
|
|
22363
|
+
const filterStatus = parsed.searchParams.get("status") ?? void 0;
|
|
22364
|
+
const filterOwner = parsed.searchParams.get("owner") ?? void 0;
|
|
22365
|
+
const data = getDocumentListData(store, type, filterStatus, filterOwner);
|
|
22366
|
+
if (!data) {
|
|
22367
|
+
notFound(res, projectName, navTypes, pathname);
|
|
22368
|
+
return;
|
|
22369
|
+
}
|
|
22370
|
+
const body = documentsPage(data);
|
|
22371
|
+
respond(res, layout({ title: `${type}`, activePath: `/docs/${type}`, projectName, navTypes }, body));
|
|
22372
|
+
return;
|
|
22373
|
+
}
|
|
22374
|
+
notFound(res, projectName, navTypes, pathname);
|
|
22375
|
+
} catch (err) {
|
|
22376
|
+
console.error("[marvin web] Error handling request:", err);
|
|
22377
|
+
res.writeHead(500, { "Content-Type": "text/html" });
|
|
22378
|
+
res.end("<h1>500 \u2014 Internal Server Error</h1>");
|
|
22379
|
+
}
|
|
22380
|
+
}
|
|
22381
|
+
function respond(res, html) {
|
|
22382
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
22383
|
+
res.end(html);
|
|
22384
|
+
}
|
|
22385
|
+
function notFound(res, projectName, navTypes, activePath) {
|
|
22386
|
+
const body = `<div class="empty"><h2>404</h2><p>Page not found.</p><p><a href="/">Go to overview</a></p></div>`;
|
|
22387
|
+
res.writeHead(404, { "Content-Type": "text/html; charset=utf-8" });
|
|
22388
|
+
res.end(layout({ title: "Not Found", activePath, projectName, navTypes }, body));
|
|
22389
|
+
}
|
|
22390
|
+
|
|
22391
|
+
// src/web/server.ts
|
|
22392
|
+
async function startWebServer(opts) {
|
|
22393
|
+
const project = loadProject();
|
|
22394
|
+
const plugin = resolvePlugin(project.config.methodology);
|
|
22395
|
+
const pluginRegs = plugin?.documentTypeRegistrations ?? [];
|
|
22396
|
+
const allSkills = loadAllSkills(project.marvinDir);
|
|
22397
|
+
const allSkillIds = [...allSkills.keys()];
|
|
22398
|
+
const skillRegs = collectSkillRegistrations(allSkillIds, allSkills);
|
|
22399
|
+
const store = new DocumentStore(project.marvinDir, [
|
|
22400
|
+
...pluginRegs,
|
|
22401
|
+
...skillRegs
|
|
22402
|
+
]);
|
|
22403
|
+
const projectName = project.config.name;
|
|
22404
|
+
const server = http.createServer((req, res) => {
|
|
22405
|
+
handleRequest(req, res, store, projectName);
|
|
22406
|
+
});
|
|
22407
|
+
server.listen(opts.port, () => {
|
|
22408
|
+
const url2 = `http://localhost:${opts.port}`;
|
|
22409
|
+
console.log(`
|
|
22410
|
+
Marvin dashboard running at ${url2}
|
|
22411
|
+
`);
|
|
22412
|
+
console.log(" Press Ctrl+C to stop.\n");
|
|
22413
|
+
if (opts.open) {
|
|
22414
|
+
openBrowser(url2);
|
|
22415
|
+
}
|
|
22416
|
+
});
|
|
22417
|
+
const shutdown = () => {
|
|
22418
|
+
console.log("\n Shutting down...\n");
|
|
22419
|
+
server.close(() => process.exit(0));
|
|
22420
|
+
setTimeout(() => process.exit(0), 2e3);
|
|
22421
|
+
};
|
|
22422
|
+
process.on("SIGINT", shutdown);
|
|
22423
|
+
process.on("SIGTERM", shutdown);
|
|
22424
|
+
}
|
|
22425
|
+
function openBrowser(url2) {
|
|
22426
|
+
const platform = process.platform;
|
|
22427
|
+
const cmd = platform === "darwin" ? `open "${url2}"` : platform === "win32" ? `start "${url2}"` : `xdg-open "${url2}"`;
|
|
22428
|
+
exec(cmd, (err) => {
|
|
22429
|
+
if (err) {
|
|
22430
|
+
}
|
|
22431
|
+
});
|
|
22432
|
+
}
|
|
22433
|
+
|
|
22434
|
+
// src/cli/commands/web.ts
|
|
22435
|
+
async function webCommand(options) {
|
|
22436
|
+
const port = options.port ? parseInt(options.port, 10) : 3e3;
|
|
22437
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
22438
|
+
console.error("Error: invalid port number");
|
|
22439
|
+
process.exit(1);
|
|
22440
|
+
}
|
|
22441
|
+
await startWebServer({ port, open: options.open });
|
|
22442
|
+
}
|
|
22443
|
+
|
|
21395
22444
|
// src/cli/program.ts
|
|
21396
22445
|
function createProgram() {
|
|
21397
22446
|
const program2 = new Command();
|
|
21398
22447
|
program2.name("marvin").description(
|
|
21399
22448
|
"AI-powered product development assistant with Product Owner, Delivery Manager, and Technical Lead personas"
|
|
21400
|
-
).version("0.2.
|
|
22449
|
+
).version("0.2.10");
|
|
21401
22450
|
program2.command("init").description("Initialize a new Marvin project in the current directory").action(async () => {
|
|
21402
22451
|
await initCommand();
|
|
21403
22452
|
});
|
|
@@ -21474,6 +22523,9 @@ function createProgram() {
|
|
|
21474
22523
|
).action(async (options) => {
|
|
21475
22524
|
await garReportCommand(options);
|
|
21476
22525
|
});
|
|
22526
|
+
program2.command("web").description("Launch a local web dashboard for project data").option("-p, --port <port>", "Port to listen on (default: 3000)").option("--no-open", "Don't auto-open the browser").action(async (options) => {
|
|
22527
|
+
await webCommand(options);
|
|
22528
|
+
});
|
|
21477
22529
|
return program2;
|
|
21478
22530
|
}
|
|
21479
22531
|
|