diff --git a/lib/commands/config.js b/lib/commands/config.js index 8d74b8d2741e7..e6c0ba79d294f 100644 --- a/lib/commands/config.js +++ b/lib/commands/config.js @@ -51,6 +51,7 @@ class Config extends BaseCommand { 'delete [ ...]', 'list [--json]', 'edit', + 'fix', ] static params = [ @@ -72,7 +73,7 @@ class Config extends BaseCommand { } if (argv.length === 2) { - const cmds = ['get', 'set', 'delete', 'ls', 'rm', 'edit'] + const cmds = ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'fix'] if (opts.partialWord !== 'l') { cmds.push('list') } @@ -97,6 +98,7 @@ class Config extends BaseCommand { case 'edit': case 'list': case 'ls': + case 'fix': default: return [] } @@ -129,6 +131,9 @@ class Config extends BaseCommand { case 'edit': await this.edit() break + case 'fix': + await this.fix() + break default: throw this.usageError() } @@ -240,6 +245,49 @@ ${defData} }) } + async fix () { + let problems + + try { + this.npm.config.validate() + return // if validate doesn't throw we have nothing to do + } catch (err) { + // coverage skipped because we don't need to test rethrowing errors + // istanbul ignore next + if (err.code !== 'ERR_INVALID_AUTH') { + throw err + } + + problems = err.problems + } + + if (!this.npm.config.isDefault('location')) { + problems = problems.filter((problem) => { + return problem.where === this.npm.config.get('location') + }) + } + + this.npm.config.repair(problems) + const locations = [] + + this.npm.output('The following configuration problems have been repaired:\n') + const summary = problems.map(({ action, from, to, key, where }) => { + // coverage disabled for else branch because it is intentionally omitted + // istanbul ignore else + if (action === 'rename') { + // we keep track of which configs were modified here so we know what to save later + locations.push(where) + return `~ \`${from}\` renamed to \`${to}\` in ${where} config` + } else if (action === 'delete') { + locations.push(where) + return `- \`${key}\` deleted from ${where} config` + } + }).join('\n') + this.npm.output(summary) + + return await Promise.all(locations.map((location) => this.npm.config.save(location))) + } + async list () { const msg = [] // long does not have a flattener diff --git a/tap-snapshots/test/lib/docs.js.test.cjs b/tap-snapshots/test/lib/docs.js.test.cjs index 44072441c9ce4..81be4ec21dd49 100644 --- a/tap-snapshots/test/lib/docs.js.test.cjs +++ b/tap-snapshots/test/lib/docs.js.test.cjs @@ -2650,6 +2650,7 @@ npm config get [ [ ...]] npm config delete [ ...] npm config list [--json] npm config edit +npm config fix Options: [--json] [-g|--global] [--editor ] [-L|--location ] @@ -2665,6 +2666,7 @@ npm config get [ [ ...]] npm config delete [ ...] npm config list [--json] npm config edit +npm config fix alias: c \`\`\` diff --git a/test/lib/commands/config.js b/test/lib/commands/config.js index 61e47244e890c..c94e0df1395c8 100644 --- a/test/lib/commands/config.js +++ b/test/lib/commands/config.js @@ -411,6 +411,102 @@ t.test('config edit - editor exits non-0', async t => { ) }) +t.test('config fix', (t) => { + t.test('no problems', async (t) => { + const home = t.testdir({ + '.npmrc': '', + }) + + const sandbox = new Sandbox(t, { home }) + await sandbox.run('config', ['fix']) + t.equal(sandbox.output, '', 'printed nothing') + }) + + t.test('repairs all configs by default', async (t) => { + const root = t.testdir({ + global: { + npmrc: '_authtoken=notatoken\n_authToken=afaketoken', + }, + home: { + '.npmrc': '_authtoken=thisisinvalid\n_auth=beef', + }, + }) + const registry = `//registry.npmjs.org/` + + const sandbox = new Sandbox(t, { + global: join(root, 'global'), + home: join(root, 'home'), + }) + await sandbox.run('config', ['fix']) + + // global config fixes + t.match(sandbox.output, '`_authtoken` deleted from global config', + 'output has deleted global _authtoken') + t.match(sandbox.output, `\`_authToken\` renamed to \`${registry}:_authToken\` in global config`, + 'output has renamed global _authToken') + t.not(sandbox.config.get('_authtoken', 'global'), '_authtoken is not set globally') + t.not(sandbox.config.get('_authToken', 'global'), '_authToken is not set globally') + t.equal(sandbox.config.get(`${registry}:_authToken`, 'global'), 'afaketoken', + 'global _authToken was scoped') + const globalConfig = await readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' }) + t.equal(globalConfig, `${registry}:_authToken=afaketoken\n`, 'global config was written') + + // user config fixes + t.match(sandbox.output, '`_authtoken` deleted from user config', + 'output has deleted user _authtoken') + t.match(sandbox.output, `\`_auth\` renamed to \`${registry}:_auth\` in user config`, + 'output has renamed user _auth') + t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config') + t.not(sandbox.config.get('_auth'), '_auth is not set in user config') + t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped') + const userConfig = await readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' }) + t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written') + }) + + t.test('repairs only the config specified by --location if asked', async (t) => { + const root = t.testdir({ + global: { + npmrc: '_authtoken=notatoken\n_authToken=afaketoken', + }, + home: { + '.npmrc': '_authtoken=thisisinvalid\n_auth=beef', + }, + }) + const registry = `//registry.npmjs.org/` + + const sandbox = new Sandbox(t, { + global: join(root, 'global'), + home: join(root, 'home'), + }) + await sandbox.run('config', ['fix', '--location=user']) + + // global config should be untouched + t.notMatch(sandbox.output, '`_authtoken` deleted from global', + 'output has deleted global _authtoken') + t.notMatch(sandbox.output, `\`_authToken\` renamed to \`${registry}:_authToken\` in global`, + 'output has renamed global _authToken') + t.equal(sandbox.config.get('_authtoken', 'global'), 'notatoken', 'global _authtoken untouched') + t.equal(sandbox.config.get('_authToken', 'global'), 'afaketoken', 'global _authToken untouched') + t.not(sandbox.config.get(`${registry}:_authToken`, 'global'), 'global _authToken not scoped') + const globalConfig = await readFile(join(root, 'global', 'npmrc'), { encoding: 'utf8' }) + t.equal(globalConfig, '_authtoken=notatoken\n_authToken=afaketoken', + 'global config was not written') + + // user config fixes + t.match(sandbox.output, '`_authtoken` deleted from user', + 'output has deleted user _authtoken') + t.match(sandbox.output, `\`_auth\` renamed to \`${registry}:_auth\` in user`, + 'output has renamed user _auth') + t.not(sandbox.config.get('_authtoken', 'user'), '_authtoken is not set in user config') + t.not(sandbox.config.get('_auth', 'user'), '_auth is not set in user config') + t.equal(sandbox.config.get(`${registry}:_auth`, 'user'), 'beef', 'user _auth was scoped') + const userConfig = await readFile(join(root, 'home', '.npmrc'), { encoding: 'utf8' }) + t.equal(userConfig, `${registry}:_auth=beef\n`, 'user config was written') + }) + + t.end() +}) + t.test('completion', async t => { const sandbox = new Sandbox(t) @@ -423,13 +519,14 @@ t.test('completion', async t => { sandbox.reset() } - await testComp([], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'list']) + await testComp([], ['get', 'set', 'delete', 'ls', 'rm', 'edit', 'fix', 'list']) await testComp(['set', 'foo'], []) await testComp(['get'], allKeys) await testComp(['set'], allKeys) await testComp(['delete'], allKeys) await testComp(['rm'], allKeys) await testComp(['edit'], []) + await testComp(['fix'], []) await testComp(['list'], []) await testComp(['ls'], [])