200 lines
6.1 KiB
JavaScript
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();
|