Public manifest registry consumed by the TuyaOpen IDE at runtime. The IDE fetches
registry.jsonfrom this repo on startup, then lazy-loads each domain'sindex.jsononly when the matching page is opened. Boards / demos / platforms are pure metadata pointing at the canonical source repos;skills/is the one domain that also ships its payload in-repo (see below).
.
├── registry.json # top-level index — fetched first
├── release-metadata.json # release compatibility range (validated by CI)
├── boards-and-chips/
│ └── index.json # boards + chips (Tuya official + ecosystem)
├── demos/
│ └── index.json # demo / example projects
├── skills/
│ ├── index.json # AI agent skills registry (Cursor, Claude Code, …)
│ ├── core/ # ── the skill payloads themselves ──
│ ├── embedded/ # grouped by INSTALL GROUP: <group>/<id>/
│ ├── cloud/ # grouped by install group
│ └── miniapp/ # grouped by install group; see skills/README.md
└── miniapp-templates/ # shipped in the tarball, but NOT a domain
└── miniapp-template-covers.json
release-metadata.json is the versioned source of truth for the IDE
compatibility range. The release workflow validates it and generates the
checked-in release.json from that metadata, registry.json, and the
published package checksum/size. The contract is intentionally executable
without project dependencies:
python3 scripts/release_contract.py validate
python3 -m pytest tests/scripts/test_release_metadata.py -qThe standalone Release Contract GitHub Action runs these checks for pull
requests touching release inputs and for matching pushes to main.
| domain | what it lists | file |
|---|---|---|
boardsAndChips |
development boards and SoCs (official + ecosystem) | boards-and-chips/index.json |
demos |
example projects (point at git repos) | demos/index.json |
skills |
pluggable AI agent skills + their payload, for both product lines (sdks says which) |
skills/index.json + skills/<group>/<id>/ |
A skill's directory is its install group (core / embedded / cloud /
miniapp) — the unit tuyaopen-cli skills install --group <g>
offers — and not its surface. surface is a separate required field
driving the IDE's filter tabs; the two are independent, so
nothing may infer one from the other or from a path. Details:
skills/README.md.
miniapp-templates/miniapp-template-covers.json rides in manifests.tar.gz
from 1.0.0 on, and the IDE reads it by its manifest-relative path like any
board detailUrl — but it must never be added to registry.json#manifests.
It is schemaVersion: 2, has no domain field and names its array templates
instead of items, so the IDE's domain-envelope check rejects it; worse, a
registry entry makes the IDE's cache-integrity check demand a file that older
releases do not carry, which forces a full tarball re-download on every startup
for anyone pinned to such a release.
peripheral-templates/index.json is the mirror case: it stays in this repo as
the canonical source, but is not packaged — the IDE serves that catalogue
from its own bundled copy and syncs it in at build time.
The boards manifest mixes Tuya official boards and ecosystem boards into
one list. The IDE tells them apart from manufacturer first, falling
back to brand — it resolves that value in the active locale and treats
Tuya / TuyaOpen / Tuya Inc., or any name starting with 涂鸦, as
official. No separate manifest needed. manufacturer is therefore the
field every board must carry; brand is optional and only worth setting
when the brand owner genuinely differs from the fabricator (ODM / OEM).
Note that a brand alone does not move a board into the official
group — manufacturer wins whenever both are present.
- Decoupled from the IDE binary — manifests version independently; neither side blocks the other's release cycle.
- Flat + inlined — each domain has exactly one
index.jsonwith every entry's fields inlined. No "catalog page → detail page" split. A few hundred entries per domain is fine. - Point at sources, don't copy them — every product reference uses
the same shape:
{ "repo": "...", "subpath": "...", "ref": "main|tag|sha" }. Skills are the deliberate exception: their payload lives here (source.localPath) so a skill and the manifest describing it can never drift apart, and the IDE gets everything from onemanifests.tar.gz. - Versioned — top-level
schemaVersion(integer, structural compat) pluspublishedAt(ISO-8601, cache busting), and per-domainversion(semver, differential refresh) insideregistry.json.skillsitems additionally carry a per-itemversion(semver), because their payload is installed into user projects: it is what lets the IDE tell an upstream update apart from a user's local edit. Seeskills/README.md. - Localizable —
name/summary/ similar fields accept either a plain string or{ "en": "...", "zh-CN": "..." }. The IDE picks by active locale and falls back to English. - Forward compatible — unknown fields must be ignored by the IDE so we can grow the schema without coordinated releases.
-
Time — ISO-8601 UTC (
2026-05-12T00:00:00Z). -
IDs — kebab-case, unique per domain.
-
Localized strings — string or
{ "en": "...", "zh-CN": "..." }; consumers must accept both. -
Cross-domain references use IDs (e.g. a board's
recommendedDemos: ["switch-3-iot"]), never URLs — moving a file doesn't break the link. -
Boards:
manufactureris who actually fabricates the board and is required;brandis the brand owner and is optional, meant for the ODM / OEM case where the two genuinely differ. Do not fillbrandwith a copy ofmanufacturer: the IDE readsmanufacturer || brand, so a duplicate buys nothing, and the board card only renders a separate manufacturer row when the two values differ. Prefer the localized form{ "en": …, "zh-CN": … }over a single-language string — a bare"微雪"shows up as Chinese for English users, and the same vendor spelled two ways reads as two vendors. -
SDK applicability (
sdks) — optional array marking which SDK(s) an entry applies to, onboardsAndChips/demos/skillsitems. Values:"tuyaopen","tuyaos"; an entry may list one or both (["tuyaopen", "tuyaos"]is allowed when a skill applies to both lines). Omitted ⇒["tuyaopen"]— every pre-existing entry is TuyaOpen-only, so existing data needs no back-fill; only TuyaOS-capable entries have to set the field explicitly. Forward-compatible: an IDE predating the field ignores it (shows everything); an SDK-aware IDE filters the catalogue by the active SDK.platformsitems do not carry this field. Onskillsthis field is load-bearing rather than advisory since 2026-09-02: it is the only thing separating the two product lines, which used to be separated by their directory (skills/TuyaOpen/vsskills/TuyaOS/). Both lines' payloads now ship in the samemanifests.tar.gzunder oneskills/index.json, so a consumer that ignoressdkswill offer TuyaOS skills to TuyaOpen users. The current index is the authority for the item count and product-line distribution; do not hard-code those counts in this README.scripts/validate-skills-index.pyreads the same field to decide which of its rules — the ones asserting a relationship with thetuyaopen-cliCLI — apply to a given item. -
SDK branch requirements (
sdkRequirements) — optional array in a board or demo detail file, one{ "sdk": "tuyaopen" | "tuyaos", "branch": "…" }entry per SDK line. It declares an exceptional branch needed by that resource; it does not decide catalogue visibility (that remainssdks). If the detail has no entry for the active SDK, the default is that SDK'smasteror a Release version. Keep this field out ofindex.jsonso catalogue lists do not need to fetch every detail file. -
Platform pinout
functionsvscaps— in a platform detail file eachpinout[]entry splits its labels into two arrays:functions[]is a controlled, selection-only vocabulary of editor-selectable routing tokens (GPIO+UART{n}_TX/I2C{n}_SCL/SPI{n}_MOSI/QSPI…/PWM{n}/ADC{u}_CH{c}— exactly what the manifest-editor's pin-picker matches);caps[]is free datasheet text for display-only capabilities (RTC_GPIOn/LP_GPIOn,TOUCHn,DACn, flash/USB/strapping,JTAG,RGB/i8080/SEG,ENET_*, power rails, …). A pin's ownGPIO{n}identity lives inname/gpio, not infunctions. -
Platform peripheral port
routable— eachperipherals.<p>.spec.ports[](PWM onspec) carriesroutable(defaultfalse= fixed pinmux, pins locked). GPIO-matrix chips (ESP32) setroutable: trueon digital ports so theirpinGroupsbecome defaults and anyGPIO-capable pin is selectable; an optionalcandidatesconstrains the routable set (e.g. LP-domain ports). It accepts individual GPIOs and inclusive ranges, such ascandidates: [0, 2, [8, 28], [30, 40]]. ADC/analog ports stayroutable: false. -
publishedgates downstream — a platform item and a board item each carrypublished(defaulttruewhen absent). A board's effective publish state isboard.published !== falseAND its chip platform'spublished !== false: if the platform (the variant a board targets viavariantId) is unpublished, every board on it is effectively unpublished too — even boards flaggedpublished: true. Consumers treat effectively-unpublished boards as not-yet-released (the editor sorts them to the end of their tab, published first; the IDE should hide them from the board picker). Rationale: you can't ship a board whose SoC platform isn't released yet. -
demosspells the same flagpublish, notpublished— deliberately, and the IDE reads the two names on different domains:publishedonboardsAndChips/platforms,publishondemos(and on theminiapp-templatescovers catalogue). Both default to visible when absent, so spelling it the other way on a demo does not hide that demo — it silently publishes it. Do not "normalise" one into the other without changing the IDE in the same release. -
Board list grouping (multi-variant platforms) — a board carries two platform pointers and they are not interchangeable:
board.platformId= the platform group, which is what the boards list tabs on (t5ai,gd32,esp32,linux). A platform item's own group is itsplatformIdfield, so several items share one: all ofesp32,esp32c3,esp32c6,esp32s3,esp32p4c6carryplatformId: "esp32".board.variantId= the exact chip variant, i.e. some platform item'sid. This is what resolves the per-chip detail, and it must be set on every board — including single-chip platforms, where it repeats the group (t5ai→t5ai).
The consumer binds on
variantIdwhen present and only falls back to matchingplatformIdagainst a platform item'sidor its group when it is absent. So a group id that no platform item uses as itsid(gd32) is fine inplatformId—variantId: "gd32vw553"is what does the resolving.What the IDE actually reads today —
BoardManifestItem(src/manifests/manifestsTypes.ts) declares onlyplatformId; there is novariantIdon it, so every IDE-side board → platform join goes throughplatformId, two-step:items.find(p => p.id === pid) || items.find(p => p.platformId === pid)(src/extension.ts, andboardOnPlatforminmedia/webview/main.js).variantIdis still required by the rule above and is whattools/manifest-editorresolves on, but do not assume it steers the IDE.Known exception — the five T1 modules carry
platformId: "t1-chl", the variant id, not the group id"t1". The T1 platform item isid: "t1-chl"/platformId: "t1", split so that both theT1-CHLchip token TuyaOS reports and the family tokenT1resolve. The TuyaOS board list filters withb.platformId === plat.id(media/webview-tuyaos/main.js, three sites) — it has not picked up theboardOnPlatformfix thatmedia/webview/main.jsalready carries — so a board holding the group id"t1"never matches thet1-chlplatform item: it falls into the ungrouped bucket at the bottom of the list, and the T1-CHL tab renders "no boards". These modules aresdks: ["tuyaos"], so that view is the only surface they appear on. Putting the variant id inplatformIdis a deliberate deviation from the rule above, taken because the fix belongs to the IDE repo and not to this one. Revert it to"t1"oncewebview-tuyaosmatches on the group id too.Two things to know while it stands.
tools/manifest-editorrecomputesplatformIdfrom the chip dropdown'sdata-groupon save (board-editor.js), so editing one of these five boards there silently restores"t1"and re-breaks the grouping. And a project scaffolded from one of them getsplatform.target: "t1-chl".platformKconfigIdis unaffected: it is overwritten fromplatformSymbol: "T1"inplatforms/t1/t1.json.
IDE startup
→ fetch <CDN>/registry.json (with ETag — 304 hits the local cache)
→ diff cached publishedAt + per-domain version
→ lazy-fetch only the domains the user navigates to
→ cache to globalStorage; re-validate via ETag on next cold start
release-metadata.json is the version-controlled source for the IDE release
gate. CI validates its strict X.Y.Z minVersion and
maxVersionExclusive fields (minVersion is inclusive, the maximum is
exclusive) and embeds the identical ideSupport object in each generated
release.json. Older releases without this field remain compatible with
historical IDE versions.
-
Add / edit / remove an item — edit the entries in the matching
<domain>/index.jsondirectly and open a PR. -
Add / edit a skill — the payload and the index entry go in the same PR, and editing a payload means bumping that item's
version; seeskills/README.md. CI runsscripts/validate-skills-index.py(structure + versions + no orphan payloads),scripts/check-skill-version-bumps.py(changed payload ⇒ version bumped) andpytest tests. -
Bump the domain version — any change to a
<domain>/index.jsonor the items under it needsregistry.json'smanifests.<domain>.versionbumped (minor for added / removed items, patch for content-only edits). That number is what lights the "this page has an update" dot in the IDE; leaving it untouched means an already-synced IDE never tells the user anything changed. Refresh that domain'spublishedAtin the same PR.Enforced since 2026-08-17 by
scripts/check-domain-version-bumps.py(workflowvalidate-domains.yml) — run it before opening the PR:python3 scripts/check-domain-version-bumps.py # structure only python3 scripts/check-domain-version-bumps.py --base-ref main # + bump rule
It was unenforced until then, and the rule was simply not followed: six commits changed 439 files under
skills/whilemanifests.skills.versionstayed at the1.0.0that shipped withv1.0.0. The check also fails on a registry entry whoseurlis missing or whosedomainfield disagrees with its key — the IDE'scacheIntegrity()refuses to start on either, so those are startup failures rather than tidiness. -
Schema bump — bump the top-level
schemaVersionand include a short migration note in the PR description. -
Release — add a
CHANGELOG.mdentry, then tag the commit and publish a GitHub Release for the tag; CI validates the JSON, packsmanifests.tar.gz, generatesrelease.jsonfromregistry.json's domain versions, and mirrors both to Gitee. The tag is what the CDN / IDE pins to. Theimages.tuyacn.comcopy named inrelease.json#package.tuyacnis not uploaded by CI — publish it by hand. Skipping it does not break the release: the IDE cyclestuyacn → gitee → github(CN locale) across six attempts, so a 404 there costs one failed request and a short backoff before Gitee serves the tarball. It does mean every Mainland-China client pays that penalty on every cold sync, which is the whole reason the CDN entry exists.
This is meant to be edited like any normal git repo: PR, review, merge, tag. No special tooling needed beyond a JSON-aware editor.
schemas/*.json— JSON Schema per domain, enforced by CI.mirrors.json— Mainland China mirrors / Gitee fallback URLs.- Uploading the
images.tuyacn.comtarball from CI instead of by hand.
Apache License 2.0. See LICENSE.