import { constants as fsConstants, promises as fs } from 'node:fs'; import * as http from 'node:http'; import * as path from 'node:path'; import type { MemberWorkSyncReportRequest } from '../../../../src/features/member-work-sync/contracts'; import type { MemberWorkSyncFeatureFacade } from '../../../../src/features/member-work-sync/main'; import type { TeamProvisioningProgress } from '../../../../src/shared/types'; export interface MemberWorkSyncLiveControlServer { baseUrl: string; close(): Promise; } export async function startMemberWorkSyncControlServer( feature: MemberWorkSyncFeatureFacade ): Promise { const server = http.createServer(async (request, response) => { try { const url = new URL(request.url ?? '/', 'http://127.0.0.1'); const parts = url.pathname.split('/').filter(Boolean).map(decodeURIComponent); if ( request.method === 'GET' && parts.length === 5 && parts[0] === 'api' && parts[1] === 'teams' && parts[3] === 'member-work-sync' ) { const payload = await feature.getStatus({ teamName: parts[2], memberName: parts[4], }); sendJson(response, 200, payload); return; } if ( request.method === 'POST' && parts.length === 5 && parts[0] === 'api' && parts[1] === 'teams' && parts[3] === 'member-work-sync' && parts[4] === 'report' ) { const body = (await readRequestJson(request)) as MemberWorkSyncReportRequest; const payload = await feature.report({ ...body, teamName: parts[2], source: 'mcp', }); sendJson( response, payload.accepted ? 200 : 400, payload.accepted ? payload : { error: payload.code } ); return; } sendJson(response, 404, { error: `Unhandled ${request.method} ${url.pathname}` }); } catch (error) { sendJson(response, 500, { error: error instanceof Error ? error.message : String(error) }); } }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', resolve); }); const address = server.address(); if (!address || typeof address === 'string') { throw new Error('Failed to bind member work sync control server'); } return { baseUrl: `http://127.0.0.1:${address.port}`, close: () => new Promise((resolve, reject) => { server.close((error) => (error ? reject(error) : resolve())); }), }; } export function restoreEnv(name: string, previous: string | undefined): void { if (previous === undefined) { delete process.env[name]; } else { process.env[name] = previous; } } export async function assertExecutable(filePath: string): Promise { await fs.access(filePath, fsConstants.X_OK); } export async function waitUntil( predicate: () => Promise, timeoutMs: number, pollMs = 2_000, getDiagnostics?: () => Promise ): Promise { const deadline = Date.now() + timeoutMs; let lastError: unknown; while (Date.now() < deadline) { try { if (await predicate()) { return; } } catch (error) { lastError = error; } await new Promise((resolve) => setTimeout(resolve, pollMs)); } const suffix = lastError instanceof Error && lastError.message ? ` Last error: ${lastError.message}` : ''; const diagnostics = getDiagnostics ? `\n${await getDiagnostics().catch(String)}` : ''; throw new Error(`Timed out after ${timeoutMs}ms waiting for condition.${suffix}${diagnostics}`); } export function formatProgressDump(progressEvents: TeamProvisioningProgress[]): string { return progressEvents .map((progress) => [ progress.state, progress.message, progress.messageSeverity, progress.error, progress.cliLogsTail, ] .filter(Boolean) .join(' | ') ) .join('\n'); } export async function formatMemberWorkSyncDiagnostics(input: { feature: MemberWorkSyncFeatureFacade; teamName: string; memberName: string; taskId?: string; }): Promise { const [{ TeamTaskReader }] = await Promise.all([ import('../../../../src/main/services/team/TeamTaskReader'), ]); const [status, metrics, tasks] = await Promise.all([ input.feature.getStatus({ teamName: input.teamName, memberName: input.memberName }), input.feature.getMetrics({ teamName: input.teamName }), input.taskId ? new TeamTaskReader().getTasks(input.teamName) : Promise.resolve([]), ]); const task = input.taskId ? tasks.find((candidate) => candidate.id === input.taskId) : undefined; return [ 'Member work sync live diagnostics:', JSON.stringify( { state: status.state, diagnostics: status.diagnostics, agendaFingerprint: status.agenda.fingerprint, agendaItems: status.agenda.items.map((item) => ({ taskId: item.taskId, subject: item.subject, assignee: item.assignee, kind: item.kind, })), report: status.report, shadow: status.shadow, queue: input.feature.getQueueDiagnostics(), comments: task?.comments?.map((comment) => ({ author: comment.author, text: comment.text, })), recentEvents: metrics.recentEvents.slice(-12), }, null, 2 ), ].join('\n'); } export async function readRuntimeTurnSettledProcessedMetas(teamsBasePath: string): Promise< Array<{ filePath: string; meta: Record; }> > { const processedDir = path.join( teamsBasePath, '.member-work-sync', 'runtime-hooks', 'processed' ); const entries = await fs.readdir(processedDir, { withFileTypes: true }).catch(() => []); const metas = await Promise.all( entries .filter((entry) => entry.isFile() && entry.name.endsWith('.meta.json')) .map(async (entry) => { const filePath = path.join(processedDir, entry.name); const raw = await fs.readFile(filePath, 'utf8'); return { filePath, meta: JSON.parse(raw) as Record }; }) ); return metas.sort((left, right) => left.filePath.localeCompare(right.filePath)); } async function readRequestJson(request: http.IncomingMessage): Promise { const chunks: Buffer[] = []; for await (const chunk of request) { chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); } const raw = Buffer.concat(chunks).toString('utf8').trim(); return raw ? JSON.parse(raw) : {}; } function sendJson(response: http.ServerResponse, statusCode: number, payload: unknown): void { response.writeHead(statusCode, { 'content-type': 'application/json', }); response.end(JSON.stringify(payload)); }