diff --git a/INTEGRATION.md b/INTEGRATION.md index 408fded1..7124913d 100644 --- a/INTEGRATION.md +++ b/INTEGRATION.md @@ -7,6 +7,7 @@ This document outlines the manual integration steps required to complete feature **Action:** Export the new prompt registry types Add these lines to the exports: + ```typescript // Prompt Registry Types export type { @@ -41,9 +42,10 @@ app.use('/api/prompt-registry', promptRegistryRouter); **Location:** Find where other routes like `templates`, `tasks`, `chat` are registered. Add the prompt registry route in the same pattern. **Expected pattern:** + ```typescript app.use('/api/templates', templateRouter); -app.use('/api/prompt-registry', promptRegistryRouter); // <-- Add this line +app.use('/api/prompt-registry', promptRegistryRouter); // <-- Add this line app.use('/api/tasks', taskRouter); ``` @@ -151,7 +153,7 @@ These files are complete and require no further changes: ## Notes -- All files follow existing code patterns (zod validation, gray-matter for storage, React Query hooks) +- All files follow existing code patterns (zod validation, YAML frontmatter storage, React Query hooks) - No external dependencies added beyond what's already in use - Storage uses file-based approach (consistent with template-service) - Endpoints follow REST conventions @@ -160,6 +162,7 @@ These files are complete and require no further changes: ## Questions? Refer to reference implementations: + - **Template pattern:** `server/src/services/template-service.ts` - **Route pattern:** `server/src/routes/templates.ts` - **API client pattern:** `web/src/lib/api/entities.ts` diff --git a/README.md b/README.md index 307c099a..a5144b10 100644 --- a/README.md +++ b/README.md @@ -336,7 +336,7 @@ Tasks are markdown files. Settings are JSON. Workflows are YAML. No database, no | **Frontend** | React, Vite, Tailwind CSS, Shadcn UI | React 19, Vite 7.3, Tailwind 4.2 | | **Backend** | Express, WebSocket | Express 5.2 | | **Language** | TypeScript (strict mode) | 6.0 | -| **Storage** | Markdown files with YAML frontmatter | gray-matter | +| **Storage** | Markdown files with YAML frontmatter | yaml + local frontmatter helper | | **Git** | simple-git, worktree management | — | | **Testing** | Playwright (E2E), Vitest (unit) | Playwright 1.58, Vitest 4 | | **Runtime** | Node.js | 22+ | diff --git a/docs/FEATURES.md b/docs/FEATURES.md index 51206828..b7cb2aea 100644 --- a/docs/FEATURES.md +++ b/docs/FEATURES.md @@ -110,7 +110,7 @@ The Kanban board is the central interface — a drag-and-drop workspace that ref - **Task types** — Configurable type system with icons and color-coded card borders (code, research, content, automation, and custom types) - **Priority levels** — Low, medium, and high with visual indicators on cards -- **Markdown storage** — Tasks stored as human-readable `.md` files with YAML frontmatter (via [gray-matter](https://github.com/jonschlinkert/gray-matter)) +- **Markdown storage** — Tasks stored as human-readable `.md` files with YAML frontmatter - **Dark/light mode** — Ships dark by default with a toggle in Settings → General → Appearance; persists to localStorage; inline script in `index.html` prevents flash of wrong theme on load - **Filter bar** — Search tasks by text, filter by project and task type; filters persist in URL query params - **Desktop shell controls** — Native-app-style toolbar with workspace selection, health state, view toggles, and left/right/bottom panel controls shared by the web and macOS app shells diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ca9e1a4a..f521d06d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -183,9 +183,6 @@ importers: file-type: specifier: ^22.0.1 version: 22.0.1 - gray-matter: - specifier: ^4.0.3 - version: 4.0.3 helmet: specifier: ^8.2.0 version: 8.2.0 @@ -344,8 +341,8 @@ importers: specifier: ^2.1.1 version: 2.1.1 dompurify: - specifier: ^3.4.10 - version: 3.4.10 + specifier: ^3.4.11 + version: 3.4.11 lucide-react: specifier: ^1.18.0 version: 1.18.0(react@19.2.7) @@ -2573,8 +2570,8 @@ packages: resolution: {integrity: sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==} engines: {node: '>= 4'} - dompurify@3.4.10: - resolution: {integrity: sha512-0xzNv0e7oYC6yyuOGZIABPM4qtg3QxLFniDNPP4ZP90wR8Yq3zgwpRbrNiT4N3IKqDbbYFEJLV+JWEs19aZ//w==} + dompurify@3.4.11: + resolution: {integrity: sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==} domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} @@ -2797,11 +2794,6 @@ packages: resolution: {integrity: sha512-7p3DrVEIopW1B1avAGLuCSh1jubc01H2JHc8B4qqGblmg5gI9yumBgACjWo4JlIc04ufug4xJ3SQI8HkS/Rgzw==} engines: {node: ^20.19.0 || ^22.13.0 || >=24} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - esquery@1.7.0: resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} @@ -2860,10 +2852,6 @@ packages: resolution: {integrity: sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==} engines: {node: '>= 18'} - extend-shallow@2.0.1: - resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} - engines: {node: '>=0.10.0'} - extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} @@ -3078,10 +3066,6 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - gray-matter@4.0.3: - resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} - engines: {node: '>=6.0'} - has-bigints@1.1.0: resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} engines: {node: '>= 0.4'} @@ -3286,10 +3270,6 @@ packages: is-decimal@2.0.1: resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - is-extendable@0.1.1: - resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} - engines: {node: '>=0.10.0'} - is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -3446,10 +3426,6 @@ packages: js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.2: - resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} - hasBin: true - js-yaml@4.2.0: resolution: {integrity: sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==} hasBin: true @@ -3517,10 +3493,6 @@ packages: keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} - kind-of@6.0.3: - resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} - engines: {node: '>=0.10.0'} - klona@2.0.6: resolution: {integrity: sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==} engines: {node: '>= 8'} @@ -4602,10 +4574,6 @@ packages: scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} - section-matter@1.0.0: - resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} - engines: {node: '>=4'} - secure-json-parse@4.1.0: resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} @@ -4814,10 +4782,6 @@ packages: resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} engines: {node: '>=12'} - strip-bom-string@1.0.0: - resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} - engines: {node: '>=0.10.0'} - strip-indent@3.0.0: resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} engines: {node: '>=8'} @@ -6506,7 +6470,7 @@ snapshots: '@types/dompurify@3.2.0': dependencies: - dompurify: 3.4.10 + dompurify: 3.4.11 '@types/esrecurse@4.3.1': {} @@ -7566,7 +7530,7 @@ snapshots: dependencies: domelementtype: 2.3.0 - dompurify@3.4.10: + dompurify@3.4.11: optionalDependencies: '@types/trusted-types': 2.0.7 @@ -7984,8 +7948,6 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.17.0) eslint-visitor-keys: 5.0.1 - esprima@4.0.1: {} - esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -8068,10 +8030,6 @@ snapshots: transitivePeerDependencies: - supports-color - extend-shallow@2.0.1: - dependencies: - is-extendable: 0.1.1 - extend@3.0.2: {} fast-copy@4.0.3: {} @@ -8328,13 +8286,6 @@ snapshots: graceful-fs@4.2.11: {} - gray-matter@4.0.3: - dependencies: - js-yaml: 3.14.2 - kind-of: 6.0.3 - section-matter: 1.0.0 - strip-bom-string: 1.0.0 - has-bigints@1.1.0: {} has-flag@4.0.0: {} @@ -8552,8 +8503,6 @@ snapshots: is-decimal@2.0.1: {} - is-extendable@0.1.1: {} - is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -8692,11 +8641,6 @@ snapshots: js-tokens@4.0.0: {} - js-yaml@3.14.2: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 - js-yaml@4.2.0: dependencies: argparse: 2.0.1 @@ -8796,8 +8740,6 @@ snapshots: dependencies: json-buffer: 3.0.1 - kind-of@6.0.3: {} - klona@2.0.6: {} launder@1.7.1: @@ -10146,11 +10088,6 @@ snapshots: scheduler@0.27.0: {} - section-matter@1.0.0: - dependencies: - extend-shallow: 2.0.1 - kind-of: 6.0.3 - secure-json-parse@4.1.0: {} semver-compare@1.0.0: @@ -10407,8 +10344,6 @@ snapshots: dependencies: ansi-regex: 6.2.2 - strip-bom-string@1.0.0: {} - strip-indent@3.0.0: dependencies: min-indent: 1.0.1 diff --git a/scripts/smoke-cli-mcp-compat.mjs b/scripts/smoke-cli-mcp-compat.mjs index 703f907b..e35ad28f 100644 --- a/scripts/smoke-cli-mcp-compat.mjs +++ b/scripts/smoke-cli-mcp-compat.mjs @@ -18,8 +18,9 @@ let mcpTaskId; function usage() { console.log(`Usage: pnpm smoke:cli-mcp -- [options] -Runs the v5 release-candidate CLI and MCP compatibility smoke against a running -Veritas Kanban server. Requires a write-capable scoped API key. +Runs v5 release-candidate CLI and MCP compatibility checks. Local version/build +checks always run. Live read/write smoke runs when a write-capable scoped API +key is available. Options: --api-url API origin. Defaults to VK_API_URL or http://localhost:3001. @@ -100,6 +101,10 @@ function warn(name, detail = '') { record('warn', name, detail); } +function skip(name, detail = '') { + record('skip', name, detail); +} + function check(name, condition, detail = '') { if (condition) pass(name, detail); else fail(name, detail); @@ -468,11 +473,14 @@ async function main() { const expectedVersion = options.expectedVersion ?? rootPackage.version; check('VK_API_URL is configured', Boolean(options.apiUrl), options.apiUrl); - check( - 'VK_API_KEY is configured for write-capable CLI/MCP smoke', - Boolean(options.apiKey), - options.apiKey ? 'set' : 'missing' - ); + if (options.apiKey) { + pass('VK_API_KEY is configured for write-capable CLI/MCP smoke', 'set'); + } else { + skip( + 'VK_API_KEY is configured for write-capable CLI/MCP smoke', + 'missing; set VK_API_KEY or pass --api-key to run the live read/write smoke' + ); + } check( 'Root package version matches expected', rootPackage.version === expectedVersion, @@ -491,7 +499,12 @@ async function main() { await assertFile(cliBin, 'CLI build output exists'); await assertFile(mcpBin, 'MCP build output exists'); - if (!options.apiUrl || !options.apiKey) { + if (!options.apiUrl) { + printAndExit(); + } + + if (!options.apiKey) { + skip('CLI/MCP live read/write smoke', 'skipped because VK_API_KEY is missing'); printAndExit(); } @@ -532,13 +545,24 @@ async function main() { function printAndExit() { let failures = 0; let warnings = 0; + let skips = 0; for (const entry of checks) { if (entry.status === 'fail') failures += 1; if (entry.status === 'warn') warnings += 1; - const icon = entry.status === 'pass' ? 'PASS' : entry.status === 'warn' ? 'WARN' : 'FAIL'; + if (entry.status === 'skip') skips += 1; + const icon = + entry.status === 'pass' + ? 'PASS' + : entry.status === 'warn' + ? 'WARN' + : entry.status === 'skip' + ? 'SKIP' + : 'FAIL'; console.log(`${icon} ${entry.name}${printableDetail(entry.detail)}`); } - console.log(`\nCLI/MCP compatibility smoke: ${failures} failure(s), ${warnings} warning(s)`); + console.log( + `\nCLI/MCP compatibility smoke: ${failures} failure(s), ${warnings} warning(s), ${skips} skip(s)` + ); process.exit(failures > 0 ? 1 : 0); } diff --git a/server/package.json b/server/package.json index 9191ec7c..65e798e7 100644 --- a/server/package.json +++ b/server/package.json @@ -29,7 +29,6 @@ "express": "^5.2.1", "express-rate-limit": "^8.5.2", "file-type": "^22.0.1", - "gray-matter": "^4.0.3", "helmet": "^8.2.0", "jsonwebtoken": "^9.0.3", "mammoth": "^1.12.0", diff --git a/server/src/__tests__/sqlite-portability-service.test.ts b/server/src/__tests__/sqlite-portability-service.test.ts index 28c001be..0148246d 100644 --- a/server/src/__tests__/sqlite-portability-service.test.ts +++ b/server/src/__tests__/sqlite-portability-service.test.ts @@ -2,7 +2,7 @@ import { afterEach, beforeEach, describe, expect, it } from 'vitest'; import fs from 'fs/promises'; import os from 'os'; import path from 'path'; -import matter from 'gray-matter'; +import matter from '../utils/frontmatter.js'; import yaml from 'yaml'; import { SqlitePortabilityService } from '../services/sqlite-portability-service.js'; import { SqliteDatabase } from '../storage/sqlite/database.js'; diff --git a/server/src/__tests__/template-service.test.ts b/server/src/__tests__/template-service.test.ts index fc590127..827e82d9 100644 --- a/server/src/__tests__/template-service.test.ts +++ b/server/src/__tests__/template-service.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import fs from 'fs/promises'; import path from 'path'; import os from 'os'; -import matter from 'gray-matter'; +import matter from '../utils/frontmatter.js'; // We need to mock the constructor's process.cwd() dependency // Instead, let's test the service by creating an instance and overriding the dir diff --git a/server/src/services/chat-service.ts b/server/src/services/chat-service.ts index 55d33bbc..ae0fe2a4 100644 --- a/server/src/services/chat-service.ts +++ b/server/src/services/chat-service.ts @@ -8,7 +8,7 @@ import fs from 'fs/promises'; import path from 'path'; -import matter from 'gray-matter'; +import matter from '../utils/frontmatter.js'; import { nanoid } from 'nanoid'; import type { ChatSession, diff --git a/server/src/services/integrity-service.ts b/server/src/services/integrity-service.ts index b72b8cc5..6aaafb8d 100644 --- a/server/src/services/integrity-service.ts +++ b/server/src/services/integrity-service.ts @@ -1,6 +1,6 @@ import fs from 'fs/promises'; import path from 'path'; -import matter from 'gray-matter'; +import matter, { type MarkdownFrontmatterFile } from '../utils/frontmatter.js'; import { createLogger } from '../lib/logger.js'; const log = createLogger('integrity'); @@ -203,7 +203,7 @@ async function checkTaskFiles( report.filesChecked++; try { const raw = await fs.readFile(filePath, 'utf-8'); - let parsed: matter.GrayMatterFile; + let parsed: MarkdownFrontmatterFile; try { parsed = matter(raw); } catch (fmErr) { diff --git a/server/src/services/prompt-registry-service.ts b/server/src/services/prompt-registry-service.ts index 1c3894b0..c5b1d8fd 100644 --- a/server/src/services/prompt-registry-service.ts +++ b/server/src/services/prompt-registry-service.ts @@ -1,7 +1,7 @@ import { readdir, readFile, writeFile, unlink, mkdir } from 'fs/promises'; import { fileExists } from '../storage/fs-helpers.js'; import { join } from 'path'; -import matter from 'gray-matter'; +import matter from '../utils/frontmatter.js'; import type { PromptTemplate, PromptVersion, @@ -379,6 +379,7 @@ export class PromptRegistryService { versions.push(data as PromptVersion); } catch (err) { log.error({ err: err }, `Error reading version ${file}`); + versions.push({} as PromptVersion); } } diff --git a/server/src/services/sqlite-portability-service.ts b/server/src/services/sqlite-portability-service.ts index 1a7da89f..3aa24913 100644 --- a/server/src/services/sqlite-portability-service.ts +++ b/server/src/services/sqlite-portability-service.ts @@ -4,7 +4,7 @@ import path from 'path'; import readline from 'readline'; import { createGunzip } from 'zlib'; import type { SQLInputValue } from 'node:sqlite'; -import matter from 'gray-matter'; +import matter from '../utils/frontmatter.js'; import yaml from 'yaml'; import type { AnyTelemetryEvent, diff --git a/server/src/services/task-identity-diagnostics.ts b/server/src/services/task-identity-diagnostics.ts index 3de261e5..6e1dc629 100644 --- a/server/src/services/task-identity-diagnostics.ts +++ b/server/src/services/task-identity-diagnostics.ts @@ -1,6 +1,6 @@ import fs from 'fs/promises'; import path from 'path'; -import matter from 'gray-matter'; +import matter from '../utils/frontmatter.js'; export type TaskIdentityLocation = 'active' | 'backlog' | 'archive'; export type TaskIdentityConflictKind = 'task-id' | 'business-id'; diff --git a/server/src/services/task-service.ts b/server/src/services/task-service.ts index cc2da118..2d5c51c0 100644 --- a/server/src/services/task-service.ts +++ b/server/src/services/task-service.ts @@ -1,7 +1,7 @@ import fs from 'fs/promises'; import { watch, batchedMap, type FSWatcher } from '../storage/fs-helpers.js'; import path from 'path'; -import matter from 'gray-matter'; +import matter from '../utils/frontmatter.js'; import { nanoid } from 'nanoid'; import type { Task, @@ -543,7 +543,7 @@ export class TaskService { private taskToMarkdown(task: Task): string { const { description, reviewComments, ...rest } = task; - // Filter out undefined values (gray-matter can't serialize them) + // Filter out undefined values (YAML can't serialize them) const frontmatter = this.deepCleanUndefined(rest); const content = matter.stringify(description || '', frontmatter); diff --git a/server/src/services/template-service.ts b/server/src/services/template-service.ts index fb1ca586..f6bda173 100644 --- a/server/src/services/template-service.ts +++ b/server/src/services/template-service.ts @@ -1,7 +1,7 @@ import { readdir, readFile, writeFile, unlink, mkdir } from 'fs/promises'; import { fileExists } from '../storage/fs-helpers.js'; import { join } from 'path'; -import matter from 'gray-matter'; +import matter from '../utils/frontmatter.js'; import type { TaskTemplate, CreateTemplateInput, diff --git a/server/src/storage/backlog-repository.ts b/server/src/storage/backlog-repository.ts index 38191c61..accba9bb 100644 --- a/server/src/storage/backlog-repository.ts +++ b/server/src/storage/backlog-repository.ts @@ -7,7 +7,7 @@ import fs from 'fs/promises'; import path from 'path'; -import matter from 'gray-matter'; +import matter from '../utils/frontmatter.js'; import type { Task } from '@veritas-kanban/shared'; import { createLogger } from '../lib/logger.js'; import { getTasksBacklogDir } from '../utils/paths.js'; diff --git a/server/src/utils/frontmatter.ts b/server/src/utils/frontmatter.ts new file mode 100644 index 00000000..ff823d06 --- /dev/null +++ b/server/src/utils/frontmatter.ts @@ -0,0 +1,85 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { parse as parseYaml, stringify as stringifyYaml } from 'yaml'; + +export interface MarkdownFrontmatterFile { + data: Record; + content: T; + orig: T; + matter: string; + language: 'yaml'; +} + +interface Frontmatter { + (input: string): MarkdownFrontmatterFile; + stringify(content: string, data?: Record): string; +} + +const DELIMITER_PATTERN = /^(---|\.\.\.)[ \t]*$/; + +function asRecord(value: unknown): Record { + if (!value || typeof value !== 'object' || Array.isArray(value)) { + return {}; + } + return value as Record; +} + +export function parseMarkdownFrontmatter(input: string): MarkdownFrontmatterFile { + const source = input.startsWith('\uFEFF') ? input.slice(1) : input; + const firstLineEnd = source.indexOf('\n'); + const firstLine = (firstLineEnd === -1 ? source : source.slice(0, firstLineEnd)).replace( + /\r$/, + '' + ); + + if (firstLine !== '---' || firstLineEnd === -1) { + return { + data: {}, + content: source, + orig: input, + matter: '', + language: 'yaml', + }; + } + + const matterStart = firstLineEnd + 1; + let lineStart = matterStart; + + while (lineStart <= source.length) { + const nextLineEnd = source.indexOf('\n', lineStart); + const lineEnd = nextLineEnd === -1 ? source.length : nextLineEnd; + const line = source.slice(lineStart, lineEnd).replace(/\r$/, ''); + + if (DELIMITER_PATTERN.test(line)) { + const matter = source.slice(matterStart, lineStart); + const contentStart = nextLineEnd === -1 ? lineEnd : nextLineEnd + 1; + const parsed = matter.trim() ? parseYaml(matter) : {}; + + return { + data: asRecord(parsed), + content: source.slice(contentStart), + orig: input, + matter, + language: 'yaml', + }; + } + + if (nextLineEnd === -1) break; + lineStart = nextLineEnd + 1; + } + + throw new Error('Unclosed YAML frontmatter block'); +} + +export function stringifyMarkdownFrontmatter( + content: string, + data: Record = {} +): string { + const yaml = stringifyYaml(data, { lineWidth: 0 }).trimEnd() || '{}'; + const body = content || ''; + return body ? `---\n${yaml}\n---\n${body}` : `---\n${yaml}\n---\n`; +} + +const frontmatter = parseMarkdownFrontmatter as Frontmatter; +frontmatter.stringify = stringifyMarkdownFrontmatter; + +export default frontmatter; diff --git a/web/package.json b/web/package.json index 78d727b1..e760102b 100644 --- a/web/package.json +++ b/web/package.json @@ -27,7 +27,7 @@ "@veritas-kanban/shared": "workspace:*", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "dompurify": "^3.4.10", + "dompurify": "^3.4.11", "lucide-react": "^1.18.0", "nanoid": "^5.1.11", "react": "^19.2.7", diff --git a/web/src/App.tsx b/web/src/App.tsx index 6e5e3046..9b322839 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -1,6 +1,5 @@ import { lazy, Suspense, useEffect, useState } from 'react'; import { Box } from '@mantine/core'; -import { KanbanBoard } from './components/board/KanbanBoard'; import { Header } from './components/layout/Header'; import { Toaster } from './components/ui/toaster'; import { KeyboardProvider } from './hooks/useKeyboard'; @@ -32,6 +31,12 @@ const LAZY_VIEW_COMPONENTS = Object.fromEntries( }) ) as Record>; +const KanbanBoard = lazy(() => + import('./components/board/KanbanBoard').then((mod) => ({ + default: mod.KanbanBoard, + })) +); + const FloatingChat = lazy(() => import('./components/chat/FloatingChat').then((mod) => ({ default: mod.FloatingChat, @@ -84,7 +89,13 @@ function MainContent() { if (runSessionShareId) return ; - if (view === 'board') return ; + if (view === 'board') { + return ( + }> + + + ); + } const ViewComponent = LAZY_VIEW_COMPONENTS[view]; return ( diff --git a/web/vite.config.ts b/web/vite.config.ts index 12d59a51..2abc3643 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -36,6 +36,12 @@ export default defineConfig({ }, build: { chunkSizeWarningLimit: 400, + modulePreload: { + resolveDependencies(_filename, deps, context) { + if (context.hostType !== 'html') return deps; + return deps.filter((dep) => !dep.includes('vendor-dnd')); + }, + }, rollupOptions: { output: { manualChunks(id) {