/** * The package as components (`pkg.manifest()`, for tooling): the package, * and nothing but the API. Package fields on top, then `components`, one per * published resource — `{ name, resource_type, resource }` with the * resource's body in snake_case, children nested the way the API nests them * (`action_specifications` and `link_specifications` under their * specification). An `event()` is a `notification_channel` component. */ import type { EventComponent } from "./components"; import { pluralKind, toApiKeys } from "./keys"; import type { Resource } from "./resource"; export interface PackageAgentConfig { /** Selector tags an agent must carry to handle this package. Default: `{ package: }`. */ selector?: Record; /** Path the agent execs in the worker image. Default: `/app/packages//entrypoint`. */ entrypoint?: string; /** Notification sources the package's channel subscribes to. Default: derived from the actions. */ sources?: string[]; /** Bind the channel to each published version. Default false. */ pinVersion?: boolean; /** Whether `np package publish` manages the channel. Default true. */ manageChannel?: boolean; } export type ResolvedAgentConfig = Required; export interface ManifestInput { name: string; slug: string; version: string; description?: string; agent: ResolvedAgentConfig; resources: Resource[]; events: EventComponent[]; } export interface ManifestComponent { name: string; resource_type: string; resource: Record; } export interface Manifest { name: string; slug: string; version: string; description?: string; agent: Record; components: ManifestComponent[]; } function specificationBody(res: Resource): Record { const body: Record = { ...res.body }; const links = res.children.filter((c) => c.kind === "link_specification"); const others = res.children.filter( (c) => c.kind !== "link_specification" && c.kind !== "scope_type" ); if (links.length) { body.availableLinks = links.map((l) => l.slug); body.linkSpecifications = links.map((l) => { const { plans: _plans, ...rest } = l.body as Record; return { ...rest }; }); } for (const child of others) { const key = pluralKind(child.kind); (body[key] as unknown[] | undefined) ? (body[key] as unknown[]).push(child.body) : (body[key] = [child.body]); } return body; } /** The channel that routes one event to this package's worker, as `np package publish` would create it. */ export function channelFor( ev: EventComponent, input: Pick ): Record { return { source: [ev.source], type: "agent", description: `np-package ${input.slug} ${ev.name}`, filters: { action: { $eq: ev.name } }, configuration: { selector: input.agent.selector, command: { type: "package-exec", data: { package: input.slug, cmdline: input.agent.entrypoint, environment: { NP_ACTION_CONTEXT: "${NOTIFICATION_CONTEXT}", NP_PLUGIN: input.slug, }, }, }, }, }; } export function buildManifest(input: ManifestInput): Manifest { const components: ManifestComponent[] = []; for (const res of input.resources) { if (res.parent && res.kind === "link_specification") continue; // nested under its specification const body = res.kind === "service_specification" ? specificationBody(res) : { ...res.body }; components.push({ name: res.slug, resource_type: res.kind, resource: body, }); } const out = toApiKeys({ name: input.name, slug: input.slug, version: input.version, ...(input.description ? { description: input.description } : {}), agent: input.agent, components, }) as Manifest; // Channels are written in the API's own case already, and carry environment // variable names that must stay verbatim. for (const ev of input.events) { out.components.push({ name: ev.name, resource_type: "notification_channel", resource: channelFor(ev, input), }); } return out; }