import getPackage from './get-package.ts' import { asRecord, isDryRun, optionalString } from './helpers.ts' export type InvokeInput = { kodyId?: string packageId?: string exportName: string params?: Record /** * Deprecated: packages.invoke sunset (#1750 / codemod 0008). Exactly-once * is not available on this lean path — use a workflow. The key is ignored. */ idempotencyKey?: string /** Deprecated: ignored after packages.invoke sunset. */ 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. The target loads via * computed `import(specifier)` (name is data). * * 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.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) : {} // idempotencyKey / topic intentionally ignored (no packages.invoke; no workflow). const idempotencyKey = optionalString(record, 'idempotencyKey') const topic = optionalString(record, 'topic') void idempotencyKey void topic const target = await getPackage({ packageId, kodyId }) const matched = target.exports.find(entry => { return ( entry.subpath === exportName || entry.exportName === exportName || entry.exportName === exportName.replace(/^\.\//, '') || (exportName === '.' && entry.subpath === '.') ) }) if (!matched) { throw new Error(`Package ${target.name} does not export ${exportName}.`) } if (isDryRun(record)) { return { dryRun: true, wouldInvoke: true, kodyId: target.kodyId, packageId: target.packageId, name: target.name, exportName, matchedExport: { subpath: matched.subpath, exportName: matched.exportName, importSpecifier: matched.importSpecifier, description: matched.description, }, params, idempotencyKey: null, topic: null, } } const raw = matched.importSpecifier const trimmed = typeof raw === 'string' ? raw.trim() : '' const specifier = typeof raw === 'string' && trimmed.startsWith('@') ? `kody:${trimmed}` : trimmed const mod = await import(specifier) return await mod.default(params) }