@salesforce/plugin-info
Version:
Plugin for accessing cli info from the command line
228 lines • 7.94 kB
JavaScript
/*
* Copyright (c) 2022, 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 fs from 'node:fs';
import { join, dirname, basename } from 'node:path';
import { Messages, SfError } from '@salesforce/core';
import { Env, omit } from '@salesforce/kit';
import { Diagnostics } from './diagnostics.js';
Messages.importMessagesDirectoryFromMetaUrl(import.meta.url);
const messages = Messages.loadMessages('@salesforce/plugin-info', 'doctor');
const PINNED_SUGGESTIONS = [
messages.getMessage('pinnedSuggestions.checkGitHubIssues'),
messages.getMessage('pinnedSuggestions.checkSfdcStatus'),
];
// private config from the CLI
// eslint-disable-next-line no-underscore-dangle
let __cliConfig;
export class Doctor {
// singleton instance
static instance;
id;
// Contains all gathered data and results of diagnostics.
diagnosis;
stdoutWriteStream;
stderrWriteStream;
constructor(config) {
this.id = Date.now();
__cliConfig = config;
const sfdxEnvVars = new Env().entries().filter((e) => e[0].startsWith('SFDX_'));
const sfEnvVars = new Env().entries().filter((e) => e[0].startsWith('SF_'));
const proxyEnvVars = new Env()
.entries()
.filter((e) => ['http_proxy', 'https_proxy', 'no_proxy'].includes(e[0].toLowerCase()));
const cliConfig = omit(config, [
'plugins',
'pjson',
'userPJSON',
'options',
'_commandIDs',
'rootPlugin',
]);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
cliConfig.nodeEngine = config.pjson.engines.node;
const { pluginVersions, ...versionDetails } = config.versionDetails;
this.diagnosis = {
versionDetail: { ...versionDetails, pluginVersions: formatPlugins(config, pluginVersions ?? {}) },
sfdxEnvVars,
sfEnvVars,
proxyEnvVars,
cliConfig,
pluginSpecificData: {},
diagnosticResults: [],
suggestions: [...PINNED_SUGGESTIONS],
logFilePaths: [],
commandExitCode: 0,
};
}
/**
* Returns a singleton instance of an SfDoctor.
*/
static getInstance() {
if (!Doctor.instance) {
throw new SfError(messages.getMessage('doctorNotInitializedError'), 'SfDoctorInitError');
}
return Doctor.instance;
}
/**
* Returns true if Doctor has been initialized.
*/
static isDoctorEnabled() {
return !!Doctor.instance;
}
/**
* Initializes a new instance of SfDoctor with CLI config data.
*
* @param config The oclif config for the CLI
* @param versionDetail The result of running a verbose version command
* @returns An instance of SfDoctor
*/
static init(config) {
if (Doctor.instance) {
return Doctor.instance;
}
Doctor.instance = new this(config);
return Doctor.instance;
}
/**
* Use the gathered data to discover potential problems by running all diagnostics.
*
* @returns An array of diagnostic promises.
*/
diagnose() {
return new Diagnostics(this, __cliConfig).run();
}
/**
* Add a suggestion in the form of:
* "Because of <this data point> we recommend to <suggestion>"
*
* @param suggestion A suggestion for the CLI user to try based on gathered data
*/
addSuggestion(suggestion) {
this.diagnosis.suggestions.push(suggestion);
}
/**
* Add a diagnostic test status.
*
* @param status a diagnostic test status
*/
addDiagnosticStatus(status) {
this.diagnosis.diagnosticResults.push(status);
}
/**
* Add diagnostic data that is specific to the passed plugin name.
*
* @param pluginName The name in the plugin's package.json
* @param data Any data to add to the doctor diagnosis that is specific
* to the plugin and a valid JSON value.
*/
addPluginData(pluginName, data) {
const pluginEntry = this.diagnosis.pluginSpecificData[pluginName];
if (pluginEntry) {
pluginEntry.push(data);
}
else {
this.diagnosis.pluginSpecificData[pluginName] = [data];
}
}
/**
* Add a command name that the doctor will run to the diagnosis data for
* use by diagnostics.
*
* @param commandName The name of the command that the doctor will run. E.g., "force:org:list"
*/
addCommandName(commandName) {
this.diagnosis.commandName = commandName;
}
/**
* Returns all the data gathered, paths to doctor files, and recommendations.
*/
getDiagnosis() {
return { ...this.diagnosis };
}
/**
* 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 filePath The path of the file to write.
* @param contents The string contents to write.
* @return The full path to the file.
*/
writeFileSync(filePath, contents) {
const fullPath = this.getDoctoredFilePath(filePath);
createOutputDir(fullPath);
this.diagnosis.logFilePaths.push(fullPath);
fs.writeFileSync(fullPath, contents);
return fullPath;
}
writeStdout(contents) {
if (!this.stdoutWriteStream) {
throw new SfError(messages.getMessage('doctorNotInitializedError'), 'SfDoctorInitError');
}
return writeFile(this.stdoutWriteStream, contents);
}
writeStderr(contents) {
if (!this.stderrWriteStream) {
throw new SfError(messages.getMessage('doctorNotInitializedError'), 'SfDoctorInitError');
}
return writeFile(this.stderrWriteStream, contents);
}
createStdoutWriteStream(fullPath) {
if (!this.stdoutWriteStream) {
createOutputDir(fullPath);
this.stdoutWriteStream = fs.createWriteStream(fullPath);
}
}
createStderrWriteStream(fullPath) {
if (!this.stderrWriteStream) {
createOutputDir(fullPath);
this.stderrWriteStream = fs.createWriteStream(join(fullPath));
}
}
closeStderr() {
this.stderrWriteStream?.end();
this.stderrWriteStream?.close();
}
closeStdout() {
this.stdoutWriteStream?.end();
this.stdoutWriteStream?.close();
}
getDoctoredFilePath(filePath) {
const dir = dirname(filePath);
const fileName = `${this.id}-${basename(filePath)}`;
const fullPath = join(dir, fileName);
this.diagnosis.logFilePaths.push(fullPath);
return fullPath;
}
setExitCode(code) {
this.diagnosis.commandExitCode = code;
}
}
export function formatPlugins(config, plugins) {
function getFriendlyName(name) {
const scope = config?.pjson?.oclif?.scope;
if (!scope)
return name;
const match = name.match(`@${scope}/plugin-(.+)`);
if (!match)
return name;
return match[1];
}
return Object.entries(plugins)
.map(([name, plugin]) => ({ name, ...plugin }))
.sort((a, b) => (a.name > b.name ? 1 : -1))
.map((plugin) => `${getFriendlyName(plugin.name)} ${plugin.version} (${plugin.type}) ${plugin.type === 'link' ? plugin.root : ''}`.trim());
}
const createOutputDir = (fullPath) => {
const dir = dirname(fullPath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
};
const writeFile = (stream, contents) => Promise.resolve(stream.write(contents));
//# sourceMappingURL=doctor.js.map