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 分发。 +Lithe Store -> 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 + + + + + + + + + + + + + + +
+

Go Support

+

Lithe Community · v0.1.0 · API 1

+

使用你自己安装的 gopls 提供 Go 补全、跳转定义和错误提示,并支持运行 main 包和 go test(整个模块、单个文件所在的包或单个测试函数)。需要先安装 Go 和 gopls(go install gola…

+

🏷️ 语言支持 · 💻 macOS · GitHub stars

+

language go

+
+

Gradle

+

Lithe Community · v0.1.1 · API 3(需 buildSystems)

+

预发布 Gradle 集成:项目与任务树、JavaExec 运行入口、筛选测试、编译诊断及依赖树。需要支持 API 3 buildSystems 和原生构建窗口的 Lithe preview 开发版、Gradle 7.6…

+

🏷️ 构建工具 · 💻 macOS · GitHub stars

+

build gradle java testing

+
+

Lua Support

+

Lithe Community · v0.3.0 · API 1

+

内置 lua-language-server(LuaLS)提供 Lua 补全、跳转定义和错误提示,无需另外安装;“运行当前文件”使用你自己的 lua(brew install lua)。

+

🏷️ 语言支持 · 💻 macOS · GitHub stars

+

language lua

+
+

Maven

+

Lithe Community · v0.1.0 · API 3(需 buildSystems)

+

预发布 Maven 集成:反应堆与目标树、Profiles、带 JUnit 报告的筛选测试、编译诊断及依赖树。附带只读 Maven 核心扩展;需要支持 API 3 buildSystems 和原生构建窗口的 Lithe…

+

🏷️ 构建工具 · 💻 macOS · GitHub stars

+

build maven java testing

+
+

PHP Support

+

Lithe Community · v0.1.0 · API 1

+

使用你自己安装的 Intelephense 提供 PHP 补全、跳转定义和错误提示,并支持运行 PHP 文件和 PHPUnit 测试。插件不内置 Intelephense(其许可证不允许再分发),请用 npm insta…

+

🏷️ 语言支持 · 💻 macOS · GitHub stars

+

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 = `GitHub stars`; + return [ + ``, + `

${escapeHTML(entry.name)}

`, + `

${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, "", "", ...rows, "
", "", 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/); +});