@angular/cli 21.0.0-next.2 → 21.0.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Binary file
@@ -4304,23 +4304,23 @@
4304
4304
  "properties": {
4305
4305
  "buildTarget": {
4306
4306
  "type": "string",
4307
- "description": "A build builder target to serve in the format of `project:target[:configuration]`. You can also pass in more than one configuration name as a comma-separated list. Example: `project:target:production,staging`.",
4307
+ "description": "Specifies the build target to use for the unit test build in the format `project:target[:configuration]`. You can also pass a comma-separated list of configurations. Example: `project:target:production,staging`.",
4308
4308
  "pattern": "^[^:\\s]*:[^:\\s]*(:[^\\s]+)?$"
4309
4309
  },
4310
4310
  "tsConfig": {
4311
4311
  "type": "string",
4312
- "description": "The name of the TypeScript configuration file."
4312
+ "description": "The path to the TypeScript configuration file, relative to the workspace root."
4313
4313
  },
4314
4314
  "runner": {
4315
4315
  "type": "string",
4316
- "description": "The name of the test runner to use for test execution.",
4316
+ "description": "Specifies the test runner to use for test execution.",
4317
4317
  "enum": [
4318
4318
  "karma",
4319
4319
  "vitest"
4320
4320
  ]
4321
4321
  },
4322
4322
  "browsers": {
4323
- "description": "A list of browsers to use for test execution. If undefined, jsdom on Node.js will be used instead of a browser. For Vitest and Karma, browser names ending with 'Headless' (e.g., 'ChromeHeadless') will enable headless mode for that browser.",
4323
+ "description": "Specifies the browsers to use for test execution. When not specified, tests are run in a Node.js environment using jsdom. For both Vitest and Karma, browser names ending with 'Headless' (e.g., 'ChromeHeadless') will enable headless mode.",
4324
4324
  "type": "array",
4325
4325
  "items": {
4326
4326
  "type": "string"
@@ -4335,41 +4335,43 @@
4335
4335
  "default": [
4336
4336
  "**/*.spec.ts"
4337
4337
  ],
4338
- "description": "Globs of files to include, relative to project root. \nThere are 2 special cases:\n - when a path to directory is provided, all spec files ending \".spec.@(ts|tsx)\" will be included\n - when a path to a file is provided, and a matching spec file exists it will be included instead."
4338
+ "description": "Specifies glob patterns of files to include for testing, relative to the project root. This option also has special handling for directory paths (includes all `.spec.ts` files within) and file paths (includes the corresponding `.spec` file if one exists)."
4339
4339
  },
4340
4340
  "exclude": {
4341
4341
  "type": "array",
4342
4342
  "items": {
4343
4343
  "type": "string"
4344
4344
  },
4345
- "default": [],
4346
- "description": "Globs of files to exclude, relative to the project root."
4345
+ "description": "Specifies glob patterns of files to exclude from testing, relative to the project root."
4346
+ },
4347
+ "filter": {
4348
+ "type": "string",
4349
+ "description": "Specifies a regular expression pattern to match against test suite and test names. Only tests with a name matching the pattern will be executed. For example, `^App` will run only tests in suites beginning with 'App'."
4347
4350
  },
4348
4351
  "watch": {
4349
4352
  "type": "boolean",
4350
- "description": "Re-run tests when source files change. Defaults to `true` in TTY environments and `false` otherwise."
4353
+ "description": "Enables watch mode, which re-runs tests when source files change. Defaults to `true` in TTY environments and `false` otherwise."
4351
4354
  },
4352
4355
  "debug": {
4353
4356
  "type": "boolean",
4354
- "description": "Initialize the test runner to support using the Node Inspector for test debugging.",
4357
+ "description": "Enables debugging mode for tests, allowing the use of the Node Inspector.",
4355
4358
  "default": false
4356
4359
  },
4357
4360
  "codeCoverage": {
4358
4361
  "type": "boolean",
4359
- "description": "Output a code coverage report.",
4362
+ "description": "Enables code coverage reporting for tests.",
4360
4363
  "default": false
4361
4364
  },
4362
4365
  "codeCoverageExclude": {
4363
4366
  "type": "array",
4364
- "description": "Globs to exclude from code coverage.",
4367
+ "description": "Specifies glob patterns of files to exclude from the code coverage report.",
4365
4368
  "items": {
4366
4369
  "type": "string"
4367
- },
4368
- "default": []
4370
+ }
4369
4371
  },
4370
4372
  "codeCoverageReporters": {
4371
4373
  "type": "array",
4372
- "description": "Reporters to use for code coverage results.",
4374
+ "description": "Specifies the reporters to use for code coverage results. Each reporter can be a string representing its name, or a tuple containing the name and an options object. Built-in reporters include 'html', 'lcov', 'lcovonly', 'text', 'text-summary', 'cobertura', 'json', and 'json-summary'.",
4373
4375
  "items": {
4374
4376
  "oneOf": [
4375
4377
  {
@@ -4393,14 +4395,49 @@
4393
4395
  },
4394
4396
  "reporters": {
4395
4397
  "type": "array",
4396
- "description": "Test runner reporters to use. Directly passed to the test runner.",
4398
+ "description": "Specifies the reporters to use during test execution. Each reporter can be a string representing its name, or a tuple containing the name and an options object. Built-in reporters include 'default', 'verbose', 'dots', 'json', 'junit', 'tap', 'tap-flat', and 'html'. You can also provide a path to a custom reporter.",
4397
4399
  "items": {
4398
- "type": "string"
4400
+ "oneOf": [
4401
+ {
4402
+ "anyOf": [
4403
+ {
4404
+ "$ref": "#/definitions/AngularBuildBuildersUnitTestSchema/definitions/reporters-enum"
4405
+ },
4406
+ {
4407
+ "type": "string"
4408
+ }
4409
+ ]
4410
+ },
4411
+ {
4412
+ "type": "array",
4413
+ "minItems": 1,
4414
+ "maxItems": 2,
4415
+ "items": [
4416
+ {
4417
+ "anyOf": [
4418
+ {
4419
+ "$ref": "#/definitions/AngularBuildBuildersUnitTestSchema/definitions/reporters-enum"
4420
+ },
4421
+ {
4422
+ "type": "string"
4423
+ }
4424
+ ]
4425
+ },
4426
+ {
4427
+ "type": "object"
4428
+ }
4429
+ ]
4430
+ }
4431
+ ]
4399
4432
  }
4400
4433
  },
4434
+ "outputFile": {
4435
+ "type": "string",
4436
+ "description": "Specifies a file path for the test report, applying only to the first reporter. To configure output files for multiple reporters, use the tuple format `['reporter-name', { outputFile: '...' }]` within the `reporters` option. When not provided, output is written to the console."
4437
+ },
4401
4438
  "providersFile": {
4402
4439
  "type": "string",
4403
- "description": "TypeScript file that exports an array of Angular providers to use during test execution. The array must be a default export.",
4440
+ "description": "Specifies the path to a TypeScript file that provides an array of Angular providers for the test environment. The file must contain a default export of the provider array.",
4404
4441
  "minLength": 1
4405
4442
  },
4406
4443
  "setupFiles": {
@@ -4408,11 +4445,17 @@
4408
4445
  "items": {
4409
4446
  "type": "string"
4410
4447
  },
4411
- "description": "A list of global setup and configuration files that are included before the test files. The application's polyfills are always included before these files. The Angular Testbed is also initialized prior to the execution of these files."
4448
+ "description": "A list of paths to global setup files that are executed before the test files. The application's polyfills and the Angular TestBed are always initialized before these files."
4412
4449
  },
4413
4450
  "progress": {
4414
4451
  "type": "boolean",
4415
- "description": "Log progress to the console while building. Defaults to the build target's progress value."
4452
+ "description": "Shows build progress information in the console. Defaults to the `progress` setting of the specified `buildTarget`."
4453
+ },
4454
+ "dumpVirtualFiles": {
4455
+ "type": "boolean",
4456
+ "description": "Dumps build output files to the `.angular/cache` directory for debugging purposes.",
4457
+ "default": false,
4458
+ "visible": false
4416
4459
  }
4417
4460
  },
4418
4461
  "additionalProperties": false,
@@ -4428,6 +4471,19 @@
4428
4471
  "json",
4429
4472
  "json-summary"
4430
4473
  ]
4474
+ },
4475
+ "reporters-enum": {
4476
+ "type": "string",
4477
+ "enum": [
4478
+ "default",
4479
+ "verbose",
4480
+ "dots",
4481
+ "json",
4482
+ "junit",
4483
+ "tap",
4484
+ "tap-flat",
4485
+ "html"
4486
+ ]
4431
4487
  }
4432
4488
  }
4433
4489
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/cli",
3
- "version": "21.0.0-next.2",
3
+ "version": "21.0.0-next.4",
4
4
  "description": "CLI tool for Angular",
5
5
  "main": "lib/cli/index.js",
6
6
  "bin": {
@@ -25,20 +25,20 @@
25
25
  },
26
26
  "homepage": "https://github.com/angular/angular-cli",
27
27
  "dependencies": {
28
- "@angular-devkit/architect": "0.2100.0-next.2",
29
- "@angular-devkit/core": "21.0.0-next.2",
30
- "@angular-devkit/schematics": "21.0.0-next.2",
31
- "@inquirer/prompts": "7.8.4",
32
- "@listr2/prompt-adapter-inquirer": "3.0.3",
33
- "@modelcontextprotocol/sdk": "1.17.5",
34
- "@schematics/angular": "21.0.0-next.2",
28
+ "@angular-devkit/architect": "0.2100.0-next.4",
29
+ "@angular-devkit/core": "21.0.0-next.4",
30
+ "@angular-devkit/schematics": "21.0.0-next.4",
31
+ "@inquirer/prompts": "7.8.6",
32
+ "@listr2/prompt-adapter-inquirer": "3.0.4",
33
+ "@modelcontextprotocol/sdk": "1.18.0",
34
+ "@schematics/angular": "21.0.0-next.4",
35
35
  "@yarnpkg/lockfile": "1.1.0",
36
36
  "algoliasearch": "5.37.0",
37
37
  "ini": "5.0.0",
38
38
  "jsonc-parser": "3.3.1",
39
- "listr2": "9.0.3",
39
+ "listr2": "9.0.4",
40
40
  "npm-package-arg": "13.0.0",
41
- "pacote": "21.0.0",
41
+ "pacote": "21.0.1",
42
42
  "resolve": "1.22.10",
43
43
  "semver": "7.7.2",
44
44
  "yargs": "18.0.0",
@@ -47,17 +47,17 @@
47
47
  "ng-update": {
48
48
  "migrations": "@schematics/angular/migrations/migration-collection.json",
49
49
  "packageGroup": {
50
- "@angular/cli": "21.0.0-next.2",
51
- "@angular/build": "21.0.0-next.2",
52
- "@angular/ssr": "21.0.0-next.2",
53
- "@angular-devkit/architect": "0.2100.0-next.2",
54
- "@angular-devkit/build-angular": "21.0.0-next.2",
55
- "@angular-devkit/build-webpack": "0.2100.0-next.2",
56
- "@angular-devkit/core": "21.0.0-next.2",
57
- "@angular-devkit/schematics": "21.0.0-next.2"
50
+ "@angular/cli": "21.0.0-next.4",
51
+ "@angular/build": "21.0.0-next.4",
52
+ "@angular/ssr": "21.0.0-next.4",
53
+ "@angular-devkit/architect": "0.2100.0-next.4",
54
+ "@angular-devkit/build-angular": "21.0.0-next.4",
55
+ "@angular-devkit/build-webpack": "0.2100.0-next.4",
56
+ "@angular-devkit/core": "21.0.0-next.4",
57
+ "@angular-devkit/schematics": "21.0.0-next.4"
58
58
  }
59
59
  },
60
- "packageManager": "pnpm@10.15.1",
60
+ "packageManager": "pnpm@10.17.0",
61
61
  "engines": {
62
62
  "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
63
63
  "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
@@ -13,16 +13,6 @@ import { AnyMcpToolDeclaration } from './tools/tool-registry';
13
13
  * These tools are considered experimental and may have limitations.
14
14
  */
15
15
  export declare const EXPERIMENTAL_TOOLS: readonly [import("./tools/tool-registry").McpToolDeclaration<{
16
- query: import("zod").ZodString;
17
- }, {
18
- examples: import("zod").ZodArray<import("zod").ZodObject<{
19
- content: import("zod").ZodString;
20
- }, "strip", import("zod").ZodTypeAny, {
21
- content: string;
22
- }, {
23
- content: string;
24
- }>, "many">;
25
- }>, import("./tools/tool-registry").McpToolDeclaration<{
26
16
  transformations: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodEnum<[string, ...string[]]>, "many">>;
27
17
  }, {
28
18
  instructions: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodString, "many">>;
@@ -28,16 +28,17 @@ const tool_registry_1 = require("./tools/tool-registry");
28
28
  * The set of tools that are enabled by default for the MCP server.
29
29
  * These tools are considered stable and suitable for general use.
30
30
  */
31
- const STABLE_TOOLS = [best_practices_1.BEST_PRACTICES_TOOL, doc_search_1.DOC_SEARCH_TOOL, projects_1.LIST_PROJECTS_TOOL];
31
+ const STABLE_TOOLS = [
32
+ best_practices_1.BEST_PRACTICES_TOOL,
33
+ doc_search_1.DOC_SEARCH_TOOL,
34
+ examples_1.FIND_EXAMPLE_TOOL,
35
+ projects_1.LIST_PROJECTS_TOOL,
36
+ ];
32
37
  /**
33
38
  * The set of tools that are available but not enabled by default.
34
39
  * These tools are considered experimental and may have limitations.
35
40
  */
36
- exports.EXPERIMENTAL_TOOLS = [
37
- examples_1.FIND_EXAMPLE_TOOL,
38
- modernize_1.MODERNIZE_TOOL,
39
- zoneless_migration_1.ZONELESS_MIGRATION_TOOL,
40
- ];
41
+ exports.EXPERIMENTAL_TOOLS = [modernize_1.MODERNIZE_TOOL, zoneless_migration_1.ZONELESS_MIGRATION_TOOL];
41
42
  async function createMcpServer(options, logger) {
42
43
  const server = new mcp_js_1.McpServer({
43
44
  name: 'angular-cli-server',
@@ -48,8 +49,34 @@ async function createMcpServer(options, logger) {
48
49
  tools: {},
49
50
  logging: {},
50
51
  },
51
- instructions: 'For Angular development, this server provides tools to adhere to best practices, search documentation, and find code examples. ' +
52
- 'When writing or modifying Angular code, use the MCP server and its tools instead of direct shell commands where possible.',
52
+ instructions: `
53
+ <General Purpose>
54
+ This server provides a safe, programmatic interface to the Angular CLI for an AI assistant.
55
+ Your primary goal is to use these tools to understand, analyze, refactor, and run Angular
56
+ projects. You MUST prefer the tools provided by this server over using \`run_shell_command\` for
57
+ equivalent actions.
58
+ </General Purpose>
59
+
60
+ <Core Workflows & Tool Guide>
61
+ * **1. Discover Project Structure (Mandatory First Step):** Always begin by calling
62
+ \`list_projects\` to understand the workspace. The outputs from this tool are often
63
+ required inputs for other tools.
64
+
65
+ * **2. Write & Modify Code:** Before writing or changing code, you MUST consult the
66
+ \`get_best_practices\` tool to learn the current, non-negotiable coding standards.
67
+
68
+ * **3. Answer User Questions:**
69
+ - For conceptual questions ("what is..."), use \`search_documentation\`.
70
+ - For code examples ("show me how to..."), use \`find_examples\`.
71
+ </Core Workflows & Tool Guide>
72
+
73
+ <Key Concepts>
74
+ * **Workspace vs. Project:** A 'workspace' contains an \`angular.json\` file and defines 'projects'
75
+ (applications or libraries). A monorepo can have multiple workspaces.
76
+ * **Targeting Projects:** Always use the \`workspaceConfigPath\` from \`list_projects\` when
77
+ available to ensure you are targeting the correct project in a monorepo.
78
+ </Key Concepts>
79
+ `,
53
80
  });
54
81
  (0, instructions_1.registerInstructionsResource)(server);
55
82
  const toolDeclarations = assembleToolDeclarations(STABLE_TOOLS, exports.EXPERIMENTAL_TOOLS, {
@@ -8,13 +8,38 @@
8
8
  import { z } from 'zod';
9
9
  export declare const FIND_EXAMPLE_TOOL: import("./tool-registry").McpToolDeclaration<{
10
10
  query: z.ZodString;
11
+ keywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
12
+ required_packages: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
+ related_concepts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
14
+ includeExperimental: z.ZodDefault<z.ZodOptional<z.ZodBoolean>>;
11
15
  }, {
12
16
  examples: z.ZodArray<z.ZodObject<{
17
+ title: z.ZodString;
18
+ summary: z.ZodString;
19
+ keywords: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
20
+ required_packages: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
21
+ related_concepts: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
22
+ related_tools: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
13
23
  content: z.ZodString;
24
+ snippet: z.ZodOptional<z.ZodString>;
14
25
  }, "strip", z.ZodTypeAny, {
26
+ title: string;
15
27
  content: string;
28
+ summary: string;
29
+ keywords?: string[] | undefined;
30
+ required_packages?: string[] | undefined;
31
+ related_concepts?: string[] | undefined;
32
+ related_tools?: string[] | undefined;
33
+ snippet?: string | undefined;
16
34
  }, {
35
+ title: string;
17
36
  content: string;
37
+ summary: string;
38
+ keywords?: string[] | undefined;
39
+ required_packages?: string[] | undefined;
40
+ related_concepts?: string[] | undefined;
41
+ related_tools?: string[] | undefined;
42
+ snippet?: string | undefined;
18
43
  }>, "many">;
19
44
  }>;
20
45
  /**
@@ -50,28 +50,92 @@ const node_path_1 = __importDefault(require("node:path"));
50
50
  const zod_1 = require("zod");
51
51
  const tool_registry_1 = require("./tool-registry");
52
52
  const findExampleInputSchema = zod_1.z.object({
53
- query: zod_1.z.string().describe(`Performs a full-text search using FTS5 syntax. The query should target relevant Angular concepts.
54
-
55
- Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):
56
- - AND (default): Space-separated terms are combined with AND.
57
- - Example: 'standalone component' (finds results with both "standalone" and "component")
58
- - OR: Use the OR operator to find results with either term.
59
- - Example: 'validation OR validator'
60
- - NOT: Use the NOT operator to exclude terms.
61
- - Example: 'forms NOT reactive'
62
- - Grouping: Use parentheses () to group expressions.
63
- - Example: '(validation OR validator) AND forms'
64
- - Phrase Search: Use double quotes "" for exact phrases.
65
- - Example: '"template-driven forms"'
66
- - Prefix Search: Use an asterisk * for prefix matching.
67
- - Example: 'rout*' (matches "route", "router", "routing")
68
-
69
- Examples of queries:
70
- - Find standalone components: 'standalone component'
71
- - Find ngFor with trackBy: 'ngFor trackBy'
72
- - Find signal inputs: 'signal input'
73
- - Find lazy loading a route: 'lazy load route'
74
- - Find forms with validation: 'form AND (validation OR validator)'`),
53
+ query: zod_1.z
54
+ .string()
55
+ .describe(`The primary, conceptual search query. This should capture the user's main goal or question ` +
56
+ `(e.g., 'lazy loading a route' or 'how to use signal inputs'). The query will be processed ` +
57
+ 'by a powerful full-text search engine.\n\n' +
58
+ 'Key Syntax Features (see https://www.sqlite.org/fts5.html for full documentation):\n' +
59
+ ' - AND (default): Space-separated terms are combined with AND.\n' +
60
+ ' - Example: \'standalone component\' (finds results with both "standalone" and "component")\n' +
61
+ ' - OR: Use the OR operator to find results with either term.\n' +
62
+ " - Example: 'validation OR validator'\n" +
63
+ ' - NOT: Use the NOT operator to exclude terms.\n' +
64
+ " - Example: 'forms NOT reactive'\n" +
65
+ ' - Grouping: Use parentheses () to group expressions.\n' +
66
+ " - Example: '(validation OR validator) AND forms'\n" +
67
+ ' - Phrase Search: Use double quotes "" for exact phrases.\n' +
68
+ ' - Example: \'"template-driven forms"\'\n' +
69
+ ' - Prefix Search: Use an asterisk * for prefix matching.\n' +
70
+ ' - Example: \'rout*\' (matches "route", "router", "routing")'),
71
+ keywords: zod_1.z
72
+ .array(zod_1.z.string())
73
+ .optional()
74
+ .describe('A list of specific, exact keywords to narrow the search. Use this for precise terms like ' +
75
+ 'API names, function names, or decorators (e.g., `ngFor`, `trackBy`, `inject`).'),
76
+ required_packages: zod_1.z
77
+ .array(zod_1.z.string())
78
+ .optional()
79
+ .describe("A list of NPM packages that an example must use. Use this when the user's request is " +
80
+ 'specific to a feature within a certain package (e.g., if the user asks about `ngModel`, ' +
81
+ 'you should filter by `@angular/forms`).'),
82
+ related_concepts: zod_1.z
83
+ .array(zod_1.z.string())
84
+ .optional()
85
+ .describe('A list of high-level concepts to filter by. Use this to find examples related to broader ' +
86
+ 'architectural ideas or patterns (e.g., `signals`, `dependency injection`, `routing`).'),
87
+ includeExperimental: zod_1.z
88
+ .boolean()
89
+ .optional()
90
+ .default(false)
91
+ .describe('By default, this tool returns only production-safe examples. Set this to `true` **only if** ' +
92
+ 'the user explicitly asks for a bleeding-edge feature or if a stable solution to their ' +
93
+ 'problem cannot be found. If you set this to `true`, you **MUST** preface your answer by ' +
94
+ 'warning the user that the example uses experimental APIs that are not suitable for production.'),
95
+ });
96
+ const findExampleOutputSchema = zod_1.z.object({
97
+ examples: zod_1.z.array(zod_1.z.object({
98
+ title: zod_1.z
99
+ .string()
100
+ .describe('The title of the example. Use this as a heading when presenting the example to the user.'),
101
+ summary: zod_1.z
102
+ .string()
103
+ .describe("A one-sentence summary of the example's purpose. Use this to help the user decide " +
104
+ 'if the example is relevant to them.'),
105
+ keywords: zod_1.z
106
+ .array(zod_1.z.string())
107
+ .optional()
108
+ .describe('A list of keywords for the example. You can use these to explain why this example ' +
109
+ "was a good match for the user's query."),
110
+ required_packages: zod_1.z
111
+ .array(zod_1.z.string())
112
+ .optional()
113
+ .describe('A list of NPM packages required for the example to work. Before presenting the code, ' +
114
+ 'you should inform the user if any of these packages need to be installed.'),
115
+ related_concepts: zod_1.z
116
+ .array(zod_1.z.string())
117
+ .optional()
118
+ .describe('A list of related concepts. You can suggest these to the user as topics for ' +
119
+ 'follow-up questions.'),
120
+ related_tools: zod_1.z
121
+ .array(zod_1.z.string())
122
+ .optional()
123
+ .describe('A list of related MCP tools. You can suggest these as potential next steps for the user.'),
124
+ content: zod_1.z
125
+ .string()
126
+ .describe('A complete, self-contained Angular code example in Markdown format. This should be ' +
127
+ 'presented to the user inside a markdown code block.'),
128
+ snippet: zod_1.z
129
+ .string()
130
+ .optional()
131
+ .describe('A contextual snippet from the content showing the matched search term. This field is ' +
132
+ 'critical for efficiently evaluating a result`s relevance. It enables two primary ' +
133
+ 'workflows:\n\n' +
134
+ '1. For direct questions: You can internally review snippets to select the single best ' +
135
+ 'result before generating a comprehensive answer from its full `content`.\n' +
136
+ '2. For ambiguous or exploratory questions: You can present a summary of titles and ' +
137
+ 'snippets to the user, allowing them to guide the next step.'),
138
+ })),
75
139
  });
76
140
  exports.FIND_EXAMPLE_TOOL = (0, tool_registry_1.declareTool)({
77
141
  name: 'find_examples',
@@ -89,7 +153,9 @@ new or evolving features.
89
153
  * **Modern Implementation:** Finding the correct modern syntax for features
90
154
  (e.g., query: 'functional route guard' or 'http client with fetch').
91
155
  * **Refactoring to Modern Patterns:** Upgrading older code by finding examples of new syntax
92
- (e.g., query: 'built-in control flow' to replace "*ngIf').
156
+ (e.g., query: 'built-in control flow' to replace "*ngIf").
157
+ * **Advanced Filtering:** Combining a full-text search with filters to narrow results.
158
+ (e.g., query: 'forms', required_packages: ['@angular/forms'], keywords: ['validation'])
93
159
  </Use Cases>
94
160
  <Operational Notes>
95
161
  * **Tool Selection:** This database primarily contains examples for new and recently updated Angular
@@ -98,15 +164,11 @@ new or evolving features.
98
164
  * The examples in this database are the single source of truth for modern Angular coding patterns.
99
165
  * The search query uses a powerful full-text search syntax (FTS5). Refer to the 'query'
100
166
  parameter description for detailed syntax rules and examples.
167
+ * You can combine the main 'query' with optional filters like 'keywords', 'required_packages',
168
+ and 'related_concepts' to create highly specific searches.
101
169
  </Operational Notes>`,
102
170
  inputSchema: findExampleInputSchema.shape,
103
- outputSchema: {
104
- examples: zod_1.z.array(zod_1.z.object({
105
- content: zod_1.z
106
- .string()
107
- .describe('A complete, self-contained Angular code example in Markdown format.'),
108
- })),
109
- },
171
+ outputSchema: findExampleOutputSchema.shape,
110
172
  isReadOnly: true,
111
173
  isLocalOnly: true,
112
174
  shouldRegister: ({ logger }) => {
@@ -123,12 +185,11 @@ new or evolving features.
123
185
  });
124
186
  async function createFindExampleHandler({ exampleDatabasePath }) {
125
187
  let db;
126
- let queryStatement;
127
188
  if (process.env['NG_MCP_EXAMPLES_DIR']) {
128
189
  db = await setupRuntimeExamples(process.env['NG_MCP_EXAMPLES_DIR']);
129
190
  }
130
191
  suppressSqliteWarning();
131
- return async ({ query }) => {
192
+ return async (input) => {
132
193
  if (!db) {
133
194
  if (!exampleDatabasePath) {
134
195
  // This should be prevented by the registration logic in mcp-server.ts
@@ -137,17 +198,67 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
137
198
  const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
138
199
  db = new DatabaseSync(exampleDatabasePath, { readOnly: true });
139
200
  }
140
- if (!queryStatement) {
141
- queryStatement = db.prepare('SELECT * from examples WHERE examples MATCH ? ORDER BY rank;');
201
+ const { query, keywords, required_packages, related_concepts, includeExperimental } = input;
202
+ // Build the query dynamically
203
+ const params = [];
204
+ let sql = 'SELECT title, summary, keywords, required_packages, related_concepts, related_tools, content, ' +
205
+ // The `snippet` function generates a contextual snippet of the matched text.
206
+ // Column 6 is the `content` column. We highlight matches with asterisks and limit the snippet size.
207
+ "snippet(examples_fts, 6, '**', '**', '...', 15) AS snippet " +
208
+ 'FROM examples_fts';
209
+ const whereClauses = [];
210
+ // FTS query
211
+ if (query) {
212
+ whereClauses.push('examples_fts MATCH ?');
213
+ params.push(escapeSearchQuery(query));
214
+ }
215
+ // JSON array filters
216
+ const addJsonFilter = (column, values) => {
217
+ if (values?.length) {
218
+ for (const value of values) {
219
+ whereClauses.push(`${column} LIKE ?`);
220
+ params.push(`%"${value}"%`);
221
+ }
222
+ }
223
+ };
224
+ addJsonFilter('keywords', keywords);
225
+ addJsonFilter('required_packages', required_packages);
226
+ addJsonFilter('related_concepts', related_concepts);
227
+ if (!includeExperimental) {
228
+ whereClauses.push('experimental = 0');
229
+ }
230
+ if (whereClauses.length > 0) {
231
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
142
232
  }
143
- const sanitizedQuery = escapeSearchQuery(query);
233
+ // Order the results by relevance using the BM25 algorithm.
234
+ // The weights assigned to each column boost the ranking of documents where the
235
+ // search term appears in a more important field.
236
+ // Column order: title, summary, keywords, required_packages, related_concepts, related_tools, content
237
+ sql += ' ORDER BY bm25(examples_fts, 10.0, 5.0, 5.0, 1.0, 2.0, 1.0, 1.0);';
238
+ const queryStatement = db.prepare(sql);
144
239
  // Query database and return results
145
240
  const examples = [];
146
241
  const textContent = [];
147
- for (const exampleRecord of queryStatement.all(sanitizedQuery)) {
148
- const exampleContent = exampleRecord['content'];
149
- examples.push({ content: exampleContent });
150
- textContent.push({ type: 'text', text: exampleContent });
242
+ for (const exampleRecord of queryStatement.all(...params)) {
243
+ const record = exampleRecord;
244
+ const example = {
245
+ title: record['title'],
246
+ summary: record['summary'],
247
+ keywords: JSON.parse(record['keywords'] || '[]'),
248
+ required_packages: JSON.parse(record['required_packages'] || '[]'),
249
+ related_concepts: JSON.parse(record['related_concepts'] || '[]'),
250
+ related_tools: JSON.parse(record['related_tools'] || '[]'),
251
+ content: record['content'],
252
+ snippet: record['snippet'],
253
+ };
254
+ examples.push(example);
255
+ // Also create a more structured text output
256
+ let text = `## Example: ${example.title}\n**Summary:** ${example.summary}`;
257
+ if (example.snippet) {
258
+ text += `\n**Snippet:** ${example.snippet}`;
259
+ }
260
+ text += `\n\n---\n\n${example.content}`;
261
+ textContent.push({ type: 'text', text });
151
262
  }
152
263
  return {
153
264
  content: textContent,
@@ -232,18 +343,131 @@ function suppressSqliteWarning() {
232
343
  return originalProcessEmit.apply(process, arguments);
233
344
  };
234
345
  }
346
+ /**
347
+ * A simple YAML front matter parser.
348
+ *
349
+ * This function extracts the YAML block enclosed by `---` at the beginning of a string
350
+ * and parses it into a JavaScript object. It is not a full YAML parser and only
351
+ * supports simple key-value pairs and string arrays.
352
+ *
353
+ * @param content The string content to parse.
354
+ * @returns A record containing the parsed front matter data.
355
+ */
356
+ function parseFrontmatter(content) {
357
+ const match = content.match(/^---\r?\n(.*?)\r?\n---/s);
358
+ if (!match) {
359
+ return {};
360
+ }
361
+ const frontmatter = match[1];
362
+ const data = {};
363
+ const lines = frontmatter.split(/\r?\n/);
364
+ let currentKey = '';
365
+ let isArray = false;
366
+ const arrayValues = [];
367
+ for (const line of lines) {
368
+ const keyValueMatch = line.match(/^([^:]+):\s*(.*)/);
369
+ if (keyValueMatch) {
370
+ if (currentKey && isArray) {
371
+ data[currentKey] = arrayValues.slice();
372
+ arrayValues.length = 0;
373
+ }
374
+ const [, key, value] = keyValueMatch;
375
+ currentKey = key.trim();
376
+ isArray = value.trim() === '';
377
+ if (!isArray) {
378
+ const trimmedValue = value.trim();
379
+ if (trimmedValue === 'true') {
380
+ data[currentKey] = true;
381
+ }
382
+ else if (trimmedValue === 'false') {
383
+ data[currentKey] = false;
384
+ }
385
+ else {
386
+ data[currentKey] = trimmedValue;
387
+ }
388
+ }
389
+ }
390
+ else {
391
+ const arrayItemMatch = line.match(/^\s*-\s*(.*)/);
392
+ if (arrayItemMatch && currentKey && isArray) {
393
+ arrayValues.push(arrayItemMatch[1].trim());
394
+ }
395
+ }
396
+ }
397
+ if (currentKey && isArray) {
398
+ data[currentKey] = arrayValues;
399
+ }
400
+ return data;
401
+ }
235
402
  async function setupRuntimeExamples(examplesPath) {
236
403
  const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
237
404
  const db = new DatabaseSync(':memory:');
238
- db.exec(`CREATE VIRTUAL TABLE examples USING fts5(content, tokenize = 'porter ascii');`);
239
- const insertStatement = db.prepare('INSERT INTO examples(content) VALUES(?);');
405
+ // Create a relational table to store the structured example data.
406
+ db.exec(`
407
+ CREATE TABLE examples (
408
+ id INTEGER PRIMARY KEY,
409
+ title TEXT NOT NULL,
410
+ summary TEXT NOT NULL,
411
+ keywords TEXT,
412
+ required_packages TEXT,
413
+ related_concepts TEXT,
414
+ related_tools TEXT,
415
+ experimental INTEGER NOT NULL DEFAULT 0,
416
+ content TEXT NOT NULL
417
+ );
418
+ `);
419
+ // Create an FTS5 virtual table to provide full-text search capabilities.
420
+ db.exec(`
421
+ CREATE VIRTUAL TABLE examples_fts USING fts5(
422
+ title,
423
+ summary,
424
+ keywords,
425
+ required_packages,
426
+ related_concepts,
427
+ related_tools,
428
+ content,
429
+ content='examples',
430
+ content_rowid='id',
431
+ tokenize = 'porter ascii'
432
+ );
433
+ `);
434
+ // Create triggers to keep the FTS table synchronized with the examples table.
435
+ db.exec(`
436
+ CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
437
+ INSERT INTO examples_fts(rowid, title, summary, keywords, required_packages, related_concepts, related_tools, content)
438
+ VALUES (
439
+ new.id, new.title, new.summary, new.keywords, new.required_packages, new.related_concepts,
440
+ new.related_tools, new.content
441
+ );
442
+ END;
443
+ `);
444
+ const insertStatement = db.prepare('INSERT INTO examples(' +
445
+ 'title, summary, keywords, required_packages, related_concepts, related_tools, experimental, content' +
446
+ ') VALUES(?, ?, ?, ?, ?, ?, ?, ?);');
447
+ const frontmatterSchema = zod_1.z.object({
448
+ title: zod_1.z.string(),
449
+ summary: zod_1.z.string(),
450
+ keywords: zod_1.z.array(zod_1.z.string()).optional(),
451
+ required_packages: zod_1.z.array(zod_1.z.string()).optional(),
452
+ related_concepts: zod_1.z.array(zod_1.z.string()).optional(),
453
+ related_tools: zod_1.z.array(zod_1.z.string()).optional(),
454
+ experimental: zod_1.z.boolean().optional(),
455
+ });
240
456
  db.exec('BEGIN TRANSACTION');
241
- for await (const entry of (0, promises_1.glob)('*.md', { cwd: examplesPath, withFileTypes: true })) {
457
+ for await (const entry of (0, promises_1.glob)('**/*.md', { cwd: examplesPath, withFileTypes: true })) {
242
458
  if (!entry.isFile()) {
243
459
  continue;
244
460
  }
245
- const example = await (0, promises_1.readFile)(node_path_1.default.join(entry.parentPath, entry.name), 'utf-8');
246
- insertStatement.run(example);
461
+ const content = await (0, promises_1.readFile)(node_path_1.default.join(entry.parentPath, entry.name), 'utf-8');
462
+ const frontmatter = parseFrontmatter(content);
463
+ const validation = frontmatterSchema.safeParse(frontmatter);
464
+ if (!validation.success) {
465
+ // eslint-disable-next-line no-console
466
+ console.warn(`Skipping invalid example file ${entry.name}:`, validation.error.issues);
467
+ continue;
468
+ }
469
+ const { title, summary, keywords, required_packages, related_concepts, related_tools, experimental, } = validation.data;
470
+ insertStatement.run(title, summary, JSON.stringify(keywords ?? []), JSON.stringify(required_packages ?? []), JSON.stringify(related_concepts ?? []), JSON.stringify(related_tools ?? []), experimental ? 1 : 0, content);
247
471
  }
248
472
  db.exec('END TRANSACTION');
249
473
  return db;
@@ -148,6 +148,7 @@ function generateZonelessMigrationInstructionsForComponent(filePath) {
148
148
  3. **DO NOT** use \`ChangeDetectionStrategy.OnPush\`. This will be the next step in the migration, but it is not part of this task.
149
149
  4. **DO NOT** modify properties that are already signals or are used with the \`async\` pipe in the template, as they are already zoneless-compatible.
150
150
  5. **DO NOT** make any changes to files other than the component file at \`${filePath}\` and its direct template/style files if necessary.
151
+ 6. **DO NOT** remove or modify usages of \`NgZone.run\` or \`NgZone.runOutsideAngular\`. These are still required.
151
152
 
152
153
  ### Final Step
153
154
  After you have applied all the required changes and followed all the rules, consult this tool again for the next steps in the migration process.`;
@@ -12,8 +12,14 @@ export interface PackageManagerUtilsContext {
12
12
  workspace?: AngularWorkspace;
13
13
  root: string;
14
14
  }
15
+ /**
16
+ * Utilities for interacting with various package managers.
17
+ */
15
18
  export declare class PackageManagerUtils {
16
19
  private readonly context;
20
+ /**
21
+ * @param context The context for the package manager utilities, including workspace and global configuration.
22
+ */
17
23
  constructor(context: PackageManagerUtilsContext);
18
24
  /** Get the package manager name. */
19
25
  get name(): PackageManager;
@@ -32,6 +38,12 @@ export declare class PackageManagerUtils {
32
38
  private run;
33
39
  private getVersion;
34
40
  private getName;
41
+ /**
42
+ * Checks if a lockfile for a specific package manager exists in the root directory.
43
+ * @param packageManager The package manager to check for.
44
+ * @param filesInRoot An array of file names in the root directory.
45
+ * @returns True if the lockfile exists, false otherwise.
46
+ */
35
47
  private hasLockfile;
36
48
  private getConfiguredPackageManager;
37
49
  }
@@ -50,6 +50,18 @@ const node_path_1 = require("node:path");
50
50
  const workspace_schema_1 = require("../../lib/config/workspace-schema");
51
51
  const config_1 = require("./config");
52
52
  const memoize_1 = require("./memoize");
53
+ /**
54
+ * A map of package managers to their corresponding lockfile names.
55
+ */
56
+ const LOCKFILE_NAMES = {
57
+ [workspace_schema_1.PackageManager.Yarn]: 'yarn.lock',
58
+ [workspace_schema_1.PackageManager.Pnpm]: 'pnpm-lock.yaml',
59
+ [workspace_schema_1.PackageManager.Bun]: ['bun.lockb', 'bun.lock'],
60
+ [workspace_schema_1.PackageManager.Npm]: 'package-lock.json',
61
+ };
62
+ /**
63
+ * Utilities for interacting with various package managers.
64
+ */
53
65
  let PackageManagerUtils = (() => {
54
66
  let _instanceExtraInitializers = [];
55
67
  let _getVersion_decorators;
@@ -64,6 +76,9 @@ let PackageManagerUtils = (() => {
64
76
  if (_metadata) Object.defineProperty(this, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
65
77
  }
66
78
  context = __runInitializers(this, _instanceExtraInitializers);
79
+ /**
80
+ * @param context The context for the package manager utilities, including workspace and global configuration.
81
+ */
67
82
  constructor(context) {
68
83
  this.context = context;
69
84
  }
@@ -211,10 +226,11 @@ let PackageManagerUtils = (() => {
211
226
  if (packageManager) {
212
227
  return packageManager;
213
228
  }
214
- const hasNpmLock = this.hasLockfile(workspace_schema_1.PackageManager.Npm);
215
- const hasYarnLock = this.hasLockfile(workspace_schema_1.PackageManager.Yarn);
216
- const hasPnpmLock = this.hasLockfile(workspace_schema_1.PackageManager.Pnpm);
217
- const hasBunLock = this.hasLockfile(workspace_schema_1.PackageManager.Bun);
229
+ const filesInRoot = (0, node_fs_1.readdirSync)(this.context.root);
230
+ const hasNpmLock = this.hasLockfile(workspace_schema_1.PackageManager.Npm, filesInRoot);
231
+ const hasYarnLock = this.hasLockfile(workspace_schema_1.PackageManager.Yarn, filesInRoot);
232
+ const hasPnpmLock = this.hasLockfile(workspace_schema_1.PackageManager.Pnpm, filesInRoot);
233
+ const hasBunLock = this.hasLockfile(workspace_schema_1.PackageManager.Bun, filesInRoot);
218
234
  // PERF NOTE: `this.getVersion` spawns the package a the child_process which can take around ~300ms at times.
219
235
  // Therefore, we should only call this method when needed. IE: don't call `this.getVersion(PackageManager.Pnpm)` unless truly needed.
220
236
  // The result of this method is not stored in a variable because it's memoized.
@@ -259,24 +275,17 @@ let PackageManagerUtils = (() => {
259
275
  // Potentially with a prompt to choose and optionally set as the default.
260
276
  return workspace_schema_1.PackageManager.Npm;
261
277
  }
262
- hasLockfile(packageManager) {
263
- let lockfileName;
264
- switch (packageManager) {
265
- case workspace_schema_1.PackageManager.Yarn:
266
- lockfileName = 'yarn.lock';
267
- break;
268
- case workspace_schema_1.PackageManager.Pnpm:
269
- lockfileName = 'pnpm-lock.yaml';
270
- break;
271
- case workspace_schema_1.PackageManager.Bun:
272
- lockfileName = 'bun.lockb';
273
- break;
274
- case workspace_schema_1.PackageManager.Npm:
275
- default:
276
- lockfileName = 'package-lock.json';
277
- break;
278
- }
279
- return (0, node_fs_1.existsSync)((0, node_path_1.join)(this.context.root, lockfileName));
278
+ /**
279
+ * Checks if a lockfile for a specific package manager exists in the root directory.
280
+ * @param packageManager The package manager to check for.
281
+ * @param filesInRoot An array of file names in the root directory.
282
+ * @returns True if the lockfile exists, false otherwise.
283
+ */
284
+ hasLockfile(packageManager, filesInRoot) {
285
+ const lockfiles = LOCKFILE_NAMES[packageManager];
286
+ return typeof lockfiles === 'string'
287
+ ? filesInRoot.includes(lockfiles)
288
+ : lockfiles.some((lockfile) => filesInRoot.includes(lockfile));
280
289
  }
281
290
  getConfiguredPackageManager() {
282
291
  const getPackageManager = (source) => {
@@ -22,4 +22,4 @@ class Version {
22
22
  this.patch = patch;
23
23
  }
24
24
  }
25
- exports.VERSION = new Version('21.0.0-next.2');
25
+ exports.VERSION = new Version('21.0.0-next.4');