feat(cli): command architecture — agents, missions, gateway-aware prdy (#158)
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Some checks failed
ci/woodpecker/push/ci Pipeline failed
Co-authored-by: Jason Woltje <jason@diversecanvas.com> Co-committed-by: Jason Woltje <jason@diversecanvas.com>
This commit was merged in pull request #158.
This commit is contained in:
@@ -2,7 +2,6 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
@@ -17,33 +16,42 @@ import type { Brain } from '@mosaic/brain';
|
||||
import { BRAIN } from '../brain/brain.tokens.js';
|
||||
import { AuthGuard } from '../auth/auth.guard.js';
|
||||
import { CurrentUser } from '../auth/current-user.decorator.js';
|
||||
import { assertOwner } from '../auth/resource-ownership.js';
|
||||
import { CreateMissionDto, UpdateMissionDto } from './missions.dto.js';
|
||||
import {
|
||||
CreateMissionDto,
|
||||
UpdateMissionDto,
|
||||
CreateMissionTaskDto,
|
||||
UpdateMissionTaskDto,
|
||||
} from './missions.dto.js';
|
||||
|
||||
@Controller('api/missions')
|
||||
@UseGuards(AuthGuard)
|
||||
export class MissionsController {
|
||||
constructor(@Inject(BRAIN) private readonly brain: Brain) {}
|
||||
|
||||
// ── Missions CRUD (user-scoped) ──
|
||||
|
||||
@Get()
|
||||
async list() {
|
||||
return this.brain.missions.findAll();
|
||||
async list(@CurrentUser() user: { id: string }) {
|
||||
return this.brain.missions.findAllByUser(user.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
async findOne(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
return this.getOwnedMission(id, user.id);
|
||||
const mission = await this.brain.missions.findByIdAndUser(id, user.id);
|
||||
if (!mission) throw new NotFoundException('Mission not found');
|
||||
return mission;
|
||||
}
|
||||
|
||||
@Post()
|
||||
async create(@Body() dto: CreateMissionDto, @CurrentUser() user: { id: string }) {
|
||||
if (dto.projectId) {
|
||||
await this.getOwnedProject(dto.projectId, user.id, 'Mission');
|
||||
}
|
||||
return this.brain.missions.create({
|
||||
name: dto.name,
|
||||
description: dto.description,
|
||||
projectId: dto.projectId,
|
||||
userId: user.id,
|
||||
phase: dto.phase,
|
||||
milestones: dto.milestones,
|
||||
config: dto.config,
|
||||
status: dto.status,
|
||||
});
|
||||
}
|
||||
@@ -54,10 +62,8 @@ export class MissionsController {
|
||||
@Body() dto: UpdateMissionDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
await this.getOwnedMission(id, user.id);
|
||||
if (dto.projectId) {
|
||||
await this.getOwnedProject(dto.projectId, user.id, 'Mission');
|
||||
}
|
||||
const existing = await this.brain.missions.findByIdAndUser(id, user.id);
|
||||
if (!existing) throw new NotFoundException('Mission not found');
|
||||
const mission = await this.brain.missions.update(id, dto);
|
||||
if (!mission) throw new NotFoundException('Mission not found');
|
||||
return mission;
|
||||
@@ -66,33 +72,81 @@ export class MissionsController {
|
||||
@Delete(':id')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
await this.getOwnedMission(id, user.id);
|
||||
const existing = await this.brain.missions.findByIdAndUser(id, user.id);
|
||||
if (!existing) throw new NotFoundException('Mission not found');
|
||||
const deleted = await this.brain.missions.remove(id);
|
||||
if (!deleted) throw new NotFoundException('Mission not found');
|
||||
}
|
||||
|
||||
private async getOwnedMission(id: string, userId: string) {
|
||||
const mission = await this.brain.missions.findById(id);
|
||||
// ── Mission Tasks sub-routes ──
|
||||
|
||||
@Get(':missionId/tasks')
|
||||
async listTasks(@Param('missionId') missionId: string, @CurrentUser() user: { id: string }) {
|
||||
const mission = await this.brain.missions.findByIdAndUser(missionId, user.id);
|
||||
if (!mission) throw new NotFoundException('Mission not found');
|
||||
await this.getOwnedProject(mission.projectId, userId, 'Mission');
|
||||
return mission;
|
||||
return this.brain.missionTasks.findByMissionAndUser(missionId, user.id);
|
||||
}
|
||||
|
||||
private async getOwnedProject(
|
||||
projectId: string | null | undefined,
|
||||
userId: string,
|
||||
resourceName: string,
|
||||
@Get(':missionId/tasks/:taskId')
|
||||
async getTask(
|
||||
@Param('missionId') missionId: string,
|
||||
@Param('taskId') taskId: string,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
if (!projectId) {
|
||||
throw new ForbiddenException(`${resourceName} does not belong to the current user`);
|
||||
}
|
||||
const mission = await this.brain.missions.findByIdAndUser(missionId, user.id);
|
||||
if (!mission) throw new NotFoundException('Mission not found');
|
||||
const task = await this.brain.missionTasks.findByIdAndUser(taskId, user.id);
|
||||
if (!task) throw new NotFoundException('Mission task not found');
|
||||
return task;
|
||||
}
|
||||
|
||||
const project = await this.brain.projects.findById(projectId);
|
||||
if (!project) {
|
||||
throw new ForbiddenException(`${resourceName} does not belong to the current user`);
|
||||
}
|
||||
@Post(':missionId/tasks')
|
||||
async createTask(
|
||||
@Param('missionId') missionId: string,
|
||||
@Body() dto: CreateMissionTaskDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
const mission = await this.brain.missions.findByIdAndUser(missionId, user.id);
|
||||
if (!mission) throw new NotFoundException('Mission not found');
|
||||
return this.brain.missionTasks.create({
|
||||
missionId,
|
||||
taskId: dto.taskId,
|
||||
userId: user.id,
|
||||
status: dto.status,
|
||||
description: dto.description,
|
||||
notes: dto.notes,
|
||||
pr: dto.pr,
|
||||
});
|
||||
}
|
||||
|
||||
assertOwner(project.ownerId, userId, resourceName);
|
||||
return project;
|
||||
@Patch(':missionId/tasks/:taskId')
|
||||
async updateTask(
|
||||
@Param('missionId') missionId: string,
|
||||
@Param('taskId') taskId: string,
|
||||
@Body() dto: UpdateMissionTaskDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
const mission = await this.brain.missions.findByIdAndUser(missionId, user.id);
|
||||
if (!mission) throw new NotFoundException('Mission not found');
|
||||
const existing = await this.brain.missionTasks.findByIdAndUser(taskId, user.id);
|
||||
if (!existing) throw new NotFoundException('Mission task not found');
|
||||
const updated = await this.brain.missionTasks.update(taskId, dto);
|
||||
if (!updated) throw new NotFoundException('Mission task not found');
|
||||
return updated;
|
||||
}
|
||||
|
||||
@Delete(':missionId/tasks/:taskId')
|
||||
@HttpCode(HttpStatus.NO_CONTENT)
|
||||
async removeTask(
|
||||
@Param('missionId') missionId: string,
|
||||
@Param('taskId') taskId: string,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
const mission = await this.brain.missions.findByIdAndUser(missionId, user.id);
|
||||
if (!mission) throw new NotFoundException('Mission not found');
|
||||
const existing = await this.brain.missionTasks.findByIdAndUser(taskId, user.id);
|
||||
if (!existing) throw new NotFoundException('Mission task not found');
|
||||
const deleted = await this.brain.missionTasks.remove(taskId);
|
||||
if (!deleted) throw new NotFoundException('Mission task not found');
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user