| |
| |
| |
| |
| |
|
|
| import path from 'node:path'; |
| import { fileURLToPath, pathToFileURL } from 'node:url'; |
| import { mkdir, readFile, writeFile } from 'node:fs/promises'; |
|
|
| import { |
| getSettingsSchema, |
| type SettingCollectionDefinition, |
| type SettingDefinition, |
| type SettingsSchema, |
| type SettingsSchemaType, |
| SETTINGS_SCHEMA_DEFINITIONS, |
| type SettingsJsonSchemaDefinition, |
| } from '../packages/cli/src/config/settingsSchema.js'; |
| import { |
| formatDefaultValue, |
| formatWithPrettier, |
| normalizeForCompare, |
| } from './utils/autogen.js'; |
|
|
| const OUTPUT_RELATIVE_PATH = ['schemas', 'settings.schema.json']; |
| const SCHEMA_ID = |
| 'https://raw.githubusercontent.com/google-gemini/gemini-cli/main/schemas/settings.schema.json'; |
|
|
| type JsonPrimitive = string | number | boolean | null; |
| type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; |
|
|
| interface JsonSchema { |
| [key: string]: JsonValue | JsonSchema | JsonSchema[] | undefined; |
| $schema?: string; |
| $id?: string; |
| title?: string; |
| description?: string; |
| markdownDescription?: string; |
| type?: string | string[]; |
| enum?: JsonPrimitive[]; |
| default?: JsonValue; |
| properties?: Record<string, JsonSchema>; |
| items?: JsonSchema; |
| additionalProperties?: boolean | JsonSchema; |
| required?: string[]; |
| $ref?: string; |
| anyOf?: JsonSchema[]; |
| } |
|
|
| interface GenerateOptions { |
| checkOnly: boolean; |
| } |
|
|
| export async function generateSettingsSchema( |
| options: GenerateOptions, |
| ): Promise<void> { |
| const repoRoot = path.resolve( |
| path.dirname(fileURLToPath(import.meta.url)), |
| '..', |
| ); |
| const outputPath = path.join(repoRoot, ...OUTPUT_RELATIVE_PATH); |
| await mkdir(path.dirname(outputPath), { recursive: true }); |
|
|
| const schemaObject = buildSchemaObject(getSettingsSchema()); |
| const formatted = await formatWithPrettier( |
| JSON.stringify(schemaObject, null, 2), |
| outputPath, |
| ); |
|
|
| let existing: string | undefined; |
| try { |
| existing = await readFile(outputPath, 'utf8'); |
| } catch (error) { |
| if ((error as NodeJS.ErrnoException).code !== 'ENOENT') { |
| throw error; |
| } |
| } |
|
|
| if ( |
| existing && |
| normalizeForCompare(existing) === normalizeForCompare(formatted) |
| ) { |
| if (!options.checkOnly) { |
| console.log('Settings JSON schema already up to date.'); |
| } |
| return; |
| } |
|
|
| if (options.checkOnly) { |
| console.error( |
| 'Settings JSON schema is out of date. Run `npm run schema:settings` to regenerate.', |
| ); |
| process.exitCode = 1; |
| return; |
| } |
|
|
| await writeFile(outputPath, formatted); |
| console.log('Settings JSON schema regenerated.'); |
| } |
|
|
| export async function main(argv = process.argv.slice(2)): Promise<void> { |
| const checkOnly = argv.includes('--check'); |
| await generateSettingsSchema({ checkOnly }); |
| } |
|
|
| function buildSchemaObject(schema: SettingsSchemaType): JsonSchema { |
| const defs = new Map<string, JsonSchema>( |
| Object.entries(SETTINGS_SCHEMA_DEFINITIONS as Record<string, JsonSchema>), |
| ); |
|
|
| const root: JsonSchema = { |
| $schema: 'https://json-schema.org/draft/2020-12/schema', |
| $id: SCHEMA_ID, |
| title: 'Gemini CLI Settings', |
| description: |
| 'Configuration file schema for Gemini CLI settings. This schema enables IDE completion for `settings.json`.', |
| type: 'object', |
| additionalProperties: false, |
| properties: {}, |
| }; |
|
|
| root.properties!['$schema'] = { |
| title: 'Schema', |
| description: |
| 'The URL of the JSON schema for this settings file. Used by editors for validation and autocompletion.', |
| type: 'string', |
| default: SCHEMA_ID, |
| }; |
|
|
| for (const [key, definition] of Object.entries(schema)) { |
| root.properties![key] = buildSettingSchema(definition, [key], defs); |
| } |
|
|
| if (defs.size > 0) { |
| root.$defs = Object.fromEntries(defs.entries()); |
| } |
|
|
| return root; |
| } |
|
|
| function buildSettingSchema( |
| definition: SettingDefinition, |
| pathSegments: string[], |
| defs: Map<string, JsonSchema>, |
| ): JsonSchema { |
| const base: JsonSchema = { |
| title: definition.label, |
| description: definition.description, |
| markdownDescription: buildMarkdownDescription(definition), |
| }; |
|
|
| if (definition.default !== undefined) { |
| base.default = definition.default as JsonValue; |
| } |
|
|
| const schemaShape = definition.ref |
| ? buildRefSchema(definition.ref, defs) |
| : buildSchemaForType(definition, pathSegments, defs); |
|
|
| return { ...base, ...schemaShape }; |
| } |
|
|
| function buildCollectionSchema( |
| collection: SettingCollectionDefinition, |
| pathSegments: string[], |
| defs: Map<string, JsonSchema>, |
| ): JsonSchema { |
| if (collection.ref) { |
| return buildRefSchema(collection.ref, defs); |
| } |
| return buildSchemaForType(collection, pathSegments, defs); |
| } |
|
|
| function buildSchemaForType( |
| source: SettingDefinition | SettingCollectionDefinition, |
| pathSegments: string[], |
| defs: Map<string, JsonSchema>, |
| ): JsonSchema { |
| switch (source.type) { |
| case 'boolean': |
| case 'string': |
| case 'number': |
| return { type: source.type }; |
| case 'enum': |
| return buildEnumSchema(source.options); |
| case 'array': { |
| const itemPath = [...pathSegments, '<items>']; |
| const items = isSettingDefinition(source) |
| ? source.items |
| ? buildCollectionSchema(source.items, itemPath, defs) |
| : {} |
| : source.properties |
| ? buildInlineObjectSchema(source.properties, itemPath, defs) |
| : {}; |
| return { type: 'array', items }; |
| } |
| case 'object': |
| return isSettingDefinition(source) |
| ? buildObjectDefinitionSchema(source, pathSegments, defs) |
| : buildObjectCollectionSchema(source, pathSegments, defs); |
| default: |
| return {}; |
| } |
| } |
|
|
| function buildEnumSchema( |
| options: |
| | SettingDefinition['options'] |
| | SettingCollectionDefinition['options'], |
| ): JsonSchema { |
| const values = options?.map((option) => option.value) ?? []; |
| const inferred = inferTypeFromValues(values); |
| return { |
| type: inferred ?? undefined, |
| enum: values, |
| }; |
| } |
|
|
| function buildObjectDefinitionSchema( |
| definition: SettingDefinition, |
| pathSegments: string[], |
| defs: Map<string, JsonSchema>, |
| ): JsonSchema { |
| const properties = definition.properties |
| ? buildObjectProperties(definition.properties, pathSegments, defs) |
| : undefined; |
|
|
| const schema: JsonSchema = { |
| type: 'object', |
| }; |
|
|
| if (properties && Object.keys(properties).length > 0) { |
| schema.properties = properties; |
| } |
|
|
| if (definition.additionalProperties) { |
| schema.additionalProperties = buildCollectionSchema( |
| definition.additionalProperties, |
| [...pathSegments, '<additionalProperties>'], |
| defs, |
| ); |
| } else if (!definition.properties) { |
| schema.additionalProperties = true; |
| } else { |
| schema.additionalProperties = false; |
| } |
|
|
| return schema; |
| } |
|
|
| function buildObjectCollectionSchema( |
| collection: SettingCollectionDefinition, |
| pathSegments: string[], |
| defs: Map<string, JsonSchema>, |
| ): JsonSchema { |
| if (collection.properties) { |
| return buildInlineObjectSchema(collection.properties, pathSegments, defs); |
| } |
| return { type: 'object', additionalProperties: true }; |
| } |
|
|
| function buildObjectProperties( |
| properties: SettingsSchema, |
| pathSegments: string[], |
| defs: Map<string, JsonSchema>, |
| ): Record<string, JsonSchema> { |
| const result: Record<string, JsonSchema> = {}; |
| for (const [childKey, childDefinition] of Object.entries(properties)) { |
| result[childKey] = buildSettingSchema( |
| childDefinition, |
| [...pathSegments, childKey], |
| defs, |
| ); |
| } |
| return result; |
| } |
|
|
| function buildInlineObjectSchema( |
| properties: SettingsSchema, |
| pathSegments: string[], |
| defs: Map<string, JsonSchema>, |
| ): JsonSchema { |
| const childSchemas = buildObjectProperties(properties, pathSegments, defs); |
| return { |
| type: 'object', |
| properties: childSchemas, |
| additionalProperties: false, |
| }; |
| } |
|
|
| function buildRefSchema( |
| ref: string, |
| defs: Map<string, JsonSchema>, |
| ): JsonSchema { |
| ensureDefinition(ref, defs); |
| return { $ref: `#/$defs/${ref}` }; |
| } |
|
|
| function isSettingDefinition( |
| source: SettingDefinition | SettingCollectionDefinition, |
| ): source is SettingDefinition { |
| return 'label' in source; |
| } |
|
|
| function buildMarkdownDescription(definition: SettingDefinition): string { |
| const lines: string[] = []; |
|
|
| if (definition.description?.trim()) { |
| lines.push(definition.description.trim()); |
| } else { |
| lines.push('Description not provided.'); |
| } |
|
|
| lines.push(''); |
| lines.push(`- Category: \`${definition.category}\``); |
| lines.push( |
| `- Requires restart: \`${definition.requiresRestart ? 'yes' : 'no'}\``, |
| ); |
|
|
| if (definition.default !== undefined) { |
| lines.push(`- Default: \`${formatDefaultValue(definition.default)}\``); |
| } |
|
|
| return lines.join('\n'); |
| } |
|
|
| function inferTypeFromValues( |
| values: Array<string | number>, |
| ): string | undefined { |
| if (values.length === 0) { |
| return undefined; |
| } |
| if (values.every((value) => typeof value === 'string')) { |
| return 'string'; |
| } |
| if (values.every((value) => typeof value === 'number')) { |
| return 'number'; |
| } |
| return undefined; |
| } |
|
|
| function ensureDefinition(ref: string, defs: Map<string, JsonSchema>): void { |
| if (defs.has(ref)) { |
| return; |
| } |
| const predefined = SETTINGS_SCHEMA_DEFINITIONS[ref] as |
| | SettingsJsonSchemaDefinition |
| | undefined; |
| if (predefined) { |
| defs.set(ref, predefined as JsonSchema); |
| } else { |
| defs.set(ref, { description: `Definition for ${ref}` }); |
| } |
| } |
|
|
| if (process.argv[1]) { |
| const entryUrl = pathToFileURL(path.resolve(process.argv[1])).href; |
| if (entryUrl === import.meta.url) { |
| await main(); |
| } |
| } |
|
|