@pi-unipi/kanboard 2.6.1 → 2.9.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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@pi-unipi/kanboard",
3
- "version": "2.6.1",
4
- "description": "Visualization layer for unipi workflow HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
3
+ "version": "2.9.0",
4
+ "description": "Visualization layer for unipi workflow \u2014 HTTP server with htmx/Alpine.js UI, modular parsers, TUI overlay, and kanban board",
5
5
  "type": "module",
6
6
  "main": "index.ts",
7
7
  "license": "MIT",
@@ -39,19 +39,23 @@
39
39
  "access": "public"
40
40
  },
41
41
  "dependencies": {
42
- "@pi-unipi/core": "2.6.1"
42
+ "@pi-unipi/core": "2.9.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "@earendil-works/pi-coding-agent": "^0.80.0",
46
- "@earendil-works/pi-tui": "^0.80.0"
45
+ "@earendil-works/pi-coding-agent": "^0.84.0",
46
+ "@earendil-works/pi-tui": "^0.84.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/node": "^25.6.0",
50
50
  "typescript": "^6.0.0"
51
51
  },
52
52
  "pi": {
53
- "extensions": [],
54
- "skills": [],
53
+ "extensions": [
54
+ "./index.ts"
55
+ ],
56
+ "skills": [
57
+ "./skills"
58
+ ],
55
59
  "prompts": [],
56
60
  "themes": []
57
61
  }
@@ -0,0 +1,165 @@
1
+ /**
2
+ * @pi-unipi/kanboard — Config-driven checkbox document parser
3
+ *
4
+ * Replaces 7 separate parser classes (Spec, QuickWork, Debug, Fix, Chore, Review,
5
+ * and the checkbox-extraction half of Milestone) with one config-driven class.
6
+ * Each doc type is just: path regex + type label + command string + checkbox pattern.
7
+ */
8
+
9
+ import * as fs from "node:fs";
10
+ import * as path from "node:path";
11
+ import type { DocParser, ParsedDoc, ParsedItem, DocType } from "../types.js";
12
+ import { parseFrontmatter } from "./frontmatter.js";
13
+
14
+ /** Config for a checkbox-based document type. */
15
+ interface CheckboxDocConfig {
16
+ /** Document type label */
17
+ type: DocType;
18
+ /** Path regex to match (e.g. /\/specs\//) */
19
+ pathRegex: RegExp;
20
+ /** Command string for parsed items */
21
+ command: string | ((fileName: string) => string);
22
+ /** Whether to also extract ## headers as items (default: false) */
23
+ extractHeaders?: boolean;
24
+ /** Status for header items (default: "todo") */
25
+ headerStatus?: "todo" | "done";
26
+ /** Extra metadata fields to include */
27
+ extraMetadata?: (metadata: Record<string, string>) => Record<string, string>;
28
+ /** Custom title extraction (default: metadata.title ?? fileName) */
29
+ titleExtractor?: (metadata: Record<string, string>, fileName: string) => string;
30
+ }
31
+
32
+ const CHECKBOX_PATTERN = /^\s*-\s*\[([ xX])\]\s*(.*)$/;
33
+ const HEADER_PATTERN = /^##\s+(.+)$/;
34
+
35
+ /** Config-driven parser for checkbox-style documents. */
36
+ export class CheckboxParser implements DocParser {
37
+ constructor(private config: CheckboxDocConfig) {}
38
+
39
+ canParse(filePath: string): boolean {
40
+ return this.config.pathRegex.test(filePath) && filePath.endsWith(".md");
41
+ }
42
+
43
+ parse(filePath: string): ParsedDoc {
44
+ const warnings: string[] = [];
45
+ const items: ParsedItem[] = [];
46
+ let content: string;
47
+
48
+ try {
49
+ content = fs.readFileSync(filePath, "utf-8");
50
+ } catch (err: unknown) {
51
+ warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
52
+ return this.emptyDoc(filePath, warnings);
53
+ }
54
+
55
+ const { metadata, bodyStart } = parseFrontmatter(content);
56
+ const lines = content.split("\n");
57
+ const fileName = path.basename(filePath);
58
+ const cmd = typeof this.config.command === "function"
59
+ ? this.config.command(fileName)
60
+ : this.config.command;
61
+
62
+ for (let i = bodyStart; i < lines.length; i++) {
63
+ const line = lines[i];
64
+ const lineNum = i + 1;
65
+
66
+ // Match ## headers if configured
67
+ if (this.config.extractHeaders) {
68
+ const headerMatch = line.match(HEADER_PATTERN);
69
+ if (headerMatch) {
70
+ items.push({
71
+ text: headerMatch[1].trim(),
72
+ status: this.config.headerStatus ?? "todo",
73
+ lineNumber: lineNum,
74
+ sourceFile: fileName,
75
+ command: cmd,
76
+ });
77
+ }
78
+ }
79
+
80
+ // Match checklist items
81
+ const checkboxMatch = line.match(CHECKBOX_PATTERN);
82
+ if (checkboxMatch) {
83
+ const checked = checkboxMatch[1].toLowerCase() === "x";
84
+ const text = checkboxMatch[2].trim();
85
+ if (text) {
86
+ items.push({
87
+ text,
88
+ status: checked ? "done" : "todo",
89
+ lineNumber: lineNum,
90
+ sourceFile: fileName,
91
+ command: cmd,
92
+ });
93
+ }
94
+ }
95
+ }
96
+
97
+ const title = this.config.titleExtractor
98
+ ? this.config.titleExtractor(metadata, fileName)
99
+ : (metadata.title ?? fileName.replace(/\.md$/, ""));
100
+
101
+ const finalMetadata = this.config.extraMetadata
102
+ ? this.config.extraMetadata(metadata)
103
+ : metadata;
104
+
105
+ return {
106
+ type: this.config.type,
107
+ title,
108
+ filePath,
109
+ items,
110
+ metadata: finalMetadata,
111
+ warnings,
112
+ };
113
+ }
114
+
115
+ private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
116
+ return {
117
+ type: this.config.type,
118
+ title: path.basename(filePath).replace(/\.md$/, ""),
119
+ filePath,
120
+ items: [],
121
+ metadata: {},
122
+ warnings,
123
+ };
124
+ }
125
+ }
126
+
127
+ /** All checkbox doc configs — drives the 6 simple parsers. */
128
+ export const CHECKBOX_DOC_CONFIGS: CheckboxDocConfig[] = [
129
+ {
130
+ type: "spec",
131
+ pathRegex: /\/specs\//,
132
+ command: (f) => `/unipi:plan specs:${f}`,
133
+ },
134
+ {
135
+ type: "quick-work",
136
+ pathRegex: /\/quick-work\//,
137
+ command: "/unipi:quick-work",
138
+ },
139
+ {
140
+ type: "debug",
141
+ pathRegex: /\/debug\//,
142
+ command: (f) => `/unipi:fix debug:${f}`,
143
+ extractHeaders: true,
144
+ headerStatus: "todo",
145
+ },
146
+ {
147
+ type: "fix",
148
+ pathRegex: /\/fix\//,
149
+ command: "/unipi:fix",
150
+ extractHeaders: true,
151
+ headerStatus: "done",
152
+ extraMetadata: (m) => ({ ...m, related_debug: m.related_debug ?? m.debug ?? "" }),
153
+ },
154
+ {
155
+ type: "chore",
156
+ pathRegex: /\/chore\//,
157
+ command: (f) => `/unipi:chore-execute chore:${f}`,
158
+ titleExtractor: (m, f) => m.title ?? m.name ?? f.replace(/\.md$/, ""),
159
+ },
160
+ {
161
+ type: "review",
162
+ pathRegex: /\/reviews\//,
163
+ command: "/unipi:review-work",
164
+ },
165
+ ];
@@ -0,0 +1,26 @@
1
+ /** Shared frontmatter parser for kanboard document parsers. */
2
+
3
+ /** Parse frontmatter from markdown content.
4
+ * Returns metadata key-value pairs and the line number where the body starts. */
5
+ export function parseFrontmatter(content: string): {
6
+ metadata: Record<string, string>;
7
+ bodyStart: number;
8
+ } {
9
+ const metadata: Record<string, string> = {};
10
+ const lines = content.split("\n");
11
+
12
+ if (lines[0]?.trim() !== "---") return { metadata, bodyStart: 0 };
13
+
14
+ for (let i = 1; i < lines.length; i++) {
15
+ const line = lines[i];
16
+ if (line.trim() === "---") {
17
+ return { metadata, bodyStart: i + 1 };
18
+ }
19
+ const match = line.match(/^(\w[\w-]*):\s*(.*)$/);
20
+ if (match) {
21
+ metadata[match[1]] = match[2].trim();
22
+ }
23
+ }
24
+
25
+ return { metadata, bodyStart: 0 };
26
+ }
package/parser/index.ts CHANGED
@@ -7,27 +7,7 @@
7
7
 
8
8
  import * as fs from "node:fs";
9
9
  import * as path from "node:path";
10
- import type { DocParser, ParsedDoc, DocType } from "../types.js";
11
-
12
- /** Path patterns for doc type detection */
13
- const PATH_PATTERNS: Array<{ pattern: RegExp; type: DocType }> = [
14
- { pattern: /\/specs\//, type: "spec" },
15
- { pattern: /\/plans\//, type: "plan" },
16
- { pattern: /MILESTONES\.md$/i, type: "milestone" },
17
- { pattern: /\/quick-work\//, type: "quick-work" },
18
- { pattern: /\/debug\//, type: "debug" },
19
- { pattern: /\/fix\//, type: "fix" },
20
- { pattern: /\/chore\//, type: "chore" },
21
- { pattern: /\/reviews\//, type: "review" },
22
- ];
23
-
24
- /** Detect doc type from file path */
25
- export function detectDocType(filePath: string): DocType | null {
26
- for (const { pattern, type } of PATH_PATTERNS) {
27
- if (pattern.test(filePath)) return type;
28
- }
29
- return null;
30
- }
10
+ import type { DocParser, ParsedDoc } from "../types.js";
31
11
 
32
12
  /** Parser registry — manages all document parsers */
33
13
  export class ParserRegistry {
@@ -92,30 +72,23 @@ export class ParserRegistry {
92
72
  }
93
73
  }
94
74
 
95
- /** Create a registry with all default parsers registered */
75
+ /** Create a registry with all default parsers registered. */
96
76
  export async function createDefaultRegistry(): Promise<ParserRegistry> {
97
77
  const registry = new ParserRegistry();
98
78
 
99
- // Import and register all parsers
100
- const { SpecParser } = await import("./specs.js");
79
+ // Register checkbox-driven parsers (spec, quick-work, debug, fix, chore, review)
80
+ const { CheckboxParser, CHECKBOX_DOC_CONFIGS } = await import("./checkbox-parser.js");
81
+ for (const config of CHECKBOX_DOC_CONFIGS) {
82
+ registry.register(new CheckboxParser(config));
83
+ }
84
+
85
+ // Register plan parser (task-header + status-line format, not checkbox)
101
86
  const { PlanParser } = await import("./plans.js");
102
- const { MilestoneParser } = await import("./milestones.js");
103
- const {
104
- QuickWorkParser,
105
- DebugParser,
106
- FixParser,
107
- ChoreParser,
108
- ReviewParser,
109
- } = await import("./remaining.js");
110
-
111
- registry.register(new SpecParser());
112
87
  registry.register(new PlanParser());
88
+
89
+ // Register milestone parser (inline frontmatter + phase headers)
90
+ const { MilestoneParser } = await import("./milestones.js");
113
91
  registry.register(new MilestoneParser());
114
- registry.register(new QuickWorkParser());
115
- registry.register(new DebugParser());
116
- registry.register(new FixParser());
117
- registry.register(new ChoreParser());
118
- registry.register(new ReviewParser());
119
92
 
120
93
  return registry;
121
94
  }
package/parser/plans.ts CHANGED
@@ -9,31 +9,9 @@
9
9
  import * as fs from "node:fs";
10
10
  import * as path from "node:path";
11
11
  import type { DocParser, ParsedDoc, ParsedItem, ItemStatus } from "../types.js";
12
+ import { parseFrontmatter } from "./frontmatter.js";
12
13
 
13
14
  /** Parse frontmatter from markdown file */
14
- function parseFrontmatter(content: string): {
15
- metadata: Record<string, string>;
16
- bodyStart: number;
17
- } {
18
- const metadata: Record<string, string> = {};
19
- const lines = content.split("\n");
20
-
21
- if (lines[0]?.trim() !== "---") return { metadata, bodyStart: 0 };
22
-
23
- for (let i = 1; i < lines.length; i++) {
24
- const line = lines[i];
25
- if (line.trim() === "---") {
26
- return { metadata, bodyStart: i + 1 };
27
- }
28
- const match = line.match(/^(\w[\w-]*):\s*(.*)$/);
29
- if (match) {
30
- metadata[match[1]] = match[2].trim();
31
- }
32
- }
33
-
34
- return { metadata, bodyStart: 0 };
35
- }
36
-
37
15
  /** Status keyword to ItemStatus mapping */
38
16
  const STATUS_MAP: Record<string, ItemStatus> = {
39
17
  unstarted: "todo",
package/server/index.ts CHANGED
@@ -78,12 +78,6 @@ export class KanboardServer {
78
78
 
79
79
  /** Start the server with port allocation */
80
80
  async start(): Promise<{ port: number; url: string }> {
81
- // Check for existing instance
82
- const existing = this.checkExistingInstance();
83
- if (existing) {
84
- // Removed console.log — existing instance detection is silent.
85
- }
86
-
87
81
  this.server = http.createServer((req, res) => this.handleRequest(req, res));
88
82
 
89
83
  const port = await this.allocatePort();
@@ -267,12 +261,6 @@ export async function startServer(
267
261
  registerMilestoneRoutes(server, docsRoot);
268
262
  registerWorkflowRoutes(server, docsRoot);
269
263
 
270
- server.route("POST", "/api/docs/:type/:file/items/:line", async (req, res) => {
271
- // Placeholder — will be implemented with actual file updating
272
- res.writeHead(200, { "Content-Type": "application/json" });
273
- res.end(JSON.stringify({ ok: true }));
274
- });
275
-
276
264
  const { port, url } = await server.start();
277
265
  return { server, port, url };
278
266
  }
@@ -1,386 +0,0 @@
1
- /**
2
- * @pi-unipi/kanboard — Remaining Parsers
3
- *
4
- * Parsers for quick-work, debug, fix, chore, and review document types.
5
- */
6
-
7
- import * as fs from "node:fs";
8
- import * as path from "node:path";
9
- import type { DocParser, ParsedDoc, ParsedItem } from "../types.js";
10
-
11
- /** Parse frontmatter from markdown file */
12
- function parseFrontmatter(content: string): {
13
- metadata: Record<string, string>;
14
- bodyStart: number;
15
- } {
16
- const metadata: Record<string, string> = {};
17
- const lines = content.split("\n");
18
-
19
- if (lines[0]?.trim() !== "---") return { metadata, bodyStart: 0 };
20
-
21
- for (let i = 1; i < lines.length; i++) {
22
- const line = lines[i];
23
- if (line.trim() === "---") {
24
- return { metadata, bodyStart: i + 1 };
25
- }
26
- const match = line.match(/^(\w[\w-]*):\s*(.*)$/);
27
- if (match) {
28
- metadata[match[1]] = match[2].trim();
29
- }
30
- }
31
-
32
- return { metadata, bodyStart: 0 };
33
- }
34
-
35
- /** Quick-work parser — extracts summary and checklist items */
36
- export class QuickWorkParser implements DocParser {
37
- canParse(filePath: string): boolean {
38
- return /\/quick-work\//.test(filePath) && filePath.endsWith(".md");
39
- }
40
-
41
- parse(filePath: string): ParsedDoc {
42
- const warnings: string[] = [];
43
- const items: ParsedItem[] = [];
44
- let content: string;
45
-
46
- try {
47
- content = fs.readFileSync(filePath, "utf-8");
48
- } catch (err: unknown) {
49
- warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
50
- return this.emptyDoc(filePath, warnings);
51
- }
52
-
53
- const { metadata, bodyStart } = parseFrontmatter(content);
54
- const lines = content.split("\n");
55
- const fileName = path.basename(filePath);
56
-
57
- // Extract checklist items if present
58
- for (let i = bodyStart; i < lines.length; i++) {
59
- const line = lines[i];
60
- const lineNum = i + 1;
61
-
62
- const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
63
- if (checkboxMatch) {
64
- const checked = checkboxMatch[1].toLowerCase() === "x";
65
- const text = checkboxMatch[2].trim();
66
- if (text) {
67
- items.push({
68
- text,
69
- status: checked ? "done" : "todo",
70
- lineNumber: lineNum,
71
- sourceFile: fileName,
72
- command: `/unipi:quick-work`,
73
- });
74
- }
75
- }
76
- }
77
-
78
- return {
79
- type: "quick-work",
80
- title: metadata.title ?? fileName.replace(/\.md$/, ""),
81
- filePath,
82
- items,
83
- metadata,
84
- warnings,
85
- };
86
- }
87
-
88
- private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
89
- return {
90
- type: "quick-work",
91
- title: path.basename(filePath).replace(/\.md$/, ""),
92
- filePath,
93
- items: [],
94
- metadata: {},
95
- warnings,
96
- };
97
- }
98
- }
99
-
100
- /** Debug parser — extracts bug description and status */
101
- export class DebugParser implements DocParser {
102
- canParse(filePath: string): boolean {
103
- return /\/debug\//.test(filePath) && filePath.endsWith(".md");
104
- }
105
-
106
- parse(filePath: string): ParsedDoc {
107
- const warnings: string[] = [];
108
- const items: ParsedItem[] = [];
109
- let content: string;
110
-
111
- try {
112
- content = fs.readFileSync(filePath, "utf-8");
113
- } catch (err: unknown) {
114
- warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
115
- return this.emptyDoc(filePath, warnings);
116
- }
117
-
118
- const { metadata, bodyStart } = parseFrontmatter(content);
119
- const lines = content.split("\n");
120
- const fileName = path.basename(filePath);
121
-
122
- // Extract sections as items
123
- for (let i = bodyStart; i < lines.length; i++) {
124
- const line = lines[i];
125
- const lineNum = i + 1;
126
-
127
- // Match ## headers as items
128
- const headerMatch = line.match(/^##\s+(.+)$/);
129
- if (headerMatch) {
130
- const text = headerMatch[1].trim();
131
- items.push({
132
- text,
133
- status: "todo",
134
- lineNumber: lineNum,
135
- sourceFile: fileName,
136
- command: `/unipi:fix debug:${fileName}`,
137
- });
138
- }
139
-
140
- // Match checklist items
141
- const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
142
- if (checkboxMatch) {
143
- const checked = checkboxMatch[1].toLowerCase() === "x";
144
- const text = checkboxMatch[2].trim();
145
- if (text) {
146
- items.push({
147
- text,
148
- status: checked ? "done" : "todo",
149
- lineNumber: lineNum,
150
- sourceFile: fileName,
151
- command: `/unipi:fix debug:${fileName}`,
152
- });
153
- }
154
- }
155
- }
156
-
157
- return {
158
- type: "debug",
159
- title: metadata.title ?? fileName.replace(/\.md$/, ""),
160
- filePath,
161
- items,
162
- metadata,
163
- warnings,
164
- };
165
- }
166
-
167
- private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
168
- return {
169
- type: "debug",
170
- title: path.basename(filePath).replace(/\.md$/, ""),
171
- filePath,
172
- items: [],
173
- metadata: {},
174
- warnings,
175
- };
176
- }
177
- }
178
-
179
- /** Fix parser — extracts what was fixed */
180
- export class FixParser implements DocParser {
181
- canParse(filePath: string): boolean {
182
- return /\/fix\//.test(filePath) && filePath.endsWith(".md");
183
- }
184
-
185
- parse(filePath: string): ParsedDoc {
186
- const warnings: string[] = [];
187
- const items: ParsedItem[] = [];
188
- let content: string;
189
-
190
- try {
191
- content = fs.readFileSync(filePath, "utf-8");
192
- } catch (err: unknown) {
193
- warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
194
- return this.emptyDoc(filePath, warnings);
195
- }
196
-
197
- const { metadata, bodyStart } = parseFrontmatter(content);
198
- const lines = content.split("\n");
199
- const fileName = path.basename(filePath);
200
-
201
- // Extract sections and checklist items
202
- for (let i = bodyStart; i < lines.length; i++) {
203
- const line = lines[i];
204
- const lineNum = i + 1;
205
-
206
- const headerMatch = line.match(/^##\s+(.+)$/);
207
- if (headerMatch) {
208
- items.push({
209
- text: headerMatch[1].trim(),
210
- status: "done",
211
- lineNumber: lineNum,
212
- sourceFile: fileName,
213
- command: `/unipi:fix`,
214
- });
215
- }
216
-
217
- const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
218
- if (checkboxMatch) {
219
- const checked = checkboxMatch[1].toLowerCase() === "x";
220
- const text = checkboxMatch[2].trim();
221
- if (text) {
222
- items.push({
223
- text,
224
- status: checked ? "done" : "todo",
225
- lineNumber: lineNum,
226
- sourceFile: fileName,
227
- command: `/unipi:fix`,
228
- });
229
- }
230
- }
231
- }
232
-
233
- // Extract related debug reference
234
- const related = metadata.related_debug ?? metadata.debug ?? "";
235
-
236
- return {
237
- type: "fix",
238
- title: metadata.title ?? fileName.replace(/\.md$/, ""),
239
- filePath,
240
- items,
241
- metadata: { ...metadata, related_debug: related },
242
- warnings,
243
- };
244
- }
245
-
246
- private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
247
- return {
248
- type: "fix",
249
- title: path.basename(filePath).replace(/\.md$/, ""),
250
- filePath,
251
- items: [],
252
- metadata: {},
253
- warnings,
254
- };
255
- }
256
- }
257
-
258
- /** Chore parser — extracts chore name and steps */
259
- export class ChoreParser implements DocParser {
260
- canParse(filePath: string): boolean {
261
- return /\/chore\//.test(filePath) && filePath.endsWith(".md");
262
- }
263
-
264
- parse(filePath: string): ParsedDoc {
265
- const warnings: string[] = [];
266
- const items: ParsedItem[] = [];
267
- let content: string;
268
-
269
- try {
270
- content = fs.readFileSync(filePath, "utf-8");
271
- } catch (err: unknown) {
272
- warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
273
- return this.emptyDoc(filePath, warnings);
274
- }
275
-
276
- const { metadata, bodyStart } = parseFrontmatter(content);
277
- const lines = content.split("\n");
278
- const fileName = path.basename(filePath);
279
-
280
- // Extract checklist items as steps
281
- for (let i = bodyStart; i < lines.length; i++) {
282
- const line = lines[i];
283
- const lineNum = i + 1;
284
-
285
- const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
286
- if (checkboxMatch) {
287
- const checked = checkboxMatch[1].toLowerCase() === "x";
288
- const text = checkboxMatch[2].trim();
289
- if (text) {
290
- items.push({
291
- text,
292
- status: checked ? "done" : "todo",
293
- lineNumber: lineNum,
294
- sourceFile: fileName,
295
- command: `/unipi:chore-execute chore:${fileName}`,
296
- });
297
- }
298
- }
299
- }
300
-
301
- return {
302
- type: "chore",
303
- title: metadata.title ?? metadata.name ?? fileName.replace(/\.md$/, ""),
304
- filePath,
305
- items,
306
- metadata,
307
- warnings,
308
- };
309
- }
310
-
311
- private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
312
- return {
313
- type: "chore",
314
- title: path.basename(filePath).replace(/\.md$/, ""),
315
- filePath,
316
- items: [],
317
- metadata: {},
318
- warnings,
319
- };
320
- }
321
- }
322
-
323
- /** Review parser — extracts review remarks and status */
324
- export class ReviewParser implements DocParser {
325
- canParse(filePath: string): boolean {
326
- return /\/reviews\//.test(filePath) && filePath.endsWith(".md");
327
- }
328
-
329
- parse(filePath: string): ParsedDoc {
330
- const warnings: string[] = [];
331
- const items: ParsedItem[] = [];
332
- let content: string;
333
-
334
- try {
335
- content = fs.readFileSync(filePath, "utf-8");
336
- } catch (err: unknown) {
337
- warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
338
- return this.emptyDoc(filePath, warnings);
339
- }
340
-
341
- const { metadata, bodyStart } = parseFrontmatter(content);
342
- const lines = content.split("\n");
343
- const fileName = path.basename(filePath);
344
-
345
- // Extract checklist items (remarks)
346
- for (let i = bodyStart; i < lines.length; i++) {
347
- const line = lines[i];
348
- const lineNum = i + 1;
349
-
350
- const checkboxMatch = line.match(/^\s*-\s*\[([ xX])\]\s*(.*)$/);
351
- if (checkboxMatch) {
352
- const checked = checkboxMatch[1].toLowerCase() === "x";
353
- const text = checkboxMatch[2].trim();
354
- if (text) {
355
- items.push({
356
- text,
357
- status: checked ? "done" : "todo",
358
- lineNumber: lineNum,
359
- sourceFile: fileName,
360
- command: `/unipi:review-work`,
361
- });
362
- }
363
- }
364
- }
365
-
366
- return {
367
- type: "review",
368
- title: metadata.title ?? fileName.replace(/\.md$/, ""),
369
- filePath,
370
- items,
371
- metadata,
372
- warnings,
373
- };
374
- }
375
-
376
- private emptyDoc(filePath: string, warnings: string[]): ParsedDoc {
377
- return {
378
- type: "review",
379
- title: path.basename(filePath).replace(/\.md$/, ""),
380
- filePath,
381
- items: [],
382
- metadata: {},
383
- warnings,
384
- };
385
- }
386
- }
package/parser/specs.ts DELETED
@@ -1,105 +0,0 @@
1
- /**
2
- * @pi-unipi/kanboard — Spec Parser
3
- *
4
- * Parses brainstorm specs for `- [ ]` / `- [x]` checklist items.
5
- */
6
-
7
- import * as fs from "node:fs";
8
- import * as path from "node:path";
9
- import type { DocParser, ParsedDoc, ParsedItem } from "../types.js";
10
-
11
- /** Parse frontmatter from markdown file */
12
- function parseFrontmatter(content: string): {
13
- metadata: Record<string, string>;
14
- bodyStart: number;
15
- } {
16
- const metadata: Record<string, string> = {};
17
- const lines = content.split("\n");
18
-
19
- if (lines[0]?.trim() !== "---") return { metadata, bodyStart: 0 };
20
-
21
- for (let i = 1; i < lines.length; i++) {
22
- const line = lines[i];
23
- if (line.trim() === "---") {
24
- return { metadata, bodyStart: i + 1 };
25
- }
26
- const match = line.match(/^(\w[\w-]*):\s*(.*)$/);
27
- if (match) {
28
- metadata[match[1]] = match[2].trim();
29
- }
30
- }
31
-
32
- return { metadata, bodyStart: 0 };
33
- }
34
-
35
- /** Spec parser — extracts checklist items from brainstorm specs */
36
- export class SpecParser implements DocParser {
37
- canParse(filePath: string): boolean {
38
- return /\/specs\//.test(filePath) && filePath.endsWith(".md");
39
- }
40
-
41
- parse(filePath: string): ParsedDoc {
42
- const warnings: string[] = [];
43
- const items: ParsedItem[] = [];
44
- let content: string;
45
-
46
- try {
47
- content = fs.readFileSync(filePath, "utf-8");
48
- } catch (err: unknown) {
49
- warnings.push(`Could not read file: ${err instanceof Error ? err.message : String(err)}`);
50
- return this.emptyDoc(filePath, warnings);
51
- }
52
-
53
- const { metadata, bodyStart } = parseFrontmatter(content);
54
- const lines = content.split("\n");
55
- const fileName = path.basename(filePath);
56
-
57
- for (let i = bodyStart; i < lines.length; i++) {
58
- const line = lines[i];
59
- const lineNum = i + 1; // 1-indexed
60
-
61
- // Match `- [ ]` and `- [x]` patterns
62
- const checkboxMatch = line.match(/^(\s*)-\s*\[([ xX])\]\s*(.*)$/);
63
- if (checkboxMatch) {
64
- const checked = checkboxMatch[2].toLowerCase() === "x";
65
- const text = checkboxMatch[3].trim();
66
-
67
- if (!text) {
68
- warnings.push(`Line ${lineNum}: Empty checkbox text`);
69
- continue;
70
- }
71
-
72
- items.push({
73
- text,
74
- status: checked ? "done" : "todo",
75
- lineNumber: lineNum,
76
- sourceFile: fileName,
77
- command: `/unipi:plan specs:${fileName}`,
78
- });
79
- }
80
- }
81
-
82
- return {
83
- type: "spec",
84
- title: metadata.title ?? fileName.replace(/\.md$/, ""),
85
- filePath,
86
- items,
87
- metadata,
88
- warnings,
89
- };
90
- }
91
-
92
- private emptyDoc(
93
- filePath: string,
94
- warnings: string[],
95
- ): ParsedDoc {
96
- return {
97
- type: "spec",
98
- title: path.basename(filePath).replace(/\.md$/, ""),
99
- filePath,
100
- items: [],
101
- metadata: {},
102
- warnings,
103
- };
104
- }
105
- }