import express from 'express';
import { promises as fs } from 'fs';
import path from 'path';
import { spawn } from 'child_process';
import os from 'os';
import { addProjectManually, extractProjectDirectory } from '../projects.js';
const router = express.Router();
function sanitizeGitError(message, token) {
    if (!message || !token)
        return message;
    return message.replace(new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '***');
}
// Configure allowed workspace root (defaults to user's home directory)
export const WORKSPACES_ROOT = process.env.WORKSPACES_ROOT || os.homedir();
export const WORKSPACES_BASE = path.resolve(process.env.WORKSPACES_BASE || path.join(WORKSPACES_ROOT, 'pixcode', 'projects'));
// System-critical paths that should never be used as workspace directories.
// `/root` is conditional — included only when the server is NOT running as
// root. On a typical VPS deployment (`sudo` install, root-owned daemon)
// `/root` IS the user's home directory, blocking projects under it locks
// users out of their own filesystem. The carve-out below was meant to
// handle this but only allowed paths under WORKSPACES_BASE — users with
// `/root/foo` from before Pixcode existed couldn't open them.
const RUNNING_AS_ROOT = process.platform !== 'win32' &&
    typeof process.getuid === 'function' &&
    process.getuid() === 0;
export const FORBIDDEN_PATHS = [
    // Unix
    '/',
    '/etc',
    '/bin',
    '/sbin',
    '/usr',
    '/dev',
    '/proc',
    '/sys',
    '/var',
    '/boot',
    ...(RUNNING_AS_ROOT ? [] : ['/root']),
    '/lib',
    '/lib64',
    '/opt',
    '/tmp',
    '/run',
    // Windows
    'C:\\Windows',
    'C:\\Program Files',
    'C:\\Program Files (x86)',
    'C:\\ProgramData',
    'C:\\System Volume Information',
    'C:\\$Recycle.Bin'
];
const WINDOWS_ABSOLUTE_PATH_PATTERN = /^[A-Za-z]:[\\/]/;
function isPathWithin(basePath, targetPath) {
    const normalizedBase = path.normalize(basePath);
    const normalizedTarget = path.normalize(targetPath);
    return (normalizedTarget === normalizedBase ||
        normalizedTarget.startsWith(normalizedBase + path.sep));
}
async function realpathOrResolved(targetPath) {
    try {
        return await fs.realpath(targetPath);
    }
    catch (error) {
        if (error.code === 'ENOENT') {
            return path.resolve(targetPath);
        }
        throw error;
    }
}
export function normalizeWorkspacePath(requestedPath) {
    if (typeof requestedPath !== 'string') {
        return WORKSPACES_BASE;
    }
    const trimmedPath = requestedPath.trim();
    if (!trimmedPath) {
        return WORKSPACES_BASE;
    }
    if (trimmedPath === '~') {
        return WORKSPACES_BASE;
    }
    if (trimmedPath.startsWith('~/') || trimmedPath.startsWith('~\\')) {
        return path.join(WORKSPACES_BASE, trimmedPath.slice(2));
    }
    const isWindowsAbsolutePath = WINDOWS_ABSOLUTE_PATH_PATTERN.test(trimmedPath);
    if (!path.isAbsolute(trimmedPath) && !isWindowsAbsolutePath) {
        return path.join(WORKSPACES_BASE, trimmedPath);
    }
    return path.resolve(trimmedPath);
}
/**
 * Validates that a path is safe for workspace operations
 * @param {string} requestedPath - The path to validate
 * @returns {Promise<{valid: boolean, resolvedPath?: string, error?: string}>}
 */
export async function validateWorkspacePath(requestedPath) {
    try {
        if (typeof requestedPath !== 'string' || requestedPath.trim().length === 0) {
            return {
                valid: false,
                error: 'Workspace path is required'
            };
        }
        // Resolve aliases and relative paths into a safe default base.
        // Example: "my-app" -> "<WORKSPACES_BASE>/my-app"
        const normalizedInputPath = normalizeWorkspacePath(requestedPath);
        // Resolve to absolute path
        let absolutePath = path.resolve(normalizedInputPath);
        let resolvedWorkspaceBase = await realpathOrResolved(WORKSPACES_BASE);
        resolvedWorkspaceBase = path.normalize(resolvedWorkspaceBase);
        // Check if path is a forbidden system directory
        const normalizedPath = path.normalize(absolutePath);
        if (FORBIDDEN_PATHS.includes(normalizedPath) || normalizedPath === '/') {
            const isRootWorkspaceException = (normalizedPath === '/root' || normalizedPath.startsWith('/root' + path.sep)) &&
                isPathWithin(resolvedWorkspaceBase, normalizedPath);
            if (isRootWorkspaceException) {
                // Allow /root/<base> carve-out for root installations.
            }
            else {
                return {
                    valid: false,
                    error: 'Cannot use system-critical directories as workspace locations'
                };
            }
        }
        // Additional check for paths starting with forbidden directories
        for (const forbidden of FORBIDDEN_PATHS) {
            const isInsideForbidden = normalizedPath === forbidden ||
                normalizedPath.startsWith(forbidden + path.sep);
            if (!isInsideForbidden) {
                continue;
            }
            const isRootWorkspaceException = (forbidden === '/root') && isPathWithin(resolvedWorkspaceBase, normalizedPath);
            if (isRootWorkspaceException) {
                continue;
            }
            // Exception: /var/tmp and similar user-accessible paths might be allowed
            // but /var itself and most /var subdirectories should be blocked
            if (forbidden === '/var' &&
                (normalizedPath.startsWith('/var/tmp') ||
                    normalizedPath.startsWith('/var/folders'))) {
                continue; // Allow these specific cases
            }
            return {
                valid: false,
                error: `Cannot create workspace in system directory: ${forbidden}`
            };
        }
        // Try to resolve the real path (following symlinks)
        let realPath;
        try {
            // Check if path exists to resolve real path
            await fs.access(absolutePath);
            realPath = await fs.realpath(absolutePath);
        }
        catch (error) {
            if (error.code === 'ENOENT') {
                // Path doesn't exist yet - check parent directory
                let parentPath = path.dirname(absolutePath);
                try {
                    const parentRealPath = await fs.realpath(parentPath);
                    // Reconstruct the full path with real parent
                    realPath = path.join(parentRealPath, path.basename(absolutePath));
                }
                catch (parentError) {
                    if (parentError.code === 'ENOENT') {
                        // Parent doesn't exist either - use the absolute path as-is
                        // We'll validate it's within allowed root
                        realPath = absolutePath;
                    }
                    else {
                        throw parentError;
                    }
                }
            }
            else {
                throw error;
            }
        }
        // Symlink safety: if the chosen path is a symlink, make sure its
        // target doesn't dive into a forbidden system directory. The plain
        // FORBIDDEN_PATHS check above only sees the link path itself.
        try {
            await fs.access(absolutePath);
            const stats = await fs.lstat(absolutePath);
            if (stats.isSymbolicLink()) {
                const linkTarget = await fs.readlink(absolutePath);
                const resolvedTarget = path.resolve(path.dirname(absolutePath), linkTarget);
                const realTarget = path.normalize(await fs.realpath(resolvedTarget));
                for (const forbidden of FORBIDDEN_PATHS) {
                    if (realTarget === forbidden || realTarget.startsWith(forbidden + path.sep)) {
                        return {
                            valid: false,
                            error: `Symlink target resolves into a system directory: ${forbidden}`
                        };
                    }
                }
            }
        }
        catch (error) {
            if (error.code !== 'ENOENT') {
                throw error;
            }
            // Path doesn't exist - that's fine for new workspace creation
        }
        return {
            valid: true,
            resolvedPath: realPath
        };
    }
    catch (error) {
        return {
            valid: false,
            error: `Path validation failed: ${error.message}`
        };
    }
}
/**
 * Is this `pixcode-project-N` slot already in use? "In use" means the
 * user has sent at least one message under any provider — presence of a
 * session file under ~/.claude/projects/<encoded>/, ~/.codex/sessions/,
 * or ~/.gemini/… is our signal. We keep it best-effort: if we can't
 * probe a provider's session dir (no permissions, path missing), we
 * treat it as "no sessions for this provider" rather than raise.
 *
 * Checking the on-disk workspace dir for files is NOT a reliable signal
 * — providers store their history outside the workspace, so a project
 * that has had 20 messages still has an empty folder.
 */
async function projectHasAnySessions(workspacePath) {
    const home = os.homedir();
    // encodeProjectName strips drive separators (C:\ → -C--…) and dots so
    // `extractProjectDirectory` can round-trip. Using the same encoder as
    // the rest of projects.js keeps us aligned with however Claude's CLI
    // computes its per-project directory name.
    const slug = workspacePath.replace(/[\\/:]/g, '-').replace(/\./g, '-');
    const probes = [
        // Claude Code: JSONL-per-session files under a per-project subdir.
        path.join(home, '.claude', 'projects', slug),
        // Codex writes session logs under ~/.codex/sessions — they're cross-project
        // so we can't cheaply attribute them to a specific slot; skip.
        // Gemini: same layout as Claude.
        path.join(home, '.gemini', 'projects', slug),
        // Qwen Code (Gemini fork): same layout.
        path.join(home, '.qwen', 'projects', slug),
    ];
    for (const dir of probes) {
        try {
            const entries = await fs.readdir(dir);
            if (entries.some((name) => name.endsWith('.jsonl') || name.endsWith('.json'))) {
                return true;
            }
        }
        catch {
            // Missing / unreadable dir just means "no sessions here", not fatal.
        }
    }
    return false;
}
/**
 * GET /api/projects/:projectName/dir-status
 *
 * Lightweight "does the workspace still exist on disk?" check used by
 * the chat composer to detect deleted-directory sessions. We decode the
 * project name back to an absolute path and stat it — a slug alone isn't
 * useful because the user may have deleted the workspace while the
 * session metadata still lives under ~/.<provider>/projects/.
 *
 * Returns `{ exists, path, isDirectory }` so the UI can lock the
 * composer and surface a "directory deleted" warning instead of letting
 * the user fire prompts into a void.
 */
router.get('/:projectName/dir-status', async (req, res) => {
    const { projectName } = req.params;
    try {
        const actualPath = await extractProjectDirectory(projectName);
        if (!actualPath) {
            return res.json({ exists: false, path: null, isDirectory: false });
        }
        try {
            const stat = await fs.stat(actualPath);
            return res.json({
                exists: true,
                path: actualPath,
                isDirectory: stat.isDirectory(),
            });
        }
        catch (err) {
            // ENOENT is the typical "user rm -rf'd the workspace" path.
            if (err.code === 'ENOENT') {
                return res.json({ exists: false, path: actualPath, isDirectory: false });
            }
            throw err;
        }
    }
    catch (error) {
        console.error(`[projects] dir-status ${projectName}:`, error);
        res.status(500).json({ error: error.message || 'Failed to check project directory' });
    }
});
/**
 * POST /api/projects/quick-start
 *
 * Zero-config project creation: **reuses** the first unused
 * `pixcode-project-N` slot if one exists, otherwise creates the next
 * free index. "Unused" = no session files on disk for any provider.
 * Without reuse, clicking "New chat" rapidly stacks up pixcode-project-1
 * through pixcode-project-N and litters the workspace — the UX we want
 * matches ChatGPT's "New chat" which reuses the empty canvas until the
 * user actually commits a message.
 */
router.post('/quick-start', async (req, res) => {
    try {
        await fs.mkdir(WORKSPACES_BASE, { recursive: true });
        let entries = [];
        try {
            entries = await fs.readdir(WORKSPACES_BASE, { withFileTypes: true });
        }
        catch { /* empty is fine */ }
        // Pixcode-owned slots, sorted by numeric index so reuse is deterministic
        // and picks the lowest idle slot (pixcode-project-1 before -3).
        const existingSlots = entries
            .filter((e) => e.isDirectory() && /^pixcode-project-\d+$/i.test(e.name))
            .map((e) => ({
            name: e.name,
            index: parseInt(e.name.split('-').pop(), 10) || 0,
        }))
            .sort((a, b) => a.index - b.index);
        // 1. First pass: reuse the lowest-indexed slot that has no sessions.
        for (const slot of existingSlots) {
            const absolutePath = path.join(WORKSPACES_BASE, slot.name);
            const used = await projectHasAnySessions(absolutePath);
            if (!used) {
                let project;
                try {
                    project = await addProjectManually(absolutePath);
                }
                catch (err) {
                    // addProjectManually throws when the project is already
                    // registered. That's fine — look it up via its encoded name
                    // instead of creating a duplicate.
                    const msg = err?.message || '';
                    if (!/already configured/i.test(msg))
                        throw err;
                    project = {
                        name: absolutePath.replace(/[\\/:]/g, '-').replace(/\./g, '-'),
                        path: absolutePath,
                        fullPath: absolutePath,
                        displayName: slot.name,
                        isManuallyAdded: true,
                        sessions: [],
                        cursorSessions: [],
                    };
                }
                return res.json({
                    success: true,
                    project,
                    suggestedName: slot.name,
                    reused: true,
                });
            }
        }
        // 2. No idle slot — create the next free index above what exists.
        const takenIndices = new Set(existingSlots.map((s) => s.index));
        let nextIndex = 1;
        while (takenIndices.has(nextIndex)) {
            nextIndex += 1;
            if (nextIndex > 9999) {
                return res.status(500).json({ error: 'No free pixcode-project slot (exhausted 1..9999)' });
            }
        }
        const name = `pixcode-project-${nextIndex}`;
        const absolutePath = path.join(WORKSPACES_BASE, name);
        await fs.mkdir(absolutePath, { recursive: true });
        const project = await addProjectManually(absolutePath);
        res.json({ success: true, project, suggestedName: name, reused: false });
    }
    catch (error) {
        console.error('[projects] quick-start failed:', error);
        res.status(500).json({ error: error.message || 'Failed to quick-start project' });
    }
});
/**
 * Create a new workspace
 * POST /api/projects/create-workspace
 *
 * Body:
 * - workspaceType: 'existing' | 'new'
 * - path: string (workspace path)
 * - githubUrl?: string (optional, for new workspaces)
 * - githubTokenId?: number (optional, ID of stored token)
 * - newGithubToken?: string (optional, one-time token)
 */
router.post('/create-workspace', async (req, res) => {
    try {
        const { workspaceType, path: workspacePath, githubUrl, githubTokenId, newGithubToken, subfolderName } = req.body;
        // Validate required fields
        if (!workspaceType || !workspacePath) {
            return res.status(400).json({ error: 'workspaceType and path are required' });
        }
        // 'existing' = open the picked folder as-is
        // 'new'      = clone a github repo into the picked folder (legacy name kept for client compat)
        // 'subfolder'= create a fresh subfolder INSIDE the picked folder and open that
        if (!['existing', 'new', 'subfolder'].includes(workspaceType)) {
            return res.status(400).json({ error: 'workspaceType must be "existing", "new", or "subfolder"' });
        }
        // Validate path safety before any operations
        const validation = await validateWorkspacePath(workspacePath);
        if (!validation.valid) {
            return res.status(400).json({
                error: 'Invalid workspace path',
                details: validation.error
            });
        }
        const absolutePath = validation.resolvedPath;
        // Handle existing workspace
        if (workspaceType === 'existing') {
            // Check if the path exists
            try {
                await fs.access(absolutePath);
                const stats = await fs.stat(absolutePath);
                if (!stats.isDirectory()) {
                    return res.status(400).json({ error: 'Path exists but is not a directory' });
                }
            }
            catch (error) {
                if (error.code === 'ENOENT') {
                    return res.status(404).json({ error: 'Workspace path does not exist' });
                }
                throw error;
            }
            // Add the existing workspace to the project list. If the user picks
            // a folder Pixcode has already registered (very common when bouncing
            // between sessions or re-opening the wizard on the same project),
            // `addProjectManually` throws "Project already configured…" — that
            // used to surface as a hard error in the UI even though the right
            // outcome is "great, let's just open it." Treat that one specific
            // throw as a soft re-open and return a 200 with `alreadyExisted: true`
            // so the wizard can show "Opened existing workspace" instead of the
            // raw error message.
            let project;
            let alreadyExisted = false;
            try {
                project = await addProjectManually(absolutePath);
            }
            catch (error) {
                const msg = error?.message || '';
                if (!/already configured/i.test(msg))
                    throw error;
                alreadyExisted = true;
                project = {
                    name: absolutePath.replace(/[\\/:]/g, '-').replace(/\./g, '-'),
                    path: absolutePath,
                    fullPath: absolutePath,
                    displayName: path.basename(absolutePath),
                    isManuallyAdded: true,
                    sessions: [],
                    cursorSessions: [],
                };
            }
            return res.json({
                success: true,
                project,
                alreadyExisted,
                message: alreadyExisted
                    ? 'Workspace was already registered — opening it'
                    : 'Existing workspace added successfully'
            });
        }
        // Handle subfolder creation: user picked a parent dir, we mkdir
        // <parent>/<subfolderName> and open that.
        if (workspaceType === 'subfolder') {
            const trimmedName = typeof subfolderName === 'string' ? subfolderName.trim() : '';
            if (!trimmedName) {
                return res.status(400).json({ error: 'subfolderName is required when workspaceType is "subfolder"' });
            }
            // Reject path-traversal / nested separators / reserved names. The
            // wizard's UI will only ever send a flat folder name; anything else
            // is either a bug or someone fishing.
            if (/[\\/]/.test(trimmedName) || trimmedName === '.' || trimmedName === '..') {
                return res.status(400).json({ error: 'subfolderName must be a single folder name (no path separators)' });
            }
            // Verify parent dir exists (we don't auto-create the picked parent —
            // user already pointed at a real folder).
            try {
                const stats = await fs.stat(absolutePath);
                if (!stats.isDirectory()) {
                    return res.status(400).json({ error: 'Parent path is not a directory' });
                }
            }
            catch (error) {
                if (error.code === 'ENOENT') {
                    return res.status(404).json({ error: 'Parent directory does not exist' });
                }
                throw error;
            }
            const childPath = path.join(absolutePath, trimmedName);
            // Validate the resulting path too — don't let "subfolder=foo/../../etc"
            // bypass the parent-only check above. validateWorkspacePath already
            // rejects symlink escapes and FORBIDDEN_PATHS.
            const childValidation = await validateWorkspacePath(childPath);
            if (!childValidation.valid) {
                return res.status(400).json({
                    error: 'Invalid subfolder path',
                    details: childValidation.error,
                });
            }
            const childAbsolute = childValidation.resolvedPath;
            // Refuse to clobber an existing folder with content — user can pick
            // "existing" instead. Empty/missing → mkdir.
            try {
                const childEntries = await fs.readdir(childAbsolute);
                if (childEntries.length > 0) {
                    return res.status(409).json({
                        error: 'Subfolder already exists and is not empty',
                        details: `Pick a different name or open "${childAbsolute}" as an existing workspace.`,
                    });
                }
            }
            catch (error) {
                if (error.code !== 'ENOENT')
                    throw error;
            }
            await fs.mkdir(childAbsolute, { recursive: true });
            let subProject;
            let subAlreadyExisted = false;
            try {
                subProject = await addProjectManually(childAbsolute);
            }
            catch (error) {
                const msg = error?.message || '';
                if (!/already configured/i.test(msg))
                    throw error;
                subAlreadyExisted = true;
                subProject = {
                    name: childAbsolute.replace(/[\\/:]/g, '-').replace(/\./g, '-'),
                    path: childAbsolute,
                    fullPath: childAbsolute,
                    displayName: trimmedName,
                    isManuallyAdded: true,
                    sessions: [],
                    cursorSessions: [],
                };
            }
            return res.json({
                success: true,
                project: subProject,
                alreadyExisted: subAlreadyExisted,
                message: subAlreadyExisted
                    ? 'Subfolder was already registered — opening it'
                    : 'Subfolder created successfully',
            });
        }
        // Handle new workspace creation
        if (workspaceType === 'new') {
            // Create the directory if it doesn't exist
            await fs.mkdir(absolutePath, { recursive: true });
            // If GitHub URL is provided, clone the repository
            if (githubUrl) {
                let githubToken = null;
                // Get GitHub token if needed
                if (githubTokenId) {
                    // Fetch token from database
                    const token = await getGithubTokenById(githubTokenId, req.user.id);
                    if (!token) {
                        // Clean up created directory
                        await fs.rm(absolutePath, { recursive: true, force: true });
                        return res.status(404).json({ error: 'GitHub token not found' });
                    }
                    githubToken = token.github_token;
                }
                else if (newGithubToken) {
                    githubToken = newGithubToken;
                }
                // Extract repo name from URL for the clone destination
                const normalizedUrl = githubUrl.replace(/\/+$/, '').replace(/\.git$/, '');
                const repoName = normalizedUrl.split('/').pop() || 'repository';
                const clonePath = path.join(absolutePath, repoName);
                // Check if clone destination already exists to prevent data loss
                try {
                    await fs.access(clonePath);
                    return res.status(409).json({
                        error: 'Directory already exists',
                        details: `The destination path "${clonePath}" already exists. Please choose a different location or remove the existing directory.`
                    });
                }
                catch (err) {
                    // Directory doesn't exist, which is what we want
                }
                // Clone the repository into a subfolder
                try {
                    await cloneGitHubRepository(githubUrl, clonePath, githubToken);
                }
                catch (error) {
                    // Only clean up if clone created partial data (check if dir exists and is empty or partial)
                    try {
                        const stats = await fs.stat(clonePath);
                        if (stats.isDirectory()) {
                            await fs.rm(clonePath, { recursive: true, force: true });
                        }
                    }
                    catch (cleanupError) {
                        // Directory doesn't exist or cleanup failed - ignore
                    }
                    throw new Error(`Failed to clone repository: ${error.message}`);
                }
                // Add the cloned repo path to the project list
                const project = await addProjectManually(clonePath);
                return res.json({
                    success: true,
                    project,
                    message: 'New workspace created and repository cloned successfully'
                });
            }
            // Add the new workspace to the project list (no clone)
            const project = await addProjectManually(absolutePath);
            return res.json({
                success: true,
                project,
                message: 'New workspace created successfully'
            });
        }
    }
    catch (error) {
        console.error('Error creating workspace:', error);
        res.status(500).json({
            error: error.message || 'Failed to create workspace',
            details: process.env.NODE_ENV === 'development' ? error.stack : undefined
        });
    }
});
/**
 * Helper function to get GitHub token from database
 */
async function getGithubTokenById(tokenId, userId) {
    const { db } = await import('../database/db.js');
    const credential = db.prepare('SELECT * FROM user_credentials WHERE id = ? AND user_id = ? AND credential_type = ? AND is_active = 1').get(tokenId, userId, 'github_token');
    // Return in the expected format (github_token field for compatibility)
    if (credential) {
        return {
            ...credential,
            github_token: credential.credential_value
        };
    }
    return null;
}
/**
 * Clone repository with progress streaming (SSE)
 * GET /api/projects/clone-progress
 */
router.get('/clone-progress', async (req, res) => {
    const { path: workspacePath, githubUrl, githubTokenId, newGithubToken } = req.query;
    res.setHeader('Content-Type', 'text/event-stream');
    res.setHeader('Cache-Control', 'no-cache');
    res.setHeader('Connection', 'keep-alive');
    res.flushHeaders();
    const sendEvent = (type, data) => {
        res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`);
    };
    try {
        if (!workspacePath || !githubUrl) {
            sendEvent('error', { message: 'workspacePath and githubUrl are required' });
            res.end();
            return;
        }
        const validation = await validateWorkspacePath(workspacePath);
        if (!validation.valid) {
            sendEvent('error', { message: validation.error });
            res.end();
            return;
        }
        const absolutePath = validation.resolvedPath;
        await fs.mkdir(absolutePath, { recursive: true });
        let githubToken = null;
        if (githubTokenId) {
            const token = await getGithubTokenById(parseInt(githubTokenId), req.user.id);
            if (!token) {
                await fs.rm(absolutePath, { recursive: true, force: true });
                sendEvent('error', { message: 'GitHub token not found' });
                res.end();
                return;
            }
            githubToken = token.github_token;
        }
        else if (newGithubToken) {
            githubToken = newGithubToken;
        }
        const normalizedUrl = githubUrl.replace(/\/+$/, '').replace(/\.git$/, '');
        const repoName = normalizedUrl.split('/').pop() || 'repository';
        const clonePath = path.join(absolutePath, repoName);
        // Check if clone destination already exists to prevent data loss
        try {
            await fs.access(clonePath);
            sendEvent('error', { message: `Directory "${repoName}" already exists. Please choose a different location or remove the existing directory.` });
            res.end();
            return;
        }
        catch (err) {
            // Directory doesn't exist, which is what we want
        }
        let cloneUrl = githubUrl;
        if (githubToken) {
            try {
                const url = new URL(githubUrl);
                url.username = githubToken;
                url.password = '';
                cloneUrl = url.toString();
            }
            catch (error) {
                // SSH URL or invalid - use as-is
            }
        }
        sendEvent('progress', { message: `Cloning into '${repoName}'...` });
        const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, clonePath], {
            stdio: ['ignore', 'pipe', 'pipe'],
            env: {
                ...process.env,
                GIT_TERMINAL_PROMPT: '0'
            }
        });
        let lastError = '';
        gitProcess.stdout.on('data', (data) => {
            const message = data.toString().trim();
            if (message) {
                sendEvent('progress', { message });
            }
        });
        gitProcess.stderr.on('data', (data) => {
            const message = data.toString().trim();
            lastError = message;
            if (message) {
                sendEvent('progress', { message });
            }
        });
        gitProcess.on('close', async (code) => {
            if (code === 0) {
                try {
                    const project = await addProjectManually(clonePath);
                    sendEvent('complete', { project, message: 'Repository cloned successfully' });
                }
                catch (error) {
                    sendEvent('error', { message: `Clone succeeded but failed to add project: ${error.message}` });
                }
            }
            else {
                const sanitizedError = sanitizeGitError(lastError, githubToken);
                let errorMessage = 'Git clone failed';
                if (lastError.includes('Authentication failed') || lastError.includes('could not read Username')) {
                    errorMessage = 'Authentication failed. Please check your credentials.';
                }
                else if (lastError.includes('Repository not found')) {
                    errorMessage = 'Repository not found. Please check the URL and ensure you have access.';
                }
                else if (lastError.includes('already exists')) {
                    errorMessage = 'Directory already exists';
                }
                else if (sanitizedError) {
                    errorMessage = sanitizedError;
                }
                try {
                    await fs.rm(clonePath, { recursive: true, force: true });
                }
                catch (cleanupError) {
                    console.error('Failed to clean up after clone failure:', sanitizeGitError(cleanupError.message, githubToken));
                }
                sendEvent('error', { message: errorMessage });
            }
            res.end();
        });
        gitProcess.on('error', (error) => {
            if (error.code === 'ENOENT') {
                sendEvent('error', { message: 'Git is not installed or not in PATH' });
            }
            else {
                sendEvent('error', { message: error.message });
            }
            res.end();
        });
        req.on('close', () => {
            gitProcess.kill();
        });
    }
    catch (error) {
        sendEvent('error', { message: error.message });
        res.end();
    }
});
/**
 * Helper function to clone a GitHub repository
 */
function cloneGitHubRepository(githubUrl, destinationPath, githubToken = null) {
    return new Promise((resolve, reject) => {
        let cloneUrl = githubUrl;
        if (githubToken) {
            try {
                const url = new URL(githubUrl);
                url.username = githubToken;
                url.password = '';
                cloneUrl = url.toString();
            }
            catch (error) {
                // SSH URL - use as-is
            }
        }
        const gitProcess = spawn('git', ['clone', '--progress', cloneUrl, destinationPath], {
            stdio: ['ignore', 'pipe', 'pipe'],
            env: {
                ...process.env,
                GIT_TERMINAL_PROMPT: '0'
            }
        });
        let stdout = '';
        let stderr = '';
        gitProcess.stdout.on('data', (data) => {
            stdout += data.toString();
        });
        gitProcess.stderr.on('data', (data) => {
            stderr += data.toString();
        });
        gitProcess.on('close', (code) => {
            if (code === 0) {
                resolve({ stdout, stderr });
            }
            else {
                let errorMessage = 'Git clone failed';
                if (stderr.includes('Authentication failed') || stderr.includes('could not read Username')) {
                    errorMessage = 'Authentication failed. Please check your GitHub token.';
                }
                else if (stderr.includes('Repository not found')) {
                    errorMessage = 'Repository not found. Please check the URL and ensure you have access.';
                }
                else if (stderr.includes('already exists')) {
                    errorMessage = 'Directory already exists';
                }
                else if (stderr) {
                    errorMessage = stderr;
                }
                reject(new Error(errorMessage));
            }
        });
        gitProcess.on('error', (error) => {
            if (error.code === 'ENOENT') {
                reject(new Error('Git is not installed or not in PATH'));
            }
            else {
                reject(error);
            }
        });
    });
}
export default router;
//# sourceMappingURL=projects.js.map