diff --git a/.github/workflows/track-releases.yml b/.github/workflows/track-releases.yml
index 6d3609a..25acbc6 100644
--- a/.github/workflows/track-releases.yml
+++ b/.github/workflows/track-releases.yml
@@ -49,6 +49,7 @@ jobs:
args=""
if [ -n "$PLUGIN" ]; then args="--plugin $PLUGIN"; fi
node scripts/track-releases.mjs lithe-plugin-index.json $args
+ node scripts/render-readme.mjs
- name: Verify the new archives
if: steps.track.outputs.added != '0'
@@ -62,7 +63,7 @@ jobs:
git config user.name "lithe-store-bot"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git switch -C "$BRANCH"
- git add lithe-plugin-index.json
+ git add lithe-plugin-index.json README.md
git commit -m "chore(index): track new plugin releases" -m "$SUMMARY"
git push --force origin "$BRANCH"
body=$(printf '自动追踪到新的插件版本:\n\n%s\n\n已校验 SHA-256 与包内 plugin.json(id、version、pluginAPIVersion、signatureRequirement)。维护者请按 docs/maintainers.md 的清单审查后合并。' "$(echo "$SUMMARY" | tr ',' '\n' | sed 's/^ */- /')")
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index 23fabdd..fc132ca 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -37,6 +37,9 @@ jobs:
- name: Validate index
run: node scripts/validate-index.mjs lithe-plugin-index.json
+ - name: Check the README plugin overview
+ run: node scripts/render-readme.mjs --check
+
- name: Pick the base index
id: base
run: |
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 55bc17b..e6d92a2 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -4,7 +4,7 @@
| --- | --- |
| 收录自己的插件 | 读 [docs/publishing.md](docs/publishing.md),提 PR 或开“插件收录申请” issue |
| 发布插件新版本 | 在自己仓库发布 Release 即可,bot 会自动开 PR(见 [发布新版本](docs/publishing.md#4-发布新版本)) |
-| 修改插件信息 | 提 PR 修改 `lithe-plugin-index.json` 中自己的条目 |
+| 修改插件信息 | 提 PR 修改 `lithe-plugin-index.json` 中自己的条目,并运行 `node scripts/render-readme.mjs` 更新 README |
| 报告有问题的插件 | 开“报告插件问题” issue;安全问题请勾选“安全问题”,维护者会优先处理 |
| 改进脚本、CI 或文档 | 直接提 PR,并运行 `node --test scripts/store.test.mjs` |
@@ -13,6 +13,7 @@
- 一个 PR 只改一个插件,便于审查和回退。
- `plugins` 按 `id` 字母序排列;每个插件的 `versions` 新版本在前。
- 不修改已发布版本的 `downloadURL` 与 `sha256`;不删除历史版本(撤回问题版本除外)。
+- 修改索引后运行 `node scripts/render-readme.mjs`,README 的插件总览由它生成,CI 会检查两者是否一致。
- 插件的 bug 与功能请求请提到插件自己的仓库,本仓库只处理收录相关事务。
第一次开发语言插件,先读[LSP 插件完整生命周期](docs/lsp-plugin-lifecycle.md),
@@ -20,3 +21,57 @@
最后按[发布文档](docs/publishing.md)提交收录。
审查标准见 [docs/maintainers.md](docs/maintainers.md#审查清单)。
+
+## 索引如何工作
+
+这个仓库只存放一份索引文件 [`lithe-plugin-index.json`](lithe-plugin-index.json),列出社区插件、
+它们的版本和每个平台的下载包;插件代码和安装包都留在各自作者的 GitHub 仓库里,通过 GitHub Releases 分发。
+
+Lithe 会把仓库地址 `https://github.com/Lithe-IDEA/Lithe-store` 解析为
+`https://raw.githubusercontent.com/Lithe-IDEA/Lithe-store/HEAD/lithe-plugin-index.json`,
+索引缓存在本机缓存目录,不会写入应用安装包。
+
+## 社区插件与官方插件
+
+| | 官方插件 | 社区插件(本仓库) |
+| --- | --- | --- |
+| 索引位置 | Lithe 主仓库 `Plugins/marketplace/index.json`,随应用附带 | 本仓库,Lithe 预置为社区插件源(用户可停用或移除) |
+| ID 命名空间 | `dev.lithe.*` | 不得使用 `dev.lithe.*`,Lithe 会直接拒绝 |
+| 完整性 | Lithe 发布者 Ed25519 签名 | 索引固定每个包的 SHA-256,安装后记录内容摘要并在每次加载时复查 |
+| 安装前 | 直接安装 | 弹窗列出权限、要运行的程序和来源仓库,用户确认后安装 |
+
+社区插件**由各自的开发者维护**。本仓库的审查只保证:索引格式正确、下载包与 SHA-256 一致、
+包内 `plugin.json` 与索引条目一致、来源仓库真实存在且公开。它不代表对插件代码的安全审计。
+
+## 文档
+
+| 文档 | 内容 |
+| --- | --- |
+| [LSP 插件完整生命周期](docs/lsp-plugin-lifecycle.md) | 从 Lua 模板开始:清单、服务器打包、激活、状态目录、停止、升级与卸载 |
+| [构建、运行与测试接入](docs/build-tool-integration.md) | 可用的执行提供者示例、工程后端边界,以及开发中的 API 3 状态 |
+| [docs/publishing.md](docs/publishing.md) | 插件作者:如何打包、发布 Release、提交收录和发布新版本 |
+| [docs/versioning-and-updates.md](docs/versioning-and-updates.md) | 版本号如何追踪、Lithe 如何选择可安装版本、用户如何更新/回退 |
+| [docs/maintainers.md](docs/maintainers.md) | 维护者:审查清单、下架与撤回、同步 Lithe 契约 |
+
+## 仓库结构
+
+```text
+lithe-plugin-index.json # 唯一的数据文件,Lithe 客户端读取它
+schema/plugin-index.schema.json # 索引 JSON Schema(Lithe 契约 v1 的副本)
+scripts/validate-index.mjs # 与 Lithe Rust Core 同规则的校验 + 下载包校验
+scripts/track-releases.mjs # 追踪已收录插件的新 Release 并生成版本条目
+scripts/render-readme.mjs # 根据索引生成 README 的插件总览
+scripts/store.test.mjs # 校验脚本的测试(fixtures 复制自 Lithe 主仓库)
+assets/ # README 用到的图片(无图标插件的默认图标)
+.github/workflows/ # validate(PR/推送/每周)与 track-releases(每 6 小时)
+```
+
+## 本地校验
+
+需要 Node.js 22+ 与 `unzip`:
+
+```bash
+node --test scripts/store.test.mjs
+node scripts/validate-index.mjs --verify-archives
+node scripts/render-readme.mjs --check
+```
diff --git a/README.md b/README.md
index 04d6514..391107b 100644
--- a/README.md
+++ b/README.md
@@ -1,69 +1,93 @@
-# Lithe Store
+
-Lithe 的**社区插件市场索引**。这个仓库只存放一份索引文件
-[`lithe-plugin-index.json`](lithe-plugin-index.json),列出社区插件、它们的版本和每个平台的下载包;
-插件代码和安装包都留在各自作者的 GitHub 仓库里,通过 GitHub Releases 分发。
+
-> English: This repository is the community plugin index for [Lithe](https://github.com/Lithe-IDEA/Lithe-IDEA).
-> Plugin authors publish archives in their own GitHub Releases; this index pins each archive's SHA-256.
-> See [docs/publishing.md](docs/publishing.md) to list a plugin.
-
-## 在 Lithe 中使用
-
-需要 Lithe 0.6.1 或更新版本(目前插件市场只在 macOS 提供)。Lithe 已预置本仓库作为社区插件源,
-打开 **设置 → 插件 → 插件市场** 即可浏览、安装社区插件,不需要额外配置。
-
-如果你之前移除了 Lithe Store,可以重新添加:
+
Lithe Store
-1. 打开 **设置 → 插件 → ⚙ → 管理插件仓库…**
-2. 添加 `https://github.com/Lithe-IDEA/Lithe-store`
-3. 回到 **插件市场** 标签
+
Lithe 的社区插件市场
-Lithe 会把仓库地址解析为
-`https://raw.githubusercontent.com/Lithe-IDEA/Lithe-store/HEAD/lithe-plugin-index.json`,
-索引缓存在本机缓存目录,不会写入应用安装包。
+
+ 🧩 浏览插件 ·
+ ⬇️ 安装方式 ·
+ 📤 发布插件 ·
+ 🤝 参与贡献
+
-## 社区插件与官方插件
+
-| | 官方插件 | 社区插件(本仓库) |
-| --- | --- | --- |
-| 索引位置 | Lithe 主仓库 `Plugins/marketplace/index.json`,随应用附带 | 本仓库,Lithe 预置为社区插件源(用户可停用或移除) |
-| ID 命名空间 | `dev.lithe.*` | 不得使用 `dev.lithe.*`,Lithe 会直接拒绝 |
-| 完整性 | Lithe 发布者 Ed25519 签名 | 索引固定每个包的 SHA-256,安装后记录内容摘要并在每次加载时复查 |
-| 安装前 | 直接安装 | 弹窗列出权限、要运行的程序和来源仓库,用户确认后安装 |
-
-社区插件**由各自的开发者维护**。本仓库的审查只保证:索引格式正确、下载包与 SHA-256 一致、
-包内 `plugin.json` 与索引条目一致、来源仓库真实存在且公开。它不代表对插件代码的安全审计。
-
-## 文档
+> English: Community plugin index for [Lithe](https://github.com/Lithe-IDEA/Lithe-IDEA).
+> Plugins live in their authors' repositories; this index pins every archive's SHA-256.
+> See [docs/publishing.md](docs/publishing.md) to list a plugin.
-| 文档 | 内容 |
-| --- | --- |
-| [LSP 插件完整生命周期](docs/lsp-plugin-lifecycle.md) | 从 Lua 模板开始:清单、服务器打包、激活、状态目录、停止、升级与卸载 |
-| [构建、运行与测试接入](docs/build-tool-integration.md) | 可用的执行提供者示例、工程后端边界,以及开发中的 API 3 状态 |
-| [docs/publishing.md](docs/publishing.md) | 插件作者:如何打包、发布 Release、提交收录和发布新版本 |
-| [docs/versioning-and-updates.md](docs/versioning-and-updates.md) | 版本号如何追踪、Lithe 如何选择可安装版本、用户如何更新/回退 |
-| [docs/maintainers.md](docs/maintainers.md) | 维护者:审查清单、下架与撤回、同步 Lithe 契约 |
-| [CONTRIBUTING.md](CONTRIBUTING.md) | 参与方式总览 |
+## 🧩 插件总览
+
+社区插件**由各自的开发者维护**。收录审查只核对索引格式、下载包 SHA-256 和包内清单,**不代表对插件代码做过安全审计**,安装前请先看代码。
+插件本身的问题请提到插件自己的仓库。
+
+
+
+**全部 · 5** | 语言支持 · 3 | 构建工具 · 2
+
+
+
+
+
+Lithe Community · v0.1.0 · API 1
+使用你自己安装的 gopls 提供 Go 补全、跳转定义和错误提示,并支持运行 main 包和 go test(整个模块、单个文件所在的包或单个测试函数)。需要先安装 Go 和 gopls(go install gola…
+🏷️ 语言支持 · 💻 macOS ·
+language go
+
+
+
+Lithe Community · v0.1.1 · API 3(需 buildSystems)
+预发布 Gradle 集成:项目与任务树、JavaExec 运行入口、筛选测试、编译诊断及依赖树。需要支持 API 3 buildSystems 和原生构建窗口的 Lithe preview 开发版、Gradle 7.6…
+🏷️ 构建工具 · 💻 macOS ·
+build gradle java testing
+
+
+
+
+
+Lithe Community · v0.3.0 · API 1
+内置 lua-language-server(LuaLS)提供 Lua 补全、跳转定义和错误提示,无需另外安装;“运行当前文件”使用你自己的 lua(brew install lua)。
+🏷️ 语言支持 · 💻 macOS ·
+language lua
+
+
+
+Lithe Community · v0.1.0 · API 3(需 buildSystems)
+预发布 Maven 集成:反应堆与目标树、Profiles、带 JUnit 报告的筛选测试、编译诊断及依赖树。附带只读 Maven 核心扩展;需要支持 API 3 buildSystems 和原生构建窗口的 Lithe…
+🏷️ 构建工具 · 💻 macOS ·
+build maven java testing
+
+
+
+
+
+Lithe Community · v0.1.0 · API 1
+使用你自己安装的 Intelephense 提供 PHP 补全、跳转定义和错误提示,并支持运行 PHP 文件和 PHPUnit 测试。插件不内置 Intelephense(其许可证不允许再分发),请用 npm insta…
+🏷️ 语言支持 · 💻 macOS ·
+language php
+
+
+
+
+
+
+
+## ⬇️ 在 Lithe 中安装
-## 仓库结构
+需要 Lithe 0.6.1 或更新版本(目前插件市场只在 macOS 提供)。Lithe 已预置本仓库作为社区插件源,
+打开 **设置 → 插件 → 插件市场** 即可浏览和安装;安装前会弹窗列出插件权限、要运行的程序和来源仓库。
-```text
-lithe-plugin-index.json # 唯一的数据文件,Lithe 客户端读取它
-schema/plugin-index.schema.json # 索引 JSON Schema(Lithe 契约 v1 的副本)
-scripts/validate-index.mjs # 与 Lithe Rust Core 同规则的校验 + 下载包校验
-scripts/track-releases.mjs # 追踪已收录插件的新 Release 并生成版本条目
-scripts/store.test.mjs # 校验脚本的测试(fixtures 复制自 Lithe 主仓库)
-.github/workflows/ # validate(PR/推送/每周)与 track-releases(每 6 小时)
-```
+如果之前移除了 Lithe Store,在 **设置 → 插件 → ⚙ → 管理插件仓库…** 中重新添加
+`https://github.com/Lithe-IDEA/Lithe-store` 即可。
-本地校验(需要 Node.js 22+ 与 `unzip`):
+## 🤝 参与贡献
-```bash
-node --test scripts/store.test.mjs
-node scripts/validate-index.mjs --verify-archives
-```
+想收录自己的插件、发布新版本或报告有问题的插件,请看 [CONTRIBUTING.md](CONTRIBUTING.md)。
+上方的插件总览由 `scripts/render-readme.mjs` 根据 [`lithe-plugin-index.json`](lithe-plugin-index.json) 生成,修改索引后运行一次即可。
-## 许可
+## 📄 许可
-本仓库的脚本与文档使用 [Apache-2.0](LICENSE)。索引中每个插件的许可以该条目的 `license` 字段为准。
+本仓库的脚本与文档使用 [Apache-2.0](LICENSE)。每个插件的许可以索引条目的 `license` 字段为准。
diff --git a/assets/plugin-default.svg b/assets/plugin-default.svg
new file mode 100644
index 0000000..08eaae4
--- /dev/null
+++ b/assets/plugin-default.svg
@@ -0,0 +1,10 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/scripts/render-readme.mjs b/scripts/render-readme.mjs
new file mode 100644
index 0000000..c9f1823
--- /dev/null
+++ b/scripts/render-readme.mjs
@@ -0,0 +1,111 @@
+#!/usr/bin/env node
+// Renders the plugin overview in README.md from lithe-plugin-index.json, so the
+// cards always match what Lithe shows in its marketplace. Only the block between
+// the `plugins:start` / `plugins:end` markers is generated; edit the rest by hand.
+//
+// Usage: node scripts/render-readme.mjs [--check]
+// --check exit 1 when README.md is out of date instead of rewriting it.
+
+import { readFileSync, writeFileSync } from "node:fs";
+import { fileURLToPath } from "node:url";
+
+export const START = "";
+export const END = "";
+const DEFAULT_ICON = "assets/plugin-default.svg";
+const COLUMNS = 2;
+const DESCRIPTION_LIMIT = 110;
+
+// Tag → overview category. The first matching tag wins; anything else is “其他”.
+const CATEGORIES = [
+ ["language", "语言支持"],
+ ["build", "构建工具"],
+ ["theme", "主题"],
+ ["tool", "工具"],
+];
+const OS_LABELS = { darwin: "macOS", windows: "Windows", linux: "Linux" };
+
+const escapeHTML = (text) =>
+ String(text).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """);
+
+export function categoryOf(entry) {
+ const tags = entry.tags ?? [];
+ return CATEGORIES.find(([tag]) => tags.includes(tag))?.[1] ?? "其他";
+}
+
+/** Operating systems served by the newest version, in a stable order. */
+export function systemsOf(version) {
+ const systems = new Set(version.downloads.flatMap((download) => download.platforms.map((platform) => platform.split("-")[0])));
+ return Object.keys(OS_LABELS).filter((os) => systems.has(os)).map((os) => OS_LABELS[os]);
+}
+
+export function truncate(text, limit = DESCRIPTION_LIMIT) {
+ const chars = [...text];
+ return chars.length <= limit ? text : `${chars.slice(0, limit).join("").trimEnd()}…`;
+}
+
+function repositoryPath(url) {
+ return new URL(url).pathname.replace(/^\/|\/$/g, "");
+}
+
+export function renderCard(entry) {
+ const latest = entry.versions[0];
+ const description = entry.localizedDescriptions?.["zh-Hans"] ?? entry.description;
+ const requires = Object.keys(latest.requires ?? {});
+ const meta = [
+ escapeHTML(entry.author),
+ `v${escapeHTML(latest.version)}`,
+ `API ${latest.pluginAPIVersion}${requires.length ? `(需 ${requires.map(escapeHTML).join("、")})` : ""}`,
+ ].join(" · ");
+ const tags = (entry.tags ?? []).map((tag) => `${escapeHTML(tag)}`).join(" ");
+ const repo = repositoryPath(entry.repository);
+ const stars = ` `;
+ return [
+ ``,
+ ``,
+ `${meta}
`,
+ `${escapeHTML(truncate(description))}
`,
+ `🏷️ ${escapeHTML(categoryOf(entry))} · 💻 ${systemsOf(latest).join(" / ")} · ${stars}
`,
+ tags ? `${tags}
` : "",
+ ` `,
+ ].filter(Boolean).join("\n");
+}
+
+export function renderOverview(index) {
+ const plugins = [...index.plugins].sort((a, b) => a.name.localeCompare(b.name, "en"));
+ const counts = new Map();
+ for (const entry of plugins) counts.set(categoryOf(entry), (counts.get(categoryOf(entry)) ?? 0) + 1);
+ const summary = [`**全部 · ${plugins.length}**`, ...[...counts].map(([name, count]) => `${name} · ${count}`)].join(" | ");
+
+ const rows = [];
+ for (let i = 0; i < plugins.length; i += COLUMNS) {
+ const cells = plugins.slice(i, i + COLUMNS).map(renderCard);
+ while (cells.length < COLUMNS) cells.push(` `);
+ rows.push(`\n${cells.join("\n")}\n `);
+ }
+ return [START, "", summary, "", "", "", END].join("\n");
+}
+
+export function renderReadme(readme, index) {
+ const start = readme.indexOf(START);
+ const end = readme.indexOf(END);
+ if (start === -1 || end === -1 || end < start) {
+ throw new Error("README.md must contain the plugins:start and plugins:end markers in order");
+ }
+ return readme.slice(0, start) + renderOverview(index) + readme.slice(end + END.length);
+}
+
+if (process.argv[1] === fileURLToPath(import.meta.url)) {
+ const readmeURL = new URL("../README.md", import.meta.url);
+ const index = JSON.parse(readFileSync(new URL("../lithe-plugin-index.json", import.meta.url), "utf8"));
+ const current = readFileSync(readmeURL, "utf8");
+ const next = renderReadme(current, index);
+ if (process.argv.includes("--check")) {
+ if (next !== current) {
+ console.error("README.md plugin overview is out of date. Run: node scripts/render-readme.mjs");
+ process.exit(1);
+ }
+ } else if (next !== current) {
+ writeFileSync(readmeURL, next);
+ console.log("README.md plugin overview updated.");
+ }
+}
diff --git a/scripts/store.test.mjs b/scripts/store.test.mjs
index a424c5d..bda31f5 100644
--- a/scripts/store.test.mjs
+++ b/scripts/store.test.mjs
@@ -5,6 +5,7 @@ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "nod
import { tmpdir } from "node:os";
import { join } from "node:path";
import { test } from "node:test";
+import { END, START, categoryOf, renderOverview, renderReadme, systemsOf, truncate } from "./render-readme.mjs";
import { addVersion, platformsForAsset, versionFromRelease } from "./track-releases.mjs";
import { checkManifest, formatIndex, inspectArchive, validateEntry, validateIndex, verifyArchives } from "./validate-index.mjs";
@@ -325,3 +326,33 @@ test("archives must hold one matching community plugin.json", () => {
rmSync(workDir, { recursive: true, force: true });
}
});
+
+test("the README plugin overview matches the published index", () => {
+ const index = JSON.parse(readFileSync(new URL("../lithe-plugin-index.json", import.meta.url)));
+ const readme = readFileSync(new URL("../README.md", import.meta.url), "utf8");
+ assert.equal(renderReadme(readme, index), readme, "run: node scripts/render-readme.mjs");
+});
+
+test("the overview renders one escaped card per plugin with a default icon", () => {
+ const entry = validEntry();
+ delete entry.icon;
+ entry.name = "";
+ const html = renderOverview({ schemaVersion: 1, plugins: [entry] });
+ assert.match(html, /<Zig>/);
+ assert.match(html, /assets\/plugin-default\.svg/);
+ assert.equal(html.match(/ {
+ assert.equal(categoryOf({ tags: ["go", "language"] }), "语言支持");
+ assert.equal(categoryOf({ tags: ["misc"] }), "其他");
+ const version = { downloads: [{ platforms: ["windows-x64"] }, { platforms: ["darwin-arm64", "darwin-x64"] }] };
+ assert.deepEqual(systemsOf(version), ["macOS", "Windows"]);
+ assert.equal(truncate("abcdef", 3), "abc…");
+ assert.equal(truncate("abc", 3), "abc");
+});
+
+test("rendering refuses a README without the overview markers", () => {
+ assert.throws(() => renderReadme("# Lithe Store\n", { plugins: [] }), /markers/);
+});