Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: env open @W-9104301@ #31

Merged
merged 8 commits into from
Jun 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions command-snapshot.json
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
[
{
"command": "env:list",
"plugin": "@salesforce/plugin-env",
"flags": [
"all",
"columns",
"csv",
"extended",
"filter",
"no-header",
"no-truncate",
"output",
"sort"
]
}
]
{
"command": "env:list",
"plugin": "@salesforce/plugin-env",
"flags": ["all", "columns", "csv", "extended", "filter", "no-header", "no-truncate", "output", "sort"]
},
{
"command": "env:open",
"plugin": "@salesforce/plugin-env",
"flags": ["browser", "path", "target-env", "url-only"]
}
]
73 changes: 73 additions & 0 deletions messages/open.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# summary

Open an environment in your web browser.

# description

You can open the following types of environments in a web browser: scratch orgs, sandboxes, Dev Hubs, and production orgs.

If you run the command without flags, it attempts to open your default environment in your default web browser.

Each of your environments is associated with an instance URL, such as https://login.salesforce.com. To open a specific web page at that URL, specify the portion of the URL after "<URL>/" with the --path flag, such as /apex/YourPage to open a Visualforce page.

# flags.path.summary

Path to append to the end of the login URL.

# flags.url-only.summary

Display the URL, but don’t launch it in a browser.

# flags.target-env.summary

Environment name or alias to open.

# flags.target-env.description

Specify the login user or alias that’s associated with the environment. For scratch orgs, the login user is generated by the command that created the scratch org. You can also set an alias for the scratch org when you create it.

For Dev Hubs, sandboxes, and production orgs, specify the alias you set when you logged into the org with "sf login".

# flags.browser.summary

Browser in which to open the environment.

# flag.browser.description

Specify a browser by its app name according to your operating system. For example, Chrome’s app name is "google chrome" on macOS, "google-chrome" on Linux and "chrome" on Windows. So to open an environment in Chrome on macOS, specify --browser "google chrome". If you don’t specify --browser, the environment opens in your default browser.

# examples

- To open your default environment, run the command without flags:
sf env open
- This example opens the Visualforce page /apex/StartHere in a scratch org
with alias "test-org":

sf env open --target-env test-org --path /apex/StartHere

- If you want to view the URL for the preceding command, but not launch it in a browser,
add the --url-only flag:

sf env open --target-env test-org --path /apex/StartHere --url-only

- The preceding examples open the environment in your default web browser. To use
a different browser, set the --browser flag to its OS-specific name. For example,
to use Chrome on macOS:

sf env open --target-env test-org --path /apex/StartHere --browser "google chrome"

# error.NoDefaultEnv

No default target-env found. Use --target-env to specify which env to open.

# error.NoEnvFound

No environment found for %s.

# error.EnvironmentNotSupported

The environment %s doesn't support bring opened.

# error.ApplicationNotFound

Unable to find application named %s.
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
"bugs": "https://github.com/forcedotcom/cli/issues",
"dependencies": {
"@oclif/core": "^0.5.4",
"@salesforce/core": "3.0.1-v3.0",
"open": "^8.0.7",
"@salesforce/core": "3.1.0",
"open": "^8.2.0",
"tslib": "^2"
},
"devDependencies": {
"@oclif/dev-cli": "^1",
"@oclif/plugin-command-snapshot": "^2.0.0",
"@oclif/test": "^1.2.8",
"@salesforce/cli-plugins-testkit": "^0.0.25",
"@salesforce/cli-plugins-testkit": "^1.1.4",
"@salesforce/dev-config": "^2.1.0",
"@salesforce/dev-scripts": "^0.9.1",
"@salesforce/plugin-command-reference": "^1.3.0",
Expand Down Expand Up @@ -100,7 +100,7 @@
"test": "sf-test",
"test:command-reference": "./bin/dev commandreference:generate --erroronwarnings",
"test:deprecation-policy": "./bin/dev snapshot:compare",
"test:nuts": "nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel",
"test:nuts": "yarn build && nyc mocha \"**/*.nut.ts\" --slow 4500 --timeout 600000 --parallel",
"version": "oclif-dev readme"
},
"husky": {
Expand Down
10 changes: 5 additions & 5 deletions src/commands/env/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { EOL } from 'os';

import { Command, flags } from '@oclif/command';
import { cli, Table } from 'cli-ux';
import { AuthInfo, Authorization, SfdxError } from '@salesforce/core';
import { AuthInfo, SfOrg, SfdxError } from '@salesforce/core';

// TODO: add back once md messages are supported
// Messages.importMessagesDirectory(__dirname);
Expand All @@ -33,10 +33,10 @@ export default class EnvList extends Command {
all: boolean;
};

public async run(): Promise<Authorization[]> {
public async run(): Promise<SfOrg[]> {
this.flags = this.parse(EnvList).flags;

let authorizations: Authorization[];
let authorizations: SfOrg[];

try {
if (await AuthInfo.hasAuthentications()) {
Expand All @@ -56,12 +56,12 @@ export default class EnvList extends Command {
oauthMethod: {
header: 'OAuth Method',
},
} as Table.table.Columns<Partial<Authorization>>;
} as Table.table.Columns<Partial<SfOrg>>;
if (hasErrors) {
columns.error = {
// eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-member-access
get: (row) => row.error ?? '',
} as Table.table.Columns<Partial<Authorization>>;
} as Table.table.Columns<Partial<SfOrg>>;
}
cli.styledHeader('Authenticated Envs');
cli.table(authorizations, columns, this.flags);
Expand Down
155 changes: 155 additions & 0 deletions src/commands/env/open.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/*
* Copyright (c) 2021, 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 { EOL } from 'os';

import { Command, Flags } from '@oclif/core';
import { Logger, Messages, Org, SfdxError } from '@salesforce/core';
import * as open from 'open';
import type { Options } from 'open';
import { isArray } from '@salesforce/ts-types';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-env', 'open');

type Environment = { name: string; openUrl: string };

export default class EnvOpen extends Command {
// Use summary and description until a summary is supported in oclif
public static readonly description = messages.getMessage('description') + EOL + messages.getMessage('description');
public static readonly examples = messages.getMessages('examples');

public static flags = {
path: Flags.string({
char: 'p',
description: messages.getMessage('flags.path.summary'),
}),
'url-only': Flags.boolean({
char: 'r',
description: messages.getMessage('flags.url-only.summary'),
}),
'target-env': Flags.string({
char: 'e',
description: messages.getMessage('flags.target-env.summary'),
}),
browser: Flags.string({
description: messages.getMessage('flags.browser.summary'),
}),
};

public async run(): Promise<void> {
const { flags } = await this.parse(EnvOpen);
const nameOrAlias = flags['target-env'];
let url;

if (!nameOrAlias) {
// TODO this should be retrieved from sf config once we have those commands. If not found, still throw.
throw messages.createError('error.NoDefaultEnv');
}

try {
const org = await Org.create({ aliasOrUsername: nameOrAlias });
const conn = org.getConnection();
await org.refreshAuth();
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore in the next core version
url = conn.options.authInfo.getOrgFrontDoorUrl(); // eslint-disable-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-call
} catch (err) {
if (err instanceof Error && err.name !== 'NamedOrgNotFoundError' && err.name !== 'AuthInfoCreationError') {
throw err;
}
/* Expected - Do nothing */
}

if (!url) {
let foundEnvs: Environment[] = [];
const push = (envs: Environment | Environment[]): void => {
foundEnvs = [...foundEnvs, ...(isArray(envs) ? envs : [envs])];
};
await this.config.runHook('environments-request', { push });

const foundEnv = foundEnvs.find((env) => env.name === nameOrAlias);

if (!foundEnv) {
throw messages.createError('error.NoEnvFound', [nameOrAlias]);
}

url = foundEnv.openUrl;
}

if (url) {
if (flags['url-only']) {
this.log(url);
} else {
const browser = flags.browser;
const browserName = browser ? browser : 'the default browser';

await this.open(url, browser);
this.log(`Opening ${nameOrAlias} in ${browserName}.`);
}
} else {
throw messages.createError('error.EnvironmentNotSupported', [nameOrAlias]);
}
}

// TODO login and env open should probably share the same open code. Maybe we should use cli-ux.open?
private async open(url: string, browser: string): Promise<void> {
let options: Options;

if (browser) {
if (browser?.toLowerCase().includes('chrome')) {
browser = open.apps.chrome as string;
}

if (browser?.toLowerCase().includes('firefox')) {
browser = open.apps.firefox as string;
}

if (browser?.toLowerCase().includes('edge')) {
browser = open.apps.edge as string;
}
options = { app: { name: browser } };
}

const chunks = [];
const process = await open(url, options);

return new Promise((resolve, reject) => {
process.stderr.on('data', (chunk) => chunks.push(Buffer.from(chunk)));

const resolveOrReject = (code): void => {
// stderr could contain warnings or random data, so we will only error if we know there is a valid error.
const validErrors = [
'Unable to find application named',
'InvalidOperationException',
'cannot find file',
'cannot be run',
];
const errorMessage = Buffer.concat(chunks).toString('utf8');

if (code > 0 || validErrors.find((error) => errorMessage.includes(error))) {
Logger.childFromRoot('open').debug(errorMessage);
reject(messages.createError('error.ApplicationNotFound', [browser]));
} else {
resolve();
}
};

// This never seems to fire.
process.once('error', (err) => reject(new SfdxError(err.message, 'OpenError')));

// These are sometimes not fired (non-deterministic) for whatever reason, especially on windows. We will just rely on known errors in stderr.
// It could be because of See https://github.com/sindresorhus/open/issues/144 but hacking around the open library didn't
// seem to fix it.
// process.once('close', resolveOrReject);
// process.once('exit', resolveOrReject);

// Nothing is ever printed to stdout, but we really only care about stderr.
process.stderr.once('close', resolveOrReject);
});
}
}
33 changes: 33 additions & 0 deletions test/commands/env/open.nut.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/*
* 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 { execCmd, TestSession } from '@salesforce/cli-plugins-testkit';
import { ConfigAggregator } from '@salesforce/core';
import { expect } from 'chai';

describe('env open NUTs', () => {
let session: TestSession;
let usernameOrAlias: string;
before(async () => {
session = await TestSession.create({});

usernameOrAlias = ConfigAggregator.getValue('defaultdevhubusername').value as string;

if (!usernameOrAlias) throw Error('no default username set');
});

after(async () => {
await session?.clean();
});

it('should show url with decrypted token', () => {
const command = `env open --url-only --target-env ${usernameOrAlias}`;
const output = execCmd(command, { ensureExitCode: 0 }).shellOutput.stdout;
expect(output).to.contain('salesforce.com');
expect(output).to.match(/sid=00D[0-9a-zA-Z]+!/);
});
});
Loading