raycast-extension-dev¶
Build, verify, and ship Raycast extensions in
TypeScript. Raycast's own docs cover what List and Form accept; this skill
covers the other 20% — what the toolchain does not check, what the runtime does
not provide, and what the Raycast Store checks that the linter does not.
Every claim in it was paid for once while building
Pueue for Raycast, and
most of them trace back to a file in that repo's
pitfalls/.
| Surface | Question it answers |
|---|---|
new-raycast-extension.sh |
"Scaffold me an extension that is gated from the first commit." |
check-store-readiness.sh |
"What will store review reject that ray lint just passed?" |
references/runtime-and-subprocess.md |
"Why is my binary not found, when which finds it instantly?" |
references/manifest-and-commands.md |
"What can a package.json command / preference / argument actually declare?" |
references/data-and-state.md |
"Why does my action flash, revert, and then re-apply?" |
references/ui-patterns.md |
"Which List/Form/ActionPanel idiom, and why did my dropdown reset itself?" |
references/menu-bar.md |
"How do I show a count without opening Raycast, and why is mine stale?" |
references/store-publishing.md |
"What is left before I can submit, and what can never be automated?" |
The gate, and why it is four commands¶
The single most expensive fact about the Raycast toolchain:
| Command | Catches | Misses |
|---|---|---|
tsc --noEmit |
types | manifest, formatting, runtime shape |
node dev-check.js |
your own invariants, wire shapes, generated argv | anything you did not assert |
ray lint |
manifest schema, icons, ESLint, Prettier, reserved-shortcut collisions | types |
ray build (-e dev) |
syntax, that esbuild can bundle it | types — esbuild strips them without checking |
ray build -e dist |
the above plus types (it runs tsc -p tsconfig.json --noEmit) |
manifest, formatting |
Measured on a scaffold carrying one genuine TS2345: ray build exits 0 and
prints ready - built extension successfully; ray build -e dist exits 1 and
reports it. The dist build is therefore a stronger pre-submit check than most
people realise — but npm run build, just build, and ray develop are all the
dev build, which is precisely why a type error survives long enough to ship.
Two real bugs took that route in the source extension: a MutatePromise<GroupMap>
passed where MutatePromise<State> was expected — which at runtime would have
emptied the list on every action — and a ReactNode/JSX.Element clash caused by
@raycast/api bundling its own copy of @types/react. Neither produced a single
line of output.
The skill ships that gate as assets/Justfile.template, and the scaffolder writes
it into every new extension.
What ray lint does not check¶
Verified, not assumed: ray lint exits 0 with a completely empty metadata/
directory. Its "validate extension metadata" stage passes. So the linter cannot
be your submission gate — screenshot count and dimensions, icon size, whether the
icon is still a placeholder, the CHANGELOG placeholder, and a real author are all
review-time requirements nothing runs locally.
check-store-readiness.sh covers exactly that column, emitting a JSON array of
{id, status, detail, fix} and exiting 4 when something fails, 3 when a check
had to be skipped for a missing tool (skipped is never reported as passed).
When the skill triggers¶
- "Build me a Raycast extension for X" / "wrap this CLI in Raycast"
- "It works in
npm run devbut not from Raycast" — the launchd PATH trap, which the dev console structurally cannot reproduce - "The menu bar shows the wrong count / nothing / stale data"
- "My action flashes the new state, reverts, then re-applies"
- "Get this ready for the Raycast Store"
When it doesn't¶
- Raycast Script Commands — a separate repo, no manifest, no React.
- Generic React/TSX quality →
react-best-practices. - The wrapped CLI's own semantics → that tool's skill, or its
--help. - AI Extensions without Raycast Pro —
tools[]is Pro-gated, and there is no confirmation surface inside a tool call, so a first version must be read-only. - Taking the screenshots. The skill verifies their count and dimensions. It cannot capture a window, and neither can any CLI.
Structure¶
skills/local/raycast-extension-dev/
├── SKILL.md 485 lines — workflows A-F + 27 gotchas
├── references/
│ ├── runtime-and-subprocess.md launchd env, path probing, execFile, streaming, error taxonomy
│ ├── manifest-and-commands.md every manifest field + the schema length minimums
│ ├── data-and-state.md hooks, cache-key scoping, the reconcile measurement loop
│ ├── ui-patterns.md List/Detail/Form/ActionPanel, dropdowns, shortcuts, markdown fencing
│ ├── menu-bar.md every MenuBarExtra constraint with its consequence
│ └── store-publishing.md checklist, screenshot mechanics, review criteria
├── scripts/
│ ├── new-raycast-extension.sh scaffold a gated extension (exit 0/1/2/3/4)
│ └── check-store-readiness.sh the store checks ray lint skips (exit 0/1/2/3/4)
└── assets/
├── Justfile.template the four-stage gate
├── tsconfig.json.template note: include must list raycast-env.d.ts
├── eslint.config.mjs.template flat config for @raycast/eslint-config v2
├── package.json.template manifest skeleton, known-good deps
├── dev-check.ts.template the no-test-runner harness
├── transport.ts.template the Mutation data-union seam
├── error-descriptor.tsx.template one descriptor, four renderers
├── metadata-README.md.template makes "Save to Metadata" appear
└── extension-icon.placeholder.png 512x512 — its sha256 is its own tripwire
Three ideas the skill argues for¶
Model mutations as a data union, not argv strings. A Mutation type with one
variant per operation is what makes the transport a swappable seam. If mutate()
took a string[] of argv, a socket or HTTP transport would have to parse argv back
into intent — which is not a seam, it is a shell.
There is no test runner, and adding one is store-review noise. Keep every pure
module free of @raycast/api imports, and assert them from one dev-check.ts
compiled by the already-installed tsc and run under node. That import
discipline is the whole precondition — a barrel that pulls in the transport pulls
in @raycast/api with it, and then nothing is assertable.
One error descriptor, N renderers. A structural flag decides whether cached
data is a moment stale or a snapshot of a system nobody can reach; actions stay
data rather than JSX so the menu bar — which cannot render a List.EmptyView —
maps them to its own primitives instead of growing a second copy that drifts.
Verification¶
The skill was built against the real toolchain, not from documentation:
# scaffold, then run every stage of the gate on the result
bash scripts/new-raycast-extension.sh --dir /tmp/trial --name trial --author me \
--command tasks:view --command queue-menu:menu-bar --command quick:no-view
cd /tmp/trial && npm install
npx ray build && npx tsc --noEmit && npx ray lint # all three exit 0
And the claim the skill rests on, reproduced:
npx ray lint; echo $? # 0 — with metadata/ EMPTY
bash scripts/check-store-readiness.sh .; echo $? # 4 — names the screenshots
Adding three correctly-sized PNGs shrinks the failure list by exactly one id and
still reports icon-not-placeholder, which is how you know the checker
discriminates rather than merely counting files.
The canonical source is
SKILL.md.