import { packageStorage } from 'kody:runtime' export const COMMAND_PREFIX = 'command:' export type RaycastCommand = { id: string title: string description: string /** Raycast Icon key (e.g. "Hammer") or a single emoji. */ icon: string /** Full ESM module string with `export default async function main(params)`. */ code: string createdAt: string updatedAt: string } export type RaycastCommandSummary = Omit & { codeBytes: number } function trim(value: unknown) { return typeof value === 'string' ? value.trim() : '' } function slugify(value: string) { const base = value .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 64) return base || `cmd-${Date.now().toString(36)}` } export function commandKey(id: string) { return `${COMMAND_PREFIX}${id}` } function asCommand(value: unknown): RaycastCommand | null { if (!value || typeof value !== 'object' || Array.isArray(value)) return null const row = value as Record const id = trim(row.id) const title = trim(row.title) const code = typeof row.code === 'string' ? row.code : '' if (!id || !title || !code.trim()) return null return { id, title, description: trim(row.description), icon: trim(row.icon) || 'Terminal', code, createdAt: trim(row.createdAt) || new Date().toISOString(), updatedAt: trim(row.updatedAt) || new Date().toISOString(), } } function normalizeListEntries(listed: unknown): unknown[] { if (Array.isArray(listed)) return listed if (!listed || typeof listed !== 'object') return [] const obj = listed as Record for (const candidate of [obj.keys, obj.entries, obj.items]) { if (Array.isArray(candidate)) return candidate } return [] } function entryKey(entry: unknown): string | null { if (typeof entry === 'string') return entry if (!entry || typeof entry !== 'object') return null const obj = entry as Record for (const key of ['key', 'name', 'id']) { if (typeof obj[key] === 'string' && obj[key]) return obj[key] as string } return null } function nextStartAfterOf(listed: unknown): string | null { if (!listed || typeof listed !== 'object') return null const obj = listed as Record for (const key of ['nextStartAfter', 'cursor', 'continuationToken', 'nextCursor']) { const value = obj[key] if (typeof value === 'string' && value) return value } return null } function isListTruncated(listed: unknown): boolean { if (!listed || typeof listed !== 'object') return false return Boolean((listed as Record).truncated) } export async function listCommandRecords(): Promise { const storage = packageStorage() const keys: string[] = [] let startAfter: string | undefined for (let page = 0; page < 40; page++) { const listed = await storage.list({ prefix: COMMAND_PREFIX, limit: 256, ...(startAfter ? { startAfter } : {}), }) for (const entry of normalizeListEntries(listed)) { const key = entryKey(entry) if (key?.startsWith(COMMAND_PREFIX)) keys.push(key) } const next = nextStartAfterOf(listed) if (!isListTruncated(listed) || !next || next === startAfter) break startAfter = next } const out: RaycastCommand[] = [] for (const key of keys) { const command = asCommand(await storage.get(key)) if (command) out.push(command) } out.sort((a, b) => a.title.localeCompare(b.title, undefined, { sensitivity: 'base' })) return out } export async function getCommandRecord(id: string): Promise { const trimmed = trim(id) if (!trimmed) return null return asCommand(await packageStorage().get(commandKey(trimmed))) } export function toSummary(command: RaycastCommand): RaycastCommandSummary { const { code, ...rest } = command return { ...rest, codeBytes: new TextEncoder().encode(code).length } } export type UpsertCommandInput = { id?: string title: string description?: string icon?: string code: string } export async function upsertCommandRecord( input: UpsertCommandInput, ): Promise { const title = trim(input.title) const code = typeof input.code === 'string' ? input.code : '' if (!title) throw new Error('title is required.') if (!code.trim()) throw new Error('code is required (ESM module with export default).') const id = trim(input.id) || slugify(title) const existing = await getCommandRecord(id) const now = new Date().toISOString() const command: RaycastCommand = { id, title, description: trim(input.description), icon: trim(input.icon) || existing?.icon || 'Terminal', code, createdAt: existing?.createdAt || now, updatedAt: now, } await packageStorage().set(commandKey(id), command) return command } export async function deleteCommandRecord(id: string): Promise<{ deleted: boolean; id: string }> { const trimmed = trim(id) if (!trimmed) throw new Error('id is required.') const existing = await getCommandRecord(trimmed) if (!existing) return { deleted: false, id: trimmed } await packageStorage().delete(commandKey(trimmed)) return { deleted: true, id: trimmed } }