UNPKG

vibetunnel

Version:

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

817 lines 110 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; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.createGitRoutes = createGitRoutes; const express_1 = require("express"); const path = __importStar(require("path")); const util_1 = require("util"); const session_manager_js_1 = require("../pty/session-manager.js"); const git_error_js_1 = require("../utils/git-error.js"); const git_utils_js_1 = require("../utils/git-utils.js"); const logger_js_1 = require("../utils/logger.js"); const path_utils_js_1 = require("../utils/path-utils.js"); const control_protocol_js_1 = require("../websocket/control-protocol.js"); const control_unix_handler_js_1 = require("../websocket/control-unix-handler.js"); const logger = (0, logger_js_1.createLogger)('git-routes'); const execFile = (0, util_1.promisify)(require('child_process').execFile); // Store for pending notifications when macOS client is not connected const pendingNotifications = []; const repoLocks = new Map(); /** * Acquire a lock for a repository path * @param repoPath The repository path to lock * @returns A promise that resolves when the lock is acquired */ async function acquireRepoLock(repoPath) { return new Promise((resolve) => { let lock = repoLocks.get(repoPath); if (!lock) { lock = { isLocked: false, queue: [] }; repoLocks.set(repoPath, lock); } if (!lock.isLocked) { lock.isLocked = true; resolve(); } else { lock.queue.push(resolve); } }); } /** * Release a lock for a repository path * @param repoPath The repository path to unlock */ function releaseRepoLock(repoPath) { const lock = repoLocks.get(repoPath); if (!lock) { return; } if (lock.queue.length > 0) { const next = lock.queue.shift(); if (next) { next(); } } else { lock.isLocked = false; } } /** * Execute a git command with proper error handling and security * @param args Git command arguments * @param options Execution options * @returns Command output */ async function execGit(args, options = {}) { try { const { stdout, stderr } = await execFile('git', args, { cwd: options.cwd || process.cwd(), timeout: options.timeout || 5000, maxBuffer: 1024 * 1024, // 1MB env: { ...process.env, GIT_TERMINAL_PROMPT: '0' }, // Disable git prompts }); return { stdout: stdout.toString(), stderr: stderr.toString() }; } catch (error) { // Re-throw with more context throw (0, git_error_js_1.createGitError)(error, 'Git command failed'); } } /** * Create Git-related routes */ function createGitRoutes() { const router = (0, express_1.Router)(); /** * GET /api/git/repo-info * Check if a path is within a Git repository */ router.get('/git/repo-info', async (req, res) => { try { const { path: queryPath } = req.query; logger.info(`🔍 [git/repo-info] Received request for path: ${queryPath}`); if (!queryPath || typeof queryPath !== 'string') { logger.warn('❌ Missing or invalid path parameter'); return res.status(400).json({ error: 'Missing or invalid path parameter', }); } // Resolve the path to absolute, expanding tilde if present const absolutePath = (0, path_utils_js_1.resolveAbsolutePath)(queryPath); logger.info(`🔍 [git/repo-info] Resolved ${queryPath} to absolute path: ${absolutePath}`); try { // Use git rev-parse to find the repository root const { stdout } = await execGit(['rev-parse', '--show-toplevel'], { cwd: absolutePath, }); const repoPath = stdout.trim(); const response = { isGitRepo: true, repoPath, }; logger.info(`✅ [git/repo-info] Path is in git repo: ${repoPath}`); return res.json(response); } catch (error) { // If git command fails, it's not a git repo if ((0, git_error_js_1.isGitNotFoundError)(error)) { logger.debug('Git command not found'); return res.json({ isGitRepo: false }); } // Git returns exit code 128 when not in a git repo if ((0, git_error_js_1.isNotGitRepositoryError)(error)) { logger.info(`❌ [git/repo-info] Path is not in a git repository: ${absolutePath}`); return res.json({ isGitRepo: false }); } // Unexpected error throw error; } } catch (error) { logger.error('Error checking git repo info:', error); return res.status(500).json({ error: 'Failed to check git repository info', }); } }); /** * POST /api/git/event * Handle Git repository change events with locking to prevent race conditions */ router.post('/git/event', async (req, res) => { let lockAcquired = false; let repoPath; try { const { repoPath: requestedRepoPath, branch, event } = req.body; if (!requestedRepoPath || typeof requestedRepoPath !== 'string') { return res.status(400).json({ error: 'Missing or invalid repoPath parameter', }); } // Normalize the repository path repoPath = path.resolve(requestedRepoPath); logger.debug(`Processing git event for repo: ${repoPath}, branch: ${branch}, event: ${event}`); // Acquire lock for this repository await acquireRepoLock(repoPath); lockAcquired = true; // Get all sessions and find those within the repository path const sessionManager = new session_manager_js_1.SessionManager(); const allSessions = sessionManager.listSessions(); const sessionsInRepo = allSessions.filter((session) => { if (!session.workingDir || !repoPath) return false; const sessionPath = path.resolve(session.workingDir); return sessionPath.startsWith(repoPath); }); logger.debug(`Found ${sessionsInRepo.length} sessions in repository ${repoPath}`); const updatedSessionIds = []; // Check follow mode status let followWorktree; let currentBranch; let followMode = false; let isMainRepo = false; let isWorktreeRepo = false; try { // Check if this is a worktree const { stdout: gitDirOutput } = await execGit(['rev-parse', '--git-dir'], { cwd: repoPath, }); const gitDir = gitDirOutput.trim(); isWorktreeRepo = gitDir.includes('/.git/worktrees/'); // If this is a worktree, find the main repo let mainRepoPath = repoPath; if (isWorktreeRepo) { // Extract main repo from git dir (e.g., /path/to/main/.git/worktrees/branch) mainRepoPath = gitDir.replace(/\/\.git\/worktrees\/.*$/, ''); logger.debug(`Worktree detected, main repo: ${mainRepoPath}`); } else { isMainRepo = true; } // Get follow worktree setting from main repo const { stdout: followWorktreeOutput } = await execGit(['config', 'vibetunnel.followWorktree'], { cwd: mainRepoPath, }); followWorktree = followWorktreeOutput.trim(); followMode = !!followWorktree; // Get current branch const { stdout: branchOutput } = await execGit(['branch', '--show-current'], { cwd: repoPath, }); currentBranch = branchOutput.trim(); } catch (error) { // Config not set or git command failed - follow mode is disabled logger.debug('Follow worktree check failed or not configured:', error); } // Extract repository name from path const _repoName = path.basename(repoPath); // Update session titles for all sessions in the repository for (const session of sessionsInRepo) { try { // Get the branch for this specific session's working directory let _sessionBranch = currentBranch; try { const { stdout: sessionBranchOutput } = await execGit(['branch', '--show-current'], { cwd: session.workingDir, }); if (sessionBranchOutput.trim()) { _sessionBranch = sessionBranchOutput.trim(); } } catch (_error) { // Use current branch as fallback logger.debug(`Could not get branch for session ${session.id}, using repo branch`); } // Extract base session name (remove any existing git info in square brackets at the end) // Use a more specific regex to only match git-related content in brackets const baseSessionName = session.name?.replace(/\s*\[(checkout|branch|merge|rebase|commit|push|pull|fetch|stash|reset|cherry-pick):[^\]]+\]\s*$/, '') || 'Terminal'; // Construct new title with format: baseSessionName [event: branch] let newTitle = baseSessionName; if (event && branch) { newTitle = `${baseSessionName} [${event}: ${branch}]`; } // Update the session name sessionManager.updateSessionName(session.id, newTitle); updatedSessionIds.push(session.id); logger.debug(`Updated session ${session.id} title to: ${newTitle}`); } catch (error) { logger.error(`Failed to update session ${session.id}:`, error); } } // Handle follow mode sync logic if (followMode && followWorktree) { logger.info(`Follow mode active: processing event from ${repoPath}`); // Determine which repo we're in and which direction to sync if (repoPath === followWorktree && isWorktreeRepo) { // Event from worktree - sync to main repo logger.info(`Syncing from worktree to main repo`); try { // Find the main repo path const { stdout: gitDirOutput } = await execGit(['rev-parse', '--git-dir'], { cwd: repoPath, }); const gitDir = gitDirOutput.trim(); const mainRepoPath = gitDir.replace(/\/\.git\/worktrees\/.*$/, ''); // Get the current branch in worktree const { stdout: worktreeBranchOutput } = await execGit(['branch', '--show-current'], { cwd: repoPath, }); const worktreeBranch = worktreeBranchOutput.trim(); if (worktreeBranch) { // Sync main repo to worktree's branch logger.info(`Syncing main repo to branch: ${worktreeBranch}`); await execGit(['checkout', worktreeBranch], { cwd: mainRepoPath }); // Pull latest changes in main repo await execGit(['pull', '--ff-only'], { cwd: mainRepoPath }); // Send sync success notification const syncNotif = { level: 'info', title: 'Main Repository Synced', message: `Main repository synced to branch '${worktreeBranch}'`, }; if (control_unix_handler_js_1.controlUnixHandler.isMacAppConnected()) { const syncNotification = (0, control_protocol_js_1.createControlEvent)('system', 'notification', syncNotif); control_unix_handler_js_1.controlUnixHandler.sendToMac(syncNotification); } else { pendingNotifications.push({ timestamp: Date.now(), notification: syncNotif, }); } } } catch (error) { logger.error('Failed to sync from worktree to main:', error); // Send error notification const errorNotif = { level: 'error', title: 'Sync Failed', message: `Failed to sync main repository: ${error instanceof Error ? error.message : 'Unknown error'}`, }; if (control_unix_handler_js_1.controlUnixHandler.isMacAppConnected()) { const errorNotification = (0, control_protocol_js_1.createControlEvent)('system', 'notification', errorNotif); control_unix_handler_js_1.controlUnixHandler.sendToMac(errorNotification); } else { pendingNotifications.push({ timestamp: Date.now(), notification: errorNotif, }); } } } else if (isMainRepo && event === 'commit') { // Event from main repo (commit only) - sync to worktree logger.info(`Syncing commit from main repo to worktree`); try { // Pull latest changes in worktree await execGit(['pull', '--ff-only'], { cwd: followWorktree }); // Send sync success notification const syncNotif = { level: 'info', title: 'Worktree Synced', message: `Worktree synced with latest commits`, }; if (control_unix_handler_js_1.controlUnixHandler.isMacAppConnected()) { const syncNotification = (0, control_protocol_js_1.createControlEvent)('system', 'notification', syncNotif); control_unix_handler_js_1.controlUnixHandler.sendToMac(syncNotification); } else { pendingNotifications.push({ timestamp: Date.now(), notification: syncNotif, }); } } catch (error) { logger.error('Failed to sync commit to worktree:', error); } } else if (isMainRepo && event === 'checkout') { // Branch switch in main repo - disable follow mode logger.info('Branch switched in main repo, disabling follow mode'); try { await execGit(['config', '--local', '--unset', 'vibetunnel.followWorktree'], { cwd: repoPath, }); followMode = false; followWorktree = undefined; // Send notification about follow mode being disabled const disableNotif = { level: 'info', title: 'Follow Mode Disabled', message: `Follow mode disabled due to branch switch in main repository`, }; if (control_unix_handler_js_1.controlUnixHandler.isMacAppConnected()) { const disableNotification = (0, control_protocol_js_1.createControlEvent)('system', 'notification', disableNotif); control_unix_handler_js_1.controlUnixHandler.sendToMac(disableNotification); } else { pendingNotifications.push({ timestamp: Date.now(), notification: disableNotif, }); } } catch (error) { logger.error('Failed to disable follow mode:', error); } } } // Create notification payload const notification = { type: 'git-event', repoPath, branch: branch || currentBranch, event, followMode, sessionsUpdated: updatedSessionIds, }; // Prepare notifications const notificationsToSend = []; // Add specific follow mode notifications if (followMode && followWorktree) { const worktreeName = path.basename(followWorktree); notificationsToSend.push({ level: 'info', title: 'Follow Mode Active', message: `Following worktree '${worktreeName}' in ${path.basename(repoPath)}`, }); } // Send notifications via Unix socket to Mac app if connected if (control_unix_handler_js_1.controlUnixHandler.isMacAppConnected()) { // Send repository changed event const controlMessage = (0, control_protocol_js_1.createControlEvent)('git', 'repository-changed', notification); control_unix_handler_js_1.controlUnixHandler.sendToMac(controlMessage); logger.debug('Sent git event notification to Mac app'); // Send specific notifications for (const notif of notificationsToSend) { const notificationMessage = (0, control_protocol_js_1.createControlEvent)('system', 'notification', notif); control_unix_handler_js_1.controlUnixHandler.sendToMac(notificationMessage); } } else { // Store notifications for web UI when macOS client is not connected const now = Date.now(); for (const notif of notificationsToSend) { pendingNotifications.push({ timestamp: now, notification: notif, }); } // Keep only notifications from the last 5 minutes const fiveMinutesAgo = now - 5 * 60 * 1000; while (pendingNotifications.length > 0 && pendingNotifications[0].timestamp < fiveMinutesAgo) { pendingNotifications.shift(); } logger.debug(`Stored ${notificationsToSend.length} notifications for web UI`); } // Return success response res.json({ success: true, repoPath, sessionsUpdated: updatedSessionIds.length, followMode, notification, }); } catch (error) { logger.error('Error handling git event:', error); return res.status(500).json({ error: 'Failed to process git event', message: error instanceof Error ? error.message : String(error), }); } finally { // Always release the lock if (lockAcquired && repoPath) { releaseRepoLock(repoPath); } } }); /** * GET /api/git/notifications * Get pending notifications for the web UI */ router.get('/git/notifications', async (_req, res) => { try { // Clean up old notifications (older than 5 minutes) const now = Date.now(); const fiveMinutesAgo = now - 5 * 60 * 1000; while (pendingNotifications.length > 0 && pendingNotifications[0].timestamp < fiveMinutesAgo) { pendingNotifications.shift(); } // Return current notifications and clear them const notifications = pendingNotifications.map((n) => n.notification); pendingNotifications.length = 0; logger.debug(`Returning ${notifications.length} pending notifications`); res.json({ notifications }); } catch (error) { logger.error('Error fetching notifications:', error); return res.status(500).json({ error: 'Failed to fetch notifications', }); } }); /** * GET /api/git/status * Get repository status with file counts and branch info */ router.get('/git/status', async (req, res) => { try { const { path: queryPath } = req.query; if (!queryPath || typeof queryPath !== 'string') { return res.status(400).json({ error: 'Missing or invalid path parameter', }); } // Resolve the path to absolute const absolutePath = (0, path_utils_js_1.resolveAbsolutePath)(queryPath); logger.debug(`Getting git status for path: ${absolutePath}`); try { // Get repository root const { stdout: repoPathOutput } = await execGit(['rev-parse', '--show-toplevel'], { cwd: absolutePath, }); const repoPath = repoPathOutput.trim(); // Get current branch const { stdout: branchOutput } = await execGit(['branch', '--show-current'], { cwd: repoPath, }); const currentBranch = branchOutput.trim(); // Get status in porcelain format const { stdout: statusOutput } = await execGit(['status', '--porcelain=v1'], { cwd: repoPath, }); // Parse status output const lines = statusOutput .trim() .split('\n') .filter((line) => line.length > 0); let modifiedCount = 0; let untrackedCount = 0; let stagedCount = 0; let addedCount = 0; let deletedCount = 0; for (const line of lines) { if (line.length < 2) continue; const indexStatus = line[0]; const workTreeStatus = line[1]; // Staged changes if (indexStatus !== ' ' && indexStatus !== '?') { stagedCount++; // Count specific types of staged changes if (indexStatus === 'A') { addedCount++; } else if (indexStatus === 'D') { deletedCount++; } } // Working tree changes if (workTreeStatus === 'M') { modifiedCount++; } else if (workTreeStatus === 'D' && indexStatus === ' ') { // Deleted in working tree but not staged deletedCount++; } // Untracked files if (indexStatus === '?' && workTreeStatus === '?') { untrackedCount++; } } // Get ahead/behind counts let aheadCount = 0; let behindCount = 0; let hasUpstream = false; try { // Check if we have an upstream branch const { stdout: upstreamOutput } = await execGit(['rev-parse', '--abbrev-ref', '--symbolic-full-name', '@{u}'], { cwd: repoPath }); if (upstreamOutput.trim()) { hasUpstream = true; // Get ahead/behind counts const { stdout: aheadBehindOutput } = await execGit(['rev-list', '--left-right', '--count', 'HEAD...@{u}'], { cwd: repoPath }); const [ahead, behind] = aheadBehindOutput .trim() .split('\t') .map((n) => Number.parseInt(n, 10)); aheadCount = ahead || 0; behindCount = behind || 0; } } catch (_error) { // No upstream branch configured logger.debug('No upstream branch configured'); } return res.json({ isGitRepo: true, repoPath, currentBranch, hasChanges: lines.length > 0, modifiedCount, untrackedCount, stagedCount, addedCount, deletedCount, aheadCount, behindCount, hasUpstream, }); } catch (error) { if ((0, git_error_js_1.isNotGitRepositoryError)(error)) { return res.json({ isGitRepo: false, }); } throw error; } } catch (error) { logger.error('Error getting git status:', error); return res.status(500).json({ error: 'Failed to get git status', message: error instanceof Error ? error.message : String(error), }); } }); /** * GET /api/git/remote * Get remote URL for a repository */ router.get('/git/remote', async (req, res) => { try { const { path: queryPath } = req.query; if (!queryPath || typeof queryPath !== 'string') { return res.status(400).json({ error: 'Missing or invalid path parameter', }); } // Resolve the path to absolute const absolutePath = (0, path_utils_js_1.resolveAbsolutePath)(queryPath); logger.debug(`Getting git remote for path: ${absolutePath}`); try { // Get repository root const { stdout: repoPathOutput } = await execGit(['rev-parse', '--show-toplevel'], { cwd: absolutePath, }); const repoPath = repoPathOutput.trim(); // Get remote URL const { stdout: remoteOutput } = await execGit(['remote', 'get-url', 'origin'], { cwd: repoPath, }); const remoteUrl = remoteOutput.trim(); // Parse GitHub URL from remote URL let githubUrl = null; if (remoteUrl) { // Handle HTTPS URLs: https://github.com/user/repo.git if (remoteUrl.startsWith('https://github.com/')) { githubUrl = remoteUrl.endsWith('.git') ? remoteUrl.slice(0, -4) : remoteUrl; } // Handle SSH URLs: git@github.com:user/repo.git else if (remoteUrl.startsWith('git@github.com:')) { const pathPart = remoteUrl.substring('git@github.com:'.length); const cleanPath = pathPart.endsWith('.git') ? pathPart.slice(0, -4) : pathPart; githubUrl = `https://github.com/${cleanPath}`; } } return res.json({ isGitRepo: true, repoPath, remoteUrl, githubUrl, }); } catch (error) { if ((0, git_error_js_1.isNotGitRepositoryError)(error)) { return res.json({ isGitRepo: false, }); } // Check if it's just missing remote const errorMessage = error instanceof Error ? error.message : String(error); if (errorMessage.includes('No such remote')) { return res.json({ isGitRepo: true, remoteUrl: null, githubUrl: null, }); } throw error; } } catch (error) { logger.error('Error getting git remote:', error); return res.status(500).json({ error: 'Failed to get git remote', message: error instanceof Error ? error.message : String(error), }); } }); /** * GET /api/git/repository-info * Get comprehensive repository information (combines multiple git commands) */ router.get('/git/repository-info', async (req, res) => { try { const { path: queryPath } = req.query; if (!queryPath || typeof queryPath !== 'string') { return res.status(400).json({ error: 'Missing or invalid path parameter', }); } // Resolve the path to absolute const absolutePath = (0, path_utils_js_1.resolveAbsolutePath)(queryPath); logger.debug(`Getting comprehensive git info for path: ${absolutePath}`); try { // Get repository root const { stdout: repoPathOutput } = await execGit(['rev-parse', '--show-toplevel'], { cwd: absolutePath, }); const repoPath = repoPathOutput.trim(); // Check if this is a worktree const worktreeStatus = await (0, git_utils_js_1.isWorktree)(repoPath); // Gather all information in parallel const [branchResult, statusResult, remoteResult, aheadBehindResult] = await Promise.allSettled([ // Current branch execGit(['branch', '--show-current'], { cwd: repoPath }), // Status execGit(['status', '--porcelain=v1'], { cwd: repoPath }), // Remote URL execGit(['remote', 'get-url', 'origin'], { cwd: repoPath }), // Ahead/behind counts execGit(['rev-list', '--left-right', '--count', 'HEAD...@{u}'], { cwd: repoPath }), ]); // Process results const currentBranch = branchResult.status === 'fulfilled' ? branchResult.value.stdout.trim() : null; // Parse status let modifiedCount = 0; let untrackedCount = 0; let stagedCount = 0; let addedCount = 0; let deletedCount = 0; let hasChanges = false; if (statusResult.status === 'fulfilled') { const lines = statusResult.value.stdout .trim() .split('\n') .filter((line) => line.length > 0); hasChanges = lines.length > 0; for (const line of lines) { if (line.length < 2) continue; const indexStatus = line[0]; const workTreeStatus = line[1]; if (indexStatus !== ' ' && indexStatus !== '?') { stagedCount++; if (indexStatus === 'A') { addedCount++; } else if (indexStatus === 'D') { deletedCount++; } } if (workTreeStatus === 'M') { modifiedCount++; } else if (workTreeStatus === 'D' && indexStatus === ' ') { deletedCount++; } if (indexStatus === '?' && workTreeStatus === '?') { untrackedCount++; } } } // Remote URL const remoteUrl = remoteResult.status === 'fulfilled' ? remoteResult.value.stdout.trim() : null; // Ahead/behind counts let aheadCount = 0; let behindCount = 0; let hasUpstream = false; if (aheadBehindResult.status === 'fulfilled') { hasUpstream = true; const [ahead, behind] = aheadBehindResult.value.stdout .trim() .split('\t') .map((n) => Number.parseInt(n, 10)); aheadCount = ahead || 0; behindCount = behind || 0; } return res.json({ isGitRepo: true, repoPath, currentBranch, remoteUrl, hasChanges, modifiedCount, untrackedCount, stagedCount, addedCount, deletedCount, aheadCount, behindCount, hasUpstream, isWorktree: worktreeStatus, }); } catch (error) { if ((0, git_error_js_1.isNotGitRepositoryError)(error)) { return res.json({ isGitRepo: false, }); } throw error; } } catch (error) { logger.error('Error getting repository info:', error); return res.status(500).json({ error: 'Failed to get repository info', message: error instanceof Error ? error.message : String(error), }); } }); return router; } //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZ2l0LmpzIiwic291cmNlUm9vdCI6IiIsInNvdXJjZXMiOlsiLi4vLi4vLi4vc3JjL3NlcnZlci9yb3V0ZXMvZ2l0LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7O0FBMkhBLDBDQWl5QkM7QUE1NUJELHFDQUFpQztBQUNqQywyQ0FBNkI7QUFDN0IsK0JBQWlDO0FBQ2pDLGtFQUEyRDtBQUMzRCx3REFBb0c7QUFDcEcsd0RBQW1EO0FBQ25ELGtEQUFrRDtBQUNsRCwwREFBNkQ7QUFDN0QsMEVBQXNFO0FBQ3RFLGtGQUEwRTtBQUUxRSxNQUFNLE1BQU0sR0FBRyxJQUFBLHdCQUFZLEVBQUMsWUFBWSxDQUFDLENBQUM7QUFDMUMsTUFBTSxRQUFRLEdBQUcsSUFBQSxnQkFBUyxFQUFDLE9BQU8sQ0FBQyxlQUFlLENBQUMsQ0FBQyxRQUFRLENBQUMsQ0FBQztBQXNCOUQscUVBQXFFO0FBQ3JFLE1BQU0sb0JBQW9CLEdBT3JCLEVBQUUsQ0FBQztBQVFSLE1BQU0sU0FBUyxHQUFHLElBQUksR0FBRyxFQUFvQixDQUFDO0FBRTlDOzs7O0dBSUc7QUFDSCxLQUFLLFVBQVUsZUFBZSxDQUFDLFFBQWdCO0lBQzdDLE9BQU8sSUFBSSxPQUFPLENBQUMsQ0FBQyxPQUFPLEVBQUUsRUFBRTtRQUM3QixJQUFJLElBQUksR0FBRyxTQUFTLENBQUMsR0FBRyxDQUFDLFFBQVEsQ0FBQyxDQUFDO1FBRW5DLElBQUksQ0FBQyxJQUFJLEVBQUUsQ0FBQztZQUNWLElBQUksR0FBRyxFQUFFLFFBQVEsRUFBRSxLQUFLLEVBQUUsS0FBSyxFQUFFLEVBQUUsRUFBRSxDQUFDO1lBQ3RDLFNBQVMsQ0FBQyxHQUFHLENBQUMsUUFBUSxFQUFFLElBQUksQ0FBQyxDQUFDO1FBQ2hDLENBQUM7UUFFRCxJQUFJLENBQUMsSUFBSSxDQUFDLFFBQVEsRUFBRSxDQUFDO1lBQ25CLElBQUksQ0FBQyxRQUFRLEdBQUcsSUFBSSxDQUFDO1lBQ3JC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