Skip to content

Commit

Permalink
fix: doctor command updates
Browse files Browse the repository at this point in the history
  • Loading branch information
shetzel committed Sep 8, 2022
1 parent d482d03 commit 5c5c444
Show file tree
Hide file tree
Showing 12 changed files with 473 additions and 648 deletions.
2 changes: 1 addition & 1 deletion command-snapshot.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,6 @@
{
"command": "doctor",
"plugin": "@salesforce/plugin-info",
"flags": ["command", "newissue", "plugin", "json", "loglevel"]
"flags": ["command", "newissue", "plugin", "outputdir", "json", "loglevel"]
}
]
15 changes: 12 additions & 3 deletions messages/doctor.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,10 @@ The doctor can create a new GitHub issue for the Salesforce CLI, attaching all d
Plugin providers can also implement their own doctor diagnostic tests by listening to the "sf-doctor" event and running plugin specific tests that will be included in the doctor diagnostics log.
`,
flags: {
command: 'Run the specified command in debug mode and write results to a file.',
newissue: 'Create a new GitHub issue for the CLI, attaching doctor diagnostic results.',
plugin: 'Run doctor command diagnostics for a specific plugin.',
command: 'run the specified command in debug mode and write results to a file.',
newissue: 'create a new GitHub issue for the CLI, attaching doctor diagnostic results.',
plugin: 'run doctor command diagnostics for a specific plugin.',
outputdir: 'directory to save all created files rather than the current working directory',
},
examples: [
`Run CLI doctor diagnostics:
Expand All @@ -23,4 +24,12 @@ Run CLI doctor diagnostics and create a new CLI GitHub issue, attaching all doct
$ <%= config.bin %> doctor --newissue
`,
],
pinnedSuggestions: {
checkGitHubIssues: 'check https://github.com/forcedotcom/cli/issues for community posted CLI issues',
checkSfdcStatus: 'check http://status.salesforce.com for any Salesforce announced problems',
},
doctorNotInitializedError: 'Must first initialize a new SfDoctor',
doctorAlreadyInitializedError: 'SfDoctor has already been initialized',
pluginNotInstalledError:
'Specified plugin [%s] is not installed. Please install it, correct the name, or choose another plugin.',
};
27 changes: 10 additions & 17 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,24 +33,21 @@
"@types/marked-terminal": "^3.1.3",
"@types/proxy-from-env": "^1.0.1",
"@types/semver": "^7.3.8",
"@typescript-eslint/eslint-plugin": "^4.33.0",
"@typescript-eslint/parser": "^4.33.0",
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"chai": "^4.3.4",
"cz-conventional-changelog": "^3.3.0",
"eslint": "^7.32.0",
"eslint-config-prettier": "^8.3.0",
"eslint-config-salesforce": "^0.1.6",
"eslint": "^8.21.0",
"eslint-config-prettier": "^8.5.0",
"eslint-config-salesforce": "^1.1.0",
"eslint-config-salesforce-license": "^0.1.6",
"eslint-config-salesforce-typescript": "^0.2.8",
"eslint-config-salesforce-typescript": "^1.1.1",
"eslint-plugin-header": "^3.1.1",
"eslint-plugin-import": "^2.25.3",
"eslint-plugin-jsdoc": "^35.5.1",
"eslint-plugin-prettier": "^3.4.1",
"eslint-plugin-import": "^2.26.0",
"eslint-plugin-jsdoc": "^39.3.6",
"husky": "^7.0.4",
"lint-staged": "^11.2.6",
"mocha": "^9.1.3",
"nyc": "^15.1.0",
"prettier": "^2.4.1",
"prettier": "^2.7.1",
"pretty-quick": "^3.1.0",
"sfdx-cli": "^7.160.0",
"shx": "0.3.4",
Expand All @@ -59,11 +56,7 @@
"ts-node": "^10.4.0",
"typescript": "^4.5.2"
},
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
"config": {},
"engines": {
"node": ">=14.0.0"
},
Expand Down
64 changes: 33 additions & 31 deletions src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,10 @@
*/

import * as os from 'os';
import * as path from 'path';
import { exec } from 'child_process';
import { flags, SfdxCommand } from '@salesforce/command';
import { Messages, Lifecycle } from '@salesforce/core';
import { Messages, Lifecycle, SfError } from '@salesforce/core';
import { Doctor as SFDoctor, SfDoctor, SfDoctorDiagnosis } from '../doctor';
import { DiagnosticStatus } from '../diagnostics';

Expand All @@ -17,7 +18,6 @@ const messages = Messages.loadMessages('@salesforce/plugin-info', 'doctor');

export default class Doctor extends SfdxCommand {
public static description = messages.getMessage('commandDescription');

public static examples = messages.getMessage('examples').split(os.EOL);

// Hide for now
Expand All @@ -36,49 +36,47 @@ export default class Doctor extends SfdxCommand {
char: 'p',
description: messages.getMessage('flags.plugin'),
}),
outputdir: flags.directory({
char: 'o',
description: messages.getMessage('flags.outputdir'),
}),
};

// 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 outputDir: string;
private filesWrittenMsgs: string[] = [];

public async run(): Promise<SfDoctorDiagnosis> {
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;
const pluginFlag = this.flags.plugin as string;
const commandFlag = this.flags.command as string;
const newissueFlag = this.flags.newissue as boolean;
const outputdirFlag = this.flags.outputdir as string;
this.outputDir = path.resolve(outputdirFlag ?? process.cwd());

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

if (command) {
this.setupCommandExecution(command);
if (commandFlag) {
this.setupCommandExecution(commandFlag);
}

if (plugin) {
if (pluginFlag) {
// verify the plugin flag matches an installed plugin
if (!this.config.plugins.some((p) => p.name === plugin)) {
const errMsg = `Specified plugin [${plugin}] is not installed. Please install it or choose another plugin.`;
throw Error(errMsg);
if (!this.config.plugins.some((p) => p.name === pluginFlag)) {
throw new SfError(messages.getMessage('pluginNotInstalledError', [pluginFlag]));
}

// run the diagnostics for a specific plugin
this.ux.styledHeader(`Running diagnostics for plugin: ${plugin}`);
this.tasks.push(lifecycle.emit(`sf-doctor-${plugin}`, this.doctor));
this.ux.styledHeader(`Running diagnostics for plugin: ${pluginFlag}`);
this.tasks.push(lifecycle.emit(`sf-doctor-${pluginFlag}`, this.doctor));
} else {
this.ux.styledHeader('Running all diagnostics');
// run all diagnostics
Expand All @@ -93,17 +91,20 @@ export default class Doctor extends SfdxCommand {
await Promise.all(this.tasks);

const diagnosis = this.doctor.getDiagnosis();
const diagnosisLocation = this.doctor.writeFileSync('diagnosis.json', JSON.stringify(diagnosis, null, 2));
const diagnosisLocation = this.doctor.writeFileSync(
path.join(this.outputDir, 'diagnosis.json'),
JSON.stringify(diagnosis, null, 2)
);
this.filesWrittenMsgs.push(`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}`));
diagnosis.suggestions.forEach((s) => this.ux.log(` * ${s}`));

if (newissue) {
if (newissueFlag) {
this.createNewIssue();
}

Expand Down Expand Up @@ -132,9 +133,9 @@ export default class Doctor extends SfdxCommand {
// 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);
this.ux.styledHeader('Running command with debugging');
this.ux.log(`${cmdString}\n`);
this.doctor.addCommandName(cmdString);

const execPromise = new Promise<void>((resolve) => {
const execOptions = {
Expand All @@ -144,10 +145,11 @@ export default class Doctor extends SfdxCommand {
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);
const stdoutLogLocation = this.doctor.writeFileSync(
path.join(this.outputDir, 'command-stdout.log'),
stdoutWithCode
);
const debugLogLocation = this.doctor.writeFileSync(path.join(this.outputDir, 'command-debug.log'), stderr);
this.filesWrittenMsgs.push(`Wrote command stdout log to: ${stdoutLogLocation}`);
this.filesWrittenMsgs.push(`Wrote command debug log to: ${debugLogLocation}`);
resolve();
Expand Down
21 changes: 13 additions & 8 deletions src/diagnostics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ 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
* Doctor class.
*
* Create a new diagnostic test by adding a method to the `Diagnostics` class,
* appending "Check" to the name. Emit a "Doctor:diagnostic" event with a
* `DiagnosticStatus` payload so the CLI can report on the diagnostic result.
*/
export class Diagnostics {
private diagnosis: SfDoctorDiagnosis;
Expand All @@ -49,7 +53,7 @@ export class Diagnostics {
// **********************************************************
// D I A G N O S T I C S
//
// NOTE: All diagnostic function names must end with "Check"
// NOTE: Diagnostic function names must end with "Check"
// or they will not be run with all diagnostics.
//
// **********************************************************
Expand All @@ -63,24 +67,25 @@ export class Diagnostics {
const cliVersion = cliVersionArray[1];

return new Promise<void>((resolve) => {
const testName = 'using latest CLI version';
const testName = 'using latest or latest-rc CLI version';
let status: DiagnosticStatus['status'] = 'unknown';

exec(`npm view ${cliName} --json`, {}, async (error, stdout, stderr) => {
exec(`npm view ${cliName} dist-tags.latest`, {}, (error, stdout, stderr) => {
const code = error?.code ?? 0;
if (code === 0) {
const latestVersion = JSON.parse(stdout)['dist-tags'].latest as string;
if (cliVersion < latestVersion) {
const latest = stdout.trim();
if (cliVersion < latest) {
status = 'fail';
this.doctor.addSuggestion(messages.getMessage('updateCliVersion', [cliVersion, latestVersion]));
this.doctor.addSuggestion(messages.getMessage('updateCliVersion', [cliVersion, latest]));
} else {
status = 'pass';
}
} else {
this.doctor.addSuggestion(messages.getMessage('latestCliVersionError', [stderr]));
}
await Lifecycle.getInstance().emit('Doctor:diagnostic', { testName, status });
resolve();
void Lifecycle.getInstance()
.emit('Doctor:diagnostic', { testName, status })
.then(() => resolve());
});
});
}
Expand Down
36 changes: 19 additions & 17 deletions src/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@

import * as fs from 'fs';
import * as path from 'path';
import { Messages, SfError } from '@salesforce/core';
import { Env, omit } from '@salesforce/kit';
import { AnyJson, KeyValue } from '@salesforce/ts-types';
import { Global } from '@salesforce/core';
import { Config } from '@oclif/core';
import { VersionDetail } from '@oclif/plugin-version';
import { Diagnostics } from './diagnostics';
Expand All @@ -35,16 +35,18 @@ export interface SfDoctorDiagnosis {
suggestions: string[];
}

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

const PINNED_SUGGESTIONS = [
'check https://github.com/forcedotcom/cli/issues for community posted CLI issues',
'check http://status.salesforce.com for any Salesforce announced problems',
messages.getMessage('pinnedSuggestions.checkGitHubIssues'),
messages.getMessage('pinnedSuggestions.checkSfdcStatus'),
];

export class Doctor implements SfDoctor {
// singleton instance
private static instance: SfDoctor;

public readonly dir: string;
public readonly id: number;

// Contains all gathered data and results of diagnostics.
Expand All @@ -66,19 +68,14 @@ export class Doctor implements SfDoctor {
pluginSpecificData: {},
suggestions: [...PINNED_SUGGESTIONS],
};
const globalDir = config.bin === 'sfdx' ? Global.SFDX_DIR : Global.SF_DIR;
this.dir = path.join(globalDir, 'sf-doctor');
if (!fs.existsSync(this.dir)) {
fs.mkdirSync(this.dir, { recursive: true });
}
}

/**
* Returns a singleton instance of an SfDoctor.
*/
public static getInstance(): SfDoctor {
if (!Doctor.instance) {
throw Error('Must first initialize a new SfDoctor');
throw new SfError(messages.getMessage('doctorNotInitializedError'), 'SfDoctorInitError');
}
return Doctor.instance;
}
Expand All @@ -92,7 +89,7 @@ export class Doctor implements SfDoctor {
*/
public static init(config: Config, versionDetail: VersionDetail): SfDoctor {
if (Doctor.instance) {
throw Error('SfDoctor has already been initialized');
throw new SfError(messages.getMessage('doctorAlreadyInitializedError'), 'SfDoctorInitError');
}

Doctor.instance = new this(config, versionDetail);
Expand Down Expand Up @@ -156,18 +153,23 @@ export class Doctor implements SfDoctor {
}

/**
* Write a file to the doctor directory. The file name will be prepended
* Write a file with the provided path. The file name will be prepended
* with this doctor's id.
*
* E.g., `name = myContent.json` will write `1658350735579-myContent.json`
*
* @param name The name of the file to write within the SfDocter directory.
* @param path The path of the file to write.
* @param contents The string contents to write.
* @return The full path to the file.
*/
public writeFileSync(name: string, content: string): string {
const filePath = path.join(this.dir, `${this.id}-${name}`);
fs.writeFileSync(filePath, content);
return filePath;
public writeFileSync(filePath: string, content: string): string {
const dir = path.dirname(filePath);
const fileName = `${this.id}-${path.basename(filePath)}`;
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
const fullPath = path.join(dir, fileName);
fs.writeFileSync(fullPath, content);
return fullPath;
}
}
1 change: 1 addition & 0 deletions test/commands/info/releasenotes/display.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import * as getDistTagVersion from '../../../../src/shared/getDistTagVersion';
import * as parseReleaseNotes from '../../../../src/shared/parseReleaseNotes';
import Display from '../../../../src/commands/info/releasenotes/display';

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
chaiUse(SinonChai);

describe('info:releasenotes:display', () => {
Expand Down
3 changes: 2 additions & 1 deletion test/shared/getDistTagVersion.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,15 @@ import * as SinonChai from 'sinon-chai';
import { stubMethod } from '@salesforce/ts-sinon';
import { getDistTagVersion, DistTagJson } from '../../src/shared/getDistTagVersion';

// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
chaiUse(SinonChai);

describe('getDistTagVersion tests', () => {
const sandbox = Sinon.createSandbox();

let gotStub: sinon.SinonStub;

let url;
let url: string;
let gotResponse: DistTagJson;

beforeEach(() => {
Expand Down
Loading

0 comments on commit 5c5c444

Please sign in to comment.