UNPKG

@vibeship/devtools

Version:

Comprehensive markdown-based project management system with AI capabilities for Next.js applications

1,637 lines (1,622 loc) 70.4 kB
var __defProp = Object.defineProperty; var __getOwnPropNames = Object.getOwnPropertyNames; var __esm = (fn, res) => function __init() { return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; }; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; // src/server/security/path-validator.ts import * as path from "path"; import * as fs from "fs"; var PathValidator; var init_path_validator = __esm({ "src/server/security/path-validator.ts"() { "use strict"; 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 import * as fs2 from "fs/promises"; import * as path2 from "path"; import { glob } from "glob"; var FileScanner; var init_file_scanner = __esm({ "src/server/file-scanner.ts"() { "use strict"; init_path_validator(); 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; var init_task_extractor = __esm({ "src/server/task-extractor.ts"() { "use strict"; 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; var init_markdown_parser = __esm({ "src/server/markdown-parser.ts"() { "use strict"; 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; var init_rate_limiter = __esm({ "src/server/security/rate-limiter.ts"() { "use strict"; 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/security/index.ts var init_security = __esm({ "src/server/security/index.ts"() { "use strict"; init_path_validator(); init_rate_limiter(); } }); // src/server/cache-manager.ts import * as crypto2 from "crypto"; var CacheManager; var init_cache_manager = __esm({ "src/server/cache-manager.ts"() { "use strict"; 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, Logger, logger; var init_logger = __esm({ "src/server/logger.ts"() { "use strict"; 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 || {}); Logger = class _Logger { constructor(options = {}) { this.handlers = []; this.options = { level: 2 /* INFO */, timestamp: true, colors: true, ...options }; } /** * Add a custom log handler */ addHandler(handler2) { this.handlers.push(handler2); } /** * Remove a log handler */ removeHandler(handler2) { const index = this.handlers.indexOf(handler2); 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((handler2) => handler2(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((handler2) => child.addHandler(handler2)); return child; } /** * Set the log level */ setLevel(level) { this.options.level = level; } /** * Get the current log level */ getLevel() { return this.options.level; } }; logger = new Logger(); } }); // src/server/error-handler.ts var VibecodeError, ValidationError, AuthorizationError, NotFoundError, RateLimitError, ErrorHandler, errorHandler; var init_error_handler = __esm({ "src/server/error-handler.ts"() { "use strict"; init_logger(); 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); } } }; ValidationError = class extends VibecodeError { constructor(message, context) { super(message, "VALIDATION_ERROR", 400, true, context); this.name = "ValidationError"; } }; AuthorizationError = class extends VibecodeError { constructor(message, context) { super(message, "AUTHORIZATION_ERROR", 403, true, context); this.name = "AuthorizationError"; } }; NotFoundError = class extends VibecodeError { constructor(message, context) { super(message, "NOT_FOUND", 404, true, context); this.name = "NotFoundError"; } }; RateLimitError = class extends VibecodeError { constructor(message, retryAfter, context) { super(message, "RATE_LIMIT_EXCEEDED", 429, true, context); this.name = "RateLimitError"; this.retryAfter = retryAfter; } }; ErrorHandler = class { constructor() { this.errorHandlers = /* @__PURE__ */ new Map(); } /** * Register a custom error handler for specific error codes */ registerHandler(errorCode, handler2) { this.errorHandlers.set(errorCode, handler2); } /** * 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 }); }; } }; errorHandler = new ErrorHandler(); } }); // src/server/types.ts var init_types = __esm({ "src/server/types.ts"() { "use strict"; } }); // src/server/index.ts var server_exports = {}; __export(server_exports, { AuthorizationError: () => AuthorizationError, CacheManager: () => CacheManager, ErrorHandler: () => ErrorHandler, FileScanner: () => FileScanner, LogLevel: () => LogLevel, Logger: () => Logger, MarkdownParser: () => MarkdownParser, NotFoundError: () => NotFoundError, PathValidator: () => PathValidator, RateLimitError: () => RateLimitError, RateLimiter: () => RateLimiter, TaskExtractor: () => TaskExtractor, ValidationError: () => ValidationError, VibecodeError: () => VibecodeError, errorHandler: () => errorHandler, logger: () => logger }); var init_server = __esm({ "src/server/index.ts"() { "use strict"; init_file_scanner(); init_task_extractor(); init_markdown_parser(); init_security(); init_cache_manager(); init_logger(); init_error_handler(); init_types(); } }); // src/api/config-loader.ts function getDefaultApiConfig() { return { scanPaths: ["./src", "./app", "./docs", "./README.md", "./.tickets"], include: ["**/*.md", "**/*.mdx", "**/*.tsx", "**/*.ts", "**/*.jsx", "**/*.js"], exclude: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"], features: { tasks: true, ai: false, // Disabled by default for safety realtime: true, files: true, health: true }, security: { authentication: false, rateLimit: false, cors: true, // Enabled for dev convenience allowedPaths: ["**/*"] }, cache: { enabled: true, ttl: 3e5 // 5 minutes } }; } async function loadApiConfig() { const defaultConfig = getDefaultApiConfig(); try { const routeConfig = await tryLoadRouteConfig(); if (routeConfig) { return mergeConfigs(defaultConfig, routeConfig); } } catch { } try { const configModule = await import(process.cwd() + "/vibeship.config"); const userConfig = configModule.default || configModule; return mergeConfigs(defaultConfig, userConfig); } catch { } try { const packageJson = await import(process.cwd() + "/package.json"); if (packageJson.vibeship) { return mergeConfigs(defaultConfig, packageJson.vibeship); } } catch { } const envConfig = loadConfigFromEnv(); if (envConfig) { return mergeConfigs(defaultConfig, envConfig); } return defaultConfig; } async function tryLoadRouteConfig() { try { return null; } catch { return null; } } function loadConfigFromEnv() { const envConfig = {}; if (process.env.VIBESHIP_SCAN_PATHS) { envConfig.scanPaths = process.env.VIBESHIP_SCAN_PATHS.split(",").map((p) => p.trim()); } if (process.env.VIBESHIP_DISABLE_AI === "true") { envConfig.features = { ai: false }; } if (process.env.VIBESHIP_DISABLE_CACHE === "true") { envConfig.cache = { enabled: false }; } if (process.env.VIBESHIP_DISABLE_CORS === "true") { envConfig.security = { cors: false }; } return Object.keys(envConfig).length > 0 ? envConfig : null; } function mergeConfigs(base, override) { const result = { ...base }; if (override.scanPaths) result.scanPaths = override.scanPaths; if (override.include) result.include = override.include; if (override.exclude) result.exclude = override.exclude; if (override.features) { result.features = { ...result.features, ...override.features }; } if (override.security) { result.security = { ...result.security, ...override.security }; } if (override.cache) { result.cache = { ...result.cache, ...override.cache }; } return result; } // src/templates/utils/fallback-handlers.ts async function safeDependencyLoad(importFn, fallbackFn, dependencyName, requestId) { try { return await importFn(); } catch (error) { if (true) { console.warn(`\u26A0\uFE0F Vibeship DevTools: ${dependencyName} not available, using fallback`); console.warn(` Error: ${error instanceof Error ? error.message : String(error)}`); } return fallbackFn(); } } // src/api/routes/tasks.ts async function tasksHandler(context) { const { request, config, requestId } = context; const method = request.method; if (method === "GET") { return await handleTasksGet(context); } if (method === "POST") { return await handleTasksPost(context); } throw new Error(`Method ${method} not supported for tasks endpoint`); } async function handleTasksGet(context) { const { request, config } = context; const url = new URL(request.url); const searchParams = url.searchParams; const queryParams = { type: searchParams.get("type") || "all", status: searchParams.get("status") || "pending", priority: searchParams.get("priority") || "all", path: searchParams.get("path"), search: searchParams.get("search"), limit: Math.min(parseInt(searchParams.get("limit") || "100"), 1e3), offset: Math.max(parseInt(searchParams.get("offset") || "0"), 0), sortBy: searchParams.get("sortBy") || "priority", sortOrder: searchParams.get("sortOrder") || "desc" }; try { const FileScanner2 = await safeDependencyLoad( async () => { const { FileScanner: FileScanner3 } = await Promise.resolve().then(() => (init_server(), server_exports));