diff --git a/packages/angular/cli/src/commands/mcp/cli.ts b/packages/angular/cli/src/commands/mcp/cli.ts index 9f8cfef91997..0076752ac6f3 100644 --- a/packages/angular/cli/src/commands/mcp/cli.ts +++ b/packages/angular/cli/src/commands/mcp/cli.ts @@ -7,8 +7,11 @@ */ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'; -import { Argv } from 'yargs'; -import { CommandModule, CommandModuleImplementation } from '../../command-builder/command-module'; +import type { Argv } from 'yargs'; +import { + CommandModule, + type CommandModuleImplementation, +} from '../../command-builder/command-module'; import { isTTY } from '../../utilities/tty'; import { EXPERIMENTAL_TOOLS, createMcpServer } from './mcp-server'; diff --git a/packages/angular/cli/src/commands/mcp/dev-server.ts b/packages/angular/cli/src/commands/mcp/dev-server.ts index 7645e6010abb..e6da33aa7bd3 100644 --- a/packages/angular/cli/src/commands/mcp/dev-server.ts +++ b/packages/angular/cli/src/commands/mcp/dev-server.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import { ChildProcess } from 'child_process'; -import { Host } from './host'; +import type { ChildProcess } from 'child_process'; +import type { Host } from './host'; // Log messages that we want to catch to identify the build status. diff --git a/packages/angular/cli/src/commands/mcp/mcp-server.ts b/packages/angular/cli/src/commands/mcp/mcp-server.ts index e4c2c799f4b5..dfcd162a44f7 100644 --- a/packages/angular/cli/src/commands/mcp/mcp-server.ts +++ b/packages/angular/cli/src/commands/mcp/mcp-server.ts @@ -7,10 +7,10 @@ */ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; -import path from 'node:path'; +import { join } from 'node:path'; import type { AngularWorkspace } from '../../utilities/config'; import { VERSION } from '../../utilities/version'; -import { DevServer } from './dev-server'; +import type { DevServer } from './dev-server'; import { registerInstructionsResource } from './resources/instructions'; import { AI_TUTOR_TOOL } from './tools/ai-tutor'; import { BEST_PRACTICES_TOOL } from './tools/best-practices'; @@ -23,7 +23,7 @@ import { FIND_EXAMPLE_TOOL } from './tools/examples'; import { MODERNIZE_TOOL } from './tools/modernize'; import { ZONELESS_MIGRATION_TOOL } from './tools/onpush-zoneless-migration/zoneless-migration'; import { LIST_PROJECTS_TOOL } from './tools/projects'; -import { AnyMcpToolDeclaration, registerTools } from './tools/tool-registry'; +import { type AnyMcpToolDeclaration, registerTools } from './tools/tool-registry'; /** * Tools to manage devservers. Should be bundled together, then added to experimental or stable as a group. @@ -113,7 +113,7 @@ equivalent actions. { workspace: options.workspace, logger, - exampleDatabasePath: path.join(__dirname, '../../../lib/code-examples.db'), + exampleDatabasePath: join(__dirname, '../../../lib/code-examples.db'), devServers: new Map(), }, toolDeclarations, diff --git a/packages/angular/cli/src/commands/mcp/resources/instructions.ts b/packages/angular/cli/src/commands/mcp/resources/instructions.ts index f2a0ac814b0a..bf85a3d54384 100644 --- a/packages/angular/cli/src/commands/mcp/resources/instructions.ts +++ b/packages/angular/cli/src/commands/mcp/resources/instructions.ts @@ -6,9 +6,9 @@ * found in the LICENSE file at https://angular.dev/license */ -import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; +import type { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'; import { readFile } from 'node:fs/promises'; -import path from 'node:path'; +import { join } from 'node:path'; export function registerInstructionsResource(server: McpServer): void { server.registerResource( @@ -24,7 +24,7 @@ export function registerInstructionsResource(server: McpServer): void { mimeType: 'text/markdown', }, async () => { - const text = await readFile(path.join(__dirname, 'best-practices.md'), 'utf-8'); + const text = await readFile(join(__dirname, 'best-practices.md'), 'utf-8'); return { contents: [{ uri: 'instructions://best-practices', text }] }; }, diff --git a/packages/angular/cli/src/commands/mcp/testing/mock-host.ts b/packages/angular/cli/src/commands/mcp/testing/mock-host.ts index 0a758a6925f5..1720b1377792 100644 --- a/packages/angular/cli/src/commands/mcp/testing/mock-host.ts +++ b/packages/angular/cli/src/commands/mcp/testing/mock-host.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import { Host } from '../host'; +import type { Host } from '../host'; /** * A mock implementation of the `Host` interface for testing purposes. diff --git a/packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts b/packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts index 2d1780b283f9..590d0940c747 100644 --- a/packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts +++ b/packages/angular/cli/src/commands/mcp/tools/ai-tutor.ts @@ -7,7 +7,7 @@ */ import { readFile } from 'node:fs/promises'; -import path from 'node:path'; +import { join } from 'node:path'; import { declareTool } from './tool-registry'; export const AI_TUTOR_TOOL = declareTool({ @@ -40,10 +40,7 @@ with a new core identity and knowledge base. let aiTutorText: string; return async () => { - aiTutorText ??= await readFile( - path.join(__dirname, '..', 'resources', 'ai-tutor.md'), - 'utf-8', - ); + aiTutorText ??= await readFile(join(__dirname, '../resources/ai-tutor.md'), 'utf-8'); return { content: [ diff --git a/packages/angular/cli/src/commands/mcp/tools/best-practices.ts b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts index 3245bbb5e5af..2c72d2175bac 100644 --- a/packages/angular/cli/src/commands/mcp/tools/best-practices.ts +++ b/packages/angular/cli/src/commands/mcp/tools/best-practices.ts @@ -17,10 +17,10 @@ import { readFile, stat } from 'node:fs/promises'; import { createRequire } from 'node:module'; -import path from 'node:path'; +import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import { z } from 'zod'; import { VERSION } from '../../../utilities/version'; -import { McpToolContext, declareTool } from './tool-registry'; +import { type McpToolContext, declareTool } from './tool-registry'; const bestPracticesInputSchema = z.object({ workspacePath: z @@ -72,7 +72,7 @@ that **MUST** be followed for any task involving the creation, analysis, or modi * @returns A promise that resolves to the string content of the bundled markdown file. */ async function getBundledBestPractices(): Promise { - return readFile(path.join(__dirname, '..', 'resources', 'best-practices.md'), 'utf-8'); + return readFile(join(__dirname, '../resources/best-practices.md'), 'utf-8'); } /** @@ -126,14 +126,14 @@ async function getVersionSpecificBestPractices( bestPracticesInfo.format === 'markdown' && typeof bestPracticesInfo.path === 'string' ) { - const packageDirectory = path.dirname(pkgJsonPath); - const guidePath = path.resolve(packageDirectory, bestPracticesInfo.path); + const packageDirectory = dirname(pkgJsonPath); + const guidePath = resolve(packageDirectory, bestPracticesInfo.path); // Ensure the resolved guide path is within the package boundary. // Uses path.relative to create a cross-platform, case-insensitive check. // If the relative path starts with '..' or is absolute, it is a traversal attempt. - const relativePath = path.relative(packageDirectory, guidePath); - if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + const relativePath = relative(packageDirectory, guidePath); + if (relativePath.startsWith('..') || isAbsolute(relativePath)) { logger.warn( `Detected a potential path traversal attempt in '${pkgJsonPath}'. ` + `The path '${bestPracticesInfo.path}' escapes the package boundary. ` + diff --git a/packages/angular/cli/src/commands/mcp/tools/build.ts b/packages/angular/cli/src/commands/mcp/tools/build.ts index 75812460bd22..f2618f8e6309 100644 --- a/packages/angular/cli/src/commands/mcp/tools/build.ts +++ b/packages/angular/cli/src/commands/mcp/tools/build.ts @@ -7,9 +7,9 @@ */ import { z } from 'zod'; -import { CommandError, Host, LocalWorkspaceHost } from '../host'; +import { CommandError, type Host, LocalWorkspaceHost } from '../host'; import { createStructuredContentOutput } from '../utils'; -import { McpToolDeclaration, declareTool } from './tool-registry'; +import { type McpToolDeclaration, declareTool } from './tool-registry'; const DEFAULT_CONFIGURATION = 'development'; diff --git a/packages/angular/cli/src/commands/mcp/tools/build_spec.ts b/packages/angular/cli/src/commands/mcp/tools/build_spec.ts index 20678501b977..4ad98e0456b9 100644 --- a/packages/angular/cli/src/commands/mcp/tools/build_spec.ts +++ b/packages/angular/cli/src/commands/mcp/tools/build_spec.ts @@ -7,7 +7,7 @@ */ import { CommandError, Host } from '../host'; -import { MockHost } from '../testing/mock-host'; +import type { MockHost } from '../testing/mock-host'; import { runBuild } from './build'; describe('Build Tool', () => { @@ -18,7 +18,7 @@ describe('Build Tool', () => { runCommand: jasmine.createSpy('runCommand').and.resolveTo({ logs: [] }), stat: jasmine.createSpy('stat'), existsSync: jasmine.createSpy('existsSync'), - } as Partial as MockHost; + } as MockHost; }); it('should construct the command correctly with default configuration', async () => { diff --git a/packages/angular/cli/src/commands/mcp/tools/devserver/serve_spec.ts b/packages/angular/cli/src/commands/mcp/tools/devserver/serve_spec.ts index 5b3116125223..7b9ce0f10a08 100644 --- a/packages/angular/cli/src/commands/mcp/tools/devserver/serve_spec.ts +++ b/packages/angular/cli/src/commands/mcp/tools/devserver/serve_spec.ts @@ -7,9 +7,9 @@ */ import { EventEmitter } from 'events'; -import { ChildProcess } from 'node:child_process'; -import { MockHost } from '../../testing/mock-host'; -import { McpToolContext } from '../tool-registry'; +import type { ChildProcess } from 'node:child_process'; +import type { MockHost } from '../../testing/mock-host'; +import type { McpToolContext } from '../tool-registry'; import { startDevServer } from './start-devserver'; import { stopDevserver } from './stop-devserver'; import { WATCH_DELAY, waitForDevserverBuild } from './wait-for-devserver-build'; @@ -34,7 +34,7 @@ describe('Serve Tools', () => { getAvailablePort: jasmine.createSpy('getAvailablePort').and.callFake(() => { return Promise.resolve(portCounter++); }), - } as Partial as MockHost; + } as MockHost; mockContext = { devServers: new Map(), diff --git a/packages/angular/cli/src/commands/mcp/tools/devserver/start-devserver.ts b/packages/angular/cli/src/commands/mcp/tools/devserver/start-devserver.ts index abc6a8cdfa33..dbbc4dbd3cfd 100644 --- a/packages/angular/cli/src/commands/mcp/tools/devserver/start-devserver.ts +++ b/packages/angular/cli/src/commands/mcp/tools/devserver/start-devserver.ts @@ -8,9 +8,9 @@ import { z } from 'zod'; import { LocalDevServer, devServerKey } from '../../dev-server'; -import { Host, LocalWorkspaceHost } from '../../host'; +import { type Host, LocalWorkspaceHost } from '../../host'; import { createStructuredContentOutput } from '../../utils'; -import { McpToolContext, McpToolDeclaration, declareTool } from '../tool-registry'; +import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry'; const startDevServerToolInputSchema = z.object({ project: z diff --git a/packages/angular/cli/src/commands/mcp/tools/devserver/stop-devserver.ts b/packages/angular/cli/src/commands/mcp/tools/devserver/stop-devserver.ts index 842910e6cac0..ed33ff3f0f7d 100644 --- a/packages/angular/cli/src/commands/mcp/tools/devserver/stop-devserver.ts +++ b/packages/angular/cli/src/commands/mcp/tools/devserver/stop-devserver.ts @@ -9,7 +9,7 @@ import { z } from 'zod'; import { devServerKey } from '../../dev-server'; import { createStructuredContentOutput } from '../../utils'; -import { McpToolContext, McpToolDeclaration, declareTool } from '../tool-registry'; +import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry'; const stopDevserverToolInputSchema = z.object({ project: z diff --git a/packages/angular/cli/src/commands/mcp/tools/devserver/wait-for-devserver-build.ts b/packages/angular/cli/src/commands/mcp/tools/devserver/wait-for-devserver-build.ts index 0d698c8f452a..1a002f4c5b2a 100644 --- a/packages/angular/cli/src/commands/mcp/tools/devserver/wait-for-devserver-build.ts +++ b/packages/angular/cli/src/commands/mcp/tools/devserver/wait-for-devserver-build.ts @@ -9,7 +9,7 @@ import { z } from 'zod'; import { devServerKey } from '../../dev-server'; import { createStructuredContentOutput } from '../../utils'; -import { McpToolContext, McpToolDeclaration, declareTool } from '../tool-registry'; +import { type McpToolContext, type McpToolDeclaration, declareTool } from '../tool-registry'; /** * How long to wait to give "ng serve" time to identify whether the watched workspace has changed. diff --git a/packages/angular/cli/src/commands/mcp/tools/doc-search.ts b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts index dbf80794cc26..7d0f1bd92ab5 100644 --- a/packages/angular/cli/src/commands/mcp/tools/doc-search.ts +++ b/packages/angular/cli/src/commands/mcp/tools/doc-search.ts @@ -11,7 +11,7 @@ import { createDecipheriv } from 'node:crypto'; import { Readable } from 'node:stream'; import { z } from 'zod'; import { at, iv, k1 } from '../constants'; -import { McpToolContext, declareTool } from './tool-registry'; +import { type McpToolContext, declareTool } from './tool-registry'; const ALGOLIA_APP_ID = 'L1XWT2UJ7F'; // https://www.algolia.com/doc/guides/security/api-keys/#search-only-api-key diff --git a/packages/angular/cli/src/commands/mcp/tools/examples.ts b/packages/angular/cli/src/commands/mcp/tools/examples.ts index 8866761017a6..709bbebfc6ae 100644 --- a/packages/angular/cli/src/commands/mcp/tools/examples.ts +++ b/packages/angular/cli/src/commands/mcp/tools/examples.ts @@ -8,10 +8,10 @@ import { glob, readFile, stat } from 'node:fs/promises'; import { createRequire } from 'node:module'; -import path from 'node:path'; +import { dirname, isAbsolute, join, relative, resolve } from 'node:path'; import type { DatabaseSync, SQLInputValue } from 'node:sqlite'; import { z } from 'zod'; -import { McpToolContext, declareTool } from './tool-registry'; +import { type McpToolContext, declareTool } from './tool-registry'; const findExampleInputSchema = z.object({ workspacePath: z @@ -246,12 +246,12 @@ async function getVersionSpecificExampleDatabase( const examplesInfo = pkgJson['angular']?.examples; if (examplesInfo && examplesInfo.format === 'sqlite' && typeof examplesInfo.path === 'string') { - const packageDirectory = path.dirname(pkgJsonPath); - const dbPath = path.resolve(packageDirectory, examplesInfo.path); + const packageDirectory = dirname(pkgJsonPath); + const dbPath = resolve(packageDirectory, examplesInfo.path); // Ensure the resolved database path is within the package boundary. - const relativePath = path.relative(packageDirectory, dbPath); - if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) { + const relativePath = relative(packageDirectory, dbPath); + if (relativePath.startsWith('..') || isAbsolute(relativePath)) { logger.warn( `Detected a potential path traversal attempt in '${pkgJsonPath}'. ` + `The path '${examplesInfo.path}' escapes the package boundary. ` + @@ -634,7 +634,7 @@ async function setupRuntimeExamples(examplesPath: string): Promise continue; } - const content = await readFile(path.join(entry.parentPath, entry.name), 'utf-8'); + const content = await readFile(join(entry.parentPath, entry.name), 'utf-8'); const frontmatter = parseFrontmatter(content); const validation = frontmatterSchema.safeParse(frontmatter); diff --git a/packages/angular/cli/src/commands/mcp/tools/modernize.ts b/packages/angular/cli/src/commands/mcp/tools/modernize.ts index fc9b1d6cc45a..754bb683bdc0 100644 --- a/packages/angular/cli/src/commands/mcp/tools/modernize.ts +++ b/packages/angular/cli/src/commands/mcp/tools/modernize.ts @@ -8,9 +8,9 @@ import { dirname, join, relative } from 'path'; import { z } from 'zod'; -import { CommandError, Host, LocalWorkspaceHost } from '../host'; -import { createStructuredContentOutput } from '../utils'; -import { McpToolDeclaration, declareTool } from './tool-registry'; +import { CommandError, type Host, LocalWorkspaceHost } from '../host'; +import { createStructuredContentOutput, findAngularJsonDir } from '../utils'; +import { type McpToolDeclaration, declareTool } from './tool-registry'; interface Transformation { name: string; @@ -93,20 +93,6 @@ const modernizeOutputSchema = z.object({ export type ModernizeInput = z.infer; export type ModernizeOutput = z.infer; -function findAngularJsonDir(startDir: string, host: Host): string | null { - let currentDir = startDir; - while (true) { - if (host.existsSync(join(currentDir, 'angular.json'))) { - return currentDir; - } - const parentDir = dirname(currentDir); - if (parentDir === currentDir) { - return null; - } - currentDir = parentDir; - } -} - export async function runModernization(input: ModernizeInput, host: Host) { const transformationNames = input.transformations ?? []; const directories = input.directories ?? []; diff --git a/packages/angular/cli/src/commands/mcp/tools/modernize_spec.ts b/packages/angular/cli/src/commands/mcp/tools/modernize_spec.ts index 13b0e55f6946..82f0c70e11d3 100644 --- a/packages/angular/cli/src/commands/mcp/tools/modernize_spec.ts +++ b/packages/angular/cli/src/commands/mcp/tools/modernize_spec.ts @@ -11,8 +11,8 @@ import { mkdir, mkdtemp, rm, writeFile } from 'fs/promises'; import { tmpdir } from 'os'; import { join } from 'path'; import { CommandError } from '../host'; -import { MockHost } from '../testing/mock-host'; -import { ModernizeOutput, runModernization } from './modernize'; +import type { MockHost } from '../testing/mock-host'; +import { type ModernizeOutput, runModernization } from './modernize'; describe('Modernize Tool', () => { let projectDir: string; @@ -29,7 +29,7 @@ describe('Modernize Tool', () => { existsSync: jasmine.createSpy('existsSync').and.callFake((p: string) => { return p === join(projectDir, 'angular.json'); }), - } as Partial as MockHost; + } as MockHost; }); afterEach(async () => { diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/analyze-for-unsupported-zone-uses.ts similarity index 95% rename from packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.ts rename to packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/analyze-for-unsupported-zone-uses.ts index dd3d848e8883..a523b43145eb 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/analyze_for_unsupported_zone_uses.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/analyze-for-unsupported-zone-uses.ts @@ -8,8 +8,8 @@ import type { ImportSpecifier, Node, SourceFile } from 'typescript'; import { createUnsupportedZoneUsagesMessage } from './prompts'; -import { getImportSpecifier, loadTypescript } from './ts_utils'; -import { MigrationResponse } from './types'; +import { getImportSpecifier, loadTypescript } from './ts-utils'; +import type { MigrationResponse } from './types'; export async function analyzeForUnsupportedZoneUses( sourceFile: SourceFile, diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file.ts similarity index 86% rename from packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.ts rename to packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file.ts index 757da8883505..1a1bc2c58f34 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file.ts @@ -6,15 +6,15 @@ * found in the LICENSE file at https://angular.dev/license */ -import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; -import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; +import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; +import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; import type { SourceFile } from 'typescript'; -import { analyzeForUnsupportedZoneUses } from './analyze_for_unsupported_zone_uses'; -import { migrateTestFile } from './migrate_test_file'; +import { analyzeForUnsupportedZoneUses } from './analyze-for-unsupported-zone-uses'; +import { migrateTestFile } from './migrate-test-file'; import { generateZonelessMigrationInstructionsForComponent } from './prompts'; -import { sendDebugMessage } from './send_debug_message'; -import { getImportSpecifier, loadTypescript } from './ts_utils'; -import { MigrationResponse } from './types'; +import { sendDebugMessage } from './send-debug-message'; +import { getImportSpecifier, loadTypescript } from './ts-utils'; +import type { MigrationResponse } from './types'; export async function migrateSingleFile( sourceFile: SourceFile, diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file_spec.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file_spec.ts similarity index 95% rename from packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file_spec.ts rename to packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file_spec.ts index da2f59db0182..20ed43626639 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate_single_file_spec.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-single-file_spec.ts @@ -6,10 +6,10 @@ * found in the LICENSE file at https://angular.dev/license */ -import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; -import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; +import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; +import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; import ts from 'typescript'; -import { migrateSingleFile } from './migrate_single_file'; +import { migrateSingleFile } from './migrate-single-file'; const fakeExtras = { sendDebugMessage: jasmine.createSpy(), diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-test-file.ts similarity index 82% rename from packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.ts rename to packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-test-file.ts index 479251c428a8..bae1762282a1 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate_test_file.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/migrate-test-file.ts @@ -6,12 +6,13 @@ * found in the LICENSE file at https://angular.dev/license */ -import * as fs from 'node:fs'; +import { existsSync, readFileSync } from 'node:fs'; import { glob } from 'node:fs/promises'; import { dirname, join } from 'node:path'; import type { SourceFile } from 'typescript'; +import { findAngularJsonDir } from '../../utils'; import { createFixResponseForZoneTests, createProvideZonelessForTestsSetupPrompt } from './prompts'; -import { loadTypescript } from './ts_utils'; +import { loadTypescript } from './ts-utils'; import { MigrationResponse } from './types'; export async function migrateTestFile(sourceFile: SourceFile): Promise { @@ -52,7 +53,7 @@ export async function searchForGlobalZoneless(startPath: string): Promise { it('should return setup prompt when zoneless is not detected', async () => { diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/prompts.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/prompts.ts index b01dd5bdee94..f4eba63ceb4c 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/prompts.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/prompts.ts @@ -7,8 +7,8 @@ */ import type { Node, SourceFile } from 'typescript'; -import { loadTypescript } from './ts_utils'; -import { MigrationResponse } from './types'; +import { loadTypescript } from './ts-utils'; +import type { MigrationResponse } from './types'; /* eslint-disable max-len */ @@ -31,7 +31,7 @@ export function createProvideZonelessForTestsSetupPrompt(testFilePath: string): \`\`\`diff - import {{ SomeImport }} from '@angular/core'; + import {{ SomeImport, provideZonelessChangeDetection }} from '@angular/core'; - + describe('MyComponent', () => { + beforeEach(() => { + TestBed.configureTestingModule({providers: [provideZonelessChangeDetection()]}); diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send-debug-message.ts similarity index 73% rename from packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.ts rename to packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send-debug-message.ts index 73a1b068a698..b2f5436d497a 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send_debug_message.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/send-debug-message.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.dev/license */ -import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; -import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; +import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; +import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; export function sendDebugMessage( message: string, diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/ts-utils.ts similarity index 90% rename from packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.ts rename to packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/ts-utils.ts index 72764d648b88..08a3f6f0dcfd 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/ts_utils.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/ts-utils.ts @@ -6,8 +6,7 @@ * found in the LICENSE file at https://angular.dev/license */ -import * as fs from 'node:fs'; -import type { ImportSpecifier, NodeArray, SourceFile } from 'typescript'; +import { readFileSync } from 'node:fs'; import type ts from 'typescript'; let typescriptModule: typeof ts; @@ -34,10 +33,10 @@ export async function loadTypescript(): Promise { * their original name. */ export async function getImportSpecifier( - sourceFile: SourceFile, + sourceFile: ts.SourceFile, moduleName: string | RegExp, specifierName: string, -): Promise { +): Promise { return ( getImportSpecifiers(sourceFile, moduleName, specifierName, await loadTypescript())[0] ?? null ); @@ -61,12 +60,12 @@ export async function getImportSpecifier( * names. Aliases will be resolved to their original name. */ function getImportSpecifiers( - sourceFile: SourceFile, + sourceFile: ts.SourceFile, moduleName: string | RegExp, specifierOrSpecifiers: string | string[], { isNamedImports, isImportDeclaration, isStringLiteral }: typeof ts, -): ImportSpecifier[] { - const matches: ImportSpecifier[] = []; +): ts.ImportSpecifier[] { + const matches: ts.ImportSpecifier[] = []; for (const node of sourceFile.statements) { if (!isImportDeclaration(node) || !isStringLiteral(node.moduleSpecifier)) { continue; @@ -106,9 +105,9 @@ function getImportSpecifiers( * @param specifierName Name of the specifier to look for. */ export function findImportSpecifier( - nodes: NodeArray, + nodes: ts.NodeArray, specifierName: string, -): ImportSpecifier | undefined { +): ts.ImportSpecifier | undefined { return nodes.find((element) => { const { name, propertyName } = element; @@ -118,7 +117,7 @@ export function findImportSpecifier( /** Creates a TypeScript source file from a file path. */ export async function createSourceFile(file: string) { - const content = fs.readFileSync(file, 'utf8'); + const content = readFileSync(file, 'utf8'); const ts = await loadTypescript(); diff --git a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts index eaca30e274d9..bb2b1574d294 100644 --- a/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts +++ b/packages/angular/cli/src/commands/mcp/tools/onpush-zoneless-migration/zoneless-migration.ts @@ -6,19 +6,19 @@ * found in the LICENSE file at https://angular.dev/license */ -import { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; -import { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; -import * as fs from 'node:fs'; +import type { RequestHandlerExtra } from '@modelcontextprotocol/sdk/shared/protocol'; +import type { ServerNotification, ServerRequest } from '@modelcontextprotocol/sdk/types'; +import { existsSync, statSync } from 'node:fs'; import { glob } from 'node:fs/promises'; -import { type SourceFile } from 'typescript'; +import type { SourceFile } from 'typescript'; import { z } from 'zod'; import { declareTool } from '../tool-registry'; -import { analyzeForUnsupportedZoneUses } from './analyze_for_unsupported_zone_uses'; -import { migrateSingleFile } from './migrate_single_file'; -import { migrateTestFile } from './migrate_test_file'; +import { analyzeForUnsupportedZoneUses } from './analyze-for-unsupported-zone-uses'; +import { migrateSingleFile } from './migrate-single-file'; +import { migrateTestFile } from './migrate-test-file'; import { createResponse, createTestDebuggingGuideForNonActionableInput } from './prompts'; -import { sendDebugMessage } from './send_debug_message'; -import { createSourceFile, getImportSpecifier } from './ts_utils'; +import { sendDebugMessage } from './send-debug-message'; +import { createSourceFile, getImportSpecifier } from './ts-utils'; export const ZONELESS_MIGRATION_TOOL = declareTool({ name: 'onpush-zoneless-migration', @@ -127,7 +127,7 @@ async function discoverAndCategorizeFiles( let isDirectory: boolean; try { - isDirectory = fs.statSync(fileOrDirPath).isDirectory(); + isDirectory = statSync(fileOrDirPath).isDirectory(); } catch (e) { // Re-throw to be handled by the main function as a user input error throw new Error(`Failed to access path: ${fileOrDirPath}`); @@ -232,7 +232,7 @@ async function rankComponentFilesForMigration( async function getTestFilePath(filePath: string): Promise { const testFilePath = filePath.replace(/\.ts$/, '.spec.ts'); - if (fs.existsSync(testFilePath)) { + if (existsSync(testFilePath)) { return testFilePath; } diff --git a/packages/angular/cli/src/commands/mcp/tools/projects.ts b/packages/angular/cli/src/commands/mcp/tools/projects.ts index e3990b1b74c5..e6abff0ca418 100644 --- a/packages/angular/cli/src/commands/mcp/tools/projects.ts +++ b/packages/angular/cli/src/commands/mcp/tools/projects.ts @@ -7,13 +7,13 @@ */ import { readFile, readdir, stat } from 'node:fs/promises'; -import path from 'node:path'; +import { dirname, extname, join, normalize, posix, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import semver from 'semver'; -import z from 'zod'; +import { z } from 'zod'; import { AngularWorkspace } from '../../../utilities/config'; import { assertIsError } from '../../../utilities/error'; -import { McpToolContext, declareTool } from './tool-registry'; +import { type McpToolContext, declareTool } from './tool-registry'; // Single source of truth for what constitutes a valid style language. const styleLanguageSchema = z.enum(['css', 'scss', 'sass', 'less']); @@ -181,7 +181,7 @@ async function* findAngularJsonFiles(rootDir: string): AsyncGenerator { const entries = await readdir(dir, { withFileTypes: true }); const subdirectories: string[] = []; for (const entry of entries) { - const fullPath = path.join(dir, entry.name); + const fullPath = join(dir, entry.name); if (entry.isDirectory()) { // Exclude dot-directories, build/cache directories, and node_modules if (entry.name.startsWith('.') || EXCLUDED_DIRS.has(entry.name)) { @@ -251,7 +251,7 @@ async function findAngularCoreVersion( return cachedResult; } - const pkgPath = path.join(currentDir, 'package.json'); + const pkgPath = join(currentDir, 'package.json'); try { const pkgContent = await readFile(pkgPath, 'utf-8'); const pkg = JSON.parse(pkgContent); @@ -279,7 +279,7 @@ async function findAngularCoreVersion( if (currentDir === searchRoot) { break; } - const parentDir = path.dirname(currentDir); + const parentDir = dirname(currentDir); if (parentDir === currentDir) { break; // Reached the filesystem root. } @@ -388,7 +388,7 @@ async function getProjectStyleLanguage( const styles = buildTarget.options['styles'] as string[] | undefined; if (Array.isArray(styles)) { for (const stylePath of styles) { - const style = getStyleLanguageFromExtension(path.extname(stylePath)); + const style = getStyleLanguageFromExtension(extname(stylePath)); if (style) { return style; } @@ -399,7 +399,7 @@ async function getProjectStyleLanguage( // 5. Infer from implicit default styles file (future-proofing). for (const ext of STYLE_LANGUAGE_SEARCH_ORDER) { try { - await stat(path.join(fullSourceRoot, `styles.${ext}`)); + await stat(join(fullSourceRoot, `styles.${ext}`)); return ext; } catch { @@ -424,7 +424,7 @@ async function loadAndParseWorkspace( seenPaths: Set, ): Promise<{ workspace: WorkspaceData | null; error: ParsingError | null }> { try { - const resolvedPath = path.resolve(configFile); + const resolvedPath = resolve(configFile); if (seenPaths.has(resolvedPath)) { return { workspace: null, error: null }; // Already processed, skip. } @@ -432,10 +432,10 @@ async function loadAndParseWorkspace( const ws = await AngularWorkspace.load(configFile); const projects = []; - const workspaceRoot = path.dirname(configFile); + const workspaceRoot = dirname(configFile); for (const [name, project] of ws.projects.entries()) { - const sourceRoot = path.posix.join(project.root, project.sourceRoot ?? 'src'); - const fullSourceRoot = path.join(workspaceRoot, sourceRoot); + const sourceRoot = posix.join(project.root, project.sourceRoot ?? 'src'); + const fullSourceRoot = join(workspaceRoot, sourceRoot); const unitTestFramework = getUnitTestFramework(project.targets.get('test')); const styleLanguage = await getProjectStyleLanguage(project, ws, fullSourceRoot); @@ -492,7 +492,7 @@ async function processConfigFile( } try { - const workspaceDir = path.dirname(configFile); + const workspaceDir = dirname(configFile); workspace.frameworkVersion = await findAngularCoreVersion( workspaceDir, versionCache, @@ -523,7 +523,7 @@ async function createListProjectsHandler({ server }: McpToolContext) { const clientCapabilities = server.server.getClientCapabilities(); if (clientCapabilities?.roots) { const { roots } = await server.server.listRoots(); - searchRoots = roots?.map((r) => path.normalize(fileURLToPath(r.uri))) ?? []; + searchRoots = roots?.map((r) => normalize(fileURLToPath(r.uri))) ?? []; } else { // Fallback to the current working directory if client does not support roots searchRoots = [process.cwd()]; diff --git a/packages/angular/cli/src/commands/mcp/tools/tool-registry.ts b/packages/angular/cli/src/commands/mcp/tools/tool-registry.ts index a70d4185dd81..9bbce768000b 100644 --- a/packages/angular/cli/src/commands/mcp/tools/tool-registry.ts +++ b/packages/angular/cli/src/commands/mcp/tools/tool-registry.ts @@ -7,9 +7,9 @@ */ import type { McpServer, ToolCallback } from '@modelcontextprotocol/sdk/server/mcp.js'; -import { ZodRawShape } from 'zod'; +import type { ZodRawShape } from 'zod'; import type { AngularWorkspace } from '../../../utilities/config'; -import { DevServer } from '../dev-server'; +import type { DevServer } from '../dev-server'; type ToolConfig = Parameters[1]; diff --git a/packages/angular/cli/src/commands/mcp/utils.ts b/packages/angular/cli/src/commands/mcp/utils.ts index 49fa697ceca8..f5fdd70ef40e 100644 --- a/packages/angular/cli/src/commands/mcp/utils.ts +++ b/packages/angular/cli/src/commands/mcp/utils.ts @@ -11,6 +11,9 @@ * Utility functions shared across MCP tools. */ +import { dirname, join } from 'node:path'; +import { LocalWorkspaceHost } from './host'; + /** * Returns simple structured content output from an MCP tool. * @@ -22,3 +25,30 @@ export function createStructuredContentOutput(structuredContent: Out structuredContent, }; } + +/** + * Searches for an angular.json file by traversing up the directory tree from a starting directory. + * + * @param startDir The directory path to start searching from + * @param host The workspace host instance used to check file existence. Defaults to LocalWorkspaceHost + * @returns The absolute path to the directory containing angular.json, or null if not found + * + * @remarks + * This function performs an upward directory traversal starting from `startDir`. + * It checks each directory for the presence of an angular.json file until either: + * - The file is found (returns the directory path) + * - The root of the filesystem is reached (returns null) + */ +export function findAngularJsonDir(startDir: string, host = LocalWorkspaceHost): string | null { + let currentDir = startDir; + while (true) { + if (host.existsSync(join(currentDir, 'angular.json'))) { + return currentDir; + } + const parentDir = dirname(currentDir); + if (parentDir === currentDir) { + return null; + } + currentDir = parentDir; + } +} diff --git a/packages/angular_devkit/schematics_cli/test/schematics.spec.ts b/packages/angular_devkit/schematics_cli/test/schematics_spec.ts similarity index 100% rename from packages/angular_devkit/schematics_cli/test/schematics.spec.ts rename to packages/angular_devkit/schematics_cli/test/schematics_spec.ts