2026-02-06 23:01:30 -05:00

232 lines
6.4 KiB
TypeScript

/**
* MCPEngine Studio — Deploy Orchestrator
*
* Main entry point for the deployment pipeline.
* Takes a DeployTarget + config, routes to the correct target,
* and yields progress events via async generator.
*/
import type {
DeployTarget,
DeployConfig,
DeployResult,
ServerBundle,
} from '@mcpengine/ai-pipeline/types';
import { deployToMCPEngine } from './targets/mcpengine';
import { deployAsDownload } from './targets/download';
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
export interface DeployEvent {
type: 'progress' | 'log' | 'complete' | 'error';
step?: DeployStep;
message: string;
percent: number;
level: 'info' | 'success' | 'warning' | 'error';
timestamp: string;
result?: DeployResult;
}
export type DeployStep = 'build' | 'test' | 'package' | 'deploy' | 'verify';
const STEP_ORDER: DeployStep[] = ['build', 'test', 'package', 'deploy', 'verify'];
// Map internal steps from targets to canonical DeploySteps
function mapStep(raw: string): DeployStep {
const mapping: Record<string, DeployStep> = {
compile: 'build',
build: 'build',
test: 'test',
package: 'package',
deploy: 'deploy',
upload: 'deploy',
verify: 'verify',
};
return mapping[raw] ?? 'build';
}
// ---------------------------------------------------------------------------
// Orchestrator
// ---------------------------------------------------------------------------
export async function* deploy(
bundle: ServerBundle,
config: DeployConfig,
): AsyncGenerator<DeployEvent, DeployResult, void> {
const ts = () => new Date().toISOString();
// ── Pre-flight ─────────────────────────────────────────────────────────
yield {
type: 'log',
message: `Starting deployment to ${config.target}`,
percent: 0,
level: 'info',
timestamp: ts(),
};
yield {
type: 'progress',
step: 'build',
message: 'Initializing build pipeline…',
percent: 5,
level: 'info',
timestamp: ts(),
};
// ── Build step (validate bundle) ───────────────────────────────────────
if (!bundle.files.length) {
yield {
type: 'error',
step: 'build',
message: 'Server bundle is empty — nothing to deploy',
percent: 0,
level: 'error',
timestamp: ts(),
};
throw new Error('Empty server bundle');
}
yield {
type: 'log',
message: `Bundle: ${bundle.files.length} files, ${bundle.toolCount} tools, entry: ${bundle.entryPoint}`,
percent: 8,
level: 'info',
timestamp: ts(),
};
// ── Test step (placeholder — real tests come from test pipeline) ──────
yield {
type: 'progress',
step: 'test',
message: 'Running pre-deploy checks…',
percent: 15,
level: 'info',
timestamp: ts(),
};
// Quick static checks
const hasEntry = bundle.files.some((f) => f.path === bundle.entryPoint);
if (!hasEntry) {
yield {
type: 'error',
step: 'test',
message: `Entry point "${bundle.entryPoint}" not found in bundle`,
percent: 15,
level: 'error',
timestamp: ts(),
};
throw new Error('Missing entry point');
}
yield {
type: 'progress',
step: 'test',
message: 'Pre-deploy checks passed',
percent: 20,
level: 'success',
timestamp: ts(),
};
// ── Route to target ────────────────────────────────────────────────────
let innerGen: AsyncGenerator<any, DeployResult, void>;
switch (config.target) {
case 'mcpengine':
case 'cloudflare':
innerGen = deployToMCPEngine(
config.slug ?? 'mcp-server',
bundle,
config,
);
break;
case 'download':
innerGen = deployAsDownload(bundle, config.slug);
break;
case 'npm':
// TODO: npm publish pipeline
yield {
type: 'log',
message: 'npm deployment is not yet implemented — falling back to download',
percent: 25,
level: 'warning',
timestamp: ts(),
};
innerGen = deployAsDownload(bundle, config.slug);
break;
case 'docker':
// TODO: Docker build/push pipeline
yield {
type: 'log',
message: 'Docker deployment is not yet implemented — falling back to download',
percent: 25,
level: 'warning',
timestamp: ts(),
};
innerGen = deployAsDownload(bundle, config.slug);
break;
default:
throw new Error(`Unknown deploy target: ${config.target}`);
}
// ── Stream inner target events ─────────────────────────────────────────
let result: DeployResult | undefined;
while (true) {
const { value, done } = await innerGen.next();
if (done) {
result = value;
break;
}
// Forward progress events with normalized step names
yield {
type: 'progress',
step: mapStep(value.step),
message: value.message,
percent: Math.min(20 + Math.round(value.percent * 0.75), 95),
level: value.level,
timestamp: ts(),
};
yield {
type: 'log',
message: value.message,
percent: Math.min(20 + Math.round(value.percent * 0.75), 95),
level: value.level,
timestamp: ts(),
};
}
if (!result) {
throw new Error('Deploy target did not return a result');
}
// ── Complete ───────────────────────────────────────────────────────────
yield {
type: 'complete',
step: 'verify',
message: `Deployment complete! ${result.url ?? ''}`,
percent: 100,
level: 'success',
timestamp: ts(),
result,
};
return result;
}
// Re-exports for convenience
export { deployToMCPEngine } from './targets/mcpengine';
export { deployAsDownload } from './targets/download';
export { compileServer, compileWithMeta } from './compiler';
export { generateWorkerScript } from './worker-template';
export { STEP_ORDER };
export type { DeployTarget, DeployConfig, DeployResult };