From 95c6d44a38dee64e755c4d5492b7811289a919df Mon Sep 17 00:00:00 2001 From: Steve Hetzel Date: Mon, 11 Nov 2024 18:01:27 -0700 Subject: [PATCH 1/4] feat: add org open agent --- command-snapshot.json | 8 ++ messages/open.agent.md | 41 +++++++++ schemas/org-open-agent.json | 22 +++++ src/commands/org/open.ts | 146 +++---------------------------- src/commands/org/open/agent.ts | 67 ++++++++++++++ src/shared/orgOpenCommandBase.ts | 105 ++++++++++++++++++++++ src/shared/orgOpenUtils.ts | 73 ++++++++++++++++ src/shared/orgTypes.ts | 6 ++ src/shared/utils.ts | 5 -- test/nut/listAndDisplay.nut.ts | 3 +- test/nut/open.nut.ts | 2 +- test/unit/org/open.test.ts | 5 +- 12 files changed, 337 insertions(+), 146 deletions(-) create mode 100644 messages/open.agent.md create mode 100644 schemas/org-open-agent.json create mode 100644 src/commands/org/open/agent.ts create mode 100644 src/shared/orgOpenCommandBase.ts create mode 100644 src/shared/orgOpenUtils.ts diff --git a/command-snapshot.json b/command-snapshot.json index 5b15dfe1..769326fb 100644 --- a/command-snapshot.json +++ b/command-snapshot.json @@ -136,6 +136,14 @@ ], "plugin": "@salesforce/plugin-org" }, + { + "alias": [], + "command": "org:open:agent", + "flagAliases": ["urlonly"], + "flagChars": ["b", "n", "o", "r"], + "flags": ["api-version", "browser", "flags-dir", "json", "name", "private", "target-org", "url-only"], + "plugin": "@salesforce/plugin-org" + }, { "alias": [], "command": "org:refresh:sandbox", diff --git a/messages/open.agent.md b/messages/open.agent.md new file mode 100644 index 00000000..07581f14 --- /dev/null +++ b/messages/open.agent.md @@ -0,0 +1,41 @@ +# summary + +Open an agent in the Agent Builder org UI in a browser. + +# description + +Use the --name flag to open an agent using the developer name (aka API name) in the Agent Builder Org UI. + +To generate a URL but not launch it in your browser, specify --url-only. + +To open in a specific browser, use the --browser flag. Supported browsers are "chrome", "edge", and "firefox". If you don't specify --browser, the org opens in your default browser. + +# examples + +- Open the agent with developer name "Coral_Cloud_Agent using the default browser: + + $ <%= config.bin %> <%= command.id %> --name Coral_Cloud_Agent + +- Open the agent in an incognito window of your default browser: + + $ <%= config.bin %> <%= command.id %> --private --name Coral_Cloud_Agent + +- Open the agent in the org with alias MyTestOrg1 using the Firefox browser: + + $ <%= config.bin %> <%= command.id %> --target-org MyTestOrg1 --browser firefox --name Coral_Cloud_Agent + +# flags.name.summary + +The developer name (aka API name) of the agent to open in the Agent Builder org UI. + +# flags.private.summary + +Open the org in the default browser using private (incognito) mode. + +# flags.browser.summary + +Browser where the org opens. + +# flags.url-only.summary + +Display navigation URL, but don’t launch browser. diff --git a/schemas/org-open-agent.json b/schemas/org-open-agent.json new file mode 100644 index 00000000..3522eaab --- /dev/null +++ b/schemas/org-open-agent.json @@ -0,0 +1,22 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "$ref": "#/definitions/OrgOpenOutput", + "definitions": { + "OrgOpenOutput": { + "type": "object", + "properties": { + "url": { + "type": "string" + }, + "username": { + "type": "string" + }, + "orgId": { + "type": "string" + } + }, + "required": ["url", "username", "orgId"], + "additionalProperties": false + } + } +} diff --git a/src/commands/org/open.ts b/src/commands/org/open.ts index 635ad584..f620a8c2 100644 --- a/src/commands/org/open.ts +++ b/src/commands/org/open.ts @@ -6,27 +6,22 @@ */ import path from 'node:path'; -import { platform, tmpdir } from 'node:os'; -import fs from 'node:fs'; -import { execSync } from 'node:child_process'; import { Flags, loglevel, orgApiVersionFlagWithDeprecations, requiredOrgFlagWithDeprecations, - SfCommand, } from '@salesforce/sf-plugins-core'; -import isWsl from 'is-wsl'; -import { Connection, Logger, Messages, Org, SfdcUrl, SfError } from '@salesforce/core'; -import { Duration, Env, sleep } from '@salesforce/kit'; +import { Connection, Messages } from '@salesforce/core'; import { MetadataResolver } from '@salesforce/source-deploy-retrieve'; -import { apps } from 'open'; -import utils from '../../shared/utils.js'; +import { buildFrontdoorUrl } from '../../shared/orgOpenUtils.js'; +import { OrgOpenCommandBase } from '../../shared/orgOpenCommandBase.js'; +import { type OrgOpenOutput } from '../../shared/orgTypes.js'; Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); const messages = Messages.loadMessages('@salesforce/plugin-org', 'open'); -export class OrgOpenCommand extends SfCommand { +export class OrgOpenCommand extends OrgOpenCommandBase { public static readonly summary = messages.getMessage('summary'); public static readonly description = messages.getMessage('description'); public static readonly examples = messages.getMessages('examples'); @@ -71,104 +66,18 @@ export class OrgOpenCommand extends SfCommand { public async run(): Promise { const { flags } = await this.parse(OrgOpenCommand); - const conn = flags['target-org'].getConnection(flags['api-version']); + this.org = flags['target-org']; + this.connection = this.org.getConnection(flags['api-version']); - const env = new Env(); const [frontDoorUrl, retUrl] = await Promise.all([ - buildFrontdoorUrl(flags['target-org'], conn), - flags['source-file'] ? generateFileUrl(flags['source-file'], conn) : flags.path, + buildFrontdoorUrl(this.org, this.connection), + flags['source-file'] ? generateFileUrl(flags['source-file'], this.connection) : flags.path, ]); - const url = `${frontDoorUrl}${retUrl ? `&retURL=${retUrl}` : ''}`; - - const orgId = flags['target-org'].getOrgId(); - // TODO: better typings in sfdx-core for orgs read from auth files - const username = flags['target-org'].getUsername() as string; - const output = { orgId, url, username }; - // NOTE: Deliberate use of `||` here since getBoolean() defaults to false, and we need to consider both env vars. - const containerMode = env.getBoolean('SF_CONTAINER_MODE') || env.getBoolean('SFDX_CONTAINER_MODE'); - - // security warning only for --json OR --url-only OR containerMode - if (flags['url-only'] || Boolean(flags.json) || containerMode) { - const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages'); - this.warn(sharedMessages.getMessage('SecurityWarning')); - this.log(''); - } - - if (containerMode) { - // instruct the user that they need to paste the URL into the browser - this.styledHeader('Action Required!'); - this.log(messages.getMessage('containerAction', [orgId, url])); - return output; - } - - if (flags['url-only']) { - // this includes the URL - this.logSuccess(messages.getMessage('humanSuccess', [orgId, username, url])); - return output; - } - - this.logSuccess(messages.getMessage('humanSuccessNoUrl', [orgId, username])); - // we actually need to open the org - try { - this.spinner.start(messages.getMessage('domainWaiting')); - await new SfdcUrl(url).checkLightningDomain(); - this.spinner.stop(); - } catch (err) { - handleDomainError(err, url, env); - } - - // create a local html file that contains the POST stuff. - const tempFilePath = path.join(tmpdir(), `org-open-${new Date().valueOf()}.html`); - await fs.promises.writeFile( - tempFilePath, - getFileContents( - conn.accessToken as string, - conn.instanceUrl, - // the path flag is URI-encoded in its `parse` func. - // For the form redirect to work we need it decoded. - flags.path ? decodeURIComponent(flags.path) : retUrl - ) - ); - const filePathUrl = isWsl - ? 'file:///' + execSync(`wslpath -m ${tempFilePath}`).toString().trim() - : `file:///${tempFilePath}`; - const cp = await utils.openUrl(filePathUrl, { - ...(flags.browser ? { app: { name: apps[flags.browser] } } : {}), - ...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}), - }); - cp.on('error', (err) => { - fileCleanup(tempFilePath); - throw SfError.wrap(err); - }); - // so we don't delete the file while the browser is still using it - // open returns when the CP is spawned, but there's not way to know if the browser is still using the file - await sleep(platform() === 'win32' || isWsl ? 7000 : 5000); - fileCleanup(tempFilePath); - - return output; + return this.openOrgUI(flags, frontDoorUrl, retUrl); } } -export type OrgOpenOutput = { - url: string; - username: string; - orgId: string; -}; - -const fileCleanup = (tempFilePath: string): void => - fs.rmSync(tempFilePath, { force: true, maxRetries: 3, recursive: true }); - -const buildFrontdoorUrl = async (org: Org, conn: Connection): Promise => { - await org.refreshAuth(); // we need a live accessToken for the frontdoor url - const accessToken = conn.accessToken; - if (!accessToken) { - throw new SfError('NoAccessToken', 'NoAccessToken'); - } - const instanceUrlClean = org.getField(Org.Fields.INSTANCE_URL).replace(/\/$/, ''); - return `${instanceUrlClean}/secur/frontdoor.jsp?sid=${accessToken}`; -}; - const generateFileUrl = async (file: string, conn: Connection): Promise => { try { const metadataResolver = new MetadataResolver(); @@ -225,38 +134,3 @@ const flowFileNameToId = async (conn: Connection, filePath: string): Promise ` - - -
- - -
- -`; - -const handleDomainError = (err: unknown, url: string, env: Env): string => { - if (err instanceof Error) { - if (err.message.includes('timeout')) { - const host = /https?:\/\/([^.]*)/.exec(url)?.[1]; - if (!host) { - throw new SfError('InvalidUrl', 'InvalidUrl'); - } - const domain = `https://${host}.lightning.force.com`; - const domainRetryTimeout = env.getNumber('SF_DOMAIN_RETRY') ?? env.getNumber('SFDX_DOMAIN_RETRY', 240); - const timeout = new Duration(domainRetryTimeout, Duration.Unit.SECONDS); - const logger = Logger.childFromRoot('org:open'); - logger.debug(`Did not find IP for ${domain} after ${timeout.seconds} seconds`); - throw new SfError(messages.getMessage('domainTimeoutError'), 'domainTimeoutError'); - } - throw SfError.wrap(err); - } - throw err; -}; diff --git a/src/commands/org/open/agent.ts b/src/commands/org/open/agent.ts new file mode 100644 index 00000000..ede402ef --- /dev/null +++ b/src/commands/org/open/agent.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { Flags } from '@salesforce/sf-plugins-core'; +import { Connection, Messages } from '@salesforce/core'; +import { buildFrontdoorUrl } from '../../../shared/orgOpenUtils.js'; +import { OrgOpenCommandBase } from '../../../shared/orgOpenCommandBase.js'; +import { type OrgOpenOutput } from '../../../shared/orgTypes.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-org', 'open.agent'); + +export class OrgOpenAgent extends OrgOpenCommandBase { + public static readonly summary = messages.getMessage('summary'); + public static readonly description = messages.getMessage('description'); + public static readonly examples = messages.getMessages('examples'); + + public static readonly flags = { + 'target-org': Flags.requiredOrg(), + 'api-version': Flags.orgApiVersion(), + name: Flags.string({ + char: 'n', + summary: messages.getMessage('flags.name.summary'), + required: true, + }), + private: Flags.boolean({ + summary: messages.getMessage('flags.private.summary'), + exclusive: ['url-only', 'browser'], + }), + browser: Flags.option({ + char: 'b', + summary: messages.getMessage('flags.browser.summary'), + options: ['chrome', 'edge', 'firefox'] as const, // These are ones supported by "open" package + exclusive: ['url-only', 'private'], + })(), + 'url-only': Flags.boolean({ + char: 'r', + summary: messages.getMessage('flags.url-only.summary'), + aliases: ['urlonly'], + deprecateAliases: true, + }), + }; + + public async run(): Promise { + const { flags } = await this.parse(OrgOpenAgent); + this.org = flags['target-org']; + this.connection = this.org.getConnection(flags['api-version']); + + const [frontDoorUrl, retUrl] = await Promise.all([ + buildFrontdoorUrl(this.org, this.connection), + buildRetUrl(this.connection, flags.name), + ]); + + return this.openOrgUI(flags, frontDoorUrl, retUrl); + } +} + +// Build the URL part to the Agent Builder given a Bot API name. +const buildRetUrl = async (conn: Connection, botName: string): Promise => { + const query = `SELECT id FROM BotDefinition WHERE DeveloperName='${botName}'`; + const botId = (await conn.singleRecordQuery<{ Id: string }>(query)).Id; + return `AiCopilot/copilotStudio.app#/copilot/builder?copilotId=${botId}`; +}; diff --git a/src/shared/orgOpenCommandBase.ts b/src/shared/orgOpenCommandBase.ts new file mode 100644 index 00000000..421c4a59 --- /dev/null +++ b/src/shared/orgOpenCommandBase.ts @@ -0,0 +1,105 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import path from 'node:path'; +import fs from 'node:fs'; +import { platform, tmpdir } from 'node:os'; +import { execSync } from 'node:child_process'; +import isWsl from 'is-wsl'; +import { apps } from 'open'; +import { SfCommand } from '@salesforce/sf-plugins-core'; +import { Connection, Messages, Org, SfdcUrl, SfError } from '@salesforce/core'; +import { env, sleep } from '@salesforce/kit'; +import utils, { fileCleanup, getFileContents, handleDomainError } from './orgOpenUtils.js'; +import { type OrgOpenOutput } from './orgTypes.js'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-org', 'open'); + +type OrgOpenFlags = { + 'url-only': boolean; + browser?: 'chrome' | 'firefox' | 'edge'; + path?: string; + private: boolean; +}; + +export abstract class OrgOpenCommandBase extends SfCommand { + // Set by concrete classes in `run()` + protected org!: Org; + protected connection!: Connection; + + protected async openOrgUI(flags: OrgOpenFlags, frontDoorUrl: string, retUrl?: string): Promise { + const orgId = this.org.getOrgId(); + const url = `${frontDoorUrl}${retUrl ? `&retURL=${retUrl}` : ''}`; + + // TODO: better typings in sfdx-core for orgs read from auth files + const username = this.org.getUsername() as string; + const output = { orgId, url, username }; + // NOTE: Deliberate use of `||` here since getBoolean() defaults to false, and we need to consider both env vars. + const containerMode = env.getBoolean('SF_CONTAINER_MODE') || env.getBoolean('SFDX_CONTAINER_MODE'); + + // security warning only for --json OR --url-only OR containerMode + if (flags['url-only'] || this.jsonEnabled() || containerMode) { + const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages'); + this.warn(sharedMessages.getMessage('SecurityWarning')); + this.log(''); + } + + if (containerMode) { + // instruct the user that they need to paste the URL into the browser + this.styledHeader('Action Required!'); + this.log(messages.getMessage('containerAction', [orgId, url])); + return output; + } + + if (flags['url-only']) { + // this includes the URL + this.logSuccess(messages.getMessage('humanSuccess', [orgId, username, url])); + return output; + } + + this.logSuccess(messages.getMessage('humanSuccessNoUrl', [orgId, username])); + // we actually need to open the org + try { + this.spinner.start(messages.getMessage('domainWaiting')); + await new SfdcUrl(url).checkLightningDomain(); + this.spinner.stop(); + } catch (err) { + handleDomainError(err, url, env); + } + + // create a local html file that contains the POST stuff. + const tempFilePath = path.join(tmpdir(), `org-open-${new Date().valueOf()}.html`); + await fs.promises.writeFile( + tempFilePath, + getFileContents( + this.connection.accessToken as string, + this.connection.instanceUrl, + // the path flag is URI-encoded in its `parse` func. + // For the form redirect to work we need it decoded. + flags.path ? decodeURIComponent(flags.path) : retUrl + ) + ); + const filePathUrl = isWsl + ? 'file:///' + execSync(`wslpath -m ${tempFilePath}`).toString().trim() + : `file:///${tempFilePath}`; + const cp = await utils.openUrl(filePathUrl, { + ...(flags.browser ? { app: { name: apps[flags.browser] } } : {}), + ...(flags.private ? { newInstance: platform() === 'darwin', app: { name: apps.browserPrivate } } : {}), + }); + cp.on('error', (err) => { + fileCleanup(tempFilePath); + throw SfError.wrap(err); + }); + // so we don't delete the file while the browser is still using it + // open returns when the CP is spawned, but there's not way to know if the browser is still using the file + await sleep(platform() === 'win32' || isWsl ? 7000 : 5000); + fileCleanup(tempFilePath); + + return output; + } +} diff --git a/src/shared/orgOpenUtils.ts b/src/shared/orgOpenUtils.ts new file mode 100644 index 00000000..0ad91ed5 --- /dev/null +++ b/src/shared/orgOpenUtils.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2020, salesforce.com, inc. + * All rights reserved. + * Licensed under the BSD 3-Clause license. + * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause + */ + +import { rmSync } from 'node:fs'; +import { ChildProcess } from 'node:child_process'; +import open, { Options } from 'open'; +import { Connection, Logger, Messages, Org, SfError } from '@salesforce/core'; +import { Duration, Env } from '@salesforce/kit'; + +Messages.importMessagesDirectoryFromMetaUrl(import.meta.url); +const messages = Messages.loadMessages('@salesforce/plugin-org', 'open'); + +export const openUrl = async (url: string, options: Options): Promise => open(url, options); + +export const fileCleanup = (tempFilePath: string): void => + rmSync(tempFilePath, { force: true, maxRetries: 3, recursive: true }); + +export const buildFrontdoorUrl = async (org: Org, conn: Connection): Promise => { + await org.refreshAuth(); // we need a live accessToken for the frontdoor url + const accessToken = conn.accessToken; + if (!accessToken) { + throw new SfError('NoAccessToken', 'NoAccessToken'); + } + const instanceUrlClean = org.getField(Org.Fields.INSTANCE_URL).replace(/\/$/, ''); + return `${instanceUrlClean}/secur/frontdoor.jsp?sid=${accessToken}`; +}; + +export const handleDomainError = (err: unknown, url: string, env: Env): string => { + if (err instanceof Error) { + if (err.message.includes('timeout')) { + const host = /https?:\/\/([^.]*)/.exec(url)?.[1]; + if (!host) { + throw new SfError('InvalidUrl', 'InvalidUrl'); + } + const domain = `https://${host}.lightning.force.com`; + const domainRetryTimeout = env.getNumber('SF_DOMAIN_RETRY') ?? env.getNumber('SFDX_DOMAIN_RETRY', 240); + const timeout = new Duration(domainRetryTimeout, Duration.Unit.SECONDS); + const logger = Logger.childFromRoot('org:open'); + logger.debug(`Did not find IP for ${domain} after ${timeout.seconds} seconds`); + throw new SfError(messages.getMessage('domainTimeoutError'), 'domainTimeoutError'); + } + throw SfError.wrap(err); + } + throw err; +}; + +/** builds the html file that does an automatic post to the frontdoor url */ +export const getFileContents = ( + authToken: string, + instanceUrl: string, + // we have to defalt this to get to Setup only on the POST version. GET goes to Setup automatically + retUrl = '/lightning/setup/SetupOneHome/home' +): string => ` + + +
+ + +
+ +`; + +export default { + openUrl, + fileCleanup, + buildFrontdoorUrl, + handleDomainError, + getFileContents, +}; diff --git a/src/shared/orgTypes.ts b/src/shared/orgTypes.ts index 226cde7d..434d3e2f 100644 --- a/src/shared/orgTypes.ts +++ b/src/shared/orgTypes.ts @@ -23,6 +23,12 @@ export type OrgDisplayReturn = Partial & { sfdxAuthUrl?: string; }; +export type OrgOpenOutput = { + url: string; + username: string; + orgId: string; +}; + /** Convenience type for the fields that are in the auth file * * core's AuthFields has everything as optional. diff --git a/src/shared/utils.ts b/src/shared/utils.ts index 47b730f7..211ef780 100644 --- a/src/shared/utils.ts +++ b/src/shared/utils.ts @@ -4,10 +4,8 @@ * Licensed under the BSD 3-Clause license. * For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause */ -import { ChildProcess } from 'node:child_process'; import { upperFirst } from '@salesforce/kit'; import { StateAggregator } from '@salesforce/core'; -import open, { Options } from 'open'; export const getAliasByUsername = async (username: string): Promise => { const stateAggregator = await StateAggregator.getInstance(); @@ -16,8 +14,6 @@ export const getAliasByUsername = async (username: string): Promise => open(url, options); - export const lowerToUpper = (object: Record): Record => // the API has keys defined in capital camel case, while the definition schema has them as lower camel case // we need to convert lower camel case to upper before merging options so they will override properly @@ -25,7 +21,6 @@ export const lowerToUpper = (object: Record): Record Date: Tue, 12 Nov 2024 21:23:10 +0000 Subject: [PATCH 2/4] chore(release): 5.1.5-beta.0 [skip ci] --- README.md | 79 ++++++++++++++++++++++++++++++++++++++++++---------- package.json | 2 +- 2 files changed, 66 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index a1335444..b41fd2d1 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ the [SandboxNuts](https://github.com/salesforcecli/plugin-org/actions/workflows/ - [`sf org list metadata`](#sf-org-list-metadata) - [`sf org list metadata-types`](#sf-org-list-metadata-types) - [`sf org open`](#sf-org-open) +- [`sf org open agent`](#sf-org-open-agent) - [`sf org refresh sandbox`](#sf-org-refresh-sandbox) - [`sf org resume sandbox`](#sf-org-resume-sandbox) - [`sf org resume scratch`](#sf-org-resume-scratch) @@ -241,7 +242,7 @@ FLAG DESCRIPTIONS You can specify either --source-sandbox-name or --source-id when cloning an existing sandbox, but not both. ``` -_See code: [src/commands/org/create/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/create/sandbox.ts)_ +_See code: [src/commands/org/create/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/create/sandbox.ts)_ ## `sf org create scratch` @@ -395,7 +396,7 @@ FLAG DESCRIPTIONS Omit this flag to have Salesforce generate a unique username for your org. ``` -_See code: [src/commands/org/create/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/create/scratch.ts)_ +_See code: [src/commands/org/create/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/create/scratch.ts)_ ## `sf org delete sandbox` @@ -441,7 +442,7 @@ EXAMPLES $ sf org delete sandbox --target-org my-sandbox --no-prompt ``` -_See code: [src/commands/org/delete/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/delete/sandbox.ts)_ +_See code: [src/commands/org/delete/sandbox.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/delete/sandbox.ts)_ ## `sf org delete scratch` @@ -485,7 +486,7 @@ EXAMPLES $ sf org delete scratch --target-org my-scratch-org --no-prompt ``` -_See code: [src/commands/org/delete/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/delete/scratch.ts)_ +_See code: [src/commands/org/delete/scratch.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/delete/scratch.ts)_ ## `sf org disable tracking` @@ -524,7 +525,7 @@ EXAMPLES $ sf org disable tracking ``` -_See code: [src/commands/org/disable/tracking.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/disable/tracking.ts)_ +_See code: [src/commands/org/disable/tracking.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/disable/tracking.ts)_ ## `sf org display` @@ -569,7 +570,7 @@ EXAMPLES $ sf org display --target-org TestOrg1 --verbose ``` -_See code: [src/commands/org/display.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/display.ts)_ +_See code: [src/commands/org/display.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/display.ts)_ ## `sf org enable tracking` @@ -611,7 +612,7 @@ EXAMPLES $ sf org enable tracking ``` -_See code: [src/commands/org/enable/tracking.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/enable/tracking.ts)_ +_See code: [src/commands/org/enable/tracking.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/enable/tracking.ts)_ ## `sf org list` @@ -650,7 +651,7 @@ EXAMPLES $ sf org list --clean ``` -_See code: [src/commands/org/list.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/list.ts)_ +_See code: [src/commands/org/list.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/list.ts)_ ## `sf org list metadata` @@ -717,7 +718,7 @@ FLAG DESCRIPTIONS Examples of metadata types that use folders are Dashboard, Document, EmailTemplate, and Report. ``` -_See code: [src/commands/org/list/metadata.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/list/metadata.ts)_ +_See code: [src/commands/org/list/metadata.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/list/metadata.ts)_ ## `sf org list metadata-types` @@ -772,7 +773,7 @@ FLAG DESCRIPTIONS Override the api version used for api requests made by this command ``` -_See code: [src/commands/org/list/metadata-types.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/list/metadata-types.ts)_ +_See code: [src/commands/org/list/metadata-types.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/list/metadata-types.ts)_ ## `sf org open` @@ -848,7 +849,57 @@ EXAMPLES $ sf org open --source-file force-app/main/default/bots/Coral_Cloud_Agent/Coral_Cloud_Agent.bot-meta.xml ``` -_See code: [src/commands/org/open.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.4/src/commands/org/open.ts)_ +_See code: [src/commands/org/open.ts](https://github.com/salesforcecli/plugin-org/blob/5.1.5-beta.0/src/commands/org/open.ts)_ + +## `sf org open agent` + +Open an agent in the Agent Builder org UI in a browser. + +``` +USAGE + $ sf org open agent -o -n [--json] [--flags-dir ] [--api-version ] [--private | -r | + -b chrome|edge|firefox] + +FLAGS + -b, --browser=