import type { z } from "zod/v4"; import type { ToolEffect } from "../manifest/effect"; import type { AppCommand } from "../manifest/command"; import type { AppEndpointInput } from "../manifest/endpoints"; import type { AppFileTypeV2Input } from "../manifest/file-type"; import type { AppJobDefinitionInput } from "../manifest/jobs"; import { colineAppManifestV2Schema } from "../manifest/manifest"; import { extractManifestInput } from "./extraction"; import type { AnyColineJobDefinitionV2, AnyColineToolDefinitionV2, ColineAppConfigV2, ColineAppV2, ColineCommandDefinitionV2, ColineEndpointDefinitionV2, ColineEndpointHandler, ColineFileTypeDefinitionV2, ColineFileTypeRuntime, ColineJobContextV2, ColineJobDefinitionV2, ColineJobOutcome, ColineToolContext, ColineToolDefinitionV2, ColineToolResult, } from "./types"; /** * `defineApp` and friends (plan §6.1). * * `defineApp` performs extraction and validates the manifest eagerly, so a * mis-declared app fails at module evaluation in development and in the * restricted build environment — never at install time. */ export class ColineAppDefinitionError extends Error { readonly issues: readonly string[]; constructor(appKey: string, issues: readonly string[]) { super( `App definition "${appKey}" is invalid:\n${issues .map((issue) => ` - ${issue}`) .join("\n")}`, ); this.name = "ColineAppDefinitionError"; this.issues = issues; } } export function defineTool(definition: { name: string; description: string; input: z.ZodType; effect: ToolEffect; execute: ( input: TInput, context: ColineToolContext, ) => Promise | ColineToolResult; }): ColineToolDefinitionV2 { return Object.freeze({ kind: "tool" as const, ...definition }); } export function defineFileType(definition: { metadata: AppFileTypeV2Input; runtime?: ColineFileTypeRuntime; }): ColineFileTypeDefinitionV2 { return Object.freeze({ kind: "fileType" as const, metadata: definition.metadata, runtime: definition.runtime ?? {}, }); } export function defineCommand(command: AppCommand): ColineCommandDefinitionV2 { return Object.freeze({ kind: "command" as const, command }); } /** * An inbound HTTP endpoint. The metadata becomes `manifest.endpoints[]`; * `handle` runs after Coline verifies the request. */ export function defineEndpoint( definition: AppEndpointInput & { handle: ColineEndpointHandler }, ): ColineEndpointDefinitionV2 { const { handle, ...metadata } = definition; return Object.freeze({ kind: "endpoint" as const, metadata, handle }); } /** * A background job. The metadata becomes `manifest.jobs[]`; `run` executes * on Coline's job workers. Give `payload` a zod schema to validate what * `coline.jobs.enqueue` passes before each attempt. */ export function defineJob>( definition: AppJobDefinitionInput & { payload?: z.ZodType; run: ( payload: TPayload, context: ColineJobContextV2, ) => Promise | ColineJobOutcome; }, ): ColineJobDefinitionV2 { const { run, payload, ...metadata } = definition; return Object.freeze({ kind: "job" as const, metadata, payload: payload ?? null, run }); } export function defineApp(config: ColineAppConfigV2): ColineAppV2 { const manifestInput = extractManifestInput(config); const parsed = colineAppManifestV2Schema.safeParse(manifestInput); if (!parsed.success) { throw new ColineAppDefinitionError( typeof config.key === "string" ? config.key : "(unknown)", parsed.error.issues.map( (issue) => `${issue.path.join(".") || "(root)"}: ${issue.message}`, ), ); } const manifest = parsed.data; const tools = new Map(); for (const tool of config.tools ?? []) { tools.set(tool.name, tool); } const fileTypes = new Map(); const runtimeIssues: string[] = []; for (const fileType of config.files ?? []) { fileTypes.set(fileType.metadata.typeKey, fileType.runtime); } // Runtime completeness: every tree-tier surface needs its handler. const manifestFilesByKey = new Map(manifest.files.map((file) => [file.typeKey, file])); for (const [typeKey, runtime] of fileTypes) { const declared = manifestFilesByKey.get(typeKey); if (!declared) continue; if (declared.surfaces.editor?.tier === "tree" && !runtime.renderEditor) { runtimeIssues.push(`files.${typeKey}: tree editor surface declared without a renderEditor handler.`); } if (declared.surfaces.preview && !runtime.renderPreview) { runtimeIssues.push(`files.${typeKey}: preview surface declared without a renderPreview handler.`); } if (declared.surfaces.inline && !runtime.renderInline) { runtimeIssues.push(`files.${typeKey}: inline surface declared without a renderInline handler.`); } if (declared.indexable && declared.storage !== "external_pointer" && !runtime.index) { runtimeIssues.push(`files.${typeKey}: indexable file types need index extractors (or set indexable: false).`); } } if (manifest.surfaces.home?.tier === "tree" && !config.handlers?.renderHome) { runtimeIssues.push("surfaces.home: tree home surface declared without a handlers.renderHome."); } if ( manifest.triggers.schedules.length > 0 && manifest.hosting.default === "coline" && !config.handlers?.onSchedule ) { runtimeIssues.push( "triggers.schedules: Coline-hosted schedules declared without a handlers.onSchedule.", ); } const eventsHosted = (manifest.hosting.overrides["events"] ?? manifest.hosting.default) === "coline"; if (manifest.triggers.webhooks.length > 0 && eventsHosted && !config.handlers?.onEvent) { runtimeIssues.push( "triggers.webhooks: Coline-hosted event subscriptions declared without a handlers.onEvent.", ); } const publicHosted = (manifest.hosting.overrides["public"] ?? manifest.hosting.default) === "coline"; if (manifest.surfaces.public?.tier === "tree" && publicHosted && !config.handlers?.renderPublic) { runtimeIssues.push("surfaces.public: tree public surface declared without a handlers.renderPublic."); } const endpoints = new Map(); for (const endpoint of config.endpoints ?? []) { const id = endpoint.metadata.id; if (endpoints.has(id)) runtimeIssues.push(`endpoints.${id}: defined more than once.`); endpoints.set(id, endpoint.handle); } const jobs = new Map(); for (const job of config.jobs ?? []) { if (jobs.has(job.metadata.name)) runtimeIssues.push(`jobs.${job.metadata.name}: defined more than once.`); jobs.set(job.metadata.name, job); } if (runtimeIssues.length > 0) { throw new ColineAppDefinitionError(manifest.key, runtimeIssues); } return Object.freeze({ kind: "app" as const, manifest, runtime: Object.freeze({ tools, fileTypes, endpoints, jobs, renderHome: config.handlers?.renderHome ?? null, renderPublic: config.handlers?.renderPublic ?? null, onSchedule: config.handlers?.onSchedule ?? null, onEvent: config.handlers?.onEvent ?? null, }), }); }