diff --git a/src/client/test/integration/authEnvironment.test.ts b/src/client/test/integration/authEnvironment.test.ts new file mode 100644 index 000000000..78f327d36 --- /dev/null +++ b/src/client/test/integration/authEnvironment.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; +import { AuthEnvironmentService } from "../../uriHandler/utils/authEnvironment"; +import { UriParameters } from "../../uriHandler/utils/uriHandlerUtils"; +import { PacWrapper } from "../../pac/PacWrapper"; + +type ProgressReporter = vscode.Progress<{ message?: string; increment?: number }>; +type ProgressTask = (progress: ProgressReporter, token: vscode.CancellationToken) => Thenable; + +// Minimal stubbed surface of PacWrapper that the service depends on. +interface PacWrapperStub { + activeOrg: sinon.SinonStub; + orgSelect: sinon.SinonStub; + authCreateNewAuthProfileForOrg: sinon.SinonStub; + resetPacProcess: sinon.SinonStub; +} + +describe("AuthEnvironmentService", () => { + let sandbox: sinon.SinonSandbox; + let pacWrapperStub: PacWrapperStub; + let service: AuthEnvironmentService; + let warningStub: sinon.SinonStub; + + const uriParams = { + environmentId: "env-1", + orgUrl: "https://org.crm.dynamics.com/" + } as unknown as UriParameters; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + pacWrapperStub = { + activeOrg: sandbox.stub(), + orgSelect: sandbox.stub().resolves(), + authCreateNewAuthProfileForOrg: sandbox.stub().resolves(), + resetPacProcess: sandbox.stub().resolves() + }; + + // Run the progress task immediately with a no-op reporter. + sandbox.stub(vscode.window, "withProgress").callsFake( + ((_options: vscode.ProgressOptions, task: ProgressTask): Thenable => + task({ report: () => undefined }, new vscode.CancellationTokenSource().token) + ) as unknown as typeof vscode.window.withProgress + ); + + warningStub = sandbox.stub(vscode.window, "showWarningMessage"); + sandbox.stub(vscode.window, "showInformationMessage"); + + service = new AuthEnvironmentService(pacWrapperStub as unknown as PacWrapper); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("does not prompt when already authenticated to the requested environment", async () => { + pacWrapperStub.activeOrg.resolves({ + Status: "Success", + Results: { EnvironmentId: "env-1" } + }); + + await service.prepareAuthenticationAndEnvironment(uriParams, {}); + + expect(warningStub.called).to.be.false; + expect(pacWrapperStub.orgSelect.called).to.be.false; + expect(pacWrapperStub.authCreateNewAuthProfileForOrg.called).to.be.false; + }); + + it("switches environment when the active org points at a different environment", async () => { + pacWrapperStub.activeOrg + .onFirstCall().resolves({ Status: "Success", Results: { EnvironmentId: "env-1" } }) + .onSecondCall().resolves({ Status: "Success", Results: { EnvironmentId: "other-env" } }) + .onThirdCall().resolves({ Status: "Success", Results: { EnvironmentId: "env-1" } }); + warningStub.resolves("Yes"); + + await service.prepareAuthenticationAndEnvironment(uriParams, {}); + + expect(pacWrapperStub.orgSelect.calledOnceWith("https://org.crm.dynamics.com/")).to.be.true; + }); + + it("resetPacProcessSafely swallows reset errors", async () => { + pacWrapperStub.resetPacProcess.rejects(new Error("reset boom")); + + await service.resetPacProcessSafely({}); + + expect(pacWrapperStub.resetPacProcess.calledOnce).to.be.true; + }); + + it("resetPacProcessAndThrow resets and rethrows the original error", async () => { + const original = new Error("boom"); + let thrown: unknown; + + try { + await service.resetPacProcessAndThrow(original, {}, "message", "error_type"); + } catch (error) { + thrown = error; + } + + expect(thrown).to.equal(original); + expect(pacWrapperStub.resetPacProcess.calledOnce).to.be.true; + }); +}); diff --git a/src/client/test/integration/createHandlers.test.ts b/src/client/test/integration/createHandlers.test.ts new file mode 100644 index 000000000..f67d0dcfe --- /dev/null +++ b/src/client/test/integration/createHandlers.test.ts @@ -0,0 +1,111 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; +import { AgenticCreateHandler } from "../../uriHandler/handlers/agenticCreateHandler"; +import { PacCreateHandler } from "../../uriHandler/handlers/pacCreateHandler"; +import { URI_CONSTANTS } from "../../uriHandler/constants/uriConstants"; +import { ECSFeaturesClient } from "../../../common/ecs-features/ecsFeatureClient"; +import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { uriHandlerTelemetryEventNames } from "../../uriHandler/telemetry/uriHandlerTelemetryEvents"; +import { PacWrapper } from "../../pac/PacWrapper"; + +describe("Create deep-link handlers (gated)", () => { + let sandbox: sinon.SinonSandbox; + let getConfigStub: sinon.SinonStub; + let traceInfoStub: sinon.SinonStub; + let traceErrorStub: sinon.SinonStub; + + const pacCreateUri = vscode.Uri.parse( + `vscode://${URI_CONSTANTS.EXTENSION_ID}${URI_CONSTANTS.PATHS.PAC_CREATE}` + + `?${URI_CONSTANTS.PARAMETERS.SOURCE}=${URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME}` + + `&${URI_CONSTANTS.PARAMETERS.ENV_ID}=env-1&${URI_CONSTANTS.PARAMETERS.VERSION}=${URI_CONSTANTS.CONTRACT_VERSION.CURRENT}` + ); + const agenticCreateUri = vscode.Uri.parse( + `vscode://${URI_CONSTANTS.EXTENSION_ID}${URI_CONSTANTS.PATHS.AGENTIC_CREATE}` + + `?${URI_CONSTANTS.PARAMETERS.SOURCE}=${URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME}` + + `&${URI_CONSTANTS.PARAMETERS.AGENT_HOST}=${URI_CONSTANTS.AGENT_HOST_VALUES.COPILOT}` + ); + + const setFlags = (enabled: boolean): void => { + getConfigStub.returns({ + enablePacCreateFromHome: enabled, + enableAgenticCreateFromHome: enabled + }); + }; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + getConfigStub = sandbox.stub(ECSFeaturesClient, "getConfig") as unknown as sinon.SinonStub; + traceInfoStub = sandbox.stub(); + traceErrorStub = sandbox.stub(); + sandbox.stub(oneDSLoggerWrapper, "getLogger").returns( + { traceInfo: traceInfoStub, traceError: traceErrorStub } as unknown as ReturnType + ); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("PacCreateHandler is a no-op that only emits disabled telemetry when the flag is off", async () => { + setFlags(false); + const handler = new PacCreateHandler({} as PacWrapper); + + await handler.handle(pacCreateUri); + + expect(PacCreateHandler.isEnabled()).to.be.false; + expect(traceInfoStub.calledWith(uriHandlerTelemetryEventNames.URI_HANDLER_PAC_CREATE_DISABLED)).to.be.true; + expect(traceInfoStub.calledWith(uriHandlerTelemetryEventNames.URI_HANDLER_PAC_CREATE_TRIGGERED)).to.be.false; + }); + + it("PacCreateHandler parses params and emits triggered telemetry when the flag is on", async () => { + setFlags(true); + const handler = new PacCreateHandler({} as PacWrapper); + + await handler.handle(pacCreateUri); + + expect(PacCreateHandler.isEnabled()).to.be.true; + const triggered = traceInfoStub.getCalls().find( + (call) => call.args[0] === uriHandlerTelemetryEventNames.URI_HANDLER_PAC_CREATE_TRIGGERED + ); + expect(triggered, "expected a triggered telemetry event").to.not.be.undefined; + expect(triggered?.args[1]).to.include({ + source: URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME, + version: URI_CONSTANTS.CONTRACT_VERSION.CURRENT, + hasEnvironmentId: "true" + }); + }); + + it("AgenticCreateHandler is a no-op that only emits disabled telemetry when the flag is off", async () => { + setFlags(false); + const handler = new AgenticCreateHandler({} as PacWrapper); + + await handler.handle(agenticCreateUri); + + expect(AgenticCreateHandler.isEnabled()).to.be.false; + expect(traceInfoStub.calledWith(uriHandlerTelemetryEventNames.URI_HANDLER_AGENTIC_CREATE_DISABLED)).to.be.true; + expect(traceInfoStub.calledWith(uriHandlerTelemetryEventNames.URI_HANDLER_AGENTIC_CREATE_TRIGGERED)).to.be.false; + }); + + it("AgenticCreateHandler parses params and emits triggered telemetry when the flag is on", async () => { + setFlags(true); + const handler = new AgenticCreateHandler({} as PacWrapper); + + await handler.handle(agenticCreateUri); + + expect(AgenticCreateHandler.isEnabled()).to.be.true; + const triggered = traceInfoStub.getCalls().find( + (call) => call.args[0] === uriHandlerTelemetryEventNames.URI_HANDLER_AGENTIC_CREATE_TRIGGERED + ); + expect(triggered, "expected a triggered telemetry event").to.not.be.undefined; + expect(triggered?.args[1]).to.include({ + source: URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME, + agentHost: URI_CONSTANTS.AGENT_HOST_VALUES.COPILOT + }); + }); +}); diff --git a/src/client/test/integration/uriHandler.test.ts b/src/client/test/integration/uriHandler.test.ts new file mode 100644 index 000000000..bbdc868cb --- /dev/null +++ b/src/client/test/integration/uriHandler.test.ts @@ -0,0 +1,90 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { expect } from "chai"; +import * as sinon from "sinon"; +import * as vscode from "vscode"; +import { UriHandler } from "../../uriHandler/uriHandler"; +import { URI_CONSTANTS } from "../../uriHandler/constants/uriConstants"; +import { PacWrapper } from "../../pac/PacWrapper"; +import { AgenticCreateHandler } from "../../uriHandler/handlers/agenticCreateHandler"; +import { PacCreateHandler } from "../../uriHandler/handlers/pacCreateHandler"; + +// Shape used to stub the private route targets on the prototype without resorting to `any`. +type UriHandlerRoutes = { + pcfInit: () => Promise; + handleOpenPowerPages: (uri: vscode.Uri) => Promise; +}; + +describe("UriHandler routing", () => { + let sandbox: sinon.SinonSandbox; + let pcfInitStub: sinon.SinonStub; + let openStub: sinon.SinonStub; + let agenticCreateStub: sinon.SinonStub; + let pacCreateStub: sinon.SinonStub; + let handler: UriHandler; + + const makeUri = (path: string): vscode.Uri => + vscode.Uri.parse(`vscode://${URI_CONSTANTS.EXTENSION_ID}${path}`); + + beforeEach(() => { + sandbox = sinon.createSandbox(); + const prototype = UriHandler.prototype as unknown as UriHandlerRoutes; + pcfInitStub = sandbox.stub(prototype, "pcfInit").resolves(); + openStub = sandbox.stub(prototype, "handleOpenPowerPages").resolves(); + agenticCreateStub = sandbox.stub(AgenticCreateHandler.prototype, "handle").resolves(); + pacCreateStub = sandbox.stub(PacCreateHandler.prototype, "handle").resolves(); + handler = new UriHandler({} as PacWrapper); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("dispatches /pcfInit to the PCF init handler", async () => { + await handler.handleUri(makeUri(URI_CONSTANTS.PATHS.PCF_INIT)); + + expect(pcfInitStub.calledOnce).to.be.true; + expect(openStub.called).to.be.false; + expect(agenticCreateStub.called).to.be.false; + expect(pacCreateStub.called).to.be.false; + }); + + it("dispatches /open to the open Power Pages handler", async () => { + await handler.handleUri(makeUri(URI_CONSTANTS.PATHS.OPEN)); + + expect(openStub.calledOnce).to.be.true; + expect(pcfInitStub.called).to.be.false; + expect(agenticCreateStub.called).to.be.false; + expect(pacCreateStub.called).to.be.false; + }); + + it("dispatches /agenticCreate to the agentic create handler", async () => { + await handler.handleUri(makeUri(URI_CONSTANTS.PATHS.AGENTIC_CREATE)); + + expect(agenticCreateStub.calledOnce).to.be.true; + expect(pacCreateStub.called).to.be.false; + expect(pcfInitStub.called).to.be.false; + expect(openStub.called).to.be.false; + }); + + it("dispatches /pacCreate to the PAC create handler", async () => { + await handler.handleUri(makeUri(URI_CONSTANTS.PATHS.PAC_CREATE)); + + expect(pacCreateStub.calledOnce).to.be.true; + expect(agenticCreateStub.called).to.be.false; + expect(pcfInitStub.called).to.be.false; + expect(openStub.called).to.be.false; + }); + + it("ignores unknown paths without throwing", async () => { + await handler.handleUri(makeUri("/someUnknownPath")); + + expect(pcfInitStub.called).to.be.false; + expect(openStub.called).to.be.false; + expect(agenticCreateStub.called).to.be.false; + expect(pacCreateStub.called).to.be.false; + }); +}); diff --git a/src/client/test/unit/uriConstants.test.ts b/src/client/test/unit/uriConstants.test.ts new file mode 100644 index 000000000..f75c4e499 --- /dev/null +++ b/src/client/test/unit/uriConstants.test.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import { expect } from "chai"; +import { URI_CONSTANTS } from "../../uriHandler/constants/uriConstants"; + +// These constants form the versioned deep-link contract between the Power Pages +// home page and the VS Code extension. They are asserted here so that any accidental +// rename or removal is caught before it can break an already-shipped deep link. +describe("URI_CONSTANTS deep-link contract", () => { + it("keeps the existing open and pcfInit paths unchanged", () => { + expect(URI_CONSTANTS.PATHS.PCF_INIT).to.equal("/pcfInit"); + expect(URI_CONSTANTS.PATHS.OPEN).to.equal("/open"); + }); + + it("registers the agentic and PAC create paths", () => { + expect(URI_CONSTANTS.PATHS.AGENTIC_CREATE).to.equal("/agenticCreate"); + expect(URI_CONSTANTS.PATHS.PAC_CREATE).to.equal("/pacCreate"); + }); + + it("exposes the shared context parameter names", () => { + expect(URI_CONSTANTS.PARAMETERS.ENV_ID).to.equal("envid"); + expect(URI_CONSTANTS.PARAMETERS.ORG_URL).to.equal("orgurl"); + expect(URI_CONSTANTS.PARAMETERS.REGION).to.equal("region"); + expect(URI_CONSTANTS.PARAMETERS.TENANT_ID).to.equal("tenantid"); + expect(URI_CONSTANTS.PARAMETERS.SOURCE).to.equal("source"); + expect(URI_CONSTANTS.PARAMETERS.AGENT_HOST).to.equal("agenthost"); + expect(URI_CONSTANTS.PARAMETERS.VERSION).to.equal("v"); + }); + + it("defines the versioned contract and known enumerated values", () => { + expect(URI_CONSTANTS.CONTRACT_VERSION.CURRENT).to.equal("1"); + expect(URI_CONSTANTS.SOURCE_VALUES.POWER_PAGES_HOME).to.equal("powerPagesHome"); + expect(URI_CONSTANTS.AGENT_HOST_VALUES.COPILOT).to.equal("copilot"); + expect(URI_CONSTANTS.AGENT_HOST_VALUES.CLAUDE).to.equal("claude"); + expect(URI_CONSTANTS.AGENT_HOST_VALUES.AUTO).to.equal("auto"); + }); +}); diff --git a/src/client/uriHandler/constants/uriConstants.ts b/src/client/uriHandler/constants/uriConstants.ts index 00330e7d8..36cb1473f 100644 --- a/src/client/uriHandler/constants/uriConstants.ts +++ b/src/client/uriHandler/constants/uriConstants.ts @@ -10,7 +10,9 @@ export const URI_CONSTANTS = { EXTENSION_ID: 'microsoft-IsvExpTools.powerplatform-vscode', PATHS: { PCF_INIT: '/pcfInit', - OPEN: '/open' + OPEN: '/open', + AGENTIC_CREATE: '/agenticCreate', + PAC_CREATE: '/pacCreate' }, PARAMETERS: { WEBSITE_ID: 'websiteid', @@ -20,11 +22,27 @@ export const URI_CONSTANTS = { SITE_NAME: 'sitename', WEBSITE_NAME: 'websitename', SITE_URL: 'siteurl', - WEBSITE_PREVIEW_URL: 'websitepreviewurl' + WEBSITE_PREVIEW_URL: 'websitepreviewurl', + REGION: 'region', + TENANT_ID: 'tenantid', + SOURCE: 'source', + AGENT_HOST: 'agenthost', + VERSION: 'v' }, SCHEMA_VALUES: { PORTAL_SCHEMA_V2: 'portalschemav2' }, + SOURCE_VALUES: { + POWER_PAGES_HOME: 'powerPagesHome' + }, + AGENT_HOST_VALUES: { + COPILOT: 'copilot', + CLAUDE: 'claude', + AUTO: 'auto' + }, + CONTRACT_VERSION: { + CURRENT: '1' + }, MODEL_VERSIONS: { VERSION_1: 1, VERSION_2: 2 @@ -40,4 +58,6 @@ export const URI_CONSTANTS = { export const enum UriPath { PcfInit = '/pcfInit', Open = '/open', + AgenticCreate = '/agenticCreate', + PacCreate = '/pacCreate', } diff --git a/src/client/uriHandler/handlers/agenticCreateHandler.ts b/src/client/uriHandler/handlers/agenticCreateHandler.ts new file mode 100644 index 000000000..b59854ff1 --- /dev/null +++ b/src/client/uriHandler/handlers/agenticCreateHandler.ts @@ -0,0 +1,72 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import { PacWrapper } from "../../pac/PacWrapper"; +import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { ECSFeaturesClient } from "../../../common/ecs-features/ecsFeatureClient"; +import { EnableAgenticCreateFromHome } from "../../../common/ecs-features/ecsFeatureGates"; +import { uriHandlerTelemetryEventNames } from "../telemetry/uriHandlerTelemetryEvents"; +import { buildCreateFlowTelemetry, parseCreateFlowParameters } from "./createFlowParams"; + +/** + * Handles the `/agenticCreate` deep link launched from the Power Pages home page, which will + * open VS Code into an agentic (terminal CLI agent host) create experience. + * + * This is a dark, flag-gated scaffold. When {@link EnableAgenticCreateFromHome} is off (the + * default) the handler is a no-op. When enabled it only parses the deep-link parameters and + * emits a "triggered" telemetry event — the actual agentic behavior (agent-host selection and + * Power Pages plugin bootstrapping) is intentionally deferred to a follow-up change. + */ +export class AgenticCreateHandler { + private readonly pacWrapper: PacWrapper; + + constructor(pacWrapper: PacWrapper) { + this.pacWrapper = pacWrapper; + } + + /** + * Whether the agentic create deep link is enabled via ECS. Defaults to false. + */ + public static isEnabled(): boolean { + const enabled = ECSFeaturesClient.getConfig(EnableAgenticCreateFromHome).enableAgenticCreateFromHome; + return enabled === undefined ? false : enabled; + } + + /** + * Entry point wired into the URI route map. + */ + public async handle(uri: vscode.Uri): Promise { + if (!AgenticCreateHandler.isEnabled()) { + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_AGENTIC_CREATE_DISABLED, + {} + ); + return; + } + + try { + const params = parseCreateFlowParameters(uri); + const telemetryData = buildCreateFlowTelemetry(params); + + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_AGENTIC_CREATE_TRIGGERED, + telemetryData + ); + + // NOTE: Behavior intentionally not implemented yet. The agentic create flow + // (agent-host detection/selection + Power Pages plugin bootstrapping) is a + // follow-up. `pacWrapper` is retained for use by that implementation. + void this.pacWrapper; + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + uriHandlerTelemetryEventNames.URI_HANDLER_AGENTIC_CREATE_FAILED, + 'Agentic create deep link failed', + error instanceof Error ? error : new Error(String(error)), + {} + ); + } + } +} diff --git a/src/client/uriHandler/handlers/createFlowParams.ts b/src/client/uriHandler/handlers/createFlowParams.ts new file mode 100644 index 000000000..9ad6678d5 --- /dev/null +++ b/src/client/uriHandler/handlers/createFlowParams.ts @@ -0,0 +1,59 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import { URI_CONSTANTS } from "../constants/uriConstants"; + +/** + * Parameters carried by the Power Pages "create" deep links (`/agenticCreate` and + * `/pacCreate`). The link is treated as a versioned, secret-free contract; see + * {@link URI_CONSTANTS.PARAMETERS} for the canonical query-parameter names. + */ +export interface CreateFlowParameters { + environmentId: string | null; + orgUrl: string | null; + region: string | null; + tenantId: string | null; + websiteId: string | null; + source: string | null; + agentHost: string | null; + version: string | null; +} + +/** + * Parses the shared create-flow query parameters from a deep-link URI. + */ +export function parseCreateFlowParameters(uri: vscode.Uri): CreateFlowParameters { + const urlParams = new URLSearchParams(uri.query); + + return { + environmentId: urlParams.get(URI_CONSTANTS.PARAMETERS.ENV_ID), + orgUrl: urlParams.get(URI_CONSTANTS.PARAMETERS.ORG_URL), + region: urlParams.get(URI_CONSTANTS.PARAMETERS.REGION), + tenantId: urlParams.get(URI_CONSTANTS.PARAMETERS.TENANT_ID), + websiteId: urlParams.get(URI_CONSTANTS.PARAMETERS.WEBSITE_ID), + source: urlParams.get(URI_CONSTANTS.PARAMETERS.SOURCE), + agentHost: urlParams.get(URI_CONSTANTS.PARAMETERS.AGENT_HOST), + version: urlParams.get(URI_CONSTANTS.PARAMETERS.VERSION) + }; +} + +/** + * Builds a non-sensitive telemetry payload describing a create-flow deep link. + * Only low-cardinality, non-secret values (source, agent host, contract version, region) + * and presence flags for identifiers are recorded — never raw org URLs or tenant IDs. + */ +export function buildCreateFlowTelemetry(params: CreateFlowParameters): Record { + return { + source: params.source || 'unknown', + agentHost: params.agentHost || 'unspecified', + version: params.version || 'unspecified', + region: params.region || 'unspecified', + hasEnvironmentId: params.environmentId ? 'true' : 'false', + hasOrgUrl: params.orgUrl ? 'true' : 'false', + hasWebsiteId: params.websiteId ? 'true' : 'false', + hasTenantId: params.tenantId ? 'true' : 'false' + }; +} diff --git a/src/client/uriHandler/handlers/pacCreateHandler.ts b/src/client/uriHandler/handlers/pacCreateHandler.ts new file mode 100644 index 000000000..6cfccccca --- /dev/null +++ b/src/client/uriHandler/handlers/pacCreateHandler.ts @@ -0,0 +1,72 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import { PacWrapper } from "../../pac/PacWrapper"; +import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { ECSFeaturesClient } from "../../../common/ecs-features/ecsFeatureClient"; +import { EnablePacCreateFromHome } from "../../../common/ecs-features/ecsFeatureGates"; +import { uriHandlerTelemetryEventNames } from "../telemetry/uriHandlerTelemetryEvents"; +import { buildCreateFlowTelemetry, parseCreateFlowParameters } from "./createFlowParams"; + +/** + * Handles the `/pacCreate` deep link launched from the Power Pages home page, which will open + * VS Code into a Power Platform CLI (PAC) create experience. + * + * This is a dark, flag-gated scaffold. When {@link EnablePacCreateFromHome} is off (the + * default) the handler is a no-op. When enabled it only parses the deep-link parameters and + * emits a "triggered" telemetry event — the actual PAC create behavior (auth, environment + * selection, folder selection, PAC CLI terminal) is intentionally deferred to a follow-up. + */ +export class PacCreateHandler { + private readonly pacWrapper: PacWrapper; + + constructor(pacWrapper: PacWrapper) { + this.pacWrapper = pacWrapper; + } + + /** + * Whether the PAC create deep link is enabled via ECS. Defaults to false. + */ + public static isEnabled(): boolean { + const enabled = ECSFeaturesClient.getConfig(EnablePacCreateFromHome).enablePacCreateFromHome; + return enabled === undefined ? false : enabled; + } + + /** + * Entry point wired into the URI route map. + */ + public async handle(uri: vscode.Uri): Promise { + if (!PacCreateHandler.isEnabled()) { + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_PAC_CREATE_DISABLED, + {} + ); + return; + } + + try { + const params = parseCreateFlowParameters(uri); + const telemetryData = buildCreateFlowTelemetry(params); + + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_PAC_CREATE_TRIGGERED, + telemetryData + ); + + // NOTE: Behavior intentionally not implemented yet. The PAC create flow (auth -> + // environment -> folder -> PAC CLI terminal) is a follow-up. `pacWrapper` is + // retained for use by that implementation. + void this.pacWrapper; + } catch (error) { + oneDSLoggerWrapper.getLogger().traceError( + uriHandlerTelemetryEventNames.URI_HANDLER_PAC_CREATE_FAILED, + 'PAC create deep link failed', + error instanceof Error ? error : new Error(String(error)), + {} + ); + } + } +} diff --git a/src/client/uriHandler/telemetry/uriHandlerTelemetryEvents.ts b/src/client/uriHandler/telemetry/uriHandlerTelemetryEvents.ts index 40339b871..a448f2e60 100644 --- a/src/client/uriHandler/telemetry/uriHandlerTelemetryEvents.ts +++ b/src/client/uriHandler/telemetry/uriHandlerTelemetryEvents.ts @@ -16,5 +16,11 @@ export enum uriHandlerTelemetryEventNames { URI_HANDLER_DOWNLOAD_STARTED = "UriHandlerDownloadStarted", URI_HANDLER_DOWNLOAD_COMPLETED = "UriHandlerDownloadCompleted", URI_HANDLER_FOLDER_OPENED = "UriHandlerFolderOpened", - URI_HANDLER_PCF_INIT_TRIGGERED = "UriHandlerPcfInitTriggered" + URI_HANDLER_PCF_INIT_TRIGGERED = "UriHandlerPcfInitTriggered", + URI_HANDLER_AGENTIC_CREATE_TRIGGERED = "UriHandlerAgenticCreateTriggered", + URI_HANDLER_AGENTIC_CREATE_DISABLED = "UriHandlerAgenticCreateDisabled", + URI_HANDLER_AGENTIC_CREATE_FAILED = "UriHandlerAgenticCreateFailed", + URI_HANDLER_PAC_CREATE_TRIGGERED = "UriHandlerPacCreateTriggered", + URI_HANDLER_PAC_CREATE_DISABLED = "UriHandlerPacCreateDisabled", + URI_HANDLER_PAC_CREATE_FAILED = "UriHandlerPacCreateFailed" } diff --git a/src/client/uriHandler/uriHandler.ts b/src/client/uriHandler/uriHandler.ts index 96bd60b8f..5b5cbc314 100644 --- a/src/client/uriHandler/uriHandler.ts +++ b/src/client/uriHandler/uriHandler.ts @@ -10,17 +10,46 @@ import { UriPath } from "./constants/uriConstants"; import { URI_HANDLER_STRINGS } from "./constants/uriStrings"; import { uriHandlerTelemetryEventNames } from "./telemetry/uriHandlerTelemetryEvents"; import { UriHandlerUtils, UriParameters } from "./utils/uriHandlerUtils"; +import { AuthEnvironmentService } from "./utils/authEnvironment"; +import { AgenticCreateHandler } from "./handlers/agenticCreateHandler"; +import { PacCreateHandler } from "./handlers/pacCreateHandler"; + +/** + * Signature for a deep-link route handler. Each registered URI path maps to one handler. + */ +type UriRouteHandler = (uri: vscode.Uri) => Promise; export function RegisterUriHandler(pacWrapper: PacWrapper): vscode.Disposable { const uriHandler = new UriHandler(pacWrapper); return vscode.window.registerUriHandler(uriHandler); } -class UriHandler implements vscode.UriHandler { +export class UriHandler implements vscode.UriHandler { private readonly pacWrapper: PacWrapper; + private readonly routes: ReadonlyMap; + private readonly authEnvironmentService: AuthEnvironmentService; + private readonly agenticCreateHandler: AgenticCreateHandler; + private readonly pacCreateHandler: PacCreateHandler; constructor(pacWrapper: PacWrapper) { this.pacWrapper = pacWrapper; + this.authEnvironmentService = new AuthEnvironmentService(pacWrapper); + this.agenticCreateHandler = new AgenticCreateHandler(pacWrapper); + this.pacCreateHandler = new PacCreateHandler(pacWrapper); + this.routes = this.buildRoutes(); + } + + /** + * Builds the deep-link routing table (URI path -> handler). Register new deep-link + * paths here so `handleUri` can dispatch to them. + */ + private buildRoutes(): ReadonlyMap { + return new Map([ + [UriPath.PcfInit, () => this.pcfInit()], + [UriPath.Open, (uri) => this.handleOpenPowerPages(uri)], + [UriPath.AgenticCreate, (uri) => this.agenticCreateHandler.handle(uri)], + [UriPath.PacCreate, (uri) => this.pacCreateHandler.handle(uri)], + ]); } // URIs targeting our extension are in the format @@ -28,11 +57,12 @@ class UriHandler implements vscode.UriHandler { // vscode://microsoft-IsvExpTools.powerplatform-vscode/pcfInit // vscode://microsoft-IsvExpTools.powerplatform-vscode/open?websiteid=xxx&envid=yyy&orgurl=zzz&schema=PortalSchemaV2 public async handleUri(uri: vscode.Uri): Promise { - if (uri.path === UriPath.PcfInit) { - return this.pcfInit(); - } else if (uri.path === UriPath.Open) { - return this.handleOpenPowerPages(uri); + const route = this.routes.get(uri.path); + if (route) { + await route(uri); + return; } + // Unrecognized paths are intentionally ignored for forward compatibility. } async pcfInit(): Promise { @@ -103,7 +133,7 @@ class UriHandler implements vscode.UriHandler { this.validateRequiredParameters(uriParams, telemetryData); // Prepare authentication and environment - await this.prepareAuthenticationAndEnvironment(uriParams, telemetryData); + await this.authEnvironmentService.prepareAuthenticationAndEnvironment(uriParams, telemetryData); // Handle the download process await this.handleSiteDownload(uriParams, telemetryData, startTime); @@ -209,170 +239,6 @@ class UriHandler implements vscode.UriHandler { } } - /** - * Handle authentication and environment setup - */ - private async prepareAuthenticationAndEnvironment(uriParams: UriParameters, telemetryData: Record): Promise { - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: URI_HANDLER_STRINGS.TITLES.POWER_PAGES, - cancellable: false - }, - async (progress) => { - progress.report({ - message: URI_HANDLER_STRINGS.PROGRESS.PREPARING, - increment: 10 - }); - - progress.report({ - message: URI_HANDLER_STRINGS.PROGRESS.VALIDATING_AUTH, - increment: 20 - }); - - // Check and handle authentication - await this.ensureAuthentication(uriParams, telemetryData, progress); - - progress.report({ - message: URI_HANDLER_STRINGS.PROGRESS.CHECKING_ENV, - increment: 20 - }); - - // Check and handle environment switching - await this.ensureCorrectEnvironment(uriParams, telemetryData, progress); - - progress.report({ - message: URI_HANDLER_STRINGS.PROGRESS.READY_TO_SELECT, - increment: 30 - }); - - // Brief delay to let user see the final progress message - await new Promise(resolve => setTimeout(resolve, 500)); - } - ); - } - - /** - * Ensure user is authenticated with PAC CLI - */ - private async ensureAuthentication(uriParams: UriParameters, telemetryData: Record, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { - let authInfo; - try { - authInfo = await this.pacWrapper.activeOrg(); - } catch (error) { - await this.resetPacProcessAndThrow(error, telemetryData, 'Failed to check authentication status', 'auth_check_failed'); - } - - if (!authInfo || authInfo.Status !== "Success") { - oneDSLoggerWrapper.getLogger().traceInfo( - uriHandlerTelemetryEventNames.URI_HANDLER_AUTH_REQUIRED, - { ...telemetryData, authStatus: authInfo?.Status || 'none' } - ); - - progress.report({ - message: URI_HANDLER_STRINGS.PROGRESS.AUTH_REQUIRED, - increment: 10 - }); - - const authRequired = await vscode.window.showWarningMessage( - URI_HANDLER_STRINGS.PROMPTS.AUTH_REQUIRED, - { modal: true }, - URI_HANDLER_STRINGS.BUTTONS.YES, - URI_HANDLER_STRINGS.BUTTONS.NO - ); - - if (authRequired === URI_HANDLER_STRINGS.BUTTONS.YES) { - try { - progress.report({ - message: URI_HANDLER_STRINGS.PROGRESS.AUTHENTICATING, - increment: 10 - }); - - await this.pacWrapper.authCreateNewAuthProfileForOrg(uriParams.orgUrl!); - - const newAuthInfo = await this.pacWrapper.activeOrg(); - if (!newAuthInfo || newAuthInfo.Status !== "Success") { - throw new Error(URI_HANDLER_STRINGS.ERRORS.AUTH_FAILED); - } - - oneDSLoggerWrapper.getLogger().traceInfo( - uriHandlerTelemetryEventNames.URI_HANDLER_AUTH_COMPLETED, - { ...telemetryData, newAuthStatus: newAuthInfo.Status } - ); - } catch (authError) { - await this.resetPacProcessAndThrow(authError, telemetryData, 'Authentication operation failed', 'auth_operation_failed'); - } - } else { - vscode.window.showInformationMessage(URI_HANDLER_STRINGS.INFO.DOWNLOAD_CANCELLED_AUTH); - oneDSLoggerWrapper.getLogger().traceInfo( - uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, - { ...telemetryData, reason: 'user_cancelled_auth' } - ); - throw new Error(URI_HANDLER_STRINGS.ERRORS.USER_CANCELLED_AUTH); - } - } - } - - /** - * Ensure we're connected to the correct environment - */ - private async ensureCorrectEnvironment(uriParams: UriParameters, telemetryData: Record, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { - let currentAuthInfo; - try { - currentAuthInfo = await this.pacWrapper.activeOrg(); - } catch (error) { - await this.resetPacProcessAndThrow(error, telemetryData, 'Failed to check current environment', 'env_check_failed'); - } - - if (currentAuthInfo?.Status === "Success" && currentAuthInfo.Results?.EnvironmentId !== uriParams.environmentId) { - oneDSLoggerWrapper.getLogger().traceInfo( - uriHandlerTelemetryEventNames.URI_HANDLER_ENV_SWITCH_REQUIRED, - { - ...telemetryData, - currentEnvId: currentAuthInfo.Results?.EnvironmentId || 'unknown', - requestedEnvId: uriParams.environmentId - } - ); - - const switchEnv = await vscode.window.showWarningMessage( - URI_HANDLER_STRINGS.PROMPTS.ENV_SWITCH_REQUIRED, - { modal: true }, - URI_HANDLER_STRINGS.BUTTONS.YES, - URI_HANDLER_STRINGS.BUTTONS.NO - ); - - if (switchEnv === URI_HANDLER_STRINGS.BUTTONS.YES) { - try { - progress.report({ - message: URI_HANDLER_STRINGS.PROGRESS.SWITCHING_ENV, - increment: 10 - }); - - await this.pacWrapper.orgSelect(uriParams.orgUrl!); - - const verifyAuthInfo = await this.pacWrapper.activeOrg(); - if (verifyAuthInfo?.Status !== "Success" || verifyAuthInfo.Results?.EnvironmentId !== uriParams.environmentId) { - throw new Error(URI_HANDLER_STRINGS.ERRORS.ENV_SWITCH_FAILED); - } - - oneDSLoggerWrapper.getLogger().traceInfo( - uriHandlerTelemetryEventNames.URI_HANDLER_ENV_SWITCH_COMPLETED, - { ...telemetryData, switchedToEnvId: verifyAuthInfo.Results?.EnvironmentId } - ); - } catch (error) { - await this.resetPacProcessAndThrow(error, telemetryData, 'Error switching environment', 'env_switch_error'); - } - } else { - vscode.window.showInformationMessage(URI_HANDLER_STRINGS.INFO.DOWNLOAD_CANCELLED_ENV); - oneDSLoggerWrapper.getLogger().traceInfo( - uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, - { ...telemetryData, reason: 'user_cancelled_env_switch' } - ); - throw new Error(URI_HANDLER_STRINGS.ERRORS.USER_CANCELLED_ENV_SWITCH); - } - } - } - /** * Handle the site download process */ @@ -457,44 +323,9 @@ class UriHandler implements vscode.UriHandler { { ...telemetryData, error: 'download_failed' } ); - await this.resetPacProcessSafely(telemetryData); + await this.authEnvironmentService.resetPacProcessSafely(telemetryData); throw error; } } - /** - * Reset PAC process and throw error - */ - private async resetPacProcessAndThrow(error: unknown, telemetryData: Record, message: string, errorType: string): Promise { - oneDSLoggerWrapper.getLogger().traceError( - uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, - message, - error instanceof Error ? error : new Error(String(error)), - { ...telemetryData, error: errorType } - ); - - await this.resetPacProcessSafely(telemetryData); - throw error; - } - - /** - * Safely reset PAC process without throwing - */ - private async resetPacProcessSafely(telemetryData: Record): Promise { - try { - await this.pacWrapper.resetPacProcess(); - oneDSLoggerWrapper.getLogger().traceInfo( - uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, - { ...telemetryData, message: 'PAC process reset after failure' } - ); - } catch (resetError) { - oneDSLoggerWrapper.getLogger().traceError( - uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, - 'Failed to reset PAC process after failure', - resetError instanceof Error ? resetError : new Error(String(resetError)), - { ...telemetryData, error: 'pac_reset_failed' } - ); - } - } - } diff --git a/src/client/uriHandler/utils/authEnvironment.ts b/src/client/uriHandler/utils/authEnvironment.ts new file mode 100644 index 000000000..d0cc94b18 --- /dev/null +++ b/src/client/uriHandler/utils/authEnvironment.ts @@ -0,0 +1,224 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + */ + +import * as vscode from "vscode"; +import { PacWrapper } from "../../pac/PacWrapper"; +import { oneDSLoggerWrapper } from "../../../common/OneDSLoggerTelemetry/oneDSLoggerWrapper"; +import { uriHandlerTelemetryEventNames } from "../telemetry/uriHandlerTelemetryEvents"; +import { URI_HANDLER_STRINGS } from "../constants/uriStrings"; +import { UriParameters } from "./uriHandlerUtils"; + +/** + * Encapsulates the PAC CLI authentication and environment-selection steps shared by the + * Power Pages deep-link flows. Extracted from `UriHandler` so that each deep-link handler + * (`/open` today, `/pacCreate` and `/agenticCreate` in follow-ups) can reuse the exact same + * auth/environment/reset behavior without duplicating it. + */ +export class AuthEnvironmentService { + private readonly pacWrapper: PacWrapper; + + constructor(pacWrapper: PacWrapper) { + this.pacWrapper = pacWrapper; + } + + /** + * Handle authentication and environment setup, reporting progress to the user. + */ + public async prepareAuthenticationAndEnvironment(uriParams: UriParameters, telemetryData: Record): Promise { + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: URI_HANDLER_STRINGS.TITLES.POWER_PAGES, + cancellable: false + }, + async (progress) => { + progress.report({ + message: URI_HANDLER_STRINGS.PROGRESS.PREPARING, + increment: 10 + }); + + progress.report({ + message: URI_HANDLER_STRINGS.PROGRESS.VALIDATING_AUTH, + increment: 20 + }); + + // Check and handle authentication + await this.ensureAuthentication(uriParams, telemetryData, progress); + + progress.report({ + message: URI_HANDLER_STRINGS.PROGRESS.CHECKING_ENV, + increment: 20 + }); + + // Check and handle environment switching + await this.ensureCorrectEnvironment(uriParams, telemetryData, progress); + + progress.report({ + message: URI_HANDLER_STRINGS.PROGRESS.READY_TO_SELECT, + increment: 30 + }); + + // Brief delay to let user see the final progress message + await new Promise(resolve => setTimeout(resolve, 500)); + } + ); + } + + /** + * Ensure user is authenticated with PAC CLI + */ + private async ensureAuthentication(uriParams: UriParameters, telemetryData: Record, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { + let authInfo; + try { + authInfo = await this.pacWrapper.activeOrg(); + } catch (error) { + await this.resetPacProcessAndThrow(error, telemetryData, 'Failed to check authentication status', 'auth_check_failed'); + } + + if (!authInfo || authInfo.Status !== "Success") { + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_AUTH_REQUIRED, + { ...telemetryData, authStatus: authInfo?.Status || 'none' } + ); + + progress.report({ + message: URI_HANDLER_STRINGS.PROGRESS.AUTH_REQUIRED, + increment: 10 + }); + + const authRequired = await vscode.window.showWarningMessage( + URI_HANDLER_STRINGS.PROMPTS.AUTH_REQUIRED, + { modal: true }, + URI_HANDLER_STRINGS.BUTTONS.YES, + URI_HANDLER_STRINGS.BUTTONS.NO + ); + + if (authRequired === URI_HANDLER_STRINGS.BUTTONS.YES) { + try { + progress.report({ + message: URI_HANDLER_STRINGS.PROGRESS.AUTHENTICATING, + increment: 10 + }); + + await this.pacWrapper.authCreateNewAuthProfileForOrg(uriParams.orgUrl!); + + const newAuthInfo = await this.pacWrapper.activeOrg(); + if (!newAuthInfo || newAuthInfo.Status !== "Success") { + throw new Error(URI_HANDLER_STRINGS.ERRORS.AUTH_FAILED); + } + + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_AUTH_COMPLETED, + { ...telemetryData, newAuthStatus: newAuthInfo.Status } + ); + } catch (authError) { + await this.resetPacProcessAndThrow(authError, telemetryData, 'Authentication operation failed', 'auth_operation_failed'); + } + } else { + vscode.window.showInformationMessage(URI_HANDLER_STRINGS.INFO.DOWNLOAD_CANCELLED_AUTH); + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, + { ...telemetryData, reason: 'user_cancelled_auth' } + ); + throw new Error(URI_HANDLER_STRINGS.ERRORS.USER_CANCELLED_AUTH); + } + } + } + + /** + * Ensure we're connected to the correct environment + */ + private async ensureCorrectEnvironment(uriParams: UriParameters, telemetryData: Record, progress: vscode.Progress<{ message?: string; increment?: number }>): Promise { + let currentAuthInfo; + try { + currentAuthInfo = await this.pacWrapper.activeOrg(); + } catch (error) { + await this.resetPacProcessAndThrow(error, telemetryData, 'Failed to check current environment', 'env_check_failed'); + } + + if (currentAuthInfo?.Status === "Success" && currentAuthInfo.Results?.EnvironmentId !== uriParams.environmentId) { + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_ENV_SWITCH_REQUIRED, + { + ...telemetryData, + currentEnvId: currentAuthInfo.Results?.EnvironmentId || 'unknown', + requestedEnvId: uriParams.environmentId + } + ); + + const switchEnv = await vscode.window.showWarningMessage( + URI_HANDLER_STRINGS.PROMPTS.ENV_SWITCH_REQUIRED, + { modal: true }, + URI_HANDLER_STRINGS.BUTTONS.YES, + URI_HANDLER_STRINGS.BUTTONS.NO + ); + + if (switchEnv === URI_HANDLER_STRINGS.BUTTONS.YES) { + try { + progress.report({ + message: URI_HANDLER_STRINGS.PROGRESS.SWITCHING_ENV, + increment: 10 + }); + + await this.pacWrapper.orgSelect(uriParams.orgUrl!); + + const verifyAuthInfo = await this.pacWrapper.activeOrg(); + if (verifyAuthInfo?.Status !== "Success" || verifyAuthInfo.Results?.EnvironmentId !== uriParams.environmentId) { + throw new Error(URI_HANDLER_STRINGS.ERRORS.ENV_SWITCH_FAILED); + } + + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_ENV_SWITCH_COMPLETED, + { ...telemetryData, switchedToEnvId: verifyAuthInfo.Results?.EnvironmentId } + ); + } catch (error) { + await this.resetPacProcessAndThrow(error, telemetryData, 'Error switching environment', 'env_switch_error'); + } + } else { + vscode.window.showInformationMessage(URI_HANDLER_STRINGS.INFO.DOWNLOAD_CANCELLED_ENV); + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, + { ...telemetryData, reason: 'user_cancelled_env_switch' } + ); + throw new Error(URI_HANDLER_STRINGS.ERRORS.USER_CANCELLED_ENV_SWITCH); + } + } + } + + /** + * Reset PAC process and throw error + */ + public async resetPacProcessAndThrow(error: unknown, telemetryData: Record, message: string, errorType: string): Promise { + oneDSLoggerWrapper.getLogger().traceError( + uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, + message, + error instanceof Error ? error : new Error(String(error)), + { ...telemetryData, error: errorType } + ); + + await this.resetPacProcessSafely(telemetryData); + throw error; + } + + /** + * Safely reset PAC process without throwing + */ + public async resetPacProcessSafely(telemetryData: Record): Promise { + try { + await this.pacWrapper.resetPacProcess(); + oneDSLoggerWrapper.getLogger().traceInfo( + uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, + { ...telemetryData, message: 'PAC process reset after failure' } + ); + } catch (resetError) { + oneDSLoggerWrapper.getLogger().traceError( + uriHandlerTelemetryEventNames.URI_HANDLER_OPEN_POWER_PAGES_FAILED, + 'Failed to reset PAC process after failure', + resetError instanceof Error ? resetError : new Error(String(resetError)), + { ...telemetryData, error: 'pac_reset_failed' } + ); + } + } +} diff --git a/src/common/ecs-features/ecsFeatureGates.ts b/src/common/ecs-features/ecsFeatureGates.ts index 54f334b43..c187946fc 100644 --- a/src/common/ecs-features/ecsFeatureGates.ts +++ b/src/common/ecs-features/ecsFeatureGates.ts @@ -130,3 +130,23 @@ export const { enableMetadataDiff: true, } }); + +export const { + feature: EnableAgenticCreateFromHome +} = getFeatureConfigs({ + teamName: PowerPagesClientName, + description: 'Enable the agentic create deep link (agenticCreate) launched from the Power Pages home page', + fallback: { + enableAgenticCreateFromHome: false, + } +}); + +export const { + feature: EnablePacCreateFromHome +} = getFeatureConfigs({ + teamName: PowerPagesClientName, + description: 'Enable the PAC CLI create deep link (pacCreate) launched from the Power Pages home page', + fallback: { + enablePacCreateFromHome: false, + } +});