diff --git a/.changeset/sweet-chicken-smash.md b/.changeset/sweet-chicken-smash.md new file mode 100644 index 0000000000..204d84a628 --- /dev/null +++ b/.changeset/sweet-chicken-smash.md @@ -0,0 +1,12 @@ +--- +'@backstage/cli': minor +--- + +**BREAKING**: Removed the following deprecated commands: + +- `create`: Use `backstage-cli new` instead +- `create-plugin`: Use `backstage-cli new` instead +- `plugin:diff`: Use `backstage-cli fix` instead +- `test`: Use `backstage-cli repo test` or `backstage-cli package test` instead +- `versions:check`: Use `yarn dedupe` or `yarn-deduplicate` instead +- `clean`: Use `backstage-cli package clean` instead diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md index d77271c4b3..2eb9bb8dea 100644 --- a/packages/cli/cli-report.md +++ b/packages/cli/cli-report.md @@ -13,7 +13,6 @@ Options: Commands: new [options] - test config:docs [options] config:print [options] config:check [options] @@ -23,7 +22,6 @@ Commands: migrate [command] versions:bump [options] versions:migrate [options] - clean build-workspace [options] [packages...] create-github-app info @@ -40,15 +38,6 @@ Options: -h, --help ``` -### `backstage-cli clean` - -``` -Usage: backstage-cli clean [options] - -Options: - -h, --help -``` - ### `backstage-cli config:check` ``` @@ -481,120 +470,6 @@ Options: -h, --help ``` -### `backstage-cli test` - -``` -Usage: backstage-cli [--config=] [TestPathPattern] - -Options: - -h, --help - --version - --all - --automock - -b, --bail - --cache - --cacheDirectory - --changedFilesWithAncestor - --changedSince - --ci - --clearCache - --clearMocks - --collectCoverage - --collectCoverageFrom - --color - --colors - -c, --config - --coverage - --coverageDirectory - --coveragePathIgnorePatterns - --coverageProvider - --coverageReporters - --coverageThreshold - --debug - --detectLeaks - --detectOpenHandles - --env - --errorOnDeprecated - -e, --expand - --filter - --findRelatedTests - --forceExit - --globalSetup - --globalTeardown - --globals - --haste - --ignoreProjects - --init - --injectGlobals - --json - --lastCommit - --listTests - --logHeapUsage - --maxConcurrency - -w, --maxWorkers - --moduleDirectories - --moduleFileExtensions - --moduleNameMapper - --modulePathIgnorePatterns - --modulePaths - --noStackTrace - --notify - --notifyMode - -o, --onlyChanged - -f, --onlyFailures - --openHandlesTimeout - --outputFile - --passWithNoTests - --preset - --prettierPath - --projects - --randomize - --reporters - --resetMocks - --resetModules - --resolver - --restoreMocks - --rootDir - --roots - -i, --runInBand - --runTestsByPath - --runner - --seed - --selectProjects - --setupFiles - --setupFilesAfterEnv - --shard - --showConfig - --showSeed - --silent - --skipFilter - --snapshotSerializers - --testEnvironment - --testEnvironmentOptions - --testFailureExitCode - --testLocationInResults - --testMatch - -t, --testNamePattern - --testPathIgnorePatterns - --testPathPattern - --testRegex - --testResultsProcessor - --testRunner - --testSequencer - --testTimeout - --transform - --transformIgnorePatterns - --unmockedModulePathPatterns - -u, --updateSnapshot - --useStderr - --verbose - --watch - --watchAll - --watchPathIgnorePatterns - --watchman - --workerThreads -``` - ### `backstage-cli versions:bump` ``` diff --git a/packages/cli/src/commands/create-plugin/createPlugin.test.ts b/packages/cli/src/commands/create-plugin/createPlugin.test.ts deleted file mode 100644 index 84af6ebf69..0000000000 --- a/packages/cli/src/commands/create-plugin/createPlugin.test.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs-extra'; -import path from 'path'; -import { movePlugin } from './createPlugin'; -import { createMockDirectory } from '@backstage/backend-test-utils'; - -const id = 'testPluginMock'; - -describe('createPlugin', () => { - const mockDir = createMockDirectory(); - - describe('movePlugin', () => { - it('should move the temporary plugin directory to its final place', async () => { - mockDir.setContent({ - [id]: {}, - }); - const tempDir = mockDir.resolve(id); - const pluginDir = mockDir.resolve('test-temp/plugins', id); - - await movePlugin(tempDir, pluginDir, id); - await expect(fs.pathExists(pluginDir)).resolves.toBe(true); - expect(pluginDir).toMatch(path.join('', 'plugins', id)); - }); - }); -}); diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts deleted file mode 100644 index f8d7e54e97..0000000000 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ /dev/null @@ -1,343 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs-extra'; -import { promisify } from 'util'; -import chalk from 'chalk'; -import inquirer, { Answers, Question } from 'inquirer'; -import { exec as execCb } from 'child_process'; -import { join as joinPath, resolve as resolvePath } from 'path'; -import camelCase from 'lodash/camelCase'; -import upperFirst from 'lodash/upperFirst'; -import os from 'os'; -import { OptionValues } from 'commander'; -import { assertError } from '@backstage/errors'; -import { - addCodeownersEntry, - getCodeownersFilePath, - parseOwnerIds, -} from '../../lib/codeowners'; -import { paths } from '../../lib/paths'; -import { Task, templatingTask } from '../../lib/tasks'; -import { Lockfile } from '../../lib/versioning'; -import { createPackageVersionProvider } from '../../lib/version'; - -const exec = promisify(execCb); - -async function checkExists(destination: string) { - await Task.forItem('checking', destination, async () => { - if (await fs.pathExists(destination)) { - const existing = chalk.cyan( - destination.replace(`${paths.targetRoot}/`, ''), - ); - throw new Error( - `A plugin with the same name already exists: ${existing}\nPlease try again with a different plugin ID`, - ); - } - }); -} - -const sortObjectByKeys = (obj: { [name in string]: string }) => { - return Object.keys(obj) - .sort() - .reduce((result, key: string) => { - result[key] = obj[key]; - return result; - }, {} as { [name in string]: string }); -}; - -export const capitalize = (str: string): string => - str.charAt(0).toUpperCase() + str.slice(1); - -export const addExportStatement = async ( - file: string, - exportStatement: string, -) => { - const newContents = fs - .readFileSync(file, 'utf8') - .split('\n') - .filter(Boolean) // get rid of empty lines - .concat([exportStatement]) - .concat(['']) // newline at end of file - .join('\n'); - - await fs.writeFile(file, newContents, 'utf8'); -}; - -export async function addPluginDependencyToApp( - rootDir: string, - pluginPackage: string, - versionStr: string, -) { - const packageFilePath = 'packages/app/package.json'; - const packageFile = resolvePath(rootDir, packageFilePath); - - await Task.forItem('processing', packageFilePath, async () => { - const packageFileContent = await fs.readFile(packageFile, 'utf-8'); - const packageFileJson = JSON.parse(packageFileContent); - const dependencies = packageFileJson.dependencies; - - if (dependencies[pluginPackage]) { - throw new Error( - `Plugin ${pluginPackage} already exists in ${packageFile}`, - ); - } - - dependencies[pluginPackage] = `^${versionStr}`; - packageFileJson.dependencies = sortObjectByKeys(dependencies); - const newContents = `${JSON.stringify(packageFileJson, null, 2)}\n`; - - await fs.writeFile(packageFile, newContents, 'utf-8').catch(error => { - throw new Error( - `Failed to add plugin as dependency to app: ${packageFile}: ${error.message}`, - ); - }); - }); -} - -export async function addPluginExtensionToApp( - pluginId: string, - extensionName: string, - pluginPackage: string, -) { - const pluginsFilePath = paths.resolveTargetRoot('packages/app/src/App.tsx'); - if (!(await fs.pathExists(pluginsFilePath))) { - return; - } - - await Task.forItem('processing', pluginsFilePath, async () => { - const content = await fs.readFile(pluginsFilePath, 'utf8'); - const revLines = content.split('\n').reverse(); - - const lastImportIndex = revLines.findIndex(line => - line.match(/ from ("|').*("|')/), - ); - const lastRouteIndex = revLines.findIndex(line => - line.match(/<\/FlatRoutes/), - ); - - if (lastImportIndex !== -1 && lastRouteIndex !== -1) { - revLines.splice( - lastImportIndex, - 0, - `import { ${extensionName} } from '${pluginPackage}';`, - ); - const [indentation] = revLines[lastRouteIndex + 1].match(/^\s*/) ?? []; - revLines.splice( - lastRouteIndex + 1, - 0, - `${indentation}}/>`, - ); - - const newContent = revLines.reverse().join('\n'); - await fs.writeFile(pluginsFilePath, newContent, 'utf8'); - } - }); -} - -async function cleanUp(tempDir: string) { - await Task.forItem('remove', 'temporary directory', async () => { - await fs.remove(tempDir); - }); -} - -async function buildPlugin(pluginFolder: string) { - const commands = [ - 'yarn install', - 'yarn lint --fix', - 'yarn tsc', - 'yarn build', - ]; - for (const command of commands) { - try { - await Task.forItem('executing', command, async () => { - process.chdir(pluginFolder); - await exec(command); - }).catch(error => { - process.stdout.write(error.stderr); - process.stdout.write(error.stdout); - throw new Error( - `Warning: Could not execute command ${chalk.cyan(command)}`, - ); - }); - } catch (error) { - assertError(error); - Task.error(error.message); - break; - } - } -} - -export async function movePlugin( - tempDir: string, - destination: string, - id: string, -) { - await Task.forItem('moving', id, async () => { - await fs.move(tempDir, destination).catch(error => { - throw new Error( - `Failed to move plugin from ${tempDir} to ${destination}: ${error.message}`, - ); - }); - }); -} - -export default async (opts: OptionValues) => { - const codeownersPath = await getCodeownersFilePath(paths.targetRoot); - - const questions: Question[] = [ - { - type: 'input', - name: 'id', - message: chalk.blue('Enter an ID for the plugin [required]'), - validate: (value: any) => { - if (!value) { - return chalk.red('Please enter an ID for the plugin'); - } else if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(value)) { - return chalk.red( - 'Plugin IDs must be lowercase and contain only letters, digits, and dashes.', - ); - } - return true; - }, - }, - ]; - - if (codeownersPath) { - questions.push({ - type: 'input', - name: 'owner', - message: chalk.blue( - 'Enter the owner(s) of the plugin. If specified, this will be added to CODEOWNERS for the plugin path. [optional]', - ), - validate: (value: any) => { - if (!value) { - return true; - } - - const ownerIds = parseOwnerIds(value); - if (!ownerIds) { - return chalk.red( - 'The owner must be a space separated list of team names (e.g. @org/team-name), usernames (e.g. @username), or the email addresses of users (e.g. user@example.com).', - ); - } - - return true; - }, - }); - } - - const answers: Answers = await inquirer.prompt(questions); - const pluginId = - opts.backend && !answers.id.endsWith('-backend') - ? `${answers.id}-backend` - : answers.id; - - const name = opts.scope - ? `@${opts.scope.replace(/^@/, '')}/plugin-${pluginId}` - : `plugin-${pluginId}`; - const pluginVar = `${camelCase(answers.id)}Plugin`; - const extensionName = `${upperFirst(camelCase(answers.id))}Page`; - const npmRegistry = opts.npmRegistry && opts.scope ? opts.npmRegistry : ''; - const privatePackage = opts.private === false ? false : true; - const isMonoRepo = await fs.pathExists(paths.resolveTargetRoot('lerna.json')); - const appPackage = paths.resolveTargetRoot('packages/app'); - const templateDir = paths.resolveOwn( - opts.backend - ? 'templates/default-backend-plugin' - : 'templates/default-plugin', - ); - const pluginDir = isMonoRepo - ? paths.resolveTargetRoot('plugins', pluginId) - : paths.resolveTargetRoot(pluginId); - const { version: pluginVersion } = isMonoRepo - ? await fs.readJson(paths.resolveTargetRoot('lerna.json')) - : { version: '0.1.0' }; - const license = opts.license ?? 'Apache-2.0'; - - let lockfile: Lockfile | undefined; - try { - lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock')); - } catch (error) { - console.warn(`No yarn.lock available, ${error}`); - } - - Task.log(); - Task.log('Creating the plugin...'); - - Task.section('Checking if the plugin ID is available'); - await checkExists(pluginDir); - - Task.section('Creating a temporary plugin directory'); - const tempDir = await fs.mkdtemp( - joinPath(os.tmpdir(), `backstage-plugin-${pluginId}`), - ); - - try { - Task.section('Preparing files'); - - await templatingTask( - templateDir, - tempDir, - { - ...answers, - pluginVar, - pluginVersion, - extensionName, - name, - privatePackage, - npmRegistry, - license, - }, - createPackageVersionProvider(lockfile), - isMonoRepo, - ); - - Task.section('Moving to final location'); - await movePlugin(tempDir, pluginDir, pluginId); - - Task.section('Building the plugin'); - await buildPlugin(pluginDir); - - if ((await fs.pathExists(appPackage)) && !opts.backend) { - Task.section('Adding plugin as dependency in app'); - await addPluginDependencyToApp(paths.targetRoot, name, pluginVersion); - - Task.section('Import plugin in app'); - await addPluginExtensionToApp(pluginId, extensionName, name); - } - - if (answers.owner) { - await addCodeownersEntry(`/plugins/${pluginId}`, answers.owner); - } - - Task.log(); - Task.log(`🥇 Successfully created ${chalk.cyan(`${name}`)}`); - Task.log(); - Task.exit(); - } catch (error) { - assertError(error); - Task.error(error.message); - - Task.log('It seems that something went wrong when creating the plugin 🤔'); - Task.log('We are going to clean up, and then you can try again.'); - - Task.section('Cleanup'); - await cleanUp(tempDir); - Task.error('🔥 Failed to create plugin!'); - Task.exit(1); - } -}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index b1658a5ff1..9e8996647d 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -265,63 +265,6 @@ export function registerCommands(program: Command) { .option('--no-private', 'Do not mark new packages as private') .action(lazy(() => import('./new/new').then(m => m.default))); - program - .command('create', { hidden: true }) - .storeOptionsAsProperties(false) - .description( - 'Open up an interactive guide to creating new things in your app [DEPRECATED]', - ) - .option( - '--select ', - 'Select the thing you want to be creating upfront', - ) - .option( - '--option =', - 'Pre-fill options for the creation process', - (opt, arr: string[]) => [...arr, opt], - [], - ) - .option('--scope ', 'The scope to use for new packages') - .option( - '--npm-registry ', - 'The package registry to use for new packages', - ) - .option('--no-private', 'Do not mark new packages as private') - .action(lazy(() => import('./new/new').then(m => m.default))); - - program - .command('create-plugin', { hidden: true }) - .option( - '--backend', - 'Create plugin with the backend dependencies as default', - ) - .description('Creates a new plugin in the current repository [DEPRECATED]') - .option('--scope ', 'npm scope') - .option('--npm-registry ', 'npm registry URL') - .option('--no-private', 'Public npm package') - .action( - lazy(() => import('./create-plugin/createPlugin').then(m => m.default)), - ); - - program - .command('plugin:diff', { hidden: true }) - .option('--check', 'Fail if changes are required') - .option('--yes', 'Apply all changes') - .description( - 'Diff an existing plugin with the creation template [DEPRECATED]', - ) - .action(lazy(() => import('./plugin/diff').then(m => m.default))); - - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('test') - .allowUnknownOption(true) // Allows the command to run, but we still need to parse raw args - .helpOption(', --backstage-cli-help') // Let Jest handle help - .description( - 'Run tests, forwarding args to Jest, defaulting to watch mode [DEPRECATED]', - ) - .action(lazy(() => import('./test').then(m => m.default))); - program .command('config:docs') .option( @@ -403,12 +346,6 @@ export function registerCommands(program: Command) { .description('Bump Backstage packages to the latest versions') .action(lazy(() => import('./versions/bump').then(m => m.default))); - program - .command('versions:check', { hidden: true }) - .option('--fix', 'Fix any auto-fixable versioning problems') - .description('Check Backstage package versioning') - .action(lazy(() => import('./versions/lint').then(m => m.default))); - program .command('versions:migrate') .option( @@ -424,12 +361,6 @@ export function registerCommands(program: Command) { ) .action(lazy(() => import('./versions/migrate').then(m => m.default))); - // TODO(Rugvip): Deprecate in favor of package variant - program - .command('clean') - .description('Delete cache directories [DEPRECATED]') - .action(lazy(() => import('./clean/clean').then(m => m.default))); - program .command('build-workspace [packages...]') .option( diff --git a/packages/cli/src/commands/plugin/diff.ts b/packages/cli/src/commands/plugin/diff.ts deleted file mode 100644 index 07f2a94ea8..0000000000 --- a/packages/cli/src/commands/plugin/diff.ts +++ /dev/null @@ -1,102 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import fs from 'fs-extra'; -import { OptionValues } from 'commander'; -import { - diffTemplateFiles, - handlers, - handleAllFiles, - inquirerPromptFunc, - makeCheckPromptFunc, - yesPromptFunc, -} from '../../lib/diff'; -import { paths } from '../../lib/paths'; - -export type PluginData = { - id: string; - name: string; - privatePackage: string; - pluginVersion: string; - npmRegistry: string; -}; - -const fileHandlers = [ - { - patterns: ['package.json'], - handler: handlers.packageJson, - }, - { - // make sure files in 1st level of src/ and dev/ exist - patterns: ['.eslintrc.js'], - handler: handlers.exists, - }, - { - patterns: ['README.md', 'tsconfig.json', /^src\//, /^dev\//], - handler: handlers.skip, - }, -]; - -export default async (opts: OptionValues) => { - let promptFunc = inquirerPromptFunc; - let finalize = () => {}; - - if (opts.check) { - [promptFunc, finalize] = makeCheckPromptFunc(); - } else if (opts.yes) { - promptFunc = yesPromptFunc; - } - - const data = await readPluginData(); - const templateFiles = await diffTemplateFiles('default-plugin', data); - await handleAllFiles(fileHandlers, templateFiles, promptFunc); - finalize(); -}; - -// Reads templating data from the existing plugin -async function readPluginData(): Promise { - let name: string; - let privatePackage: string; - let pluginVersion: string; - let npmRegistry: string; - try { - const pkg = require(paths.resolveTarget('package.json')); - name = pkg.name; - privatePackage = pkg.private; - pluginVersion = pkg.version; - const scope = name.split('/')[0]; - if (`${scope}:registry` in pkg.publishConfig) { - const registryURL = pkg.publishConfig[`${scope}:registry`]; - npmRegistry = `"${scope}:registry" : "${registryURL}"`; - } else npmRegistry = ''; - } catch (error) { - throw new Error(`Failed to read target package, ${error}`); - } - - const pluginTsContents = await fs.readFile( - paths.resolveTarget('src/plugin.ts'), - 'utf8', - ); - // TODO: replace with some proper parsing logic or plugin metadata file - const pluginIdMatch = pluginTsContents.match(/id: ['"`](.+?)['"`]/); - if (!pluginIdMatch) { - throw new Error(`Failed to parse plugin.ts, no plugin ID found`); - } - - const id = pluginIdMatch[1]; - - return { id, name, privatePackage, pluginVersion, npmRegistry }; -} diff --git a/packages/cli/src/commands/plugin/test.ts b/packages/cli/src/commands/plugin/test.ts deleted file mode 100644 index ca076ae6fc..0000000000 --- a/packages/cli/src/commands/plugin/test.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { OptionValues } from 'commander'; -import { run } from '../../lib/run'; - -export default async (opts: OptionValues) => { - const args = ['test']; - - if (opts.watch) { - args.push('--watch'); - } - if (opts.coverage) { - args.push('--coverage'); - } - - await run('web-scripts', args); -}; diff --git a/packages/cli/src/commands/remove-plugin/file-mocks.ts b/packages/cli/src/commands/remove-plugin/file-mocks.ts deleted file mode 100644 index 1264dc8f2e..0000000000 --- a/packages/cli/src/commands/remove-plugin/file-mocks.ts +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -export const pluginsFileContent = ` -export { plugin as WelcomePlugin } from '@backstage/plugin-welcome'; -export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse';`; - -export const codeownersFileContent = ` -* @spotify/backstage-core -/docs/features/techdocs @spotify/techdocs-core -/plugins/cost-insights @spotify/silver-lining -`; - -export const packageFileContent = { - name: 'example-app', - version: '0.1.1', - dependencies: {}, - devDependencies: {}, - scripts: {}, -}; diff --git a/packages/cli/src/commands/versions/lint.ts b/packages/cli/src/commands/versions/lint.ts deleted file mode 100644 index 66b5fc04c4..0000000000 --- a/packages/cli/src/commands/versions/lint.ts +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -export default async () => { - throw new Error( - 'This command has been removed, please consider alternatives such as `yarn dedupe` instead.', - ); -};