From 5c098a549d9c98003cea790c35e6f767da68d647 Mon Sep 17 00:00:00 2001 From: John Yenter-Briars Date: Wed, 15 Oct 2025 10:10:59 -0500 Subject: [PATCH 1/6] initial logic - not working --- CHANGELOG.md | 5 +- package-lock.json | 67 +++++++++++++- package.json | 8 +- src/classes/syncer/BambooManager.ts | 15 ++++ src/dataverse/DataverseClient.ts | 133 +++++++++++++++++++++++++++- src/extension.ts | 3 +- 6 files changed, 223 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3806e87..9be8575 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -47,4 +47,7 @@ ## [0.3.3] - Load / error messages controlled via verbosity setting -- Update internal fspath handling \ No newline at end of file +- Update internal fspath handling + +## [0.3.4] +- Support for Plugin Package \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 2c19232..c951593 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,23 +1,27 @@ { "name": "bamboo-pa-vscode", - "version": "0.3.0", + "version": "0.3.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "bamboo-pa-vscode", - "version": "0.3.0", + "version": "0.3.4", "dependencies": { "@azure/msal-node": "^3.2.3", + "adm-zip": "^0.5.16", "jsonwebtoken": "^9.0.2", - "open": "^10.1.0" + "open": "^10.1.0", + "xml2js": "^0.6.2" }, "devDependencies": { + "@types/adm-zip": "^0.5.7", "@types/glob": "^7.1.4", "@types/jsonwebtoken": "^9.0.9", "@types/mocha": "^9.0.0", "@types/node": "14.x", "@types/vscode": "^1.73.0", + "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/parser": "^4.31.1", "@vscode/test-electron": "^2.4.1", @@ -260,6 +264,16 @@ "node": ">= 8" } }, + "node_modules/@types/adm-zip": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@types/adm-zip/-/adm-zip-0.5.7.tgz", + "integrity": "sha512-DNEs/QvmyRLurdQPChqq0Md4zGvPwHerAJYWk9l2jCbD1VPpnzRJorOdiq4zsw09NFbYnhfsoEhWtxIzXpn2yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/glob": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.2.0.tgz", @@ -324,6 +338,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/xml2js": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.14.tgz", + "integrity": "sha512-4YnrRemBShWRO2QjvUin8ESA41rH+9nQGLUGZV/1IDhi3SL9OhdpNC/MrulTWuptXKwhx/aDxE7toV0f/ypIXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "4.33.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.33.0.tgz", @@ -535,6 +559,15 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/adm-zip": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.16.tgz", + "integrity": "sha512-TGw5yVi4saajsSEgz25grObGHEUaDrniwvA2qwSC060KfqGPdglhvPMA2lPIoxs3PQIItj2iag35fONcQqgUaQ==", + "license": "MIT", + "engines": { + "node": ">=12.0" + } + }, "node_modules/agent-base": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", @@ -2919,6 +2952,12 @@ ], "license": "MIT" }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "license": "ISC" + }, "node_modules/semver": { "version": "7.7.1", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", @@ -3315,6 +3354,28 @@ "dev": true, "license": "ISC" }, + "node_modules/xml2js": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", + "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", + "license": "MIT", + "dependencies": { + "sax": ">=0.6.0", + "xmlbuilder": "~11.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/xmlbuilder": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", + "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", + "license": "MIT", + "engines": { + "node": ">=4.0" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index e5ce581..0c9c6e4 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "url": "https://github.com/Root16/bamboo" }, "description": "Enables users to create, update, and publish Web Resources and Custom Controls for Microsoft Power Platform — directly from VS Code.", - "version": "0.3.3", + "version": "0.3.4", "icon": "resources/bamboo-green.png", "engines": { "vscode": "^1.73.0" @@ -103,11 +103,13 @@ "package": "vsce package" }, "devDependencies": { + "@types/adm-zip": "^0.5.7", "@types/glob": "^7.1.4", "@types/jsonwebtoken": "^9.0.9", "@types/mocha": "^9.0.0", "@types/node": "14.x", "@types/vscode": "^1.73.0", + "@types/xml2js": "^0.4.14", "@typescript-eslint/eslint-plugin": "^4.31.1", "@typescript-eslint/parser": "^4.31.1", "@vscode/test-electron": "^2.4.1", @@ -118,7 +120,9 @@ }, "dependencies": { "@azure/msal-node": "^3.2.3", + "adm-zip": "^0.5.16", "jsonwebtoken": "^9.0.2", - "open": "^10.1.0" + "open": "^10.1.0", + "xml2js": "^0.6.2" } } diff --git a/src/classes/syncer/BambooManager.ts b/src/classes/syncer/BambooManager.ts index 3a6e0fc..3f0201e 100644 --- a/src/classes/syncer/BambooManager.ts +++ b/src/classes/syncer/BambooManager.ts @@ -341,4 +341,19 @@ export class BambooManager { } } + public async updatePluginPackage( + ): Promise { + const token = await this.getToken(); + if (token === null) { + return; + } + + const [success, error] = await this.client.registerPluginPackage( + "C:\\Users\\jyenterbriars\\dev\\bamboo_test\\CRM Customizations\\Plugins\\JYB.Plugins\\bin\\Debug\\JYB.Plugins.1.0.0.nupkg", + token, + "CrmCore"); + + const foo = 10; + } + } diff --git a/src/dataverse/DataverseClient.ts b/src/dataverse/DataverseClient.ts index f2ab6d5..0e45da0 100644 --- a/src/dataverse/DataverseClient.ts +++ b/src/dataverse/DataverseClient.ts @@ -8,6 +8,8 @@ import { ISolution } from "./ISolution"; import { logErrorMessage, logMessage, logMessageWithProgress, logTemporaryMessage, VerboseSetting } from "../log/message"; import { BambooConfig } from "../classes/syncer/BambooConfig"; import { ICustomControl } from "./ICustomControl"; +import AdmZip from "adm-zip"; +import { parseStringPromise } from "xml2js"; import * as crypto from 'crypto'; export class DataverseClient { @@ -17,6 +19,7 @@ export class DataverseClient { private publishApi: string; private publishAllApi: string; private importSolutionApi: string; + private pluginPackagesApi: string; constructor(private config: BambooConfig) { this.webResourcesApi = `${this.config.baseUrl}/api/data/v9.2/webresourceset`; @@ -25,6 +28,7 @@ export class DataverseClient { this.publishApi = `${this.config.baseUrl}/api/data/v9.2/PublishXml`; this.publishAllApi = `${this.config.baseUrl}/api/data/v9.0/PublishAllXml`; this.importSolutionApi = `${this.config.baseUrl}/api/data/v9.0/ImportSolution`; + this.pluginPackagesApi = `${this.config.baseUrl}/api/data/v9.2/pluginpackages`; } public async syncSolution(solutionName: string, solutionPath: string, token: string): Promise<[boolean, string | null]> { @@ -304,7 +308,7 @@ export class DataverseClient { }); if (!response.ok) { - const data = await response.json(); + const data = await response.json(); console.log(data); return [false, `Failed to publish all customizations: ${response.statusText}`]; } @@ -449,6 +453,133 @@ export class DataverseClient { return [true, null]; } + /** + * Registers (create or update) a plugin package in D365. + * @param filePath Path to the .nupkg file + * @param token OAuth access token + * @param solutionUniqueName (Optional) solution to add the package to + */ + public async registerPluginPackage( + filePath: string, + token: string, + solutionUniqueName?: string + ): Promise<[boolean, string | null]> { + try { + const { id, version } = await this.analyzeNupkg(filePath); + if (!id || !version) throw new Error("Could not read .nuspec metadata"); + + const content = (await fs.readFile(filePath)).toString("base64"); + const name = id; + const uniquename = id; + + const existing = await this.findPluginPackage('jyb_JYB.Plugins', token); + + if (existing) { + console.log(`Updating existing plugin package: ${name}`); + await this.updatePluginPackage(existing.pluginpackageid, content, token); + } else { + return [false, `Package: ${name} is not found. Creating a plugin package is not implemented.`]; + } + + return await this.publishAllCustomizations(token); + } catch (err: any) { + console.error("Error registering plugin package:", err); + return [false, err.message]; + } + } + + private async analyzeNupkg(filePath: string): Promise<{ id: string; version: string }> { + try { + await fs.access(filePath); + } catch { + throw new Error(`File not found: ${filePath}`); + } + + const buffer = await fs.readFile(filePath); + const zip = new AdmZip(buffer); + const nuspecEntry = zip.getEntries().find(e => e.entryName.endsWith(".nuspec")); + if (!nuspecEntry) throw new Error("Could not find .nuspec in package"); + + const xmlContent = nuspecEntry.getData().toString("utf-8"); + const parsed = await parseStringPromise(xmlContent); + const metadata = parsed.package?.metadata?.[0]; + const id = metadata?.id?.[0]; + const version = metadata?.version?.[0]; + + return { id, version }; + } + + private async findPluginPackage(name: string, token: string): Promise { + const query = `${this.pluginPackagesApi}?$select=pluginpackageid,name&$filter=name eq '${name}'`; + //@ts-expect-error cause i said so + const res = await fetch(query, { + headers: { Authorization: `Bearer ${token}`, Accept: "application/json" }, + }); + if (!res.ok) throw new Error(`Failed to query pluginpackage: ${res.statusText}`); + const data = await res.json(); + return data.value?.[0] ?? null; + } + + private async createPluginPackage( + name: string, + version: string, + content: string, + solutionUniqueName: string | undefined, + token: string + ): Promise { + const body: any = { + name, + uniquename: name, + version, + content, + }; + + if (solutionUniqueName) { + body["solutionid@odata.bind"] = `/solutions(${solutionUniqueName})`; + } + + //@ts-expect-error cause i said so + const res = await fetch(`${this.config.baseUrl}/pluginpackages`, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify(body), + }); + + if (!res.ok) { + const err = await res.text(); + throw new Error(`Failed to create pluginpackage: ${res.statusText} ${err}`); + } + + const location = res.headers.get("OData-EntityId"); + const id = location?.match(/\(([^)]+)\)/)?.[1]; + if (!id) throw new Error("Could not extract pluginpackage ID from response"); + + return id; + } + + private async updatePluginPackage(id: string, content: string, token: string): Promise { + //@ts-expect-error cause i said so + const res = await fetch(`${this.pluginPackagesApi}(${id})`, { + method: "PATCH", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json", + Accept: "application/json", + }, + body: JSON.stringify({ content }), + }); + + if (!res.ok) { + const err = await res.text(); + throw new Error(`Failed to update pluginpackage: ${res.statusText} ${err}`); + } + } + + public async getOAuthToken(): Promise { const cachedToken = await this.loadCachedToken(); if (cachedToken && cachedToken.expires_at > Date.now() + 60_000) { diff --git a/src/extension.ts b/src/extension.ts index f126412..0a0922e 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -65,7 +65,8 @@ export async function activate(context: vscode.ExtensionContext) { const currentWorkspace = currentWorkspaceFolders![0]; - await bambooManager.syncCurrentFile(currentWorkspace, currentOpenFile); + // await bambooManager.syncCurrentFile(currentWorkspace, currentOpenFile); + await bambooManager.updatePluginPackage(); }); vscode.commands.registerCommand('bamboo.syncAllFiles', async () => { From 942116418d6835c0b1107104ff645c2a6809bcdb Mon Sep 17 00:00:00 2001 From: John Yenter-Briars Date: Fri, 17 Oct 2025 08:16:15 -0500 Subject: [PATCH 2/6] seems to work --- src/dataverse/DataverseClient.ts | 102 ++++++++++++++++++++----------- 1 file changed, 66 insertions(+), 36 deletions(-) diff --git a/src/dataverse/DataverseClient.ts b/src/dataverse/DataverseClient.ts index 0e45da0..3b2fd98 100644 --- a/src/dataverse/DataverseClient.ts +++ b/src/dataverse/DataverseClient.ts @@ -20,6 +20,7 @@ export class DataverseClient { private publishAllApi: string; private importSolutionApi: string; private pluginPackagesApi: string; + private pluginPackagesExportKeyApi: string; constructor(private config: BambooConfig) { this.webResourcesApi = `${this.config.baseUrl}/api/data/v9.2/webresourceset`; @@ -29,6 +30,7 @@ export class DataverseClient { this.publishAllApi = `${this.config.baseUrl}/api/data/v9.0/PublishAllXml`; this.importSolutionApi = `${this.config.baseUrl}/api/data/v9.0/ImportSolution`; this.pluginPackagesApi = `${this.config.baseUrl}/api/data/v9.2/pluginpackages`; + this.pluginPackagesExportKeyApi = `${this.config.baseUrl}/api/data/v9.2/UpdatePluginTypeExportKey`; } public async syncSolution(solutionName: string, solutionPath: string, token: string): Promise<[boolean, string | null]> { @@ -477,6 +479,7 @@ export class DataverseClient { if (existing) { console.log(`Updating existing plugin package: ${name}`); await this.updatePluginPackage(existing.pluginpackageid, content, token); + await this.refreshAllPluginTypesForPackage(existing.pluginpackageid, token); } else { return [false, `Package: ${name} is not found. Creating a plugin package is not implemented.`]; } @@ -520,65 +523,92 @@ export class DataverseClient { return data.value?.[0] ?? null; } - private async createPluginPackage( - name: string, - version: string, - content: string, - solutionUniqueName: string | undefined, - token: string - ): Promise { - const body: any = { - name, - uniquename: name, - version, - content, - }; - - if (solutionUniqueName) { - body["solutionid@odata.bind"] = `/solutions(${solutionUniqueName})`; - } - + private async updatePluginPackage(id: string, content: string, token: string): Promise { //@ts-expect-error cause i said so - const res = await fetch(`${this.config.baseUrl}/pluginpackages`, { - method: "POST", + const res = await fetch(`${this.pluginPackagesApi}(${id})`, { + method: "PATCH", headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json", Accept: "application/json", }, - body: JSON.stringify(body), + body: JSON.stringify({ content }), }); if (!res.ok) { const err = await res.text(); - throw new Error(`Failed to create pluginpackage: ${res.statusText} ${err}`); + logErrorMessage(`Failed to update pluginpackage: ${res.statusText} ${err}`, VerboseSetting.High); } + } - const location = res.headers.get("OData-EntityId"); - const id = location?.match(/\(([^)]+)\)/)?.[1]; - if (!id) throw new Error("Could not extract pluginpackage ID from response"); + private async refreshAllPluginTypesForPackage( + pluginPackageId: string, + token: string + ): Promise { + const apiBase = `${this.config.baseUrl}/api/data/v9.0`; - return id; - } + const fetchXml = ` + + + + + + + + + + + + `; + + const url = `${apiBase}/plugintypes?fetchXml=${encodeURIComponent(fetchXml)}`; - private async updatePluginPackage(id: string, content: string, token: string): Promise { //@ts-expect-error cause i said so - const res = await fetch(`${this.pluginPackagesApi}(${id})`, { - method: "PATCH", + const typeRes = await fetch(url, { headers: { Authorization: `Bearer ${token}`, - "Content-Type": "application/json", Accept: "application/json", + "OData-Version": "4.0", + "OData-MaxVersion": "4.0", }, - body: JSON.stringify({ content }), }); - if (!res.ok) { - const err = await res.text(); - throw new Error(`Failed to update pluginpackage: ${res.statusText} ${err}`); + if (!typeRes.ok) { + const err = await typeRes.text(); + logErrorMessage(`Failed to retrieve plugintypes: ${typeRes.status} ${typeRes.statusText}\n${err}`, VerboseSetting.High); } - } + const { value: pluginTypes } = await typeRes.json(); + if (!pluginTypes || pluginTypes.length === 0) { + logErrorMessage("No plugintypes found for this package.", VerboseSetting.High); + return; + } + + for (const pluginType of pluginTypes) { + const refreshUrl = `${apiBase}/plugintypes(${pluginType.plugintypeid})/Microsoft.Dynamics.CRM.UpdatePluginTypeExportKey`; + + //@ts-expect-error cause i said so + const res = await fetch(refreshUrl, { + method: "POST", + headers: { + Authorization: `Bearer ${token}`, + "Content-Type": "application/json; charset=utf-8", + Accept: "application/json", + "OData-Version": "4.0", + "OData-MaxVersion": "4.0", + }, + body: JSON.stringify({}), // empty body + }); + + if (!res.ok) { + const errText = await res.text(); + //Even a fail response is a success. Go figure + logMessage('Even though the .', VerboseSetting.High); + } else { + logMessage('Success? This should never hit.', VerboseSetting.High); + } + } + } public async getOAuthToken(): Promise { const cachedToken = await this.loadCachedToken(); From daa33ae971f5960cbe921be708aac042b9c0b1c1 Mon Sep 17 00:00:00 2001 From: John Yenter-Briars Date: Fri, 17 Oct 2025 08:21:03 -0500 Subject: [PATCH 3/6] no need to publish all --- src/dataverse/DataverseClient.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/dataverse/DataverseClient.ts b/src/dataverse/DataverseClient.ts index 3b2fd98..789dec1 100644 --- a/src/dataverse/DataverseClient.ts +++ b/src/dataverse/DataverseClient.ts @@ -484,7 +484,7 @@ export class DataverseClient { return [false, `Package: ${name} is not found. Creating a plugin package is not implemented.`]; } - return await this.publishAllCustomizations(token); + return [true, null]; } catch (err: any) { console.error("Error registering plugin package:", err); return [false, err.message]; From 040a3bf8b4cd1dd5ca1a486381170d4cb1ed4b65 Mon Sep 17 00:00:00 2001 From: John Yenter-Briars Date: Fri, 17 Oct 2025 09:44:39 -0500 Subject: [PATCH 4/6] documentation + command --- README.md | 15 +++++++++++-- package.json | 5 +++++ src/classes/syncer/BambooConfig.ts | 6 +++++ src/classes/syncer/BambooManager.ts | 31 +++++++++++++++++++++----- src/dataverse/DataverseClient.ts | 15 ++++++++----- src/extension.ts | 34 +++++++++++++++++++++++++++-- 6 files changed, 91 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index c6774b7..9cfd3dc 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,13 @@ Bamboo provides the following features inside VS Code: "solutionName": "ControlTwoSolution" }, ... + ], + "pluginPackages": [ + { + "pluginPackageName": "new_NEW.Plugins", + "relativePathOnDiskToNugetPackage": "path/to/NEW.Plugins.1.0.0.nupkg" + }, + ... ] } ``` @@ -81,11 +88,12 @@ Bamboo provides the following features inside VS Code: - `baseUrl` must *not* end with a `/`. - The [app registration](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/walkthrough-register-app-azure-active-directory#confidential-client-app-registration) specified must have: - Access to the specified Dataverse environment - - The appropiate Security Role necessary to: + - The appropriate Security Role necessary to: - Upload solutions - Publish solutions - Upload web resources - Publish web resources + - Upload plugin packages - Add components to solutions - `relativePathOnDisk` and `relativePathOnDiskToSolution` must *not* start with a `/`. - For web resources, `dataverseName` and `relativePathOnDisk` don't *need* to be similar (as shown in the example), this is just encouraged for ease of development @@ -97,6 +105,7 @@ Bamboo provides the following features inside VS Code: | `bamboo.syncCurrentFile` | Sync current file. (Must be present on conf.) | | `bamboo.syncAllFiles` | Sync all files. (Each file present in the conf.) | | `bamboo.syncCustomControl` | Sync a Custom Control. (Opens up a choice dropdown for each control specified in the conf.) | +| `bamboo.syncPluginPackage` | Sync a Plugin Package. (Opens up a choice dropdown for each Plugin Package specified in the conf.) | - All command can be run in the command palette. @@ -125,7 +134,9 @@ Bamboo provides the following features inside VS Code: - [❌] Automatically add custom controls to solution - [❌] Manage upload / sync from context of tree view - [❌] Sync data from Power Apps to local files -- [❌] Plugin support +- [❌] Create a plugin package or plugin assembly +- [✅] Update / sync a *Plugin Package* +- [❌] Update / sync a *Plugin Assembly* ## License Distributed under the MIT License. See [`LICENSE`](LICENSE) for more information. diff --git a/package.json b/package.json index 0c9c6e4..d51a78c 100644 --- a/package.json +++ b/package.json @@ -80,6 +80,11 @@ "light": "resources/light/refresh.svg", "dark": "resources/dark/refresh.svg" } + }, + { + "command": "bamboo.syncPluginPackage", + "title": "Sync a Plugin Package.", + "category": "Bamboo" } ], "menus": { diff --git a/src/classes/syncer/BambooConfig.ts b/src/classes/syncer/BambooConfig.ts index 589df3d..578a254 100644 --- a/src/classes/syncer/BambooConfig.ts +++ b/src/classes/syncer/BambooConfig.ts @@ -4,6 +4,7 @@ export interface BambooConfig { credential: Credential; webResources: WebResourceMapping[]; customControls: CustomControlMapping[]; + pluginPackages: PluginPackageMapping[]; } export enum CredentialType { @@ -28,3 +29,8 @@ export interface CustomControlMapping { relativePathOnDiskToSolution: string; solutionName: string; } + +export interface PluginPackageMapping { + pluginPackageName: string; + relativePathOnDiskToNugetPackage: string; +} diff --git a/src/classes/syncer/BambooManager.ts b/src/classes/syncer/BambooManager.ts index 3f0201e..25986bf 100644 --- a/src/classes/syncer/BambooManager.ts +++ b/src/classes/syncer/BambooManager.ts @@ -1,6 +1,6 @@ import * as vscode from 'vscode'; -import { BambooConfig, CredentialType, CustomControlMapping } from './BambooConfig'; -import path from 'path'; +import { BambooConfig, CredentialType, CustomControlMapping, PluginPackageMapping } from './BambooConfig'; +import path, { relative } from 'path'; import { IWebResource } from '../../dataverse/IWebResource'; import { logErrorMessage, logMessage, logMessageWithProgress, logTemporaryMessage, VerboseSetting } from '../../log/message'; import { DataverseClient } from '../../dataverse/DataverseClient'; @@ -113,6 +113,7 @@ export class BambooManager { solutionUniqueName: rawData.solutionUniqueName, webResources: rawData.webResources, customControls: rawData.customControls, + pluginPackages: rawData.pluginPackages, credential: { ...rawData.credential, type: credentialTypeMap[rawData.credential.type] ?? CredentialType.ClientSecret, @@ -342,18 +343,36 @@ export class BambooManager { } public async updatePluginPackage( + currentWorkspace: vscode.WorkspaceFolder, + pluginPackage: PluginPackageMapping ): Promise { + const config = await this.getConfig(); + if (!config) { + return; + } + const token = await this.getToken(); if (token === null) { return; } - const [success, error] = await this.client.registerPluginPackage( - "C:\\Users\\jyenterbriars\\dev\\bamboo_test\\CRM Customizations\\Plugins\\JYB.Plugins\\bin\\Debug\\JYB.Plugins.1.0.0.nupkg", + const workspaceRoot = currentWorkspace.uri.fsPath; + + const fullPath = path.join(workspaceRoot, pluginPackage.relativePathOnDiskToNugetPackage); + + const normalizedPath = path.normalize(fullPath).replace(/\\/g, "/"); + + const [success, errorMessage] = await this.client.registerPluginPackage( + pluginPackage.pluginPackageName, + normalizedPath, token, - "CrmCore"); + ); - const foo = 10; + if (success) { + logTemporaryMessage(`Synced plugin package: ${pluginPackage.pluginPackageName}.`, VerboseSetting.Low); + } else { + logErrorMessage(errorMessage!, VerboseSetting.Low); + } } } diff --git a/src/dataverse/DataverseClient.ts b/src/dataverse/DataverseClient.ts index 789dec1..285a62f 100644 --- a/src/dataverse/DataverseClient.ts +++ b/src/dataverse/DataverseClient.ts @@ -462,9 +462,9 @@ export class DataverseClient { * @param solutionUniqueName (Optional) solution to add the package to */ public async registerPluginPackage( + pluginPackageName: string, filePath: string, token: string, - solutionUniqueName?: string ): Promise<[boolean, string | null]> { try { const { id, version } = await this.analyzeNupkg(filePath); @@ -474,12 +474,17 @@ export class DataverseClient { const name = id; const uniquename = id; - const existing = await this.findPluginPackage('jyb_JYB.Plugins', token); + const existing = await this.findPluginPackage(pluginPackageName, token); if (existing) { - console.log(`Updating existing plugin package: ${name}`); - await this.updatePluginPackage(existing.pluginpackageid, content, token); - await this.refreshAllPluginTypesForPackage(existing.pluginpackageid, token); + await logMessageWithProgress(`Updating existing plugin package: ${name}`, () => { + return this.updatePluginPackage(existing.pluginpackageid, content, token); + }); + + await logMessageWithProgress(`Refreshing types for plugin package: ${name}`, () => { + return this.refreshAllPluginTypesForPackage(existing.pluginpackageid, token); + }); + } else { return [false, `Package: ${name} is not found. Creating a plugin package is not implemented.`]; } diff --git a/src/extension.ts b/src/extension.ts index 0a0922e..7a2b284 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -65,8 +65,7 @@ export async function activate(context: vscode.ExtensionContext) { const currentWorkspace = currentWorkspaceFolders![0]; - // await bambooManager.syncCurrentFile(currentWorkspace, currentOpenFile); - await bambooManager.updatePluginPackage(); + await bambooManager.syncCurrentFile(currentWorkspace, currentOpenFile); }); vscode.commands.registerCommand('bamboo.syncAllFiles', async () => { @@ -112,6 +111,37 @@ export async function activate(context: vscode.ExtensionContext) { } }); + vscode.commands.registerCommand('bamboo.syncPluginPackage', async () => { + const currentWorkspaceFolders = vscode.workspace.workspaceFolders; + if (currentWorkspaceFolders === undefined || currentWorkspaceFolders?.length > 1) { + logErrorMessage(`Either no workspace is open - or too many are! Please open only one workspace in order to use Bamboo`, VerboseSetting.High); + return; + } + + const currentWorkspacePath = currentWorkspaceFolders![0]; + + const config = await bambooManager.getConfig(); + + if (config === null) { + return; + } + + const items: vscode.QuickPickItem[] = config.pluginPackages.map(c => { + return { label: c.pluginPackageName, description: c.relativePathOnDiskToNugetPackage }; + }); + + const selected = await vscode.window.showQuickPick(items, { + placeHolder: 'Select a Plugin Package...', + canPickMany: false + }); + + if (selected) { + const selectedPluginPackages = config.pluginPackages.filter(c => c.pluginPackageName === selected.label)![0]; + + await bambooManager.updatePluginPackage(currentWorkspacePath, selectedPluginPackages); + } + }); + logMessage(`Bamboo initialized successfully.`, VerboseSetting.High) } From b80f53a08bddda0a4c49bb71ba8dfb95018f978d Mon Sep 17 00:00:00 2001 From: John Yenter-Briars Date: Fri, 17 Oct 2025 09:59:48 -0500 Subject: [PATCH 5/6] readme --- README.md | 24 +++++++++++------------- images/command_palette.png | Bin 8155 -> 0 bytes images/command_palette2.png | Bin 0 -> 13914 bytes 3 files changed, 11 insertions(+), 13 deletions(-) delete mode 100644 images/command_palette.png create mode 100644 images/command_palette2.png diff --git a/README.md b/README.md index 9cfd3dc..4f8570d 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ Bamboo is a simple, friendly, and ⚡*blazingly*⚡ fast customization manager, designed to speed up development time on the [Microsoft Power Platform](https://powerplatform.microsoft.com/en-us/). 🚀 -Currently supporting [web resources](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/web-resources) and [custom controls](https://learn.microsoft.com/en-us/power-apps/developer/component-framework/create-custom-controls-using-pcf), Bamboo provides a seamless experience for developers to edit and manage these solution components - all from within VS Code. +Currently supporting [web resources](https://learn.microsoft.com/en-us/power-apps/developer/model-driven-apps/web-resources), [custom controls](https://learn.microsoft.com/en-us/power-apps/developer/component-framework/create-custom-controls-using-pcf), and [plugin packages](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/build-and-package), Bamboo provides a seamless experience for developers to edit and manage these solution components - all from within VS Code. ## Features Bamboo provides the following features inside VS Code: @@ -12,21 +12,23 @@ Bamboo provides the following features inside VS Code: - Add web resources to a solution automatically. - Manage custom controls (PCF components) through the import + publish of solutions. - List all web resources and custom controls in a given solution in a VS Code tree view. +- Update plugin packages #### Component Tree ![Component Tree](./images/component_tree.png) #### Commands -![Command Palette](./images/command_palette.png) +![Command Palette](./images/command_palette2.png) ## Getting Started 1. Install the extension [here](https://marketplace.visualstudio.com/publishers/root16). -2. Add a `bamboo.conf.json` at the **root** of your VS Code workspace. - - ![Example Project Strucutre](./images/project_structure.png) - - **Do not check `bamboo.conf.json` into source control.** -3. Populate the json file with the following data: +2. Add the files: `bamboo.conf.json` and `.bamboo_tokens/tokenCache.json` at the **root** of your VS Code workspace. + - A suggested structure is: + - ![Example Project Structure](./images/project_structure.png) + - **Do not check `bamboo.conf.json` or `.bamboo_tokens` into source control.** +3. Populate `bamboo.conf.json` with the following data: ```json { @@ -95,7 +97,7 @@ Bamboo provides the following features inside VS Code: - Publish web resources - Upload plugin packages - Add components to solutions -- `relativePathOnDisk` and `relativePathOnDiskToSolution` must *not* start with a `/`. +- `relativePathOnDisk`, `relativePathOnDiskToSolution` and `relativePathOnDiskToNugetPackage` must *not* start with a `/`. - For web resources, `dataverseName` and `relativePathOnDisk` don't *need* to be similar (as shown in the example), this is just encouraged for ease of development ## Usage @@ -105,15 +107,11 @@ Bamboo provides the following features inside VS Code: | `bamboo.syncCurrentFile` | Sync current file. (Must be present on conf.) | | `bamboo.syncAllFiles` | Sync all files. (Each file present in the conf.) | | `bamboo.syncCustomControl` | Sync a Custom Control. (Opens up a choice dropdown for each control specified in the conf.) | -| `bamboo.syncPluginPackage` | Sync a Plugin Package. (Opens up a choice dropdown for each Plugin Package specified in the conf.) | +| `bamboo.syncPluginPackage` | Sync a Plugin Package. (Opens up a choice dropdown for each package specified in the conf.) | - All command can be run in the command palette. -## Token Refresh + Cache -- Bamboo can use the previously cached token to speed up initial load times. -- Add the file: `/bamboo_tokens/tokenCache.json` and then restart VS Code. - ## Extension Settings | Property | Type | Default | Description | @@ -134,7 +132,7 @@ Bamboo provides the following features inside VS Code: - [❌] Automatically add custom controls to solution - [❌] Manage upload / sync from context of tree view - [❌] Sync data from Power Apps to local files -- [❌] Create a plugin package or plugin assembly +- [❌] Create a Plugin Package or Plugin Assembly - [✅] Update / sync a *Plugin Package* - [❌] Update / sync a *Plugin Assembly* diff --git a/images/command_palette.png b/images/command_palette.png deleted file mode 100644 index 47ec92bfa8f74fb3282137f653d8ace296268ddb..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 8155 zcma)h2UJtdw=XJU1yn4c0t(WbNRuuWs({j^>PIJp&^rV{K&jHDOAUk)N+8q#iXa`N z69@=WLk|g%5b_Rw@2z+5TK{#|&05Li%-J)uXU}fGJ)d4_snB1%eUXNShF(qexeg7@ zX)N&n{dqdzcYnB94`@!g>8L!VDeu3#3Ovx-DrqXw&{W1=I(l~&c)sAQ`o@ihhT;3k z_f#jO$cl!B-C6CqlAgEu#!P^--gxHHjtYYLhia42M3fR(nrsiP!vU$4>Bb#pr~?wvO{Q4 z>-AHqt9JoPPPK=2=Wc!Y=4Dh7a}6D`v0JE#kM$OibKKt}h9#@1YiR1LJHU0~6P{@^ z0OZp)=#|x>TQf5a*csN#lj`a^u2EOm)Cc2~>&mjb`1rb+<&9TYS3fwN&=P@RNm*It zQA2r28S(Q!`)#rqR2iad&OwwH{v=+X5p~dW{&>PfkOjczCbI-TGn!S3(~=yHIAcF; z(86jb$1gwQ?F~@Sc;@{YW@RzHI4}{@5cg!EwHrC2r`&J~7_0Dri;GW4Vw4&QZpR3} zxq8~`t+yPPv+ck$MGjM%ge8A zZ0N@l@cpt|NC^*h4UN%>UQu-o%hTKdwK5tv-bg6kf|pxtNgAFtYxL}cCUyU@qK*nA zBs?>$zD&3EWTp1(wo#j`-?zsUG)~MTfOyXB1~ByDyj2wXnMF5qSxis%my+PqZ5Xz? zGxz1z>@`UIl4=?nT9^J{I(}eKId5cwE1OC;CUVY@@y?vtV z4_D5gDE^bvSDH?wMSIWtWVaWd{C#;XXpSCudO*{52WV7jf~A1wGY##@boWmE-+|M6 zb7s1PA_q8|#I2LDsHAkZCJ=6Ru)VPgS>la=xt;Rj&tByE1@w zRXlZayr_Z?v|B+Z-Ozmtsv155PxziPR&=1KDU}Yqrs33i8nQpGVY>>YEX=CMkICcD zu#yQZ)fa$w4>`F0iX4kIC+zM0+^I$xT5dKCJF@Tb>|5cy*gd~Qa{Ag(EI?B!Tch=} zXaum#$iLiNBXzeLBFuDM|C&HzM#lP)y(F8XgnlfXvY;h|n0&ed2&fe-aG&!;LanNU zhIDMduKpF}g)GyP)u+q<7s6`|3fU*%X1bXJ!t0rVLCkTFlxK5*$U* z5|VCFK+~sxvd%|0iZ4omU>41SwlsTU+xl1bLAs@L@^d;&00GSo=YKk*)*v3k!oosc z2`>GvF100ofbi|}x>*m%_Uv)gbY51LxyqeJT}{|IFIQ^^hlpRpzs}D9G0sQjq#ZX~6} z&;-@FS5|2g`8)JPU4l$HLM*b&cGZrdPzl@ky;W(-Zp!kW{gl;W@3zQ5#+pGrVe9t; z>u(1;vk4x)*E_Y<0OQRp6SB8QP5kL@@|hwr;>95SylXmz6;jte-YTRgtR3L>KZV8yo`qZk=k`nga zD++p=cmTNv)x`{a?{3{~4Vg89cZb9HAx=3F$|wN}`gAn0sSr$_9dCvj=>s*=OgO80l@^a3_L}T`rj)jcg=v{g z=Z1^^mknh(RF)+9T^YvFJ<=7ons(X@ORX|cIIPO@8#V=`IS8%pPJ!=&Qb}_b(84fX z1#88ZCkl5mMrty)WJPhP|FLS&zA95&N--0yvv8=jr^Aby3Bv6NNWmn ziYDk(YK>#)niFi@%D#EKQem8D_n7pIb%Fv%lfS*AtZjUk@yE6tOVOVj-5x-!H4ZeD zXM0nNkbBqdM{bdhIPq~|{a0uqux1Fd2-N$yzV`;_NGV6kqo(K%)e4mXFk#{rpdYP; zIH~BV+4$mfRGUJyU@5TFr5hkaP|j@+ zBYX(BL8}>F(^#-2*Qj@Bx!zNnn>B}LOH2-(`iUt@Ql85MwhWJStdXNQEsPC|^pDe$ z@tA`xoZC9&RsL~Yb*$9}H714wBWZfkuW16aa=oObZ=7e48pkHEE?u^9(XpDUFiGmK z*6^Ud@uMD)9o6TY3{8;V6`rC2-4x2Ydmyxg23(O*Z|~Lan*n!GrIuz7A^@eEKWN8*B6MgiK4R5)4R_x51GU{6@a z5N5F31Sjm#c?^qMMB(*m*g3puT9#J}_Y5Jl|ES2ETCMiWY#8~wr8fbmp)=m7QnL`g z>|SVMtf!l|Tih~!j_RUiD;!uH zEhul`Z?kre1-<=3UykQ%;g(x|uF&tb zQebO77Ne^GoeO-V!)F^FlTF~`9N6gPx>RT%A-ww1XeDN#)LH=P+(gLeQ0OzReLCuc z9c-AoscFQM&sDH(9YWU$c?wzaHyT zH=fojfQE30nw9;15xqW(L$Q(f>>EQ^3Y!_5#2e`GiY5GzD2`VOZ@)sj^>T5XmitEk zcw&i=j1`nALms}op6!p7gGF}h8Ejh?K-(5&zTEv?U zA0^1*m+Z@XU}MI9M(cwbjV2=!9R<=pF12wg181p^IWHUy{aiaoW;Y2pm+|d7uyg@6 z0K`&`oK1j^@*Er-liRVe%msowd;6N=pf_=a%mq@%yPPG8uqfRye^Tpl!wU>xv~=ll zx+EE=K4XJ?z7ypREcL}N9}|7>;^Q7_;jB0gt7N0j!^auZ0qVCx>e~?<(5eP4)=0}I zf{zS63*8GiukW5AKR~HR=8w(PGSb+_uCXG-xn>o4jdtvmj8ZqSWe5`9WZK_>vO2Ig z9jj%xRjj?IgnBTOr{;C&zEr~`v)zAT1G2-%Nfw6U2mEEGz12lVDK8O{hf$rkWu9GW z%1jV2WMhRnxb2O*5ScKB(xUDjd-|;l8QVN+4F{hZ5{uQ|2YHQ_ED*0ThAQV&7q(Aw zYw>t(jdb%oJLQjigk#+7bL&@3K07S_!(WShXZAYyjwQ^62V({gw|a9r06c6;nA;Pgp(jS@rjO_w?)hxn}?LDUoKJ!QiMHdR#`-@OIjko z8v9yEte6A^S6ySb=_n-pGjWSkfKkh3)npNmd^ajp<8Je3_!L9v{qtD5g#_~-Q={+_ zm(>|eK638QW~PDt%SwXU*kPwmoZ_2Xv1t~Z^jW_LXU$-a1;TA6bC?TMDO!PS$C1U{ z0yt;T8JaMo^#ud=(_9=KLL{lhGu5b3B0VYCK5bt9#w_uL8?k6ifrU3(w577eN&r$b zL$`5f&CS=EjFa)-jStM0*SwTMNkYq;#48NNKz+?@_h7z`JK5?&2pp8!J{ud`n~hLm zODkNLRTxU`2qEpn4x}>5JP1?pmaUVRu}=OlEms|3nltn}GJB%?h2!pY12js)m`ywV zeMM}ONj87d@|a+~#LVb`Z>b%I`Sm|!*=rD$4>HJ)M_h8UT0bysG(m0S<1PYAq(Ig@ z$Y6Ts2tU`=%b*V4g#AA8v-tS2UvoR}8#t}D_tGu!o>NVahsY3NE zR_1*WMD$Q@aV;A}x&i0iND8$)mdoQf+Y7l6J-cRj zCc{X20@#BHufg$`)^T?B55@HMIx*6uxJbgT1|B)=?Za^W4*Ede$m=j8AV!irhFxb394W~DYfN~Im_j$8eB zYo$Jw!kV1`Yg)Ul`&C^9I$7)0wvf}s5(cC@3t1x<0q?hht6M}MgkRNA>h3&Fep66< z*pty&9#KPS{4&=S>#vgy`)6}-r}`J=1%95n9lsJ~nD$5nX;z(_B(4577n0{O`=g(b z)5KKtI(zGJ7UKhmzfap!`T2NZ^v)&Lm1J=llgK5kWm2YU1CA|#EOJR4gv^gk-wWx` z7GE3+g)@WX2Cv)>o&%4Z5=v&k^j!2rsT!y$%|w<+G<#I2v->;DP(LkAtyNlO>_WB- zaKQ?-4JG6O^x6UZL6&6Yi=q&nJW+v3YG=Gf;%bH-3v3&lA@brV-%pGzt7RMO04{U8 zvt}-+0|U3f1Q`hW9$Dj$t$}R!!pO2qPu}urTdhl_7J>o?o+mC|zvQAGxaX9cEJ=EG zNRrAoMl;f-YhGORglYbZUcDD6k9sDgMl(sK=)1XJORQ}aV0a!703rb`sl0eqbKTe5VHL=)1ebH}t#n_(-TiZemhJ|mw zZfj2cRpgT9ZV_?#_8E8O@{aO!Fl_0*)oGfb_a8k2KATMZ%_|Ys0)%RY8nowAQe>yQ zc8?Z?(sJ~AHV|%X;Y0P1DD2li>~%R-8fV`e(h!m|uzB=lc}FYtVc^mJgJKlQFeW=* zxd0~1-W8vGlXK1Vc-<)UVr3-gmvdeD#@llYG4Xn%vm ztSmMlqg@4H5^vW<>=O9Ay5=D4?2m4CH?^(HyO#ls?A+*Fg5<>)X0d>aj@H1jT7gT{ z{gxI#wcE0q7UU`M=;lDRTUz84a=#za>&nypw|OI0Mi)$j$9xWr04U&nR(ON0pcp7kHb{si{WkJeI_E%i zh_#%&=ay3Bq*>k%!Q}L@xh=l>IbJxWQ@I&d6 ztUXTlaoB1!HyFD1F=jG_p2zyelzw?S)eEQ~eY;$Z_x6JOU~UQ_c4irni*`eoxcDX0 zqxCkgjgBU()8{A{nY*c)SI<-=BQ$GpNQZeBjDq9PE?Dzd`zM=)Q(uY*BBi=!qi^NL zTJ@cbU}m5WoDf-IY+bLq1E)~EL(jrSzC=UgR1O3XEkB|}v4C4eNbk139FHLjI2^h2 zHNd_^68k7`hN*xdfiLb{U>@XUsm1VKqoS1bnne|BH0Xpy=T+x|iSQyuy}3G5-j+ zj*p7b;qw;EL62%xUZUy27KWw%@;x2u&q-vz{nNXdGrE_)YY{U+2iY59Y}ZEN;3zR(XlEnyL(j7m(bu>#f9V6wld z<@ks0$lw78@qJ~pu6(#V)(rmbC1!y*bZ!T&4B+8y`f44fMt?B>Kpd*==9Ou#v>~+E z-e}>?r5yMlX@eH+KJ;Co2=U3`;aoggP&0EuP`XI4H*QKUN1=X zi5YW#o(pY(djMW%_6)n{ZCmWzC$j-JgPGM7xE`h&c)bzXSrdwr$@)`p$SaeRry?P0y-P%hgq=`gCy zDJNY6!t0tKDIZD$E5fHe!A?zIFMGWgYF`E^JHgWSCzWFYG<~`LYa)uuDUqK-6GF&N z)+M^xbc$DzAUwX3TW|@qvo5$Gkw}z-s+up_DW{g%Px#&cRkghz9C|UZs z2>RveH|ra-lEjqR0peW_@A1x3{JK#GhLVTQ0f#)n5r z`r-lMN`619y%9wkZyLshx+Kh5(2~uTS%mi6p=`K_YR!z8(XGMVv{y8YF`wD?e%`sf zGqzH4n319w!S*T-Y$-@t-->Gc@?=#y7d2JN_^x({R5a}+AUn_NUZRCkn@sozqC30A z`0Os8VKeSa&5j#|1Cc`$R1N2~!SlHgsI%klZ}2*@sjXT(B3rqCAMHMyR>o zDl$4ui@Rh0K`iVf&7zVqifOXCkqdmFhi4iGHhHg!(!-hog?g zVti`Vx;!}3g9gbe6x_28>M~M=Ll8(RDpO-<~W>KAMu|4V1x+UuY zpk@SVU+6_XCEDs$ zvbDlt8lamQv*EFcJIgxZ`GWDB77s7J`3fcA?bd9?u^36X2|Di$ZGTJ5%WcBu1UeIy zwwsr|SvlpP6UTH~Jk03m0K$7bVEa7%h~I{nZeHC}YOgH7QE6h7z~P#WAT z;QshFcd3ChTl8-YmOFUgS&#VnqL|2vkorlF^5x+DiuvsG%Po)Yr;q`E&a76na}sIk zE=c?>Z(jM2RmK0Y^84Qd>gPxAjt;iebbw2TyNX=V>e^nlX#IK(y?x3xP3i{3ecjTH zOSu#Xv!u;DQirA3-Xd;cubl+3)7oz--l6Vf0nA8sE@wl_AK>D5mV_!fu_v}$J}lf> z_B6?+@v4YLdT-#wXNcstXy=roaygAYOs$cGDw;G_bo{3XAbQEm`}!IS_wlyo4G=+K+# zm9_zi0#*aLdIVw^zzk8(llAQk>)pTFAc#hh{8FXExzmF)P4qc&-QDy!q-wbPlY%x$ zGagR|>t^I}=nei}qzGuAdd(jjprc>=!<+&1GGG5Ln{c1S#Z}D8K^#6s{+cSSdt6tcUvr_r4 zLzx$5GuZ^r(w762?G5b=H!enlks>DZO|eyabvB>$AOc%Ig*-+K44sT@L+78QXF6oP zf}7RqeDR)YcP$OfP79DW)&s34Jn%7g$EFk%yK&3hG`zW_3eI}PzL>mrVdIi~i-=0( zKbtC1IyX!9xX90o22j~6;wFUqDv=lEkI9&)z8&k*ZO06uo2HqJ$+zDGEEhorhI08= zZ(wTcy32fM&h69f{{SVeeF2hVqTCjg@j&tOV-z2}t`zXH1|`3SMVQj%#}3q56MqM9fxTL**(;a4;zlc%B!l#&W8H=8Ng73-roUm z6l)F9KTnxOJ7rYYJbQGJ>NC|UHIeDK}B|1bju z9K|&0q^R%*q+I^ynk)YLKo@;9gCF9gs4#stEzLeUxpUa&-a6;jSgQFS;a8PFT-*=V zQYo^G;h&OJ9x2}!(J24M?9sc#VhhA>%(36}ym$G1vA7XpIzQ3V+#pgmiNYs0OVgz zWk`-;jws>(#;tE4oVqH=xPx42a+N@J3#A0;Sj%xZS)wIhq&}##wMb}i)nLWkneGT- z^GV7J55biN(ATbK%lF;NpVnEHuAAYSx?X_bxCD*89EPqfbN${>$1?~3z~_%>+b|0C zu9+TbO|9oq(A-?dnp&~6b0s7Bx_$J9d(Lw{)KUS6;Xd-ID9(uKRT+2<|5Rq@b|M&XnjeM#;^7FuS#{6YWIQ{$F8n_E zP$5=|BfY5J$%N%H(Bb4myY;(h{r(Lb^FfnDX`m;HnRe^l4@FToL!V@qzjuMtYyVSJ qqEquVhEtQX3itNJqpK)K)E-3ix(U)5fH^cYYA>{&mp^?Q{C@!Rt1aLF diff --git a/images/command_palette2.png b/images/command_palette2.png new file mode 100644 index 0000000000000000000000000000000000000000..16530f0b890190448fbfc375b4692155556eb436 GIT binary patch literal 13914 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%0Dp zH)v?2a&}btI7M8Sz-+}or7xQ~R&kU*4VId$tsPjl%dSv1rMRcOfy0`tme0&b@#E(} zX;%qI7Ng2d$<5>O){fN9C<2Ykx&H+OoAS(#tq_n>D1Orj`dlH_XB@ znATI>@n_-GAI<;Jzo~}d`tonRsp{W)6TBn1&<3tIsYY~=FQ4~bW7q{ z^xso15lcvtoYpv3dv;9+#@u5xsR0SocD>a#;OUy^$0Xi` z3c({86A2FSx?ErCAIK)fA@)M1`88f;>S~FWSdl|}N6Z)4;`_(RMCg!ZpatQ+tic=J z3Lz8|?m{IQaO}z?Qv~igO4e`sPX847x02KU-9U{IQpxd|k@@r~e}oF}Y-Y2@{Tp*} z9Il?Lz!0`?W66r6Y)`KEqFB6VQCD)RejUm8;#AWWr5yYC;%4W41Xk04Njy9ORQuxJ zN0w?0&A3;v#Rfkq;WDHraC+o=>~7Tzp%>-CAjyK`#$@~S%6VmXZ!g28X%`pqt2uig zHN>(D8!k;lMxD1!@ZG`Ommde4xrp;11a&rVpx%n*INm4#)2?Mc>LBrJ3#9+g#O4e~ z1lgxns&K319c(0PMvYB@c#g);mkzlV>fTBHaHdvwaBz=`n?F2tCf{_EVEs!=^jVjd zmL@k@d6d->+{IXFeR=7Z7xh%^9=|L6{QFe3AcghjrJs6_B(`Xt1FI0m+M(@G3qq>| z2fJpYHAK9V9$Z8XT0YoQYz9jRmhlMOSMD*&)XbFvNRq`=yR1Ssr-erjS)swQa&#um)*j`LLJ9{vby6<`hf6T;>WUFG$8StA*N_ zFcpEa-haLbN2}u#CNk)>+KT98yhx#3wp^DLk_E+9uEALd?(*+a#!AwyOwx^^wD0w} zxrV*gI~u~sr9~BS4qioNera<*QU{MaQ>G&^Gn8VN9MTkAzohsPZF4x@yW2uMTK$I! z7ggUolqlw4kp<)JQJXxRnk*6Bu&TD~d2+BnfqOeQj6ir+3w1tMC7{hr4LLRiraO9A z;PB}>KWP+>`LBbDr)EOIn;W>5id8`}G$Dr&qSmGjOuOcsoYS=nwJKuXfFTFKzvBbw&y>|tN#1T}pW4Gp7MI4rPTB{{bm(l`mX&;4LQfGFUBb} z(1n07RIi<+&i*MspKn%QNHUrr!Ad&?NTc=^*Z)-t=G&GQmj2LaIwGSHFMwcN;+Qje zzi(e5#_@G(V`zfE+FJsK1oJuco^Rjx97)@#5Q}+m*@b<1ZJJdMbShxe&C@#0!EL5p z1}pLzaOubhdP63CH#MR-Q<>Wd%FT`itB*$#>@B9&|;;mbIab-BJ{V(SYT z-=p5T*i@)rFSrqnf5R{rJK3aB-t8|Lt|os!7y8QU{tHs?s0ng5sXM|5^Ygc#d>^0{ zC{$5^Crey-%$jvJs~rJhW7&wsDz#pT>$(nn78v$2x!WB~>p-w`3P($Uos(i#U%Vp8 zRUdai*p~^R8i9y4rORzNWr$V1D^pWiMZrNDsSjKQ6vFj~WAtf$ss{f$Bw7q%%Wuko zhk3QFY(lMEN1(;%B;EX`Y8bkaU-WI_9D2J`3lH3Sj)p>>Sg*^QENRi|3@z5 zUUJFza8DPetjrOIX$S;_KCk>7_PU9Q{i8pHeY-E$@qLSch$_0k6{)*rSJ6O~3i2Hs_MoB}$7^IY>DUfLgs_Am2*fy2HW_5~FSSAY*t&Ptv zg^Z4I@nti5brD33rp%1(SZ54!+Z-BU1om7vS^qUQc=r6v;y%@C%=|Na#kdZmrJfJZ Zt Date: Fri, 17 Oct 2025 17:36:14 -0500 Subject: [PATCH 6/6] changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9be8575..e724a72 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -50,4 +50,4 @@ - Update internal fspath handling ## [0.3.4] -- Support for Plugin Package \ No newline at end of file +- Support for updating Plugin Packages \ No newline at end of file