/** Static extraction of an extension's public README surface. */ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join, relative } from "node:path"; import { parseSync } from "oxc-parser"; import { type EsNode, literalString, walkEs } from "../lib/esast.ts"; export interface SettingRow { key: string; env?: string; flag?: string; shape?: string; fields?: SettingField[]; default: string; } export interface SettingField { path: string; shape: string; optional: boolean; } export interface FlagRow { name: string; type: string; description: string; } export interface NamedRow { name: string; description: string; } export interface Surface { settings: SettingRow[]; flags: FlagRow[]; commands: NamedRow[]; tools: NamedRow[]; toolDiscovery: boolean; diagnostics: string[]; } export const CONFIG_START = ""; export const CONFIG_END = ""; const EXCLUDED_DIRECTORIES = new Set([ "node_modules", "dist", "assets", "demo", "skills", "__tests__", "__e2e__", "__bench__", "__benches__", "__fuzz__", "__serial__", "__fixtures__", ]); type Value = EsNode | undefined; type Bindings = Map; type TypeBindings = Map; type Source = { file: string; text: string; program: EsNode; known: Bindings }; const MAX_RESOLVE_DEPTH = 20; const TYPE_WRAPPERS = new Set([ "TSAsExpression", "TSSatisfiesExpression", "TSNonNullExpression", "TypeAssertion", "ParenthesizedExpression", ]); function sourceFiles(dir: string): string[] { const files: string[] = []; const walk = (current: string) => { for (const entry of readdirSync(current, { withFileTypes: true })) { const path = join(current, entry.name); if (entry.isDirectory()) { if (!EXCLUDED_DIRECTORIES.has(entry.name)) walk(path); } else if ( entry.name.endsWith(".ts") && !/(?:\.(?:test|spec|bench)|^pi-ext-(?:debug|settings))\.ts$/u.test( entry.name, ) ) files.push(path); } }; walk(dir); return files.sort(); } function property(node: EsNode | undefined, name: string): Value { for (const item of (node?.["properties"] as EsNode[] | undefined) ?? []) { if (item.type !== "Property") continue; if (nameOf(item["key"] as EsNode | undefined) === name) return item["value"] as EsNode; } return undefined; } function resolvedProperty( node: Value, name: string, known: Bindings, seen = new Set(), depth = 0, ): Value { if (depth >= MAX_RESOLVE_DEPTH) return undefined; node = resolve(node, known); if (node?.type === "CallExpression" && callName(node) === "defineTool") return resolvedProperty( (node["arguments"] as EsNode[] | undefined)?.[0], name, known, seen, depth + 1, ); if (node?.type !== "ObjectExpression" || seen.has(node)) return undefined; seen.add(node); const properties = (node["properties"] as EsNode[] | undefined) ?? []; for (const item of properties) if (item.type === "Property" && nameOf(item["key"] as EsNode) === name) return item["value"] as EsNode; for (const item of properties) if (item.type === "SpreadElement") { const value = resolvedProperty( item["argument"] as EsNode, name, known, seen, depth + 1, ); if (value) return value; } return undefined; } function nameOf(node: Value): string | undefined { if (node?.type === "Identifier") return node["name"] as string; return literalString(node); } function bindings(program: EsNode): Bindings { const result: Bindings = new Map(); walkEs(program, (node) => { if (node.type === "FunctionDeclaration") { const id = node["id"] as EsNode | undefined; if (id?.type === "Identifier") result.set(id["name"] as string, node); return; } if (node.type !== "VariableDeclarator") return; const id = node["id"] as EsNode; const init = node["init"] as EsNode | undefined; if (id?.type === "Identifier" && init) result.set(id["name"] as string, init); if (id?.type === "ObjectPattern" && init?.type === "ObjectExpression") for (const item of (id["properties"] as EsNode[] | undefined) ?? []) { const local = item["value"] as EsNode; const remote = nameOf(item["key"] as EsNode); if (local?.type === "Identifier" && remote) result.set(local["name"] as string, property(init, remote) as EsNode); } }); return result; } function typeBindings(sources: Source[]): TypeBindings { const result: TypeBindings = new Map(); for (const { program } of sources) walkEs(program, (node) => { if ( node.type !== "TSInterfaceDeclaration" && node.type !== "TSTypeAliasDeclaration" ) return; const id = node["id"] as EsNode | undefined; if (id?.type === "Identifier") result.set(id["name"] as string, node); }); return result; } function typeArguments(node: EsNode): EsNode[] { const parameters = (node["typeArguments"] ?? node["typeParameters"]) as | EsNode | undefined; return (parameters?.["params"] as EsNode[] | undefined) ?? []; } function settingType(node: EsNode): EsNode | undefined { if (node.type !== "VariableDeclarator") return undefined; const id = node["id"] as EsNode; const annotation = id["typeAnnotation"] as EsNode | undefined; const type = annotation?.["typeAnnotation"] as EsNode | undefined; if ( type?.type !== "TSTypeReference" || nameOf(type["typeName"] as EsNode) !== "SettingDeclaration" ) return undefined; return typeArguments(type)[0]; } function settingShape(node: EsNode, known: TypeBindings): string | undefined { const value = settingType(node); if (!value) return undefined; const name = value.type === "TSTypeReference" ? nameOf(value["typeName"] as EsNode) : undefined; const definition = name ? known.get(name) : undefined; if (definition && isParsedWrapper(definition)) return `parser-defined input (parsed as ${name})`; return formatType(value, known); } function settingFields( node: Value, known: TypeBindings, path: string, depth = 0, ): SettingField[] { if (!node || depth >= MAX_RESOLVE_DEPTH) return []; if (node.type === "TSTypeReference") { const name = nameOf(node["typeName"] as EsNode); if (name === "Array") return settingFields( typeArguments(node)[0], known, `${path}[]`, depth + 1, ); return settingFields( name ? known.get(name) : undefined, known, path, depth + 1, ); } if ( node.type === "TSTypeAliasDeclaration" || node.type === "TSTypeAnnotation" ) return settingFields( node["typeAnnotation"] as EsNode, known, path, depth + 1, ); if (node.type === "TSArrayType") return settingFields( node["elementType"] as EsNode, known, `${path}[]`, depth + 1, ); if (isParsedWrapper(node)) { const members = (node["body"] as EsNode)["body"] as EsNode[]; const config = members.find( (member) => nameOf(member["key"] as EsNode) === "config", ); return settingFields( config?.["typeAnnotation"] as EsNode | undefined, known, path, depth + 1, ); } const members = node.type === "TSInterfaceDeclaration" ? ((node["body"] as EsNode)["body"] as EsNode[]) : node.type === "TSTypeLiteral" ? (node["members"] as EsNode[]) : []; return members.flatMap((member): SettingField[] => { if (member.type !== "TSPropertySignature") return []; const key = nameOf(member["key"] as EsNode); const type = member["typeAnnotation"] as EsNode | undefined; if (!key || !type) return []; const fieldPath = `${path}.${key}`; const children = settingFields(type, known, fieldPath, depth + 1); return [ { path: fieldPath, shape: children.length ? "object / array (fields below)" : formatType(type, known), optional: Boolean(member["optional"]), }, ...children, ]; }); } function isParsedWrapper(node: EsNode): boolean { if (node.type !== "TSInterfaceDeclaration") return false; const members = ((node["body"] as EsNode | undefined)?.["body"] as EsNode[] | undefined) ?? []; const names = new Set( members .filter((member) => member.type === "TSPropertySignature") .map((member) => nameOf(member["key"] as EsNode)), ); return names.has("config") && names.has("errors"); } function formatType( node: EsNode, known: TypeBindings, seen = new Set(), depth = 0, ): string { if (depth >= MAX_RESOLVE_DEPTH) return "…"; if (node.type === "TSTypeAnnotation") return formatType(node["typeAnnotation"] as EsNode, known, seen, depth + 1); if (node.type === "TSTypeAliasDeclaration") return formatType(node["typeAnnotation"] as EsNode, known, seen, depth + 1); if (node.type === "TSStringKeyword") return "string"; if (node.type === "TSNumberKeyword") return "number"; if (node.type === "TSBooleanKeyword") return "boolean"; if (node.type === "TSNullKeyword") return "null"; if (node.type === "TSUndefinedKeyword") return "undefined"; if (node.type === "TSArrayType") return `${formatType(node["elementType"] as EsNode, known, seen, depth + 1)}[]`; if (node.type === "TSUnionType") return ((node["types"] as EsNode[] | undefined) ?? []) .map((item) => formatType(item, known, seen, depth + 1)) .join(" | "); if (node.type === "TSLiteralType") { const literal = node["literal"] as EsNode; const value = literalString(literal); return value === undefined ? String(literal["value"] ?? "literal") : JSON.stringify(value); } if (node.type === "TSTypeLiteral") return formatMembers( node["members"] as EsNode[] | undefined, known, seen, depth, ); if (node.type === "TSInterfaceDeclaration") return formatMembers( ((node["body"] as EsNode | undefined)?.["body"] as | EsNode[] | undefined) ?? [], known, seen, depth, ); if (node.type === "TSTypeReference") { const name = nameOf(node["typeName"] as EsNode); const args = typeArguments(node); if (name === "Array" && args[0]) return `${formatType(args[0], known, seen, depth + 1)}[]`; if (name === "Record" && args.length === 2) { const [key, value] = args; if (key && value) return `Record<${formatType(key, known, seen, depth + 1)}, ${formatType(value, known, seen, depth + 1)}>`; } const declaration = name ? known.get(name) : undefined; if (!name || !declaration || seen.has(name)) return name ?? "unknown"; return formatType(declaration, known, new Set([...seen, name]), depth + 1); } return "unknown"; } function formatMembers( members: EsNode[] | undefined, known: TypeBindings, seen: Set, depth: number, ): string { const fields = (members ?? []).flatMap((member) => { if (member.type !== "TSPropertySignature") return []; const key = nameOf(member["key"] as EsNode); const type = member["typeAnnotation"] as EsNode | undefined; if (!key || !type) return []; return `${key}${member["optional"] ? "?" : ""}: ${formatType(type, known, seen, depth + 1)}`; }); return `{ ${fields.join("; ")} }`; } function substitute(node: EsNode, values: Map): EsNode { if (node.type === "Identifier" && values.has(node["name"] as string)) return structuredClone(values.get(node["name"] as string)) as EsNode; const copy = structuredClone(node) as EsNode; for (const [key, value] of Object.entries(copy)) { if (key === "type" || (copy.type === "Property" && key === "key")) continue; if (Array.isArray(value)) copy[key] = value.map((item) => item && typeof item === "object" && "type" in item ? substitute(item as EsNode, values) : item, ); else if (value && typeof value === "object" && "type" in value) copy[key] = substitute(value as EsNode, values); } return copy; } function resolve( node: Value, known: Bindings, seen = new Set(), depth = 0, ): Value { if (!node || depth >= MAX_RESOLVE_DEPTH) return node; if (TYPE_WRAPPERS.has(node.type)) return resolve(node["expression"] as EsNode, known, seen, depth + 1); if (node.type === "Identifier") { const name = node["name"] as string; if (seen.has(name)) return undefined; const value = known.get(name); if (!value) return node; return resolve(value, known, new Set([...seen, name]), depth + 1); } if ( node.type !== "CallExpression" || (node["callee"] as EsNode)?.type !== "Identifier" ) return node; const factory = resolve( known.get((node["callee"] as EsNode)["name"] as string), known, seen, depth + 1, ); if ( factory?.type !== "FunctionDeclaration" && factory?.type !== "FunctionExpression" && factory?.type !== "ArrowFunctionExpression" ) return node; const params = (factory["params"] as EsNode[] | undefined) ?? []; const args = (node["arguments"] as EsNode[] | undefined) ?? []; const values = new Map(); for (const [index, param] of params.entries()) { if (param.type !== "Identifier" || !args[index]) return node; values.set(param["name"] as string, args[index] as EsNode); } const body = factory["body"] as EsNode; const returned = body?.type === "BlockStatement" ? ((body["body"] as EsNode[] | undefined)?.find( (item) => item.type === "ReturnStatement", )?.["argument"] as EsNode | undefined) : body; return returned ? resolve(substitute(returned, values), known, seen, depth + 1) : node; } function stringValue( node: Value, known: Bindings, depth = 0, ): string | undefined { if (depth >= MAX_RESOLVE_DEPTH) return undefined; node = resolve(node, known); const literal = literalString(node); if (literal !== undefined) return literal; if (node?.type === "BinaryExpression" && node["operator"] === "+") { const left = stringValue(node["left"] as EsNode, known, depth + 1); const right = stringValue(node["right"] as EsNode, known, depth + 1); return left === undefined || right === undefined ? undefined : left + right; } if (node?.type === "TemplateLiteral") { const quasis = (node["quasis"] as EsNode[] | undefined) ?? []; const expressions = (node["expressions"] as EsNode[] | undefined) ?? []; let value = ""; for (const [index, quasi] of quasis.entries()) { const cooked = (quasi["value"] as { cooked?: string } | undefined) ?.cooked; if (cooked === undefined) return undefined; value += cooked; if (expressions[index]) { const expression = stringValue(expressions[index], known, depth + 1); if (expression === undefined) return undefined; value += expression; } } return value; } return undefined; } function sourceText(text: string, node: EsNode): string { return text .slice(node["start"] as number, node["end"] as number) .replaceAll(/\s+/g, " ") .trim(); } function describeDefault(text: string, node: Value, known: Bindings): string { node = resolve(node, known); if (!node) return ""; if (node.type === "Literal") return `\`${JSON.stringify(node["value"])}\``; const literal = literalString(node); if (literal !== undefined) return `\`${JSON.stringify(literal)}\``; const raw = sourceText(text, node); return `\`${raw.length > 60 ? `${raw.slice(0, 57)}…` : raw}\``; } function callName(node: EsNode): string | undefined { const callee = node["callee"] as EsNode; if (callee?.type === "Identifier") return callee["name"] as string; if (callee?.type === "MemberExpression" && !callee["computed"]) return nameOf(callee["property"] as EsNode); return undefined; } function addNamed( rows: NamedRow[], seen: Set, kind: string, name: string, description: string, ): void { if (seen.has(`${kind}:${name}`)) return; seen.add(`${kind}:${name}`); rows.push({ name, description }); } function conciseDescription(description: string): string { const firstLine = description.trim().split(/\r?\n/u, 1)[0] ?? ""; const firstSentence = firstLine.match(/^.*?[.!?](?:\s|$)/u)?.[0] ?? firstLine; return firstSentence.length > 180 ? `${firstSentence.slice(0, 177).trimEnd()}…` : firstSentence; } function addTool( surface: Surface, seen: Set, definition: Value, known: Bindings, where: string, toolFactories: Set, ): void { definition = resolve(definition, known); if ( definition?.type === "CallExpression" && callName(definition) === "defineTool" ) definition = (definition["arguments"] as EsNode[] | undefined)?.[0]; if (definition?.type !== "ObjectExpression") { const callee = definition?.type === "CallExpression" ? (definition["callee"] as EsNode | undefined) : undefined; if ( callee?.type !== "Identifier" || !toolFactories.has(callee["name"] as string) ) surface.diagnostics.push( `${where}: tool definition cannot be resolved statically`, ); return; } const nameNode = resolvedProperty(definition, "name", known); const name = stringValue(nameNode, known); if (!name) { const dynamicName = resolve(nameNode, known); if (dynamicName?.type === "MemberExpression") { surface.toolDiscovery = true; return; } surface.diagnostics.push( `${where}: tool name cannot be resolved statically`, ); return; } addNamed( surface.tools, seen, "tool", name, conciseDescription( stringValue(resolvedProperty(definition, "description", known), known) ?? "", ), ); } function settingRow( surface: Surface, seen: Set, text: string, declaration: Value, known: Bindings, types: TypeBindings, ): void { const type = declaration ? settingType(declaration) : undefined; const shape = declaration ? settingShape(declaration, types) : undefined; if (declaration?.type === "VariableDeclarator") declaration = declaration["init"] as EsNode | undefined; declaration = resolve(declaration, known); if (declaration?.type !== "ObjectExpression") return; const key = stringValue(resolvedProperty(declaration, "key", known), known); const parse = resolvedProperty(declaration, "parse", known); const defaultValue = resolvedProperty(declaration, "default", known); if (!key || !parse) return; const env = stringValue(resolvedProperty(declaration, "env", known), known); if (seen.has(`setting:${key}`)) { const existing = surface.settings.find((row) => row.key === key); if (existing && env && !existing.env?.includes(env)) existing.env = existing.env ? `${existing.env} | ${env}` : env; return; } seen.add(`setting:${key}`); const flag = resolve(resolvedProperty(declaration, "flag", known), known); const flagName = flag?.type === "ObjectExpression" ? stringValue(property(flag, "name"), known) : undefined; const fields = type ? settingFields(type, types, key) : []; surface.settings.push({ key, ...(env ? { env } : {}), ...(flagName ? { flag: `--${flagName}` } : {}), ...(shape ? { shape } : {}), ...(fields.length ? { fields } : {}), default: defaultValue ? describeDefault(text, defaultValue, known) : "omitted", }); } function toolFactoryNames(sources: Source[]): Set { const names = new Set(); for (const { known } of sources) for (const [name, value] of known) { let containsDefinition = false; walkEs(value, (node) => { if (node.type === "CallExpression" && callName(node) === "defineTool") containsDefinition = true; }); if (containsDefinition) names.add(name); } return names; } /** Collect one extension's public surface without executing extension code. */ export function extractSurface(dir: string): Surface { const surface: Surface = { settings: [], flags: [], commands: [], tools: [], toolDiscovery: false, diagnostics: [], }; const sources: Source[] = []; for (const file of sourceFiles(dir)) { const text = readFileSync(file, "utf8"); try { const parsed = parseSync(file, text); if (parsed.errors.length) throw new Error(parsed.errors[0]?.message); const program = parsed.program as unknown as EsNode; sources.push({ file, text, program, known: bindings(program) }); } catch (error) { surface.diagnostics.push( `${relative(dir, file)}: parse failed: ${String(error)}`, ); } } const seen = new Set(); const types = typeBindings(sources); const toolFactories = toolFactoryNames(sources); for (const { file, text, program, known } of sources) { walkEs(program, (node) => { settingRow(surface, seen, text, node, known, types); if (node.type !== "CallExpression") return; const method = callName(node); const args = (node["arguments"] as EsNode[] | undefined) ?? []; if (method === "registerCommand") { const name = stringValue(args[0], known); if (!name) { surface.diagnostics.push( `${relative(dir, file)}: command name cannot be resolved statically`, ); return; } const options = resolve(args[1], known); addNamed( surface.commands, seen, "command", `/${name}`, options?.type === "ObjectExpression" ? (stringValue(property(options, "description"), known) ?? "") : "", ); } else if (method === "registerFlag") { const name = stringValue(args[0], known); if (!name) { surface.diagnostics.push( `${relative(dir, file)}: flag name cannot be resolved statically`, ); return; } if (seen.has(`flag:${name}`)) return; seen.add(`flag:${name}`); const options = resolve(args[1], known); surface.flags.push({ name: `--${name}`, type: options?.type === "ObjectExpression" ? (stringValue(property(options, "type"), known) ?? "boolean") : "boolean", description: options?.type === "ObjectExpression" ? (stringValue(property(options, "description"), known) ?? "") : "", }); } else if (method === "registerTool" || method === "defineTool") { const definition = resolve(args[0], known); if ( method === "registerTool" && definition?.type === "MemberExpression" ) return; addTool( surface, seen, args[0], known, relative(dir, file), toolFactories, ); } else if ( method === "resolveSetting" || method === "registerSettingFlag" ) { settingRow(surface, seen, text, args.at(-1), known, types); } }); } for (const rows of [ surface.settings, surface.flags, surface.commands, surface.tools, ]) rows.sort((a, b) => ("key" in a ? a.key : a.name).localeCompare("key" in b ? b.key : b.name), ); surface.diagnostics = [...new Set(surface.diagnostics)].sort(); return surface; } const cell = (value: string | undefined) => (value ?? "").replaceAll("|", "\\|").replaceAll("\n", " ").trim() || " "; /** Render generated Markdown between README config markers. */ export function renderSurface(surface: Surface): string { const lines = [CONFIG_START]; if (surface.commands.length) { lines.push("### Commands", "", "| Command | Purpose |", "| --- | --- |"); for (const row of surface.commands) lines.push(`| \`${row.name}\` | ${cell(row.description)} |`); lines.push(""); } if (surface.settings.length) { const hasEnv = surface.settings.some((row) => row.env); const hasFlag = surface.settings.some((row) => row.flag); lines.push( "### Setting names", "", `| Setting${hasEnv ? " | Environment" : ""}${hasFlag ? " | CLI flag" : ""} |`, `| ---${hasEnv ? " | ---" : ""}${hasFlag ? " | ---" : ""} |`, ); for (const row of surface.settings) lines.push( `| \`${row.key}\`${hasEnv ? ` | ${row.env ? `\`${cell(row.env)}\`` : " "}` : ""}${hasFlag ? ` | ${row.flag ? `\`${cell(row.flag)}\`` : " "}` : ""} |`, ); lines.push(""); const fields = surface.settings.flatMap((row) => row.fields ?? []); if (fields.length) { lines.push( "#### Object fields", "", "Paths below describe fields inside their parent settings object, not literal top-level keys.", "`[]` denotes an array element; see setup above for examples and defaults.", "", "| Field |", "| --- |", ); for (const field of fields) lines.push(`| \`${cell(field.path)}\` |`); lines.push(""); } } if (surface.flags.length) { lines.push( "### CLI flags", "", "| Flag | Type | Purpose |", "| --- | --- | --- |", ); for (const row of surface.flags) lines.push( `| \`${row.name}\` | ${cell(row.type)} | ${cell(row.description)} |`, ); lines.push(""); } if (surface.diagnostics.length) { lines.push( "### Extraction diagnostics", "", "Some source registrations cannot be resolved statically.", "", "```text", ...surface.diagnostics, "```", ); } while (lines.at(-1) === "") lines.pop(); lines.push(CONFIG_END); return lines.join("\n"); } export function injectSurface( readme: string, block: string, ): string | undefined { const start = readme.indexOf(CONFIG_START); const end = readme.indexOf(CONFIG_END); if (start < 0 || end < start) return undefined; return `${readme.slice(0, start)}${block}${readme.slice(end + CONFIG_END.length)}`; } export function extensionDirs(root: string): string[] { const base = join(root, "extensions"); return readdirSync(base) .filter( (name) => statSync(join(base, name)).isDirectory() && existsSync(join(base, name, "package.json")), ) .sort(); }