UNPKG

@salesforce/plugin-info

Version:

Plugin for accessing cli info from the command line

228 lines 7.94 kB
/* * 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