← Public packages
@kentcdodds/raycast
Raycast launcher: stored execute commands via webhooks (canonical; do not use raycast-shortcuts).
src/commands-store.ts
169 lines · 5.0 KB · TypeScriptimport { 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<RaycastCommand, 'code'> & {
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<string, unknown>
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<string, unknown>
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<string, unknown>
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<string, unknown>
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<string, unknown>).truncated)
}
export async function listCommandRecords(): Promise<RaycastCommand[]> {
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<RaycastCommand | null> {
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<RaycastCommand> {
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 }
}