singularity-forge/scripts/generate-features-inventory.mjs
Mikael Hugo efd5e14e0a feat: add FEATURES.md capability map and generator
Human-oriented documentation of SF capabilities, with a script that
keeps it in sync with workflow-tools.ts and extension manifests.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 04:18:12 +02:00

127 lines
4.2 KiB
JavaScript

import { readFileSync, readdirSync, writeFileSync } from "node:fs";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const repoRoot = resolve(__dirname, "..");
const featuresPath = join(repoRoot, "FEATURES.md");
const workflowToolsPath = join(repoRoot, "packages", "mcp-server", "src", "workflow-tools.ts");
const providersPath = join(repoRoot, "packages", "pi-ai", "src", "types.ts");
const extensionsRoot = join(repoRoot, "src", "resources", "extensions");
const searchToolPath = join(repoRoot, "src", "resources", "extensions", "search-the-web", "tool-search.ts");
export const START = "<!-- GENERATED_FEATURE_INVENTORY_START -->";
export const END = "<!-- GENERATED_FEATURE_INVENTORY_END -->";
function uniqueSorted(values) {
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
}
export function parseWorkflowToolNames() {
const src = readFileSync(workflowToolsPath, "utf8");
const matches = [...src.matchAll(/server\.tool\(\s*"([^"]+)"/g)].map((m) => m[1]);
return uniqueSorted(matches);
}
export function parseKnownProviders() {
const src = readFileSync(providersPath, "utf8");
const match = src.match(/export type KnownProvider =([\s\S]*?);/);
if (!match) throw new Error("Could not find KnownProvider in packages/pi-ai/src/types.ts");
const providers = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
return uniqueSorted(providers);
}
export function parseBundledExtensions() {
const entries = readdirSync(extensionsRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.filter((name) => {
try {
const manifestPath = join(extensionsRoot, name, "extension-manifest.json");
readFileSync(manifestPath, "utf8");
return true;
} catch {
return false;
}
});
return uniqueSorted(entries);
}
export function parseSearchProviders() {
const src = readFileSync(searchToolPath, "utf8");
const providers = [
...src.matchAll(/providers\.push\('([^']+)'\)/g),
...src.matchAll(/provider\?: '([^']+)'/g),
]
.map((m) => m[1])
.filter((p) => p !== "combosearch");
return uniqueSorted(providers);
}
function formatBullets(values, formatter = (value) => `- \`${value}\``) {
return values.map((value) => formatter(value)).join("\n");
}
export function buildSection() {
const workflowTools = parseWorkflowToolNames();
const extensions = parseBundledExtensions();
const searchProviders = parseSearchProviders();
const knownProviders = parseKnownProviders();
return [
"### Workflow Tools",
"",
"Generated from `packages/mcp-server/src/workflow-tools.ts`.",
"",
formatBullets(workflowTools),
"",
"### Bundled Extensions",
"",
"Generated from `src/resources/extensions/*/extension-manifest.json`.",
"",
formatBullets(
extensions,
(value) => `- \`${value}\` — [extension-manifest.json](${relative(repoRoot, join(extensionsRoot, value, "extension-manifest.json"))})`,
),
"",
"### Search Providers",
"",
"Generated from the `search-the-web` extension provider declarations.",
"",
formatBullets(searchProviders),
"",
"### Known Model Providers",
"",
"Generated from `packages/pi-ai/src/types.ts` (`KnownProvider`).",
"",
formatBullets(knownProviders),
"",
].join("\n");
}
export function updateFeaturesContent(features) {
const startIndex = features.indexOf(START);
const endIndex = features.indexOf(END);
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
throw new Error("FEATURES.md is missing generated inventory markers");
}
const before = features.slice(0, startIndex + START.length);
const after = features.slice(endIndex);
const section = `\n\n${buildSection()}`;
return `${before}${section}\n${after}`;
}
export function main() {
const features = readFileSync(featuresPath, "utf8");
const updated = updateFeaturesContent(features);
writeFileSync(featuresPath, updated);
process.stdout.write(`Updated ${relative(repoRoot, featuresPath)}\n`);
}
if (process.argv[1] && resolve(process.argv[1]) === __filename) {
main();
}