@vibeship/devtools
Version:
Comprehensive markdown-based project management system with AI capabilities for Next.js applications
1,406 lines (1,398 loc) • 38 kB
JavaScript
// src/server/file-scanner.ts
import * as fs2 from "fs/promises";
import * as path2 from "path";
import { glob } from "glob";
// src/server/security/path-validator.ts
import * as path from "path";
import * as fs from "fs";
var PathValidator = class {
constructor(allowedPaths, options = {}) {
this.allowedPaths = allowedPaths;
this.options = {
allowSymlinks: false,
maxDepth: 20,
blockPatterns: [
/\.git\//,
/node_modules\//,
/\.env/,
/\.ssh\//,
/\.aws\//
],
...options
};
this.normalizedAllowedPaths = allowedPaths.map(
(p) => path.normalize(path.resolve(p))
);
}
/**
* Validate if a path is safe and allowed
*/
isValid(filePath) {
if (!filePath || typeof filePath !== "string") {
return false;
}
try {
if (this.hasPathTraversal(filePath)) {
return false;
}
const normalizedPath = path.normalize(path.resolve(filePath));
if (this.isBlockedPattern(normalizedPath)) {
return false;
}
const isWithinAllowed = this.normalizedAllowedPaths.some(
(allowedPath) => normalizedPath.startsWith(allowedPath)
);
if (!isWithinAllowed) {
return false;
}
if (!this.options.allowSymlinks && fs.existsSync(normalizedPath)) {
try {
const stats = fs.lstatSync(normalizedPath);
if (stats.isSymbolicLink()) {
return false;
}
} catch {
return false;
}
}
if (this.options.maxDepth) {
const depth = normalizedPath.split(path.sep).length;
const maxAllowedDepth = Math.max(
...this.normalizedAllowedPaths.map((p) => p.split(path.sep).length)
) + this.options.maxDepth;
if (depth > maxAllowedDepth) {
return false;
}
}
return true;
} catch {
return false;
}
}
/**
* Check for path traversal attempts
*/
hasPathTraversal(filePath) {
const dangerous = [
"../",
"..\\",
"%2e%2e%2f",
"%2e%2e/",
"..%2f",
"%2e%2e\\",
"..%5c",
"%2e%2e%5c",
"..\\..\\",
"../.."
];
const lowerPath = filePath.toLowerCase();
return dangerous.some((pattern) => lowerPath.includes(pattern));
}
/**
* Check if path matches blocked patterns
*/
isBlockedPattern(filePath) {
return this.options.blockPatterns?.some(
(pattern) => pattern.test(filePath)
) || false;
}
/**
* Sanitize a file path for safe usage
*/
sanitize(filePath) {
if (!filePath || typeof filePath !== "string") {
return "";
}
let sanitized = filePath.replace(/\0/g, "").replace(/[\x00-\x1F\x7F]/g, "");
sanitized = sanitized.replace(/\/+/g, "/").replace(/\\+/g, "\\");
sanitized = sanitized.replace(/[\s.]+$/g, "");
return path.normalize(sanitized);
}
/**
* Get the relative path from allowed base
*/
getRelativePath(filePath) {
const normalizedPath = path.normalize(path.resolve(filePath));
for (const allowedPath of this.normalizedAllowedPaths) {
if (normalizedPath.startsWith(allowedPath)) {
return path.relative(allowedPath, normalizedPath);
}
}
return null;
}
/**
* Add a new allowed path
*/
addAllowedPath(newPath) {
const normalized = path.normalize(path.resolve(newPath));
if (!this.normalizedAllowedPaths.includes(normalized)) {
this.allowedPaths.push(newPath);
this.normalizedAllowedPaths.push(normalized);
}
}
/**
* Remove an allowed path
*/
removeAllowedPath(removePath) {
const normalized = path.normalize(path.resolve(removePath));
const index = this.normalizedAllowedPaths.indexOf(normalized);
if (index !== -1) {
this.allowedPaths.splice(index, 1);
this.normalizedAllowedPaths.splice(index, 1);
}
}
};
// src/server/file-scanner.ts
var FileScanner = class {
constructor(config) {
this.config = config;
this.pathValidator = new PathValidator(config.scanPaths);
}
/**
* Scan for files matching the configured patterns
*/
async scan(options) {
const files = [];
const scanPaths = options?.paths || this.config.scanPaths;
const includePatterns = options?.include || this.config.include;
const excludePatterns = options?.exclude || this.config.exclude;
let totalScanned = 0;
const totalPaths = scanPaths.length;
for (const scanPath of scanPaths) {
if (!this.pathValidator.isValid(scanPath)) {
throw new Error(`Invalid scan path: ${scanPath}`);
}
try {
const stats = await fs2.stat(scanPath).catch(() => null);
if (stats?.isFile()) {
const fileName = path2.basename(scanPath);
const shouldInclude = includePatterns.some((pattern) => {
if (pattern.includes("*")) {
const ext = pattern.replace("**/", "").replace("*", "");
return fileName.endsWith(ext);
}
return fileName === pattern;
});
if (shouldInclude) {
files.push(path2.resolve(scanPath));
}
} else if (stats?.isDirectory()) {
for (const pattern of includePatterns) {
const matches = await glob(pattern, {
cwd: scanPath,
ignore: excludePatterns,
absolute: true,
nodir: true,
dot: true
});
files.push(...matches);
}
} else {
const matches = await glob(scanPath, {
ignore: excludePatterns,
absolute: true,
nodir: true,
dot: true
});
for (const match of matches) {
const shouldInclude = includePatterns.some((pattern) => {
const ext = pattern.replace("**/", "").replace("*", "");
return match.endsWith(ext);
});
if (shouldInclude) {
files.push(match);
}
}
}
} catch (error) {
console.warn(`Failed to scan path ${scanPath}:`, error);
}
totalScanned++;
if (this.progressCallback) {
this.progressCallback({
current: totalScanned,
total: totalPaths,
currentPath: scanPath,
filesFound: files.length
});
}
}
return [...new Set(files)];
}
/**
* Scan with detailed file information
*/
async scanWithInfo(options) {
const filePaths = await this.scan(options);
const fileInfos = [];
for (const filePath of filePaths) {
try {
const stats = await fs2.stat(filePath);
const content = await this.readFile(filePath);
fileInfos.push({
path: filePath,
content,
stats: {
size: stats.size,
modified: stats.mtime
}
});
} catch (error) {
console.warn(`Failed to read file ${filePath}:`, error);
}
}
return fileInfos;
}
/**
* Read a file with validation
*/
async readFile(filePath) {
if (!this.pathValidator.isValid(filePath)) {
throw new Error(`Access denied: ${filePath}`);
}
try {
return await fs2.readFile(filePath, "utf-8");
} catch (error) {
if (error.code === "ENOENT") {
throw new Error(`File not found: ${filePath}`);
}
if (error.code === "EACCES") {
throw new Error(`Permission denied: ${filePath}`);
}
throw error;
}
}
/**
* Check if a file exists
*/
async exists(filePath) {
try {
await fs2.access(filePath);
return true;
} catch {
return false;
}
}
/**
* Get file stats
*/
async getStats(filePath) {
if (!this.pathValidator.isValid(filePath)) {
throw new Error(`Access denied: ${filePath}`);
}
return fs2.stat(filePath);
}
/**
* Set progress callback for long operations
*/
onProgress(callback) {
this.progressCallback = callback;
}
/**
* Get supported file extensions from include patterns
*/
getSupportedExtensions() {
const extensions = /* @__PURE__ */ new Set();
this.config.include.forEach((pattern) => {
const match = pattern.match(/\*\.(\w+)$/);
if (match) {
extensions.add(`.${match[1]}`);
}
});
return Array.from(extensions);
}
};
// src/server/task-extractor.ts
var TaskExtractor = class {
constructor() {
this.defaultPatterns = {
TODO: { pattern: /TODO[:\s]+(.+)/gi, priority: "medium" },
FIXME: { pattern: /FIXME[:\s]+(.+)/gi, priority: "high" },
HACK: { pattern: /HACK[:\s]+(.+)/gi, priority: "low" },
NOTE: { pattern: /NOTE[:\s]+(.+)/gi, priority: "low" },
BUG: { pattern: /BUG[:\s]+(.+)/gi, priority: "high" },
OPTIMIZE: { pattern: /OPTIMIZE[:\s]+(.+)/gi, priority: "medium" },
REFACTOR: { pattern: /REFACTOR[:\s]+(.+)/gi, priority: "medium" }
};
}
/**
* Extract tasks from content
*/
extract(content, filePath, options) {
const tasks = [];
const lines = content.split("\n");
const patterns = options?.customPatterns || this.defaultPatterns;
lines.forEach((line, lineIndex) => {
Object.entries(patterns).forEach(([type, config]) => {
const matches = [...line.matchAll(config.pattern)];
matches.forEach((match) => {
if (match.index !== void 0) {
const task = {
id: `${filePath}:${lineIndex + 1}:${match.index}`,
type,
text: match[1].trim(),
file: filePath,
line: lineIndex + 1,
column: match.index + 1,
priority: config.priority
};
if (options?.parseMetadata) {
const metadata = this.extractMetadata(task.text);
task.text = metadata.text;
task.assignee = metadata.assignee;
task.date = metadata.date;
task.priority = metadata.priority || task.priority;
task.metadata = metadata.extra;
}
if (options?.includeContext) {
const contextLines = options.contextLines || 2;
task.context = this.getContext(lines, lineIndex, contextLines);
}
tasks.push(task);
}
});
});
});
return tasks;
}
/**
* Extract metadata from task text
* Format: TODO(assignee): text [priority:high] [due:2024-01-01] {key:value}
*/
extractMetadata(text) {
let cleanText = text;
const metadata = { extra: {} };
const assigneeMatch = text.match(/^\(([^)]+)\):\s*/);
if (assigneeMatch) {
metadata.assignee = assigneeMatch[1];
cleanText = cleanText.replace(assigneeMatch[0], "");
}
const priorityMatch = cleanText.match(/\[priority:(low|medium|high)\]/i);
if (priorityMatch) {
metadata.priority = priorityMatch[1].toLowerCase();
cleanText = cleanText.replace(priorityMatch[0], "");
}
const dateMatch = cleanText.match(/\[due:(\d{4}-\d{2}-\d{2})\]/);
if (dateMatch) {
metadata.date = dateMatch[1];
cleanText = cleanText.replace(dateMatch[0], "");
}
const metadataMatches = [...cleanText.matchAll(/\{([^:}]+):([^}]+)\}/g)];
metadataMatches.forEach((match) => {
metadata.extra[match[1]] = match[2];
cleanText = cleanText.replace(match[0], "");
});
return {
text: cleanText.trim(),
...metadata
};
}
/**
* Get context lines around a task
*/
getContext(lines, lineIndex, contextLines) {
const start = Math.max(0, lineIndex - contextLines);
const end = Math.min(lines.length, lineIndex + contextLines + 1);
return lines.slice(start, end).map((line, idx) => {
const actualLine = start + idx;
const prefix = actualLine === lineIndex ? ">" : " ";
return `${prefix} ${actualLine + 1}: ${line}`;
}).join("\n");
}
/**
* Extract tasks from multiple files
*/
async extractFromFiles(files, options) {
const allTasks = [];
for (const file of files) {
const tasks = this.extract(file.content, file.path, options);
allTasks.push(...tasks);
}
return allTasks;
}
/**
* Group tasks by type
*/
groupByType(tasks) {
return tasks.reduce((groups, task) => {
const type = task.type;
if (!groups[type]) {
groups[type] = [];
}
groups[type].push(task);
return groups;
}, {});
}
/**
* Group tasks by file
*/
groupByFile(tasks) {
return tasks.reduce((groups, task) => {
const file = task.file;
if (!groups[file]) {
groups[file] = [];
}
groups[file].push(task);
return groups;
}, {});
}
/**
* Filter tasks by priority
*/
filterByPriority(tasks, priority) {
return tasks.filter((task) => task.priority === priority);
}
/**
* Sort tasks by various criteria
*/
sortTasks(tasks, by) {
const sorted = [...tasks];
switch (by) {
case "priority":
const priorityOrder = { high: 0, medium: 1, low: 2, undefined: 3 };
sorted.sort(
(a, b) => (priorityOrder[a.priority || "undefined"] || 3) - (priorityOrder[b.priority || "undefined"] || 3)
);
break;
case "type":
sorted.sort((a, b) => a.type.localeCompare(b.type));
break;
case "file":
sorted.sort((a, b) => a.file.localeCompare(b.file));
break;
case "line":
sorted.sort((a, b) => {
const fileCompare = a.file.localeCompare(b.file);
return fileCompare !== 0 ? fileCompare : a.line - b.line;
});
break;
}
return sorted;
}
};
// src/server/markdown-parser.ts
import matter from "gray-matter";
import * as crypto from "crypto";
var MarkdownParser = class {
constructor() {
this.defaultOptions = {
excerpt: true,
excerptSeparator: "<!-- more -->",
generateTOC: true,
calculateStats: true,
extractSections: false
};
}
/**
* Parse markdown content with frontmatter
*/
parse(content, options) {
const opts = { ...this.defaultOptions, ...options };
const { content: markdownContent, data, excerpt } = matter(content, {
excerpt: opts.excerpt,
excerpt_separator: opts.excerptSeparator
});
const result = {
content: markdownContent,
data,
excerpt
};
if (opts.generateTOC) {
result.toc = this.generateTableOfContents(markdownContent);
}
if (opts.calculateStats) {
const stats = this.calculateStats(markdownContent);
result.wordCount = stats.wordCount;
result.readingTime = stats.readingTime;
}
return result;
}
/**
* Extract all headings with hierarchy
*/
extractHeadings(content) {
const headingPattern = /^(#{1,6})\s+(.+)$/gm;
const headings = [];
let match;
while ((match = headingPattern.exec(content)) !== null) {
const level = match[1].length;
const text = match[2].trim();
const id = this.createSlug(text);
headings.push({ text, level, id });
}
return headings;
}
/**
* Generate table of contents with nested structure
*/
generateTableOfContents(content) {
const headings = this.extractHeadings(content);
const toc = [];
const stack = [];
headings.forEach((heading) => {
const item = {
id: heading.id,
text: heading.text,
level: heading.level,
children: []
};
while (stack.length > 0 && stack[stack.length - 1].level >= heading.level) {
stack.pop();
}
if (stack.length === 0) {
toc.push(item);
} else {
stack[stack.length - 1].children.push(item);
}
stack.push(item);
});
return toc;
}
/**
* Extract sections by headings
*/
extractSections(content) {
const lines = content.split("\n");
const sections = [];
let currentSection = null;
let sectionContent = [];
lines.forEach((line) => {
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
if (headingMatch) {
if (currentSection) {
currentSection.content = sectionContent.join("\n").trim();
sections.push(currentSection);
}
const level = headingMatch[1].length;
const title = headingMatch[2].trim();
currentSection = {
id: this.createSlug(title),
title,
content: "",
level
};
sectionContent = [];
} else if (currentSection) {
sectionContent.push(line);
}
});
if (currentSection) {
currentSection.content = sectionContent.join("\n").trim();
sections.push(currentSection);
}
return sections;
}
/**
* Extract links from markdown
*/
extractLinks(content) {
const links = [];
const inlineLinkPattern = /\[([^\]]+)\]\(([^)]+?)(?:\s+"([^"]+)")?\)/g;
let match;
while ((match = inlineLinkPattern.exec(content)) !== null) {
links.push({
text: match[1],
url: match[2],
title: match[3]
});
}
const refLinkPattern = /\[([^\]]+)\]\[([^\]]+)\]/g;
const refDefinitions = this.extractReferenceDefinitions(content);
while ((match = refLinkPattern.exec(content)) !== null) {
const ref = match[2];
if (refDefinitions[ref]) {
links.push({
text: match[1],
url: refDefinitions[ref].url,
title: refDefinitions[ref].title
});
}
}
return links;
}
/**
* Extract reference link definitions
*/
extractReferenceDefinitions(content) {
const definitions = {};
const pattern = /^\[([^\]]+)\]:\s+(\S+)(?:\s+"([^"]+)")?$/gm;
let match;
while ((match = pattern.exec(content)) !== null) {
definitions[match[1]] = {
url: match[2],
title: match[3]
};
}
return definitions;
}
/**
* Extract code blocks
*/
extractCodeBlocks(content) {
const blocks = [];
const pattern = /```(\w+)?\n([\s\S]*?)```/g;
let match;
while ((match = pattern.exec(content)) !== null) {
blocks.push({
lang: match[1],
code: match[2].trim()
});
}
return blocks;
}
/**
* Calculate word count and reading time
*/
calculateStats(content) {
const withoutCode = content.replace(/```[\s\S]*?```/g, "");
const plainText = withoutCode.replace(/^#{1,6}\s+/gm, "").replace(/[*_~`]/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/^\s*[-*+]\s+/gm, "").replace(/^\s*\d+\.\s+/gm, "").replace(/^\s*>/gm, "");
const words = plainText.match(/\b\w+\b/g) || [];
const wordCount = words.length;
const readingTime = Math.ceil(wordCount / 200);
return { wordCount, readingTime };
}
/**
* Create URL-friendly slug from text
*/
createSlug(text) {
return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "");
}
/**
* Parse markdown table
*/
parseTables(content) {
const tables = [];
const lines = content.split("\n");
let i = 0;
while (i < lines.length) {
if (i + 1 < lines.length && lines[i].includes("|") && lines[i + 1].match(/^\s*\|?\s*:?-+:?\s*\|/)) {
const headers = lines[i].split("|").map((h) => h.trim()).filter((h) => h);
const rows = [];
i += 2;
while (i < lines.length && lines[i].includes("|")) {
const row = lines[i].split("|").map((cell) => cell.trim()).filter((cell) => cell);
if (row.length > 0) {
rows.push(row);
}
i++;
}
tables.push({ headers, rows });
} else {
i++;
}
}
return tables;
}
/**
* Convert markdown to plain text
*/
toPlainText(content) {
return content.replace(/```[\s\S]*?```/g, "").replace(/^#{1,6}\s+/gm, "").replace(/[*_~]/g, "").replace(/\[([^\]]+)\]\([^)]+\)/g, "$1").replace(/!\[([^\]]*)\]\([^)]+\)/g, "").replace(/^\s*[-*+]\s+/gm, "").replace(/^\s*\d+\.\s+/gm, "").replace(/^\s*>/gm, "").replace(/\n{3,}/g, "\n\n").trim();
}
/**
* Generate a content hash for caching
*/
generateHash(content) {
return crypto.createHash("sha256").update(content).digest("hex");
}
};
// src/server/security/rate-limiter.ts
var RateLimiter = class {
constructor(options) {
this.options = options;
this.requests = /* @__PURE__ */ new Map();
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, Math.min(options.windowMs, 6e4));
}
/**
* Check if a request is allowed
*/
isAllowed(identifier, context) {
const key = this.options.keyGenerator ? this.options.keyGenerator(context) : identifier;
const now = Date.now();
const record = this.requests.get(key) || { timestamps: [] };
if (record.blockedUntil && now < record.blockedUntil) {
return false;
}
record.timestamps = record.timestamps.filter(
(timestamp) => now - timestamp < this.options.windowMs
);
if (record.timestamps.length >= this.options.maxRequests) {
const oldestTimestamp = record.timestamps[0];
const resetTime = oldestTimestamp + this.options.windowMs;
const retryAfter = resetTime - now;
if (record.timestamps.length > this.options.maxRequests * 1.5) {
record.blockedUntil = now + this.options.windowMs * 2;
}
if (this.options.onLimitReached) {
const info = {
limit: this.options.maxRequests,
current: record.timestamps.length,
remaining: 0,
resetTime,
retryAfter: Math.ceil(retryAfter / 1e3)
};
this.options.onLimitReached(key, info);
}
return false;
}
record.timestamps.push(now);
this.requests.set(key, record);
return true;
}
/**
* Get current rate limit info for an identifier
*/
getInfo(identifier) {
const now = Date.now();
const record = this.requests.get(identifier) || { timestamps: [] };
const validTimestamps = record.timestamps.filter(
(timestamp) => now - timestamp < this.options.windowMs
);
const current = validTimestamps.length;
const remaining = Math.max(0, this.options.maxRequests - current);
let resetTime = now + this.options.windowMs;
let retryAfter = 0;
if (validTimestamps.length > 0) {
const oldestTimestamp = validTimestamps[0];
resetTime = oldestTimestamp + this.options.windowMs;
if (current >= this.options.maxRequests) {
retryAfter = Math.ceil((resetTime - now) / 1e3);
}
}
return {
limit: this.options.maxRequests,
current,
remaining,
resetTime,
retryAfter
};
}
/**
* Reset rate limit for an identifier
*/
reset(identifier) {
this.requests.delete(identifier);
}
/**
* Reset all rate limits
*/
resetAll() {
this.requests.clear();
}
/**
* Record a request result (for conditional limiting)
*/
recordResult(identifier, success) {
if (success && this.options.skipSuccessfulRequests) {
const record = this.requests.get(identifier);
if (record && record.timestamps.length > 0) {
record.timestamps.pop();
}
} else if (!success && this.options.skipFailedRequests) {
const record = this.requests.get(identifier);
if (record && record.timestamps.length > 0) {
record.timestamps.pop();
}
}
}
/**
* Clean up old entries
*/
cleanup() {
const now = Date.now();
const expiredKeys = [];
for (const [key, record] of this.requests.entries()) {
record.timestamps = record.timestamps.filter(
(timestamp) => now - timestamp < this.options.windowMs
);
if (record.timestamps.length === 0 && (!record.blockedUntil || now >= record.blockedUntil)) {
expiredKeys.push(key);
}
}
expiredKeys.forEach((key) => this.requests.delete(key));
}
/**
* Get statistics about current rate limiting
*/
getStats() {
const now = Date.now();
let blockedCount = 0;
let totalRequests = 0;
for (const record of this.requests.values()) {
const validTimestamps = record.timestamps.filter(
(timestamp) => now - timestamp < this.options.windowMs
);
totalRequests += validTimestamps.length;
if (validTimestamps.length >= this.options.maxRequests || record.blockedUntil && now < record.blockedUntil) {
blockedCount++;
}
}
return {
totalIdentifiers: this.requests.size,
blockedIdentifiers: blockedCount,
totalRequests
};
}
/**
* Destroy the rate limiter and clean up resources
*/
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
this.requests.clear();
}
};
// src/server/cache-manager.ts
import * as crypto2 from "crypto";
var CacheManager = class {
constructor(options) {
this.cache = /* @__PURE__ */ new Map();
this.stats = {
entries: 0,
hits: 0,
misses: 0,
evictions: 0,
size: 0
};
this.defaultOptions = {
ttl: 5 * 60 * 1e3,
// 5 minutes default
maxSize: 1e3,
updateOnGet: false
};
this.defaultOptions = { ...this.defaultOptions, ...options };
this.startCleanupInterval();
}
/**
* Set a value in the cache
*/
set(key, value, options) {
const opts = { ...this.defaultOptions, ...options };
const expiry = Date.now() + opts.ttl;
const size = this.estimateSize(value);
if (opts.maxSize && this.cache.size >= opts.maxSize) {
this.evictLRU();
}
const entry = {
value,
expiry,
size,
hits: 0,
lastAccess: Date.now()
};
this.cache.set(key, entry);
this.stats.entries = this.cache.size;
this.stats.size += size;
}
/**
* Get a value from the cache
*/
get(key, options) {
const entry = this.cache.get(key);
if (!entry) {
this.stats.misses++;
return void 0;
}
if (Date.now() > entry.expiry) {
this.delete(key);
this.stats.misses++;
return void 0;
}
entry.hits++;
entry.lastAccess = Date.now();
this.stats.hits++;
if (options?.updateOnGet ?? this.defaultOptions.updateOnGet) {
entry.expiry = Date.now() + (this.defaultOptions.ttl || 5 * 60 * 1e3);
}
return entry.value;
}
/**
* Get or set a value (memoization helper)
*/
async getOrSet(key, factory, options) {
const cached = this.get(key);
if (cached !== void 0) {
return cached;
}
const value = await factory();
this.set(key, value, options);
return value;
}
/**
* Check if key exists and is not expired
*/
has(key) {
const entry = this.cache.get(key);
if (!entry) return false;
if (Date.now() > entry.expiry) {
this.delete(key);
return false;
}
return true;
}
/**
* Delete a key from the cache
*/
delete(key) {
const entry = this.cache.get(key);
if (!entry) return false;
this.stats.size -= entry.size;
this.stats.entries--;
const deleted = this.cache.delete(key);
if (deleted && this.defaultOptions.onEvict) {
this.defaultOptions.onEvict(key, entry.value);
}
return deleted;
}
/**
* Clear all entries
*/
clear() {
if (this.defaultOptions.onEvict) {
for (const [key, entry] of this.cache.entries()) {
this.defaultOptions.onEvict(key, entry.value);
}
}
this.cache.clear();
this.stats.entries = 0;
this.stats.size = 0;
}
/**
* Get cache statistics
*/
getStats() {
return { ...this.stats };
}
/**
* Get all keys
*/
keys() {
return Array.from(this.cache.keys());
}
/**
* Get cache size
*/
size() {
return this.cache.size;
}
/**
* Create a cache key from multiple parts
*/
static createKey(...parts) {
const str = parts.map((p) => JSON.stringify(p)).join(":");
return crypto2.createHash("md5").update(str).digest("hex");
}
/**
* Clean up expired entries
*/
cleanup() {
const now = Date.now();
let evicted = 0;
for (const [key, entry] of this.cache.entries()) {
if (now > entry.expiry) {
this.delete(key);
evicted++;
}
}
this.stats.evictions += evicted;
}
/**
* Evict least recently used entry
*/
evictLRU() {
let lruKey = null;
let lruTime = Infinity;
for (const [key, entry] of this.cache.entries()) {
if (entry.lastAccess < lruTime) {
lruTime = entry.lastAccess;
lruKey = key;
}
}
if (lruKey) {
this.delete(lruKey);
this.stats.evictions++;
}
}
/**
* Estimate size of a value in bytes
*/
estimateSize(value) {
if (value === null || value === void 0) return 0;
if (typeof value === "string") return value.length * 2;
if (typeof value === "number") return 8;
if (typeof value === "boolean") return 4;
if (value instanceof Date) return 8;
if (Buffer.isBuffer(value)) return value.length;
try {
return JSON.stringify(value).length * 2;
} catch {
return 1024;
}
}
/**
* Start automatic cleanup interval
*/
startCleanupInterval() {
this.cleanupInterval = setInterval(() => {
this.cleanup();
}, 60 * 1e3);
}
/**
* Stop the cache manager and cleanup
*/
destroy() {
if (this.cleanupInterval) {
clearInterval(this.cleanupInterval);
}
this.clear();
}
};
// src/server/logger.ts
var LogLevel = /* @__PURE__ */ ((LogLevel2) => {
LogLevel2[LogLevel2["ERROR"] = 0] = "ERROR";
LogLevel2[LogLevel2["WARN"] = 1] = "WARN";
LogLevel2[LogLevel2["INFO"] = 2] = "INFO";
LogLevel2[LogLevel2["DEBUG"] = 3] = "DEBUG";
return LogLevel2;
})(LogLevel || {});
var Logger = class _Logger {
constructor(options = {}) {
this.handlers = [];
this.options = {
level: 2 /* INFO */,
timestamp: true,
colors: true,
...options
};
}
/**
* Add a custom log handler
*/
addHandler(handler) {
this.handlers.push(handler);
}
/**
* Remove a log handler
*/
removeHandler(handler) {
const index = this.handlers.indexOf(handler);
if (index !== -1) {
this.handlers.splice(index, 1);
}
}
/**
* Log an error message
*/
error(message, error) {
this.log(0 /* ERROR */, message, error);
}
/**
* Log a warning message
*/
warn(message, data) {
this.log(1 /* WARN */, message, data);
}
/**
* Log an info message
*/
info(message, data) {
this.log(2 /* INFO */, message, data);
}
/**
* Log a debug message
*/
debug(message, data) {
this.log(3 /* DEBUG */, message, data);
}
/**
* Core logging method
*/
log(level, message, data) {
if (level > this.options.level) {
return;
}
const entry = {
level,
message,
timestamp: /* @__PURE__ */ new Date(),
prefix: this.options.prefix,
data
};
if (data instanceof Error) {
entry.error = data;
entry.data = {
name: data.name,
message: data.message,
stack: data.stack
};
}
this.handlers.forEach((handler) => handler(entry));
this.consoleOutput(entry);
}
/**
* Format and output to console
*/
consoleOutput(entry) {
const parts = [];
if (this.options.timestamp) {
parts.push(`[${entry.timestamp.toISOString()}]`);
}
const levelName = LogLevel[entry.level];
if (this.options.colors) {
parts.push(this.colorize(levelName, entry.level));
} else {
parts.push(`[${levelName}]`);
}
if (entry.prefix) {
parts.push(`[${entry.prefix}]`);
}
parts.push(entry.message);
const logMessage = parts.join(" ");
switch (entry.level) {
case 0 /* ERROR */:
console.error(logMessage);
if (entry.error) {
console.error(entry.error);
}
break;
case 1 /* WARN */:
console.warn(logMessage);
break;
case 3 /* DEBUG */:
console.debug(logMessage);
break;
default:
console.log(logMessage);
}
if (entry.data && !entry.error) {
console.log(JSON.stringify(entry.data, null, 2));
}
}
/**
* Colorize text based on log level
*/
colorize(text, level) {
const colors = {
[0 /* ERROR */]: "\x1B[31m",
// Red
[1 /* WARN */]: "\x1B[33m",
// Yellow
[2 /* INFO */]: "\x1B[36m",
// Cyan
[3 /* DEBUG */]: "\x1B[90m"
// Gray
};
const color = colors[level] || "";
const reset = "\x1B[0m";
return `${color}[${text}]${reset}`;
}
/**
* Create a child logger with a prefix
*/
child(prefix) {
const childPrefix = this.options.prefix ? `${this.options.prefix}:${prefix}` : prefix;
const child = new _Logger({
...this.options,
prefix: childPrefix
});
this.handlers.forEach((handler) => child.addHandler(handler));
return child;
}
/**
* Set the log level
*/
setLevel(level) {
this.options.level = level;
}
/**
* Get the current log level
*/
getLevel() {
return this.options.level;
}
};
var logger = new Logger();
// src/server/error-handler.ts
var VibecodeError = class extends Error {
constructor(message, code = "UNKNOWN_ERROR", statusCode = 500, isOperational = true, context) {
super(message);
this.name = "VibecodeError";
this.code = code;
this.statusCode = statusCode;
this.isOperational = isOperational;
this.context = context;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, this.constructor);
}
}
};
var ValidationError = class extends VibecodeError {
constructor(message, context) {
super(message, "VALIDATION_ERROR", 400, true, context);
this.name = "ValidationError";
}
};
var AuthorizationError = class extends VibecodeError {
constructor(message, context) {
super(message, "AUTHORIZATION_ERROR", 403, true, context);
this.name = "AuthorizationError";
}
};
var NotFoundError = class extends VibecodeError {
constructor(message, context) {
super(message, "NOT_FOUND", 404, true, context);
this.name = "NotFoundError";
}
};
var RateLimitError = class extends VibecodeError {
constructor(message, retryAfter, context) {
super(message, "RATE_LIMIT_EXCEEDED", 429, true, context);
this.name = "RateLimitError";
this.retryAfter = retryAfter;
}
};
var ErrorHandler = class {
constructor() {
this.errorHandlers = /* @__PURE__ */ new Map();
}
/**
* Register a custom error handler for specific error codes
*/
registerHandler(errorCode, handler) {
this.errorHandlers.set(errorCode, handler);
}
/**
* Handle an error
*/
handle(error, context) {
const vibecodeError = this.toVibecodeError(error, context);
this.logError(vibecodeError);
const customHandler = this.errorHandlers.get(vibecodeError.code);
if (customHandler) {
customHandler(vibecodeError);
}
return this.createErrorResponse(vibecodeError);
}
/**
* Convert any error to VibecodeError
*/
toVibecodeError(error, context) {
if (error instanceof VibecodeError) {
if (context) {
error.context = { ...error.context, ...context };
}
return error;
}
if (error.name === "ValidationError") {
return new ValidationError(error.message, context);
}
if (error.message.includes("ENOENT")) {
return new NotFoundError(`File not found: ${error.message}`, context);
}
if (error.message.includes("EACCES")) {
return new AuthorizationError(`Permission denied: ${error.message}`, context);
}
return new VibecodeError(
error.message,
"INTERNAL_ERROR",
500,
false,
context
);
}
/**
* Log error with appropriate level
*/
logError(error) {
const logContext = {
code: error.code,
statusCode: error.statusCode,
isOperational: error.isOperational,
context: error.context,
stack: error.stack
};
if (error.isOperational) {
logger.warn(`Operational error: ${error.message}`, logContext);
} else {
logger.error(`System error: ${error.message}`, error);
}
}
/**
* Create error response
*/
createErrorResponse(error) {
const response = {
message: error.message,
code: error.code,
statusCode: error.statusCode
};
if (error instanceof RateLimitError) {
response.details = {
retryAfter: error.retryAfter
};
}
if (error.context) {
response.details = {
...response.details,
context: error.context
};
}
return response;
}
/**
* Wrap an async function with error handling
*/
wrapAsync(fn, context) {
return async (...args) => {
try {
return await fn(...args);
} catch (error) {
throw this.handle(error, context);
}
};
}
/**
* Create a middleware for Express-like frameworks
*/
middleware() {
return (err, req, res, next) => {
const context = {
operation: `${req.method} ${req.path}`,
user: req.user?.id,
metadata: {
ip: req.ip,
userAgent: req.get("user-agent")
}
};
const errorResponse = this.handle(err, context);
res.status(errorResponse.statusCode).json({
error: errorResponse.message,
code: errorResponse.code,
...errorResponse.details
});
};
}
};
var errorHandler = new ErrorHandler();
export {
AuthorizationError,
CacheManager,
ErrorHandler,
FileScanner,
LogLevel,
Logger,
MarkdownParser,
NotFoundError,
PathValidator,
RateLimitError,
RateLimiter,
TaskExtractor,
ValidationError,
VibecodeError,
errorHandler,
logger
};
//# sourceMappingURL=server.mjs.map