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,
})
}