singularity-forge/scripts/generate-features-inventory.mjs

173 lines
4.9 KiB
JavaScript
Raw Normal View History

2026-05-05 14:46:18 +02:00
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join, relative, resolve } from "node:path";
const __filename = import.meta.filename;
const __dirname = import.meta.dirname;
const repoRoot = resolve(__dirname, "..");
const featuresPath = join(repoRoot, "FEATURES.md");
const providersPath = join(repoRoot, "packages", "ai", "src", "types.ts");
const extensionsRoot = join(repoRoot, "src", "resources", "extensions");
const sfManifestPath = join(extensionsRoot, "sf", "extension-manifest.json");
2026-05-05 00:03:47 +02:00
const searchProviderPath = resolveExistingPath(
2026-05-05 14:31:16 +02:00
join(
repoRoot,
"src",
"resources",
"extensions",
"search-the-web",
"provider.ts",
),
join(
repoRoot,
"src",
"resources",
"extensions",
"search-the-web",
"provider.js",
),
2026-05-05 00:03:47 +02:00
);
export const START = "<!-- GENERATED_FEATURE_INVENTORY_START -->";
export const END = "<!-- GENERATED_FEATURE_INVENTORY_END -->";
function uniqueSorted(values) {
2026-05-05 14:31:16 +02:00
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
}
2026-05-05 00:03:47 +02:00
function resolveExistingPath(...paths) {
2026-05-05 14:31:16 +02:00
const found = paths.find((path) => existsSync(path));
if (!found) {
throw new Error(
`None of these inventory source paths exist: ${paths.join(", ")}`,
);
}
return found;
2026-05-05 00:03:47 +02:00
}
export function parseKnownProviders() {
2026-05-05 14:31:16 +02:00
const src = readFileSync(providersPath, "utf8");
const match = src.match(/export type KnownProvider =([\s\S]*?);/);
if (!match)
fix(lint): fix all pre-existing lint failures - check-sf-extension-inventory.mjs: expand parseDirectRegisteredCommands() scan to include 7 more files (guards/inturn.js, notifications/notify.js, permissions/index.js, ui/usage-bar.js, commands/legacy/audit.js, commands/legacy/create-extension.js, commands/legacy/create-slash-command.js) and filter results by BASE_RUNTIME_COMMAND_NAMES to exclude doc-string false positives ("name" in create-slash-command.js template text) - extension-manifest.json: remove 'clear' (subcommand of logs/notifications, never a top-level pi.registerCommand) - packages/pi-agent-core/src/db/sf-db.ts: fix 23 noVoidTypeReturn errors - openDatabase: void → boolean (caller uses return value at line 5625) - claimEscalationOverride: void → boolean (caller checks at escalation.js:243) - resolveSelfFeedbackEntry: void → boolean (caller checks at self-feedback.js:387) - copyWorktreeDb: void → boolean (caller checks at reconcileWorktreeDb) - compactUokMessages: void → {before,after} (caller returns value at message-bus.js:238) - insertSessionTurn: void → bigint|null (caller uses id at session-recorder.js:104) - expireStaleMemories: void → number (caller uses count at auto-start.js:1047) - deleteMemorySourceRow: void → boolean (caller returns value at memory-source-store.js:107) - deleteMemoryEmbedding: void → boolean (caller returns value at memory-embeddings.js:328) - updateBacklogItemStatus: remove dead return expression (callers discard value) - removeBacklogItem: remove dead return expression (callers discard value) - updateGateCircuitBreaker: remove dead return {total,avgMs,...} (wrong-type code accidentally merged from getGateLatencyStats, never reachable) - markUokMessageRead: remove dead return true/false (callers discard value) - Auto-fix formatting and organizeImports in ~30 source files (biome --write) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 04:02:31 +02:00
throw new Error("Could not find KnownProvider in packages/ai/src/types.ts");
2026-05-05 14:31:16 +02:00
const providers = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
return uniqueSorted(providers);
}
export function parseBundledExtensions() {
2026-05-05 14:31:16 +02:00
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 parseSfNativeTools() {
const manifest = JSON.parse(readFileSync(sfManifestPath, "utf8"));
const tools = manifest?.provides?.tools;
if (!Array.isArray(tools)) {
throw new Error(
"Could not find provides.tools in src/resources/extensions/sf/extension-manifest.json",
);
}
fix(lint): fix all pre-existing lint failures - check-sf-extension-inventory.mjs: expand parseDirectRegisteredCommands() scan to include 7 more files (guards/inturn.js, notifications/notify.js, permissions/index.js, ui/usage-bar.js, commands/legacy/audit.js, commands/legacy/create-extension.js, commands/legacy/create-slash-command.js) and filter results by BASE_RUNTIME_COMMAND_NAMES to exclude doc-string false positives ("name" in create-slash-command.js template text) - extension-manifest.json: remove 'clear' (subcommand of logs/notifications, never a top-level pi.registerCommand) - packages/pi-agent-core/src/db/sf-db.ts: fix 23 noVoidTypeReturn errors - openDatabase: void → boolean (caller uses return value at line 5625) - claimEscalationOverride: void → boolean (caller checks at escalation.js:243) - resolveSelfFeedbackEntry: void → boolean (caller checks at self-feedback.js:387) - copyWorktreeDb: void → boolean (caller checks at reconcileWorktreeDb) - compactUokMessages: void → {before,after} (caller returns value at message-bus.js:238) - insertSessionTurn: void → bigint|null (caller uses id at session-recorder.js:104) - expireStaleMemories: void → number (caller uses count at auto-start.js:1047) - deleteMemorySourceRow: void → boolean (caller returns value at memory-source-store.js:107) - deleteMemoryEmbedding: void → boolean (caller returns value at memory-embeddings.js:328) - updateBacklogItemStatus: remove dead return expression (callers discard value) - removeBacklogItem: remove dead return expression (callers discard value) - updateGateCircuitBreaker: remove dead return {total,avgMs,...} (wrong-type code accidentally merged from getGateLatencyStats, never reachable) - markUokMessageRead: remove dead return true/false (callers discard value) - Auto-fix formatting and organizeImports in ~30 source files (biome --write) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-05-11 04:02:31 +02:00
return uniqueSorted(tools.filter((tool) => typeof tool === "string"));
}
export function parseSearchProviders() {
2026-05-05 14:31:16 +02:00
const src = readFileSync(searchProviderPath, "utf8");
const preferencesMatch = src.match(
/const VALID_PREFERENCES = new Set\(\[([\s\S]*?)\]\)/,
);
const preferenceProviders = preferencesMatch
? [...preferencesMatch[1].matchAll(/["']([^"']+)["']/g)].map((m) => m[1])
: [];
const providers = [
...preferenceProviders,
...src.matchAll(/providers\.push\('([^']+)'\)/g),
...src.matchAll(/provider\?: '([^']+)'/g),
...src.matchAll(/\|\s*"([^"]+)"/g),
]
.map((m) => (typeof m === "string" ? m : m[1]))
.filter((p) => p !== "combosearch" && p !== "minimax" && p !== "auto");
return uniqueSorted(providers);
}
function formatBullets(values, formatter = (value) => `- \`${value}\``) {
2026-05-05 14:31:16 +02:00
return values.map((value) => formatter(value)).join("\n");
}
export function buildSection() {
2026-05-05 14:31:16 +02:00
const extensions = parseBundledExtensions();
const sfNativeTools = parseSfNativeTools();
2026-05-05 14:31:16 +02:00
const searchProviders = parseSearchProviders();
const knownProviders = parseKnownProviders();
return [
"### SF Native Tools",
"",
"Generated from `src/resources/extensions/sf/extension-manifest.json`.",
"",
formatBullets(sfNativeTools),
"",
2026-05-05 14:31:16 +02:00
"### 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/ai/src/types.ts` (`KnownProvider`).",
2026-05-05 14:31:16 +02:00
"",
formatBullets(knownProviders),
"",
].join("\n");
}
export function updateFeaturesContent(features) {
2026-05-05 14:31:16 +02:00
const startIndex = features.indexOf(START);
const endIndex = features.indexOf(END);
2026-05-05 14:31:16 +02:00
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
throw new Error("FEATURES.md is missing generated inventory markers");
}
2026-05-05 14:31:16 +02:00
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() {
2026-05-05 14:31:16 +02:00
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) {
2026-05-05 14:31:16 +02:00
main();
}