singularity-forge/scripts/check-sf-extension-inventory.mjs
2026-05-04 23:27:20 +02:00

200 lines
6.1 KiB
JavaScript

import { execFileSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { join, resolve } from "node:path";
const repoRoot = resolve(import.meta.dirname, "..");
const sfRoot = join(repoRoot, "src", "resources", "extensions", "sf");
const manifestPath = join(sfRoot, "extension-manifest.json");
const RESOURCE_SOURCE_RE = /\.(?:js|mjs|cjs|json|md|yaml|yml|d\.ts)$/;
const DYNAMIC_TOOL_NAMES = ["bash", "edit", "read", "write"];
const DIRECT_COMMAND_NAMES = ["exit", "kill", "sf", "worktree", "wt"];
const HIDDEN_OR_ALIAS_SUBCOMMANDS = new Set([
"?",
"auto",
"h",
"recover",
"wt",
]);
function rel(path) {
return path.replace(`${repoRoot}/`, "");
}
function read(path) {
return readFileSync(path, "utf8");
}
function uniqueSorted(values) {
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
}
function failSection(title, values) {
return [`${title}:`, ...values.map((value) => ` - ${value}`)].join("\n");
}
function ignoredResourceSources() {
const output = execFileSync(
"git",
["ls-files", "-o", "-i", "--exclude-standard", "src/resources/extensions/**"],
{ cwd: repoRoot, encoding: "utf8" },
);
return output
.split(/\r?\n/)
.filter(Boolean)
.filter((path) => RESOURCE_SOURCE_RE.test(path));
}
function untrackedResourceSources() {
const output = execFileSync(
"git",
["ls-files", "-o", "--exclude-standard", "src/resources/extensions/**"],
{ cwd: repoRoot, encoding: "utf8" },
);
return output
.split(/\r?\n/)
.filter(Boolean)
.filter((path) => RESOURCE_SOURCE_RE.test(path));
}
function parseManifest() {
const raw = JSON.parse(read(manifestPath));
return {
tools: uniqueSorted(raw?.provides?.tools ?? []),
commands: uniqueSorted(raw?.provides?.commands ?? []),
};
}
function parseRegisteredTools() {
const files = [
"bootstrap/db-tools.js",
"bootstrap/exec-tools.js",
"bootstrap/journal-tools.js",
"bootstrap/judgment-tools.js",
"bootstrap/memory-tools.js",
"bootstrap/product-audit-tool.js",
"bootstrap/query-tools.js",
"tools/sift-search-tool.js",
];
const names = new Set(DYNAMIC_TOOL_NAMES);
for (const file of files) {
const source = read(join(sfRoot, file));
for (const match of source.matchAll(/\bname:\s*["`]([^"`]+)["`]/g)) {
names.add(match[1]);
}
}
return uniqueSorted(names);
}
function parseTopLevelCatalogCommands() {
const source = read(join(sfRoot, "commands", "catalog.js"));
const start = source.indexOf("export const TOP_LEVEL_SUBCOMMANDS");
const end = source.indexOf("const NESTED_COMPLETIONS");
if (start === -1 || end === -1 || end <= start) {
throw new Error("Could not locate TOP_LEVEL_SUBCOMMANDS in commands/catalog.js");
}
return uniqueSorted(
[...source.slice(start, end).matchAll(/\bcmd:\s*"([^"]+)"/g)].map((match) => match[1]),
);
}
function parseHandledTopLevelCommands() {
const handlerFiles = [
"core.js",
"auto.js",
"parallel.js",
"workflow.js",
"ops.js",
];
const commands = new Set();
for (const file of handlerFiles) {
const source = read(join(sfRoot, "commands", "handlers", file));
for (const match of source.matchAll(/trimmed\s*(?:===|!==)\s*"([^"]+)"/g)) {
commands.add(match[1].trim().split(/\s+/)[0]);
}
for (const match of source.matchAll(/trimmed\.startsWith\(\s*"([^"]+)"/g)) {
commands.add(match[1].trim().split(/\s+/)[0]);
}
}
return uniqueSorted(commands);
}
function main() {
const failures = [];
const ignoredSources = ignoredResourceSources();
if (ignoredSources.length > 0) {
failures.push(
failSection(
`Runtime extension source files are hidden by .gitignore (${ignoredSources.length})`,
ignoredSources.slice(0, 40).concat(
ignoredSources.length > 40 ? [`... ${ignoredSources.length - 40} more`] : [],
),
),
);
}
const untrackedSources = untrackedResourceSources();
if (untrackedSources.length > 0) {
failures.push(
failSection(
`Runtime extension source files are visible but untracked (${untrackedSources.length})`,
untrackedSources.slice(0, 40).concat(
untrackedSources.length > 40 ? [`... ${untrackedSources.length - 40} more`] : [],
),
),
);
}
const manifest = parseManifest();
const registeredTools = parseRegisteredTools();
const missingManifestTools = registeredTools.filter((tool) => !manifest.tools.includes(tool));
const staleManifestTools = manifest.tools.filter((tool) => !registeredTools.includes(tool));
if (missingManifestTools.length > 0) {
failures.push(failSection("Registered tools missing from extension-manifest.json", missingManifestTools));
}
if (staleManifestTools.length > 0) {
failures.push(failSection("Manifest tools not registered by SF bootstrap", staleManifestTools));
}
const missingManifestCommands = DIRECT_COMMAND_NAMES.filter(
(command) => !manifest.commands.includes(command),
);
const staleManifestCommands = manifest.commands.filter(
(command) => !DIRECT_COMMAND_NAMES.includes(command),
);
if (missingManifestCommands.length > 0) {
failures.push(failSection("Direct commands missing from extension-manifest.json", missingManifestCommands));
}
if (staleManifestCommands.length > 0) {
failures.push(failSection("Manifest direct commands not registered by SF bootstrap", staleManifestCommands));
}
const catalogCommands = parseTopLevelCatalogCommands();
const handledCommands = parseHandledTopLevelCommands().filter(
(command) => !HIDDEN_OR_ALIAS_SUBCOMMANDS.has(command),
);
const missingCatalogCommands = handledCommands.filter(
(command) => !catalogCommands.includes(command),
);
const unroutedCatalogCommands = catalogCommands.filter(
(command) => command !== "help" && !handledCommands.includes(command),
);
if (missingCatalogCommands.length > 0) {
failures.push(failSection("Handled /sf commands missing from TOP_LEVEL_SUBCOMMANDS", missingCatalogCommands));
}
if (unroutedCatalogCommands.length > 0) {
failures.push(failSection("Catalog /sf commands with no routed handler", unroutedCatalogCommands));
}
if (failures.length > 0) {
console.error(failures.join("\n\n"));
process.exit(1);
}
console.log(
`SF extension inventory OK: ${registeredTools.length} tools, ${DIRECT_COMMAND_NAMES.length} direct commands, ${catalogCommands.length} /sf subcommands.`,
);
}
main();