import express from 'express'; import { createProject } from '@/modules/projects/services/project-management.service.js'; import { startCloneProject } from '@/modules/projects/services/project-clone.service.js'; import { getProjectTaskMaster } from '@/modules/projects/services/projects-has-taskmaster.service.js'; import { AppError, asyncHandler } from '@/shared/utils.js'; import { getProjectsWithSessions } from '@/modules/projects/services/projects-with-sessions-fetch.service.js'; const router = express.Router(); type AuthenticatedUser = { id?: number | string; }; function readQueryStringValue(value: unknown): string { if (typeof value === 'string') { return value; } if (Array.isArray(value) && typeof value[0] === 'string') { return value[0]; } return ''; } function readOptionalNumericQueryValue(value: unknown): number | null { const rawValue = readQueryStringValue(value).trim(); if (!rawValue) { return null; } const parsedValue = Number.parseInt(rawValue, 10); return Number.isNaN(parsedValue) ? null : parsedValue; } function resolveRouteErrorMessage(error: unknown): string { if (error instanceof AppError) { return error.message; } if (error instanceof Error && error.message) { return error.message; } return 'Failed to clone repository'; } router.get( '/', asyncHandler(async (_req, res) => { const projects = await getProjectsWithSessions(); res.json(projects); }), ); router.post( '/create-project', asyncHandler(async (req, res) => { const requestBody = req.body as Record; const projectPath = typeof requestBody.path === 'string' ? requestBody.path : ''; const customName = typeof requestBody.customName === 'string' ? requestBody.customName : null; if (requestBody.workspaceType !== undefined) { throw new AppError('workspaceType is no longer supported. Use the single create-project flow.', { code: 'LEGACY_WORKSPACE_TYPE_UNSUPPORTED', statusCode: 400, }); } if (requestBody.githubUrl || requestBody.githubTokenId || requestBody.newGithubToken) { throw new AppError('Repository cloning is not supported on create-project', { code: 'CLONE_NOT_SUPPORTED_ON_CREATE_PROJECT', statusCode: 400, details: 'Use /api/projects/clone-progress for cloning workflows', }); } const projectCreationResult = await createProject({ projectPath, customName, }); res.json({ success: true, project: projectCreationResult.project, message: projectCreationResult.outcome === 'reactivated_archived' ? 'Archived project path reused successfully' : 'Project created successfully', }); }), ); router.get('/clone-progress', async (req, res) => { res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.flushHeaders(); const sendEvent = (type: string, data: Record) => { if (res.writableEnded) { return; } res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); }; let cloneOperation: Awaited> | null = null; const closeListener = () => { cloneOperation?.cancel(); }; req.on('close', closeListener); try { const queryParams = req.query as Record; const workspacePath = readQueryStringValue(queryParams.path); const githubUrl = readQueryStringValue(queryParams.githubUrl); const githubTokenId = readOptionalNumericQueryValue(queryParams.githubTokenId); const newGithubToken = readQueryStringValue(queryParams.newGithubToken) || null; const authenticatedUser = (req as typeof req & { user?: AuthenticatedUser }).user; const userId = authenticatedUser?.id; if (userId === undefined || userId === null) { throw new AppError('Authenticated user is required', { code: 'AUTHENTICATION_REQUIRED', statusCode: 401, }); } cloneOperation = await startCloneProject( { workspacePath, githubUrl, githubTokenId, newGithubToken, userId, }, { onProgress: (message) => { sendEvent('progress', { message }); }, onComplete: ({ project, message }) => { sendEvent('complete', { project, message }); }, }, ); await cloneOperation.waitForCompletion; } catch (error) { sendEvent('error', { message: resolveRouteErrorMessage(error) }); } finally { req.off('close', closeListener); if (!res.writableEnded) { res.end(); } } }); router.get( '/:projectId/taskmaster', asyncHandler(async (req, res) => { const projectId = typeof req.params.projectId === 'string' ? req.params.projectId : ''; const taskMasterDetails = await getProjectTaskMaster(projectId); res.json(taskMasterDetails); }), ); export default router;