From b9883a4f1bedf9cf6f866d126fff8feefa238d8c Mon Sep 17 00:00:00 2001 From: Alan Agius <17563226+alan-agius4@users.noreply.github.com> Date: Fri, 19 Jun 2026 08:00:38 +0000 Subject: [PATCH] refactor(ng-dev): move skills validation command under misc as validate-skills Removes the top-level `ai` command and its associated subcommands (`fix`, `migrate`) entirely. Moves the `skills` validator package from `ng-dev/ai/skills` to `ng-dev/misc/validate-skills` and registers it as the `validate-skills` subcommand under `ng-dev misc`. All relative imports and Bazel build dependencies are updated and validated. --- .prettierignore | 2 +- ng-dev/BUILD.bazel | 1 - ng-dev/ai/BUILD.bazel | 20 -- ng-dev/ai/cli.ts | 16 - ng-dev/ai/consts.ts | 16 - ng-dev/ai/fix.ts | 316 ------------------ ng-dev/ai/migrate.ts | 243 -------------- ng-dev/cli.ts | 2 - ng-dev/misc/BUILD.bazel | 1 + ng-dev/misc/cli.ts | 4 +- .../validate-skills}/BUILD.bazel | 4 +- .../skills => misc/validate-skills}/cli.ts | 6 +- .../skills/complex-skill/SKILL.md | 0 .../skills/test-skill/SKILL.md | 0 .../skills/InvalidName/SKILL.md | 0 .../invalid-schema/skills/bad-schema/SKILL.md | 0 .../skills/bad-skill/SKILL.md | 0 .../multiple-mixed/skills/skill1/SKILL.md | 0 .../multiple-mixed/skills/skill2/SKILL.md | 0 .../multiple-valid/skills/skill1/SKILL.md | 0 .../multiple-valid/skills/skill2/SKILL.md | 0 .../name-mismatch/skills/wrong-name/SKILL.md | 0 .../valid-skill/skills/test-skill/SKILL.md | 0 .../validate-skills}/validate.spec.ts | 2 +- .../validate-skills}/validate.ts | 0 25 files changed, 11 insertions(+), 622 deletions(-) delete mode 100644 ng-dev/ai/BUILD.bazel delete mode 100644 ng-dev/ai/cli.ts delete mode 100644 ng-dev/ai/consts.ts delete mode 100644 ng-dev/ai/fix.ts delete mode 100644 ng-dev/ai/migrate.ts rename ng-dev/{ai/skills => misc/validate-skills}/BUILD.bazel (93%) rename ng-dev/{ai/skills => misc/validate-skills}/cli.ts (87%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/complex-skill/skills/complex-skill/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/invalid-frontmatter-location/skills/test-skill/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/invalid-name-format/skills/InvalidName/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/invalid-schema/skills/bad-schema/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/missing-frontmatter/skills/bad-skill/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/multiple-mixed/skills/skill1/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/multiple-mixed/skills/skill2/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/multiple-valid/skills/skill1/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/multiple-valid/skills/skill2/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/name-mismatch/skills/wrong-name/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/fixtures/valid-skill/skills/test-skill/SKILL.md (100%) rename ng-dev/{ai/skills => misc/validate-skills}/validate.spec.ts (97%) rename ng-dev/{ai/skills => misc/validate-skills}/validate.ts (100%) diff --git a/.prettierignore b/.prettierignore index 235a0656d5..c84556e2c6 100644 --- a/.prettierignore +++ b/.prettierignore @@ -27,7 +27,7 @@ bazel/map-size-tracking/test/size-golden.json **/pnpm-lock.yaml # AI Skills intentionally not formatted to ensure validation failures. -ng-dev/ai/skills/fixtures/invalid-frontmatter-location/skills/test-skill/SKILL.md +ng-dev/misc/validate-skills/fixtures/invalid-frontmatter-location/skills/test-skill/SKILL.md # Golden files bazel/rules/rules_sass/test/**/*_golden.css diff --git a/ng-dev/BUILD.bazel b/ng-dev/BUILD.bazel index 16f63a8976..fa7e50a7aa 100644 --- a/ng-dev/BUILD.bazel +++ b/ng-dev/BUILD.bazel @@ -50,7 +50,6 @@ ts_project( "//ng-dev:node_modules/@types/yargs", "//ng-dev:node_modules/yaml", "//ng-dev:node_modules/yargs", - "//ng-dev/ai", "//ng-dev/auth", "//ng-dev/caretaker", "//ng-dev/commit-message", diff --git a/ng-dev/ai/BUILD.bazel b/ng-dev/ai/BUILD.bazel deleted file mode 100644 index dc9df06ef9..0000000000 --- a/ng-dev/ai/BUILD.bazel +++ /dev/null @@ -1,20 +0,0 @@ -load("//tools:defaults.bzl", "ts_project") - -ts_project( - name = "ai", - srcs = glob([ - "**/*.ts", - ]), - visibility = ["//ng-dev:__subpackages__"], - deps = [ - "//ng-dev:node_modules/@google/genai", - "//ng-dev:node_modules/@types/cli-progress", - "//ng-dev:node_modules/@types/node", - "//ng-dev:node_modules/@types/yargs", - "//ng-dev:node_modules/cli-progress", - "//ng-dev:node_modules/fast-glob", - "//ng-dev:node_modules/yargs", - "//ng-dev/ai/skills", - "//ng-dev/utils", - ], -) diff --git a/ng-dev/ai/cli.ts b/ng-dev/ai/cli.ts deleted file mode 100644 index 9f1b099a66..0000000000 --- a/ng-dev/ai/cli.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @license - * Copyright Google LLC - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ -import {Argv} from 'yargs'; -import {MigrateModule} from './migrate.js'; -import {FixModule} from './fix.js'; -import {SkillsModule} from './skills/cli.js'; - -/** Build the parser for the AI commands. */ -export function buildAiParser(localYargs: Argv) { - return localYargs.command(MigrateModule).command(FixModule).command(SkillsModule); -} diff --git a/ng-dev/ai/consts.ts b/ng-dev/ai/consts.ts deleted file mode 100644 index 664c2090a9..0000000000 --- a/ng-dev/ai/consts.ts +++ /dev/null @@ -1,16 +0,0 @@ -/** - * @license - * Copyright Google LLC - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -/** Default model to use for AI-based scripts. */ -export const DEFAULT_MODEL = 'gemini-3.1-pro-preview'; - -/** Default temperature for AI-based scripts. */ -export const DEFAULT_TEMPERATURE = 0.1; - -/** Default API key to use when running AI-based scripts. */ -export const DEFAULT_API_KEY = process.env['GEMINI_API_KEY']!; diff --git a/ng-dev/ai/fix.ts b/ng-dev/ai/fix.ts deleted file mode 100644 index db3bf8abc2..0000000000 --- a/ng-dev/ai/fix.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * @license - * Copyright Google LLC - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {createPartFromUri, FileState, GoogleGenAI, Part} from '@google/genai'; -import {setTimeout} from 'node:timers/promises'; -import {readFile, writeFile} from 'node:fs/promises'; -import {basename, resolve, relative, isAbsolute} from 'node:path'; -import {determineRepoBaseDirFromCwd} from '../utils/repo-directory.js'; -import glob from 'fast-glob'; -import assert from 'node:assert'; -import {Bar} from 'cli-progress'; -import {Argv, Arguments, CommandModule} from 'yargs'; -import {randomUUID} from 'node:crypto'; -import {DEFAULT_MODEL, DEFAULT_TEMPERATURE, DEFAULT_API_KEY} from './consts.js'; -import {Spinner} from '../utils/spinner.js'; -import {Log} from '../utils/logging.js'; - -/** Command line options. */ -export interface Options { - /** Files that the fix should apply to. */ - files: string[]; - - /** Error message(s) to be resolved. */ - error: string; - - /** Model that should be used to apply the prompt. */ - model: string; - - /** Temperature for the model. */ - temperature: number; - - /** API key to use when making requests. */ - apiKey?: string; -} - -interface FixedFileContent { - filePath: string; - content: string; -} - -/** Yargs command builder for the command. */ -function builder(argv: Argv): Argv { - return argv - .positional('files', { - description: `One or more glob patterns to find target files (e.g., 'src/**/*.ts' 'test/**/*.ts').`, - type: 'string', - array: true, - demandOption: true, - }) - .option('error', { - alias: 'e', - description: 'Full error description from the build process', - type: 'string', - demandOption: true, - }) - .option('model', { - type: 'string', - alias: 'm', - description: 'Model to use for the migration', - default: DEFAULT_MODEL, - }) - .option('temperature', { - type: 'number', - alias: 't', - default: DEFAULT_TEMPERATURE, - description: 'Temperature for the model. Lower temperature reduces randomness/creativity', - }) - .option('apiKey', { - type: 'string', - alias: 'a', - default: DEFAULT_API_KEY, - description: 'API key used when making calls to the Gemini API', - }); -} - -/** Yargs command handler for the command. */ -async function handler(options: Arguments) { - const apiKey = options.apiKey || DEFAULT_API_KEY; - - assert( - apiKey, - [ - 'No API key configured. A Gemini API key must be set as the `GEMINI_API_KEY` environment ' + - 'variable, or passed in using the `--api-key` flag.', - 'For internal users, see go/aistudio-apikey', - ].join('\n'), - ); - - const fixedContents = await fixFilesWithAI( - apiKey, - options.files, - options.error, - options.model, - options.temperature, - ); - Log.info('\n--- AI Suggested Fixes Summary ---'); - if (fixedContents.length === 0) { - Log.info( - 'No files were fixed or found matching the pattern. Check your glob pattern and check whether the files exist.', - ); - - return; - } - - Log.info('Updated files:'); - const writeTasks = fixedContents.map(({filePath, content}) => - writeFile(filePath, content).then(() => Log.info(` - ${filePath}`)), - ); - await Promise.all(writeTasks); -} - -async function fixFilesWithAI( - apiKey: string, - globPatterns: string[], - errorDescription: string, - model: string, - temperature: number, -): Promise { - const filePaths = await glob(globPatterns, { - onlyFiles: true, - absolute: false, - }); - - if (filePaths.length === 0) { - Log.error(`No files found matching the patterns: ${JSON.stringify(globPatterns, null, 2)}.`); - return []; - } - - const ai = new GoogleGenAI({vertexai: false, apiKey}); - let uploadedFileNames: string[] = []; - - const progressBar = new Bar({ - format: `{step} [{bar}] ETA: {eta}s | {value}/{total} files`, - clearOnComplete: true, - }); - - const repoRoot = determineRepoBaseDirFromCwd(); - const absoluteInputFilePaths = new Set(filePaths.map((p) => resolve(p))); - - let spinner: Spinner | undefined; - - try { - const { - fileNameMap, - partsForGeneration, - uploadedFileNames: uploadedFiles, - } = await uploadFiles(ai, filePaths, progressBar); - - uploadedFileNames = uploadedFiles; - - spinner = new Spinner('AI is analyzing the files and generating potential fixes...'); - const response = await ai.models.generateContent({ - model, - contents: [{text: generatePrompt(errorDescription, fileNameMap)}, ...partsForGeneration], - config: { - responseMimeType: 'application/json', - candidateCount: 1, - maxOutputTokens: Infinity, - temperature, - }, - }); - - const responseText = response.text; - if (!responseText) { - spinner.failure(`AI returned an empty response.`); - return []; - } - - const fixes = JSON.parse(responseText) as FixedFileContent[]; - - if (!Array.isArray(fixes)) { - throw new Error('AI response is not a JSON array.'); - } - - for (const fix of fixes) { - const absoluteFixPath = resolve(repoRoot, fix.filePath); - const relativePath = relative(repoRoot, absoluteFixPath); - const startsWithRepo = !relativePath.startsWith('..') && !isAbsolute(relativePath); - const isInInputFiles = absoluteInputFilePaths.has(absoluteFixPath); - - if (!startsWithRepo) { - throw new Error(`AI-suggested file path ${fix.filePath} is outside the repository root.`); - } - if (!isInInputFiles) { - throw new Error( - `AI-suggested file path ${fix.filePath} is not in the list of input files.`, - ); - } - fix.filePath = absoluteFixPath; - } - - spinner.complete(); - return fixes; - } catch (error) { - if (spinner) { - spinner.failure('AI fix failed.'); - } - throw error; - } finally { - if (uploadedFileNames.length) { - progressBar.start(uploadedFileNames.length, 0, { - step: 'Deleting temporary uploaded files', - }); - const deleteTasks = uploadedFileNames.map((name) => { - return ai.files - .delete({name}) - .catch((error) => Log.warn(`WARNING: Failed to delete temporary file ${name}:`, error)) - .finally(() => progressBar.increment()); - }); - - await Promise.allSettled(deleteTasks).finally(() => progressBar.stop()); - } - } -} - -async function uploadFiles( - ai: GoogleGenAI, - filePaths: string[], - progressBar: Bar, -): Promise<{ - uploadedFileNames: string[]; - partsForGeneration: Part[]; - fileNameMap: Map; -}> { - const uploadedFileNames: string[] = []; - const partsForGeneration: Part[] = []; - const fileNameMap = new Map(); - - progressBar.start(filePaths.length, 0, {step: 'Uploading files'}); - - const uploadPromises = filePaths.map(async (filePath) => { - try { - const uploadedFile = await ai.files.upload({ - file: new Blob([await readFile(filePath, {encoding: 'utf8'})], { - type: 'text/plain', - }), - config: { - displayName: `fix_request_${basename(filePath)}_${randomUUID()}`, - }, - }); - - assert(uploadedFile.name, 'File name cannot be undefined after upload.'); - - let getFile = await ai.files.get({name: uploadedFile.name}); - while (getFile.state === FileState.PROCESSING) { - await setTimeout(500); // Wait for 500ms before re-checking - getFile = await ai.files.get({name: uploadedFile.name}); - } - - if (getFile.state === FileState.FAILED) { - throw new Error(`File processing failed on API for ${filePath}. Skipping this file.`); - } - - if (getFile.uri && getFile.mimeType) { - const filePart = createPartFromUri(getFile.uri, getFile.mimeType); - partsForGeneration.push(filePart); - fileNameMap.set(filePath, uploadedFile.name); - progressBar.increment(); - return uploadedFile.name; // Return the name on success - } else { - throw new Error( - `Uploaded file for ${filePath} is missing URI or MIME type after processing. Skipping.`, - ); - } - } catch (error: any) { - Log.error(`Error uploading or processing file ${filePath}: ${error.message}`); - return null; // Indicate failure for this specific file - } - }); - - const results = await Promise.allSettled(uploadPromises).finally(() => progressBar.stop()); - - for (const result of results) { - if (result.status === 'fulfilled' && result.value !== null) { - uploadedFileNames.push(result.value); - } - } - - return {uploadedFileNames, fileNameMap, partsForGeneration}; -} - -function generatePrompt(errorDescription: string, fileNameMap: Map): string { - return ` - You are a highly skilled software engineer, specializing in Bazel, Starlark, Python, Angular, JavaScript, - TypeScript, and everything related. - The following files are part of a build process that failed with the error: - \`\`\` - ${errorDescription} - \`\`\` - Please analyze the content of EACH provided file and suggest modifications to resolve the issue. - - Your response MUST be a JSON array of objects. Each object in the array MUST have two properties: - 'filePath' (the full path from the mappings provided.) and 'content' (the complete corrected content of that file). - DO NOT include any additional text, non modified files, commentary, or markdown outside the JSON array. - For example: - [ - {"filePath": "/full-path-from-mappings/file1.txt", "content": "Corrected content for file1."}, - {"filePath": "/full-path-from-mappings/file2.js", "content": "console.log('Fixed JS');"} - ] - - IMPORTANT: The input files are mapped as follows: ${Array.from(fileNameMap.entries())} -`; -} - -/** CLI command module. */ -export const FixModule: CommandModule<{}, Options> = { - builder, - handler, - command: 'fix ', - describe: 'Fixes errors from the specified error output', -}; diff --git a/ng-dev/ai/migrate.ts b/ng-dev/ai/migrate.ts deleted file mode 100644 index 5e37f20643..0000000000 --- a/ng-dev/ai/migrate.ts +++ /dev/null @@ -1,243 +0,0 @@ -/** - * @license - * Copyright Google LLC - * - * Use of this source code is governed by an MIT-style license that can be - * found in the LICENSE file at https://angular.io/license - */ - -import {GoogleGenAI} from '@google/genai'; -import {Argv, Arguments, CommandModule} from 'yargs'; -import {readFile, writeFile} from 'fs/promises'; -import {SingleBar, Presets} from 'cli-progress'; -import {DEFAULT_MODEL, DEFAULT_TEMPERATURE, DEFAULT_API_KEY} from './consts.js'; -import assert from 'node:assert'; -import {Log} from '../utils/logging.js'; -import glob from 'fast-glob'; - -/** Command line options. */ -export interface Options { - /** Prompt that should be applied. */ - prompt: string; - - /** Glob of files that the prompt should apply to. */ - files: string; - - /** Model that should be used to apply the prompt. */ - model: string; - - /** Temperature for the model. */ - temperature: number; - - /** Maximum number of concurrent API requests. */ - maxConcurrency: number; - - /** API key to use when making requests. */ - apiKey?: string; -} - -/** Yargs command builder for the command. */ -function builder(argv: Argv): Argv { - return argv - .option('prompt', { - type: 'string', - alias: 'p', - description: 'Path to the file containg the prompt that will be run', - demandOption: true, - }) - .option('files', { - type: 'string', - alias: 'f', - description: 'Glob for the files that should be migrated', - demandOption: true, - }) - .option('model', { - type: 'string', - alias: 'm', - description: 'Model to use for the migration', - default: DEFAULT_MODEL, - }) - .option('maxConcurrency', { - type: 'number', - default: 25, - description: - 'Maximum number of concurrent requests to the API. Higher numbers may hit usages limits', - }) - .option('temperature', { - type: 'number', - alias: 't', - default: DEFAULT_TEMPERATURE, - description: 'Temperature for the model. Lower temperature reduces randomness/creativity', - }) - .option('apiKey', { - type: 'string', - alias: 'a', - description: 'API key used when making calls to the Gemini API', - }); -} - -/** Yargs command handler for the command. */ -async function handler(options: Arguments) { - const apiKey = options.apiKey || DEFAULT_API_KEY; - - assert( - apiKey, - [ - 'No API key configured. A Gemini API key must be set as the `GEMINI_API_KEY` environment ' + - 'variable, or passed in using the `--api-key` flag.', - 'For internal users, see go/aistudio-apikey', - ].join('\n'), - ); - - const [files, prompt] = await Promise.all([ - glob([options.files]), - readFile(options.prompt, 'utf-8'), - ]); - - if (files.length === 0) { - Log.error(`No files matched the pattern "${options.files}"`); - process.exit(1); - } - - const ai = new GoogleGenAI({apiKey}); - const progressBar = new SingleBar({}, Presets.shades_grey); - const failures: {name: string; error: string}[] = []; - const running = new Set>(); - - Log.info( - [ - `Applying prompt from ${options.prompt} to ${files.length} files(s).`, - `Using model ${options.model} with a temperature of ${options.temperature}.`, - '', // Extra new line at the end. - ].join('\n'), - ); - progressBar.start(files.length, 0); - - // Kicks off the maximum number of concurrent requests and ensures that as many requests as - // possible are running at the same time. This is preferrable to chunking, because it allows - // the requests to keep running even if there's one which is taking a long time to resolve. - while (files.length > 0 || running.size > 0) { - // Fill up to maxConcurrency - while (files.length > 0 && running.size < options.maxConcurrency) { - const file = files.shift()!; - const task = processFile(file).finally(() => running.delete(task)); - running.add(task); - } - - // Wait for any task to finish - if (running.size > 0) { - await Promise.race(running); - } - } - - progressBar.stop(); - - for (const {name, error} of failures) { - Log.info('-------------------------------------'); - Log.info(`${name} failed to migrate:`); - Log.info(error); - } - - Log.info(`\nDone 🎉`); - - if (failures.length > 0) { - Log.info(`${failures.length} file(s) failed. See logs above for more information.`); - } - - async function processFile(file: string): Promise { - try { - const content = await readFile(file, 'utf-8'); - const result = await applyPrompt(ai, options.model, options.temperature, content, prompt); - await writeFile(file, result); - } catch (e) { - failures.push({name: file, error: (e as Error).toString()}); - } finally { - progressBar.increment(); - } - } -} - -/** - * Applies a prompt to a specific file's content. - * @param ai Instance of the GenAI SDK. - * @param model Model to use for the prompt. - * @param temperature Temperature for the promp. - * @param content Content of the file. - * @param prompt Prompt to be run. - */ -async function applyPrompt( - ai: GoogleGenAI, - model: string, - temperature: number, - content: string, - prompt: string, -): Promise { - // The schema ensures that the API returns a response in the format that we expect. - const responseSchema = { - type: 'object', - properties: { - content: {type: 'string', description: 'Changed content of the file'}, - }, - required: ['content'], - additionalProperties: false, - $schema: 'http://json-schema.org/draft-07/schema#', - }; - - // Note that technically we can batch multiple files into a single `generateContent` call. - // We don't do it, because it increases the risk that we'll hit the output token limit which - // can corrupt the entire response. This way one file failing won't break the entire run. - const response = await ai.models.generateContent({ - model, - contents: [{text: prompt}, {text: content}], - config: { - responseMimeType: 'application/json', - responseSchema, - temperature, - // We need as many output tokens as we can get. - maxOutputTokens: Infinity, - // We know that we'll only use one candidate so we can save some processing. - candidateCount: 1, - // Guide the LLM towards following our schema. - systemInstruction: - `Return output following the structured output schema. ` + - `Return an object containing the new contents of the changed file.`, - }, - }); - - const text = response.text; - - if (!text) { - throw new Error(`No response from the API. Response:\n` + JSON.stringify(response, null, 2)); - } - - let parsed: {content?: string}; - - try { - parsed = JSON.parse(text) as {content: string}; - } catch { - throw new Error( - 'Failed to parse result as JSON. This can happen if if maximum output ' + - 'token size has been reached. Try using a different model. ' + - 'Response:\n' + - JSON.stringify(response, null, 2), - ); - } - - if (!parsed.content) { - throw new Error( - 'Could not find content in parsed API response. This can indicate a problem ' + - 'with the request parameters. Parsed response:\n' + - JSON.stringify(parsed, null, 2), - ); - } - - return parsed.content; -} - -/** CLI command module. */ -export const MigrateModule: CommandModule<{}, Options> = { - builder, - handler, - command: 'migrate', - describe: 'Apply a prompt-based AI migration over a set of files', -}; diff --git a/ng-dev/cli.ts b/ng-dev/cli.ts index 3c95c09460..6786354c05 100644 --- a/ng-dev/cli.ts +++ b/ng-dev/cli.ts @@ -23,7 +23,6 @@ import {localVersion, ngDevVersionMiddleware} from './utils/version-check.js'; import {buildAuthParser} from './auth/cli.js'; import {buildPerfParser} from './perf/cli.js'; import {buildConfigParser} from './config/cli.js'; -import {buildAiParser} from './ai/cli.js'; import {Argv} from 'yargs'; runParserWithCompletedFunctions((yargs: Argv) => { @@ -44,7 +43,6 @@ runParserWithCompletedFunctions((yargs: Argv) => { .command('misc ', '', buildMiscParser) .command('ngbot ', false, buildNgbotParser) .command('perf ', '', buildPerfParser) - .command('ai ', '', buildAiParser) .command('config ', false, buildConfigParser) .version(localVersion) .wrap(120) diff --git a/ng-dev/misc/BUILD.bazel b/ng-dev/misc/BUILD.bazel index 6a7699de71..5c848684df 100644 --- a/ng-dev/misc/BUILD.bazel +++ b/ng-dev/misc/BUILD.bazel @@ -10,6 +10,7 @@ ts_project( "//ng-dev:node_modules/@types/yargs", "//ng-dev:node_modules/semver", "//ng-dev/format", + "//ng-dev/misc/validate-skills", "//ng-dev/release/build", "//ng-dev/release/config", "//ng-dev/utils", diff --git a/ng-dev/misc/cli.ts b/ng-dev/misc/cli.ts index 13f9e6ca5b..d0816541aa 100644 --- a/ng-dev/misc/cli.ts +++ b/ng-dev/misc/cli.ts @@ -10,6 +10,7 @@ import {Argv} from 'yargs'; import {SyncModuleBazelModule} from './sync-module-bazel/cli.js'; import {BuildAndLinkCommandModule} from './build-and-link/cli.js'; import {GeneratedFilesModule} from './generated-files/cli.js'; +import {ValidateSkillsModule} from './validate-skills/cli.js'; /** Build the parser for the misc commands. */ export function buildMiscParser(localYargs: Argv) { @@ -18,5 +19,6 @@ export function buildMiscParser(localYargs: Argv) { .strict() .command(SyncModuleBazelModule) .command(BuildAndLinkCommandModule) - .command(GeneratedFilesModule); + .command(GeneratedFilesModule) + .command(ValidateSkillsModule); } diff --git a/ng-dev/ai/skills/BUILD.bazel b/ng-dev/misc/validate-skills/BUILD.bazel similarity index 93% rename from ng-dev/ai/skills/BUILD.bazel rename to ng-dev/misc/validate-skills/BUILD.bazel index 6c4ae9326d..3976068a24 100644 --- a/ng-dev/ai/skills/BUILD.bazel +++ b/ng-dev/misc/validate-skills/BUILD.bazel @@ -1,7 +1,7 @@ load("//tools:defaults.bzl", "jasmine_test", "ts_project") ts_project( - name = "skills", + name = "validate-skills", srcs = glob( ["**/*.ts"], exclude = ["**/*.spec.ts"], @@ -23,7 +23,7 @@ ts_project( testonly = True, srcs = ["validate.spec.ts"], deps = [ - ":skills", + ":validate-skills", "//ng-dev:node_modules/@types/jasmine", "//ng-dev:node_modules/@types/node", "//ng-dev/utils", diff --git a/ng-dev/ai/skills/cli.ts b/ng-dev/misc/validate-skills/cli.ts similarity index 87% rename from ng-dev/ai/skills/cli.ts rename to ng-dev/misc/validate-skills/cli.ts index c2513b07a8..85439390bf 100644 --- a/ng-dev/ai/skills/cli.ts +++ b/ng-dev/misc/validate-skills/cli.ts @@ -15,7 +15,7 @@ interface Options { baseDir: string; } -async function builder(yargs: Argv) { +function builder(yargs: Argv) { return yargs.option('base-dir' as 'baseDir', { type: 'string', default: determineRepoBaseDirFromCwd(), @@ -31,8 +31,8 @@ async function handler({baseDir}: Arguments) { /** * Validates all skills found in the `skills/` directory. */ -export const SkillsModule: CommandModule<{}, Options> = { - command: 'skills validate', +export const ValidateSkillsModule: CommandModule<{}, Options> = { + command: 'validate-skills', describe: 'Validate agent skills in the repository', builder, handler, diff --git a/ng-dev/ai/skills/fixtures/complex-skill/skills/complex-skill/SKILL.md b/ng-dev/misc/validate-skills/fixtures/complex-skill/skills/complex-skill/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/complex-skill/skills/complex-skill/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/complex-skill/skills/complex-skill/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/invalid-frontmatter-location/skills/test-skill/SKILL.md b/ng-dev/misc/validate-skills/fixtures/invalid-frontmatter-location/skills/test-skill/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/invalid-frontmatter-location/skills/test-skill/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/invalid-frontmatter-location/skills/test-skill/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/invalid-name-format/skills/InvalidName/SKILL.md b/ng-dev/misc/validate-skills/fixtures/invalid-name-format/skills/InvalidName/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/invalid-name-format/skills/InvalidName/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/invalid-name-format/skills/InvalidName/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/invalid-schema/skills/bad-schema/SKILL.md b/ng-dev/misc/validate-skills/fixtures/invalid-schema/skills/bad-schema/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/invalid-schema/skills/bad-schema/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/invalid-schema/skills/bad-schema/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/missing-frontmatter/skills/bad-skill/SKILL.md b/ng-dev/misc/validate-skills/fixtures/missing-frontmatter/skills/bad-skill/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/missing-frontmatter/skills/bad-skill/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/missing-frontmatter/skills/bad-skill/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/multiple-mixed/skills/skill1/SKILL.md b/ng-dev/misc/validate-skills/fixtures/multiple-mixed/skills/skill1/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/multiple-mixed/skills/skill1/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/multiple-mixed/skills/skill1/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/multiple-mixed/skills/skill2/SKILL.md b/ng-dev/misc/validate-skills/fixtures/multiple-mixed/skills/skill2/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/multiple-mixed/skills/skill2/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/multiple-mixed/skills/skill2/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/multiple-valid/skills/skill1/SKILL.md b/ng-dev/misc/validate-skills/fixtures/multiple-valid/skills/skill1/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/multiple-valid/skills/skill1/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/multiple-valid/skills/skill1/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/multiple-valid/skills/skill2/SKILL.md b/ng-dev/misc/validate-skills/fixtures/multiple-valid/skills/skill2/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/multiple-valid/skills/skill2/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/multiple-valid/skills/skill2/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/name-mismatch/skills/wrong-name/SKILL.md b/ng-dev/misc/validate-skills/fixtures/name-mismatch/skills/wrong-name/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/name-mismatch/skills/wrong-name/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/name-mismatch/skills/wrong-name/SKILL.md diff --git a/ng-dev/ai/skills/fixtures/valid-skill/skills/test-skill/SKILL.md b/ng-dev/misc/validate-skills/fixtures/valid-skill/skills/test-skill/SKILL.md similarity index 100% rename from ng-dev/ai/skills/fixtures/valid-skill/skills/test-skill/SKILL.md rename to ng-dev/misc/validate-skills/fixtures/valid-skill/skills/test-skill/SKILL.md diff --git a/ng-dev/ai/skills/validate.spec.ts b/ng-dev/misc/validate-skills/validate.spec.ts similarity index 97% rename from ng-dev/ai/skills/validate.spec.ts rename to ng-dev/misc/validate-skills/validate.spec.ts index b68395ddcd..fa61d3c65d 100644 --- a/ng-dev/ai/skills/validate.spec.ts +++ b/ng-dev/misc/validate-skills/validate.spec.ts @@ -2,7 +2,7 @@ import {join} from 'path'; import {validateSkill, validateSkills} from './validate.js'; function getFixturePath(relativePath: string): string { - return join(process.cwd(), 'ng-dev/ai/skills/fixtures', relativePath); + return join(process.cwd(), 'ng-dev/misc/validate-skills/fixtures', relativePath); } describe('validateSkills', () => { diff --git a/ng-dev/ai/skills/validate.ts b/ng-dev/misc/validate-skills/validate.ts similarity index 100% rename from ng-dev/ai/skills/validate.ts rename to ng-dev/misc/validate-skills/validate.ts