mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
629 lines
20 KiB
JavaScript
629 lines
20 KiB
JavaScript
#!/usr/bin/env node
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
|
|
const DOC_PATH = "docs/plugins/plugin-inventory.md";
|
|
const REFERENCE_INDEX_PATH = "docs/plugins/reference.md";
|
|
const REFERENCE_DIR = "docs/plugins/reference";
|
|
const ROOT = process.cwd();
|
|
const EXTENSIONS_DIR = path.join(ROOT, "extensions");
|
|
|
|
const PROVIDER_DOC_ALIASES = new Map([
|
|
["amazon-bedrock", "/providers/bedrock"],
|
|
["amazon-bedrock-mantle", "/providers/bedrock-mantle"],
|
|
["kimi", "/providers/moonshot"],
|
|
["perplexity", "/providers/perplexity-provider"],
|
|
]);
|
|
const PLUGIN_DOC_ALIASES = new Map([
|
|
["acpx", "/tools/acp-agents-setup"],
|
|
["brave", "/tools/brave-search"],
|
|
["browser", "/tools/browser"],
|
|
["codex", "/plugins/codex-harness"],
|
|
["document-extract", "/tools/pdf"],
|
|
["duckduckgo", "/tools/duckduckgo-search"],
|
|
["exa", "/tools/exa-search"],
|
|
["firecrawl", "/tools/firecrawl"],
|
|
["perplexity", "/tools/perplexity-search"],
|
|
["tavily", "/tools/tavily"],
|
|
["tokenjuice", "/tools/tokenjuice"],
|
|
]);
|
|
const PLUGIN_REFERENCE_EXTRA_SECTIONS = new Map([
|
|
[
|
|
"whatsapp",
|
|
`## Windows install note
|
|
|
|
On Windows, the WhatsApp plugin needs Git on \`PATH\` during npm install because one of its Baileys/libsignal dependencies is fetched from a git URL. Install Git for Windows, then restart the shell and rerun the install:
|
|
|
|
\`\`\`powershell
|
|
winget install --id Git.Git -e
|
|
\`\`\`
|
|
|
|
Portable Git also works if its \`bin\` directory is on \`PATH\`.`,
|
|
],
|
|
]);
|
|
|
|
function readJson(relativePath) {
|
|
return JSON.parse(fs.readFileSync(path.join(ROOT, relativePath), "utf8"));
|
|
}
|
|
|
|
function readJsonPath(filePath) {
|
|
return JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
}
|
|
|
|
function fileExists(relativePath) {
|
|
return fs.existsSync(path.join(ROOT, relativePath));
|
|
}
|
|
|
|
function collectExcludedPackagedExtensionDirs(rootPackageJson) {
|
|
const excluded = new Set();
|
|
for (const entry of rootPackageJson.files ?? []) {
|
|
if (typeof entry !== "string") {
|
|
continue;
|
|
}
|
|
const match = /^!dist\/extensions\/([^/]+)\/\*\*$/u.exec(entry);
|
|
if (match?.[1]) {
|
|
excluded.add(match[1]);
|
|
}
|
|
}
|
|
return excluded;
|
|
}
|
|
|
|
function normalizeDocPath(value) {
|
|
if (typeof value !== "string" || !value.startsWith("/")) {
|
|
return null;
|
|
}
|
|
return value.replace(/\.mdx?$/u, "");
|
|
}
|
|
|
|
function docLink({ label, href }) {
|
|
return `[${label}](${href})`;
|
|
}
|
|
|
|
function pluginReferencePath(id) {
|
|
return `/plugins/reference/${id}`;
|
|
}
|
|
|
|
function humanizeId(value) {
|
|
const names = new Map([
|
|
["acpx", "ACPx"],
|
|
["ai", "AI"],
|
|
["api", "API"],
|
|
["aws", "AWS"],
|
|
["azure", "Azure"],
|
|
["bluebubbles", "BlueBubbles"],
|
|
["byteplus", "BytePlus"],
|
|
["codex", "Codex"],
|
|
["cli", "CLI"],
|
|
["comfy", "ComfyUI"],
|
|
["dashscope", "DashScope"],
|
|
["deepgram", "Deepgram"],
|
|
["deepinfra", "DeepInfra"],
|
|
["deepseek", "DeepSeek"],
|
|
["duckduckgo", "DuckDuckGo"],
|
|
["exa", "Exa"],
|
|
["fal", "fal"],
|
|
["feishu", "Feishu"],
|
|
["github", "GitHub"],
|
|
["googlechat", "Google Chat"],
|
|
["gpt", "GPT"],
|
|
["groq", "Groq"],
|
|
["huggingface", "Hugging Face"],
|
|
["imessage", "iMessage"],
|
|
["irc", "IRC"],
|
|
["kimi", "Kimi"],
|
|
["line", "LINE"],
|
|
["litellm", "LiteLLM"],
|
|
["llm", "LLM"],
|
|
["lmstudio", "LM Studio"],
|
|
["mdns", "mDNS"],
|
|
["minimax", "MiniMax"],
|
|
["modelstudio", "Model Studio"],
|
|
["msteams", "Microsoft Teams"],
|
|
["nextcloud", "Nextcloud"],
|
|
["nvidia", "NVIDIA"],
|
|
["openai", "OpenAI"],
|
|
["opencode", "OpenCode"],
|
|
["openrouter", "OpenRouter"],
|
|
["otel", "OpenTelemetry"],
|
|
["qa", "QA"],
|
|
["qqbot", "QQ Bot"],
|
|
["qwen", "Qwen"],
|
|
["qwencloud", "Qwen Cloud"],
|
|
["searxng", "SearXNG"],
|
|
["sglang", "SGLang"],
|
|
["stepfun", "StepFun"],
|
|
["tokenhub", "TokenHub"],
|
|
["tts", "TTS"],
|
|
["twitch", "Twitch"],
|
|
["ui", "UI"],
|
|
["vllm", "vLLM"],
|
|
["whatsapp", "WhatsApp"],
|
|
["xai", "xAI"],
|
|
["zai", "Z.AI"],
|
|
["zalouser", "Zalo Personal"],
|
|
]);
|
|
return value
|
|
.split("-")
|
|
.map((part) => names.get(part) ?? part.charAt(0).toUpperCase() + part.slice(1))
|
|
.join(" ");
|
|
}
|
|
|
|
function displayList(values) {
|
|
return values
|
|
.filter((value) => typeof value === "string" && value.length > 0)
|
|
.map(humanizeId)
|
|
.join(", ");
|
|
}
|
|
|
|
function normalizePackageDescription(value) {
|
|
if (typeof value !== "string") {
|
|
return null;
|
|
}
|
|
return value.trim().replace(/\s+/gu, " ").replace(/\.$/u, "");
|
|
}
|
|
|
|
function resolveDescription({ manifest, packageJson }) {
|
|
const manifestDescription = normalizePackageDescription(manifest.description);
|
|
if (manifestDescription) {
|
|
return `${manifestDescription}.`;
|
|
}
|
|
|
|
const channels = Array.isArray(manifest.channels) ? manifest.channels : [];
|
|
if (channels.length > 0) {
|
|
const channelLabel = displayList(channels);
|
|
const channelNoun = channelLabel.toLowerCase().includes("channel") ? "" : " channel";
|
|
return `Adds the ${channelLabel}${channelNoun} surface for sending and receiving OpenClaw messages.`;
|
|
}
|
|
|
|
const providers = Array.isArray(manifest.providers) ? manifest.providers : [];
|
|
if (providers.length > 0) {
|
|
return `Adds ${displayList(providers)} model provider support to OpenClaw.`;
|
|
}
|
|
|
|
const contracts = Object.keys(manifest.contracts ?? {}).toSorted((left, right) =>
|
|
left.localeCompare(right),
|
|
);
|
|
const contractDescriptions = {
|
|
agentToolResultMiddleware: "Adds agent tool-result middleware.",
|
|
documentExtractors: "Adds document extraction for local attachments.",
|
|
imageGenerationProviders: "Adds image generation provider support.",
|
|
mediaUnderstandingProviders: "Adds media understanding provider support.",
|
|
memoryEmbeddingProviders: "Adds memory embedding provider support.",
|
|
migrationProviders: "Adds migration import support.",
|
|
musicGenerationProviders: "Adds music generation provider support.",
|
|
realtimeTranscriptionProviders: "Adds realtime transcription provider support.",
|
|
realtimeVoiceProviders: "Adds realtime voice provider support.",
|
|
speechProviders: "Adds text-to-speech provider support.",
|
|
tools: "Adds agent-callable tools.",
|
|
videoGenerationProviders: "Adds video generation provider support.",
|
|
webContentExtractors: "Adds readable web content extraction.",
|
|
webFetchProviders: "Adds web fetch provider support.",
|
|
webSearchProviders: "Adds web search provider support.",
|
|
};
|
|
const describedContracts = contracts
|
|
.map((contract) => contractDescriptions[contract])
|
|
.filter((value) => typeof value === "string");
|
|
if (describedContracts.length > 0) {
|
|
return describedContracts.join(" ");
|
|
}
|
|
|
|
const packageDescription = normalizePackageDescription(packageJson.description);
|
|
return packageDescription ? `${packageDescription}.` : "Provides an OpenClaw plugin.";
|
|
}
|
|
|
|
function pushUniqueDocLink(values, value) {
|
|
if (
|
|
value &&
|
|
!values.some((existing) => existing.label === value.label && existing.href === value.href)
|
|
) {
|
|
values.push(value);
|
|
}
|
|
}
|
|
|
|
function resolveDocs({ dirName, manifest, packageJson }) {
|
|
const links = [];
|
|
const pluginAlias = PLUGIN_DOC_ALIASES.get(manifest.id) ?? PLUGIN_DOC_ALIASES.get(dirName);
|
|
if (pluginAlias) {
|
|
pushUniqueDocLink(links, { href: pluginAlias, label: manifest.id ?? dirName });
|
|
}
|
|
|
|
const channelDoc = normalizeDocPath(packageJson.openclaw?.channel?.docsPath);
|
|
if (channelDoc) {
|
|
pushUniqueDocLink(links, {
|
|
href: channelDoc,
|
|
label: channelDoc.replace(/^\/channels\//u, ""),
|
|
});
|
|
}
|
|
|
|
for (const channel of manifest.channels ?? []) {
|
|
if (typeof channel !== "string") {
|
|
continue;
|
|
}
|
|
const relativePath = `docs/channels/${channel}.md`;
|
|
if (fileExists(relativePath)) {
|
|
pushUniqueDocLink(links, { href: `/channels/${channel}`, label: channel });
|
|
}
|
|
}
|
|
|
|
for (const provider of manifest.providers ?? []) {
|
|
if (typeof provider !== "string") {
|
|
continue;
|
|
}
|
|
const alias = PROVIDER_DOC_ALIASES.get(provider);
|
|
if (alias) {
|
|
pushUniqueDocLink(links, { href: alias, label: provider });
|
|
continue;
|
|
}
|
|
const relativePath = `docs/providers/${provider}.md`;
|
|
if (fileExists(relativePath)) {
|
|
pushUniqueDocLink(links, { href: `/providers/${provider}`, label: provider });
|
|
}
|
|
}
|
|
|
|
for (const candidate of [manifest.id, dirName]) {
|
|
if (typeof candidate !== "string") {
|
|
continue;
|
|
}
|
|
if (fileExists(`docs/channels/${candidate}.md`)) {
|
|
pushUniqueDocLink(links, { href: `/channels/${candidate}`, label: candidate });
|
|
}
|
|
if (fileExists(`docs/providers/${candidate}.md`)) {
|
|
pushUniqueDocLink(links, { href: `/providers/${candidate}`, label: candidate });
|
|
}
|
|
if (fileExists(`docs/plugins/${candidate}.md`)) {
|
|
pushUniqueDocLink(links, { href: `/plugins/${candidate}`, label: candidate });
|
|
}
|
|
}
|
|
|
|
return links;
|
|
}
|
|
|
|
function resolveSurface(manifest) {
|
|
const parts = [];
|
|
if (Array.isArray(manifest.channels) && manifest.channels.length > 0) {
|
|
parts.push(`channels: ${manifest.channels.join(", ")}`);
|
|
}
|
|
if (Array.isArray(manifest.providers) && manifest.providers.length > 0) {
|
|
parts.push(`providers: ${manifest.providers.join(", ")}`);
|
|
}
|
|
const contracts = Object.keys(manifest.contracts ?? {}).toSorted((left, right) =>
|
|
left.localeCompare(right),
|
|
);
|
|
if (contracts.length > 0) {
|
|
parts.push(`contracts: ${contracts.join(", ")}`);
|
|
}
|
|
if (Array.isArray(manifest.skills) && manifest.skills.length > 0) {
|
|
parts.push("skills");
|
|
}
|
|
if (parts.length === 0) {
|
|
return "plugin";
|
|
}
|
|
return parts.join("; ");
|
|
}
|
|
|
|
function resolveInstallRoute(packageJson, status) {
|
|
if (status === "source") {
|
|
return "source checkout only";
|
|
}
|
|
if (status === "core") {
|
|
return "included in OpenClaw";
|
|
}
|
|
const install = packageJson.openclaw?.install;
|
|
const release = packageJson.openclaw?.release;
|
|
const clawhubSpec =
|
|
typeof install?.clawhubSpec === "string" ? `: \`${install.clawhubSpec}\`` : "";
|
|
const npmSpec =
|
|
typeof install?.npmSpec === "string" && install.npmSpec !== packageJson.name
|
|
? `: \`${install.npmSpec}\``
|
|
: "";
|
|
if (release?.publishToClawHub === true && release?.publishToNpm === true) {
|
|
if (install?.defaultChoice === "clawhub") {
|
|
return clawhubSpec ? `ClawHub${clawhubSpec}; npm${npmSpec}` : `ClawHub + npm${npmSpec}`;
|
|
}
|
|
return clawhubSpec ? `npm${npmSpec}; ClawHub${clawhubSpec}` : `npm${npmSpec}; ClawHub`;
|
|
}
|
|
if (release?.publishToClawHub === true) {
|
|
return `ClawHub${clawhubSpec || npmSpec}`;
|
|
}
|
|
if (release?.publishToNpm === true || typeof install?.npmSpec === "string") {
|
|
return `npm${npmSpec}`;
|
|
}
|
|
return "installable plugin";
|
|
}
|
|
|
|
function resolveStatus({ dirName, packageJson, excludedDirs }) {
|
|
const release = packageJson.openclaw?.release;
|
|
const hasInstallSpec =
|
|
typeof packageJson.openclaw?.install?.clawhubSpec === "string" ||
|
|
typeof packageJson.openclaw?.install?.npmSpec === "string";
|
|
if (!excludedDirs.has(dirName)) {
|
|
return "core";
|
|
}
|
|
if (release?.publishToClawHub === true || release?.publishToNpm === true || hasInstallSpec) {
|
|
return "external";
|
|
}
|
|
return "source";
|
|
}
|
|
|
|
function escapeCell(value) {
|
|
return String(value).replaceAll("\n", " ").replaceAll("|", "\\|");
|
|
}
|
|
|
|
function renderTable(records) {
|
|
const rows = [
|
|
["Plugin", "Description", "Distribution", "Surface"],
|
|
...records.map((record) => [
|
|
docLink({ href: pluginReferencePath(record.id), label: escapeCell(record.id) }),
|
|
escapeCell(record.description),
|
|
`\`${escapeCell(record.packageName)}\`<br />${escapeCell(record.installRoute)}`,
|
|
escapeCell(record.surface),
|
|
]),
|
|
];
|
|
const widths = rows[0].map((_, index) => Math.max(...rows.map((row) => row[index].length), 3));
|
|
const lines = [];
|
|
lines.push(formatTableRow(rows[0], widths));
|
|
lines.push(
|
|
formatTableRow(
|
|
widths.map((width) => "-".repeat(width)),
|
|
widths,
|
|
),
|
|
);
|
|
for (const row of rows.slice(1)) {
|
|
lines.push(formatTableRow(row, widths));
|
|
}
|
|
return lines.join("\n");
|
|
}
|
|
|
|
function formatTableRow(row, widths) {
|
|
return `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`;
|
|
}
|
|
|
|
function renderRelatedDocs(record) {
|
|
if (record.docs.length === 0) {
|
|
return "";
|
|
}
|
|
return `## Related docs
|
|
|
|
${record.docs.map((link) => `- ${docLink(link)}`).join("\n")}`;
|
|
}
|
|
|
|
function renderReferencePage(record) {
|
|
const relatedDocs = renderRelatedDocs(record);
|
|
const extraSections = PLUGIN_REFERENCE_EXTRA_SECTIONS.get(record.id);
|
|
return `---
|
|
summary: "${record.description.replaceAll('"', '\\"')}"
|
|
read_when:
|
|
- You are installing, configuring, or auditing the ${record.id} plugin
|
|
title: "${record.name} plugin"
|
|
---
|
|
|
|
# ${record.name} plugin
|
|
|
|
${record.description}
|
|
|
|
## Distribution
|
|
|
|
- Package: \`${record.packageName}\`
|
|
- Install route: ${record.installRoute}
|
|
|
|
## Surface
|
|
|
|
${record.surface}${extraSections ? `\n\n${extraSections}` : ""}${relatedDocs ? `\n\n${relatedDocs}` : ""}
|
|
`;
|
|
}
|
|
|
|
function renderReferenceIndex(records) {
|
|
return `---
|
|
summary: "Generated index of OpenClaw plugin reference pages"
|
|
read_when:
|
|
- You need a reference page for a specific OpenClaw plugin
|
|
- You are auditing plugin docs coverage
|
|
title: "Plugin reference"
|
|
---
|
|
|
|
# Plugin reference
|
|
|
|
This page is generated from \`extensions/*/package.json\` and
|
|
\`openclaw.plugin.json\`. Regenerate it with:
|
|
|
|
\`\`\`bash
|
|
pnpm plugins:inventory:gen
|
|
\`\`\`
|
|
|
|
${renderTable(records)}
|
|
`;
|
|
}
|
|
|
|
function collectPluginSourceEntries() {
|
|
const entries = [];
|
|
for (const dirName of fs
|
|
.readdirSync(EXTENSIONS_DIR)
|
|
.toSorted((left, right) => left.localeCompare(right))) {
|
|
const packagePath = path.join(EXTENSIONS_DIR, dirName, "package.json");
|
|
const manifestPath = path.join(EXTENSIONS_DIR, dirName, "openclaw.plugin.json");
|
|
if (!fs.existsSync(packagePath) || !fs.existsSync(manifestPath)) {
|
|
continue;
|
|
}
|
|
const packageJson = readJsonPath(packagePath);
|
|
const manifest = readJsonPath(manifestPath);
|
|
const id = typeof manifest.id === "string" && manifest.id ? manifest.id : dirName;
|
|
entries.push({ dirName, id, manifest, packageJson });
|
|
}
|
|
return entries;
|
|
}
|
|
|
|
function validatePluginCoverage(records, sourceEntries) {
|
|
const expectedIds = sourceEntries
|
|
.map((entry) => entry.id)
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
const actualIds = records
|
|
.map((record) => record.id)
|
|
.toSorted((left, right) => left.localeCompare(right));
|
|
const missing = expectedIds.filter((id) => !actualIds.includes(id));
|
|
const extra = actualIds.filter((id) => !expectedIds.includes(id));
|
|
const duplicateIds = actualIds.filter((id, index) => actualIds.indexOf(id) !== index);
|
|
if (missing.length > 0 || extra.length > 0 || duplicateIds.length > 0) {
|
|
throw new Error(
|
|
[
|
|
"plugin inventory coverage mismatch",
|
|
missing.length > 0 ? `missing: ${missing.join(", ")}` : null,
|
|
extra.length > 0 ? `extra: ${extra.join(", ")}` : null,
|
|
duplicateIds.length > 0 ? `duplicates: ${duplicateIds.join(", ")}` : null,
|
|
]
|
|
.filter(Boolean)
|
|
.join("; "),
|
|
);
|
|
}
|
|
}
|
|
|
|
function collectPluginRecords() {
|
|
const rootPackageJson = readJson("package.json");
|
|
const excludedDirs = collectExcludedPackagedExtensionDirs(rootPackageJson);
|
|
const sourceEntries = collectPluginSourceEntries();
|
|
const records = [];
|
|
|
|
for (const { dirName, id, manifest, packageJson } of sourceEntries) {
|
|
const status = resolveStatus({ dirName, packageJson, excludedDirs });
|
|
records.push({
|
|
description: resolveDescription({ manifest, packageJson }),
|
|
docs: resolveDocs({ dirName, manifest, packageJson }),
|
|
id,
|
|
installRoute: resolveInstallRoute(packageJson, status),
|
|
name: humanizeId(id),
|
|
packageName: packageJson.name ?? "-",
|
|
status,
|
|
surface: resolveSurface(manifest),
|
|
});
|
|
}
|
|
|
|
validatePluginCoverage(records, sourceEntries);
|
|
return records.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
}
|
|
|
|
function writeGeneratedDocs(records) {
|
|
fs.mkdirSync(path.join(ROOT, REFERENCE_DIR), { recursive: true });
|
|
for (const record of records) {
|
|
fs.writeFileSync(
|
|
path.join(ROOT, REFERENCE_DIR, `${record.id}.md`),
|
|
renderReferencePage(record),
|
|
"utf8",
|
|
);
|
|
}
|
|
fs.writeFileSync(path.join(ROOT, REFERENCE_INDEX_PATH), renderReferenceIndex(records), "utf8");
|
|
}
|
|
|
|
function readGeneratedDocs(records) {
|
|
return [
|
|
[REFERENCE_INDEX_PATH, renderReferenceIndex(records)],
|
|
...records.map((record) => [
|
|
path.join(REFERENCE_DIR, `${record.id}.md`),
|
|
renderReferencePage(record),
|
|
]),
|
|
];
|
|
}
|
|
|
|
function renderDocument() {
|
|
const records = collectPluginRecords();
|
|
const groups = {
|
|
core: records.filter((record) => record.status === "core"),
|
|
external: records.filter((record) => record.status === "external"),
|
|
source: records.filter((record) => record.status === "source"),
|
|
};
|
|
|
|
return `---
|
|
summary: "Generated inventory of OpenClaw plugins shipped in core, published externally, or kept source-only"
|
|
read_when:
|
|
- You are deciding whether a plugin ships in the core npm package or installs separately
|
|
- You are updating bundled plugin package metadata or release automation
|
|
- You need the canonical internal vs external plugin list
|
|
title: "Plugin inventory"
|
|
---
|
|
|
|
# Plugin inventory
|
|
|
|
This page is generated from \`extensions/*/package.json\`, \`openclaw.plugin.json\`,
|
|
and the root npm package \`files\` exclusions. Regenerate it with:
|
|
|
|
\`\`\`bash
|
|
pnpm plugins:inventory:gen
|
|
\`\`\`
|
|
|
|
## Definitions
|
|
|
|
- **Core npm package:** built into the \`openclaw\` npm package and available without a separate plugin install.
|
|
- **Official external package:** OpenClaw-maintained plugin omitted from the core npm package, kept in this official inventory, and installed on demand through ClawHub and/or npm.
|
|
- **Source checkout only:** repo-local plugin omitted from published npm artifacts and not advertised as an installable package.
|
|
|
|
Source checkouts are different from npm installs: after \`pnpm install\`, bundled
|
|
plugins load from \`extensions/<id>\` so local edits and package-local workspace
|
|
dependencies are available.
|
|
|
|
## Install a plugin
|
|
|
|
Use the **Distribution** column to decide whether install is needed. Plugins that
|
|
say \`included in OpenClaw\` are already present in the core package. Official
|
|
external packages need one install, then a Gateway restart.
|
|
|
|
For example, Discord is an official external package:
|
|
|
|
\`\`\`bash
|
|
openclaw plugins install @openclaw/discord
|
|
openclaw gateway restart
|
|
openclaw plugins inspect discord --runtime --json
|
|
\`\`\`
|
|
|
|
Bare package specs try ClawHub first, then npm fallback. To force a source, use
|
|
\`clawhub:@openclaw/discord\` or \`npm:@openclaw/discord\`. After install, follow
|
|
the plugin's setup doc, such as [Discord](/channels/discord), to add credentials
|
|
and channel config. See [Manage plugins](/plugins/manage-plugins) for update,
|
|
uninstall, and publishing commands.
|
|
|
|
## Core npm package
|
|
|
|
${renderTable(groups.core)}
|
|
|
|
## Official external packages
|
|
|
|
${renderTable(groups.external)}
|
|
|
|
## Source checkout only
|
|
|
|
${renderTable(groups.source)}
|
|
`;
|
|
}
|
|
|
|
function main(argv = process.argv.slice(2)) {
|
|
const write = argv.includes("--write");
|
|
const check = argv.includes("--check");
|
|
if (write === check) {
|
|
console.error("usage: node scripts/generate-plugin-inventory-doc.mjs --write|--check");
|
|
process.exit(2);
|
|
}
|
|
|
|
const records = collectPluginRecords();
|
|
const next = renderDocument();
|
|
const docPath = path.join(ROOT, DOC_PATH);
|
|
if (write) {
|
|
fs.writeFileSync(docPath, next, "utf8");
|
|
writeGeneratedDocs(records);
|
|
return;
|
|
}
|
|
|
|
const current = fs.existsSync(docPath) ? fs.readFileSync(docPath, "utf8") : "";
|
|
if (current !== next) {
|
|
console.error(`${DOC_PATH} is stale. Run \`pnpm plugins:inventory:gen\`.`);
|
|
process.exit(1);
|
|
}
|
|
for (const [relativePath, expected] of readGeneratedDocs(records)) {
|
|
const fullPath = path.join(ROOT, relativePath);
|
|
const actual = fs.existsSync(fullPath) ? fs.readFileSync(fullPath, "utf8") : "";
|
|
if (actual !== expected) {
|
|
console.error(`${relativePath} is stale. Run \`pnpm plugins:inventory:gen\`.`);
|
|
process.exit(1);
|
|
}
|
|
}
|
|
}
|
|
|
|
main();
|