diff --git a/microsoft-vscode-azext-webview-1.2.0.tgz b/microsoft-vscode-azext-webview-1.2.0.tgz new file mode 100644 index 000000000..68647f7e4 Binary files /dev/null and b/microsoft-vscode-azext-webview-1.2.0.tgz differ diff --git a/package-lock.json b/package-lock.json index f216de134..c285718d8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,12 +20,13 @@ "@azure/arm-storage": "^18.1.0", "@azure/core-client": "^1.7.3", "@azure/core-rest-pipeline": "^1.11.0", + "@azure/functions-skills": "0.0.6-preview", "@azure/storage-blob": "^12.5.0", "@microsoft/vscode-azext-azureappservice": "^5.0.2", "@microsoft/vscode-azext-azureappsettings": "^1.0.0", "@microsoft/vscode-azext-azureutils": "^4.3.0", "@microsoft/vscode-azext-utils": "^4.1.1", - "@microsoft/vscode-azext-webview": "^1.2.0", + "@microsoft/vscode-azext-webview": "file:microsoft-vscode-azext-webview-1.2.0.tgz", "@microsoft/vscode-azureresources-api": "^2.6.2", "@microsoft/vscode-container-client": "^0.3.0", "@microsoft/vscode-processutils": "^0.2.1", @@ -602,6 +603,21 @@ "node": ">=18.0.0" } }, + "node_modules/@azure/functions-skills": { + "version": "0.0.6-preview", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@azure/functions-skills/-/functions-skills-0.0.6-preview.tgz", + "integrity": "sha1-z3VUlc6t33iSHxkuMZyyGN3GlF0=", + "license": "MIT", + "dependencies": { + "applicationinsights": "1.8.10" + }, + "bin": { + "azure-functions-skills": "bin/azure-functions-skills.js" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@azure/identity": { "version": "4.13.1", "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-4.13.1.tgz", @@ -3637,8 +3653,8 @@ }, "node_modules/@microsoft/vscode-azext-webview": { "version": "1.2.0", - "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/@microsoft/vscode-azext-webview/-/vscode-azext-webview-1.2.0.tgz", - "integrity": "sha1-6D4DcHaS8uICi167HnXUZilfIzM=", + "resolved": "file:microsoft-vscode-azext-webview-1.2.0.tgz", + "integrity": "sha512-tReYFF+TsOVUcijoedtgSQnEO9zXkvbvbmS31mjRG2hRZkXJ4alXq6/h+vHR3/q9zP76q7Yv5sB4DcszijsJdg==", "license": "MIT", "dependencies": { "@fluentui/react-components": "^9.56.2", @@ -5554,6 +5570,18 @@ "node": ">= 8" } }, + "node_modules/applicationinsights": { + "version": "1.8.10", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/applicationinsights/-/applicationinsights-1.8.10.tgz", + "integrity": "sha1-//pILNFRmID7iIU2qHCBrAUTBmc=", + "license": "MIT", + "dependencies": { + "cls-hooked": "^4.2.2", + "continuation-local-storage": "^3.2.1", + "diagnostic-channel": "0.3.1", + "diagnostic-channel-publishers": "0.4.4" + } + }, "node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", @@ -5588,6 +5616,40 @@ "node": ">=8" } }, + "node_modules/async-hook-jl": { + "version": "1.7.6", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha1-T9JcL4ZNuvJ5xhDXO/l7GyhZXmg=", + "license": "MIT", + "dependencies": { + "stack-chain": "^1.3.7" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3" + } + }, + "node_modules/async-listener": { + "version": "0.6.10", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/async-listener/-/async-listener-0.6.10.tgz", + "integrity": "sha1-p8l6vlcLpgLXgic8DeYKUePhfLw=", + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^5.3.0", + "shimmer": "^1.1.0" + }, + "engines": { + "node": "<=0.11.8 || >0.11.10" + } + }, + "node_modules/async-listener/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/semver/-/semver-5.7.2.tgz", + "integrity": "sha1-SNVdtzfDKHzUg14X+hP+rOHEHvg=", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -5994,6 +6056,29 @@ "node": ">=12" } }, + "node_modules/cls-hooked": { + "version": "4.2.2", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha1-rS6aQJJoDNr/6y01UdoOIl6uGQg=", + "license": "BSD-2-Clause", + "dependencies": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" + } + }, + "node_modules/cls-hooked/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/semver/-/semver-5.7.2.tgz", + "integrity": "sha1-SNVdtzfDKHzUg14X+hP+rOHEHvg=", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/cockatiel": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/cockatiel/-/cockatiel-3.2.1.tgz", @@ -6045,6 +6130,16 @@ "node": ">=18" } }, + "node_modules/continuation-local-storage": { + "version": "3.2.1", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/continuation-local-storage/-/continuation-local-storage-3.2.1.tgz", + "integrity": "sha1-EfYT906RT+mzTJKtLSj+auHbf/s=", + "license": "BSD-2-Clause", + "dependencies": { + "async-listener": "^0.6.0", + "emitter-listener": "^1.1.1" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -6292,6 +6387,33 @@ "node": ">=8" } }, + "node_modules/diagnostic-channel": { + "version": "0.3.1", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/diagnostic-channel/-/diagnostic-channel-0.3.1.tgz", + "integrity": "sha1-f6oUPhB/hhvjBGU560kI+qs/U/0=", + "license": "MIT", + "dependencies": { + "semver": "^5.3.0" + } + }, + "node_modules/diagnostic-channel-publishers": { + "version": "0.4.4", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.4.4.tgz", + "integrity": "sha1-V8O4C35/V2+VvjolfV6UVQ8AgtY=", + "license": "MIT", + "peerDependencies": { + "diagnostic-channel": "*" + } + }, + "node_modules/diagnostic-channel/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/semver/-/semver-5.7.2.tgz", + "integrity": "sha1-SNVdtzfDKHzUg14X+hP+rOHEHvg=", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, "node_modules/diff": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", @@ -6449,6 +6571,15 @@ "embla-carousel": "8.6.0" } }, + "node_modules/emitter-listener": { + "version": "1.1.2", + "resolved": "https://ms-feed-2.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha1-VrFA6PaZI3Wz18ssqxzHQy2WMug=", + "license": "BSD-2-Clause", + "dependencies": { + "shimmer": "^1.2.0" + } + }, "node_modules/emoji-regex": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", @@ -9889,6 +10020,12 @@ "node": ">=8" } }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://ms-feed-12.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha1-YQhZ994ye1h+/r9QH7QxF/mv8zc=", + "license": "BSD-2-Clause" + }, "node_modules/side-channel": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", @@ -10120,6 +10257,12 @@ "node": "*" } }, + "node_modules/stack-chain": { + "version": "1.3.7", + "resolved": "https://ms-feed-17.pkgs.visualstudio.com/1es-public/_packaging/npm-public/npm/registry/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=", + "license": "MIT" + }, "node_modules/stdin-discarder": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", diff --git a/package.json b/package.json index 6fdebf81b..a7b57cf28 100644 --- a/package.json +++ b/package.json @@ -1623,12 +1623,13 @@ "@azure/arm-storage": "^18.1.0", "@azure/core-client": "^1.7.3", "@azure/core-rest-pipeline": "^1.11.0", + "@azure/functions-skills": "0.0.6-preview", "@azure/storage-blob": "^12.5.0", "@microsoft/vscode-azext-azureappservice": "^5.0.2", "@microsoft/vscode-azext-azureappsettings": "^1.0.0", "@microsoft/vscode-azext-azureutils": "^4.3.0", "@microsoft/vscode-azext-utils": "^4.1.1", - "@microsoft/vscode-azext-webview": "^1.2.0", + "@microsoft/vscode-azext-webview": "file:microsoft-vscode-azext-webview-1.2.0.tgz", "@microsoft/vscode-azureresources-api": "^2.6.2", "@microsoft/vscode-container-client": "^0.3.0", "@microsoft/vscode-processutils": "^0.2.1", diff --git a/src/commands/createNewProject/FunctionsTemplateGalleryController.ts b/src/commands/createNewProject/FunctionsTemplateGalleryController.ts index 1b1820fec..7080d9546 100644 --- a/src/commands/createNewProject/FunctionsTemplateGalleryController.ts +++ b/src/commands/createNewProject/FunctionsTemplateGalleryController.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { AzExtFsExtra, AzureWizard, UserCancelledError, callWithTelemetryAndErrorHandling, parseError, type IActionContext } from '@microsoft/vscode-azext-utils'; -import { TemplateGalleryController, registerWebviewExtensionVariables, type IProjectTemplate as ISharedProjectTemplate, type ProjectCreationEntryPoint, type TemplateGalleryConfig } from '@microsoft/vscode-azext-webview'; +import { TemplateGalleryController, registerWebviewExtensionVariables, type IProjectTemplate as ISharedProjectTemplate, type ProjectCreationEntryPoint, type TemplateGalleryConfig, type TemplateGalleryWorkspaceOptionValues } from '@microsoft/vscode-azext-webview'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; @@ -23,6 +23,8 @@ import { type IProjectWizardContext, type OpenBehavior } from './IProjectWizardC import { OpenBehaviorStep } from './OpenBehaviorStep'; import { OpenFolderStep } from './OpenFolderStep'; +const installAzureFunctionsSkillsOptionId = 'installAzureFunctionsSkills'; + /** * Azure Functions implementation of the shared TemplateGalleryController. * The shared package owns the UI, message routing, and panel lifecycle. @@ -78,6 +80,57 @@ export class FunctionsTemplateGalleryController extends TemplateGalleryControlle headerTitle: localize('templateGallery', 'Template Gallery'), headerSubtitle: localize('templateGallerySubtitle', 'Create a new Azure Functions project from a template'), supportsAiGeneration: true, + workspaceOptions: [ + { + id: installAzureFunctionsSkillsOptionId, + label: localize('installAzureFunctionsSkills', 'Install Azure Functions Skills'), + description: localize('installAzureFunctionsSkillsDescription', 'Recommended. Adds Azure Functions-specific skills to this workspace so Copilot Chat can scaffold, validate, diagnose, and deploy with best-practice guidance.'), + defaultValue: true, + }, + ], + aiGeneration: { + tabLabel: localize('buildWithCopilotChat', 'Build with Copilot Chat'), + introDescription: localize( + 'copilotChatIntroWithSkills', + 'Describe the serverless agent or Azure Functions app you want to build. Azure Functions Skills will be installed into your workspace and Copilot Chat will use them to scaffold, validate, and refine the project.', + ), + promptPlaceholder: localize( + 'copilotChatPromptPlaceholderWithSkills', + 'e.g., Build an Azure Functions app that receives events, processes them, and stores the result in Azure.', + ), + examplesLabel: localize('copilotChatExamplesWithSkills', 'Try an example:'), + examplePrompts: [ + { + label: localize('serverlessAgentLabel', 'Serverless agent'), + prompt: 'Create a serverless agent on Azure Functions that runs every morning, summarizes new Microsoft blog posts, stores session state, and exposes an HTTP endpoint for manual runs. If prerequisites have not been checked yet, run azure-functions-setup first. Then use the azure-functions-agents skill.', + }, + { + label: localize('httpApiCosmosDbLabel', 'HTTP API + Cosmos DB'), + prompt: 'Create an Azure Functions HTTP API that receives JSON payloads, validates them, and stores the records in Azure Cosmos DB. If prerequisites have not been checked yet, run azure-functions-setup first. Then use azure-functions-create.', + }, + { + label: localize('serviceBusConsumerLabel', 'Service Bus consumer'), + prompt: 'Create an Azure Functions Service Bus queue consumer that processes order messages, calls a downstream HTTP API to enrich each order, and writes the result to Blob Storage. If prerequisites have not been checked yet, run azure-functions-setup first. Then use azure-functions-create.', + }, + { + label: localize('timerCleanupLabel', 'Timer-driven cleanup'), + prompt: 'Create an Azure Functions timer-triggered cleanup job that runs every night, scans a Cosmos DB container for documents older than 90 days, archives them to Blob Storage, and then deletes them. If prerequisites have not been checked yet, run azure-functions-setup first. Then use azure-functions-create.', + }, + ], + openChatButtonLabel: localize('openCopilotWithSkills', 'Open in Copilot Chat'), + chatConfirmationDescription: localize( + 'copilotChatConfirmationWithSkills', + 'Copilot Chat will open with a concise Azure Functions prompt. If Azure Functions Skills are selected, they are installed locally before the chat opens.', + ), + capabilitiesTitle: localize('copilotChatSkillsCapabilitiesTitle', 'What Azure Functions Skills add'), + capabilitiesAriaLabel: localize('copilotChatSkillsCapabilitiesAria', 'Azure Functions Skills capabilities'), + capabilities: [ + localize('copilotChatSkillCapabilityCreate', 'Guided Azure Functions and serverless-agent project creation'), + localize('copilotChatSkillCapabilityBestPractices', 'Built-in runtime, trigger, identity, and deployment best practices'), + localize('copilotChatSkillCapabilityValidation', 'Local validation and diagnostics guidance before deployment'), + localize('copilotChatSkillCapabilityDeploy', 'Deployment handoff through Azure Skills when you are ready'), + ], + }, }; FunctionsTemplateGalleryController.currentController = new FunctionsTemplateGalleryController(context, config, initialLocation); @@ -128,13 +181,14 @@ export class FunctionsTemplateGalleryController extends TemplateGalleryControlle return markdown ?? ''; } - protected async createProject(sharedTemplate: ISharedProjectTemplate, language: string, location: string, entryPoint?: ProjectCreationEntryPoint): Promise { + protected async createProject(sharedTemplate: ISharedProjectTemplate, language: string, location: string, options: TemplateGalleryWorkspaceOptionValues, entryPoint?: ProjectCreationEntryPoint): Promise { const template = sharedTemplate as unknown as IProjectTemplate; await callWithTelemetryAndErrorHandling('azureFunctions.templateGallery.createProject', async (actionContext: IActionContext) => { actionContext.telemetry.properties.templateId = template.id; actionContext.telemetry.properties.templateName = template.displayName; actionContext.telemetry.properties.language = language; actionContext.telemetry.properties.entryPoint = entryPoint ?? 'unknown'; + actionContext.telemetry.properties.installAzureFunctionsSkills = String(shouldInstallAzureFunctionsSkills(options)); const projectPath = location; const branch = template.branch || 'main'; @@ -213,6 +267,15 @@ export class FunctionsTemplateGalleryController extends TemplateGalleryControlle // git init is optional } + if (shouldInstallAzureFunctionsSkills(options)) { + this.sendProgress('Installing Azure Functions Skills...'); + const skillsInstall = await installAzureFunctionsSkillsLocally(projectPath); + actionContext.telemetry.properties.skillsInstallResult = skillsInstall.result; + actionContext.telemetry.properties.skillsInstallFilesWritten = String(skillsInstall.filesWritten ?? 0); + } else { + actionContext.telemetry.properties.skillsInstallResult = 'skipped'; + } + actionContext.telemetry.properties.result = 'Succeeded'; // Open the README in the editor before disposing the webview so the user @@ -272,37 +335,17 @@ export class FunctionsTemplateGalleryController extends TemplateGalleryControlle // ── Optional overrides ── - protected override async continueInChat(prompt: string, language: string): Promise { + protected override async continueInChat(prompt: string, language: string, location: string, options: TemplateGalleryWorkspaceOptionValues): Promise { await callWithTelemetryAndErrorHandling('azureFunctions.templateGallery.continueInChat', async (actionContext: IActionContext) => { actionContext.telemetry.properties.language = language; + actionContext.telemetry.properties.skillsInstallTarget = location ? 'galleryLocation' : 'workspaceFallback'; + actionContext.telemetry.properties.installAzureFunctionsSkills = String(shouldInstallAzureFunctionsSkills(options)); - const chatQuery = [ - `Please help me build an Azure Functions app.`, - '', - `**What I want to build:** ${prompt}`, - '', - `**Language:** ${language}`, - '', - `**Programming model & project structure:**`, - languageGrounding(language), - '', - `**General guidelines:**`, - `- Only use real trigger and binding types from the official Azure Functions SDK for ${language}`, - `- Include host.json with extensionBundle configured for any non-HTTP bindings`, - `- Include local.settings.json with the correct FUNCTIONS_WORKER_RUNTIME value`, - `- Add a README.md with local setup steps and how to run the app`, - `- Add brief inline comments explaining the key parts of the code`, - '', - `**Azure Functions best practices:**`, - `- Keep functions stateless — do not store shared state in global variables`, - `- Use output bindings instead of direct SDK/client calls where the binding supports it`, - `- Prefer async/await patterns to avoid blocking the function host`, - `- Use environment variables (application settings) for all secrets and connection strings, never hardcode them`, - `- Use Managed Identity over connection strings when connecting to Azure services`, - `- Keep each function focused on a single responsibility`, - `- Use Durable Functions for long-running workflows or fan-out/fan-in patterns`, - `- Handle errors gracefully and return meaningful HTTP status codes for HTTP triggers`, - ].join('\n'); + const skillsInstall = shouldInstallAzureFunctionsSkills(options) ? await installAzureFunctionsSkillsLocally(location) : undefined; + actionContext.telemetry.properties.skillsInstallResult = skillsInstall?.result ?? 'skipped'; + actionContext.telemetry.properties.skillsInstallFilesWritten = String(skillsInstall?.filesWritten ?? 0); + + const chatQuery = buildCopilotChatQuery(prompt, language, skillsInstall); try { await vscode.commands.executeCommand('workbench.action.chat.open', { @@ -470,6 +513,102 @@ export class FunctionsTemplateGalleryController extends TemplateGalleryControlle } +interface CopilotChatSkillsInstallResult { + result: 'succeeded' | 'failed' | 'skipped'; + filesWritten?: number; + message?: string; +} + +function shouldInstallAzureFunctionsSkills(options: TemplateGalleryWorkspaceOptionValues): boolean { + return options[installAzureFunctionsSkillsOptionId] !== false; +} + +async function installAzureFunctionsSkillsLocally(location: string): Promise { + const targetDir = resolveSkillsInstallTarget(location); + if (!targetDir) { + return { + result: 'skipped', + message: localize('skillsInstallNoWorkspace', 'No workspace folder is available for local Azure Functions Skills installation.'), + }; + } + + try { + const skills = await import('@azure/functions-skills/setup'); + const result = await skills.installLocalSkills({ + targetDir, + agents: ['ghcp'], + checkForUpdates: false, + }); + + return { + result: 'succeeded', + filesWritten: result.filesWritten, + }; + } catch (error) { + const message = parseError(error).message; + ext.outputChannel.appendLog(localize('skillsInstallError', 'Failed to install Azure Functions Skills locally: {0}', message)); + return { result: 'failed', message }; + } +} + +function buildCopilotChatQuery( + prompt: string, + language: string, + skillsInstall: CopilotChatSkillsInstallResult | undefined, +): string { + if (isServerlessAgentPrompt(prompt)) { + return [ + `Please help me build a serverless agent on Azure Functions.`, + '', + `**What I want to build:** ${prompt}`, + ].join('\n'); + } + + return [ + `Please help me build an Azure Functions app.`, + '', + `**What I want to build:** ${prompt}`, + '', + `**Language:** ${language}`, + '', + skillsInstall ? azureFunctionsSkillsSection(skillsInstall) : undefined, + skillsInstall ? '' : undefined, + `**Project guidance:**`, + `- Keep the prompt-to-code flow concise: ask only for missing requirements, then scaffold working files.`, + `- Follow the selected language's current Azure Functions programming model.`, + '', + `**Language grounding:**`, + languageGrounding(language), + ].filter((line): line is string => line !== undefined).join('\n'); +} + +function azureFunctionsSkillsSection(skillsInstall: CopilotChatSkillsInstallResult): string { + const installLine = skillsInstall.result === 'succeeded' + ? `- Azure Functions Skills have been installed locally in this workspace for GitHub Copilot Chat.` + : `- Azure Functions Skills installation was requested for this workspace but did not complete successfully.`; + + return [ + `**Azure Functions Skills:**`, + installLine, + `- If prerequisites have not been checked yet, run azure-functions-setup first.`, + `- Follow the skill routing requested in the prompt: use azure-functions-agents for serverless-agent scenarios and azure-functions-create for standard Azure Functions app creation.`, + `- If Azure Skills are not available for deployment, keep generation local and explain the deployment prerequisite.`, + skillsInstall.message ? `- Install note: ${skillsInstall.message}` : undefined, + ].filter((line): line is string => line !== undefined).join('\n'); +} + +function isServerlessAgentPrompt(prompt: string): boolean { + return /\b(serverless agent|azure-functions-agents)\b/i.test(prompt); +} + +function resolveSkillsInstallTarget(location: string): string | undefined { + if (location) { + return location; + } + + return vscode.workspace.workspaceFolders?.[0]?.uri.fsPath; +} + function languageGrounding(language: string): string { switch (language) { case 'TypeScript':