diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 13bf7f8d..8c1b994c 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -5,11 +5,15 @@ on: paths: - "docs/**" - "DOCS_GUIDELINES.md" + - "packages/core/schemas/**" + - "scripts/sync-schemas.ts" push: branches: [main] paths: - "docs/**" - "DOCS_GUIDELINES.md" + - "packages/core/schemas/**" + - "scripts/sync-schemas.ts" concurrency: group: docs-${{ github.ref }} @@ -27,6 +31,9 @@ jobs: with: node-version: 22 + - name: Check schema mirror (core → docs) + run: npx tsx scripts/sync-schemas.ts --check + - name: Validate build working-directory: docs run: npx mint validate diff --git a/docs/schema/hyperframes.json b/docs/schema/hyperframes.json new file mode 100644 index 00000000..f75be026 --- /dev/null +++ b/docs/schema/hyperframes.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hyperframes.heygen.com/schema/hyperframes.json", + "title": "Hyperframes Project Config", + "description": "Per-project configuration for a Hyperframes project (hyperframes.json). Tells `hyperframes add` which registry to pull items from and where to drop them in the project tree. Created by `hyperframes init`; users may edit it to point at custom registries or reshape their project layout.", + "type": "object", + "required": ["registry", "paths"], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "format": "uri", + "description": "JSON Schema URL — https://hyperframes.heygen.com/schema/hyperframes.json." + }, + "registry": { + "type": "string", + "format": "uri", + "minLength": 1, + "description": "Base URL of the registry to pull items from. Point at the official Hyperframes registry or a custom one." + }, + "paths": { + "type": "object", + "description": "Target paths for each item type, relative to the project root.", + "required": ["blocks", "components", "assets"], + "additionalProperties": false, + "properties": { + "blocks": { + "type": "string", + "minLength": 1, + "description": "Where `hyperframes:block` items land. Defaults to `compositions`." + }, + "components": { + "type": "string", + "minLength": 1, + "description": "Where `hyperframes:component` items land. Defaults to `compositions/components`." + }, + "assets": { + "type": "string", + "minLength": 1, + "description": "Where asset files (images, fonts, videos) land. Defaults to `assets`." + } + } + } + } +} diff --git a/docs/schema/registry-item.json b/docs/schema/registry-item.json new file mode 100644 index 00000000..ba72da93 --- /dev/null +++ b/docs/schema/registry-item.json @@ -0,0 +1,141 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hyperframes.heygen.com/schema/registry-item.json", + "title": "Hyperframes Registry Item", + "description": "Manifest for a single distributable item (example, block, or component).", + "type": "object", + "required": ["name", "type", "title", "description", "files"], + "properties": { + "$schema": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", + "description": "Item name in kebab-case, must start and end with alphanumeric." + }, + "type": { + "type": "string", + "enum": ["hyperframes:example", "hyperframes:block", "hyperframes:component"] + }, + "title": { + "type": "string", + "minLength": 1 + }, + "description": { + "type": "string", + "minLength": 1 + }, + "tags": { + "type": "array", + "items": { "type": "string", "minLength": 1 } + }, + "author": { + "type": "string", + "minLength": 1 + }, + "license": { + "type": "string", + "minLength": 1, + "description": "SPDX license identifier (e.g. \"Apache-2.0\", \"MIT\")." + }, + "minCliVersion": { + "type": "string", + "pattern": "^\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "description": "Minimum `hyperframes` CLI version required to install this item." + }, + "deprecated": { + "type": "string", + "minLength": 1, + "description": "If set, the item is deprecated; the value is the reason or migration note." + }, + "dimensions": { + "type": "object", + "required": ["width", "height"], + "additionalProperties": false, + "properties": { + "width": { "type": "integer", "minimum": 1 }, + "height": { "type": "integer", "minimum": 1 } + } + }, + "duration": { + "type": "number", + "exclusiveMinimum": 0, + "description": "Duration in seconds. Must be > 0." + }, + "registryDependencies": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$" + } + }, + "files": { + "type": "array", + "minItems": 1, + "items": { + "type": "object", + "required": ["path", "target", "type"], + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1, + "description": "Source path, relative to registry-item.json." + }, + "target": { + "type": "string", + "minLength": 1, + "description": "Destination path in the user's project, relative to project root. Must not traverse outside the project (no `..` segments, no absolute paths).", + "not": { + "anyOf": [ + { "pattern": "(^|[/\\\\])\\.\\.([/\\\\]|$)" }, + { "pattern": "^[/\\\\]" }, + { "pattern": "^[A-Za-z]:[/\\\\]" } + ] + } + }, + "type": { + "type": "string", + "enum": [ + "hyperframes:composition", + "hyperframes:asset", + "hyperframes:snippet", + "hyperframes:style", + "hyperframes:timeline" + ] + } + } + } + }, + "preview": { + "type": "object", + "additionalProperties": false, + "properties": { + "video": { "type": "string" }, + "poster": { "type": "string" } + } + }, + "relatedSkill": { + "type": "string", + "minLength": 1 + } + }, + "allOf": [ + { + "if": { + "required": ["type"], + "properties": { "type": { "const": "hyperframes:component" } } + }, + "then": { + "not": { + "anyOf": [{ "required": ["dimensions"] }, { "required": ["duration"] }] + } + }, + "else": { + "required": ["dimensions", "duration"] + } + } + ] +} diff --git a/docs/schema/registry.json b/docs/schema/registry.json new file mode 100644 index 00000000..ced96b1f --- /dev/null +++ b/docs/schema/registry.json @@ -0,0 +1,45 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://hyperframes.heygen.com/schema/registry.json", + "title": "Hyperframes Registry Manifest", + "description": "Top-level manifest describing all items in a Hyperframes registry.", + "type": "object", + "required": ["name", "homepage", "items"], + "additionalProperties": false, + "properties": { + "$schema": { + "type": "string", + "format": "uri" + }, + "name": { + "type": "string", + "minLength": 1, + "description": "Registry name (e.g. \"hyperframes\")." + }, + "homepage": { + "type": "string", + "format": "uri", + "description": "Registry homepage URL." + }, + "items": { + "type": "array", + "description": "Items in this registry. Each entry is a shorthand reference; the full item manifest lives at //registry-item.json.", + "items": { + "type": "object", + "required": ["name", "type"], + "additionalProperties": false, + "properties": { + "name": { + "type": "string", + "pattern": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?$", + "description": "Item name in kebab-case, must start and end with alphanumeric." + }, + "type": { + "type": "string", + "enum": ["hyperframes:example", "hyperframes:block", "hyperframes:component"] + } + } + } + } + } +} diff --git a/package.json b/package.json index e56ce3e9..2497445e 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "build:hyperframes-runtime:modular": "bun run --filter @hyperframes/core build:hyperframes-runtime:modular", "verify:packed-manifests": "node scripts/verify-packed-manifests.mjs", "set-version": "tsx scripts/set-version.ts", + "sync-schemas": "tsx scripts/sync-schemas.ts", + "sync-schemas:check": "tsx scripts/sync-schemas.ts --check", "lint": "oxlint . && tsx scripts/lint-skills.ts", "lint:skills": "tsx scripts/lint-skills.ts", "lint:fix": "oxlint --fix .", diff --git a/scripts/sync-schemas.ts b/scripts/sync-schemas.ts new file mode 100644 index 00000000..1002d5d3 --- /dev/null +++ b/scripts/sync-schemas.ts @@ -0,0 +1,60 @@ +#!/usr/bin/env tsx +/** + * Mirror JSON Schemas from `packages/core/schemas/` into `docs/schema/` so + * Mintlify serves them at `https://hyperframes.heygen.com/schema/*`. The core + * copies stay authoritative — they're exported from `@hyperframes/core` for + * npm consumers — and this script is the single contract that prevents the + * docs mirror from drifting. + * + * Usage: + * bun run sync-schemas # copy core → docs + * bun run sync-schemas --check # exit non-zero if copies are stale (CI) + * + * `docs/schema/hyperframes.json` is authored directly in docs (no source in + * core) so it's skipped by this script. + */ + +import { readFileSync, writeFileSync } from "node:fs"; +import { join } from "node:path"; + +const ROOT = join(import.meta.dirname, ".."); +const SOURCE_DIR = join(ROOT, "packages/core/schemas"); +const TARGET_DIR = join(ROOT, "docs/schema"); +const MIRRORED = ["registry.json", "registry-item.json"]; + +function main() { + const checkOnly = process.argv.includes("--check"); + let drift = 0; + + for (const name of MIRRORED) { + const source = readFileSync(join(SOURCE_DIR, name), "utf-8"); + const targetPath = join(TARGET_DIR, name); + const target = (() => { + try { + return readFileSync(targetPath, "utf-8"); + } catch { + return null; + } + })(); + + if (target === source) { + console.log(` ✓ ${name} in sync`); + continue; + } + + drift++; + if (checkOnly) { + console.error(` ✗ ${name} out of sync (run \`bun run sync-schemas\` to fix)`); + continue; + } + writeFileSync(targetPath, source); + console.log(` → ${name} updated`); + } + + if (checkOnly && drift > 0) { + console.error(`\n${drift} schema${drift === 1 ? "" : "s"} drifted from source.`); + process.exit(1); + } +} + +main();