/
edit.ts
139 lines (124 loc) · 3.99 KB
/
edit.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
import {color} from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {cli} from 'cli-ux'
import * as _ from 'lodash'
import {parse, quote} from '../../quote'
const edit = require('edit-string')
interface Config {
[key: string]: string
}
interface UploadConfig {
[key: string]: string | null
}
function configToString(config: Config): string {
return Object.keys(config)
.sort()
.map(key => {
return `${key}=${quote(config[key])}`
})
.join('\n')
}
function stringToConfig(s: string): Config {
return s.split('\n').reduce((config: Config, line: string): Config => {
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)] = parse(line.slice(i + 1))
return config
}, {})
}
function allKeys(a: Config, b: Config): string[] {
return _.uniq([...Object.keys(a), ...Object.keys(b)].sort())
}
function showDiff(from: Config, to: Config) {
for (let k of allKeys(from, to)) {
if (from[k] === to[k]) continue
if (k in from) {
cli.log(color.red(`${k}=${quote(from[k])}`))
}
if (k in to) {
cli.log(color.green(`${k}=${quote(to[k])}`))
}
}
}
export default class ConfigEdit extends Command {
static 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.`
static 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`,
]
static flags = {
app: flags.app({required: true}),
remote: flags.remote(),
}
static args = [
{name: 'key', optional: true, description: 'edit a single key'},
]
app!: string
async run() {
const {flags: {app}, args: {key}} = this.parse(ConfigEdit)
this.app = app
cli.action.start('Fetching config')
const original = await this.fetchLatestConfig()
cli.action.stop()
let newConfig = {...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.action.start('Verifying new config')
await this.verifyUnchanged(original)
cli.action.start('Updating config')
await this.updateConfig(original, newConfig)
cli.action.stop()
}
private async fetchLatestConfig() {
const {body: original} = await this.heroku.get<Heroku.ConfigVars>(`/apps/${this.app}/config-vars`)
return original
}
private async diffPrompt(original: Config, newConfig: Config): Promise<boolean> {
if (_.isEqual(original, newConfig)) {
this.warn('no changes to config')
return false
}
cli.log()
cli.log('Config Diff:')
showDiff(original, newConfig)
cli.log()
return cli.confirm(`Update config on ${color.app(this.app)} with these values?`)
}
private async verifyUnchanged(original: Config) {
const latest = await this.fetchLatestConfig()
if (!_.isEqual(original, latest)) {
throw new Error('Config changed on server. Refusing to update.')
}
}
private async updateConfig(original: Config, newConfig: UploadConfig) {
for (let k of Object.keys(original)) {
if (!newConfig[k]) newConfig[k] = null
}
await this.heroku.patch(`/apps/${this.app}/config-vars`, {
body: newConfig,
})
}
}