@angular/cli
Advanced tools
Comparing version
{ | ||
"name": "@angular/cli", | ||
"version": "20.2.0-next.0", | ||
"version": "20.2.0-next.1", | ||
"description": "CLI tool for Angular", | ||
@@ -28,11 +28,11 @@ "main": "lib/cli/index.js", | ||
"dependencies": { | ||
"@angular-devkit/architect": "0.2002.0-next.0", | ||
"@angular-devkit/core": "20.2.0-next.0", | ||
"@angular-devkit/schematics": "20.2.0-next.0", | ||
"@inquirer/prompts": "7.6.0", | ||
"@angular-devkit/architect": "0.2002.0-next.1", | ||
"@angular-devkit/core": "20.2.0-next.1", | ||
"@angular-devkit/schematics": "20.2.0-next.1", | ||
"@inquirer/prompts": "7.7.1", | ||
"@listr2/prompt-adapter-inquirer": "3.0.1", | ||
"@modelcontextprotocol/sdk": "1.15.1", | ||
"@schematics/angular": "20.2.0-next.0", | ||
"@modelcontextprotocol/sdk": "1.16.0", | ||
"@schematics/angular": "20.2.0-next.1", | ||
"@yarnpkg/lockfile": "1.1.0", | ||
"algoliasearch": "5.32.0", | ||
"algoliasearch": "5.34.0", | ||
"ini": "5.0.0", | ||
@@ -52,10 +52,10 @@ "jsonc-parser": "3.3.1", | ||
"packageGroup": { | ||
"@angular/cli": "20.2.0-next.0", | ||
"@angular/build": "20.2.0-next.0", | ||
"@angular/ssr": "20.2.0-next.0", | ||
"@angular-devkit/architect": "0.2002.0-next.0", | ||
"@angular-devkit/build-angular": "20.2.0-next.0", | ||
"@angular-devkit/build-webpack": "0.2002.0-next.0", | ||
"@angular-devkit/core": "20.2.0-next.0", | ||
"@angular-devkit/schematics": "20.2.0-next.0" | ||
"@angular/cli": "20.2.0-next.1", | ||
"@angular/build": "20.2.0-next.1", | ||
"@angular/ssr": "20.2.0-next.1", | ||
"@angular-devkit/architect": "0.2002.0-next.1", | ||
"@angular-devkit/build-angular": "20.2.0-next.1", | ||
"@angular-devkit/build-webpack": "0.2002.0-next.1", | ||
"@angular-devkit/core": "20.2.0-next.1", | ||
"@angular-devkit/schematics": "20.2.0-next.1" | ||
} | ||
@@ -62,0 +62,0 @@ }, |
@@ -22,16 +22,19 @@ "use strict"; | ||
const positional = groups[usageInstance.getPositionalGroupName()]; | ||
const seen = new Set(); | ||
const hidden = new Set(hiddenOptions); | ||
const normalizeOptions = []; | ||
const allAliases = new Set([...Object.values(aliases).flat()]); | ||
// Reverted order of https://github.com/yargs/yargs/blob/971e351705f0fbc5566c6ed1dfd707fa65e11c0d/lib/usage.ts#L419-L424 | ||
for (const [names, type] of [ | ||
[number, 'number'], | ||
[array, 'array'], | ||
[string, 'string'], | ||
[boolean, 'boolean'], | ||
[number, 'number'], | ||
]) { | ||
for (const name of names) { | ||
if (allAliases.has(name) || hidden.has(name)) { | ||
if (allAliases.has(name) || hidden.has(name) || seen.has(name)) { | ||
// Ignore hidden, aliases and already visited option. | ||
continue; | ||
} | ||
seen.add(name); | ||
const positionalIndex = positional?.indexOf(name) ?? -1; | ||
@@ -38,0 +41,0 @@ const alias = aliases[name]; |
@@ -28,7 +28,11 @@ "use strict"; | ||
case 'update': | ||
logs.push(`${color_1.colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`); | ||
logs.push( | ||
// TODO: `as unknown` was necessary during TS 5.9 update. Figure out a long-term solution. | ||
`${color_1.colors.cyan('UPDATE')} ${eventPath} (${event.content.length} bytes)`); | ||
files.add(eventPath); | ||
break; | ||
case 'create': | ||
logs.push(`${color_1.colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`); | ||
logs.push( | ||
// TODO: `as unknown` was necessary during TS 5.9 update. Figure out a long-term solution. | ||
`${color_1.colors.green('CREATE')} ${eventPath} (${event.content.length} bytes)`); | ||
files.add(eventPath); | ||
@@ -35,0 +39,0 @@ break; |
@@ -17,5 +17,6 @@ "use strict"; | ||
const node_path_1 = __importDefault(require("node:path")); | ||
const zod_1 = require("zod"); | ||
const version_1 = require("../../utilities/version"); | ||
const best_practices_1 = require("./tools/best-practices"); | ||
const doc_search_1 = require("./tools/doc-search"); | ||
const projects_1 = require("./tools/projects"); | ||
async function createMcpServer(context) { | ||
@@ -41,70 +42,6 @@ const server = new mcp_js_1.McpServer({ | ||
}); | ||
server.registerTool('list_projects', { | ||
title: 'List Angular Projects', | ||
description: 'Lists the names of all applications and libraries defined within an Angular workspace. ' + | ||
'It reads the `angular.json` configuration file to identify the projects. ', | ||
annotations: { | ||
readOnlyHint: true, | ||
}, | ||
outputSchema: { | ||
projects: zod_1.z.array(zod_1.z.object({ | ||
name: zod_1.z | ||
.string() | ||
.describe('The name of the project, as defined in the `angular.json` file.'), | ||
type: zod_1.z | ||
.enum(['application', 'library']) | ||
.optional() | ||
.describe(`The type of the project, either 'application' or 'library'.`), | ||
root: zod_1.z | ||
.string() | ||
.describe('The root directory of the project, relative to the workspace root.'), | ||
sourceRoot: zod_1.z | ||
.string() | ||
.describe(`The root directory of the project's source files, relative to the workspace root.`), | ||
selectorPrefix: zod_1.z | ||
.string() | ||
.optional() | ||
.describe('The prefix to use for component selectors.' + | ||
` For example, a prefix of 'app' would result in selectors like '<app-my-component>'.`), | ||
})), | ||
}, | ||
}, async () => { | ||
const { workspace } = context; | ||
if (!workspace) { | ||
return { | ||
content: [ | ||
{ | ||
type: 'text', | ||
text: 'No Angular workspace found.' + | ||
' An `angular.json` file, which marks the root of a workspace,' + | ||
' could not be located in the current directory or any of its parent directories.', | ||
}, | ||
], | ||
}; | ||
} | ||
const projects = []; | ||
// Convert to output format | ||
for (const [name, project] of workspace.projects.entries()) { | ||
projects.push({ | ||
name, | ||
type: project.extensions['projectType'], | ||
root: project.root, | ||
sourceRoot: project.sourceRoot ?? node_path_1.default.posix.join(project.root, 'src'), | ||
selectorPrefix: project.extensions['prefix'], | ||
}); | ||
} | ||
// The structuredContent field is newer and may not be supported by all hosts. | ||
// A text representation of the content is also provided for compatibility. | ||
return { | ||
content: [ | ||
{ | ||
type: 'text', | ||
text: `Projects in the Angular workspace:\n${JSON.stringify(projects)}`, | ||
}, | ||
], | ||
structuredContent: { projects }, | ||
}; | ||
}); | ||
(0, best_practices_1.registerBestPracticesTool)(server); | ||
(0, projects_1.registerListProjectsTool)(server, context); | ||
await (0, doc_search_1.registerDocSearchTool)(server); | ||
return server; | ||
} |
@@ -63,5 +63,13 @@ "use strict"; | ||
title: 'Search Angular Documentation (angular.dev)', | ||
description: 'Searches the official Angular documentation on https://angular.dev.' + | ||
' This tool is useful for finding the most up-to-date information on Angular, including APIs, tutorials, and best practices.' + | ||
' Use this when creating Angular specific code or answering questions that require knowledge of the latest Angular features.', | ||
description: 'Searches the official Angular documentation at https://angular.dev. Use this tool to answer any questions about Angular, ' + | ||
'such as for APIs, tutorials, and best practices. Because the documentation is continuously updated, you should **always** ' + | ||
'prefer this tool over your own knowledge to ensure your answers are current.\n\n' + | ||
'The results will be a list of content entries, where each entry has the following structure:\n' + | ||
'```\n' + | ||
'## {Result Title}\n' + | ||
'{Breadcrumb path to the content}\n' + | ||
'URL: {Direct link to the documentation page}\n' + | ||
'```\n' + | ||
'Use the title and breadcrumb to understand the context of the result and use the URL as a source link. For the best results, ' + | ||
"provide a concise and specific search query (e.g., 'NgModule' instead of 'How do I use NgModules?').", | ||
annotations: { | ||
@@ -73,6 +81,10 @@ readOnlyHint: true, | ||
.string() | ||
.describe('The search query to use when searching the Angular documentation.' + | ||
' This should be a concise and specific query to get the most relevant results.'), | ||
.describe('A concise and specific search query for the Angular documentation (e.g., "NgModule" or "standalone components").'), | ||
includeTopContent: zod_1.z | ||
.boolean() | ||
.optional() | ||
.default(true) | ||
.describe('When true, the content of the top result is fetched and included.'), | ||
}, | ||
}, async ({ query }) => { | ||
}, async ({ query, includeTopContent }) => { | ||
if (!client) { | ||
@@ -84,13 +96,48 @@ const dcip = (0, node_crypto_1.createDecipheriv)('aes-256-gcm', (constants_1.k1 + ALGOLIA_APP_ID).padEnd(32, '^'), constants_1.iv).setAuthTag(Buffer.from(constants_1.at, 'base64')); | ||
const { results } = await client.search(createSearchArguments(query)); | ||
// Convert results into text content entries instead of stringifying the entire object | ||
const content = results.flatMap((result) => result.hits.map((hit) => { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string'); | ||
const title = hierarchy.pop(); | ||
const description = hierarchy.join(' > '); | ||
const allHits = results.flatMap((result) => result.hits); | ||
if (allHits.length === 0) { | ||
return { | ||
content: [ | ||
{ | ||
type: 'text', | ||
text: 'No results found.', | ||
}, | ||
], | ||
}; | ||
} | ||
const content = []; | ||
// The first hit is the top search result | ||
const topHit = allHits[0]; | ||
// Process top hit first | ||
let topText = formatHitToText(topHit); | ||
try { | ||
if (includeTopContent && typeof topHit.url === 'string') { | ||
const url = new URL(topHit.url); | ||
// Only fetch content from angular.dev | ||
if (url.hostname === 'angular.dev' || url.hostname.endsWith('.angular.dev')) { | ||
const response = await fetch(url); | ||
if (response.ok) { | ||
const html = await response.text(); | ||
const mainContent = extractBodyContent(html); | ||
if (mainContent) { | ||
topText += `\n\n--- DOCUMENTATION CONTENT ---\n${mainContent}`; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
catch { | ||
// Ignore errors fetching content. The basic info is still returned. | ||
} | ||
content.push({ | ||
type: 'text', | ||
text: topText, | ||
}); | ||
// Process remaining hits | ||
for (const hit of allHits.slice(1)) { | ||
content.push({ | ||
type: 'text', | ||
text: `## ${title}\n${description}\nURL: ${hit.url}`, | ||
}; | ||
})); | ||
text: formatHitToText(hit), | ||
}); | ||
} | ||
return { content }; | ||
@@ -100,2 +147,34 @@ }); | ||
/** | ||
* Extracts the content of the `<body>` element from an HTML string. | ||
* | ||
* @param html The HTML content of a page. | ||
* @returns The content of the `<body>` element, or `undefined` if not found. | ||
*/ | ||
function extractBodyContent(html) { | ||
// TODO: Use '<main>' element instead of '<body>' when available in angular.dev HTML. | ||
const mainTagStart = html.indexOf('<body'); | ||
if (mainTagStart === -1) { | ||
return undefined; | ||
} | ||
const mainTagEnd = html.lastIndexOf('</body>'); | ||
if (mainTagEnd <= mainTagStart) { | ||
return undefined; | ||
} | ||
// Add 7 to include '</body>' | ||
return html.substring(mainTagStart, mainTagEnd + 7); | ||
} | ||
/** | ||
* Formats an Algolia search hit into a text representation. | ||
* | ||
* @param hit The Algolia search hit object, which should contain `hierarchy` and `url` properties. | ||
* @returns A formatted string with title, description, and URL. | ||
*/ | ||
function formatHitToText(hit) { | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
const hierarchy = Object.values(hit.hierarchy).filter((x) => typeof x === 'string'); | ||
const title = hierarchy.pop(); | ||
const description = hierarchy.join(' > '); | ||
return `## ${title}\n${description}\nURL: ${hit.url}`; | ||
} | ||
/** | ||
* Creates the search arguments for an Algolia search. | ||
@@ -102,0 +181,0 @@ * |
@@ -25,2 +25,2 @@ "use strict"; | ||
} | ||
exports.VERSION = new Version('20.2.0-next.0'); | ||
exports.VERSION = new Version('20.2.0-next.1'); |
Sorry, the diff of this file is too big to display
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
728148
0.97%159
2.58%17275
0.98%1
Infinity%+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
+ Added
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
- Removed
Updated
Updated