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

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
@@ -4342,7 +4342,6 @@
4342
4342
  "items": {
4343
4343
  "type": "string"
4344
4344
  },
4345
- "default": [],
4346
4345
  "description": "Globs of files to exclude, relative to the project root."
4347
4346
  },
4348
4347
  "watch": {
@@ -4364,8 +4363,7 @@
4364
4363
  "description": "Globs to exclude from code coverage.",
4365
4364
  "items": {
4366
4365
  "type": "string"
4367
- },
4368
- "default": []
4366
+ }
4369
4367
  },
4370
4368
  "codeCoverageReporters": {
4371
4369
  "type": "array",
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.3",
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",
28
+ "@angular-devkit/architect": "0.2100.0-next.3",
29
+ "@angular-devkit/core": "21.0.0-next.3",
30
+ "@angular-devkit/schematics": "21.0.0-next.3",
31
31
  "@inquirer/prompts": "7.8.4",
32
32
  "@listr2/prompt-adapter-inquirer": "3.0.3",
33
33
  "@modelcontextprotocol/sdk": "1.17.5",
34
- "@schematics/angular": "21.0.0-next.2",
34
+ "@schematics/angular": "21.0.0-next.3",
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
39
  "listr2": "9.0.3",
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,14 +47,14 @@
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.3",
51
+ "@angular/build": "21.0.0-next.3",
52
+ "@angular/ssr": "21.0.0-next.3",
53
+ "@angular-devkit/architect": "0.2100.0-next.3",
54
+ "@angular-devkit/build-angular": "21.0.0-next.3",
55
+ "@angular-devkit/build-webpack": "0.2100.0-next.3",
56
+ "@angular-devkit/core": "21.0.0-next.3",
57
+ "@angular-devkit/schematics": "21.0.0-next.3"
58
58
  }
59
59
  },
60
60
  "packageManager": "pnpm@10.15.1",
@@ -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 }) => {
@@ -128,7 +190,7 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
128
190
  db = await setupRuntimeExamples(process.env['NG_MCP_EXAMPLES_DIR']);
129
191
  }
130
192
  suppressSqliteWarning();
131
- return async ({ query }) => {
193
+ return async (input) => {
132
194
  if (!db) {
133
195
  if (!exampleDatabasePath) {
134
196
  // This should be prevented by the registration logic in mcp-server.ts
@@ -137,17 +199,67 @@ async function createFindExampleHandler({ exampleDatabasePath }) {
137
199
  const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
138
200
  db = new DatabaseSync(exampleDatabasePath, { readOnly: true });
139
201
  }
140
- if (!queryStatement) {
141
- queryStatement = db.prepare('SELECT * from examples WHERE examples MATCH ? ORDER BY rank;');
202
+ const { query, keywords, required_packages, related_concepts, includeExperimental } = input;
203
+ // Build the query dynamically
204
+ const params = [];
205
+ let sql = 'SELECT title, summary, keywords, required_packages, related_concepts, related_tools, content, ' +
206
+ // The `snippet` function generates a contextual snippet of the matched text.
207
+ // Column 6 is the `content` column. We highlight matches with asterisks and limit the snippet size.
208
+ "snippet(examples_fts, 6, '**', '**', '...', 15) AS snippet " +
209
+ 'FROM examples_fts';
210
+ const whereClauses = [];
211
+ // FTS query
212
+ if (query) {
213
+ whereClauses.push('examples_fts MATCH ?');
214
+ params.push(escapeSearchQuery(query));
215
+ }
216
+ // JSON array filters
217
+ const addJsonFilter = (column, values) => {
218
+ if (values?.length) {
219
+ for (const value of values) {
220
+ whereClauses.push(`${column} LIKE ?`);
221
+ params.push(`%"${value}"%`);
222
+ }
223
+ }
224
+ };
225
+ addJsonFilter('keywords', keywords);
226
+ addJsonFilter('required_packages', required_packages);
227
+ addJsonFilter('related_concepts', related_concepts);
228
+ if (!includeExperimental) {
229
+ whereClauses.push('experimental = 0');
230
+ }
231
+ if (whereClauses.length > 0) {
232
+ sql += ` WHERE ${whereClauses.join(' AND ')}`;
142
233
  }
143
- const sanitizedQuery = escapeSearchQuery(query);
234
+ // Order the results by relevance using the BM25 algorithm.
235
+ // The weights assigned to each column boost the ranking of documents where the
236
+ // search term appears in a more important field.
237
+ // Column order: title, summary, keywords, required_packages, related_concepts, related_tools, content
238
+ sql += ' ORDER BY bm25(examples_fts, 10.0, 5.0, 5.0, 1.0, 2.0, 1.0, 1.0);';
239
+ const queryStatement = db.prepare(sql);
144
240
  // Query database and return results
145
241
  const examples = [];
146
242
  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 });
243
+ for (const exampleRecord of queryStatement.all(...params)) {
244
+ const record = exampleRecord;
245
+ const example = {
246
+ title: record['title'],
247
+ summary: record['summary'],
248
+ keywords: JSON.parse(record['keywords'] || '[]'),
249
+ required_packages: JSON.parse(record['required_packages'] || '[]'),
250
+ related_concepts: JSON.parse(record['related_concepts'] || '[]'),
251
+ related_tools: JSON.parse(record['related_tools'] || '[]'),
252
+ content: record['content'],
253
+ snippet: record['snippet'],
254
+ };
255
+ examples.push(example);
256
+ // Also create a more structured text output
257
+ let text = `## Example: ${example.title}\n**Summary:** ${example.summary}`;
258
+ if (example.snippet) {
259
+ text += `\n**Snippet:** ${example.snippet}`;
260
+ }
261
+ text += `\n\n---\n\n${example.content}`;
262
+ textContent.push({ type: 'text', text });
151
263
  }
152
264
  return {
153
265
  content: textContent,
@@ -232,18 +344,131 @@ function suppressSqliteWarning() {
232
344
  return originalProcessEmit.apply(process, arguments);
233
345
  };
234
346
  }
347
+ /**
348
+ * A simple YAML front matter parser.
349
+ *
350
+ * This function extracts the YAML block enclosed by `---` at the beginning of a string
351
+ * and parses it into a JavaScript object. It is not a full YAML parser and only
352
+ * supports simple key-value pairs and string arrays.
353
+ *
354
+ * @param content The string content to parse.
355
+ * @returns A record containing the parsed front matter data.
356
+ */
357
+ function parseFrontmatter(content) {
358
+ const match = content.match(/^---\r?\n(.*?)\r?\n---/s);
359
+ if (!match) {
360
+ return {};
361
+ }
362
+ const frontmatter = match[1];
363
+ const data = {};
364
+ const lines = frontmatter.split(/\r?\n/);
365
+ let currentKey = '';
366
+ let isArray = false;
367
+ const arrayValues = [];
368
+ for (const line of lines) {
369
+ const keyValueMatch = line.match(/^([^:]+):\s*(.*)/);
370
+ if (keyValueMatch) {
371
+ if (currentKey && isArray) {
372
+ data[currentKey] = arrayValues.slice();
373
+ arrayValues.length = 0;
374
+ }
375
+ const [, key, value] = keyValueMatch;
376
+ currentKey = key.trim();
377
+ isArray = value.trim() === '';
378
+ if (!isArray) {
379
+ const trimmedValue = value.trim();
380
+ if (trimmedValue === 'true') {
381
+ data[currentKey] = true;
382
+ }
383
+ else if (trimmedValue === 'false') {
384
+ data[currentKey] = false;
385
+ }
386
+ else {
387
+ data[currentKey] = trimmedValue;
388
+ }
389
+ }
390
+ }
391
+ else {
392
+ const arrayItemMatch = line.match(/^\s*-\s*(.*)/);
393
+ if (arrayItemMatch && currentKey && isArray) {
394
+ arrayValues.push(arrayItemMatch[1].trim());
395
+ }
396
+ }
397
+ }
398
+ if (currentKey && isArray) {
399
+ data[currentKey] = arrayValues;
400
+ }
401
+ return data;
402
+ }
235
403
  async function setupRuntimeExamples(examplesPath) {
236
404
  const { DatabaseSync } = await Promise.resolve().then(() => __importStar(require('node:sqlite')));
237
405
  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(?);');
406
+ // Create a relational table to store the structured example data.
407
+ db.exec(`
408
+ CREATE TABLE examples (
409
+ id INTEGER PRIMARY KEY,
410
+ title TEXT NOT NULL,
411
+ summary TEXT NOT NULL,
412
+ keywords TEXT,
413
+ required_packages TEXT,
414
+ related_concepts TEXT,
415
+ related_tools TEXT,
416
+ experimental INTEGER NOT NULL DEFAULT 0,
417
+ content TEXT NOT NULL
418
+ );
419
+ `);
420
+ // Create an FTS5 virtual table to provide full-text search capabilities.
421
+ db.exec(`
422
+ CREATE VIRTUAL TABLE examples_fts USING fts5(
423
+ title,
424
+ summary,
425
+ keywords,
426
+ required_packages,
427
+ related_concepts,
428
+ related_tools,
429
+ content,
430
+ content='examples',
431
+ content_rowid='id',
432
+ tokenize = 'porter ascii'
433
+ );
434
+ `);
435
+ // Create triggers to keep the FTS table synchronized with the examples table.
436
+ db.exec(`
437
+ CREATE TRIGGER examples_after_insert AFTER INSERT ON examples BEGIN
438
+ INSERT INTO examples_fts(rowid, title, summary, keywords, required_packages, related_concepts, related_tools, content)
439
+ VALUES (
440
+ new.id, new.title, new.summary, new.keywords, new.required_packages, new.related_concepts,
441
+ new.related_tools, new.content
442
+ );
443
+ END;
444
+ `);
445
+ const insertStatement = db.prepare('INSERT INTO examples(' +
446
+ 'title, summary, keywords, required_packages, related_concepts, related_tools, experimental, content' +
447
+ ') VALUES(?, ?, ?, ?, ?, ?, ?, ?);');
448
+ const frontmatterSchema = zod_1.z.object({
449
+ title: zod_1.z.string(),
450
+ summary: zod_1.z.string(),
451
+ keywords: zod_1.z.array(zod_1.z.string()).optional(),
452
+ required_packages: zod_1.z.array(zod_1.z.string()).optional(),
453
+ related_concepts: zod_1.z.array(zod_1.z.string()).optional(),
454
+ related_tools: zod_1.z.array(zod_1.z.string()).optional(),
455
+ experimental: zod_1.z.boolean().optional(),
456
+ });
240
457
  db.exec('BEGIN TRANSACTION');
241
- for await (const entry of (0, promises_1.glob)('*.md', { cwd: examplesPath, withFileTypes: true })) {
458
+ for await (const entry of (0, promises_1.glob)('**/*.md', { cwd: examplesPath, withFileTypes: true })) {
242
459
  if (!entry.isFile()) {
243
460
  continue;
244
461
  }
245
- const example = await (0, promises_1.readFile)(node_path_1.default.join(entry.parentPath, entry.name), 'utf-8');
246
- insertStatement.run(example);
462
+ const content = await (0, promises_1.readFile)(node_path_1.default.join(entry.parentPath, entry.name), 'utf-8');
463
+ const frontmatter = parseFrontmatter(content);
464
+ const validation = frontmatterSchema.safeParse(frontmatter);
465
+ if (!validation.success) {
466
+ // eslint-disable-next-line no-console
467
+ console.warn(`Skipping invalid example file ${entry.name}:`, validation.error.issues);
468
+ continue;
469
+ }
470
+ const { title, summary, keywords, required_packages, related_concepts, related_tools, experimental, } = validation.data;
471
+ insertStatement.run(title, summary, JSON.stringify(keywords ?? []), JSON.stringify(required_packages ?? []), JSON.stringify(related_concepts ?? []), JSON.stringify(related_tools ?? []), experimental ? 1 : 0, content);
247
472
  }
248
473
  db.exec('END TRANSACTION');
249
474
  return db;
@@ -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.3');