Luigit
repositories / pi-ext

pi-ext

bugabingas pi extensions

owned by admin

scripts/readme/config.ts

Raw
/** 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 = "<!-- config:start -->";
export const CONFIG_END = "<!-- 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<string, EsNode>;
type TypeBindings = Map<string, EsNode>;
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<EsNode>(),
	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<string>(),
	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<string>,
	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<string, EsNode>): 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<string>(),
	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<string, EsNode>();
	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<string>,
	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<string>,
	definition: Value,
	known: Bindings,
	where: string,
	toolFactories: Set<string>,
): 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<string>,
	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<string> {
	const names = new Set<string>();
	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<string>();
	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();
}