mirror of
https://github.com/Heretek-AI/awesome-opencode.git
synced 2026-11-07 02:35:58 -10:00
feat(schema): add kind, topics, compat, npm and path; reject unknown fields and non-root repo URLs
Entries gain a fixed kind and 1-3 topics (replacing 374 one-off tags as the categorization axis), an OpenCode API compat field (required for plugins), and npm/path for packages and monorepo folders. repo must be a repository root; unknown fields such as install: or repository: are rejected. Cross-entry rules (one entry per repo+path, kind must fit the category, no placeholder owners or mojibake) live in scripts/utils/catalog.js, and validate.js now always loads the whole catalog and fails on unparseable YAML. Adds npm run fmt for canonical formatting and drops the unused glob dependency. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
3e58e7098c
commit
56fad94c34
15 files changed
+527
-624
No files matched your search
@@ -15,7 +15,7 @@ When adding a new entry to `data/{category}/`, use the corresponding example as
|
||||
| `data/agents/` | [agent.yaml](agent.yaml) |
|
||||
| `data/projects/` | [project.yaml](project.yaml) |
|
||||
| `data/resources/` | [resource.yaml](resource.yaml) |
|
||||
| `data/forks/` | [fork.yaml](fork.yaml) |
|
||||
| `data/projects/` (`kind: fork`) | [fork.yaml](fork.yaml) |
|
||||
|
||||
## Required vs Optional Fields
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ description: A custom agent configuration for security-focused code review.
|
||||
- Predefined tools for vulnerability scanning
|
||||
- Skills for OWASP Top 10 detection
|
||||
|
||||
kind: agent # plugin, theme, agent, skill, mcp-server, client, tool, config, guide, fork
|
||||
topics: # 1-3 from the fixed list in data/schema.json; the first is used for README grouping
|
||||
- orchestration
|
||||
|
||||
# ============================================================================
|
||||
# OPTIONAL FIELDS
|
||||
# ============================================================================
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Example Fork Entry
|
||||
# Forks are modified builds of OpenCode with custom features or patches.
|
||||
# Place actual entries in: data/forks/your-fork-name.yaml
|
||||
# Place actual entries in: data/projects/your-fork-name.yaml
|
||||
|
||||
# ============================================================================
|
||||
# REQUIRED FIELDS
|
||||
@@ -17,6 +17,10 @@ description: A fork of OpenCode designed for enterprise environments.
|
||||
- Custom model routing
|
||||
- Air-gapped deployment support
|
||||
|
||||
kind: fork # plugin, theme, agent, skill, mcp-server, client, tool, config, guide, fork
|
||||
topics: # 1-3 from the fixed list in data/schema.json; the first is used for README grouping
|
||||
- environment
|
||||
|
||||
# ============================================================================
|
||||
# OPTIONAL FIELDS
|
||||
# ============================================================================
|
||||
|
||||
@@ -15,6 +15,11 @@ description: A longer description explaining the plugin in detail.
|
||||
- Use cases
|
||||
- Key benefits
|
||||
|
||||
kind: plugin # plugin, theme, agent, skill, mcp-server, client, tool, config, guide, fork
|
||||
topics: # 1-3 from the fixed list in data/schema.json; the first is used for README grouping
|
||||
- workflow
|
||||
compat: v2 # plugins only: v2, v1 or unknown (CI verifies it)
|
||||
|
||||
# ============================================================================
|
||||
# OPTIONAL FIELDS
|
||||
# ============================================================================
|
||||
|
||||
@@ -18,6 +18,10 @@ description: A standalone web application for monitoring and managing
|
||||
- Token usage analytics
|
||||
- Multi-project overview
|
||||
|
||||
kind: client # plugin, theme, agent, skill, mcp-server, client, tool, config, guide, fork
|
||||
topics: # 1-3 from the fixed list in data/schema.json; the first is used for README grouping
|
||||
- sessions
|
||||
|
||||
# ============================================================================
|
||||
# OPTIONAL FIELDS
|
||||
# ============================================================================
|
||||
|
||||
@@ -17,6 +17,10 @@ description: A Model Context Protocol (MCP) server that provides
|
||||
- Read-only query execution
|
||||
- Schema introspection
|
||||
|
||||
kind: guide # plugin, theme, agent, skill, mcp-server, client, tool, config, guide, fork
|
||||
topics: # 1-3 from the fixed list in data/schema.json; the first is used for README grouping
|
||||
- docs
|
||||
|
||||
# ============================================================================
|
||||
# OPTIONAL FIELDS
|
||||
# ============================================================================
|
||||
|
||||
@@ -13,6 +13,10 @@ description: A dark theme designed for extended coding sessions.
|
||||
Features high contrast syntax highlighting and
|
||||
carefully chosen colors to reduce eye strain.
|
||||
|
||||
kind: theme # plugin, theme, agent, skill, mcp-server, client, tool, config, guide, fork
|
||||
topics: # 1-3 from the fixed list in data/schema.json; the first is used for README grouping
|
||||
- ui
|
||||
|
||||
# ============================================================================
|
||||
# OPTIONAL FIELDS
|
||||
# ============================================================================
|
||||
|
||||
+117
-12
@@ -2,18 +2,35 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"$id": "https://github.com/Heretek-AI/awesome-opencode/data/schema.json",
|
||||
"title": "Awesome Opencode Entry",
|
||||
"description": "Schema for validating YAML entries in the Heretek-AI/awesome-opencode list",
|
||||
"description": "Schema for validating YAML entries in the Heretek-AI/awesome-opencode list. Cross-entry rules (unique repos, kind per category, compat for plugins) live in scripts/utils/catalog.js.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 80,
|
||||
"description": "Display name of the project or tool"
|
||||
},
|
||||
"repo": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "URL to the repository (https://github.com/...)"
|
||||
"pattern": "^https://((github\\.com|gitlab\\.com|codeberg\\.org|gitee\\.com)/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+|github\\.com/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/discussions/[0-9]+|gist\\.github\\.com/[A-Za-z0-9_-]+/[0-9a-f]+)/?$",
|
||||
"description": "Repository root URL (no /tree/ or /blob/ paths, no npm or marketing pages). Gists and GitHub discussions (for guides) are also accepted. Use `path` for a package inside a monorepo."
|
||||
},
|
||||
"path": {
|
||||
"type": "string",
|
||||
"pattern": "^(?!/)(?!.*\\.\\.)(?!.*/$)[A-Za-z0-9_.@/-]+$",
|
||||
"description": "Subdirectory of `repo` that holds this entry, for monorepos (e.g. packages/my-plugin)"
|
||||
},
|
||||
"npm": {
|
||||
"type": "string",
|
||||
"pattern": "^(@[a-z0-9-~][a-z0-9-._~]*/)?[a-z0-9-~][a-z0-9-._~]*$",
|
||||
"description": "npm package name, for entries installed from npm. Used for the V2 compatibility check and health checks."
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Documentation URL if different from repository"
|
||||
},
|
||||
"tagline": {
|
||||
"type": "string",
|
||||
@@ -24,13 +41,99 @@
|
||||
"description": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
"maxLength": 1500,
|
||||
"description": "Full description of the project (shown when expanded)"
|
||||
},
|
||||
"kind": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"plugin",
|
||||
"theme",
|
||||
"agent",
|
||||
"skill",
|
||||
"mcp-server",
|
||||
"client",
|
||||
"tool",
|
||||
"config",
|
||||
"guide",
|
||||
"fork"
|
||||
],
|
||||
"description": "What the entry is. plugin: OpenCode plugin package. theme: color theme. agent: agent/subagent configuration. skill: SKILL.md pack. mcp-server: Model Context Protocol server. client: GUI, TUI, web, mobile, editor or chat front-end for OpenCode. tool: CLI, proxy, library or other utility. config: starter config, commands or templates. guide: docs, tutorials or articles. fork: a modified build of OpenCode."
|
||||
},
|
||||
"also": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"plugin",
|
||||
"theme",
|
||||
"agent",
|
||||
"skill",
|
||||
"mcp-server",
|
||||
"client",
|
||||
"tool",
|
||||
"config",
|
||||
"guide",
|
||||
"fork"
|
||||
]
|
||||
},
|
||||
"uniqueItems": true,
|
||||
"maxItems": 3,
|
||||
"description": "Secondary kinds, e.g. a plugin that also ships an MCP server. Cross-listed in the README and site."
|
||||
},
|
||||
"topics": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"memory",
|
||||
"context",
|
||||
"orchestration",
|
||||
"workflow",
|
||||
"providers",
|
||||
"cost",
|
||||
"notifications",
|
||||
"sessions",
|
||||
"code-quality",
|
||||
"security",
|
||||
"git",
|
||||
"remote",
|
||||
"web",
|
||||
"ui",
|
||||
"voice",
|
||||
"media",
|
||||
"docs",
|
||||
"environment",
|
||||
"observability",
|
||||
"integrations",
|
||||
"code-intel",
|
||||
"prompting",
|
||||
"domain"
|
||||
]
|
||||
},
|
||||
"minItems": 1,
|
||||
"maxItems": 3,
|
||||
"uniqueItems": true,
|
||||
"description": "1-3 use-case topics from a fixed list. The first one is the primary topic used for README grouping."
|
||||
},
|
||||
"compat": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"v2",
|
||||
"v1",
|
||||
"n/a",
|
||||
"unknown"
|
||||
],
|
||||
"description": "OpenCode API generation. v2: depends on @opencode/* (e.g. @opencode/plugin with Plugin.define). v1: legacy @opencode-ai/* only. n/a: does not use the plugin/SDK API. unknown: not yet verified. Required for plugins; CI verifies it."
|
||||
},
|
||||
"scope": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": ["global", "project"]
|
||||
"enum": [
|
||||
"global",
|
||||
"project"
|
||||
]
|
||||
},
|
||||
"minItems": 1,
|
||||
"uniqueItems": true,
|
||||
@@ -41,23 +144,25 @@
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"description": "Tags for filtering and discoverability"
|
||||
"description": "Free-form search keywords. Not rendered; use `topics` for categorization."
|
||||
},
|
||||
"min_version": {
|
||||
"type": "string",
|
||||
"pattern": "^[0-9]+\\.[0-9]+\\.[0-9]+",
|
||||
"description": "Minimum OpenCode version required (semver format, e.g. '1.0.0')"
|
||||
},
|
||||
"homepage": {
|
||||
"type": "string",
|
||||
"format": "uri",
|
||||
"description": "Documentation URL if different from repository"
|
||||
"description": "Minimum OpenCode version required (semver format, e.g. '2.0.0')"
|
||||
},
|
||||
"installation": {
|
||||
"type": "string",
|
||||
"description": "Detailed installation instructions in markdown format"
|
||||
}
|
||||
},
|
||||
"required": ["name", "repo", "tagline", "description"],
|
||||
"additionalProperties": true
|
||||
"required": [
|
||||
"name",
|
||||
"repo",
|
||||
"tagline",
|
||||
"description",
|
||||
"kind",
|
||||
"topics"
|
||||
],
|
||||
"additionalProperties": false
|
||||
}
|
||||
Generated
-475
@@ -11,27 +11,9 @@
|
||||
"dependencies": {
|
||||
"ajv": "^8.20.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"glob": "^11.0.0",
|
||||
"js-yaml": "^4.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
"integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^5.1.2",
|
||||
"string-width-cjs": "npm:string-width@^4.2.0",
|
||||
"strip-ansi": "^7.0.1",
|
||||
"strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
|
||||
"wrap-ansi": "^8.1.0",
|
||||
"wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/ajv": {
|
||||
"version": "8.20.0",
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz",
|
||||
@@ -65,101 +47,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-regex": {
|
||||
"version": "6.2.2",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
|
||||
"integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-regex?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/ansi-styles": {
|
||||
"version": "6.2.3",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
|
||||
"integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/argparse": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
|
||||
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
|
||||
"license": "Python-2.0"
|
||||
},
|
||||
"node_modules/balanced-match": {
|
||||
"version": "4.0.4",
|
||||
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
|
||||
"integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "5.0.12",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz",
|
||||
"integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"balanced-match": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/color-convert": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/color-name": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cross-spawn": {
|
||||
"version": "7.0.6",
|
||||
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
|
||||
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"path-key": "^3.1.0",
|
||||
"shebang-command": "^2.0.0",
|
||||
"which": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/eastasianwidth": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
|
||||
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/emoji-regex": {
|
||||
"version": "9.2.2",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
|
||||
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
@@ -182,75 +75,6 @@
|
||||
],
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/foreground-child": {
|
||||
"version": "3.3.1",
|
||||
"resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
|
||||
"integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"cross-spawn": "^7.0.6",
|
||||
"signal-exit": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "11.1.0",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
|
||||
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"foreground-child": "^3.3.1",
|
||||
"jackspeak": "^4.1.1",
|
||||
"minimatch": "^10.1.1",
|
||||
"minipass": "^7.1.2",
|
||||
"package-json-from-dist": "^1.0.0",
|
||||
"path-scurry": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"glob": "dist/esm/bin.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/is-fullwidth-code-point": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/isexe": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
|
||||
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/jackspeak": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.1.1.tgz",
|
||||
"integrity": "sha512-zptv57P3GpL+O0I7VdMJNBZCu+BPHVQUk55Ft8/QCJjTVxrnJHuVuX/0Bl2A6/+2oyR/ZMEuFKwmzqqZ/U5nPQ==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"@isaacs/cliui": "^8.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.3.2",
|
||||
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz",
|
||||
@@ -279,70 +103,6 @@
|
||||
"integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
"version": "11.2.4",
|
||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.4.tgz",
|
||||
"integrity": "sha512-B5Y16Jr9LB9dHVkh6ZevG+vAbOsNOYCX+sXvFWFu7B3Iz5mijW3zdbMyhsh8ANd2mSWBYdJgnqi+mL7/LrOPYg==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "10.2.6",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
|
||||
"integrity": "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"brace-expansion": "^5.0.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": "18 || 20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/minipass": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz",
|
||||
"integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/package-json-from-dist": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
|
||||
"integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
|
||||
"license": "BlueOak-1.0.0"
|
||||
},
|
||||
"node_modules/path-key": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
|
||||
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/path-scurry": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.1.tgz",
|
||||
"integrity": "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA==",
|
||||
"license": "BlueOak-1.0.0",
|
||||
"dependencies": {
|
||||
"lru-cache": "^11.0.0",
|
||||
"minipass": "^7.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": "20 || >=22"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/require-from-string": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
|
||||
@@ -351,241 +111,6 @@
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-command": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"shebang-regex": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/shebang-regex": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
|
||||
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/signal-exit": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
|
||||
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
|
||||
"integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"eastasianwidth": "^0.2.0",
|
||||
"emoji-regex": "^9.2.2",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs": {
|
||||
"name": "string-width",
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/string-width-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi": {
|
||||
"version": "7.1.2",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
|
||||
"integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/strip-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs": {
|
||||
"name": "strip-ansi",
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/which": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"isexe": "^2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"node-which": "bin/node-which"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi": {
|
||||
"version": "8.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
|
||||
"integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^6.1.0",
|
||||
"string-width": "^5.0.1",
|
||||
"strip-ansi": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs": {
|
||||
"name": "wrap-ansi",
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
"string-width": "^4.1.0",
|
||||
"strip-ansi": "^6.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/wrap-ansi?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/ansi-styles": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/string-width": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
"is-fullwidth-code-point": "^3.0.0",
|
||||
"strip-ansi": "^6.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-5
@@ -1,19 +1,35 @@
|
||||
{
|
||||
"name": "awesome-opencode",
|
||||
"version": "1.0.0",
|
||||
"description": "A curated list of plugins, themes, agents, and resources for Opencode",
|
||||
"description": "A curated, validated catalog of OpenCode plugins, themes, agents, MCP servers, clients and resources",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"generate": "node scripts/generate-readme.js",
|
||||
"validate": "node scripts/validate.js",
|
||||
"bootstrap": "node scripts/bootstrap.js"
|
||||
"generate": "node scripts/generate-readme.js",
|
||||
"export": "node scripts/export-json.js --pretty --output dist/registry.json",
|
||||
"new": "node scripts/new-entry.js",
|
||||
"fmt": "node scripts/fmt.js",
|
||||
"check-pr": "node scripts/check-pr.js --base origin/main",
|
||||
"health": "node scripts/health-check.js --dry-run",
|
||||
"scout": "node scripts/scout.js run --dry-run",
|
||||
"site": "node scripts/build-site.js",
|
||||
"test": "node --test test/"
|
||||
},
|
||||
"keywords": ["opencode", "awesome-list", "plugins", "themes", "agents"],
|
||||
"keywords": [
|
||||
"opencode",
|
||||
"awesome-list",
|
||||
"plugins",
|
||||
"themes",
|
||||
"agents",
|
||||
"mcp"
|
||||
],
|
||||
"license": "CC0-1.0",
|
||||
"dependencies": {
|
||||
"ajv": "^8.20.0",
|
||||
"ajv-formats": "^3.0.1",
|
||||
"glob": "^11.0.0",
|
||||
"js-yaml": "^4.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Rewrite catalog entries in canonical format (key order, safe quoting).
|
||||
* Usage: node scripts/fmt.js [--check] [file.yaml ...]
|
||||
*
|
||||
* --check lists files that would change and exits 1 without writing.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { loadCatalog, formatProblems } = require('./utils/catalog');
|
||||
const { dumpEntry } = require('./utils/format');
|
||||
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
function main() {
|
||||
const argv = process.argv.slice(2);
|
||||
const check = argv.includes('--check');
|
||||
const only = argv.filter(a => a.endsWith('.yaml')).map(f => path.relative(ROOT, path.resolve(f)));
|
||||
|
||||
const { entries, errors } = loadCatalog();
|
||||
if (errors.length) {
|
||||
console.error(formatProblems(errors));
|
||||
console.error('\nFix the YAML syntax errors above first.');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const changed = [];
|
||||
for (const entry of entries) {
|
||||
if (only.length && !only.includes(entry.file)) continue;
|
||||
const target = path.join(ROOT, entry.file);
|
||||
const formatted = dumpEntry(entry.data);
|
||||
if (fs.readFileSync(target, 'utf8') !== formatted) {
|
||||
changed.push(entry.file);
|
||||
if (!check) fs.writeFileSync(target, formatted);
|
||||
}
|
||||
}
|
||||
|
||||
if (check && changed.length) {
|
||||
console.error(`${changed.length} file(s) are not in canonical format:\n${changed.map(f => ` ${f}`).join('\n')}\nRun: npm run fmt`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(check ? '✓ All entries are formatted.' : `✓ Formatted ${changed.length} file(s).`);
|
||||
}
|
||||
|
||||
main();
|
||||
@@ -0,0 +1,250 @@
|
||||
/**
|
||||
* Catalog loading and cross-entry rules for awesome-opencode.
|
||||
*
|
||||
* Every consumer (validate, generate-readme, export-json, CI checks) loads entries through
|
||||
* loadCatalog() so a single unparseable file is reported as an error instead of silently
|
||||
* dropping a whole category.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const { validateEntry } = require('./validation');
|
||||
const schema = require('../../data/schema.json');
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '../../data');
|
||||
|
||||
// Category processing order (README section order)
|
||||
const CATEGORIES = ['plugins', 'themes', 'agents', 'projects', 'resources'];
|
||||
|
||||
// Plugins still on the V1 API (compat: v1) are removed from the catalog after this date
|
||||
const V1_SUNSET = '2027-01-01';
|
||||
|
||||
const KINDS = schema.properties.kind.enum;
|
||||
const TOPICS = schema.properties.topics.items.enum;
|
||||
|
||||
// Which kinds may live in which category directory
|
||||
const CATEGORY_KINDS = {
|
||||
plugins: ['plugin'],
|
||||
themes: ['theme'],
|
||||
agents: ['agent', 'skill'],
|
||||
projects: ['client', 'tool', 'mcp-server', 'skill', 'agent', 'fork'],
|
||||
resources: ['guide', 'config', 'mcp-server', 'skill', 'tool']
|
||||
};
|
||||
|
||||
const TOPIC_LABELS = {
|
||||
memory: 'Memory',
|
||||
context: 'Context & Tokens',
|
||||
orchestration: 'Multi-Agent & Orchestration',
|
||||
workflow: 'Workflows & Planning',
|
||||
providers: 'Providers, Auth & Routing',
|
||||
cost: 'Cost & Usage',
|
||||
notifications: 'Notifications',
|
||||
sessions: 'Sessions & History',
|
||||
'code-quality': 'Code Review & Quality',
|
||||
security: 'Security & Sandboxing',
|
||||
git: 'Git & GitHub',
|
||||
remote: 'Remote & Mobile',
|
||||
web: 'Web & Browser',
|
||||
ui: 'UI & TUI',
|
||||
voice: 'Voice',
|
||||
media: 'Media & Documents',
|
||||
docs: 'Docs & Research',
|
||||
environment: 'Environment & Setup',
|
||||
observability: 'Observability',
|
||||
integrations: 'Integrations',
|
||||
'code-intel': 'Code Intelligence',
|
||||
prompting: 'Prompting & Skills',
|
||||
domain: 'Domain-Specific'
|
||||
};
|
||||
|
||||
const KIND_LABELS = {
|
||||
plugin: 'Plugins',
|
||||
theme: 'Themes',
|
||||
agent: 'Agents',
|
||||
skill: 'Skills',
|
||||
'mcp-server': 'MCP Servers',
|
||||
client: 'Clients & GUIs',
|
||||
tool: 'Tools & Utilities',
|
||||
config: 'Configs & Starters',
|
||||
guide: 'Guides',
|
||||
fork: 'Forks'
|
||||
};
|
||||
|
||||
const FILENAME_RE = /^[a-z0-9]+(?:[-.][a-z0-9]+)*\.yaml$/;
|
||||
|
||||
// Contributor-facing wording for schema pattern failures
|
||||
const PATTERN_HINTS = {
|
||||
'/repo': 'repo must be the repository root, e.g. https://github.com/owner/repo (no /tree/ or /blob/ paths, npm pages or websites; use `path` for a folder inside a monorepo)',
|
||||
'/path': 'path must be a relative folder inside the repo, e.g. packages/my-plugin',
|
||||
'/npm': 'npm must be a package name, e.g. opencode-my-plugin or @scope/opencode-my-plugin',
|
||||
'/min_version': 'min_version must be semver, e.g. "2.0.0"'
|
||||
};
|
||||
const MOJIBAKE_RE = /鈥|â€|Ã[\u0080-¿]/;
|
||||
const PLACEHOLDER_OWNER_RE =/^(your[-_]?(user(name)?|org|name)|user(name)?|owner|example|org|me)$/i;
|
||||
|
||||
/**
|
||||
* Parse a repo URL into { host, owner, name } (gists return owner/id).
|
||||
* @param {string} url
|
||||
* @returns {{host: string, owner: string, name: string}|null}
|
||||
*/
|
||||
function parseRepoUrl(url) {
|
||||
const match = String(url || '').match(/^https?:\/\/([^/]+)\/([^/]+)\/([^/#?]+)/);
|
||||
if (!match) return null;
|
||||
return { host: match[1].toLowerCase(), owner: match[2], name: match[3].replace(/\.git$/, '') };
|
||||
}
|
||||
|
||||
/**
|
||||
* Canonical identity of an entry's target: lowercased host/owner/repo plus optional path.
|
||||
* @param {object} data - Entry data
|
||||
* @returns {string}
|
||||
*/
|
||||
function canonicalKey(data) {
|
||||
const parsed = parseRepoUrl(data.repo);
|
||||
const base = parsed
|
||||
? `${parsed.host}/${parsed.owner}/${parsed.name}`.toLowerCase()
|
||||
: String(data.repo || '').toLowerCase().replace(/\/+$/, '');
|
||||
const sub = (data.path || '').replace(/^\/+|\/+$/g, '').toLowerCase();
|
||||
return sub ? `${base}#${sub}` : base;
|
||||
}
|
||||
|
||||
/**
|
||||
* Load every entry in data/<category>/*.yaml.
|
||||
* @param {object} [options]
|
||||
* @param {string} [options.dataDir] - Override the data directory
|
||||
* @returns {{entries: object[], errors: {file: string, message: string}[]}}
|
||||
*/
|
||||
function loadCatalog({ dataDir = DATA_DIR } = {}) {
|
||||
const entries = [];
|
||||
const errors = [];
|
||||
|
||||
for (const category of CATEGORIES) {
|
||||
const dir = path.join(dataDir, category);
|
||||
if (!fs.existsSync(dir)) continue;
|
||||
|
||||
for (const fileName of fs.readdirSync(dir).filter(f => f.endsWith('.yaml')).sort()) {
|
||||
const file = path.join('data', category, fileName);
|
||||
try {
|
||||
const data = yaml.load(fs.readFileSync(path.join(dir, fileName), 'utf8'));
|
||||
if (!data || typeof data !== 'object' || Array.isArray(data)) {
|
||||
errors.push({ file, message: 'file is empty or not a YAML mapping' });
|
||||
continue;
|
||||
}
|
||||
entries.push({ category, id: fileName.replace(/\.yaml$/, ''), file, data });
|
||||
} catch (err) {
|
||||
errors.push({ file, message: `invalid YAML: ${err.reason || err.message}` });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { entries, errors };
|
||||
}
|
||||
|
||||
/**
|
||||
* Rules for a single entry: schema, kind/category fit, filename, placeholder repos.
|
||||
* @param {object} entry - { category, id, file, data }
|
||||
* @returns {string[]} Error messages
|
||||
*/
|
||||
function checkEntry(entry) {
|
||||
const messages = [];
|
||||
const result = validateEntry(entry.data, entry.file);
|
||||
if (!result.valid) {
|
||||
for (const err of result.errors) {
|
||||
if (err.keyword === 'pattern' && PATTERN_HINTS[err.path]) {
|
||||
messages.push(PATTERN_HINTS[err.path]);
|
||||
continue;
|
||||
}
|
||||
const detail = err.keyword === 'additionalProperties'
|
||||
? `unknown field "${err.params.additionalProperty}"`
|
||||
: err.keyword === 'enum'
|
||||
? `${err.message}: ${err.params.allowedValues.join(', ')}`
|
||||
: err.message;
|
||||
messages.push(`${err.path === '/' ? '' : err.path + ': '}${detail}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!FILENAME_RE.test(path.basename(entry.file))) {
|
||||
messages.push('filename must be kebab-case, e.g. my-plugin.yaml');
|
||||
}
|
||||
|
||||
const allowed = CATEGORY_KINDS[entry.category];
|
||||
if (entry.data.kind && allowed && !allowed.includes(entry.data.kind)) {
|
||||
messages.push(`kind "${entry.data.kind}" does not belong in data/${entry.category}/ (allowed: ${allowed.join(', ')})`);
|
||||
}
|
||||
|
||||
if (entry.category === 'plugins' && !entry.data.compat) {
|
||||
messages.push('plugins must declare compat (v2, v1 or unknown); CI verifies it');
|
||||
}
|
||||
|
||||
for (const field of ['name', 'tagline', 'description']) {
|
||||
if (MOJIBAKE_RE.test(entry.data[field] || '')) {
|
||||
messages.push(`${field} contains mis-encoded characters (e.g. "鈥?" or "—" instead of "—"); re-save the file as UTF-8`);
|
||||
}
|
||||
}
|
||||
|
||||
const parsed = parseRepoUrl(entry.data.repo);
|
||||
if (parsed && PLACEHOLDER_OWNER_RE.test(parsed.owner)) {
|
||||
messages.push(`repo owner "${parsed.owner}" looks like a placeholder`);
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the whole catalog: per-entry rules plus cross-entry uniqueness.
|
||||
* @param {object[]} entries
|
||||
* @returns {{file: string, message: string}[]}
|
||||
*/
|
||||
function checkCatalog(entries) {
|
||||
const problems = [];
|
||||
const byKey = new Map();
|
||||
|
||||
for (const entry of entries) {
|
||||
for (const message of checkEntry(entry)) {
|
||||
problems.push({ file: entry.file, message });
|
||||
}
|
||||
if (!entry.data.repo) continue;
|
||||
const key = canonicalKey(entry.data);
|
||||
if (byKey.has(key)) {
|
||||
problems.push({
|
||||
file: entry.file,
|
||||
message: `duplicate of ${byKey.get(key)} (same repo${entry.data.path ? ' and path' : ''}); use \`path\` for separate packages in one monorepo`
|
||||
});
|
||||
} else {
|
||||
byKey.set(key, entry.file);
|
||||
}
|
||||
}
|
||||
|
||||
return problems;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format problems for terminal output.
|
||||
* @param {{file: string, message: string}[]} problems
|
||||
* @returns {string}
|
||||
*/
|
||||
function formatProblems(problems) {
|
||||
const byFile = new Map();
|
||||
for (const { file, message } of problems) {
|
||||
if (!byFile.has(file)) byFile.set(file, []);
|
||||
byFile.get(file).push(message);
|
||||
}
|
||||
return [...byFile].map(([file, messages]) => `✗ ${file}\n${messages.map(m => ` - ${m}`).join('\n')}`).join('\n');
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
CATEGORIES,
|
||||
CATEGORY_KINDS,
|
||||
DATA_DIR,
|
||||
KINDS,
|
||||
KIND_LABELS,
|
||||
TOPICS,
|
||||
TOPIC_LABELS,
|
||||
V1_SUNSET,
|
||||
canonicalKey,
|
||||
checkCatalog,
|
||||
checkEntry,
|
||||
formatProblems,
|
||||
loadCatalog,
|
||||
parseRepoUrl
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* Canonical YAML formatting for catalog entries.
|
||||
*
|
||||
* All bot-written and scaffolded entries go through dumpEntry() so quoting is always valid YAML
|
||||
* (unquoted ": " in a tagline broke README generation for every entry before this existed).
|
||||
*/
|
||||
|
||||
const yaml = require('js-yaml');
|
||||
|
||||
const KEY_ORDER = [
|
||||
'name', 'repo', 'path', 'npm', 'homepage', 'tagline', 'description',
|
||||
'kind', 'also', 'topics', 'compat', 'scope', 'tags', 'min_version', 'installation'
|
||||
];
|
||||
|
||||
/**
|
||||
* Order keys canonically; unknown keys are kept at the end so validation can report them.
|
||||
* @param {object} data
|
||||
* @returns {object}
|
||||
*/
|
||||
function orderEntry(data) {
|
||||
const ordered = {};
|
||||
for (const key of KEY_ORDER) {
|
||||
if (data[key] !== undefined && data[key] !== null && !(Array.isArray(data[key]) && data[key].length === 0)) {
|
||||
ordered[key] = data[key];
|
||||
}
|
||||
}
|
||||
for (const key of Object.keys(data)) {
|
||||
if (!(key in ordered) && !KEY_ORDER.includes(key) && !key.startsWith('_')) ordered[key] = data[key];
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize an entry to YAML text.
|
||||
* @param {object} data
|
||||
* @returns {string}
|
||||
*/
|
||||
function dumpEntry(data) {
|
||||
const clean = orderEntry(data);
|
||||
for (const key of ['name', 'tagline', 'description']) {
|
||||
if (typeof clean[key] === 'string') clean[key] = clean[key].trim();
|
||||
}
|
||||
return yaml.dump(clean, { lineWidth: -1, noRefs: true, quotingType: '"' });
|
||||
}
|
||||
|
||||
module.exports = { KEY_ORDER, dumpEntry, orderEntry };
|
||||
+3
-37
@@ -1,42 +1,8 @@
|
||||
/**
|
||||
* YAML file reading utilities for awesome-opencode
|
||||
* Filename helpers for awesome-opencode entries.
|
||||
* (Catalog loading lives in ./catalog.js.)
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const yaml = require('js-yaml');
|
||||
const { glob } = require('glob');
|
||||
|
||||
/**
|
||||
* Read and parse a single YAML file
|
||||
* @param {string} filePath - Path to YAML file
|
||||
* @returns {object} Parsed YAML object
|
||||
*/
|
||||
function readYamlFile(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return yaml.load(content, { filename: filePath });
|
||||
} catch (e) {
|
||||
throw new Error(`Failed to parse ${filePath}: ${e.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Read all YAML files from a directory
|
||||
* @param {string} dirPath - Directory containing YAML files
|
||||
* @returns {Promise<object[]>} Array of parsed YAML objects with _filePath metadata
|
||||
*/
|
||||
async function readYamlDir(dirPath) {
|
||||
const pattern = path.join(dirPath, '*.yaml').replace(/\\/g, '/');
|
||||
const files = await glob(pattern);
|
||||
|
||||
return files.map(file => ({
|
||||
...readYamlFile(file),
|
||||
_filePath: file,
|
||||
_fileName: path.basename(file, '.yaml')
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a name to a filename-safe slug
|
||||
* @param {string} name - Entry name
|
||||
@@ -49,4 +15,4 @@ function slugify(name) {
|
||||
.replace(/^-+|-+$/g, '');
|
||||
}
|
||||
|
||||
module.exports = { readYamlFile, readYamlDir, slugify };
|
||||
module.exports = { slugify };
|
||||
+17
-93
@@ -1,109 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Validate YAML files against the schema
|
||||
* Validate catalog entries.
|
||||
* Usage: node scripts/validate.js [file1.yaml] [file2.yaml] ...
|
||||
* If no files specified, validates all YAML files in data/
|
||||
*
|
||||
* Always loads the whole catalog so cross-entry rules (duplicate repos) are enforced; when files are
|
||||
* given, only problems in those files are reported. Exits 1 on any problem, including unparseable YAML.
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const glob = require('glob');
|
||||
const yaml = require('js-yaml');
|
||||
const { validateEntry, formatValidationErrors } = require('./utils/validation');
|
||||
const { checkCatalog, formatProblems, loadCatalog } = require('./utils/catalog');
|
||||
|
||||
const DATA_DIR = path.join(__dirname, '../data');
|
||||
const ROOT = path.join(__dirname, '..');
|
||||
|
||||
/**
|
||||
* Get all YAML files in the data directory
|
||||
* @returns {string[]} Array of file paths
|
||||
*/
|
||||
function getAllYamlFiles() {
|
||||
const pattern = path.join(DATA_DIR, '**/*.yaml');
|
||||
return glob.sync(pattern);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and parse a YAML file
|
||||
* @param {string} filePath - Path to the YAML file
|
||||
* @returns {object|null} Parsed YAML object or null on error
|
||||
*/
|
||||
function loadYamlFile(filePath) {
|
||||
try {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
return yaml.load(content);
|
||||
} catch (err) {
|
||||
console.error(`Error reading ${filePath}: ${err.message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate a single file
|
||||
* @param {string} filePath - Path to the YAML file
|
||||
* @returns {boolean} True if valid, false otherwise
|
||||
*/
|
||||
function validateFile(filePath) {
|
||||
const data = loadYamlFile(filePath);
|
||||
if (!data) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const result = validateEntry(data, filePath);
|
||||
if (result.valid) {
|
||||
console.log(`✓ ${filePath}`);
|
||||
} else {
|
||||
console.error(formatValidationErrors(result));
|
||||
}
|
||||
|
||||
return result.valid;
|
||||
}
|
||||
|
||||
/**
|
||||
* Main validation function
|
||||
*/
|
||||
function main() {
|
||||
const args = process.argv.slice(2);
|
||||
let files = [];
|
||||
const only = process.argv.slice(2)
|
||||
.filter(f => f.endsWith('.yaml'))
|
||||
.map(f => path.relative(ROOT, path.resolve(f)));
|
||||
|
||||
if (args.length > 0) {
|
||||
// Validate specified files
|
||||
files = args;
|
||||
} else {
|
||||
// Validate all YAML files in data/
|
||||
files = getAllYamlFiles();
|
||||
}
|
||||
const { entries, errors } = loadCatalog();
|
||||
const problems = [...errors, ...checkCatalog(entries)]
|
||||
.filter(p => only.length === 0 || only.includes(p.file));
|
||||
|
||||
if (files.length === 0) {
|
||||
console.log('No YAML files to validate.');
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
console.log(`Validating ${files.length} YAML file(s)...\n`);
|
||||
|
||||
let allValid = true;
|
||||
let validatedCount = 0;
|
||||
|
||||
for (const file of files) {
|
||||
if (fs.existsSync(file)) {
|
||||
if (!validateFile(file)) {
|
||||
allValid = false;
|
||||
}
|
||||
validatedCount++;
|
||||
} else {
|
||||
console.warn(`⚠ File not found: ${file}`);
|
||||
allValid = false;
|
||||
}
|
||||
}
|
||||
|
||||
console.log('');
|
||||
|
||||
if (allValid) {
|
||||
console.log(`✓ All ${validatedCount} file(s) passed validation.`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.error(`✗ Validation failed for ${validatedCount} file(s).`);
|
||||
const checked = only.length || entries.length + errors.length;
|
||||
if (problems.length > 0) {
|
||||
console.error(formatProblems(problems));
|
||||
console.error(`\n✗ ${new Set(problems.map(p => p.file)).size} of ${checked} file(s) failed validation.`);
|
||||
process.exit(1);
|
||||
}
|
||||
console.log(`✓ All ${checked} file(s) passed validation.`);
|
||||
}
|
||||
|
||||
main();
|
||||
Reference in new issue
Block a user