/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; import { readFile, writeFile } from 'node:fs/promises'; import { generateSettingsSchema } from './generate-settings-schema.js'; import { escapeBackticks, formatDefaultValue, formatWithPrettier, injectBetweenMarkers, normalizeForCompare, } from './utils/autogen.js'; import type { SettingDefinition, SettingsSchema, SettingsSchemaType, } from '../packages/cli/src/config/settingsSchema.js'; import { FeatureDefinitions, FeatureStage, } from '../packages/core/src/config/features.js'; const START_MARKER = ''; const END_MARKER = ''; const MANUAL_TOP_LEVEL = new Set(['mcpServers', 'telemetry', 'extensions']); interface DocEntry { path: string; type: string; label: string; category: string; description: string; defaultValue: string; requiresRestart: boolean; enumValues?: string[]; stage?: string; } export async function main(argv = process.argv.slice(2)) { const checkOnly = argv.includes('--check'); await generateSettingsSchema({ checkOnly }); const repoRoot = path.resolve( path.dirname(fileURLToPath(import.meta.url)), '..', ); const docPath = path.join(repoRoot, 'docs/reference/configuration.md'); const cliSettingsDocPath = path.join(repoRoot, 'docs/cli/settings.md'); const featureGatesDocPath = path.join( repoRoot, 'docs/cli/feature-lifecycle.md', ); const { getSettingsSchema } = await loadSettingsSchemaModule(); const schema = getSettingsSchema(); const allSettingsSections = collectEntries(schema, { includeAll: true }); const filteredSettingsSections = collectEntries(schema, { includeAll: false, }); const generatedBlock = renderSections(allSettingsSections); const generatedTableBlock = renderTableSections(filteredSettingsSections); const generatedFeatureGatesBlock = renderFeatureGatesTable(); await updateFile(docPath, generatedBlock, checkOnly); await updateFile(cliSettingsDocPath, generatedTableBlock, checkOnly); await updateFile( featureGatesDocPath, generatedFeatureGatesBlock, checkOnly, '', '', ); } async function updateFile( filePath: string, newContent: string, checkOnly: boolean, startMarker = START_MARKER, endMarker = END_MARKER, ) { const doc = await readFile(filePath, 'utf8'); const injectedDoc = injectBetweenMarkers({ document: doc, startMarker, endMarker, newContent: newContent, paddingBefore: '\n', paddingAfter: '\n', }); const formattedDoc = await formatWithPrettier(injectedDoc, filePath); if (normalizeForCompare(doc) === normalizeForCompare(formattedDoc)) { if (!checkOnly) { console.log( `Settings documentation (${path.basename(filePath)}) already up to date.`, ); } return; } if (checkOnly) { console.error( 'Settings documentation (' + path.basename(filePath) + ') is out of date. Run `npm run docs:settings` to regenerate.', ); process.exitCode = 1; return; } await writeFile(filePath, formattedDoc); console.log( `Settings documentation (${path.basename(filePath)}) regenerated.`, ); } async function loadSettingsSchemaModule() { const modulePath = '../packages/cli/src/config/settingsSchema.ts'; return import(modulePath); } function collectEntries( schema: SettingsSchemaType, options: { includeAll?: boolean } = {}, ) { const sections = new Map(); const visit = ( current: SettingsSchema, pathSegments: string[], topLevel?: string, ) => { for (const [key, definition] of Object.entries(current)) { if (pathSegments.length === 0 && MANUAL_TOP_LEVEL.has(key)) { continue; } const newPathSegments = [...pathSegments, key]; const sectionKey = topLevel ?? key; const hasChildren = definition.type === 'object' && definition.properties && Object.keys(definition.properties).length > 0; if (definition.ignoreInDocs) { continue; } if (!hasChildren && (options.includeAll || definition.showInDialog)) { if (!sections.has(sectionKey)) { sections.set(sectionKey, []); } let defaultValue = definition.default; let stage: string | undefined; if (sectionKey === 'features' && FeatureDefinitions[key]) { const specs = FeatureDefinitions[key]; const latest = specs[specs.length - 1]; stage = latest.preRelease; defaultValue = latest.default ?? (stage === FeatureStage.Beta || stage === FeatureStage.GA); } sections.get(sectionKey)!.push({ path: newPathSegments.join('.'), type: formatType(definition), label: definition.label, category: definition.category, description: formatDescription(definition), defaultValue: formatDefaultValue(defaultValue, { quoteStrings: true, }), requiresRestart: Boolean(definition.requiresRestart), enumValues: definition.options?.map((option) => formatDefaultValue(option.value, { quoteStrings: true }), ), stage, }); } if (hasChildren && definition.properties) { visit(definition.properties, newPathSegments, sectionKey); } } }; visit(schema, []); return sections; } function formatDescription(definition: SettingDefinition) { if (definition.description?.trim()) { return definition.description.trim(); } return 'Description not provided.'; } function formatType(definition: SettingDefinition): string { switch (definition.ref) { case 'StringOrStringArray': return 'string | string[]'; case 'BooleanOrString': return 'boolean | string'; default: return definition.type; } } function renderSections(sections: Map) { const lines: string[] = []; for (const [section, entries] of sections) { if (entries.length === 0) { continue; } lines.push('#### `' + section + '`'); lines.push(''); for (const entry of entries) { lines.push('- **`' + entry.path + '`** (' + entry.type + '):'); lines.push(' - **Description:** ' + entry.description); if (entry.defaultValue.includes('\n')) { lines.push(' - **Default:**'); lines.push(''); lines.push(' ```json'); lines.push( entry.defaultValue .split('\n') .map((line) => ' ' + line) .join('\n'), ); lines.push(' ```'); } else { lines.push( ' - **Default:** `' + escapeBackticks(entry.defaultValue) + '`', ); } if (entry.enumValues && entry.enumValues.length > 0) { const values = entry.enumValues .map((value) => '`' + escapeBackticks(value) + '`') .join(', '); lines.push(' - **Values:** ' + values); } if (entry.stage) { lines.push(' - **Stage:** ' + entry.stage); } if (entry.requiresRestart) { lines.push(' - **Requires restart:** Yes'); } lines.push(''); } } return lines.join('\n').trimEnd(); } function renderTableSections(sections: Map) { const lines: string[] = []; for (const [section, entries] of sections) { if (entries.length === 0) { continue; } let title = section.charAt(0).toUpperCase() + section.slice(1); if (title === 'Ui') { title = 'UI'; } else if (title === 'Ide') { title = 'IDE'; } lines.push(`### ${title}`); lines.push(''); if (section === 'features') { lines.push('| UI Label | Setting | Description | Default | Stage |'); lines.push('| --- | --- | --- | --- | --- |'); } else { lines.push('| UI Label | Setting | Description | Default |'); lines.push('| --- | --- | --- | --- |'); } for (const entry of entries) { const val = entry.defaultValue.replace(/\n/g, ' '); const defaultVal = '`' + escapeBackticks(val) + '`'; let row = '| ' + entry.label + ' | `' + entry.path + '` | ' + entry.description + ' | ' + defaultVal + ' |'; if (section === 'features') { const stageVal = entry.stage ? '`' + entry.stage + '`' : '-'; row += ' ' + stageVal + ' |'; } lines.push(row); } lines.push(''); } return lines.join('\n').trimEnd(); } function renderFeatureGatesTable() { let markdown = '| Feature | Stage | Default | Since | Description |\n'; markdown += '| --- | --- | --- | --- | --- |\n'; const sortedFeatures = Object.entries(FeatureDefinitions).sort( ([keyA], [keyB]) => keyA.localeCompare(keyB), ); for (const [key, specs] of sortedFeatures) { const latest = specs[specs.length - 1]; const stage = latest.preRelease; const isEnabled = latest.default ?? (stage === FeatureStage.Beta || stage === FeatureStage.GA); const defaultValue = isEnabled ? 'Enabled' : 'Disabled'; const since = latest.since || '-'; const description = latest.description || '-'; markdown += `| \`${key}\` | ${stage} | ${defaultValue} | ${since} | ${description} |\n`; } return markdown; } if (process.argv[1]) { const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href; if (entryUrl === import.meta.url) { await main(); } }