/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/

function uninstallSubcommand(named: string | string[]): Fig.Subcommand {
	return {
		name: named,
		description: 'Uninstall a package',
		args: {
			name: 'package',
			generators: dependenciesGenerator,
			filterStrategy: 'fuzzy',
			isVariadic: true,
		},
		options: npmUninstallOptions,
	};
}

const atsInStr = (s: string) => (s.match(/@/g) || []).length;

export const createNpmSearchHandler =
	(keywords?: string[]) =>
		async (
			context: string[],
			executeShellCommand: Fig.ExecuteCommandFunction,
			shellContext: Fig.ShellContext
		): Promise<Fig.Suggestion[]> => {
			const searchTerm = context[context.length - 1];
			if (searchTerm === '') {
				return [];
			}
			// Add optional keyword parameter
			const keywordParameter =
				keywords && keywords.length > 0 ? `+keywords:${keywords.join(',')}` : '';

			const queryPackagesUrl = keywordParameter
				? `https://api.npms.io/v2/search?size=20&q=${searchTerm}${keywordParameter}`
				: `https://api.npms.io/v2/search/suggestions?q=${searchTerm}&size=20`;

			// Query the API with the package name
			const queryPackages = [
				'-s',
				'-H',
				'Accept: application/json',
				queryPackagesUrl,
			];
			// We need to remove the '@' at the end of the searchTerm before querying versions
			const queryVersions = [
				'-s',
				'-H',
				'Accept: application/vnd.npm.install-v1+json',
				`https://registry.npmjs.org/${searchTerm.slice(0, -1)}`,
			];
			// If the end of our token is '@', then we want to generate version suggestions
			// Otherwise, we want packages
			const out = (query: string) =>
				executeShellCommand({
					command: 'curl',
					args: query[query.length - 1] === '@' ? queryVersions : queryPackages,
				});
			// If our token starts with '@', then a 2nd '@' tells us we want
			// versions.
			// Otherwise, '@' anywhere else in the string will indicate the same.
			const shouldGetVersion = searchTerm.startsWith('@')
				? atsInStr(searchTerm) > 1
				: searchTerm.includes('@');

			try {
				const data = JSON.parse((await out(searchTerm)).stdout);
				if (shouldGetVersion) {
					// create dist tags suggestions
					const versions = Object.entries(data['dist-tags'] || {}).map(
						([key, value]) => ({
							name: key,
							description: value,
						})
					) as Fig.Suggestion[];
					// create versions
					versions.push(
						...Object.keys(data.versions)
							.map((version) => ({ name: version }))
							.reverse()
					);
					return versions;
				}

				const results = keywordParameter ? data.results : data;
				return results.map(
					(item: { package: { name: string; description: string } }) => ({
						name: item.package.name,
						description: item.package.description,
					})
				) as Fig.Suggestion[];
			} catch (error) {
				console.error({ error });
				return [];
			}
		};

// GENERATORS
export const npmSearchGenerator: Fig.Generator = {
	trigger: (newToken, oldToken) => {
		// If the package name starts with '@', we want to trigger when
		// the 2nd '@' is typed because we'll need to generate version
		// suggetsions
		// e.g. @typescript-eslint/types
		if (oldToken.startsWith('@')) {
			return !(atsInStr(oldToken) > 1 && atsInStr(newToken) > 1);
		}

		// If the package name doesn't start with '@', then trigger when
		// we see the first '@' so we can generate version suggestions
		return !(oldToken.includes('@') && newToken.includes('@'));
	},
	getQueryTerm: '@',
	cache: {
		ttl: 1000 * 60 * 60 * 24 * 2, // 2 days
	},
	custom: createNpmSearchHandler(),
};

const workspaceGenerator: Fig.Generator = {
	// script: "cat $(npm prefix)/package.json",
	custom: async (tokens, executeShellCommand) => {
		const { stdout: npmPrefix } = await executeShellCommand({
			command: 'npm',
			args: ['prefix'],
		});

		const { stdout: out } = await executeShellCommand({
			command: 'cat',
			args: [`${npmPrefix}/package.json`],
		});

		const suggestions: Fig.Suggestion[] = [];
		try {
			if (out.trim() === '') {
				return suggestions;
			}

			const packageContent = JSON.parse(out);
			const workspaces = packageContent['workspaces'];

			if (workspaces) {
				for (const workspace of workspaces) {
					suggestions.push({
						name: workspace,
						description: 'Workspaces',
					});
				}
			}
		} catch (e) {
			console.log(e);
		}
		return suggestions;
	},
};

/** Generator that lists package.json dependencies */
export const dependenciesGenerator: Fig.Generator = {
	trigger: (newToken) => newToken === '-g' || newToken === '--global',
	custom: async function (tokens, executeShellCommand) {
		if (!tokens.includes('-g') && !tokens.includes('--global')) {
			const { stdout: npmPrefix } = await executeShellCommand({
				command: 'npm',
				args: ['prefix'],
			});
			const { stdout: out } = await executeShellCommand({
				command: 'cat',
				args: [`${npmPrefix}/package.json`],
			});
			const packageContent = JSON.parse(out);
			const dependencies = packageContent['dependencies'] ?? {};
			const devDependencies = packageContent['devDependencies'];
			const optionalDependencies = packageContent['optionalDependencies'] ?? {};
			Object.assign(dependencies, devDependencies, optionalDependencies);

			return Object.keys(dependencies)
				.filter((pkgName) => {
					const isListed = tokens.some((current) => current === pkgName);
					return !isListed;
				})
				.map((pkgName) => ({
					name: pkgName,
					description: dependencies[pkgName]
						? 'dependency'
						: optionalDependencies[pkgName]
							? 'optionalDependency'
							: 'devDependency',
				}));
		} else {
			const { stdout } = await executeShellCommand({
				command: 'bash',
				args: ['-c', 'ls -1 `npm root -g`'],
			});
			return stdout.split('\n').map((name) => ({
				name,
				description: 'Global dependency',
			}));
		}
	},
};

/** Generator that lists package.json scripts (with the respect to the `fig` field) */
export const npmScriptsGenerator: Fig.Generator = {
	cache: {
		strategy: 'stale-while-revalidate',
		cacheByDirectory: true,
	},
	script: [
		'bash',
		'-c',
		'until [[ -f package.json ]] || [[ $PWD = \' / \' ]]; do cd ..; done; cat package.json',
	],
	postProcess: function (out, [npmClient]) {
		if (out.trim() === '') {
			return [];
		}

		try {
			const packageContent = JSON.parse(out);
			const scripts = packageContent['scripts'];
			const figCompletions = packageContent['fig'] || {};

			if (scripts) {
				return Object.entries(scripts).map(([scriptName, scriptContents]) => {
					const icon =
						npmClient === 'yarn'
							? 'fig://icon?type=yarn'
							: 'fig://icon?type=npm';
					const customScripts: Fig.Suggestion = figCompletions[scriptName];
					return {
						name: scriptName,
						icon,
						description: scriptContents as string,
						priority: 51,
						/**
						 * If there are custom definitions for the scripts
						 * we want to override the default values
						 * */
						...customScripts,
					};
				});
			}
		} catch (e) {
			console.error(e);
		}

		return [];
	},
};

const globalOption: Fig.Option = {
	name: ['-g', '--global'],
	description:
		'Operates in \'global\' mode, so that packages are installed into the prefix folder instead of the current working directory',
};

const jsonOption: Fig.Option = {
	name: '--json',
	description: 'Show output in json format',
};

const omitOption: Fig.Option = {
	name: '--omit',
	description: 'Dependency types to omit from the installation tree on disk',
	args: {
		name: 'Package type',
		default: 'dev',
		suggestions: ['dev', 'optional', 'peer'],
	},
	isRepeatable: 3,
};

const parseableOption: Fig.Option = {
	name: ['-p', '--parseable'],
	description:
		'Output parseable results from commands that write to standard output',
};

const longOption: Fig.Option = {
	name: ['-l', '--long'],
	description: 'Show extended information',
};

const workSpaceOptions: Fig.Option[] = [
	{
		name: ['-w', '--workspace'],
		description:
			'Enable running a command in the context of the configured workspaces of the current project',
		args: {
			name: 'workspace',
			generators: workspaceGenerator,
			isVariadic: true,
		},
	},
	{
		name: ['-ws', '--workspaces'],
		description:
			'Enable running a command in the context of all the configured workspaces',
	},
];

const npmUninstallOptions: Fig.Option[] = [
	{
		name: ['-S', '--save'],
		description: 'Package will be removed from your dependencies',
	},
	{
		name: ['-D', '--save-dev'],
		description: 'Package will appear in your `devDependencies`',
	},
	{
		name: ['-O', '--save-optional'],
		description: 'Package will appear in your `optionalDependencies`',
	},
	{
		name: '--no-save',
		description: 'Prevents saving to `dependencies`',
	},
	{
		name: '-g',
		description: 'Uninstall global package',
	},
	...workSpaceOptions,
];

const npmListOptions: Fig.Option[] = [
	{
		name: ['-a', '-all'],
		description: 'Show all outdated or installed packages',
	},
	jsonOption,
	longOption,
	parseableOption,
	{
		name: '--depth',
		description: 'The depth to go when recursing packages',
		args: { name: 'depth' },
	},
	{
		name: '--link',
		description: 'Limits output to only those packages that are linked',
	},
	{
		name: '--package-lock-only',
		description:
			'Current operation will only use the package-lock.json, ignoring node_modules',
	},
	{
		name: '--no-unicode',
		description: 'Uses unicode characters in the tree output',
	},
	globalOption,
	omitOption,
	...workSpaceOptions,
];

const registryOption: Fig.Option = {
	name: '--registry',
	description: 'The base URL of the npm registry',
	args: { name: 'registry' },
};

const verboseOption: Fig.Option = {
	name: '--verbose',
	description: 'Show extra information',
	args: { name: 'verbose' },
};

const otpOption: Fig.Option = {
	name: '--otp',
	description: 'One-time password from a two-factor authenticator',
	args: { name: 'otp' },
};

const ignoreScriptsOption: Fig.Option = {
	name: '--ignore-scripts',
	description:
		'If true, npm does not run scripts specified in package.json files',
};

const scriptShellOption: Fig.Option = {
	name: '--script-shell',
	description:
		'The shell to use for scripts run with the npm exec, npm run and npm init <pkg> commands',
	args: { name: 'script-shell' },
};

const dryRunOption: Fig.Option = {
	name: '--dry-run',
	description:
		'Indicates that you don\'t want npm to make any changes and that it should only report what it would have done',
};

const completionSpec: Fig.Spec = {
	name: 'npm',
	parserDirectives: {
		flagsArePosixNoncompliant: true,
	},
	description: 'Node package manager',
	subcommands: [
		{
			name: ['install', 'i', 'add'],
			description: 'Install a package and its dependencies',
			args: {
				name: 'package',
				isOptional: true,
				generators: npmSearchGenerator,
				debounce: true,
				isVariadic: true,
			},
			options: [
				{
					name: ['-P', '--save-prod'],
					description:
						'Package will appear in your `dependencies`. This is the default unless `-D` or `-O` are present',
				},
				{
					name: ['-D', '--save-dev'],
					description: 'Package will appear in your `devDependencies`',
				},
				{
					name: ['-O', '--save-optional'],
					description: 'Package will appear in your `optionalDependencies`',
				},
				{
					name: '--no-save',
					description: 'Prevents saving to `dependencies`',
				},
				{
					name: ['-E', '--save-exact'],
					description:
						'Saved dependencies will be configured with an exact version rather than using npm\'s default semver range operator',
				},
				{
					name: ['-B', '--save-bundle'],
					description:
						'Saved dependencies will also be added to your bundleDependencies list',
				},
				globalOption,
				{
					name: '--global-style',
					description:
						'Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder',
				},
				{
					name: '--legacy-bundling',
					description:
						'Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package',
				},
				{
					name: '--legacy-peer-deps',
					description:
						'Bypass peerDependency auto-installation. Emulate install behavior of NPM v4 through v6',
				},
				{
					name: '--package-lock-only',
					description: 'Only update the `package-lock.json`, instead of checking `node_modules` and downloading dependencies.',
				},
				{
					name: '--strict-peer-deps',
					description:
						'If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure',
				},
				{
					name: '--no-package-lock',
					description: 'Ignores package-lock.json files when installing',
				},
				registryOption,
				verboseOption,
				omitOption,
				ignoreScriptsOption,
				{
					name: '--no-audit',
					description:
						'Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes',
				},
				{
					name: '--no-bin-links',
					description:
						'Tells npm to not create symlinks (or .cmd shims on Windows) for package executables',
				},
				{
					name: '--no-fund',
					description:
						'Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding',
				},
				dryRunOption,
				...workSpaceOptions,
			],
		},
		{
			name: ['run', 'run-script'],
			description: 'Run arbitrary package scripts',
			options: [
				...workSpaceOptions,
				{
					name: '--if-present',
					description:
						'Npm will not exit with an error code when run-script is invoked for a script that isn\'t defined in the scripts section of package.json',
				},
				{
					name: '--silent',
					description: '',
				},
				ignoreScriptsOption,
				scriptShellOption,
				{
					name: '--',
					args: {
						name: 'args',
						isVariadic: true,
						// TODO: load the spec based on the runned script (see yarn spec `yarnScriptParsedDirectives`)
					},
				},
			],
			args: {
				name: 'script',
				description: 'Script to run from your package.json',
				filterStrategy: 'fuzzy',
				generators: npmScriptsGenerator,
			},
		},
		{
			name: 'init',
			description: 'Trigger the initialization',
			options: [
				{
					name: ['-y', '--yes'],
					description:
						'Automatically answer \'yes\' to any prompts that npm might print on the command line',
				},
				{
					name: '-w',
					description:
						'Create the folders and boilerplate expected while also adding a reference to your project workspaces property',
					args: { name: 'dir' },
				},
			],
		},
		{ name: 'access', description: 'Set access controls on private packages' },
		{
			name: ['adduser', 'login'],
			description: 'Add a registry user account',
			options: [
				registryOption,
				{
					name: '--scope',
					description:
						'Associate an operation with a scope for a scoped registry',
					args: {
						name: 'scope',
						description: 'Scope name',
					},
				},
			],
		},
		{
			name: 'audit',
			description: 'Run a security audit',
			subcommands: [
				{
					name: 'fix',
					description:
						'If the fix argument is provided, then remediations will be applied to the package tree',
					options: [
						dryRunOption,
						{
							name: ['-f', '--force'],
							description:
								'Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input',
							isDangerous: true,
						},
						...workSpaceOptions,
					],
				},
			],
			options: [
				...workSpaceOptions,
				{
					name: '--audit-level',
					description:
						'The minimum level of vulnerability for npm audit to exit with a non-zero exit code',
					args: {
						name: 'audit',
						suggestions: [
							'info',
							'low',
							'moderate',
							'high',
							'critical',
							'none',
						],
					},
				},
				{
					name: '--package-lock-only',
					description:
						'Current operation will only use the package-lock.json, ignoring node_modules',
				},
				jsonOption,
				omitOption,
			],
		},
		{
			name: 'bin',
			description: 'Print the folder where npm will install executables',
			options: [globalOption],
		},
		{
			name: ['bugs', 'issues'],
			description: 'Report bugs for a package in a web browser',
			args: {
				name: 'package',
				isOptional: true,
				generators: npmSearchGenerator,
				debounce: true,
				isVariadic: true,
			},
			options: [
				{
					name: '--no-browser',
					description: 'Display in command line instead of browser',
					exclusiveOn: ['--browser'],
				},
				{
					name: '--browser',
					description:
						'The browser that is called by the npm bugs command to open websites',
					args: { name: 'browser' },
					exclusiveOn: ['--no-browser'],
				},
				registryOption,
			],
		},
		{
			name: 'cache',
			description: 'Manipulates packages cache',
			subcommands: [
				{
					name: 'add',
					description: 'Add the specified packages to the local cache',
				},
				{
					name: 'clean',
					description: 'Delete all data out of the cache folder',
				},
				{
					name: 'verify',
					description:
						'Verify the contents of the cache folder, garbage collecting any unneeded data, and verifying the integrity of the cache index and all cached data',
				},
			],
			options: [
				{
					name: '--cache',
					args: { name: 'cache' },
					description: 'The location of npm\'s cache directory',
				},
			],
		},
		{
			name: ['ci', 'clean-install', 'install-clean'],
			description: 'Install a project with a clean slate',
			options: [
				{
					name: '--audit',
					description:
						'When \'true\' submit audit reports alongside the current npm command to the default registry and all registries configured for scopes',
					args: {
						name: 'audit',
						suggestions: ['true', 'false'],
					},
					exclusiveOn: ['--no-audit'],
				},
				{
					name: '--no-audit',
					description:
						'Do not submit audit reports alongside the current npm command',
					exclusiveOn: ['--audit'],
				},
				ignoreScriptsOption,
				scriptShellOption,
				verboseOption,
				registryOption,
			],
		},
		{
			name: 'cit',
			description: 'Install a project with a clean slate and run tests',
		},
		{
			name: 'clean-install-test',
			description: 'Install a project with a clean slate and run tests',
		},
		{ name: 'completion', description: 'Tab completion for npm' },
		{
			name: ['config', 'c'],
			description: 'Manage the npm configuration files',
			subcommands: [
				{
					name: 'set',
					description: 'Sets the config key to the value',
					args: [{ name: 'key' }, { name: 'value' }],
					options: [
						{ name: ['-g', '--global'], description: 'Sets it globally' },
					],
				},
				{
					name: 'get',
					description: 'Echo the config value to stdout',
					args: { name: 'key' },
				},
				{
					name: 'list',
					description: 'Show all the config settings',
					options: [
						{ name: '-g', description: 'Lists globally installed packages' },
						{ name: '-l', description: 'Also shows defaults' },
						jsonOption,
					],
				},
				{
					name: 'delete',
					description: 'Deletes the key from all configuration files',
					args: { name: 'key' },
				},
				{
					name: 'edit',
					description: 'Opens the config file in an editor',
					options: [
						{ name: '--global', description: 'Edits the global config' },
					],
				},
			],
		},
		{ name: 'create', description: 'Create a package.json file' },
		{
			name: ['dedupe', 'ddp'],
			description: 'Reduce duplication in the package tree',
		},
		{
			name: 'deprecate',
			description: 'Deprecate a version of a package',
			options: [registryOption],
		},
		{ name: 'dist-tag', description: 'Modify package distribution tags' },
		{
			name: ['docs', 'home'],
			description: 'Open documentation for a package in a web browser',
			args: {
				name: 'package',
				isOptional: true,
				generators: npmSearchGenerator,
				debounce: true,
				isVariadic: true,
			},
			options: [
				...workSpaceOptions,
				registryOption,
				{
					name: '--no-browser',
					description: 'Display in command line instead of browser',
					exclusiveOn: ['--browser'],
				},
				{
					name: '--browser',
					description:
						'The browser that is called by the npm docs command to open websites',
					args: { name: 'browser' },
					exclusiveOn: ['--no-browser'],
				},
			],
		},
		{
			name: 'doctor',
			description: 'Check your npm environment',
			options: [registryOption],
		},
		{
			name: 'edit',
			description: 'Edit an installed package',
			options: [
				{
					name: '--editor',
					description: 'The command to run for npm edit or npm config edit',
				},
			],
		},
		{
			name: ['explain', 'why'],
			description: 'Explain installed packages',
			args: {
				name: 'package-spec',
				description: 'Package name or path to folder within node_modules',
				isVariadic: true,
				filterStrategy: 'fuzzy',
				generators: dependenciesGenerator,
			},
			options: [jsonOption, ...workSpaceOptions],
		},
		{
			name: 'explore',
			description: 'Browse an installed package',
			args: {
				name: 'package',
				filterStrategy: 'fuzzy',
				generators: dependenciesGenerator,
			},
		},
		{ name: 'fund', description: 'Retrieve funding information' },
		{ name: 'get', description: 'Echo the config value to stdout' },
		{
			name: 'help',
			description: 'Get help on npm',
			args: {
				name: 'term',
				isVariadic: true,
				description: 'Terms to search for',
			},
			options: [
				{
					name: '--viewer',
					description: 'The program to use to view help content',
					args: {
						name: 'viewer',
					},
				},
			],
		},
		{
			name: 'help-search',
			description: 'Search npm help documentation',
			args: {
				name: 'text',
				description: 'Text to search for',
			},
			options: [longOption],
		},
		{ name: 'hook', description: 'Manage registry hooks' },
		{
			name: 'install-ci-test',
			description: 'Install a project with a clean slate and run tests',
		},
		{ name: 'install-test', description: 'Install package(s) and run tests' },
		{ name: 'it', description: 'Install package(s) and run tests' },
		{
			name: 'link',
			description: 'Symlink a package folder',
			args: { name: 'path', template: 'filepaths' },
		},
		{ name: 'ln', description: 'Symlink a package folder' },
		{
			name: 'logout',
			description: 'Log out of the registry',
			options: [
				registryOption,
				{
					name: '--scope',
					description:
						'Associate an operation with a scope for a scoped registry',
					args: {
						name: 'scope',
						description: 'Scope name',
					},
				},
			],
		},
		{
			name: ['ls', 'list'],
			description: 'List installed packages',
			options: npmListOptions,
			args: { name: '[@scope]/pkg', isVariadic: true },
		},
		{
			name: 'org',
			description: 'Manage orgs',
			subcommands: [
				{
					name: 'set',
					description: 'Add a user to an org or manage roles',
					args: [
						{
							name: 'orgname',
							description: 'Organization name',
						},
						{
							name: 'username',
							description: 'User name',
						},
						{
							name: 'role',
							isOptional: true,
							suggestions: ['developer', 'admin', 'owner'],
						},
					],
					options: [registryOption, otpOption],
				},
				{
					name: 'rm',
					description: 'Remove a user from an org',
					args: [
						{
							name: 'orgname',
							description: 'Organization name',
						},
						{
							name: 'username',
							description: 'User name',
						},
					],
					options: [registryOption, otpOption],
				},
				{
					name: 'ls',
					description:
						'List users in an org or see what roles a particular user has in an org',
					args: [
						{
							name: 'orgname',
							description: 'Organization name',
						},
						{
							name: 'username',
							description: 'User name',
							isOptional: true,
						},
					],
					options: [registryOption, otpOption, jsonOption, parseableOption],
				},
			],
		},
		{
			name: 'outdated',
			description: 'Check for outdated packages',
			args: {
				name: '[<@scope>/]<pkg>',
				isVariadic: true,
				isOptional: true,
			},
			options: [
				{
					name: ['-a', '-all'],
					description: 'Show all outdated or installed packages',
				},
				jsonOption,
				longOption,
				parseableOption,
				{
					name: '-g',
					description: 'Checks globally',
				},
				...workSpaceOptions,
			],
		},
		{
			name: ['owner', 'author'],
			description: 'Manage package owners',
			subcommands: [
				{
					name: 'ls',
					description:
						'List all the users who have access to modify a package and push new versions. Handy when you need to know who to bug for help',
					args: { name: '[@scope/]pkg' },
					options: [registryOption],
				},
				{
					name: 'add',
					description:
						'Add a new user as a maintainer of a package. This user is enabled to modify metadata, publish new versions, and add other owners',
					args: [{ name: 'user' }, { name: '[@scope/]pkg' }],
					options: [registryOption, otpOption],
				},
				{
					name: 'rm',
					description:
						'Remove a user from the package owner list. This immediately revokes their privileges',
					args: [{ name: 'user' }, { name: '[@scope/]pkg' }],
					options: [registryOption, otpOption],
				},
			],
		},
		{
			name: 'pack',
			description: 'Create a tarball from a package',
			args: {
				name: '[<@scope>/]<pkg>',
			},
			options: [
				jsonOption,
				dryRunOption,
				...workSpaceOptions,
				{
					name: '--pack-destination',
					description: 'Directory in which npm pack will save tarballs',
					args: {
						name: 'pack-destination',
						template: ['folders'],
					},
				},
			],
		},
		{
			name: 'ping',
			description: 'Ping npm registry',
			options: [registryOption],
		},
		{
			name: 'pkg',
			description: 'Manages your package.json',
			subcommands: [
				{
					name: 'get',
					description:
						'Retrieves a value key, defined in your package.json file. It is possible to get multiple values and values for child fields',
					args: {
						name: 'field',
						description:
							'Name of the field to get. You can view child fields by separating them with a period',
						isVariadic: true,
					},
					options: [jsonOption, ...workSpaceOptions],
				},
				{
					name: 'set',
					description:
						'Sets a value in your package.json based on the field value. It is possible to set multiple values and values for child fields',
					args: {
						// Format is <field>=<value>. How to achieve this?
						name: 'field',
						description:
							'Name of the field to set. You can set child fields by separating them with a period',
						isVariadic: true,
					},
					options: [
						jsonOption,
						...workSpaceOptions,
						{
							name: ['-f', '--force'],
							description:
								'Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg',
							isDangerous: true,
						},
					],
				},
				{
					name: 'delete',
					description: 'Deletes a key from your package.json',
					args: {
						name: 'key',
						description:
							'Name of the key to delete. You can delete child fields by separating them with a period',
						isVariadic: true,
					},
					options: [
						...workSpaceOptions,
						{
							name: ['-f', '--force'],
							description:
								'Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input. Allow clobbering existing values in npm pkg',
							isDangerous: true,
						},
					],
				},
			],
		},
		{
			name: 'prefix',
			description: 'Display prefix',
			options: [
				{
					name: ['-g', '--global'],
					description: 'Print the global prefix to standard out',
				},
			],
		},
		{
			name: 'profile',
			description: 'Change settings on your registry profile',
			subcommands: [
				{
					name: 'get',
					description:
						'Display all of the properties of your profile, or one or more specific properties',
					args: {
						name: 'property',
						isOptional: true,
						description: 'Property name',
					},
					options: [registryOption, jsonOption, parseableOption, otpOption],
				},
				{
					name: 'set',
					description: 'Set the value of a profile property',
					args: [
						{
							name: 'property',
							description: 'Property name',
							suggestions: [
								'email',
								'fullname',
								'homepage',
								'freenode',
								'twitter',
								'github',
							],
						},
						{
							name: 'value',
							description: 'Property value',
						},
					],
					options: [registryOption, jsonOption, parseableOption, otpOption],
					subcommands: [
						{
							name: 'password',
							description:
								'Change your password. This is interactive, you\'ll be prompted for your current password and a new password',
						},
					],
				},
				{
					name: 'enable-2fa',
					description: 'Enables two-factor authentication',
					args: {
						name: 'mode',
						description:
							'Mode for two-factor authentication. Defaults to auth-and-writes mode',
						isOptional: true,
						suggestions: [
							{
								name: 'auth-only',
								description:
									'Require an OTP when logging in or making changes to your account\'s authentication',
							},
							{
								name: 'auth-and-writes',
								description:
									'Requires an OTP at all the times auth-only does, and also requires one when publishing a module, setting the latest dist-tag, or changing access via npm access and npm owner',
							},
						],
					},
					options: [registryOption, otpOption],
				},
				{
					name: 'disable-2fa',
					description: 'Disables two-factor authentication',
					options: [registryOption, otpOption],
				},
			],
		},
		{
			name: 'prune',
			description: 'Remove extraneous packages',
			args: {
				name: '[<@scope>/]<pkg>',
				isOptional: true,
			},
			options: [
				omitOption,
				dryRunOption,
				jsonOption,
				{
					name: '--production',
					description: 'Remove the packages specified in your devDependencies',
				},
				...workSpaceOptions,
			],
		},
		{
			name: 'publish',
			description: 'Publish a package',
			args: {
				name: 'tarball|folder',
				isOptional: true,
				description:
					'A url or file path to a gzipped tar archive containing a single folder with a package.json file inside | A folder containing a package.json file',
				template: ['folders'],
			},
			options: [
				{
					name: '--tag',
					description: 'Registers the published package with the given tag',
					args: { name: 'tag' },
				},
				...workSpaceOptions,
				{
					name: '--access',
					description:
						'Sets scoped package to be publicly viewable if set to \'public\'',
					args: {
						default: 'restricted',
						suggestions: ['restricted', 'public'],
					},
				},
				dryRunOption,
				otpOption,
			],
		},
		{
			name: ['rebuild', 'rb'],
			description: 'Rebuild a package',
			args: {
				name: '[<@scope>/]<pkg>[@<version>]',
			},
			options: [
				globalOption,
				...workSpaceOptions,
				ignoreScriptsOption,
				{
					name: '--no-bin-links',
					description:
						'Tells npm to not create symlinks (or .cmd shims on Windows) for package executables',
				},
			],
		},
		{
			name: 'repo',
			description: 'Open package repository page in the browser',
			args: {
				name: 'package',
				isOptional: true,
				generators: npmSearchGenerator,
				debounce: true,
				isVariadic: true,
			},
			options: [
				...workSpaceOptions,
				{
					name: '--no-browser',
					description: 'Display in command line instead of browser',
					exclusiveOn: ['--browser'],
				},
				{
					name: '--browser',
					description:
						'The browser that is called by the npm repo command to open websites',
					args: { name: 'browser' },
					exclusiveOn: ['--no-browser'],
				},
			],
		},
		{
			name: 'restart',
			description: 'Restart a package',
			options: [
				ignoreScriptsOption,
				scriptShellOption,
				{
					name: '--',
					args: {
						name: 'arg',
						description: 'Arguments to be passed to the restart script',
					},
				},
			],
		},
		{
			name: 'root',
			description: 'Display npm root',
			options: [
				{
					name: ['-g', '--global'],
					description:
						'Print the effective global node_modules folder to standard out',
				},
			],
		},
		{
			name: ['search', 's', 'se', 'find'],
			description: 'Search for packages',
			args: {
				name: 'search terms',
				isVariadic: true,
			},
			options: [
				longOption,
				jsonOption,
				{
					name: '--color',
					description: 'Show colors',
					args: {
						name: 'always',
						suggestions: ['always'],
						description: 'Always show colors',
					},
					exclusiveOn: ['--no-color'],
				},
				{
					name: '--no-color',
					description: 'Do not show colors',
					exclusiveOn: ['--color'],
				},
				parseableOption,
				{
					name: '--no-description',
					description: 'Do not show descriptions',
				},
				{
					name: '--searchopts',
					description:
						'Space-separated options that are always passed to search',
					args: {
						name: 'searchopts',
					},
				},
				{
					name: '--searchexclude',
					description:
						'Space-separated options that limit the results from search',
					args: {
						name: 'searchexclude',
					},
				},
				registryOption,
				{
					name: '--prefer-online',
					description:
						'If true, staleness checks for cached data will be forced, making the CLI look for updates immediately even for fresh package data',
					exclusiveOn: ['--prefer-offline', '--offline'],
				},
				{
					name: '--prefer-offline',
					description:
						'If true, staleness checks for cached data will be bypassed, but missing data will be requested from the server',
					exclusiveOn: ['--prefer-online', '--offline'],
				},
				{
					name: '--offline',
					description:
						'Force offline mode: no network requests will be done during install',
					exclusiveOn: ['--prefer-online', '--prefer-offline'],
				},
			],
		},
		{ name: 'set', description: 'Sets the config key to the value' },
		{
			name: 'set-script',
			description: 'Set tasks in the scripts section of package.json',
			args: [
				{
					name: 'script',
					description:
						'Name of the task to be added to the scripts section of package.json',
				},
				{
					name: 'command',
					description: 'Command to run when script is called',
				},
			],
			options: workSpaceOptions,
		},
		{
			name: 'shrinkwrap',
			description: 'Lock down dependency versions for publication',
		},
		{
			name: 'star',
			description: 'Mark your favorite packages',
			args: {
				name: 'pkg',
				description: 'Package to mark as favorite',
			},
			options: [
				registryOption,
				{
					name: '--no-unicode',
					description: 'Do not use unicode characters in the tree output',
				},
			],
		},
		{
			name: 'stars',
			description: 'View packages marked as favorites',
			args: {
				name: 'user',
				isOptional: true,
				description: 'View packages marked as favorites by <user>',
			},
			options: [registryOption],
		},
		{
			name: 'start',
			description: 'Start a package',
			options: [
				ignoreScriptsOption,
				scriptShellOption,
				{
					name: '--',
					args: {
						name: 'arg',
						description: 'Arguments to be passed to the start script',
					},
				},
			],
		},
		{
			name: 'stop',
			description: 'Stop a package',
			options: [
				ignoreScriptsOption,
				scriptShellOption,
				{
					name: '--',
					args: {
						name: 'arg',
						description: 'Arguments to be passed to the stop script',
					},
				},
			],
		},
		{
			name: 'team',
			description: 'Manage organization teams and team memberships',
			subcommands: [
				{
					name: 'create',
					args: { name: 'scope:team' },
					options: [registryOption, otpOption],
				},
				{
					name: 'destroy',
					args: { name: 'scope:team' },
					options: [registryOption, otpOption],
				},
				{
					name: 'add',
					args: [{ name: 'scope:team' }, { name: 'user' }],
					options: [registryOption, otpOption],
				},
				{
					name: 'rm',
					args: [{ name: 'scope:team' }, { name: 'user' }],
					options: [registryOption, otpOption],
				},
				{
					name: 'ls',
					args: { name: 'scope|scope:team' },
					options: [registryOption, jsonOption, parseableOption],
				},
			],
		},
		{
			name: ['test', 'tst', 't'],
			description: 'Test a package',
			options: [ignoreScriptsOption, scriptShellOption],
		},
		{
			name: 'token',
			description: 'Manage your authentication tokens',
			subcommands: [
				{
					name: 'list',
					description: 'Shows a table of all active authentication tokens',
					options: [jsonOption, parseableOption],
				},
				{
					name: 'create',
					description: 'Create a new authentication token',
					options: [
						{
							name: '--read-only',
							description:
								'This is used to mark a token as unable to publish when configuring limited access tokens with the npm token create command',
						},
						{
							name: '--cidr',
							description:
								'This is a list of CIDR address to be used when configuring limited access tokens with the npm token create command',
							isRepeatable: true,
							args: {
								name: 'cidr',
							},
						},
					],
				},
				{
					name: 'revoke',
					description:
						'Immediately removes an authentication token from the registry. You will no longer be able to use it',
					args: { name: 'idtoken' },
				},
			],
			options: [registryOption, otpOption],
		},
		uninstallSubcommand('uninstall'),
		uninstallSubcommand(['r', 'rm']),
		uninstallSubcommand('un'),
		uninstallSubcommand('remove'),
		uninstallSubcommand('unlink'),
		{
			name: 'unpublish',
			description: 'Remove a package from the registry',
			args: {
				name: '[<@scope>/]<pkg>[@<version>]',
			},
			options: [
				dryRunOption,
				{
					name: ['-f', '--force'],
					description:
						'Allow unpublishing all versions of a published package. Removes various protections against unfortunate side effects, common mistakes, unnecessary performance degradation, and malicious input',
					isDangerous: true,
				},
				...workSpaceOptions,
			],
		},
		{
			name: 'unstar',
			description: 'Remove an item from your favorite packages',
			args: {
				name: 'pkg',
				description: 'Package to unmark as favorite',
			},
			options: [
				registryOption,
				otpOption,
				{
					name: '--no-unicode',
					description: 'Do not use unicode characters in the tree output',
				},
			],
		},
		{
			name: ['update', 'upgrade', 'up'],
			description: 'Update a package',
			options: [
				{ name: '-g', description: 'Update global package' },
				{
					name: '--global-style',
					description:
						'Causes npm to install the package into your local node_modules folder with the same layout it uses with the global node_modules folder',
				},
				{
					name: '--legacy-bundling',
					description:
						'Causes npm to install the package such that versions of npm prior to 1.4, such as the one included with node 0.8, can install the package',
				},
				{
					name: '--strict-peer-deps',
					description:
						'If set to true, and --legacy-peer-deps is not set, then any conflicting peerDependencies will be treated as an install failure',
				},
				{
					name: '--no-package-lock',
					description: 'Ignores package-lock.json files when installing',
				},
				omitOption,
				ignoreScriptsOption,
				{
					name: '--no-audit',
					description:
						'Submit audit reports alongside the current npm command to the default registry and all registries configured for scopes',
				},
				{
					name: '--no-bin-links',
					description:
						'Tells npm to not create symlinks (or .cmd shims on Windows) for package executables',
				},
				{
					name: '--no-fund',
					description:
						'Hides the message at the end of each npm install acknowledging the number of dependencies looking for funding',
				},
				{
					name: '--save',
					description:
						'Update the semver values of direct dependencies in your project package.json',
				},
				dryRunOption,
				...workSpaceOptions,
			],
		},
		{
			name: 'version',
			description: 'Bump a package version',
			options: [
				...workSpaceOptions,
				jsonOption,
				{
					name: '--allow-same-version',
					description:
						'Prevents throwing an error when npm version is used to set the new version to the same value as the current version',
				},
				{
					name: '--no-commit-hooks',
					description:
						'Do not run git commit hooks when using the npm version command',
				},
				{
					name: '--no-git-tag-version',
					description:
						'Do not tag the commit when using the npm version command',
				},
				{
					name: '--preid',
					description:
						'The \'prerelease identifier\' to use as a prefix for the \'prerelease\' part of a semver. Like the rc in 1.2.0-rc.8',
					args: {
						name: 'prerelease-id',
					},
				},
				{
					name: '--sign-git-tag',
					description:
						'If set to true, then the npm version command will tag the version using -s to add a signature',
				},
			],
		},
		{
			name: ['view', 'v', 'info', 'show'],
			description: 'View registry info',
			options: [...workSpaceOptions, jsonOption],
		},
		{
			name: 'whoami',
			description: 'Display npm username',
			options: [registryOption],
		},
	],
};

export default completionSpec;
