2026-02-14 18:22:42 +01:00
|
|
|
/// <reference types="vitest/config" />
|
2026-02-16 16:56:44 +01:00
|
|
|
import path from 'path'
|
2026-02-20 10:39:45 +01:00
|
|
|
import fs from 'fs'
|
|
|
|
|
import { defineConfig, type Plugin } from 'vite'
|
2026-02-14 18:20:07 +01:00
|
|
|
import react from '@vitejs/plugin-react'
|
2026-02-16 16:56:44 +01:00
|
|
|
import tailwindcss from '@tailwindcss/vite'
|
2026-02-20 10:39:45 +01:00
|
|
|
import matter from 'gray-matter'
|
|
|
|
|
|
|
|
|
|
// --- Vault API middleware (dev only) ---
|
|
|
|
|
|
|
|
|
|
interface VaultEntry {
|
|
|
|
|
path: string
|
|
|
|
|
filename: string
|
|
|
|
|
title: string
|
|
|
|
|
isA: string | null
|
|
|
|
|
aliases: string[]
|
|
|
|
|
belongsTo: string[]
|
|
|
|
|
relatedTo: string[]
|
|
|
|
|
status: string | null
|
2026-03-06 21:52:40 +01:00
|
|
|
archived: boolean
|
|
|
|
|
trashed: boolean
|
|
|
|
|
trashedAt: number | null
|
2026-02-20 10:39:45 +01:00
|
|
|
modifiedAt: number | null
|
|
|
|
|
createdAt: number | null
|
|
|
|
|
fileSize: number
|
|
|
|
|
snippet: string
|
2026-03-06 21:52:40 +01:00
|
|
|
wordCount: number
|
2026-02-20 10:39:45 +01:00
|
|
|
relationships: Record<string, string[]>
|
2026-03-06 21:52:40 +01:00
|
|
|
icon: string | null
|
|
|
|
|
color: string | null
|
|
|
|
|
order: number | null
|
|
|
|
|
sidebarLabel: string | null
|
|
|
|
|
template: string | null
|
|
|
|
|
sort: string | null
|
|
|
|
|
view: string | null
|
|
|
|
|
visible: boolean | null
|
|
|
|
|
outgoingLinks: string[]
|
|
|
|
|
properties: Record<string, string | number | boolean | null>
|
2026-02-20 10:39:45 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Extract all [[wiki-links]] from a string. */
|
|
|
|
|
function extractWikiLinks(value: string): string[] {
|
|
|
|
|
const matches = value.match(/\[\[[^\]]+\]\]/g)
|
|
|
|
|
return matches ?? []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Extract wiki-links from a frontmatter value (string or array of strings). */
|
|
|
|
|
function wikiLinksFromValue(value: unknown): string[] {
|
|
|
|
|
if (typeof value === 'string') return extractWikiLinks(value)
|
|
|
|
|
if (Array.isArray(value)) {
|
|
|
|
|
return value.flatMap((v) => (typeof v === 'string' ? extractWikiLinks(v) : []))
|
|
|
|
|
}
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Frontmatter keys that map to dedicated VaultEntry fields (skip in generic relationships)
|
|
|
|
|
const DEDICATED_KEYS = new Set([
|
|
|
|
|
'aliases', 'is_a', 'is a', 'belongs_to', 'belongs to',
|
2026-03-18 01:56:33 +01:00
|
|
|
'related_to', 'related to', 'status', 'title',
|
2026-02-20 10:39:45 +01:00
|
|
|
])
|
|
|
|
|
|
|
|
|
|
function parseMarkdownFile(filePath: string): VaultEntry | null {
|
|
|
|
|
try {
|
|
|
|
|
const raw = fs.readFileSync(filePath, 'utf-8')
|
|
|
|
|
const stats = fs.statSync(filePath)
|
|
|
|
|
const { data: fm, content } = matter(raw)
|
|
|
|
|
|
|
|
|
|
const filename = path.basename(filePath)
|
|
|
|
|
const basename = filename.replace(/\.md$/, '')
|
|
|
|
|
|
|
|
|
|
// Title: first H1 in body, or frontmatter title, or filename
|
|
|
|
|
const h1Match = content.match(/^#\s+(.+)$/m)
|
|
|
|
|
const title = (fm.title as string) || (h1Match ? h1Match[1].trim() : basename)
|
|
|
|
|
|
|
|
|
|
// Helper to get a frontmatter string field (case-insensitive key)
|
|
|
|
|
const getString = (...keys: string[]): string | null => {
|
|
|
|
|
for (const k of keys) {
|
|
|
|
|
for (const fk of Object.keys(fm)) {
|
|
|
|
|
if (fk.toLowerCase() === k.toLowerCase() && typeof fm[fk] === 'string') {
|
|
|
|
|
return fm[fk]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Helper to get a frontmatter array-of-strings field
|
|
|
|
|
const getArray = (...keys: string[]): string[] => {
|
|
|
|
|
for (const k of keys) {
|
|
|
|
|
for (const fk of Object.keys(fm)) {
|
|
|
|
|
if (fk.toLowerCase() === k.toLowerCase()) {
|
|
|
|
|
const val = fm[fk]
|
|
|
|
|
if (Array.isArray(val)) return val.map(String)
|
|
|
|
|
if (typeof val === 'string') return [val]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return []
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const aliases = getArray('aliases')
|
|
|
|
|
const belongsToRaw = getArray('belongs_to', 'belongs to')
|
|
|
|
|
const relatedToRaw = getArray('related_to', 'related to')
|
|
|
|
|
const belongsTo = belongsToRaw.flatMap((v) => extractWikiLinks(v))
|
|
|
|
|
const relatedTo = relatedToRaw.flatMap((v) => extractWikiLinks(v))
|
|
|
|
|
|
2026-03-18 01:56:33 +01:00
|
|
|
// Created at from filesystem metadata (birthtime), not frontmatter
|
|
|
|
|
const createdAt = stats.birthtimeMs
|
2026-02-20 10:39:45 +01:00
|
|
|
|
|
|
|
|
// Snippet: first 200 chars of body after frontmatter, stripped of markdown
|
|
|
|
|
const bodyText = content.replace(/^#+\s+.+$/gm, '').replace(/[\n\r]+/g, ' ').trim()
|
|
|
|
|
const snippet = bodyText.slice(0, 200)
|
|
|
|
|
|
|
|
|
|
// Generic relationships: any frontmatter key whose value contains wiki-links
|
|
|
|
|
const relationships: Record<string, string[]> = {}
|
|
|
|
|
for (const key of Object.keys(fm)) {
|
|
|
|
|
if (DEDICATED_KEYS.has(key.toLowerCase())) continue
|
|
|
|
|
const links = wikiLinksFromValue(fm[key])
|
|
|
|
|
if (links.length > 0) {
|
|
|
|
|
relationships[key] = links
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
test: add real filesystem vault integration tests
Replace mock vault approach with real filesystem I/O for integration
tests. Each test copies tests/fixtures/test-vault/ to a temp directory,
overrides mock handlers to point at it, and verifies actual file
operations through the vite dev server middleware.
Tests cover: vault loading, archive/trash filtering, note creation,
rename with filesystem update, wikilink cascade on rename, relationship
display, and real file content loading.
Also extends vite.config.ts vault API middleware with write endpoints
(save, rename, delete, search, entry) and fixes getBool to handle
YAML 1.2 string values like "Yes"/"yes" (js-yaml 4.x compatibility).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:46:45 +01:00
|
|
|
// Boolean field helper — handles both real booleans and YAML 1.1 string
|
|
|
|
|
// variants ("Yes"/"yes"/"True"/"true") that js-yaml 4.x (YAML 1.2) leaves as strings.
|
2026-03-06 21:52:40 +01:00
|
|
|
const getBool = (...keys: string[]): boolean | null => {
|
|
|
|
|
for (const k of keys) {
|
|
|
|
|
for (const fk of Object.keys(fm)) {
|
test: add real filesystem vault integration tests
Replace mock vault approach with real filesystem I/O for integration
tests. Each test copies tests/fixtures/test-vault/ to a temp directory,
overrides mock handlers to point at it, and verifies actual file
operations through the vite dev server middleware.
Tests cover: vault loading, archive/trash filtering, note creation,
rename with filesystem update, wikilink cascade on rename, relationship
display, and real file content loading.
Also extends vite.config.ts vault API middleware with write endpoints
(save, rename, delete, search, entry) and fixes getBool to handle
YAML 1.2 string values like "Yes"/"yes" (js-yaml 4.x compatibility).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:46:45 +01:00
|
|
|
if (fk.toLowerCase() === k.toLowerCase()) {
|
|
|
|
|
const v = fm[fk]
|
|
|
|
|
if (typeof v === 'boolean') return v
|
|
|
|
|
if (typeof v === 'string') {
|
|
|
|
|
const lc = v.toLowerCase()
|
|
|
|
|
if (lc === 'true' || lc === 'yes') return true
|
|
|
|
|
if (lc === 'false' || lc === 'no') return false
|
|
|
|
|
}
|
2026-03-06 21:52:40 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 10:39:45 +01:00
|
|
|
return {
|
|
|
|
|
path: filePath,
|
|
|
|
|
filename,
|
|
|
|
|
title,
|
2026-03-06 21:52:40 +01:00
|
|
|
isA: getString('is_a', 'is a', 'type'),
|
2026-02-20 10:39:45 +01:00
|
|
|
aliases,
|
|
|
|
|
belongsTo,
|
|
|
|
|
relatedTo,
|
|
|
|
|
status: getString('status'),
|
2026-03-06 21:52:40 +01:00
|
|
|
archived: getBool('archived') ?? false,
|
|
|
|
|
trashed: getBool('trashed') ?? false,
|
|
|
|
|
trashedAt: null,
|
2026-02-20 10:39:45 +01:00
|
|
|
modifiedAt: stats.mtimeMs,
|
|
|
|
|
createdAt,
|
|
|
|
|
fileSize: stats.size,
|
|
|
|
|
snippet,
|
2026-03-06 21:52:40 +01:00
|
|
|
wordCount: bodyText.split(/\s+/).filter(Boolean).length,
|
2026-02-20 10:39:45 +01:00
|
|
|
relationships,
|
2026-03-06 21:52:40 +01:00
|
|
|
icon: getString('icon'),
|
|
|
|
|
color: getString('color'),
|
|
|
|
|
order: fm.order != null ? Number(fm.order) : null,
|
|
|
|
|
sidebarLabel: getString('sidebar label', 'sidebar_label'),
|
|
|
|
|
template: getString('template'),
|
|
|
|
|
sort: getString('sort'),
|
|
|
|
|
view: getString('view'),
|
|
|
|
|
visible: getBool('visible'),
|
|
|
|
|
outgoingLinks: [],
|
|
|
|
|
properties: {},
|
2026-02-20 10:39:45 +01:00
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Recursively find all .md files under a directory. */
|
|
|
|
|
function findMarkdownFiles(dir: string): string[] {
|
|
|
|
|
const results: string[] = []
|
|
|
|
|
try {
|
|
|
|
|
const items = fs.readdirSync(dir, { withFileTypes: true })
|
|
|
|
|
for (const item of items) {
|
|
|
|
|
if (item.name.startsWith('.')) continue
|
|
|
|
|
const full = path.join(dir, item.name)
|
|
|
|
|
if (item.isDirectory()) {
|
|
|
|
|
results.push(...findMarkdownFiles(full))
|
|
|
|
|
} else if (item.name.endsWith('.md')) {
|
|
|
|
|
results.push(full)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch {
|
|
|
|
|
// skip unreadable dirs
|
|
|
|
|
}
|
|
|
|
|
return results
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function vaultApiPlugin(): Plugin {
|
|
|
|
|
return {
|
|
|
|
|
name: 'vault-api',
|
|
|
|
|
configureServer(server) {
|
test: add real filesystem vault integration tests
Replace mock vault approach with real filesystem I/O for integration
tests. Each test copies tests/fixtures/test-vault/ to a temp directory,
overrides mock handlers to point at it, and verifies actual file
operations through the vite dev server middleware.
Tests cover: vault loading, archive/trash filtering, note creation,
rename with filesystem update, wikilink cascade on rename, relationship
display, and real file content loading.
Also extends vite.config.ts vault API middleware with write endpoints
(save, rename, delete, search, entry) and fixes getBool to handle
YAML 1.2 string values like "Yes"/"yes" (js-yaml 4.x compatibility).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:46:45 +01:00
|
|
|
server.middlewares.use(async (req, res, next) => {
|
2026-02-20 10:39:45 +01:00
|
|
|
const url = new URL(req.url ?? '/', `http://${req.headers.host}`)
|
|
|
|
|
|
|
|
|
|
if (url.pathname === '/api/vault/ping') {
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify({ ok: true }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.pathname === '/api/vault/list') {
|
|
|
|
|
const dirPath = url.searchParams.get('path')
|
|
|
|
|
if (!dirPath || !fs.existsSync(dirPath)) {
|
|
|
|
|
res.statusCode = 400
|
|
|
|
|
res.end(JSON.stringify({ error: 'Invalid or missing path' }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const files = findMarkdownFiles(dirPath)
|
|
|
|
|
const entries = files.map(parseMarkdownFile).filter(Boolean)
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify(entries))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.pathname === '/api/vault/content') {
|
|
|
|
|
const filePath = url.searchParams.get('path')
|
|
|
|
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
|
|
|
res.statusCode = 400
|
|
|
|
|
res.end(JSON.stringify({ error: 'Invalid or missing path' }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const content = fs.readFileSync(filePath, 'utf-8')
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify({ content }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.pathname === '/api/vault/all-content') {
|
|
|
|
|
const dirPath = url.searchParams.get('path')
|
|
|
|
|
if (!dirPath || !fs.existsSync(dirPath)) {
|
|
|
|
|
res.statusCode = 400
|
|
|
|
|
res.end(JSON.stringify({ error: 'Invalid or missing path' }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const files = findMarkdownFiles(dirPath)
|
|
|
|
|
const contentMap: Record<string, string> = {}
|
|
|
|
|
for (const f of files) {
|
|
|
|
|
try {
|
|
|
|
|
contentMap[f] = fs.readFileSync(f, 'utf-8')
|
|
|
|
|
} catch {
|
|
|
|
|
// skip unreadable files
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify(contentMap))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
test: add real filesystem vault integration tests
Replace mock vault approach with real filesystem I/O for integration
tests. Each test copies tests/fixtures/test-vault/ to a temp directory,
overrides mock handlers to point at it, and verifies actual file
operations through the vite dev server middleware.
Tests cover: vault loading, archive/trash filtering, note creation,
rename with filesystem update, wikilink cascade on rename, relationship
display, and real file content loading.
Also extends vite.config.ts vault API middleware with write endpoints
(save, rename, delete, search, entry) and fixes getBool to handle
YAML 1.2 string values like "Yes"/"yes" (js-yaml 4.x compatibility).
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-11 20:46:45 +01:00
|
|
|
if (url.pathname === '/api/vault/entry') {
|
|
|
|
|
const filePath = url.searchParams.get('path')
|
|
|
|
|
if (!filePath || !fs.existsSync(filePath)) {
|
|
|
|
|
res.statusCode = 400
|
|
|
|
|
res.end(JSON.stringify({ error: 'Invalid or missing path' }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const entry = parseMarkdownFile(filePath)
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify(entry))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.pathname === '/api/vault/search') {
|
|
|
|
|
const vaultPath = url.searchParams.get('vault_path')
|
|
|
|
|
const query = (url.searchParams.get('query') ?? '').toLowerCase()
|
|
|
|
|
const mode = url.searchParams.get('mode') ?? 'all'
|
|
|
|
|
if (!vaultPath || !query) {
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify({ results: [], elapsed_ms: 0, query, mode }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
const files = findMarkdownFiles(vaultPath)
|
|
|
|
|
const results: { title: string; path: string; snippet: string; score: number; note_type: string | null }[] = []
|
|
|
|
|
for (const f of files) {
|
|
|
|
|
const entry = parseMarkdownFile(f)
|
|
|
|
|
if (!entry || entry.trashed) continue
|
|
|
|
|
const raw = fs.readFileSync(f, 'utf-8')
|
|
|
|
|
if (entry.title.toLowerCase().includes(query) || raw.toLowerCase().includes(query)) {
|
|
|
|
|
results.push({ title: entry.title, path: entry.path, snippet: entry.snippet, score: 1.0, note_type: entry.isA })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify({ results: results.slice(0, 20), elapsed_ms: 1, query, mode }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- POST endpoints for write operations ---
|
|
|
|
|
|
|
|
|
|
if (url.pathname === '/api/vault/save' && req.method === 'POST') {
|
|
|
|
|
try {
|
|
|
|
|
const body = await readRequestBody(req)
|
|
|
|
|
const { path: filePath, content } = JSON.parse(body)
|
|
|
|
|
if (!filePath || content === undefined) {
|
|
|
|
|
res.statusCode = 400
|
|
|
|
|
res.end(JSON.stringify({ error: 'Missing path or content' }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
fs.mkdirSync(path.dirname(filePath), { recursive: true })
|
|
|
|
|
fs.writeFileSync(filePath, content, 'utf-8')
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end('null')
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
res.statusCode = 500
|
|
|
|
|
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Save failed' }))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.pathname === '/api/vault/rename' && req.method === 'POST') {
|
|
|
|
|
try {
|
|
|
|
|
const body = await readRequestBody(req)
|
|
|
|
|
const { vault_path: vaultPath, old_path: oldPath, new_title: newTitle } = JSON.parse(body)
|
|
|
|
|
const oldContent = fs.readFileSync(oldPath, 'utf-8')
|
|
|
|
|
const h1Match = oldContent.match(/^# (.+)$/m)
|
|
|
|
|
const oldTitle = h1Match ? h1Match[1].trim() : ''
|
|
|
|
|
const slug = newTitle.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/(^-|-$)/g, '')
|
|
|
|
|
const parentDir = path.dirname(oldPath)
|
|
|
|
|
const newPath = path.join(parentDir, `${slug}.md`)
|
|
|
|
|
const newContent = oldContent.replace(/^# .+$/m, `# ${newTitle}`)
|
|
|
|
|
fs.writeFileSync(newPath, newContent, 'utf-8')
|
|
|
|
|
if (newPath !== oldPath) fs.unlinkSync(oldPath)
|
|
|
|
|
let updatedFiles = 0
|
|
|
|
|
if (oldTitle && vaultPath) {
|
|
|
|
|
const allFiles = findMarkdownFiles(vaultPath)
|
|
|
|
|
const escaped = oldTitle.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
|
|
|
const pattern = new RegExp(`\\[\\[${escaped}(\\|[^\\]]*?)?\\]\\]`, 'g')
|
|
|
|
|
for (const f of allFiles) {
|
|
|
|
|
if (f === newPath) continue
|
|
|
|
|
try {
|
|
|
|
|
const c = fs.readFileSync(f, 'utf-8')
|
|
|
|
|
const replaced = c.replace(pattern, (_m: string, pipe: string | undefined) =>
|
|
|
|
|
pipe ? `[[${newTitle}${pipe}]]` : `[[${newTitle}]]`
|
|
|
|
|
)
|
|
|
|
|
if (replaced !== c) { fs.writeFileSync(f, replaced, 'utf-8'); updatedFiles++ }
|
|
|
|
|
} catch { /* skip */ }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify({ new_path: newPath, updated_files: updatedFiles }))
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
res.statusCode = 500
|
|
|
|
|
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Rename failed' }))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url.pathname === '/api/vault/delete' && req.method === 'POST') {
|
|
|
|
|
try {
|
|
|
|
|
const body = await readRequestBody(req)
|
|
|
|
|
const { path: filePath } = JSON.parse(body)
|
|
|
|
|
if (!filePath) {
|
|
|
|
|
res.statusCode = 400
|
|
|
|
|
res.end(JSON.stringify({ error: 'Missing path' }))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
fs.unlinkSync(filePath)
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify(filePath))
|
|
|
|
|
} catch (err: unknown) {
|
|
|
|
|
res.statusCode = 500
|
|
|
|
|
res.end(JSON.stringify({ error: err instanceof Error ? err.message : 'Delete failed' }))
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 10:39:45 +01:00
|
|
|
next()
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-02-14 18:20:07 +01:00
|
|
|
|
2026-03-29 15:05:56 +02:00
|
|
|
// --- Proxy helpers ---
|
2026-02-20 22:52:56 +01:00
|
|
|
|
2026-02-22 13:10:26 +01:00
|
|
|
function readRequestBody(req: import('http').IncomingMessage): Promise<string> {
|
2026-02-20 22:52:56 +01:00
|
|
|
return new Promise((resolve) => {
|
|
|
|
|
let body = ''
|
|
|
|
|
req.on('data', (chunk: Buffer) => { body += chunk.toString() })
|
|
|
|
|
req.on('end', () => resolve(body))
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-20 22:59:00 +01:00
|
|
|
/** WebSocket proxy info endpoint — tells the frontend where the MCP bridge is */
|
|
|
|
|
function mcpBridgeInfoPlugin(): Plugin {
|
|
|
|
|
return {
|
|
|
|
|
name: 'mcp-bridge-info',
|
|
|
|
|
configureServer(server) {
|
|
|
|
|
server.middlewares.use((req, res, next) => {
|
|
|
|
|
if (req.url !== '/api/mcp/info') return next()
|
|
|
|
|
res.setHeader('Content-Type', 'application/json')
|
|
|
|
|
res.end(JSON.stringify({
|
|
|
|
|
wsUrl: `ws://localhost:${process.env.MCP_WS_PORT || 9710}`,
|
|
|
|
|
available: true,
|
|
|
|
|
}))
|
|
|
|
|
})
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-14 18:20:07 +01:00
|
|
|
// https://vite.dev/config/
|
|
|
|
|
export default defineConfig({
|
2026-03-29 15:05:56 +02:00
|
|
|
plugins: [react(), tailwindcss(), vaultApiPlugin(), mcpBridgeInfoPlugin()],
|
2026-02-16 16:56:44 +01:00
|
|
|
|
|
|
|
|
resolve: {
|
|
|
|
|
alias: {
|
|
|
|
|
'@': path.resolve(__dirname, './src'),
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-02-14 18:20:07 +01:00
|
|
|
|
2026-04-06 14:31:04 +02:00
|
|
|
// Inject the demo-vault-v2 path in local dev only — production Tauri builds and
|
|
|
|
|
// CI must resolve the default vault path at runtime via the backend to avoid
|
|
|
|
|
// baking the CI runner's absolute path into the distributed bundle.
|
2026-03-17 13:14:56 +01:00
|
|
|
define: {
|
2026-04-06 14:31:04 +02:00
|
|
|
...(process.env.CI || (process.env.TAURI_PLATFORM && !process.env.TAURI_DEBUG)
|
2026-04-04 10:53:29 +02:00
|
|
|
? {}
|
|
|
|
|
: { __DEMO_VAULT_PATH__: JSON.stringify(path.resolve(__dirname, 'demo-vault-v2')) }),
|
2026-03-17 13:14:56 +01:00
|
|
|
},
|
|
|
|
|
|
2026-02-14 18:20:07 +01:00
|
|
|
// Prevent vite from obscuring Rust errors
|
|
|
|
|
clearScreen: false,
|
|
|
|
|
|
|
|
|
|
// Tauri expects a fixed port
|
|
|
|
|
server: {
|
2026-03-02 23:20:49 +01:00
|
|
|
port: 5202,
|
2026-02-14 18:20:07 +01:00
|
|
|
strictPort: true,
|
2026-02-16 14:04:15 +01:00
|
|
|
allowedHosts: true,
|
2026-02-14 18:20:07 +01:00
|
|
|
},
|
|
|
|
|
|
|
|
|
|
// Env variables starting with TAURI_ are exposed to the frontend
|
|
|
|
|
envPrefix: ['VITE_', 'TAURI_'],
|
|
|
|
|
|
|
|
|
|
build: {
|
|
|
|
|
// Tauri uses Chromium on Windows and WebKit on macOS/Linux
|
|
|
|
|
target: process.env.TAURI_PLATFORM === 'windows' ? 'chrome105' : 'safari13',
|
|
|
|
|
// Don't minify for debug builds
|
|
|
|
|
minify: !process.env.TAURI_DEBUG ? 'esbuild' : false,
|
|
|
|
|
// Produce sourcemaps for debug builds
|
|
|
|
|
sourcemap: !!process.env.TAURI_DEBUG,
|
|
|
|
|
},
|
|
|
|
|
|
|
|
|
|
test: {
|
|
|
|
|
globals: true,
|
|
|
|
|
environment: 'jsdom',
|
|
|
|
|
setupFiles: ['./src/test/setup.ts'],
|
|
|
|
|
include: ['src/**/*.{test,spec}.{ts,tsx}'],
|
2026-02-17 13:10:43 +01:00
|
|
|
coverage: {
|
|
|
|
|
provider: 'v8',
|
|
|
|
|
reporter: ['text', 'json', 'html'],
|
|
|
|
|
include: ['src/**/*.{ts,tsx}'],
|
|
|
|
|
exclude: [
|
|
|
|
|
'src/**/*.{test,spec}.{ts,tsx}',
|
|
|
|
|
'src/test/**',
|
|
|
|
|
'src/mock-tauri.ts',
|
|
|
|
|
'src/main.tsx',
|
refactor: export pure functions from useNoteActions + update coverage config
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:02:07 +01:00
|
|
|
'src/types.ts',
|
|
|
|
|
'src/hooks/useMcpBridge.ts',
|
2026-02-28 22:21:42 +01:00
|
|
|
'src/hooks/useAiAgent.ts',
|
refactor: export pure functions from useNoteActions + update coverage config
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:02:07 +01:00
|
|
|
'src/utils/ai-chat.ts',
|
2026-02-28 22:21:42 +01:00
|
|
|
'src/utils/ai-agent.ts',
|
refactor: export pure functions from useNoteActions + update coverage config
Export buildNewEntry, slugify, entryMatchesTarget, buildNoteContent,
resolveNewNote, resolveNewType as public functions for direct unit testing.
Add coverage exclusions for untestable files: shadcn/ui primitives,
AI modules (useAIChat, useMcpBridge, ai-chat, AIChatPanel), and types.ts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-22 12:02:07 +01:00
|
|
|
'src/components/ui/dropdown-menu.tsx',
|
|
|
|
|
'src/components/ui/scroll-area.tsx',
|
|
|
|
|
'src/components/ui/select.tsx',
|
|
|
|
|
'src/components/ui/separator.tsx',
|
|
|
|
|
'src/components/ui/tabs.tsx',
|
|
|
|
|
'src/components/ui/tooltip.tsx',
|
|
|
|
|
'src/components/ui/card.tsx',
|
2026-02-17 13:10:43 +01:00
|
|
|
],
|
|
|
|
|
thresholds: {
|
|
|
|
|
lines: 70,
|
|
|
|
|
functions: 70,
|
|
|
|
|
branches: 70,
|
|
|
|
|
statements: 70,
|
|
|
|
|
},
|
|
|
|
|
},
|
2026-02-14 18:20:07 +01:00
|
|
|
},
|
|
|
|
|
})
|