-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathkanban.mjs
More file actions
109 lines (96 loc) · 3.6 KB
/
Copy pathkanban.mjs
File metadata and controls
109 lines (96 loc) · 3.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
#!/usr/bin/env node
// projectstore — kanban.mjs
// Regenerates kanban.md by scanning all story files in epics/<id>/stories/*.md,
// reading their frontmatter (status, priority, title, epic) and rendering into the
// kanban template. Source of truth = story frontmatter.
//
// Output: JSON { path, content } — caller writes after approval.
import { readdirSync, statSync, readFileSync, existsSync } from "node:fs";
import { join } from "node:path";
import { readConfig, loadLayout, loadTemplate, renderTemplate, parseFrontmatter, today } from "./lib.mjs";
function die(msg) {
process.stderr.write(`projectstore/kanban: ${msg}\n`);
process.exit(1);
}
function findStories(vault, epicsPath) {
const root = join(vault, epicsPath);
if (!existsSync(root)) return [];
const stories = [];
for (const epicId of readdirSync(root)) {
const storiesDir = join(root, epicId, "stories");
if (!existsSync(storiesDir)) continue;
for (const file of readdirSync(storiesDir)) {
if (!file.endsWith(".md")) continue;
const full = join(storiesDir, file);
const md = readFileSync(full, "utf8");
const { data } = parseFrontmatter(md);
stories.push({
path: full,
relPath: `${epicsPath}/${epicId}/stories/${file}`,
epicId,
status: (data.status || "planned").toLowerCase(),
priority: data.priority || "p2",
title: data.title || file.replace(/\.md$/, ""),
id: data.id || file.replace(/\.md$/, ""),
});
}
}
return stories;
}
function statusToColumn(status) {
const m = {
planned: "Backlog",
todo: "ToDo",
"to-do": "ToDo",
"in-progress": "In Progress",
in_progress: "In Progress",
"in progress": "In Progress",
review: "Review",
done: "Done",
closed: "Done",
};
return m[status] || "Backlog";
}
function renderItem(story) {
const tags = [`#${story.priority}`];
if (story.status === "done") tags.push("#done");
if (story.status === "review") tags.push("#review");
const wikilink = `[[${story.relPath.replace(/\.md$/, "")}|${story.epicId}: ${story.title}]]`;
const check = story.status === "done" ? "[x]" : "[ ]";
return `- ${check} ${wikilink} ${tags.join(" ")}`;
}
function main() {
const cfg = readConfig();
if (!cfg) die("No projectstore config. Run /projectstore:bind first.");
const layout = loadLayout(cfg.layout);
if (!layout.kanban) die(`Layout ${cfg.layout} does not declare a kanban config.`);
const epicsFolder = layout.folders.find((f) => f.kind === "epic");
if (!epicsFolder) die("No epic folder in layout — kanban needs epics.");
const stories = findStories(cfg.vault_path, epicsFolder.path);
const columns = {};
for (const col of layout.kanban.columns) columns[col] = [];
for (const s of stories) {
const col = statusToColumn(s.status);
if (!columns[col]) columns[col] = [];
columns[col].push(renderItem(s));
}
const tpl = loadTemplate(cfg.language || "en", "kanban");
const vars = {
date: today(),
backlog_items: (columns["Backlog"] || []).join("\n") || "",
todo_items: (columns["ToDo"] || []).join("\n") || "",
in_progress_items: (columns["In Progress"] || []).join("\n") || "",
review_items: (columns["Review"] || []).join("\n") || "",
done_items: (columns["Done"] || []).join("\n") || "",
};
const out = {
path: join(cfg.vault_path, layout.kanban.file || "kanban.md"),
content: renderTemplate(tpl, vars),
stats: {
total: stories.length,
by_column: Object.fromEntries(Object.entries(columns).map(([k, v]) => [k, v.length])),
},
};
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
}
main();