Skip to content

Commit c90ef36

Browse files
committed
fix: improve URL handling of org open
1 parent a4ff82e commit c90ef36

File tree

5 files changed

+105
-57
lines changed

5 files changed

+105
-57
lines changed

AGENTS.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
# Agent Guidelines for this oclif plugin
2+
3+
This file provides guidance when working with code in this repository.
4+
5+
## Project Overview
6+
7+
This is `@salesforce/plugin-org`, an oclif plugin for the Salesforce CLI that provides commands for working with Salesforce orgs (scratch orgs, sandboxes, production orgs). It is bundled with the official Salesforce CLI and follows Salesforce's standard plugin architecture.
8+
9+
## Common Commands
10+
11+
### Development
12+
13+
```bash
14+
# Install dependencies and compile
15+
yarn install
16+
yarn build
17+
18+
# Compile TypeScript (incremental)
19+
yarn compile
20+
21+
# Run linter
22+
yarn lint
23+
24+
# Format code
25+
yarn format
26+
27+
# Run local development version of CLI
28+
./bin/dev.js org list
29+
./bin/dev.js org create scratch --help
30+
```
31+
32+
### Testing
33+
34+
```bash
35+
# Run all tests (unit + NUTs + linting + schemas)
36+
yarn test
37+
38+
# Run only unit tests
39+
yarn test:only
40+
41+
# Run unit tests in watch mode
42+
yarn test:watch
43+
44+
# Run NUTs (Non-Unit Tests) - integration tests against real orgs
45+
yarn test:nuts
46+
47+
# Run a specific NUT
48+
yarn mocha path/to/test.nut.ts
49+
```
50+
51+
### Local Development
52+
53+
```bash
54+
# Run commands via bin/dev.js, it compiles TS source on the fly (no need to run `yarn compile` after every change)
55+
./bin/dev.js org list
56+
```
57+
58+
## Architecture
59+
60+
### Command Structure
61+
62+
Commands follow oclif's file-based routing and are organized under `src/commands/org/`:
63+
64+
- `create/` - Create scratch orgs and sandboxes
65+
- `delete/` - Delete scratch orgs and sandboxes
66+
- `resume/` - Resume async org creation operations
67+
- `refresh/` - Refresh sandboxes
68+
- `list/` - List orgs and metadata
69+
- `open/` - Open orgs in browser
70+
- `enable/` and `disable/` - Manage source tracking
71+
72+
### Message Files
73+
74+
Messages are stored in `messages/*.md` files using Salesforce's message framework. Each command typically has its own message file (e.g., `create_scratch.md`, `create.sandbox.md`).
75+
76+
### Testing Structure
77+
78+
- `test/unit/` - Unit tests using Mocha + Sinon
79+
- `test/nut/` - Integration tests (NUTs) using `@salesforce/cli-plugins-testkit`
80+
- `test/shared/` - Tests for shared utilities
81+
- Sandbox NUTs (`*.sandboxNut.ts`) are extremely slow and should be run selectively via GitHub Actions
82+
83+
### Key Dependencies
84+
85+
- `@oclif/core` - CLI framework
86+
- `@salesforce/core` - Core Salesforce functionality (Org, AuthInfo, Connection, etc.)
87+
- `@salesforce/sf-plugins-core` - Shared plugin utilities and base command classes
88+
- `@salesforce/source-deploy-retrieve` - Metadata operations
89+
- `@oclif/multi-stage-output` - Progress indicators for long-running operations
90+
91+
## Testing Notes
92+
93+
- Sandbox NUTs are slow due to actual org creation/refresh operations
94+
- Use the `SandboxNuts` GitHub Action workflow instead of running locally
95+
- Unit tests should mock `@salesforce/core` components (Org, Connection, etc.)
96+
- NUTs use real orgs and require appropriate hub/production org authentication

src/commands/org/open.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
} from '@salesforce/sf-plugins-core';
1414
import { Messages, Org } from '@salesforce/core';
1515
import { MetadataResolver } from '@salesforce/source-deploy-retrieve';
16-
import { buildFrontdoorUrl } from '../../shared/orgOpenUtils.js';
1716
import { OrgOpenCommandBase } from '../../shared/orgOpenCommandBase.js';
1817
import { type OrgOpenOutput } from '../../shared/orgTypes.js';
1918

@@ -69,12 +68,12 @@ export class OrgOpenCommand extends OrgOpenCommandBase<OrgOpenOutput> {
6968
this.org = flags['target-org'];
7069
this.connection = this.org.getConnection(flags['api-version']);
7170

72-
const [frontDoorUrl, retUrl] = await Promise.all([
73-
buildFrontdoorUrl(this.org),
74-
flags['source-file'] ? generateFileUrl(flags['source-file'], this.org) : flags.path,
75-
]);
71+
// `org.getMetadataUIURL` already generates a Frontdoor URL
72+
if (flags['source-file']) {
73+
return this.openOrgUI(flags, await generateFileUrl(flags['source-file'], this.org));
74+
}
7675

77-
return this.openOrgUI(flags, frontDoorUrl, retUrl);
76+
return this.openOrgUI(flags, await this.org.getFrontDoorUrl(flags.path));
7877
}
7978
}
8079

src/commands/org/open/agent.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77

88
import { Flags } from '@salesforce/sf-plugins-core';
99
import { Connection, Messages } from '@salesforce/core';
10-
import { buildFrontdoorUrl } from '../../../shared/orgOpenUtils.js';
1110
import { OrgOpenCommandBase } from '../../../shared/orgOpenCommandBase.js';
1211
import { type OrgOpenOutput } from '../../../shared/orgTypes.js';
1312

@@ -51,12 +50,9 @@ export class OrgOpenAgent extends OrgOpenCommandBase<OrgOpenOutput> {
5150
this.org = flags['target-org'];
5251
this.connection = this.org.getConnection(flags['api-version']);
5352

54-
const [frontDoorUrl, retUrl] = await Promise.all([
55-
buildFrontdoorUrl(this.org),
56-
buildRetUrl(this.connection, flags['api-name']),
57-
]);
53+
const agentBuilderRedirect = await buildRetUrl(this.connection, flags['api-name']);
5854

59-
return this.openOrgUI(flags, frontDoorUrl, encodeURIComponent(retUrl));
55+
return this.openOrgUI(flags, await this.org.getFrontDoorUrl(agentBuilderRedirect));
6056
}
6157
}
6258

src/shared/orgOpenCommandBase.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,8 @@ export abstract class OrgOpenCommandBase<T> extends SfCommand<T> {
3030
protected org!: Org;
3131
protected connection!: Connection;
3232

33-
protected async openOrgUI(flags: OrgOpenFlags, frontDoorUrl: string, retUrl?: string): Promise<OrgOpenOutput> {
33+
protected async openOrgUI(flags: OrgOpenFlags, url: string): Promise<OrgOpenOutput> {
3434
const orgId = this.org.getOrgId();
35-
const url = `${frontDoorUrl}${
36-
retUrl ? `&${frontDoorUrl.includes('.jsp?otp=') ? `startURL=${retUrl}` : `retURL=${retUrl}`}` : ''
37-
}`;
3835

3936
// TODO: better typings in sfdx-core for orgs read from auth files
4037
const username = this.org.getUsername() as string;

src/shared/orgOpenUtils.ts

Lines changed: 1 addition & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -5,37 +5,16 @@
55
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8-
import { rmSync } from 'node:fs';
98
import { ChildProcess } from 'node:child_process';
109
import open, { Options } from 'open';
11-
import { Logger, Messages, Org, SfError } from '@salesforce/core';
10+
import { Logger, Messages, SfError } from '@salesforce/core';
1211
import { Duration, Env } from '@salesforce/kit';
1312

1413
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
1514
const messages = Messages.loadMessages('@salesforce/plugin-org', 'open');
16-
const sharedMessages = Messages.loadMessages('@salesforce/plugin-org', 'messages');
1715

1816
export const openUrl = async (url: string, options: Options): Promise<ChildProcess> => open(url, options);
1917

20-
export const fileCleanup = (tempFilePath: string): void =>
21-
rmSync(tempFilePath, { force: true, maxRetries: 3, recursive: true });
22-
23-
/**
24-
* This method generates and returns a frontdoor url for the given org.
25-
*
26-
* @param org org for which we generate the frontdoor url.
27-
*/
28-
29-
export const buildFrontdoorUrl = async (org: Org): Promise<string> => {
30-
try {
31-
return await org.getFrontDoorUrl();
32-
} catch (e) {
33-
if (e instanceof SfError) throw e;
34-
const err = e as Error;
35-
throw new SfError(sharedMessages.getMessage('SingleAccessFrontdoorError'), err.message);
36-
}
37-
};
38-
3918
export const handleDomainError = (err: unknown, url: string, env: Env): string => {
4019
if (err instanceof Error) {
4120
if (err.message.includes('timeout')) {
@@ -55,26 +34,7 @@ export const handleDomainError = (err: unknown, url: string, env: Env): string =
5534
throw err;
5635
};
5736

58-
/** builds the html file that does an automatic post to the frontdoor url */
59-
export const getFileContents = (
60-
authToken: string,
61-
instanceUrl: string,
62-
// we have to defalt this to get to Setup only on the POST version. GET goes to Setup automatically
63-
retUrl = '/lightning/setup/SetupOneHome/home'
64-
): string => `
65-
<html>
66-
<body onload="document.body.firstElementChild.submit()">
67-
<form method="POST" action="${instanceUrl}/secur/frontdoor.jsp">
68-
<input type="hidden" name="sid" value="${authToken}" />
69-
<input type="hidden" name="retURL" value="${retUrl}" />
70-
</form>
71-
</body>
72-
</html>`;
73-
7437
export default {
7538
openUrl,
76-
fileCleanup,
77-
buildFrontdoorUrl,
7839
handleDomainError,
79-
getFileContents,
8040
};

0 commit comments

Comments
 (0)