363 lines
9.4 KiB
TypeScript
363 lines
9.4 KiB
TypeScript
/**
|
|
* @license
|
|
* Copyright 2025 Google LLC
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
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();
|
|
}
|
|
}
|