Skip to content

Built for people who want to own their automations. Join the waitlist for an invite.

Package listing

@kody/raycast

src/invoke.ts

102 lines · 3.0 KB · TypeScript
import { packages } from 'kody:runtime'
import getPackage from './get-package.ts'
import { asRecord, isDryRun, optionalString } from './helpers.ts'

export type InvokeInput = {
	kodyId?: string
	packageId?: string
	exportName: string
	params?: Record<string, unknown>
	idempotencyKey?: string
	topic?: string
	dryRun?: boolean
}

/**
 * Invoke one of the caller's saved package exports from an external client.
 *
 * HTTP callers authenticate against this `raycast` package, then pass the
 * target `kodyId` (or `packageId`) and `exportName` here. `packages.invoke`
 * runs the target in its own package runtime.
 *
 * Pass `dryRun: true` to resolve the target and preview the call without
 * invoking it. Live invokes do not default to dry-run — this export is the
 * launcher front door.
 *
 * @param params.kodyId - Target package kody.id (preferred).
 * @param params.packageId - Target saved package id when kodyId is omitted.
 * @param params.exportName - Export to run (`./list-packages`, `list-packages`, or `.`).
 * @param params.params - JSON object passed to the target export.
 * @param params.idempotencyKey - Optional exactly-once key; omit for the lean path.
 * @param params.topic - Optional topic label forwarded to the target invocation.
 * @param params.dryRun - When true, preview without calling the target.
 * @returns The target export's unwrapped return value, or a dry-run preview.
 *
 * @example
 * import invoke from 'kody:@kody/raycast/invoke'
 * const preview = await invoke({
 *   kodyId: 'notify',
 *   exportName: '.',
 *   params: { dryRun: true },
 *   dryRun: true,
 * })
 */
export default async function invoke(input: InvokeInput) {
	const record = asRecord(input)
	const exportName = optionalString(record, 'exportName')
	if (!exportName) {
		throw new Error('exportName is required.')
	}

	const kodyId = optionalString(record, 'kodyId')
	const packageId = optionalString(record, 'packageId')
	if (!kodyId && !packageId) {
		throw new Error('Pass kodyId or packageId.')
	}

	const params =
		record.params && typeof record.params === 'object' && !Array.isArray(record.params)
			? (record.params as Record<string, unknown>)
			: {}
	const idempotencyKey = optionalString(record, 'idempotencyKey')
	const topic = optionalString(record, 'topic')

	if (isDryRun(record)) {
		const target = await getPackage({ packageId, kodyId })
		const matched = target.exports.find(entry => {
			return (
				entry.subpath === exportName ||
				entry.exportName === exportName ||
				(exportName === '.' && entry.subpath === '.')
			)
		})
		return {
			dryRun: true,
			wouldInvoke: true,
			kodyId: target.kodyId,
			packageId: target.packageId,
			name: target.name,
			exportName,
			matchedExport: matched
				? {
						subpath: matched.subpath,
						exportName: matched.exportName,
						importSpecifier: matched.importSpecifier,
						description: matched.description,
					}
				: null,
			params,
			idempotencyKey: idempotencyKey ?? null,
			topic: topic ?? null,
		}
	}

	return await packages.invoke({
		kodyId,
		packageId,
		exportName,
		params,
		idempotencyKey,
		topic,
	})
}