131 lines
3.4 KiB
TypeScript
131 lines
3.4 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
import type { WikiPage, WikiPageSummary } from "@/lib/types";
|
|
|
|
const WIKI_DIR = process.env.WIKI_DIR || path.join(process.cwd(), "wiki");
|
|
|
|
fs.mkdirSync(WIKI_DIR, { recursive: true });
|
|
|
|
function assertSafeFilename(filename: string) {
|
|
if (filename.includes("..") || filename.includes("/") || filename.includes("\\")) {
|
|
throw new Error("invalid_filename");
|
|
}
|
|
}
|
|
|
|
function extractMetadata(content: string) {
|
|
const metadata = {
|
|
title: "",
|
|
created: "",
|
|
modified: "",
|
|
tags: [] as string[],
|
|
};
|
|
|
|
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
if (frontmatterMatch) {
|
|
const frontmatter = frontmatterMatch[1];
|
|
const titleMatch = frontmatter.match(/title:\s*(.+)/i);
|
|
const tagsMatch = frontmatter.match(/tags:\s*\[(.+)\]/i);
|
|
if (titleMatch) {
|
|
metadata.title = titleMatch[1].trim();
|
|
}
|
|
if (tagsMatch) {
|
|
metadata.tags = tagsMatch[1].split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
}
|
|
}
|
|
|
|
if (!metadata.title) {
|
|
const headingMatch = content.match(/^#\s+(.+)$/m);
|
|
if (headingMatch) {
|
|
metadata.title = headingMatch[1].trim();
|
|
}
|
|
}
|
|
|
|
return metadata;
|
|
}
|
|
|
|
export function listWikiPages(): WikiPageSummary[] {
|
|
if (!fs.existsSync(WIKI_DIR)) {
|
|
return [];
|
|
}
|
|
|
|
return fs
|
|
.readdirSync(WIKI_DIR)
|
|
.filter((fileName) => fileName.endsWith(".md"))
|
|
.map((filename) => {
|
|
const filePath = path.join(WIKI_DIR, filename);
|
|
const stats = fs.statSync(filePath);
|
|
const content = fs.readFileSync(filePath, "utf8");
|
|
const metadata = extractMetadata(content);
|
|
return {
|
|
filename,
|
|
title: metadata.title || filename.replace(".md", "").replace(/-/g, " "),
|
|
created: stats.birthtime.toISOString(),
|
|
modified: stats.mtime.toISOString(),
|
|
tags: metadata.tags,
|
|
};
|
|
})
|
|
.sort((left, right) => new Date(right.modified).getTime() - new Date(left.modified).getTime());
|
|
}
|
|
|
|
export function readWikiPage(filename: string): WikiPage | null {
|
|
assertSafeFilename(filename);
|
|
const filePath = path.join(WIKI_DIR, filename);
|
|
|
|
if (!fs.existsSync(filePath)) {
|
|
return null;
|
|
}
|
|
|
|
const content = fs.readFileSync(filePath, "utf8");
|
|
const stats = fs.statSync(filePath);
|
|
const metadata = extractMetadata(content);
|
|
|
|
return {
|
|
filename,
|
|
content,
|
|
metadata: {
|
|
title: metadata.title || filename.replace(".md", ""),
|
|
created: stats.birthtime.toISOString(),
|
|
modified: stats.mtime.toISOString(),
|
|
tags: metadata.tags,
|
|
},
|
|
};
|
|
}
|
|
|
|
export function createWikiPage(title: string) {
|
|
const safeTitle = title
|
|
.toLowerCase()
|
|
.replace(/[^a-z0-9]+/g, "-")
|
|
.replace(/^-+|-+$/g, "")
|
|
.slice(0, 80);
|
|
|
|
const timestamp = new Date().toISOString().slice(0, 10);
|
|
let filename = `${timestamp}-${safeTitle}.md`;
|
|
let counter = 1;
|
|
|
|
while (fs.existsSync(path.join(WIKI_DIR, filename))) {
|
|
filename = `${timestamp}-${safeTitle}-${counter}.md`;
|
|
counter += 1;
|
|
}
|
|
|
|
const content = `# ${title}
|
|
|
|
## Summary
|
|
|
|
Document the architecture, deployment notes, or runbook here.
|
|
`;
|
|
|
|
fs.writeFileSync(path.join(WIKI_DIR, filename), content, "utf8");
|
|
return filename;
|
|
}
|
|
|
|
export function updateWikiPage(filename: string, content: string) {
|
|
assertSafeFilename(filename);
|
|
fs.writeFileSync(path.join(WIKI_DIR, filename), content, "utf8");
|
|
}
|
|
|
|
export function deleteWikiPage(filename: string) {
|
|
assertSafeFilename(filename);
|
|
fs.unlinkSync(path.join(WIKI_DIR, filename));
|
|
}
|