Repository: typescript-eslint/tslint-to-eslint-config (ts)

Issue:
Recommend npm or yarn installation commands for packages
When an ESLint configuration file is generated, at least the following packages will likely need to be available to use it:
* `eslint` itself
* `@typescript-eslint/*` plugins
* Any other plugins extended from by the package, such as `eslint-plugin-prettier`

When this package is run successfully, there should be a _(bright cyan?)_ recommendation for a shell command to install the requisite packages. It should use `npm` by default, or `yarn` if there is a `yarn.lock`.

Pull request description:
Used package install command for list of required ESLint-related packages
## PR Checklist

-   [x] Addresses an existing issue: fixes #11
-   [x] That issue was marked as [`status: accepting prs`](https://github.com/typescript-eslint/tslint-to-eslint-config/labels/status%3A%20accepting%20prs)

## Overview

Instead of displaying a line-by-line list of missing plugins, this creates a copy&paste-able command for the detected package manager (npm, pnpm, or Yarn) by lock file, or npm by default.

```
...

❓ 1 rule does not yet have an ESLint equivalent ❓
  See generated log file; defaulting to eslint-plugin-tslint for it.

⚡ 6 packages are required for running with ESLint. ⚡
  npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint eslint-plugin-import eslint-plugin-jsdoc eslint-plugin-prefer-arrow --save-dev

✅ All is well! ✅
```

Switches all converters to report plugins in the `eslint-plugin-` syntax, since they need to be installable by the package manager. I have a vague recollection of commenting on this / wanting it to be standardized recently but can't remember where. 🤷 

Patch:
diff --git a/.eslintrc.js b/.eslintrc.js
index 65e6900..c98de62 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -18,6 +18,7 @@ module.exports = {
     },
     plugins: ["@typescript-eslint"],
     rules: {
+        "@typescript-eslint/require-array-sort-compare": "off",
         "@typescript-eslint/consistent-type-definitions": ["error", "type"],
         "@typescript-eslint/explicit-function-return-type": "off",
         "@typescript-eslint/explicit-module-boundary-types": "off",
@@ -36,6 +37,7 @@ module.exports = {
         "@typescript-eslint/no-unsafe-member-access": "off",
         "@typescript-eslint/no-unsafe-return": "off",
         "@typescript-eslint/no-untyped-public-signature": "off",
+        "@typescript-eslint/no-unused-vars": "off",
         "@typescript-eslint/no-use-before-define": "off",
         "@typescript-eslint/prefer-readonly-parameter-types": "off",
         "@typescript-eslint/prefer-reduce-type-parameter": "off",
diff --git a/src/cli/main.ts b/src/cli/main.ts
index d0747d8..37d8ef9 100644
--- a/src/cli/main.ts
+++ b/src/cli/main.ts
@@ -43,8 +43,11 @@ import { findTSLintConfiguration } from "../input/findTSLintConfiguration";
 import { findTypeScriptConfiguration } from "../input/findTypeScriptConfiguration";
 import { importer, ImporterDependencies } from "../input/importer";
 import { mergeLintConfigurations } from "../input/mergeLintConfigurations";
-import { ReportConversionResultsDependencies } from "../reporting/dependencies";
-import { reportConversionResults } from "../reporting/reportConversionResults";
+import { choosePackageManager } from "../reporting/packages/choosePackageManager";
+import {
+    reportConversionResults,
+    ReportConversionResultsDependencies,
+} from "../reporting/reportConversionResults";
 import { reportEditorSettingConversionResults } from "../reporting/reportEditorSettingConversionResults";
 import { convertRules, ConvertRulesDependencies } from "../rules/convertRules";
 import { mergers } from "../rules/mergers";
@@ -86,7 +89,12 @@ const findOriginalConfigurationsDependencies: FindOriginalConfigurationsDependen
     mergeLintConfigurations,
 };
 
+const choosePackageManagerDependencies = {
+    fileSystem: fsFileSystem,
+};
+
 const reportConversionResultsDependencies: ReportConversionResultsDependencies = {
+    choosePackageManager: bind(choosePackageManager, choosePackageManagerDependencies),
     logger: processLogger,
 };
 
diff --git a/src/conversion/convertConfig.ts b/src/conversion/convertConfig.ts
index c016a0b..2305bad 100644
--- a/src/conversion/convertConfig.ts
+++ b/src/conversion/convertConfig.ts
@@ -57,7 +57,7 @@ export const convertConfig = async (
     }
 
     // 5. A summary of the results is printed to the user's console
-    dependencies.reportConversionResults(simplifiedConfiguration);
+    await dependencies.reportConversionResults(simplifiedConfiguration);
 
     return {
         status: ResultStatus.Succeeded,
diff --git a/src/reporting/dependencies.ts b/src/reporting/dependencies.ts
deleted file mode 100644
index 77b3b6f..0000000
--- a/src/reporting/dependencies.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import { Logger } from "../adapters/logger";
-
-export type ReportConversionResultsDependencies = {
-    logger: Logger;
-};
diff --git a/src/reporting/packages/choosePackageManager.ts b/src/reporting/packages/choosePackageManager.ts
new file mode 100644
index 0000000..92aaa9e
--- /dev/null
+++ b/src/reporting/packages/choosePackageManager.ts
@@ -0,0 +1,16 @@
+import { FileSystem } from "../../adapters/fileSystem";
+import { preferredLockfiles, PackageManager } from "./packageManagers";
+
+export type ChoosePackageManagerDependencies = {
+    fileSystem: Pick<FileSystem, "fileExists">;
+};
+
+export const choosePackageManager = async (dependencies: ChoosePackageManagerDependencies) => {
+    for (const [packageManager, lockFile] of preferredLockfiles) {
+        if (await dependencies.fileSystem.fileExists(lockFile)) {
+            return packageManager;
+        }
+    }
+
+    return PackageManager.npm;
+};
diff --git a/src/reporting/packages/packageManagers.ts b/src/reporting/packages/packageManagers.ts
new file mode 100644
index 0000000..a55caf3
--- /dev/null
+++ b/src/reporting/packages/packageManagers.ts
@@ -0,0 +1,17 @@
+export enum PackageManager {
+    npm,
+    pnpm,
+    Yarn,
+}
+
+export const preferredLockfiles = new Map([
+    [PackageManager.npm, "./package-lock.json"],
+    [PackageManager.pnpm, "./pnpm-lock.yaml"],
+    [PackageManager.Yarn, "./yarn.lock"],
+]);
+
+export const installationMessages = {
+    [PackageManager.npm]: (packages: string) => `npm install ${packages} --save-dev`,
+    [PackageManager.pnpm]: (packages: string) => `pnpm add ${packages} --save-dev`,
+    [PackageManager.Yarn]: (packages: string) => `yarn add ${packages} --dev`,
+};
diff --git a/src/reporting/reportConversionResults.ts b/src/reporting/reportConversionResults.ts
index 438370f..b68b259 100644
--- a/src/reporting/reportConversionResults.ts
+++ b/src/reporting/reportConversionResults.ts
@@ -2,20 +2,28 @@ import chalk from "chalk";
 import { EOL } from "os";
 
 import { Logger } from "../adapters/logger";
+import { SansDependencies } from "../binding";
 import { RuleConversionResults } from "../rules/convertRules";
 import { ESLintRuleOptions, TSLintRuleOptions } from "../rules/types";
-import { ReportConversionResultsDependencies } from "./dependencies";
+import { choosePackageManager } from "./packages/choosePackageManager";
 import {
     logFailedConversions,
     logMissingConversionTarget,
-    logMissingPlugins,
+    logMissingPackages,
     logSuccessfulConversions,
 } from "./reportOutputs";
 
-export const reportConversionResults = (
+export type ReportConversionResultsDependencies = {
+    logger: Logger;
+    choosePackageManager: SansDependencies<typeof choosePackageManager>;
+};
+
+export const reportConversionResults = async (
     dependencies: ReportConversionResultsDependencies,
     ruleConversionResults: RuleConversionResults,
 ) => {
+    const packageManager = await dependencies.choosePackageManager();
+
     if (ruleConversionResults.converted.size !== 0) {
         logSuccessfulConversions("rule", ruleConversionResults.converted, dependencies.logger);
         logNotices(ruleConversionResults.converted, dependencies.logger);
@@ -40,9 +48,7 @@ export const reportConversionResults = (
         );
     }
 
-    if (ruleConversionResults.plugins.size !== 0) {
-        logMissingPlugins(ruleConversionResults.plugins, dependencies.logger);
-    }
+    logMissingPackages(ruleConversionResults.plugins, packageManager, dependencies.logger);
 };
 
 type RuleWithNotices = {
diff --git a/src/reporting/reportEditorSettingConversionResults.ts b/src/reporting/reportEditorSettingConversionResults.ts
index 938988a..85e06ac 100644
--- a/src/reporting/reportEditorSettingConversionResults.ts
+++ b/src/reporting/reportEditorSettingConversionResults.ts
@@ -1,16 +1,20 @@
 import { EOL } from "os";
 
+import { Logger } from "../adapters/logger";
 import { EditorSettingConversionResults } from "../editorSettings/convertEditorSettings";
 import { EditorSetting } from "../editorSettings/types";
-import { ReportConversionResultsDependencies } from "./dependencies";
 import {
     logFailedConversions,
     logMissingConversionTarget,
     logSuccessfulConversions,
 } from "./reportOutputs";
 
+export type ReportEditorSettingConversionResultsDependencies = {
+    logger: Logger;
+};
+
 export const reportEditorSettingConversionResults = (
-    dependencies: ReportConversionResultsDependencies,
+    dependencies: ReportEditorSettingConversionResultsDependencies,
     editorSettingConversionResults: EditorSettingConversionResults,
 ) => {
     if (editorSettingConversionResults.converted.size !== 0) {
diff --git a/src/reporting/reportOutputs.ts b/src/reporting/reportOutputs.ts
index fe933b1..93e8c04 100644
--- a/src/reporting/reportOutputs.ts
+++ b/src/reporting/reportOutputs.ts
@@ -5,6 +5,7 @@ import { Logger } from "../adapters/logger";
 import { EditorSetting } from "../editorSettings/types";
 import { ErrorSummary } from "../errors/errorSummary";
 import { ESLintRuleOptions } from "../rules/types";
+import { PackageManager, installationMessages } from "./packages/packageManagers";
 
 export type EditorSettingEntry = Pick<EditorSetting, "editorSettingName">;
 
@@ -65,17 +66,22 @@ export const logMissingConversionTarget = <T>(
     logger.stdout.write(chalk.yellow(EOL));
 };
 
-export const logMissingPlugins = (plugins: Set<string>, logger: Logger) => {
-    logger.stdout.write(chalk.cyanBright(`${EOL}⚡ ${plugins.size}`));
-    logger.stdout.write(chalk.cyan(" package"));
-    logger.stdout.write(chalk.cyan(plugins.size === 1 ? " is" : "s are"));
-    logger.stdout.write(chalk.cyan(` required for new ESLint rules.`));
-    logger.stdout.write(chalk.cyanBright(` ⚡${EOL}`));
+export const logMissingPackages = (
+    plugins: Set<string>,
+    packageManager: PackageManager,
+    logger: Logger,
+) => {
+    const packageNames = [
+        "@typescript-eslint/eslint-plugin",
+        "@typescript-eslint/parser",
+        "eslint",
+        ...Array.from(plugins),
+    ].sort();
 
-    logger.stdout.write(
-        Array.from(plugins)
-            .map((pluginName) => `  ${chalk.cyanBright(pluginName)}${EOL}`)
-            .join(""),
-    );
-    logger.stdout.write(EOL);
+    logger.stdout.write(chalk.cyanBright(`${EOL}⚡ ${packageNames.length}`));
+    logger.stdout.write(chalk.cyan(" packages are required for running with ESLint."));
+    logger.stdout.write(chalk.cyanBright(" ⚡"));
+    logger.stdout.write(`${EOL}  `);
+    logger.stdout.write(chalk.cyan(installationMessages[packageManager](packageNames.join(" "))));
+    logger.stdout.write(EOL.repeat(2));
 };
diff --git a/src/rules/converters/file-name-casing.ts b/src/rules/converters/file-name-casing.ts
index 5cd6214..d51bfdf 100644
--- a/src/rules/converters/file-name-casing.ts
+++ b/src/rules/converters/file-name-casing.ts
@@ -18,7 +18,7 @@ export const convertFileNameCasing: RuleConverter = (tslintRule) => {
                 ...collectArguments(tslintRule.ruleArguments),
             },
         ],
-        plugins: ["unicorn"],
+        plugins: ["eslint-plugin-unicorn"],
     };
 };
 
diff --git a/src/rules/converters/no-default-export.ts b/src/rules/converters/no-default-export.ts
index 2b28ed4..b358335 100644
--- a/src/rules/converters/no-default-export.ts
+++ b/src/rules/converters/no-default-export.ts
@@ -7,6 +7,6 @@ export const convertNoDefaultExport: RuleConverter = () => {
                 ruleName: "import/no-default-export",
             },
         ],
-        plugins: ["import"],
+        plugins: ["eslint-plugin-import"],
     };
 };
diff --git a/src/rules/converters/no-implicit-dependencies.ts b/src/rules/converters/no-implicit-dependencies.ts
index 9b08bb3..a353c0f 100644
--- a/src/rules/converters/no-implicit-dependencies.ts
+++ b/src/rules/converters/no-implicit-dependencies.ts
@@ -27,6 +27,6 @@ export const convertNoImplicitDependencies: RuleConverter = (tslintRule) => {
             },
         ],
 
-        plugins: ["import"],
+        plugins: ["eslint-plugin-import"],
     };
 };
diff --git a/src/rules/converters/no-null-keyword.ts b/src/rules/converters/no-null-keyword.ts
index 9c7d5b0..bf88456 100644
--- a/src/rules/converters/no-null-keyword.ts
+++ b/src/rules/converters/no-null-keyword.ts
@@ -8,6 +8,6 @@ export const convertNoNullKeyword: RuleConverter = () => {
                 ruleName: "no-null/no-null",
             },
         ],
-        plugins: ["no-null"],
+        plugins: ["eslint-plugin-no-null"],
     };
 };
diff --git a/src/rules/converters/only-arrow-functions.ts b/src/rules/converters/only-arrow-functions.ts
index 8d718be..318895e 100644
--- a/src/rules/converters/only-arrow-functions.ts
+++ b/src/rules/converters/only-arrow-functions.ts
@@ -20,6 +20,6 @@ export const convertOnlyArrowFunctions: RuleConverter = (tslintRule) => {
                 ruleName: "prefer-arrow/prefer-arrow-functions",
             },
         ],
-        plugins: ["prefer-arrow"],
+        plugins: ["eslint-plugin-prefer-arrow"],
     };
 };
diff --git a/src/rules/converters/ordered-imports.ts b/src/rules/converters/ordered-imports.ts
index cf7148d..d3c4417 100644
--- a/src/rules/converters/ordered-imports.ts
+++ b/src/rules/converters/ordered-imports.ts
@@ -12,7 +12,7 @@ export const convertOrderedImports: RuleConverter = (tslintRule) => {
         .map((option) => `Option "${option}" is not supported by ESLint.`);
 
     return {
-        plugins: ["import"],
+        plugins: ["eslint-plugin-import"],
         rules: [
             {
                 ...(notices.length !== 0 && { notices }),


Test patch:
diff --git a/src/reporting/packages/choosePackageManager.test.ts b/src/reporting/packages/choosePackageManager.test.ts
new file mode 100644
index 0000000..8ab268a
--- /dev/null
+++ b/src/reporting/packages/choosePackageManager.test.ts
@@ -0,0 +1,30 @@
+import { choosePackageManager } from "./choosePackageManager";
+import { PackageManager } from "./packageManagers";
+
+describe("choosePackageManager", () => {
+    it("uses a non-npm package manager when that package manager's lock file exists", async () => {
+        // Arrange
+        const fileSystem = {
+            fileExists: async (fileName: string) => fileName === "./yarn.lock",
+        };
+
+        // Act
+        const result = await choosePackageManager({ fileSystem });
+
+        // Assert
+        expect(result).toEqual(PackageManager.Yarn);
+    });
+
+    it("uses npm when no lock file exists", async () => {
+        // Arrange
+        const fileSystem = {
+            fileExists: async () => false,
+        };
+
+        // Act
+        const result = await choosePackageManager({ fileSystem });
+
+        // Assert
+        expect(result).toEqual(PackageManager.npm);
+    });
+});
diff --git a/src/reporting/reportConversionResults.test.ts b/src/reporting/reportConversionResults.test.ts
index 0cbdb4c..bc98079 100644
--- a/src/reporting/reportConversionResults.test.ts
+++ b/src/reporting/reportConversionResults.test.ts
@@ -3,10 +3,18 @@ import { EOL } from "os";
 import { createStubLogger, expectEqualWrites } from "../adapters/logger.stubs";
 import { createEmptyConversionResults } from "../conversion/conversionResults.stubs";
 import { ESLintRuleOptions } from "../rules/types";
+import { PackageManager } from "./packages/packageManagers";
 import { reportConversionResults } from "./reportConversionResults";
 
+const createStubDependencies = (packageManager = PackageManager.Yarn) => {
+    const choosePackageManager = jest.fn().mockResolvedValueOnce(packageManager);
+    const logger = createStubLogger();
+
+    return { choosePackageManager, logger };
+};
+
 describe("reportConversionResults", () => {
-    it("logs a successful conversion without notices when there is one converted rule without notices", () => {
+    it("logs a successful conversion without notices when there is one converted rule without notices", async () => {
         // Arrange
         const conversionResults = createEmptyConversionResults({
             converted: new Map<string, ESLintRuleOptions>([
@@ -21,19 +29,22 @@ describe("reportConversionResults", () => {
             ]),
         });
 
-        const logger = createStubLogger();
+        const { choosePackageManager, logger } = createStubDependencies();
 
         // Act
-        reportConversionResults({ logger }, conversionResults);
+        await reportConversionResults({ choosePackageManager, logger }, conversionResults);
 
         // Assert
         expectEqualWrites(
             logger.stdout.write,
-            `✨ 1 rule replaced with its ESLint equivalent. ✨${EOL}`,
+            `✨ 1 rule replaced with its ESLint equivalent. ✨`,
+            ``,
+            `⚡ 3 packages are required for running with ESLint. ⚡`,
+            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`,
         );
     });
 
-    it("logs a successful conversion with notices when there is one converted rule with notices", () => {
+    it("logs a successful conversion with notices when there is one converted rule with notices", async () => {
         // Arrange
         const conversionResults = createEmptyConversionResults({
             converted: new Map<string, ESLintRuleOptions>([
@@ -49,10 +60,10 @@ describe("reportConversionResults", () => {
             ]),
         });
 
-        const logger = createStubLogger();
+        const { choosePackageManager, logger } = createStubDependencies();
 
         // Act
-        reportConversionResults({ logger }, conversionResults);
+        await reportConversionResults({ choosePackageManager, logger }, conversionResults);
 
         // Assert
         expectEqualWrites(
@@ -62,10 +73,13 @@ describe("reportConversionResults", () => {
             `  * tslint-rule-one:`,
             `    - 1`,
             `    - 2`,
+            ``,
+            `⚡ 3 packages are required for running with ESLint. ⚡`,
+            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`,
         );
     });
 
-    it("logs successful conversions when there are multiple converted rules", () => {
+    it("logs successful conversions when there are multiple converted rules", async () => {
         // Arrange
         const conversionResults = createEmptyConversionResults({
             converted: new Map<string, ESLintRuleOptions>([
@@ -90,15 +104,16 @@ describe("reportConversionResults", () => {
             ]),
         });
 
-        const logger = createStubLogger();
+        const { choosePackageManager, logger } = createStubDependencies();
 
         // Act
-        reportConversionResults({ logger }, conversionResults);
+        await reportConversionResults({ choosePackageManager, logger }, conversionResults);
 
         // Assert
         expectEqualWrites(
             logger.stdout.write,
-            `✨ 2 rules replaced with their ESLint equivalents. ✨${EOL}`,
+            `✨ 2 rules replaced with their ESLint equivalents. ✨`,
+            ``,
             `❗ 2 ESLint rules behave differently from their TSLint counterparts ❗`,
             `  * tslint-rule-one:`,
             `    - 1`,
@@ -106,19 +121,22 @@ describe("reportConversionResults", () => {
             `  * tslint-rule-two:`,
             `    - 3`,
             `    - 4`,
+            ``,
+            `⚡ 3 packages are required for running with ESLint. ⚡`,
+            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`,
         );
     });
 
-    it("logs a failed conversion when there is one failed conversion", () => {
+    it("logs a failed conversion when there is one failed conversion", async () => {
         // Arrange
         const conversionResults = createEmptyConversionResults({
             failed: [{ getSummary: () => "It broke." }],
         });
 
-        const logger = createStubLogger();
+        const { choosePackageManager, logger } = createStubDependencies();
 
         // Act
-        reportConversionResults({ logger }, conversionResults);
+        await reportConversionResults({ choosePackageManager, logger }, conversionResults);
 
         // Assert
         expectEqualWrites(
@@ -128,16 +146,16 @@ describe("reportConversionResults", () => {
         );
     });
 
-    it("logs failed conversions when there are multiple failed conversions", () => {
+    it("logs failed conversions when there are multiple failed conversions", async () => {
         // Arrange
         const conversionResults = createEmptyConversionResults({
             failed: [{ getSummary: () => "It broke." }, { getSummary: () => "It really broke." }],
         });
 
-        const logger = createStubLogger();
+        const { choosePackageManager, logger } = createStubDependencies();
 
         // Act
-        reportConversionResults({ logger }, conversionResults);
+        await reportConversionResults({ choosePackageManager, logger }, conversionResults);
 
         // Assert
         expectEqualWrites(
@@ -147,7 +165,7 @@ describe("reportConversionResults", () => {
         );
     });
 
-    it("logs a missing rule when there is a missing rule", () => {
+    it("logs a missing rule when there is a missing rule", async () => {
         // Arrange
         const conversionResults = createEmptyConversionResults({
             missing: [
@@ -159,16 +177,19 @@ describe("reportConversionResults", () => {
             ],
         });
 
-        const logger = createStubLogger();
+        const { choosePackageManager, logger } = createStubDependencies();
 
         // Act
-        reportConversionResults({ logger }, conversionResults);
+        await reportConversionResults({ choosePackageManager, logger }, conversionResults);
 
         // Assert
         expectEqualWrites(
             logger.stdout.write,
             "❓ 1 rule does not yet have an ESLint equivalent ❓",
             `  See generated log file; defaulting to eslint-plugin-tslint for it.`,
+            "",
+            "⚡ 3 packages are required for running with ESLint. ⚡",
+            "  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev",
         );
         expectEqualWrites(
             logger.info.write,
@@ -176,7 +197,7 @@ describe("reportConversionResults", () => {
         );
     });
 
-    it("logs missing rules when there are missing rules", () => {
+    it("logs missing rules when there are missing rules", async () => {
         // Arrange
         const conversionResults = createEmptyConversionResults({
             missing: [
@@ -193,16 +214,19 @@ describe("reportConversionResults", () => {
             ],
         });
 
-        const logger = createStubLogger();
+        const { choosePackageManager, logger } = createStubDependencies();
 
         // Act
-        reportConversionResults({ logger }, conversionResults);
+        await reportConversionResults({ choosePackageManager, logger }, conversionResults);
 
         // Assert
         expectEqualWrites(
             logger.stdout.write,
             "❓ 2 rules do not yet have ESLint equivalents ❓",
             `  See generated log file; defaulting to eslint-plugin-tslint for these rules.`,
+            "",
+            "⚡ 3 packages are required for running with ESLint. ⚡",
+            "  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev",
         );
         expectEqualWrites(
             logger.info.write,
@@ -211,42 +235,22 @@ describe("reportConversionResults", () => {
         );
     });
 
-    it("logs a missing plugin when there is a missing plugin", () => {
-        // Arrange
-        const conversionResults = createEmptyConversionResults({
-            plugins: new Set(["plugin-one"]),
-        });
-
-        const logger = createStubLogger();
-
-        // Act
-        reportConversionResults({ logger }, conversionResults);
-
-        // Assert
-        expectEqualWrites(
-            logger.stdout.write,
-            "⚡ 1 package is required for new ESLint rules. ⚡",
-            "  plugin-one",
-        );
-    });
-
-    it("logs missing plugins when there are missing plugins", () => {
+    it("logs missing plugins when there are missing plugins", async () => {
         // Arrange
         const conversionResults = createEmptyConversionResults({
             plugins: new Set(["plugin-one", "plugin-two"]),
         });
 
-        const logger = createStubLogger();
+        const { choosePackageManager, logger } = createStubDependencies();
 
         // Act
-        reportConversionResults({ logger }, conversionResults);
+        await reportConversionResults({ choosePackageManager, logger }, conversionResults);
 
         // Assert
         expectEqualWrites(
             logger.stdout.write,
-            "⚡ 2 packages are required for new ESLint rules. ⚡",
-            "  plugin-one",
-            "  plugin-two",
+            "⚡ 5 packages are required for running with ESLint. ⚡",
+            "  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint plugin-one plugin-two --dev",
         );
     });
 });
diff --git a/src/reporting/reportOutputs.test.ts b/src/reporting/reportOutputs.test.ts
new file mode 100644
index 0000000..e58d221
--- /dev/null
+++ b/src/reporting/reportOutputs.test.ts
@@ -0,0 +1,56 @@
+import { PackageManager } from "./packages/packageManagers";
+import { createStubLogger, expectEqualWrites } from "../adapters/logger.stubs";
+import { logMissingPackages } from "./reportOutputs";
+
+const createStubDependencies = (packageManager: PackageManager) => ({
+    packageManager,
+    plugins: new Set<string>(),
+    logger: createStubLogger(),
+});
+
+describe("reportOutputs", () => {
+    it("reports an npm command when the package manager is npm", () => {
+        // Arrange
+        const { logger, packageManager, plugins } = createStubDependencies(PackageManager.npm);
+
+        // Act
+        logMissingPackages(plugins, packageManager, logger);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 3 packages are required for running with ESLint. ⚡`,
+            `  npm install @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`,
+        );
+    });
+
+    it("reports a pnpm command when the package manager is pnpm", () => {
+        // Arrange
+        const { logger, packageManager, plugins } = createStubDependencies(PackageManager.pnpm);
+
+        // Act
+        logMissingPackages(plugins, packageManager, logger);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 3 packages are required for running with ESLint. ⚡`,
+            `  pnpm add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --save-dev`,
+        );
+    });
+
+    it("reports a Yarn command when the package manager is Yarn", () => {
+        // Arrange
+        const { logger, packageManager, plugins } = createStubDependencies(PackageManager.Yarn);
+
+        // Act
+        logMissingPackages(plugins, packageManager, logger);
+
+        // Assert
+        expectEqualWrites(
+            logger.stdout.write,
+            `⚡ 3 packages are required for running with ESLint. ⚡`,
+            `  yarn add @typescript-eslint/eslint-plugin @typescript-eslint/parser eslint --dev`,
+        );
+    });
+});
diff --git a/src/rules/converters/tests/file-name-casing.test.ts b/src/rules/converters/tests/file-name-casing.test.ts
index c9349c9..c09d254 100644
--- a/src/rules/converters/tests/file-name-casing.test.ts
+++ b/src/rules/converters/tests/file-name-casing.test.ts
@@ -12,7 +12,7 @@ describe(convertFileNameCasing, () => {
                     ruleName: "unicorn/filename-case",
                 },
             ],
-            plugins: ["unicorn"],
+            plugins: ["eslint-plugin-unicorn"],
         });
     });
 
@@ -34,7 +34,7 @@ describe(convertFileNameCasing, () => {
                     ],
                 },
             ],
-            plugins: ["unicorn"],
+            plugins: ["eslint-plugin-unicorn"],
         });
     });
 
@@ -61,7 +61,7 @@ describe(convertFileNameCasing, () => {
                     ],
                 },
             ],
-            plugins: ["unicorn"],
+            plugins: ["eslint-plugin-unicorn"],
         });
     });
 
@@ -77,7 +77,7 @@ describe(convertFileNameCasing, () => {
                     ruleName: "unicorn/filename-case",
                 },
             ],
-            plugins: ["unicorn"],
+            plugins: ["eslint-plugin-unicorn"],
         });
     });
 
@@ -96,7 +96,7 @@ describe(convertFileNameCasing, () => {
                     ruleName: "unicorn/filename-case",
                 },
             ],
-            plugins: ["unicorn"],
+            plugins: ["eslint-plugin-unicorn"],
         });
     });
 });
diff --git a/src/rules/converters/tests/no-default-export.test.ts b/src/rules/converters/tests/no-default-export.test.ts
index b985c49..29d2f50 100644
--- a/src/rules/converters/tests/no-default-export.test.ts
+++ b/src/rules/converters/tests/no-default-export.test.ts
@@ -12,7 +12,7 @@ describe(convertNoDefaultExport, () => {
                     ruleName: "import/no-default-export",
                 },
             ],
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
         });
     });
 });
diff --git a/src/rules/converters/tests/no-implicit-dependencies.test.ts b/src/rules/converters/tests/no-implicit-dependencies.test.ts
index 8d97f23..4d1c309 100644
--- a/src/rules/converters/tests/no-implicit-dependencies.test.ts
+++ b/src/rules/converters/tests/no-implicit-dependencies.test.ts
@@ -12,7 +12,7 @@ describe(convertNoImplicitDependencies, () => {
                     ruleName: "import/no-extraneous-dependencies",
                 },
             ],
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
         });
     });
 
@@ -28,7 +28,7 @@ describe(convertNoImplicitDependencies, () => {
                     ruleName: "import/no-extraneous-dependencies",
                 },
             ],
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
         });
     });
 
@@ -44,7 +44,7 @@ describe(convertNoImplicitDependencies, () => {
                     ruleName: "import/no-extraneous-dependencies",
                 },
             ],
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
         });
     });
 
@@ -60,7 +60,7 @@ describe(convertNoImplicitDependencies, () => {
                     ruleName: "import/no-extraneous-dependencies",
                 },
             ],
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
         });
     });
 
@@ -76,7 +76,7 @@ describe(convertNoImplicitDependencies, () => {
                     ruleName: "import/no-extraneous-dependencies",
                 },
             ],
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
         });
     });
 });
diff --git a/src/rules/converters/tests/no-null-keyword.test.ts b/src/rules/converters/tests/no-null-keyword.test.ts
index d337114..231f852 100644
--- a/src/rules/converters/tests/no-null-keyword.test.ts
+++ b/src/rules/converters/tests/no-null-keyword.test.ts
@@ -13,7 +13,7 @@ describe(convertNoNullKeyword, () => {
                     ruleName: "no-null/no-null",
                 },
             ],
-            plugins: ["no-null"],
+            plugins: ["eslint-plugin-no-null"],
         });
     });
 });
diff --git a/src/rules/converters/tests/only-arrow-functions.test.ts b/src/rules/converters/tests/only-arrow-functions.test.ts
index 7012ce0..f5825dc 100644
--- a/src/rules/converters/tests/only-arrow-functions.test.ts
+++ b/src/rules/converters/tests/only-arrow-functions.test.ts
@@ -7,7 +7,7 @@ describe(convertOnlyArrowFunctions, () => {
         });
 
         expect(result).toEqual({
-            plugins: ["prefer-arrow"],
+            plugins: ["eslint-plugin-prefer-arrow"],
             rules: [
                 {
                     ruleName: "prefer-arrow/prefer-arrow-functions",
@@ -22,7 +22,7 @@ describe(convertOnlyArrowFunctions, () => {
         });
 
         expect(result).toEqual({
-            plugins: ["prefer-arrow"],
+            plugins: ["eslint-plugin-prefer-arrow"],
             rules: [
                 {
                     notices: ["ESLint does not support allowing standalone function declarations."],
@@ -38,7 +38,7 @@ describe(convertOnlyArrowFunctions, () => {
         });
 
         expect(result).toEqual({
-            plugins: ["prefer-arrow"],
+            plugins: ["eslint-plugin-prefer-arrow"],
             rules: [
                 {
                     notices: [
@@ -56,7 +56,7 @@ describe(convertOnlyArrowFunctions, () => {
         });
 
         expect(result).toEqual({
-            plugins: ["prefer-arrow"],
+            plugins: ["eslint-plugin-prefer-arrow"],
             rules: [
                 {
                     notices: [
diff --git a/src/rules/converters/tests/ordered-imports.test.ts b/src/rules/converters/tests/ordered-imports.test.ts
index 98b8498..479449d 100644
--- a/src/rules/converters/tests/ordered-imports.test.ts
+++ b/src/rules/converters/tests/ordered-imports.test.ts
@@ -7,7 +7,7 @@ describe(convertOrderedImports, () => {
         });
 
         expect(result).toEqual({
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
             rules: [
                 {
                     ruleName: "import/order",
@@ -22,7 +22,7 @@ describe(convertOrderedImports, () => {
         });
 
         expect(result).toEqual({
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
             rules: [
                 {
                     notices: ['Option "import-sources-order" is not supported by ESLint.'],
@@ -38,7 +38,7 @@ describe(convertOrderedImports, () => {
         });
 
         expect(result).toEqual({
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
             rules: [
                 {
                     notices: ['Option "named-imports-order" is not supported by ESLint.'],
@@ -54,7 +54,7 @@ describe(convertOrderedImports, () => {
         });
 
         expect(result).toEqual({
-            plugins: ["import"],
+            plugins: ["eslint-plugin-import"],
             rules: [
                 {
                     notices: [