176 lines
5 KiB
JavaScript
176 lines
5 KiB
JavaScript
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", "pi-ai", "src", "types.ts");
|
|
const extensionsRoot = join(repoRoot, "src", "resources", "extensions");
|
|
const sfManifestPath = join(extensionsRoot, "sf", "extension-manifest.json");
|
|
const searchProviderPath = resolveExistingPath(
|
|
join(
|
|
repoRoot,
|
|
"src",
|
|
"resources",
|
|
"extensions",
|
|
"search-the-web",
|
|
"provider.ts",
|
|
),
|
|
join(
|
|
repoRoot,
|
|
"src",
|
|
"resources",
|
|
"extensions",
|
|
"search-the-web",
|
|
"provider.js",
|
|
),
|
|
);
|
|
|
|
export const START = "<!-- GENERATED_FEATURE_INVENTORY_START -->";
|
|
export const END = "<!-- GENERATED_FEATURE_INVENTORY_END -->";
|
|
|
|
function uniqueSorted(values) {
|
|
return [...new Set(values)].sort((a, b) => a.localeCompare(b));
|
|
}
|
|
|
|
function resolveExistingPath(...paths) {
|
|
const found = paths.find((path) => existsSync(path));
|
|
if (!found) {
|
|
throw new Error(
|
|
`None of these inventory source paths exist: ${paths.join(", ")}`,
|
|
);
|
|
}
|
|
return found;
|
|
}
|
|
|
|
export function parseKnownProviders() {
|
|
const src = readFileSync(providersPath, "utf8");
|
|
const match = src.match(/export type KnownProvider =([\s\S]*?);/);
|
|
if (!match)
|
|
throw new Error(
|
|
"Could not find KnownProvider in packages/pi-ai/src/types.ts",
|
|
);
|
|
const providers = [...match[1].matchAll(/"([^"]+)"/g)].map((m) => m[1]);
|
|
return uniqueSorted(providers);
|
|
}
|
|
|
|
export function parseBundledExtensions() {
|
|
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",
|
|
);
|
|
}
|
|
return uniqueSorted(
|
|
tools.filter((tool) => typeof tool === "string" && tool.startsWith("sf_")),
|
|
);
|
|
}
|
|
|
|
export function parseSearchProviders() {
|
|
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}\``) {
|
|
return values.map((value) => formatter(value)).join("\n");
|
|
}
|
|
|
|
export function buildSection() {
|
|
const extensions = parseBundledExtensions();
|
|
const sfNativeTools = parseSfNativeTools();
|
|
const searchProviders = parseSearchProviders();
|
|
const knownProviders = parseKnownProviders();
|
|
|
|
return [
|
|
"### SF Native Tools",
|
|
"",
|
|
"Generated from `src/resources/extensions/sf/extension-manifest.json`.",
|
|
"",
|
|
formatBullets(sfNativeTools),
|
|
"",
|
|
"### 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/pi-ai/src/types.ts` (`KnownProvider`).",
|
|
"",
|
|
formatBullets(knownProviders),
|
|
"",
|
|
].join("\n");
|
|
}
|
|
|
|
export function updateFeaturesContent(features) {
|
|
const startIndex = features.indexOf(START);
|
|
const endIndex = features.indexOf(END);
|
|
|
|
if (startIndex === -1 || endIndex === -1 || endIndex < startIndex) {
|
|
throw new Error("FEATURES.md is missing generated inventory markers");
|
|
}
|
|
|
|
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() {
|
|
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) {
|
|
main();
|
|
}
|