Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
206 lines
6.7 KiB
TypeScript
206 lines
6.7 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
Body,
|
|
Controller,
|
|
Delete,
|
|
Get,
|
|
HttpCode,
|
|
HttpStatus,
|
|
Inject,
|
|
NotFoundException,
|
|
Param,
|
|
Patch,
|
|
Post,
|
|
Query,
|
|
UseGuards,
|
|
} from '@nestjs/common';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { AuthGuard } from '../auth/auth.guard.js';
|
|
import { CurrentUser } from '../auth/current-user.decorator.js';
|
|
import { CoordService } from './coord.service.js';
|
|
import type {
|
|
CreateDbMissionDto,
|
|
UpdateDbMissionDto,
|
|
CreateMissionTaskDto,
|
|
UpdateMissionTaskDto,
|
|
} from './coord.dto.js';
|
|
|
|
/** Walk up from cwd to find the monorepo root (has pnpm-workspace.yaml). */
|
|
function findMonorepoRoot(start: string): string {
|
|
let dir = start;
|
|
for (let i = 0; i < 5; i++) {
|
|
try {
|
|
fs.accessSync(path.join(dir, 'pnpm-workspace.yaml'));
|
|
return dir;
|
|
} catch {
|
|
const parent = path.dirname(dir);
|
|
if (parent === dir) break;
|
|
dir = parent;
|
|
}
|
|
}
|
|
return start;
|
|
}
|
|
|
|
/** Only paths under these roots are allowed for coord queries. */
|
|
const WORKSPACE_ROOT = process.env['MOSAIC_WORKSPACE_ROOT'] ?? findMonorepoRoot(process.cwd());
|
|
const ALLOWED_ROOTS = [process.cwd(), WORKSPACE_ROOT];
|
|
|
|
function resolveAndValidatePath(raw: string | undefined): string {
|
|
const resolved = path.resolve(raw ?? process.cwd());
|
|
const isAllowed = ALLOWED_ROOTS.some(
|
|
(root) => resolved === root || resolved.startsWith(`${root}/`),
|
|
);
|
|
if (!isAllowed) {
|
|
throw new BadRequestException('projectPath is outside the allowed workspace');
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
@Controller('api/coord')
|
|
@UseGuards(AuthGuard)
|
|
export class CoordController {
|
|
constructor(@Inject(CoordService) private readonly coordService: CoordService) {}
|
|
|
|
// ── File-based coord endpoints (legacy) ──
|
|
|
|
@Get('status')
|
|
async missionStatus(@Query('projectPath') projectPath?: string) {
|
|
const resolvedPath = resolveAndValidatePath(projectPath);
|
|
const status = await this.coordService.getMissionStatus(resolvedPath);
|
|
if (!status) throw new NotFoundException('No active coord mission found');
|
|
return status;
|
|
}
|
|
|
|
@Get('tasks')
|
|
async listTasks(@Query('projectPath') projectPath?: string) {
|
|
const resolvedPath = resolveAndValidatePath(projectPath);
|
|
return this.coordService.listTasks(resolvedPath);
|
|
}
|
|
|
|
@Get('tasks/:taskId')
|
|
async taskStatus(@Param('taskId') taskId: string, @Query('projectPath') projectPath?: string) {
|
|
const resolvedPath = resolveAndValidatePath(projectPath);
|
|
const detail = await this.coordService.getTaskStatus(resolvedPath, taskId);
|
|
if (!detail) throw new NotFoundException(`Task ${taskId} not found in coord mission`);
|
|
return detail;
|
|
}
|
|
|
|
// ── DB-backed mission endpoints ──
|
|
|
|
@Get('missions')
|
|
async listDbMissions(@CurrentUser() user: { id: string }) {
|
|
return this.coordService.getMissionsByUser(user.id);
|
|
}
|
|
|
|
@Get('missions/:id')
|
|
async getDbMission(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
|
const mission = await this.coordService.getMissionByIdAndUser(id, user.id);
|
|
if (!mission) throw new NotFoundException('Mission not found');
|
|
return mission;
|
|
}
|
|
|
|
@Post('missions')
|
|
async createDbMission(@Body() dto: CreateDbMissionDto, @CurrentUser() user: { id: string }) {
|
|
return this.coordService.createDbMission({
|
|
name: dto.name,
|
|
description: dto.description,
|
|
projectId: dto.projectId,
|
|
userId: user.id,
|
|
phase: dto.phase,
|
|
milestones: dto.milestones,
|
|
config: dto.config,
|
|
status: dto.status,
|
|
});
|
|
}
|
|
|
|
@Patch('missions/:id')
|
|
async updateDbMission(
|
|
@Param('id') id: string,
|
|
@Body() dto: UpdateDbMissionDto,
|
|
@CurrentUser() user: { id: string },
|
|
) {
|
|
const mission = await this.coordService.updateDbMission(id, user.id, dto);
|
|
if (!mission) throw new NotFoundException('Mission not found');
|
|
return mission;
|
|
}
|
|
|
|
@Delete('missions/:id')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
async deleteDbMission(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
|
const deleted = await this.coordService.deleteDbMission(id, user.id);
|
|
if (!deleted) throw new NotFoundException('Mission not found');
|
|
}
|
|
|
|
// ── DB-backed mission task endpoints ──
|
|
|
|
@Get('missions/:missionId/mission-tasks')
|
|
async listMissionTasks(
|
|
@Param('missionId') missionId: string,
|
|
@CurrentUser() user: { id: string },
|
|
) {
|
|
const mission = await this.coordService.getMissionByIdAndUser(missionId, user.id);
|
|
if (!mission) throw new NotFoundException('Mission not found');
|
|
return this.coordService.getMissionTasksByMissionAndUser(missionId, user.id);
|
|
}
|
|
|
|
@Get('missions/:missionId/mission-tasks/:taskId')
|
|
async getMissionTask(
|
|
@Param('missionId') missionId: string,
|
|
@Param('taskId') taskId: string,
|
|
@CurrentUser() user: { id: string },
|
|
) {
|
|
const mission = await this.coordService.getMissionByIdAndUser(missionId, user.id);
|
|
if (!mission) throw new NotFoundException('Mission not found');
|
|
const task = await this.coordService.getMissionTaskByIdAndUser(taskId, user.id);
|
|
if (!task) throw new NotFoundException('Mission task not found');
|
|
return task;
|
|
}
|
|
|
|
@Post('missions/:missionId/mission-tasks')
|
|
async createMissionTask(
|
|
@Param('missionId') missionId: string,
|
|
@Body() dto: CreateMissionTaskDto,
|
|
@CurrentUser() user: { id: string },
|
|
) {
|
|
const mission = await this.coordService.getMissionByIdAndUser(missionId, user.id);
|
|
if (!mission) throw new NotFoundException('Mission not found');
|
|
return this.coordService.createMissionTask({
|
|
missionId,
|
|
taskId: dto.taskId,
|
|
userId: user.id,
|
|
status: dto.status,
|
|
description: dto.description,
|
|
notes: dto.notes,
|
|
pr: dto.pr,
|
|
});
|
|
}
|
|
|
|
@Patch('missions/:missionId/mission-tasks/:taskId')
|
|
async updateMissionTask(
|
|
@Param('missionId') missionId: string,
|
|
@Param('taskId') taskId: string,
|
|
@Body() dto: UpdateMissionTaskDto,
|
|
@CurrentUser() user: { id: string },
|
|
) {
|
|
const mission = await this.coordService.getMissionByIdAndUser(missionId, user.id);
|
|
if (!mission) throw new NotFoundException('Mission not found');
|
|
const updated = await this.coordService.updateMissionTask(taskId, user.id, dto);
|
|
if (!updated) throw new NotFoundException('Mission task not found');
|
|
return updated;
|
|
}
|
|
|
|
@Delete('missions/:missionId/mission-tasks/:taskId')
|
|
@HttpCode(HttpStatus.NO_CONTENT)
|
|
async deleteMissionTask(
|
|
@Param('missionId') missionId: string,
|
|
@Param('taskId') taskId: string,
|
|
@CurrentUser() user: { id: string },
|
|
) {
|
|
const mission = await this.coordService.getMissionByIdAndUser(missionId, user.id);
|
|
if (!mission) throw new NotFoundException('Mission not found');
|
|
const deleted = await this.coordService.deleteMissionTask(taskId, user.id);
|
|
if (!deleted) throw new NotFoundException('Mission task not found');
|
|
}
|
|
}
|