File size: 3,587 Bytes
84aa3bf | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 | /**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type { CommandModule } from 'yargs';
import { debugLogger, getErrorMessage } from '@google/gemini-cli-core';
import * as fs from 'node:fs';
import * as path from 'node:path';
import semver from 'semver';
import type { ExtensionConfig } from '../../config/extension.js';
import {
ExtensionManager,
validateContextFilePath,
} from '../../config/extension-manager.js';
import { requestConsentNonInteractive } from '../../config/extensions/consent.js';
import { promptForSetting } from '../../config/extensions/extensionSettings.js';
import { loadSettings } from '../../config/settings.js';
import { exitCli } from '../utils.js';
interface ValidateArgs {
path: string;
}
export async function handleValidate(args: ValidateArgs) {
try {
await validateExtension(args);
debugLogger.log(`Extension ${args.path} has been successfully validated.`);
} catch (error) {
debugLogger.error(getErrorMessage(error));
process.exit(1);
}
}
async function validateExtension(args: ValidateArgs) {
const workspaceDir = process.cwd();
const extensionManager = new ExtensionManager({
workspaceDir,
requestConsent: requestConsentNonInteractive,
requestSetting: promptForSetting,
settings: loadSettings(workspaceDir).merged,
});
const absoluteInputPath = path.resolve(args.path);
const extensionConfig: ExtensionConfig =
await extensionManager.loadExtensionConfig(absoluteInputPath);
const warnings: string[] = [];
const errors: string[] = [];
if (extensionConfig.contextFileName) {
const contextFileNames = Array.isArray(extensionConfig.contextFileName)
? extensionConfig.contextFileName
: [extensionConfig.contextFileName];
const missingContextFiles: string[] = [];
for (const contextFilePath of contextFileNames) {
const validation = validateContextFilePath(
contextFilePath,
absoluteInputPath,
);
if (!validation.isValid) {
errors.push(
`Invalid context file path: "${contextFilePath}". ${validation.errorMessage}`,
);
continue;
}
if (!fs.existsSync(validation.resolvedPath!)) {
missingContextFiles.push(contextFilePath);
}
}
if (missingContextFiles.length > 0) {
errors.push(
`The following context files referenced in gemini-extension.json are missing: ${missingContextFiles}`,
);
}
}
if (!semver.valid(extensionConfig.version)) {
warnings.push(
`Warning: Version '${extensionConfig.version}' does not appear to be standard semver (e.g., 1.0.0).`,
);
}
if (warnings.length > 0) {
debugLogger.warn('Validation warnings:');
for (const warning of warnings) {
debugLogger.warn(` - ${warning}`);
}
}
if (errors.length > 0) {
debugLogger.error('Validation failed with the following errors:');
for (const error of errors) {
debugLogger.error(` - ${error}`);
}
throw new Error('Extension validation failed.');
}
}
export const validateCommand: CommandModule = {
command: 'validate <path>',
describe: 'Validates an extension from a local path.',
builder: (yargs) =>
yargs.positional('path', {
describe: 'The path of the extension to validate.',
type: 'string',
demandOption: true,
}),
handler: async (args) => {
await handleValidate({
// eslint-disable-next-line @typescript-eslint/no-unsafe-type-assertion
path: args['path'] as string,
});
await exitCli();
},
};
|