@vibeship/devtools
Version:
Comprehensive markdown-based project management system with AI capabilities for Next.js applications
1 lines • 1.58 MB
Source Map (JSON)
{"version":3,"sources":["../src/cli/utils/framework-detector.ts","../src/templates/embedded-templates.ts","../src/templates/generator.ts","../src/cli/utils/template-integration.ts","../src/cli/utils/error-type-guards.ts","../src/cli/utils/validators.ts","../src/cli/utils/progress.ts","../src/cli/utils/errors.ts","../src/cli/utils/output.ts","../src/cli/utils/transaction-manager.ts","../src/cli/utils/next-config-updater.ts","../src/cli/utils/env-generator.ts","../src/cli/utils/layout-detector.ts","../../../node_modules/@babel/parser/src/util/location.ts","../../../node_modules/@babel/parser/src/parse-error/module-errors.ts","../../../node_modules/@babel/parser/src/parse-error/to-node-description.ts","../../../node_modules/@babel/parser/src/parse-error/standard-errors.ts","../../../node_modules/@babel/parser/src/parse-error/strict-mode-errors.ts","../../../node_modules/@babel/parser/src/parse-error/parse-expression-errors.ts","../../../node_modules/@babel/parser/src/parse-error/pipeline-operator-errors.ts","../../../node_modules/@babel/parser/src/parse-error.ts","../../../node_modules/@babel/parser/src/options.ts","../../../node_modules/@babel/parser/src/plugins/estree.ts","../../../node_modules/@babel/parser/src/tokenizer/context.ts","../../../node_modules/@babel/parser/src/tokenizer/types.ts","../../../node_modules/@babel/babel-helper-validator-identifier/src/identifier.ts","../../../node_modules/@babel/babel-helper-validator-identifier/src/keyword.ts","../../../node_modules/@babel/parser/src/util/identifier.ts","../../../node_modules/@babel/parser/src/util/scope.ts","../../../node_modules/@babel/parser/src/plugins/flow/scope.ts","../../../node_modules/@babel/parser/src/plugins/flow/index.ts","../../../node_modules/@babel/parser/src/plugins/jsx/xhtml.ts","../../../node_modules/@babel/parser/src/util/whitespace.ts","../../../node_modules/@babel/parser/src/plugins/jsx/index.ts","../../../node_modules/@babel/parser/src/plugins/typescript/scope.ts","../../../node_modules/@babel/parser/src/util/production-parameter.ts","../../../node_modules/@babel/parser/src/parser/base.ts","../../../node_modules/@babel/parser/src/parser/comments.ts","../../../node_modules/@babel/parser/src/tokenizer/state.ts","../../../node_modules/@babel/babel-helper-string-parser/src/index.ts","../../../node_modules/@babel/parser/src/tokenizer/index.ts","../../../node_modules/@babel/parser/src/util/class-scope.ts","../../../node_modules/@babel/parser/src/util/expression-scope.ts","../../../node_modules/@babel/parser/src/parser/util.ts","../../../node_modules/@babel/parser/src/parser/node.ts","../../../node_modules/@babel/parser/src/parser/lval.ts","../../../node_modules/@babel/parser/src/plugins/typescript/index.ts","../../../node_modules/@babel/parser/src/plugins/placeholders.ts","../../../node_modules/@babel/parser/src/plugins/v8intrinsic.ts","../../../node_modules/@babel/parser/src/plugin-utils.ts","../../../node_modules/@babel/parser/src/parser/expression.ts","../../../node_modules/@babel/parser/src/parser/statement.ts","../../../node_modules/@babel/parser/src/parser/index.ts","../../../node_modules/@babel/parser/src/index.ts","../src/cli/utils/layout-injector.ts","../src/cli/templates/quick-start-template.ts","../src/cli/templates/quick-tasks-route.ts","../src/cli/templates/quick-stream-route.ts","../src/cli/templates/quick-files-route.ts","../src/cli/templates/quick-files-content-route.ts","../src/cli/templates/config-template.ts","../src/cli/templates/provider-template.ts","../src/cli/templates/env-template.ts","../src/cli/templates/api-routes-templates.ts","../src/cli/utils/tailwind-config-updater.ts","../src/cli/commands/simple-init.ts","../src/cli/commands/init.ts","../src/cli/bin.ts","../src/cli/commands/config.ts","../src/cli/index.ts"],"sourcesContent":["import { existsSync, readFileSync } from 'fs';\nimport { join } from 'path';\n\nexport interface FrameworkInfo {\n isNextJs: boolean;\n version?: string;\n isAppRouter: boolean;\n hasSrcDir: boolean;\n hasTypeScript: boolean;\n hasTailwind: boolean;\n}\n\nexport async function detectFramework(): Promise<FrameworkInfo> {\n const cwd = process.cwd();\n \n // Check if it's a Next.js project\n const packageJsonPath = join(cwd, 'package.json');\n if (!existsSync(packageJsonPath)) {\n return {\n isNextJs: false,\n isAppRouter: false,\n hasSrcDir: false,\n hasTypeScript: false,\n hasTailwind: false,\n };\n }\n\n try {\n const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));\n const hasNext = !!(packageJson.dependencies?.next || packageJson.devDependencies?.next);\n \n if (!hasNext) {\n return {\n isNextJs: false,\n isAppRouter: false,\n hasSrcDir: false,\n hasTypeScript: false,\n hasTailwind: false,\n };\n }\n\n // Get Next.js version\n const version = packageJson.dependencies?.next || packageJson.devDependencies?.next;\n const versionNumber = version.replace(/[\\^~]/, '');\n\n // Detect router type\n const hasAppDir = existsSync(join(cwd, 'app')) || existsSync(join(cwd, 'src/app'));\n const hasPagesDir = existsSync(join(cwd, 'pages')) || existsSync(join(cwd, 'src/pages'));\n \n // Prefer App Router if both exist\n const isAppRouter = hasAppDir;\n\n // Check for src directory\n const hasSrcDir = existsSync(join(cwd, 'src'));\n\n // Check for TypeScript\n const hasTypeScript = existsSync(join(cwd, 'tsconfig.json'));\n\n // Check for Tailwind\n const hasTailwind = !!(packageJson.dependencies?.tailwindcss || packageJson.devDependencies?.tailwindcss);\n\n return {\n isNextJs: true,\n version: versionNumber,\n isAppRouter,\n hasSrcDir,\n hasTypeScript,\n hasTailwind,\n };\n } catch (error) {\n return {\n isNextJs: false,\n isAppRouter: false,\n hasSrcDir: false,\n hasTypeScript: false,\n hasTailwind: false,\n };\n }\n}\n\nexport function getApiRoutePath(framework: FrameworkInfo): string {\n const base = framework.hasSrcDir ? 'src' : '';\n \n if (framework.isAppRouter) {\n return join(base, 'app', 'api');\n } else {\n return join(base, 'pages', 'api');\n }\n}","// Auto-generated file - do not edit manually\n// Generated by scripts/embed-templates.js\n\nexport const EMBEDDED_TEMPLATES: Record<string, string> = {\n 'app-router/tasks-stream.ts': `import { NextRequest } from 'next/server';\nimport { FileScanner, TaskExtractor, CacheManager } from '@vibeship/devtools/server';\nimport { getConfig } from '@/lib/vibeship-config';\nimport { createStreamError, getRequestId } from '@/utils/api-errors';\n\nexport const runtime = 'nodejs';\nexport const dynamic = 'force-dynamic';\n\n// Store active connections for broadcasting updates\nconst clients = new Map<string, ReadableStreamDefaultController>();\n\n// Task change detection\nlet lastTaskHash: string | null = null;\n\nexport async function GET(request: NextRequest) {\n const encoder = new TextEncoder();\n const clientId = crypto.randomUUID();\n \n // Get query parameters\n const searchParams = request.nextUrl.searchParams;\n const filter = searchParams.get('filter'); // todo, fixme, etc.\n const includeCompleted = searchParams.get('includeCompleted') === 'true';\n \n const stream = new ReadableStream({\n async start(controller) {\n // Add client to active connections\n clients.set(clientId, controller);\n \n // Send initial connection message\n const connectMessage = \\`data: \\${JSON.stringify({ \n type: 'connected', \n clientId,\n timestamp: new Date().toISOString() \n })}\\\\n\\\\n\\`;\n controller.enqueue(encoder.encode(connectMessage));\n \n // Send initial tasks\n try {\n const tasks = await scanTasks(filter, includeCompleted);\n const taskMessage = \\`data: \\${JSON.stringify({\n type: 'tasks',\n tasks,\n timestamp: new Date().toISOString()\n })}\\\\n\\\\n\\`;\n controller.enqueue(encoder.encode(taskMessage));\n } catch (error) {\n const errorMessage = createStreamError(error, getRequestId(request));\n controller.enqueue(encoder.encode(errorMessage));\n }\n \n // Set up polling for changes\n const pollInterval = setInterval(async () => {\n try {\n const tasks = await scanTasks(filter, includeCompleted);\n const currentHash = generateTaskHash(tasks);\n \n if (currentHash !== lastTaskHash) {\n lastTaskHash = currentHash;\n \n // Broadcast to all connected clients\n const updateMessage = \\`data: \\${JSON.stringify({\n type: 'update',\n tasks,\n timestamp: new Date().toISOString()\n })}\\\\n\\\\n\\`;\n \n for (const [id, client] of clients.entries()) {\n try {\n client.enqueue(encoder.encode(updateMessage));\n } catch (error) {\n // Client disconnected, remove from map\n clients.delete(id);\n }\n }\n }\n } catch (error) {\n console.error('Error polling tasks:', error);\n }\n }, 5000); // Poll every 5 seconds\n \n // Keep connection alive with heartbeat\n const heartbeatInterval = setInterval(() => {\n try {\n const heartbeatMessage = \\`data: \\${JSON.stringify({ \n type: 'heartbeat', \n timestamp: new Date().toISOString() \n })}\\\\n\\\\n\\`;\n controller.enqueue(encoder.encode(heartbeatMessage));\n } catch (error) {\n // Client disconnected\n clearInterval(heartbeatInterval);\n clearInterval(pollInterval);\n clients.delete(clientId);\n }\n }, 30000); // Send heartbeat every 30 seconds\n \n // Clean up on close\n request.signal.addEventListener('abort', () => {\n clearInterval(heartbeatInterval);\n clearInterval(pollInterval);\n clients.delete(clientId);\n controller.close();\n });\n },\n });\n \n return new Response(stream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache, no-transform',\n 'Connection': 'keep-alive',\n 'X-Accel-Buffering': 'no', // Disable Nginx buffering\n },\n });\n}\n\n// Helper function to scan tasks\nasync function scanTasks(filter?: string | null, includeCompleted = false) {\n const config = await getConfig();\n const scanner = new FileScanner(config);\n const extractor = new TaskExtractor();\n const cache = new CacheManager({ maxSize: 100, ttl: 60000 }); // 1 minute cache\n \n // Check cache first\n const cacheKey = \\`tasks:\\${filter || 'all'}:\\${includeCompleted}\\`;\n const cached = cache.get(cacheKey);\n if (cached) {\n return cached;\n }\n \n // Scan files\n const files = await scanner.scanWithInfo();\n const allTasks = [];\n \n for (const file of files) {\n const tasks = extractor.extract(file.content, file.path, {\n includeContext: true,\n parseMetadata: true,\n });\n \n // Apply filter if specified\n const filteredTasks = filter \n ? tasks.filter(task => task.type.toLowerCase() === filter.toLowerCase())\n : tasks;\n \n // Filter completed tasks if needed\n const finalTasks = includeCompleted\n ? filteredTasks\n : filteredTasks.filter(task => !task.metadata?.completed);\n \n allTasks.push(...finalTasks);\n }\n \n // Sort by priority and date\n allTasks.sort((a, b) => {\n // Priority first (high > medium > low)\n const priorityOrder = { high: 3, medium: 2, low: 1 };\n const aPriority = priorityOrder[a.metadata?.priority || 'low'];\n const bPriority = priorityOrder[b.metadata?.priority || 'low'];\n \n if (aPriority !== bPriority) {\n return bPriority - aPriority;\n }\n \n // Then by date (newer first)\n const aDate = a.metadata?.createdAt || 0;\n const bDate = b.metadata?.createdAt || 0;\n return bDate - aDate;\n });\n \n // Cache results\n cache.set(cacheKey, allTasks);\n \n return allTasks;\n}\n\n// Generate hash for task comparison\nfunction generateTaskHash(tasks: any[]): string {\n const taskString = JSON.stringify(tasks.map(t => ({\n id: t.id,\n type: t.type,\n text: t.text,\n completed: t.metadata?.completed\n })));\n \n // Simple hash function\n let hash = 0;\n for (let i = 0; i < taskString.length; i++) {\n const char = taskString.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash; // Convert to 32-bit integer\n }\n \n return hash.toString(36);\n}`,\n 'app-router/ai-chat.ts': `import { NextRequest } from 'next/server';\nimport { FileScanner, MarkdownParser } from '@vibeship/devtools/server';\nimport { getConfig } from '@/lib/vibeship-config';\n\n// Error handling utility\nfunction getErrorMessage(error: unknown): string {\n if (error instanceof Error) return error.message;\n return String(error);\n}\n\nexport const runtime = 'edge'; // Use edge runtime for streaming\n\nexport async function POST(request: NextRequest) {\n try {\n const { messages, context, stream = true } = await request.json();\n \n if (!messages || !Array.isArray(messages)) {\n return new Response(\n JSON.stringify({ error: 'Messages array is required' }),\n { status: 400, headers: { 'Content-Type': 'application/json' } }\n );\n }\n \n // Get relevant context from markdown files\n const relevantContext = await getRelevantContext(context);\n \n // Prepare system message with context\n const systemMessage = {\n role: 'system',\n content: \\`You are an AI assistant helping with a development project. \n\\${relevantContext ? \\`Here is relevant context from the project:\\\\n\\\\n\\${relevantContext}\\` : ''}\n\nPlease provide helpful, accurate, and concise responses. When discussing code, use proper markdown formatting.\\`\n };\n \n const allMessages = [systemMessage, ...messages];\n \n if (!stream) {\n // Non-streaming response\n const response = await callAIProvider(allMessages, false);\n return new Response(\n JSON.stringify({ message: response }),\n { headers: { 'Content-Type': 'application/json' } }\n );\n }\n \n // Streaming response\n const encoder = new TextEncoder();\n const responseStream = new ReadableStream({\n async start(controller) {\n try {\n // Call AI provider with streaming\n const aiStream = await callAIProvider(allMessages, true);\n \n // Process the stream\n const reader = aiStream.getReader();\n \n while (true) {\n const { done, value } = await reader.read();\n \n if (done) {\n // Send final message\n controller.enqueue(encoder.encode('data: [DONE]\\\\n\\\\n'));\n break;\n }\n \n // Format as SSE\n const sseMessage = \\`data: \\${JSON.stringify({\n type: 'content',\n content: value,\n timestamp: new Date().toISOString()\n })}\\\\n\\\\n\\`;\n \n controller.enqueue(encoder.encode(sseMessage));\n }\n } catch (error) {\n // Send error message\n const errorMessage = \\`data: \\${JSON.stringify({\n type: 'error',\n error: getErrorMessage(error),\n timestamp: new Date().toISOString()\n })}\\\\n\\\\n\\`;\n \n controller.enqueue(encoder.encode(errorMessage));\n } finally {\n controller.close();\n }\n }\n });\n \n return new Response(responseStream, {\n headers: {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n 'Connection': 'keep-alive',\n },\n });\n } catch (error) {\n console.error('AI chat error:', error);\n return new Response(\n JSON.stringify({ error: 'Internal server error' }),\n { status: 500, headers: { 'Content-Type': 'application/json' } }\n );\n }\n}\n\n// Get relevant context from markdown files\nasync function getRelevantContext(contextRequest?: {\n files?: string[];\n search?: string;\n limit?: number;\n}): Promise<string> {\n if (!contextRequest) return '';\n \n const config = await getConfig();\n const scanner = new FileScanner(config);\n const parser = new MarkdownParser();\n \n let contexts: string[] = [];\n \n // Get specific files if requested\n if (contextRequest.files?.length) {\n for (const file of contextRequest.files) {\n try {\n const content = await scanner.readFile(file);\n const parsed = parser.parse(content);\n contexts.push(\\`File: \\${file}\\\\n\\${parsed.content.slice(0, 1000)}\\`);\n } catch (error) {\n console.error(\\`Error reading file \\${file}:\\`, error);\n }\n }\n }\n \n // Search for relevant content\n if (contextRequest.search) {\n const files = await scanner.scan();\n const relevantFiles = files\n .filter((file: any) => file.endsWith('.md') || file.endsWith('.mdx'))\n .slice(0, contextRequest.limit || 5);\n \n for (const file of relevantFiles) {\n try {\n const content = await scanner.readFile(file);\n if (content.toLowerCase().includes(contextRequest.search.toLowerCase())) {\n const parsed = parser.parse(content);\n contexts.push(\\`File: \\${file}\\\\n\\${parsed.content.slice(0, 500)}\\`);\n }\n } catch (error) {\n console.error(\\`Error searching file \\${file}:\\`, error);\n }\n }\n }\n \n return contexts.join('\\\\n\\\\n---\\\\n\\\\n');\n}\n\n// Placeholder for AI provider integration\nasync function callAIProvider(\n messages: any[], \n stream: boolean\n): Promise<any> {\n // This is where you would integrate with your AI provider\n // Examples: OpenAI, Anthropic, Azure OpenAI, etc.\n \n // For now, return a mock response\n if (!stream) {\n return \"This is a placeholder response. Please integrate with your preferred AI provider.\";\n }\n \n // Mock streaming response\n const mockStream = new ReadableStream({\n async start(controller) {\n const response = \"This is a streaming placeholder response. Please integrate with your preferred AI provider.\";\n const words = response.split(' ');\n \n for (const word of words) {\n controller.enqueue(word + ' ');\n await new Promise(resolve => setTimeout(resolve, 100));\n }\n \n controller.close();\n }\n });\n \n return mockStream;\n}\n\n// Example integrations (commented out):\n\n/*\n// OpenAI Integration\nimport OpenAI from 'openai';\n\nconst openai = new OpenAI({\n apiKey: process.env.OPENAI_API_KEY,\n});\n\nasync function callOpenAI(messages: any[], stream: boolean) {\n if (!stream) {\n const completion = await openai.chat.completions.create({\n model: \"gpt-4\",\n messages,\n });\n return completion.choices[0].message.content;\n }\n \n const stream = await openai.chat.completions.create({\n model: \"gpt-4\",\n messages,\n stream: true,\n });\n \n return new ReadableStream({\n async start(controller) {\n for await (const chunk of stream) {\n controller.enqueue(chunk.choices[0]?.delta?.content || '');\n }\n controller.close();\n }\n });\n}\n*/\n\n/*\n// Anthropic Integration\nimport Anthropic from '@anthropic-ai/sdk';\n\nconst anthropic = new Anthropic({\n apiKey: process.env.ANTHROPIC_API_KEY,\n});\n\nasync function callAnthropic(messages: any[], stream: boolean) {\n const systemMessage = messages.find(m => m.role === 'system');\n const userMessages = messages.filter(m => m.role !== 'system');\n \n if (!stream) {\n const response = await anthropic.messages.create({\n model: \"claude-3-opus-20240229\",\n system: systemMessage?.content,\n messages: userMessages,\n max_tokens: 1024,\n });\n return response.content[0].text;\n }\n \n const stream = await anthropic.messages.create({\n model: \"claude-3-opus-20240229\",\n system: systemMessage?.content,\n messages: userMessages,\n max_tokens: 1024,\n stream: true,\n });\n \n return new ReadableStream({\n async start(controller) {\n for await (const chunk of stream) {\n if (chunk.type === 'content_block_delta') {\n controller.enqueue(chunk.delta.text);\n }\n }\n controller.close();\n }\n });\n}\n*/`,\n 'app-router/files.ts': `import { NextRequest, NextResponse } from 'next/server';\nimport { FileScanner, PathValidator, Logger } from '@vibeship/devtools/server';\nimport { getConfig } from '@/lib/vibeship-config';\nimport { z } from 'zod';\n\n// Request validation schemas\nconst ReadFileSchema = z.object({\n path: z.string().min(1),\n encoding: z.enum(['utf8', 'base64']).optional().default('utf8'),\n});\n\nconst WriteFileSchema = z.object({\n path: z.string().min(1),\n content: z.string(),\n encoding: z.enum(['utf8', 'base64']).optional().default('utf8'),\n});\n\nconst DeleteFileSchema = z.object({\n path: z.string().min(1),\n});\n\nconst ListFilesSchema = z.object({\n path: z.string().optional(),\n pattern: z.string().optional(),\n recursive: z.boolean().optional().default(true),\n});\n\n// GET: Read file or list files\nexport async function GET(request: NextRequest) {\n const logger = new Logger({ prefix: 'FileOperations' });\n const requestId = getRequestId(request);\n \n return await withErrorHandling(async () => {\n const searchParams = request.nextUrl.searchParams;\n const path = searchParams.get('path');\n \n if (path) {\n // Read single file\n const result = ReadFileSchema.safeParse({ \n path, \n encoding: searchParams.get('encoding') || 'utf8' \n });\n \n if (!result.success) {\n throw new ApiError(\n ErrorCodes.INVALID_PARAMETERS,\n 400,\n {\n message: 'Invalid file read parameters',\n cause: 'File path or encoding parameter is invalid',\n solution: 'Provide a valid file path and encoding (utf8 or base64)',\n docs: 'https://vibeship.dev/docs/api/files#read-file',\n context: { validationErrors: result.error.flatten() },\n },\n undefined,\n requestId\n );\n }\n \n const config = await getConfig();\n const scanner = new FileScanner(config);\n const validator = new PathValidator(config.scanPaths);\n \n // Validate path security\n if (!validator.isValid(result.data.path)) {\n logger.warn(\\`Blocked access to invalid path: \\${result.data.path}\\`);\n return NextResponse.json(\n { error: 'Access denied: Invalid path' },\n { status: 403 }\n );\n }\n \n try {\n const content = await scanner.readFile(result.data.path);\n \n // Handle binary files\n if (result.data.encoding === 'base64') {\n const buffer = Buffer.from(content);\n return NextResponse.json({\n path: result.data.path,\n content: buffer.toString('base64'),\n encoding: 'base64',\n size: buffer.length,\n });\n }\n \n return NextResponse.json({\n path: result.data.path,\n content,\n encoding: 'utf8',\n size: content.length,\n lines: content.split('\\\\n').length,\n });\n } catch (error) {\n logger.error(\\`Failed to read file \\${result.data.path}:\\`, error);\n return NextResponse.json(\n { error: 'File not found' },\n { status: 404 }\n );\n }\n } else {\n // List files\n const result = ListFilesSchema.safeParse({\n path: searchParams.get('path') || '.',\n pattern: searchParams.get('pattern'),\n recursive: searchParams.get('recursive') === 'true',\n });\n \n if (!result.success) {\n throw new ApiError(\n ErrorCodes.INVALID_PARAMETERS,\n 400,\n {\n message: 'Invalid file read parameters',\n cause: 'File path or encoding parameter is invalid',\n solution: 'Provide a valid file path and encoding (utf8 or base64)',\n docs: 'https://vibeship.dev/docs/api/files#read-file',\n context: { validationErrors: result.error.flatten() },\n },\n undefined,\n requestId\n );\n }\n \n const config = await getConfig();\n const scanner = new FileScanner(config);\n \n const files = await scanner.scan(result.data.path || undefined);\n \n // Get file info for each file\n const filesWithInfo = await Promise.all(\n files.map(async (file) => {\n try {\n const info = await scanner.getFileInfo(file);\n return {\n path: file,\n size: info.size,\n modified: info.mtime,\n created: info.birthtime,\n isDirectory: info.isDirectory(),\n };\n } catch (error) {\n return {\n path: file,\n error: 'Failed to get file info',\n };\n }\n })\n );\n \n return NextResponse.json({\n files: filesWithInfo,\n total: filesWithInfo.length,\n });\n }\n } catch (error) {\n logger.error('File operation error:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// POST: Write file\nexport async function POST(request: NextRequest) {\n const logger = new Logger({ prefix: 'FileOperations' });\n \n try {\n const body = await request.json();\n const result = WriteFileSchema.safeParse(body);\n \n if (!result.success) {\n return NextResponse.json(\n { error: 'Invalid parameters', details: result.error.flatten() },\n { status: 400 }\n );\n }\n \n const config = await getConfig();\n const validator = new PathValidator({\n allowedPaths: config.scanPaths,\n basePath: process.cwd(),\n });\n \n // Validate path security\n if (!validator.isValid(result.data.path)) {\n logger.warn(\\`Blocked write to invalid path: \\${result.data.path}\\`);\n return NextResponse.json(\n { error: 'Access denied: Invalid path' },\n { status: 403 }\n );\n }\n \n // Check if file writes are explicitly disabled\n const disableWrites = process.env.VIBESHIP_DISABLE_FILE_WRITES === 'true';\n \n if (disableWrites) {\n logger.warn('File writes are disabled by VIBESHIP_DISABLE_FILE_WRITES environment variable');\n return NextResponse.json({\n success: false,\n error: 'File writing is disabled in this environment',\n path: result.data.path,\n }, { status: 403 });\n }\n \n // Implement file writing\n try {\n const scanner = new FileScanner(config);\n await scanner.writeFile(result.data.path, result.data.content);\n \n logger.info(\\`Successfully wrote file: \\${result.data.path}\\`);\n \n return NextResponse.json({\n success: true,\n path: result.data.path,\n size: result.data.content.length,\n });\n } catch (writeError: any) {\n logger.error(\\`Failed to write file \\${result.data.path}:\\`, writeError);\n return NextResponse.json(\n { error: 'Failed to write file', details: writeError.message },\n { status: 500 }\n );\n }\n } catch (error) {\n logger.error('File write error:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// DELETE: Delete file\nexport async function DELETE(request: NextRequest) {\n const logger = new Logger({ prefix: 'FileOperations' });\n \n try {\n const body = await request.json();\n const result = DeleteFileSchema.safeParse(body);\n \n if (!result.success) {\n return NextResponse.json(\n { error: 'Invalid parameters', details: result.error.flatten() },\n { status: 400 }\n );\n }\n \n const config = await getConfig();\n const validator = new PathValidator({\n allowedPaths: config.scanPaths,\n basePath: process.cwd(),\n });\n \n // Validate path security\n if (!validator.isValid(result.data.path)) {\n logger.warn(\\`Blocked delete of invalid path: \\${result.data.path}\\`);\n return NextResponse.json(\n { error: 'Access denied: Invalid path' },\n { status: 403 }\n );\n }\n \n // In production, implement proper file deletion\n // For now, return success with warning\n logger.info(\\`File delete requested for: \\${result.data.path}\\`);\n \n return NextResponse.json({\n success: true,\n path: result.data.path,\n warning: 'File deletion is disabled in template. Implement proper file deletion with security checks.',\n });\n } catch (error) {\n logger.error('File delete error:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}\n\n// PUT: Update file (partial update)\nexport async function PUT(request: NextRequest) {\n const logger = new Logger({ prefix: 'FileOperations' });\n \n try {\n const body = await request.json();\n const { path, updates } = body;\n \n if (!path || !updates) {\n return NextResponse.json(\n { error: 'Path and updates are required' },\n { status: 400 }\n );\n }\n \n const config = await getConfig();\n const validator = new PathValidator({\n allowedPaths: config.scanPaths,\n basePath: process.cwd(),\n });\n \n // Validate path security\n if (!validator.isValid(path)) {\n logger.warn(\\`Blocked update to invalid path: \\${path}\\`);\n return NextResponse.json(\n { error: 'Access denied: Invalid path' },\n { status: 403 }\n );\n }\n \n // In production, implement proper file updating\n // This could support operations like:\n // - Append to file\n // - Replace specific lines\n // - Update frontmatter\n // - etc.\n \n logger.info(\\`File update requested for: \\${path}\\`);\n \n return NextResponse.json({\n success: true,\n path,\n warning: 'File updating is disabled in template. Implement proper file updating with security checks.',\n });\n } catch (error) {\n logger.error('File update error:', error);\n return NextResponse.json(\n { error: 'Internal server error' },\n { status: 500 }\n );\n }\n}`,\n 'app-router/tasks.ts': `import { NextRequest, NextResponse } from 'next/server';\nimport { z } from 'zod';\nimport { ApiError, ErrorCodes, ErrorFactory, getRequestId, withErrorHandling } from '@/utils/api-errors';\nimport { loadDependenciesWithFallbacks } from '@/utils/fallback-handlers';\n\n// Request validation schema\nconst TaskQuerySchema = z.object({\n type: z.enum(['todo', 'fixme', 'hack', 'note', 'all']).optional().default('all'),\n status: z.enum(['pending', 'completed', 'all']).optional().default('pending'),\n priority: z.enum(['high', 'medium', 'low', 'all']).optional().default('all'),\n path: z.string().optional(),\n search: z.string().optional(),\n limit: z.number().min(1).max(1000).optional().default(100),\n offset: z.number().min(0).optional().default(0),\n sortBy: z.enum(['priority', 'date', 'type', 'path']).optional().default('priority'),\n sortOrder: z.enum(['asc', 'desc']).optional().default('desc'),\n});\n\n// GET: Scan and return tasks\nexport async function GET(request: NextRequest) {\n const requestId = getRequestId(request);\n \n return await withErrorHandling(async () => {\n // Parse query parameters\n const searchParams = Object.fromEntries(request.nextUrl.searchParams);\n const query = TaskQuerySchema.safeParse({\n ...searchParams,\n limit: searchParams.limit ? parseInt(searchParams.limit) : undefined,\n offset: searchParams.offset ? parseInt(searchParams.offset) : undefined,\n });\n \n if (!query.success) {\n const requestId = getRequestId(request);\n const validationError = new ApiError(\n ErrorCodes.INVALID_PARAMETERS,\n 400,\n {\n message: 'Invalid query parameters provided',\n cause: 'One or more query parameters are invalid or missing',\n solution: 'Check the API documentation for correct parameter format and values',\n docs: 'https://vibeship.dev/docs/api/tasks#query-parameters',\n context: { validationErrors: query.error.flatten() },\n },\n undefined,\n requestId\n );\n return validationError.toResponse();\n }\n \n // Load all dependencies with fallbacks\n const deps = await loadDependenciesWithFallbacks(requestId);\n const { config, FileScanner, TaskExtractor, Logger, CacheManager } = deps;\n \n const logger = new Logger({ prefix: 'TaskScanning' });\n logger.info('Config loaded:', { scanPaths: config.scanPaths });\n \n const scanner = new FileScanner(config);\n const extractor = new TaskExtractor();\n const cache = new CacheManager({ maxSize: 100, ttl: 300000 }); // 5 minute cache\n \n // Generate cache key\n const cacheKey = \\`tasks:\\${JSON.stringify(query.data)}\\`;\n const cached = cache.get(cacheKey);\n \n if (cached) {\n logger.info('Returning cached tasks');\n return NextResponse.json(cached);\n }\n \n // Scan files\n let scanOptions = undefined;\n if (query.data.path) {\n // Normalize the path - if it doesn't start with . or /, add ./\n let normalizedPath = query.data.path;\n if (!normalizedPath.startsWith('.') && !normalizedPath.startsWith('/')) {\n normalizedPath = \\`./\\${normalizedPath}\\`;\n }\n scanOptions = { paths: [normalizedPath] };\n logger.info(\\`Using normalized scan path: \\${normalizedPath}\\`);\n }\n \n const files = await scanner.scanWithInfo(scanOptions);\n logger.info(\\`Scanned \\${files.length} files\\`);\n \n const allTasks = [];\n \n // Extract tasks from each file\n for (const file of files) {\n const tasks = extractor.extract(file.content, file.path, {\n includeContext: true,\n parseMetadata: true,\n contextLines: 3,\n });\n \n if (tasks.length > 0) {\n logger.info(\\`Found \\${tasks.length} tasks in \\${file.path}\\`);\n }\n \n allTasks.push(...tasks);\n }\n \n logger.info(\\`Total tasks found: \\${allTasks.length}\\`);\n \n // Apply filters\n let filteredTasks = allTasks;\n \n // Filter by type\n if (query.data.type !== 'all') {\n filteredTasks = filteredTasks.filter(\n task => task.type.toLowerCase() === query.data.type\n );\n }\n \n // Filter by status\n if (query.data.status !== 'all') {\n const isCompleted = query.data.status === 'completed';\n filteredTasks = filteredTasks.filter(\n task => (task.metadata?.completed || false) === isCompleted\n );\n }\n \n // Filter by priority\n if (query.data.priority !== 'all') {\n filteredTasks = filteredTasks.filter(\n task => (task.metadata?.priority || 'low') === query.data.priority\n );\n }\n \n // Search filter\n if (query.data.search) {\n const searchLower = query.data.search.toLowerCase();\n filteredTasks = filteredTasks.filter(\n task => \n task.text.toLowerCase().includes(searchLower) ||\n task.file.toLowerCase().includes(searchLower) ||\n task.context?.toLowerCase().includes(searchLower)\n );\n }\n \n // Sort tasks\n const sortedTasks = sortTasks(filteredTasks, query.data.sortBy, query.data.sortOrder);\n \n // Apply pagination\n const totalCount = sortedTasks.length;\n const paginatedTasks = sortedTasks.slice(\n query.data.offset,\n query.data.offset + query.data.limit\n );\n \n // Group by type and calculate stats\n const grouped = extractor.groupByType(filteredTasks);\n const stats = {\n total: totalCount,\n byType: Object.entries(grouped).map(([type, tasks]) => ({\n type,\n count: tasks.length,\n completed: tasks.filter(t => t.metadata?.completed).length,\n })),\n byPriority: {\n high: filteredTasks.filter(t => t.metadata?.priority === 'high').length,\n medium: filteredTasks.filter(t => t.metadata?.priority === 'medium').length,\n low: filteredTasks.filter(t => t.metadata?.priority === 'low').length,\n },\n };\n \n // Enhanced response format with file and path information\n const enhancedTasks = paginatedTasks.map(task => ({\n ...task,\n id: task.id || \\`\\${task.file}-\\${task.line}\\`,\n path: task.file, // Ensure path is included\n title: task.text, // Ensure title is included\n status: task.metadata?.completed ? 'completed' : 'pending',\n }));\n \n const response = {\n tasks: enhancedTasks,\n pagination: {\n total: totalCount,\n limit: query.data.limit,\n offset: query.data.offset,\n hasMore: query.data.offset + query.data.limit < totalCount,\n },\n stats,\n query: query.data,\n };\n \n // Cache the response\n cache.set(cacheKey, response);\n \n return NextResponse.json(response);\n }, requestId, { operation: 'taskScan' });\n}\n\n// POST: Update task status\nexport async function POST(request: NextRequest) {\n const requestId = getRequestId(request);\n \n return await withErrorHandling(async () => {\n // Load dependencies with fallbacks\n const deps = await loadDependenciesWithFallbacks(requestId);\n const { Logger } = deps;\n const logger = new Logger({ prefix: 'TaskUpdate' });\n const body = await request.json();\n const { taskId, updates } = body;\n \n if (!taskId || !updates) {\n throw ErrorFactory.invalidRequest(\n 'taskId and updates are required',\n !taskId ? 'taskId' : 'updates',\n requestId\n );\n }\n \n // In a real implementation, you would:\n // 1. Find the task by ID\n // 2. Update the task in the source file\n // 3. Return the updated task\n \n logger.info(\\`Task update requested for: \\${taskId}\\`);\n \n return NextResponse.json({\n success: true,\n taskId,\n updates,\n warning: 'Task updating is disabled in template. Implement proper task updating.',\n });\n }, requestId, { operation: 'taskUpdate', taskId });\n}\n\n// Helper function to sort tasks\nfunction sortTasks(\n tasks: Task[],\n sortBy: string,\n sortOrder: 'asc' | 'desc'\n): Task[] {\n const sorted = [...tasks].sort((a, b) => {\n let compareValue = 0;\n \n switch (sortBy) {\n case 'priority':\n const priorityOrder: Record<string, number> = { high: 3, medium: 2, low: 1 };\n const aPriority = priorityOrder[a.metadata?.priority || 'low'] || 1;\n const bPriority = priorityOrder[b.metadata?.priority || 'low'] || 1;\n compareValue = aPriority - bPriority;\n break;\n \n case 'date':\n const aDate = a.metadata?.createdAt || 0;\n const bDate = b.metadata?.createdAt || 0;\n compareValue = aDate - bDate;\n break;\n \n case 'type':\n compareValue = a.type.localeCompare(b.type);\n break;\n \n case 'path':\n compareValue = a.file.localeCompare(b.file);\n break;\n }\n \n return sortOrder === 'asc' ? compareValue : -compareValue;\n });\n \n return sorted;\n}\n\n// Export metadata for Next.js\nexport const metadata = {\n title: 'Task Scanner API',\n description: 'Scan and manage tasks in your codebase',\n};`,\n 'pages-router/tasks-stream.ts': `import type { NextApiRequest, NextApiResponse } from 'next';\nimport { FileScanner, TaskExtractor, CacheManager } from '@vibeship/devtools/server';\nimport { getConfig } from '@/lib/vibeship-config';\n\n// Store active connections\nconst clients = new Map<string, NextApiResponse>();\nlet lastTaskHash: string | null = null;\n\nexport default async function handler(\n req: NextApiRequest,\n res: NextApiResponse\n) {\n if (req.method !== 'GET') {\n res.setHeader('Allow', ['GET']);\n return res.status(405).end(\\`Method \\${req.method} Not Allowed\\`);\n }\n \n // Set SSE headers\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache, no-transform');\n res.setHeader('Connection', 'keep-alive');\n res.setHeader('X-Accel-Buffering', 'no');\n \n const clientId = crypto.randomUUID();\n const { filter, includeCompleted } = req.query;\n \n // Add client to active connections\n clients.set(clientId, res);\n \n // Send initial connection message\n res.write(\\`data: \\${JSON.stringify({ \n type: 'connected', \n clientId,\n timestamp: new Date().toISOString() \n })}\\\\n\\\\n\\`);\n \n // Send initial tasks\n try {\n const tasks = await scanTasks(\n filter as string | undefined, \n includeCompleted === 'true'\n );\n res.write(\\`data: \\${JSON.stringify({\n type: 'tasks',\n tasks,\n timestamp: new Date().toISOString()\n })}\\\\n\\\\n\\`);\n } catch (error) {\n res.write(\\`data: \\${JSON.stringify({\n type: 'error',\n error: error.message,\n timestamp: new Date().toISOString()\n })}\\\\n\\\\n\\`);\n }\n \n // Set up polling for changes\n const pollInterval = setInterval(async () => {\n try {\n const tasks = await scanTasks(\n filter as string | undefined,\n includeCompleted === 'true'\n );\n const currentHash = generateTaskHash(tasks);\n \n if (currentHash !== lastTaskHash) {\n lastTaskHash = currentHash;\n \n // Broadcast to all connected clients\n const updateMessage = \\`data: \\${JSON.stringify({\n type: 'update',\n tasks,\n timestamp: new Date().toISOString()\n })}\\\\n\\\\n\\`;\n \n for (const [id, client] of clients.entries()) {\n try {\n client.write(updateMessage);\n } catch (error) {\n // Client disconnected\n clients.delete(id);\n }\n }\n }\n } catch (error) {\n console.error('Error polling tasks:', error);\n }\n }, 5000);\n \n // Keep connection alive with heartbeat\n const heartbeatInterval = setInterval(() => {\n try {\n res.write(\\`data: \\${JSON.stringify({ \n type: 'heartbeat', \n timestamp: new Date().toISOString() \n })}\\\\n\\\\n\\`);\n } catch (error) {\n // Client disconnected\n clearInterval(heartbeatInterval);\n clearInterval(pollInterval);\n clients.delete(clientId);\n }\n }, 30000);\n \n // Handle client disconnect\n req.on('close', () => {\n clearInterval(heartbeatInterval);\n clearInterval(pollInterval);\n clients.delete(clientId);\n res.end();\n });\n}\n\n// Helper function to scan tasks\nasync function scanTasks(filter?: string, includeCompleted = false) {\n const config = await getConfig();\n const scanner = new FileScanner(config);\n const extractor = new TaskExtractor();\n const cache = new CacheManager({ maxSize: 100, ttl: 60000 });\n \n // Check cache first\n const cacheKey = \\`tasks:\\${filter || 'all'}:\\${includeCompleted}\\`;\n const cached = cache.get(cacheKey);\n if (cached) {\n return cached;\n }\n \n // Scan files\n const files = await scanner.scanWithInfo();\n const allTasks = [];\n \n for (const file of files) {\n const tasks = extractor.extract(file.content, file.path, {\n includeContext: true,\n parseMetadata: true,\n });\n \n // Apply filter if specified\n const filteredTasks = filter \n ? tasks.filter(task => task.type.toLowerCase() === filter.toLowerCase())\n : tasks;\n \n // Filter completed tasks if needed\n const finalTasks = includeCompleted\n ? filteredTasks\n : filteredTasks.filter(task => !task.metadata?.completed);\n \n allTasks.push(...finalTasks);\n }\n \n // Sort by priority and date\n allTasks.sort((a, b) => {\n const priorityOrder = { high: 3, medium: 2, low: 1 };\n const aPriority = priorityOrder[a.metadata?.priority || 'low'];\n const bPriority = priorityOrder[b.metadata?.priority || 'low'];\n \n if (aPriority !== bPriority) {\n return bPriority - aPriority;\n }\n \n const aDate = a.metadata?.createdAt || 0;\n const bDate = b.metadata?.createdAt || 0;\n return bDate - aDate;\n });\n \n // Cache results\n cache.set(cacheKey, allTasks);\n \n return allTasks;\n}\n\n// Generate hash for task comparison\nfunction generateTaskHash(tasks: any[]): string {\n const taskString = JSON.stringify(tasks.map(t => ({\n id: t.id,\n type: t.type,\n text: t.text,\n completed: t.metadata?.completed\n })));\n \n let hash = 0;\n for (let i = 0; i < taskString.length; i++) {\n const char = taskString.charCodeAt(i);\n hash = ((hash << 5) - hash) + char;\n hash = hash & hash;\n }\n \n return hash.toString(36);\n}`,\n 'pages-router/ai-chat.ts': `import type { NextApiRequest, NextApiResponse } from 'next';\nimport { FileScanner, MarkdownParser } from '@vibeship/devtools/server';\nimport { getConfig } from '@/lib/vibeship-config';\n\nexport default async function handler(\n req: NextApiRequest,\n res: NextApiResponse\n) {\n if (req.method !== 'POST') {\n res.setHeader('Allow', ['POST']);\n return res.status(405).end(\\`Method \\${req.method} Not Allowed\\`);\n }\n \n try {\n const { messages, context, stream = true } = req.body;\n \n if (!messages || !Array.isArray(messages)) {\n return res.status(400).json({ error: 'Messages array is required' });\n }\n \n // Get relevant context from markdown files\n const relevantContext = await getRelevantContext(context);\n \n // Prepare system message with context\n const systemMessage = {\n role: 'system',\n content: \\`You are an AI assistant helping with a development project. \n\\${relevantContext ? \\`Here is relevant context from the project:\\\\n\\\\n\\${relevantContext}\\` : ''}\n\nPlease provide helpful, accurate, and concise responses. When discussing code, use proper markdown formatting.\\`\n };\n \n const allMessages = [systemMessage, ...messages];\n \n if (!stream) {\n // Non-streaming response\n const response = await callAIProvider(allMessages, false);\n return res.status(200).json({ message: response });\n }\n \n // Set up SSE for streaming\n res.setHeader('Content-Type', 'text/event-stream');\n res.setHeader('Cache-Control', 'no-cache');\n res.setHeader('Connection', 'keep-alive');\n \n // Call AI provider with streaming\n const aiStream = await callAIProvider(allMessages, true);\n const reader = aiStream.getReader();\n \n // Process the stream\n const processStream = async () => {\n try {\n while (true) {\n const { done, value } = await reader.read();\n \n if (done) {\n res.write('data: [DONE]\\\\n\\\\n');\n res.end();\n break;\n }\n \n // Format as SSE\n const sseMessage = \\`data: \\${JSON.stringify({\n type: 'content',\n content: value,\n timestamp: new Date().toISOString()\n })}\\\\n\\\\n\\`;\n \n res.write(sseMessage);\n }\n } catch (error) {\n const errorMessage = \\`data: \\${JSON.stringify({\n type: 'error',\n error: error.message,\n timestamp: new Date().toISOString()\n })}\\\\n\\\\n\\`;\n \n res.write(errorMessage);\n res.end();\n }\n };\n \n // Handle client disconnect\n req.on('close', () => {\n reader.cancel();\n res.end();\n });\n \n processStream();\n \n } catch (error) {\n console.error('AI chat error:', error);\n \n if (!res.headersSent) {\n res.status(500).json({ error: 'Internal server error' });\n }\n }\n}\n\n// Get relevant context from markdown files\nasync function getRelevantContext(contextRequest?: {\n files?: string[];\n search?: string;\n limit?: number;\n}): Promise<string> {\n if (!contextRequest) return '';\n \n const config = await getConfig();\n const scanner = new FileScanner(config);\n const parser = new MarkdownParser();\n \n let contexts: string[] = [];\n \n // Get specific files if requested\n if (contextRequest.files?.length) {\n for (const file of contextRequest.files) {\n try {\n const content = await scanner.readFile(file);\n const parsed = parser.parse(content);\n contexts.push(\\`File: \\${file}\\\\n\\${parsed.content.slice(0, 1000)}\\`);\n } catch (error) {\n console.error(\\`Error reading file \\${file}:\\`, error);\n }\n }\n }\n \n // Search for relevant content\n if (contextRequest.search) {\n const files = await scanner.scan();\n const relevantFiles = files\n .filter(file => file.endsWith('.md') || file.endsWith('.mdx'))\n .slice(0, contextRequest.limit || 5);