"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const color_1 = require("@heroku-cli/color"); const command_1 = require("@heroku-cli/command"); const cli_ux_1 = require("cli-ux"); const _ = require("lodash"); const quote_1 = require("../../quote"); const edit = require('edit-string'); function configToString(config) { return Object.keys(config) .sort() .map(key => { return `${key}=${quote_1.quote(config[key])}`; }) .join('\n'); } function stringToConfig(s) { return s.split('\n').reduce((config, line) => { const error = () => { throw new Error(`Invalid line: ${line}`); }; if (!line) return config; let i = line.indexOf('='); if (i === -1) error(); config[line.slice(0, i)] = quote_1.parse(line.slice(i + 1)); return config; }, {}); } function allKeys(a, b) { return _.uniq([...Object.keys(a), ...Object.keys(b)].sort()); } function showDiff(from, to) { for (let k of allKeys(from, to)) { if (from[k] === to[k]) continue; if (k in from) { cli_ux_1.cli.log(color_1.color.red(`${k}=${quote_1.quote(from[k])}`)); } if (k in to) { cli_ux_1.cli.log(color_1.color.green(`${k}=${quote_1.quote(to[k])}`)); } } } class ConfigEdit extends command_1.Command { async run() { const { flags: { app }, args: { key } } = this.parse(ConfigEdit); this.app = app; cli_ux_1.cli.action.start('Fetching config'); const original = await this.fetchLatestConfig(); cli_ux_1.cli.action.stop(); let newConfig = Object.assign({}, original); const prefix = `heroku-${app}-config-`; if (key) { newConfig[key] = await edit(original[key] || '', { prefix }); if (!original[key].endsWith('\n') && newConfig[key].endsWith('\n')) newConfig[key] = newConfig[key].slice(0, -1); } else { const s = await edit(configToString(original), { prefix, postfix: '.sh' }); newConfig = stringToConfig(s); } for (let k of Object.keys(newConfig)) { if (!newConfig[k]) delete newConfig[k]; } if (!await this.diffPrompt(original, newConfig)) return; cli_ux_1.cli.action.start('Verifying new config'); await this.verifyUnchanged(original); cli_ux_1.cli.action.start('Updating config'); await this.updateConfig(original, newConfig); cli_ux_1.cli.action.stop(); } async fetchLatestConfig() { const { body: original } = await this.heroku.get(`/apps/${this.app}/config-vars`); return original; } async diffPrompt(original, newConfig) { if (_.isEqual(original, newConfig)) { this.warn('no changes to config'); return false; } cli_ux_1.cli.log(); cli_ux_1.cli.log('Config Diff:'); showDiff(original, newConfig); cli_ux_1.cli.log(); return cli_ux_1.cli.confirm(`Update config on ${color_1.color.app(this.app)} with these values?`); } async verifyUnchanged(original) { const latest = await this.fetchLatestConfig(); if (!_.isEqual(original, latest)) { throw new Error('Config changed on server. Refusing to update.'); } } async updateConfig(original, newConfig) { for (let k of Object.keys(original)) { if (!newConfig[k]) newConfig[k] = null; } await this.heroku.patch(`/apps/${this.app}/config-vars`, { body: newConfig, }); } } ConfigEdit.description = `interactively edit config vars This command opens the app config in a text editor set by $VISUAL or $EDITOR. Any variables added/removed/changed will be updated on the app after saving and closing the file.`; ConfigEdit.examples = [ `# edit with vim $ EDITOR="vim" heroku config:edit`, `# edit with emacs $ EDITOR="emacs" heroku config:edit`, `# edit with pico $ EDITOR="pico" heroku config:edit`, `# edit with atom editor $ VISUAL="atom --wait" heroku config:edit`, ]; ConfigEdit.flags = { app: command_1.flags.app({ required: true }), remote: command_1.flags.remote(), }; ConfigEdit.args = [ { name: 'key', optional: true, description: 'edit a single key' }, ]; exports.default = ConfigEdit;