Skip to content

Commit

Permalink
feat: add initial doctor command
Browse files Browse the repository at this point in the history
  • Loading branch information
shetzel committed Sep 7, 2022
1 parent 8bd75b1 commit d482d03
Show file tree
Hide file tree
Showing 8 changed files with 1,654 additions and 2,500 deletions.
16 changes: 16 additions & 0 deletions messages/diagnostics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# updateCliVersion

Update your CLI version from: %s to the latest version: %s

# latestCliVersionError

Could not determine latest CLI version due to:
%s

# salesforceDxPluginDetected

The salesforcedx plugin is deprecated. Please uninstall by running: `%s plugins:uninstall salesforcedx`

# linkedPluginWarning

Warning: the [%s] plugin is linked.
3 changes: 0 additions & 3 deletions messages/doctor.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,10 @@ Plugin providers can also implement their own doctor diagnostic tests by listeni
examples: [
`Run CLI doctor diagnostics:
$ <%= config.bin %> doctor
Run CLI doctor diagnostics and the specified command, writing debug output to a file:
$ <%= config.bin %> doctor --command "force:org:list --all"
Run CLI doctor diagnostics for a specific plugin:
$ <%= config.bin %> doctor --plugin @salesforce/plugin-source
Run CLI doctor diagnostics and create a new CLI GitHub issue, attaching all doctor diagnostics:
$ <%= config.bin %> doctor --newissue
`,
Expand Down
15 changes: 8 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
"bugs": "https://github.com/forcedotcom/cli/issues",
"main": "lib/index.js",
"dependencies": {
"@oclif/core": "^1.6.3",
"@salesforce/command": "^5.0.4",
"@salesforce/core": "^3.10.1",
"@salesforce/kit": "^1.5.34",
"@oclif/core": "^1.16.0",
"@salesforce/command": "^5.2.6",
"@salesforce/core": "^3.26.3",
"@salesforce/kit": "^1.6.0",
"got": "^11.8.2",
"marked": "^4.0.1",
"marked-terminal": "^4.2.0",
Expand All @@ -21,9 +21,10 @@
"devDependencies": {
"@oclif/dev-cli": "^1",
"@oclif/plugin-command-snapshot": "^3",
"@salesforce/cli-plugins-testkit": "^1.4.17",
"@salesforce/dev-config": "^3.0.0",
"@salesforce/dev-scripts": "^2.0.0",
"@oclif/plugin-version": "1.1.2",
"@salesforce/cli-plugins-testkit": "^2.3.13",
"@salesforce/dev-config": "^3.1.0",
"@salesforce/dev-scripts": "^3.1.0",
"@salesforce/plugin-command-reference": "^1.3.17",
"@salesforce/prettier-config": "^0.0.2",
"@salesforce/ts-sinon": "1.3.21",
Expand Down
143 changes: 90 additions & 53 deletions src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ import * as os from 'os';
import { exec } from 'child_process';
import { flags, SfdxCommand } from '@salesforce/command';
import { Messages, Lifecycle } from '@salesforce/core';
import { Doctor as SFDoctor, SfDoctorDiagnosis } from '../doctor';
import { Doctor as SFDoctor, SfDoctor, SfDoctorDiagnosis } from '../doctor';
import { DiagnosticStatus } from '../diagnostics';

Messages.importMessagesDirectory(__dirname);
const messages = Messages.loadMessages('@salesforce/plugin-info', 'doctor');
Expand Down Expand Up @@ -37,39 +38,35 @@ export default class Doctor extends SfdxCommand {
}),
};

// Array of promises that are various doctor tasks to perform
// such as running a command and running diagnostics.
private tasks: Array<Promise<void>> = [];

private doctor: SfDoctor;

private filesWrittenMsgs: string[] = [];

public async run(): Promise<SfDoctorDiagnosis> {
let promises: Array<Promise<void>> = [];
const doctor = SFDoctor.getInstance();
SFDoctor.init(this.config, {
cliVersion: 'sfdx-cli/7.165.1',
pluginVersions: ['foo', 'bar (link)', 'salesforcedx'],
nodeVersion: 'node-v16.17.0',
architecture: 'darwin-x64'
});
this.doctor = SFDoctor.getInstance();
const lifecycle = Lifecycle.getInstance();

const plugin = this.flags.plugin as string;
const command = this.flags.command as string;
const newissue = this.flags.newissue as boolean;

// eslint-disable-next-line @typescript-eslint/require-await
lifecycle.on<DiagnosticStatus>('Doctor:diagnostic', async (data) => {
this.ux.log(`${data.status} - ${data.testName}`);
});

if (command) {
const cmdString = this.parseCommand(command);
this.ux.log(`Running Command: ${cmdString}\n`);
const cmdName = cmdString.split(' ')[1];
doctor.addCommandName(cmdName);

const execPromise = new Promise<void>((resolve) => {
const execOptions = {
env: Object.assign({}, process.env),
};

exec(cmdString, execOptions, (error, stdout, stderr) => {
const code = error?.code || 0;
const stdoutWithCode = `Command exit code: ${code}\n\n${stdout}`;
const stdoutFileName = `${cmdName}-stdout.log`;
const stderrFileName = `${cmdName}-stderr.log`;
const stdoutLogLocation = doctor.writeFileSync(stdoutFileName, stdoutWithCode);
const debugLogLocation = doctor.writeFileSync(stderrFileName, stderr);
this.ux.log(`Wrote command stdout log to: ${stdoutLogLocation}`);
this.ux.log(`Wrote command debug log to: ${debugLogLocation}`);
resolve();
});
});
promises.push(execPromise);
this.setupCommandExecution(command);
}

if (plugin) {
Expand All @@ -80,44 +77,34 @@ export default class Doctor extends SfdxCommand {
}

// run the diagnostics for a specific plugin
this.ux.log(`Running diagnostics for plugin: ${plugin}`);
promises.push(lifecycle.emit(`sf-doctor-${plugin}`, doctor));
this.ux.styledHeader(`Running diagnostics for plugin: ${plugin}`);
this.tasks.push(lifecycle.emit(`sf-doctor-${plugin}`, this.doctor));
} else {
this.ux.log('Running diagnostics for all plugins and the core CLI');
this.ux.styledHeader('Running all diagnostics');
// run all diagnostics
promises.push(lifecycle.emit('sf-doctor', doctor));
this.tasks.push(lifecycle.emit('sf-doctor', this.doctor));

/* eslint-disable @typescript-eslint/ban-ts-comment,@typescript-eslint/no-unsafe-assignment */
// @ts-ignore Seems like a TypeScript bug. Compiler thinks doctor.diagnose() returns `void`.
promises = [...promises, ...doctor.diagnose()];
this.tasks = [...this.tasks, ...this.doctor.diagnose()];
/* eslint-enable @typescript-eslint/ban-ts-comment,@typescript-eslint/no-unsafe-assignment */
}

await Promise.all(promises);
await Promise.all(this.tasks);

const diagnosis = this.doctor.getDiagnosis();
const diagnosisLocation = this.doctor.writeFileSync('diagnosis.json', JSON.stringify(diagnosis, null, 2));
this.filesWrittenMsgs.push(`Wrote doctor diagnosis to: ${diagnosisLocation}`);

const diagnosis = doctor.getDiagnosis();
const diagnosisLocation = doctor.writeFileSync('diagnosis.json', JSON.stringify(diagnosis, null, 2));
this.ux.log(`Wrote doctor diagnosis to: ${diagnosisLocation}`);
this.ux.log();
this.filesWrittenMsgs.forEach((msg) => this.ux.log(msg));

this.ux.log();
this.ux.styledHeader('Suggestions');
diagnosis.suggestions.forEach(s => this.ux.log(` * ${s}`));

if (newissue) {
// create a new issue via prompts (Inquirer)

// See https://docs.github.com/en/enterprise-server@3.1/issues/tracking-your-work-with-issues/creating-an-issue#creating-an-issue-from-a-url-query
// Example: https://github.com/forcedotcom/cli/issues/new?title=PLEASE+UPDATE&body=Autofill+info+collected+by+doctor...&labels=doctor

this.ux.warn('New GitHub issue creation is not yet implemented. Coming soon!');
// this.ux.log('\nCreating a new GitHub issue for the CLI...\n');
// const isItNew = await this.ux.prompt(
// 'Have you first checked the list of GitHub issues to ensure it has not already been posted? (y/n)'
// );

// if (isItNew.toLowerCase() === 'y') {
// const title = await this.ux.prompt('What is the subject of the issue?');
// this.ux.log('Encoded title=', encodeURI(title));
// this.ux.log("I'll create an issue for you with that title and attach the doctor diagnostics.");
// } else {
// this.ux.log('Please check https://github.com/forcedotcom/cli/issues first');
// }
this.createNewIssue();
}

return diagnosis;
Expand All @@ -139,4 +126,54 @@ export default class Doctor extends SfdxCommand {

return fullCmd;
}

// Adds a promise to execute the provided command and all
// parameters in debug mode, writing stdout and stderr to files
// in the doctor directory.
private setupCommandExecution(command: string): void {
const cmdString = this.parseCommand(command);
this.ux.log(`Running Command: "${cmdString}"\n`);
const cmdName = cmdString.split(' ')[1];
this.doctor.addCommandName(cmdName);

const execPromise = new Promise<void>((resolve) => {
const execOptions = {
env: Object.assign({}, process.env),
};

exec(cmdString, execOptions, (error, stdout, stderr) => {
const code = error?.code || 0;
const stdoutWithCode = `Command exit code: ${code}\n\n${stdout}`;
const stdoutFileName = `${cmdName}-stdout.log`;
const stderrFileName = `${cmdName}-stderr.log`;
const stdoutLogLocation = this.doctor.writeFileSync(stdoutFileName, stdoutWithCode);
const debugLogLocation = this.doctor.writeFileSync(stderrFileName, stderr);
this.filesWrittenMsgs.push(`Wrote command stdout log to: ${stdoutLogLocation}`);
this.filesWrittenMsgs.push(`Wrote command debug log to: ${debugLogLocation}`);
resolve();
});
});
this.tasks.push(execPromise);
}

private createNewIssue(): void {
// create a new issue via prompts (Inquirer)

// See https://docs.github.com/en/enterprise-server@3.1/issues/tracking-your-work-with-issues/creating-an-issue#creating-an-issue-from-a-url-query
// Example: https://github.com/forcedotcom/cli/issues/new?title=PLEASE+UPDATE&body=Autofill+info+collected+by+doctor...&labels=doctor

this.ux.warn('New GitHub issue creation is not yet implemented. Coming soon!');
// this.ux.log('\nCreating a new GitHub issue for the CLI...\n');
// const isItNew = await this.ux.prompt(
// 'Have you first checked the list of GitHub issues to ensure it has not already been posted? (y/n)'
// );

// if (isItNew.toLowerCase() === 'y') {
// const title = await this.ux.prompt('What is the subject of the issue?');
// this.ux.log('Encoded title=', encodeURI(title));
// this.ux.log("I'll create an issue for you with that title and attach the doctor diagnostics.");
// } else {
// this.ux.log('Please check https://github.com/forcedotcom/cli/issues first');
// }
}
}
61 changes: 45 additions & 16 deletions src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,25 @@
*/

import { exec } from 'child_process';
import { IConfig } from '@oclif/config';
import { Config } from '@oclif/core';
import { Lifecycle, Messages } from '@salesforce/core';
import { SfDoctor, SfDoctorDiagnosis } from './doctor';

// @fixme: remove this when we can get better typing of VersionDetail from sfdx-cli
/* eslint-disable */

// const SUPPORTED_SHELLS = [
// 'bash',
// 'zsh',
// 'powershell'
// 'cmd.exe'
// ];

export interface DiagnosticStatus {
testName: string;
status: 'pass' | 'fail' | 'warn' | 'unknown';
}

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

/**
* Diagnostics are all the tests that ensure a known, clean CLI configuration
* and a way to run them asynchronously. Typically this is used only by the
Expand All @@ -27,7 +33,7 @@ import { SfDoctor, SfDoctorDiagnosis } from './doctor';
export class Diagnostics {
private diagnosis: SfDoctorDiagnosis;

public constructor(private readonly doctor: SfDoctor, private config: IConfig) {
public constructor(private readonly doctor: SfDoctor, private config: Config) {
this.diagnosis = doctor.getDiagnosis();
}

Expand All @@ -48,44 +54,67 @@ export class Diagnostics {
//
// **********************************************************

/**
* Checks to see if the running version of the CLI is the latest.
*/
public async outdatedCliVersionCheck(): Promise<void> {
const cliVersionArray = this.diagnosis.versionDetail.cliVersion.split('/');
const cliName = cliVersionArray[0];
const cliVersion = cliVersionArray[1];

return new Promise<void>((resolve) => {
exec(`npm view ${cliName} --json`, {}, (error, stdout, stderr) => {
const code = error?.code || 0;
const testName = 'using latest CLI version';
let status: DiagnosticStatus['status'] = 'unknown';

exec(`npm view ${cliName} --json`, {}, async (error, stdout, stderr) => {
const code = error?.code ?? 0;
if (code === 0) {
const latestVersion = JSON.parse(stdout)['dist-tags'].latest;
const latestVersion = JSON.parse(stdout)['dist-tags'].latest as string;
if (cliVersion < latestVersion) {
this.doctor.addSuggestion(
`Update your CLI version from ${cliVersion} to the latest version: ${latestVersion}`
);
status = 'fail';
this.doctor.addSuggestion(messages.getMessage('updateCliVersion', [cliVersion, latestVersion]));
} else {
status = 'pass';
}
} else {
this.doctor.addSuggestion('Could not determine latest CLI version');
this.doctor.addSuggestion(messages.getMessage('latestCliVersionError', [stderr]));
}
await Lifecycle.getInstance().emit('Doctor:diagnostic', { testName, status });
resolve();
});
});
}

/**
* Checks if the salesforcedx plugin is installed and suggests
* to uninstall it if there.
*/
public async salesforceDxPluginCheck(): Promise<void> {
const testName = 'salesforcedx plugin not installed';
let status: DiagnosticStatus['status'] = 'pass';

const plugins = this.diagnosis.versionDetail.pluginVersions;
if (plugins?.some((p) => p.split(' ')[0] === 'salesforcedx')) {
status = 'fail';
const bin = this.diagnosis.cliConfig.bin;
this.doctor.addSuggestion(
`The salesforcedx plugin is deprecated. Please uninstall by running \`${bin} plugins:uninstall salesforcedx\``
);
this.doctor.addSuggestion(messages.getMessage('salesforceDxPluginDetected', [bin]));
}
await Lifecycle.getInstance().emit('Doctor:diagnostic', { testName, status });
}

/**
* Checks and warns if any plugins are linked.
*/
public async linkedPluginCheck(): Promise<void> {
const testName = 'no linked plugins';
let status: DiagnosticStatus['status'] = 'pass';

const plugins = this.config.plugins;
const linkedPlugins = plugins.filter((p) => p.name.includes('(link)'));
linkedPlugins.forEach((lp) => {
this.doctor.addSuggestion(`Warning: the [${lp.name}] plugin is linked.`);
status = 'fail';
this.doctor.addSuggestion(messages.getMessage('linkedPluginWarning', [lp.name]));
});
await Lifecycle.getInstance().emit('Doctor:diagnostic', { testName, status });
}
}
Loading

0 comments on commit d482d03

Please sign in to comment.