Dead code removed: - ops.js: second 'rate' handler block (lines 248-256) — unreachable because the top-level import block at line 187 fires first and returns true - autonomous.js: 'stop' handler (trimmed === 'stop') — /stop is in BASE_RUNTIME_COMMANDS, platform intercepts it before SF extension sees it - core.js: 'session-rename' handler block — /rename is the canonical command; alias added zero value and created confusion Catalog duplicates fixed: - 'plan' appeared twice (line 85 + 248) with contradictory descriptions; merged into single entry describing both phase-trigger and artifact-promotion - 'steer' appeared twice (line 72 + 167); removed the TUI-panel shortcut entry (Shift+Tab is a keyboard binding, not a slash command) Discoverability fix: - 'recover' was handled in ops.js but absent from catalog and manifest; added to both with accurate description (reconstruct DB hierarchy from markdown on disk) - 'session-rename' removed from catalog and manifest; users use /rename Check script improvements: - HIDDEN_OR_ALIAS_SUBCOMMANDS now filters both directions of the catalog ↔ handler consistency check (was only filtering 'handled but missing from catalog', not 'catalog but no SF handler') - Added 'stop' to HIDDEN_OR_ALIAS_SUBCOMMANDS with comment explaining it is platform-intercepted; removed 'recover' (now properly in catalog) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
339 lines
8.4 KiB
JavaScript
339 lines
8.4 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",
|
|
"footer-config", // alias for /statusline
|
|
"h",
|
|
"review-code", // alias for /rubber-duck
|
|
"stop", // platform-intercepted via BASE_RUNTIME_COMMANDS — never reaches SF handler
|
|
"undo-turn", // alias for /rewind
|
|
"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" &&
|
|
!HIDDEN_OR_ALIAS_SUBCOMMANDS.has(command) &&
|
|
!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();
|