UNPKG

vibetunnel

Version:

Terminal sharing server with web interface - supports macOS, Linux, and headless environments

790 lines 113 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.createFilesystemRoutes = createFilesystemRoutes; const chalk_1 = __importDefault(require("chalk")); const child_process_1 = require("child_process"); const express_1 = require("express"); const fs_1 = require("fs"); const fs = __importStar(require("fs/promises")); const mime_types_1 = __importDefault(require("mime-types")); const path = __importStar(require("path")); const util_1 = require("util"); const logger_js_1 = require("../utils/logger.js"); const path_utils_js_1 = require("../utils/path-utils.js"); const logger = (0, logger_js_1.createLogger)('filesystem'); const execAsync = (0, util_1.promisify)(child_process_1.exec); function createFilesystemRoutes() { const router = (0, express_1.Router)(); // Helper to check if path is safe (no directory traversal) - DISABLED for full filesystem access function isPathSafe(_requestedPath, _basePath) { // Security check disabled - allow access to all directories return true; } // Helper to get Git status for a directory async function getGitStatus(dirPath) { try { // Check if directory is a git repository and get repo root const { stdout: repoRoot } = await execAsync('git rev-parse --show-toplevel', { cwd: dirPath, }); const gitRepoRoot = repoRoot.trim(); // Get current branch const { stdout: branch } = await execAsync('git branch --show-current', { cwd: dirPath }); // Get status relative to repository root const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: gitRepoRoot, }); const status = { isGitRepo: true, branch: branch.trim(), modified: [], added: [], deleted: [], untracked: [], }; // Parse git status output statusOutput.split('\n').forEach((line) => { if (!line) return; const statusCode = line.substring(0, 2); const filename = line.substring(3); if (statusCode === ' M' || statusCode === 'M ' || statusCode === 'MM') { status.modified.push(filename); } else if (statusCode === 'A ' || statusCode === 'AM') { status.added.push(filename); } else if (statusCode === ' D' || statusCode === 'D ') { status.deleted.push(filename); } else if (statusCode === '??') { status.untracked.push(filename); } }); return { status, repoRoot: gitRepoRoot }; } catch { return null; } } // Helper to get file Git status function getFileGitStatus(filePath, gitStatus, gitRepoPath) { if (!gitStatus) return undefined; // Get path relative to git repository root const relativePath = path.relative(gitRepoPath, filePath); if (gitStatus.modified.includes(relativePath)) return 'modified'; if (gitStatus.added.includes(relativePath)) return 'added'; if (gitStatus.deleted.includes(relativePath)) return 'deleted'; if (gitStatus.untracked.includes(relativePath)) return 'untracked'; return 'unchanged'; } // Browse directory endpoint router.get('/fs/browse', async (req, res) => { try { let requestedPath = req.query.path || '.'; const showHidden = req.query.showHidden === 'true'; const gitFilter = req.query.gitFilter; // 'all' | 'changed' | 'none' // Handle tilde expansion for home directory requestedPath = (0, path_utils_js_1.expandTildePath)(requestedPath); logger.debug(`browsing directory: ${requestedPath}, showHidden: ${showHidden}, gitFilter: ${gitFilter}`); // Security check if (!isPathSafe(requestedPath, process.cwd())) { logger.warn(`access denied for path: ${requestedPath}`); return res.status(403).json({ error: 'Access denied' }); } const fullPath = path.resolve(requestedPath); // Check if path exists and is a directory let stats; try { stats = await fs.stat(fullPath); } catch (error) { if (error instanceof Error && 'code' in error && error.code === 'ENOENT') { logger.warn(`directory not found: ${requestedPath}`); return res.status(404).json({ error: 'Directory not found' }); } // Re-throw other errors to be handled by outer catch throw error; } if (!stats.isDirectory()) { logger.warn(`path is not a directory: ${requestedPath}`); return res.status(400).json({ error: 'Path is not a directory' }); } // Get Git status if requested const gitStatusStart = Date.now(); const gitInfo = gitFilter !== 'none' ? await getGitStatus(fullPath) : null; const gitStatus = gitInfo?.status || null; const gitRepoRoot = gitInfo?.repoRoot || ''; if (gitFilter !== 'none') { logger.debug(`git status check took ${Date.now() - gitStatusStart}ms for ${requestedPath}`); } let files = []; // If filtering by git changes, show all changed files recursively if (gitFilter === 'changed' && gitStatus) { // Get all changed files from git status const allChangedFiles = [ ...gitStatus.modified.map((f) => ({ path: f, status: 'modified' })), ...gitStatus.added.map((f) => ({ path: f, status: 'added' })), ...gitStatus.deleted.map((f) => ({ path: f, status: 'deleted' })), ...gitStatus.untracked.map((f) => ({ path: f, status: 'untracked' })), ]; // Filter to only files under the current directory const currentDirRelativeToRepo = path.relative(gitRepoRoot, fullPath); const relevantFiles = allChangedFiles.filter((f) => { // If we're at repo root, show all files if (fullPath === gitRepoRoot) return true; // Otherwise, only show files under current directory return f.path.startsWith(`${currentDirRelativeToRepo}/`); }); // Convert to FileInfo objects files = await Promise.all(relevantFiles.map(async (changedFile) => { const absolutePath = path.join(gitRepoRoot, changedFile.path); // Check if file exists (it might be deleted) let fileStats = null; let fileType = 'file'; try { fileStats = await fs.stat(absolutePath); fileType = fileStats.isDirectory() ? 'directory' : 'file'; } catch { // File might be deleted fileStats = null; } // Get relative display name (relative to current directory) const relativeToCurrentDir = path.relative(fullPath, absolutePath); const fileInfo = { name: relativeToCurrentDir, path: path.relative(process.cwd(), absolutePath), type: fileType, size: fileStats?.size || 0, modified: fileStats?.mtime.toISOString() || new Date().toISOString(), permissions: fileStats?.mode?.toString(8).slice(-3) || '000', isGitTracked: true, gitStatus: changedFile.status, }; return fileInfo; })); } else { // Normal directory listing const entries = await fs.readdir(fullPath, { withFileTypes: true }); files = await Promise.all(entries .filter((entry) => showHidden || !entry.name.startsWith('.')) .map(async (entry) => { const entryPath = path.join(fullPath, entry.name); try { // Use fs.stat() which follows symlinks, instead of entry.isDirectory() const stats = await fs.stat(entryPath); const relativePath = path.relative(process.cwd(), entryPath); // Check if this is a symlink const isSymlink = entry.isSymbolicLink(); const fileInfo = { name: entry.name, path: relativePath, type: stats.isDirectory() ? 'directory' : 'file', size: stats.size, modified: stats.mtime.toISOString(), permissions: stats.mode.toString(8).slice(-3), isGitTracked: gitStatus?.isGitRepo || false, gitStatus: getFileGitStatus(entryPath, gitStatus, gitRepoRoot), isSymlink, }; return fileInfo; } catch (error) { // Handle broken symlinks or permission errors logger.warn(`failed to stat ${entryPath}:`, error); // For broken symlinks, we'll still show them but as files const fileInfo = { name: entry.name, path: path.relative(process.cwd(), entryPath), type: 'file', size: 0, modified: new Date().toISOString(), permissions: '000', isGitTracked: false, gitStatus: undefined, }; return fileInfo; } })); } // No additional filtering needed if we already filtered by git status above const filteredFiles = files; // Sort: directories first, then by name filteredFiles.sort((a, b) => { if (a.type !== b.type) { return a.type === 'directory' ? -1 : 1; } return a.name.localeCompare(b.name); }); logger.debug(`directory browsed successfully: ${requestedPath} (${filteredFiles.length} items)`); res.json({ path: requestedPath, fullPath, gitStatus, files: filteredFiles, }); } catch (error) { logger.error(`failed to browse directory ${req.query.path}:`, error); res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Get file preview router.get('/fs/preview', async (req, res) => { try { const requestedPath = req.query.path; if (!requestedPath) { return res.status(400).json({ error: 'Path is required' }); } logger.debug(`previewing file: ${requestedPath}`); // Security check if (!isPathSafe(requestedPath, process.cwd())) { logger.warn(`access denied for file preview: ${requestedPath}`); return res.status(403).json({ error: 'Access denied' }); } const fullPath = path.resolve(process.cwd(), requestedPath); const stats = await fs.stat(fullPath); if (stats.isDirectory()) { logger.warn(`cannot preview directory: ${requestedPath}`); return res.status(400).json({ error: 'Cannot preview directories' }); } // Determine file type const mimeType = mime_types_1.default.lookup(fullPath) || 'application/octet-stream'; const isText = mimeType.startsWith('text/') || mimeType === 'application/json' || mimeType === 'application/javascript' || mimeType === 'application/typescript' || mimeType === 'application/xml'; const isImage = mimeType.startsWith('image/'); if (isImage) { // For images, return URL to fetch the image logger.log(chalk_1.default.green(`image preview generated: ${requestedPath} (${formatBytes(stats.size)})`)); res.json({ type: 'image', mimeType, url: `/api/fs/raw?path=${encodeURIComponent(requestedPath)}`, size: stats.size, }); } else if (isText || stats.size < 1024 * 1024) { // Text or small files (< 1MB) const content = await fs.readFile(fullPath, 'utf-8'); const language = getLanguageFromPath(fullPath); logger.log(chalk_1.default.green(`text file preview generated: ${requestedPath} (${formatBytes(stats.size)}, ${language})`)); res.json({ type: 'text', content, language, mimeType, size: stats.size, }); } else { // Binary or large files logger.log(`binary file preview metadata returned: ${requestedPath} (${formatBytes(stats.size)})`); res.json({ type: 'binary', mimeType, size: stats.size, humanSize: formatBytes(stats.size), }); } } catch (error) { logger.error(`failed to preview file ${req.query.path}:`, error); res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Serve raw file content router.get('/fs/raw', (req, res) => { try { const requestedPath = req.query.path; if (!requestedPath) { return res.status(400).json({ error: 'Path is required' }); } logger.debug(`serving raw file: ${requestedPath}`); // Security check if (!isPathSafe(requestedPath, process.cwd())) { logger.warn(`access denied for raw file: ${requestedPath}`); return res.status(403).json({ error: 'Access denied' }); } const fullPath = path.resolve(process.cwd(), requestedPath); // Check if file exists if (!(0, fs_1.statSync)(fullPath).isFile()) { logger.warn(`file not found for raw access: ${requestedPath}`); return res.status(404).json({ error: 'File not found' }); } // Set appropriate content type const mimeType = mime_types_1.default.lookup(fullPath) || 'application/octet-stream'; res.setHeader('Content-Type', mimeType); // Stream the file const stream = (0, fs_1.createReadStream)(fullPath); stream.pipe(res); stream.on('end', () => { logger.log(chalk_1.default.green(`raw file served: ${requestedPath}`)); }); } catch (error) { logger.error(`failed to serve raw file ${req.query.path}:`, error); res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Get file content (text files only) router.get('/fs/content', async (req, res) => { try { const requestedPath = req.query.path; if (!requestedPath) { return res.status(400).json({ error: 'Path is required' }); } logger.debug(`getting file content: ${requestedPath}`); // Security check if (!isPathSafe(requestedPath, process.cwd())) { logger.warn(`access denied for file content: ${requestedPath}`); return res.status(403).json({ error: 'Access denied' }); } const fullPath = path.resolve(process.cwd(), requestedPath); const content = await fs.readFile(fullPath, 'utf-8'); logger.log(chalk_1.default.green(`file content retrieved: ${requestedPath}`)); res.json({ path: requestedPath, content, language: getLanguageFromPath(fullPath), }); } catch (error) { logger.error(`failed to get file content ${req.query.path}:`, error); res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Get Git diff for a file router.get('/fs/diff', async (req, res) => { try { const requestedPath = req.query.path; if (!requestedPath) { return res.status(400).json({ error: 'Path is required' }); } logger.debug(`getting git diff: ${requestedPath}`); // Security check if (!isPathSafe(requestedPath, process.cwd())) { logger.warn(`access denied for git diff: ${requestedPath}`); return res.status(403).json({ error: 'Access denied' }); } const fullPath = path.resolve(process.cwd(), requestedPath); const relativePath = path.relative(process.cwd(), fullPath); // Get git diff const diffStart = Date.now(); const { stdout: diff } = await execAsync(`git diff HEAD -- "${relativePath}"`, { cwd: process.cwd(), }); const diffTime = Date.now() - diffStart; if (diffTime > 1000) { logger.warn(`slow git diff operation: ${requestedPath} took ${diffTime}ms`); } logger.log(chalk_1.default.green(`git diff retrieved: ${requestedPath} (${diff.length > 0 ? 'has changes' : 'no changes'})`)); res.json({ path: requestedPath, diff, hasDiff: diff.length > 0, }); } catch (error) { logger.error(`failed to get git diff for ${req.query.path}:`, error); res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Get file content for diff view (current and HEAD versions) router.get('/fs/diff-content', async (req, res) => { try { const requestedPath = req.query.path; if (!requestedPath) { return res.status(400).json({ error: 'Path is required' }); } logger.debug(`getting diff content: ${requestedPath}`); // Security check if (!isPathSafe(requestedPath, process.cwd())) { logger.warn(`access denied for diff content: ${requestedPath}`); return res.status(403).json({ error: 'Access denied' }); } const fullPath = path.resolve(process.cwd(), requestedPath); const relativePath = path.relative(process.cwd(), fullPath); logger.debug(`Getting diff content for: ${requestedPath}`); logger.debug(`Full path: ${fullPath}`); logger.debug(`CWD: ${process.cwd()}`); // Get current file content const currentContent = await fs.readFile(fullPath, 'utf-8'); logger.debug(`Current content length: ${currentContent.length}`); // Get HEAD version content let originalContent = ''; // Default to empty string for new files try { // Use ./ prefix as git suggests for paths relative to current directory const gitPath = `./${relativePath}`; logger.debug(`Getting HEAD version: git show HEAD:"${gitPath}"`); const { stdout } = await execAsync(`git show HEAD:"${gitPath}"`, { cwd: process.cwd(), }); originalContent = stdout; logger.debug(`Got HEAD version for ${gitPath}, length: ${originalContent.length}`); } catch (error) { // File might be new (not in HEAD), use empty string if (error instanceof Error && error.message.includes('does not exist')) { originalContent = ''; logger.debug(`File ${requestedPath} does not exist in HEAD (new file)`); } else { // For other errors, log the full error logger.error(`Failed to get HEAD version of ./${relativePath}:`, error); // Check if it's a stderr message if (error instanceof Error && 'stderr' in error) { const execError = error; if (execError.stderr) { logger.error(`Git stderr: ${execError.stderr}`); } } // For non-git repos, show no diff originalContent = currentContent; } } logger.log(chalk_1.default.green(`diff content retrieved: ${requestedPath}`)); res.json({ path: requestedPath, originalContent, modifiedContent: currentContent, language: getLanguageFromPath(fullPath), }); } catch (error) { logger.error(`failed to get diff content for ${req.query.path}:`, error); res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Create directory router.post('/fs/mkdir', async (req, res) => { try { const { path: dirPath, name } = req.body; if (!dirPath || !name) { return res.status(400).json({ error: 'Path and name are required' }); } logger.log(`creating directory: ${name} in ${dirPath}`); // Validate name (no slashes, no dots at start) if (name.includes('/') || name.includes('\\') || name.startsWith('.')) { logger.warn(`invalid directory name attempted: ${name}`); return res.status(400).json({ error: 'Invalid directory name' }); } // Security check if (!isPathSafe(dirPath, process.cwd())) { logger.warn(`access denied for mkdir: ${dirPath}/${name}`); return res.status(403).json({ error: 'Access denied' }); } const fullPath = path.resolve(process.cwd(), dirPath, name); // Create directory await fs.mkdir(fullPath, { recursive: true }); logger.log(chalk_1.default.green(`directory created: ${path.relative(process.cwd(), fullPath)}`)); res.json({ success: true, path: path.relative(process.cwd(), fullPath), }); } catch (error) { logger.error(`failed to create directory ${req.body.path}/${req.body.name}:`, error); res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); // Path completions endpoint for autocomplete router.get('/fs/completions', async (req, res) => { try { const originalPath = req.query.path || ''; let partialPath = originalPath; // Handle tilde expansion for home directory partialPath = (0, path_utils_js_1.expandTildePath)(partialPath); // Separate directory and partial name let dirPath; let partialName; if (partialPath.endsWith('/')) { // If path ends with slash, list contents of that directory dirPath = partialPath; partialName = ''; } else { // Otherwise, get the directory and partial filename dirPath = path.dirname(partialPath); partialName = path.basename(partialPath); } // Resolve the directory path const fullDirPath = path.resolve(dirPath); // Security check if (!isPathSafe(fullDirPath, '/')) { logger.warn(`access denied for path completions: ${fullDirPath}`); return res.status(403).json({ error: 'Access denied' }); } // Check if directory exists let dirStats; try { dirStats = await fs.stat(fullDirPath); if (!dirStats.isDirectory()) { return res.json({ completions: [] }); } } catch { // Directory doesn't exist, return empty completions return res.json({ completions: [] }); } // Read directory contents const entries = await fs.readdir(fullDirPath, { withFileTypes: true }); // Filter and map entries const mappedEntries = await Promise.all(entries .filter((entry) => { // Filter by partial name (case-insensitive) if (partialName && !entry.name.toLowerCase().startsWith(partialName.toLowerCase())) { return false; } // Optionally hide hidden files unless the partial name starts with '.' if (!partialName.startsWith('.') && entry.name.startsWith('.')) { return false; } return true; }) .map(async (entry) => { const isDirectory = entry.isDirectory(); const entryPath = path.join(fullDirPath, entry.name); // Build the suggestion path based on the original input let displayPath; if (originalPath.endsWith('/')) { displayPath = originalPath + entry.name; } else { const lastSlash = originalPath.lastIndexOf('/'); if (lastSlash >= 0) { displayPath = originalPath.substring(0, lastSlash + 1) + entry.name; } else { displayPath = entry.name; } } // Check if this directory is a git repository and get branch + status let isGitRepo = false; let gitBranch; let gitStatusCount = 0; let gitAddedCount = 0; let gitModifiedCount = 0; let gitDeletedCount = 0; let isWorktree = false; if (isDirectory) { try { const gitPath = path.join(entryPath, '.git'); const gitStat = await fs.stat(gitPath); isGitRepo = true; // Check if it's a worktree (has a .git file instead of directory) if (gitStat.isFile()) { isWorktree = true; } // Get the current git branch try { const { stdout: branch } = await execAsync('git branch --show-current', { cwd: entryPath, }); gitBranch = branch.trim(); } catch { // Failed to get branch } // Get the number of changed files by type try { const { stdout: statusOutput } = await execAsync('git status --porcelain', { cwd: entryPath, }); const lines = statusOutput.split('\n').filter((line) => line.trim() !== ''); for (const line of lines) { const statusCode = line.substring(0, 2); if (statusCode === '??' || statusCode === 'A ' || statusCode === 'AM') { gitAddedCount++; } else if (statusCode === ' D' || statusCode === 'D ') { gitDeletedCount++; } else if (statusCode === ' M' || statusCode === 'M ' || statusCode === 'MM') { gitModifiedCount++; } } gitStatusCount = gitAddedCount + gitModifiedCount + gitDeletedCount; } catch { // Failed to get status } } catch { // Not a git repository } } return { name: entry.name, path: displayPath, type: isDirectory ? 'directory' : 'file', // Add trailing slash for directories suggestion: isDirectory ? `${displayPath}/` : displayPath, isRepository: isGitRepo, gitBranch, gitStatusCount, gitAddedCount, gitModifiedCount, gitDeletedCount, isWorktree, }; })); const completions = mappedEntries .sort((a, b) => { // Sort directories first, then by name if (a.type !== b.type) { return a.type === 'directory' ? -1 : 1; } return a.name.localeCompare(b.name); }) .slice(0, 20); // Limit to 20 suggestions logger.debug(`path completions for "${originalPath}": ${completions.length} results`); res.json({ completions, partialPath: originalPath, }); } catch (error) { logger.error(`failed to get path completions for ${req.query.path}:`, error); res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } }); return router; } // Helper function to determine language from file path function getLanguageFromPath(filePath) { const ext = path.extname(filePath).toLowerCase(); const languageMap = { '.js': 'javascript', '.jsx': 'javascript', '.ts': 'typescript', '.tsx': 'typescript', '.py': 'python', '.java': 'java', '.c': 'c', '.cpp': 'cpp', '.cs': 'csharp', '.php': 'php', '.rb': 'ruby', '.go': 'go', '.rs': 'rust', '.swift': 'swift', '.kt': 'kotlin', '.scala': 'scala', '.r': 'r', '.m': 'objective-c', '.mm': 'objective-c', '.h': 'c', '.hpp': 'cpp', '.sh': 'shell', '.bash': 'shell', '.zsh': 'shell', '.fish': 'shell', '.ps1': 'powershell', '.html': 'html', '.htm': 'html', '.xml': 'xml', '.css': 'css', '.scss': 'scss', '.sass': 'sass', '.less': 'less', '.json': 'json', '.yaml': 'yaml', '.yml': 'yaml', '.toml': 'toml', '.ini': 'ini', '.cfg': 'ini', '.conf': 'ini', '.sql': 'sql', '.md': 'markdown', '.markdown': 'markdown', '.tex': 'latex', '.dockerfile': 'dockerfile', '.makefile': 'makefile', '.cmake': 'cmake', '.gradle': 'gradle', '.vue': 'vue', '.svelte': 'svelte', '.elm': 'elm', '.clj': 'clojure', '.cljs': 'clojure', '.ex': 'elixir', '.exs': 'elixir', '.erl': 'erlang', '.hrl': 'erlang', '.fs': 'fsharp', '.fsx': 'fsharp', '.fsi': 'fsharp', '.ml': 'ocaml', '.mli': 'ocaml', '.pas': 'pascal', '.pp': 'pascal', '.pl': 'perl', '.pm': 'perl', '.t': 'perl', '.lua': 'lua', '.dart': 'dart', '.nim': 'nim', '.nims': 'nim', '.zig': 'zig', '.jl': 'julia', }; return languageMap[ext] || 'plaintext'; } // Helper function to format bytes function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 Bytes'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return `${Number.parseFloat((bytes / k ** i).toFixed(dm))} ${sizes[i]}`; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZmlsZXN5c3RlbS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3NyYy9zZXJ2ZXIvcm91dGVzL2ZpbGVzeXN0ZW0udHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFvQ0Esd0RBK3VCQztBQW54QkQsa0RBQTBCO0FBQzFCLGlEQUFxQztBQUNyQyxxQ0FBOEQ7QUFDOUQsMkJBQWdEO0FBQ2hELGdEQUFrQztBQUNsQyw0REFBOEI7QUFDOUIsMkNBQTZCO0FBQzdCLCtCQUFpQztBQUNqQyxrREFBa0Q7QUFDbEQsMERBQXlEO0FBRXpELE1BQU0sTUFBTSxHQUFHLElBQUEsd0JBQVksRUFBQyxZQUFZLENBQUMsQ0FBQztBQUUxQyxNQUFNLFNBQVMsR0FBRyxJQUFBLGdCQUFTLEVBQUMsb0JBQUksQ0FBQyxDQUFDO0FBdUJsQyxTQUFnQixzQkFBc0I7SUFDcEMsTUFBTSxNQUFNLEdBQUcsSUFBQSxnQkFBTSxHQUFFLENBQUM7SUFFeEIsaUdBQWlHO0lBQ2pHLFNBQVMsVUFBVSxDQUFDLGNBQXNCLEVBQUUsU0FBaUI7UUFDM0QsNERBQTREO1FBQzVELE9BQU8sSUFBSSxDQUFDO0lBQ2QsQ0FBQztJQUVELDJDQUEyQztJQUMzQyxLQUFLLFVBQVUsWUFBWSxDQUN6QixPQUFlO1FBRWYsSUFBSSxDQUFDO1lBQ0gsMkRBQTJEO1lBQzNELE1BQU0sRUFBRSxNQUFNLEVBQUUsUUFBUSxFQUFFLEdBQUcsTUFBTSxTQUFTLENBQUMsK0JBQStCLEVBQUU7Z0JBQzVFLEdBQUcsRUFBRSxPQUFPO2FBQ2IsQ0FBQyxDQUFDO1lBQ0gsTUFBTSxXQUFXLEdBQUcsUUFBUSxDQUFDLElBQUksRUFBRSxDQUFDO1lBRXBDLHFCQUFxQjtZQUNyQixNQUFNLEVBQUUsTUFBTSxFQUFFLE1BQU0sRUFBRSxHQUFHLE1BQU0sU0FBUyxDQUFDLDJCQUEyQixFQUFFLEVBQUUsR0FBRyxFQUFFLE9BQU8sRUFB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