feat: initial public release (MAESTRO)
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
import { Router, Request, Response } from 'express';
|
||||
import { existsSync, readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
import { type Repository, type Job, localTaskRepoName } from '../db/repository.js';
|
||||
import { logger } from '../logger.js';
|
||||
import { canViewTask } from './local-api-helpers.js';
|
||||
|
||||
const MAX_ACTIVITY_LOG_CHARS = 4000;
|
||||
|
||||
function readActivityLog(worktreePath: string | null, maxChars: number = 0): string {
|
||||
if (!worktreePath) return '';
|
||||
const logPath = join(worktreePath, 'logs', 'activity.log');
|
||||
if (!existsSync(logPath)) return '';
|
||||
try {
|
||||
const content = readFileSync(logPath, 'utf-8');
|
||||
return maxChars > 0 && content.length > maxChars
|
||||
? content.slice(-maxChars)
|
||||
: content;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
export function createSubtaskActivityRouter(repo: Repository): Router {
|
||||
const router = Router();
|
||||
|
||||
// GET /:id/subtasks/activities — bulk fetch all subtask activities (includes nested subtasks)
|
||||
router.get('/:id/subtasks/activities', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const taskId = Number(req.params.id);
|
||||
const viewer = req.user as Express.User | undefined;
|
||||
const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
|
||||
if (!canViewTask(req, res, task)) return;
|
||||
|
||||
const latestJob = await repo.getLatestJobForIssue(localTaskRepoName(taskId), taskId);
|
||||
if (!latestJob) { res.status(404).json({ error: 'No job found' }); return; }
|
||||
|
||||
// 再帰的に全サブジョブ(孫含む)を収集
|
||||
const collectAllSubJobs = async (parentId: string): Promise<Job[]> => {
|
||||
const jobs = await repo.getSubJobs(parentId);
|
||||
const result = [...jobs];
|
||||
for (const job of jobs) {
|
||||
if (job.status === 'waiting_subtasks') {
|
||||
result.push(...await collectAllSubJobs(job.id));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
};
|
||||
const allJobs = await collectAllSubJobs(latestJob.id);
|
||||
|
||||
const subtasks = allJobs.map(job => ({
|
||||
jobId: job.id,
|
||||
issueNumber: job.issueNumber,
|
||||
status: job.status,
|
||||
currentMovement: job.currentMovement ?? null,
|
||||
currentActivity: job.currentActivity ?? null,
|
||||
activityLog: readActivityLog(job.worktreePath, MAX_ACTIVITY_LOG_CHARS),
|
||||
}));
|
||||
|
||||
res.json({ subtasks });
|
||||
} catch (err) {
|
||||
logger.error(`Subtask activities API error: ${err}`);
|
||||
res.status(500).json({ error: 'Failed to fetch subtask activities' });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /:id/subtasks/:jobId/activity — individual subtask activity (supports nested subtasks)
|
||||
router.get('/:id/subtasks/:jobId/activity', async (req: Request, res: Response) => {
|
||||
try {
|
||||
const taskId = Number(req.params.id);
|
||||
const jobId = req.params.jobId;
|
||||
|
||||
const viewer = (req.user as Express.User | undefined) ?? undefined;
|
||||
const task = await repo.getLocalTask(taskId, viewer ? { viewer } : undefined);
|
||||
if (!canViewTask(req, res, task)) return;
|
||||
|
||||
// jobId で直接取得(孫タスクにも対応)
|
||||
const job = await repo.getJob(jobId, viewer ? { viewer } : undefined);
|
||||
if (!job || !job.worktreePath) { res.status(404).json({ error: 'Subtask not found' }); return; }
|
||||
|
||||
// タスクのワークスペース配下であることを確認
|
||||
if (task!.workspacePath && !job.worktreePath.startsWith(task!.workspacePath)) {
|
||||
res.status(404).json({ error: 'Subtask not found' }); return;
|
||||
}
|
||||
|
||||
res.json({ activityLog: readActivityLog(job.worktreePath) });
|
||||
} catch (err) {
|
||||
logger.error(`Subtask activity API error: ${err}`);
|
||||
res.status(500).json({ error: 'Failed to fetch subtask activity' });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
Reference in New Issue
Block a user