vibetunnel
Version:
Terminal sharing server with web interface - supports macOS, Linux, and headless environments
817 lines • 110 kB
JavaScript
"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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