singularity-forge/scripts/check-sf-extension-inventory.mjs
2026-05-08 05:51:06 +02:00

333 lines
8.1 KiB
JavaScript

import { execFileSync } from "node:child_process";
import { existsSync, readdirSync, 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 extensionsRoot = join(repoRoot, "src", "resources", "extensions");
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 BASE_DIRECT_COMMAND_NAMES = ["kill", "wt"];
const BASE_RUNTIME_COMMAND_NAMES = new Set([
"settings",
"model",
"scoped-models",
"export",
"share",
"copy",
"name",
"session",
"changelog",
"hotkeys",
"fork",
"tree",
"provider",
"login",
"logout",
"new",
"compact",
"resume",
"reload",
"thinking",
"edit-mode",
"terminal",
"stop",
"exit",
"quit",
]);
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 readJsonOrNull(path) {
try {
return JSON.parse(read(path));
} catch {
return null;
}
}
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 isLoadableExtensionDir(dirPath) {
const packageJsonPath = join(dirPath, "package.json");
if (existsSync(packageJsonPath)) {
const pkg = readJsonOrNull(packageJsonPath);
if (pkg?.pi && typeof pkg.pi === "object") {
return Array.isArray(pkg.pi.extensions) && pkg.pi.extensions.length > 0;
}
}
return (
existsSync(join(dirPath, "index.js")) ||
existsSync(join(dirPath, "index.ts"))
);
}
function manifestlessLoadableExtensions() {
return readdirSync(extensionsRoot, { withFileTypes: true })
.filter((entry) => entry.isDirectory())
.map((entry) => entry.name)
.filter((name) => {
const dirPath = join(extensionsRoot, name);
return (
isLoadableExtensionDir(dirPath) &&
!existsSync(join(dirPath, "extension-manifest.json"))
);
})
.sort((a, b) => a.localeCompare(b));
}
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",
"autonomous.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 manifestlessExtensions = manifestlessLoadableExtensions();
if (manifestlessExtensions.length > 0) {
failures.push(
failSection(
`Loadable bundled extensions missing extension-manifest.json (${manifestlessExtensions.length})`,
manifestlessExtensions,
),
);
}
const manifest = parseManifest();
const registeredTools = parseRegisteredTools();
const catalogCommands = parseTopLevelCatalogCommands();
const directCommandNames = uniqueSorted(
BASE_DIRECT_COMMAND_NAMES.concat(
catalogCommands.filter(
(command) => !BASE_RUNTIME_COMMAND_NAMES.has(command),
),
),
);
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 = directCommandNames.filter(
(command) => !manifest.commands.includes(command),
);
const staleManifestCommands = manifest.commands.filter(
(command) => !directCommandNames.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 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, ${directCommandNames.length} direct commands, ${catalogCommands.length} catalog commands.`,
);
}
main();