@heroku/heroku-cli-util 8.0.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,3 @@
1
+ Copyright (c) 2016, Heroku
2
+ Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
3
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,269 @@
1
+ # Heroku CLI Utilities
2
+
3
+ ![GitHub Actions CI](https://github.com/heroku/heroku-cli-util/actions/workflows/ci.yml/badge.svg)
4
+ [![npm version](https://badge.fury.io/js/heroku-cli-util.svg)](http://badge.fury.io/js/heroku-cli-util)
5
+ [![License](https://img.shields.io/github/license/heroku/heroku-cli-util.svg)](https://github.com/heroku/heroku-cli-util/blob/master/LICENSE)
6
+
7
+ Set of helpful CLI utilities
8
+
9
+ ## Installation
10
+
11
+ ```sh
12
+ npm install heroku-cli-util --save
13
+ ```
14
+
15
+ ## Action
16
+
17
+ ```js
18
+ let cli = require('heroku-cli-util');
19
+ await cli.action('restarting dynos', async function() {
20
+ let app = await heroku.get(`/apps/${context.app}`);
21
+ await heroku.request({method: 'DELETE', path: `/apps/${app.name}/dynos`});
22
+ });
23
+
24
+ // restarting dynos... done
25
+ ```
26
+
27
+ ## Prompt
28
+
29
+ ```js
30
+ let cli = require('heroku-cli-util');
31
+ let email = await cli.prompt('email', {});
32
+ console.log(`your email is: ${email}`);
33
+ ```
34
+
35
+ **cli.prompt options**
36
+
37
+ ```js
38
+ cli.prompt('email', {
39
+ mask: true, // mask input field after submitting
40
+ hide: true // mask characters while entering
41
+ });
42
+ ```
43
+
44
+ ## Confirm App
45
+
46
+ Supports the same async styles as `prompt()`. Errors if not confirmed.
47
+
48
+ Basic
49
+
50
+ ```js
51
+ let cli = require('heroku-cli-util');
52
+ await cli.confirmApp('appname', context.flags.confirm);
53
+
54
+ // ! WARNING: Destructive Action
55
+ // ! This command will affect the app appname
56
+ // ! To proceed, type appname or re-run this command with --confirm appname
57
+
58
+ > appname
59
+ ```
60
+
61
+ Custom message
62
+
63
+ ```js
64
+ let cli = require('heroku-cli-util');
65
+ await cli.confirmApp('appname', context.flags.confirm, 'foo');
66
+
67
+ // ! foo
68
+ // ! To proceed, type appname or re-run this command with --confirm appname
69
+
70
+ > appname
71
+ ```
72
+
73
+ Note that you will still need to define a `confirm` flag for your command.
74
+
75
+ ## Errors
76
+
77
+ ```js
78
+ let cli = require('heroku-cli-util');
79
+ cli.error("App not found");
80
+ // ! App not found
81
+ ```
82
+
83
+ ## Warnings
84
+
85
+ ```js
86
+ let cli = require('heroku-cli-util');
87
+ cli.warn("App not found");
88
+ // ! App not found
89
+ ```
90
+
91
+ ## Dates
92
+
93
+ ```js
94
+ let cli = require('heroku-cli-util');
95
+ let d = new Date();
96
+ console.log(cli.formatDate(d));
97
+ // 2001-01-01T08:00:00.000Z
98
+ ```
99
+
100
+ ## Hush
101
+
102
+ Use hush for verbose logging when `HEROKU_DEBUG=1`.
103
+
104
+ ```js
105
+ let cli = require('heroku-cli-util');
106
+ cli.hush('foo');
107
+ // only prints if HEROKU_DEBUG is set
108
+ ```
109
+
110
+ ## Debug
111
+
112
+ Pretty print an object.
113
+
114
+ ```js
115
+ let cli = require('heroku-cli-util');
116
+ cli.debug({foo: [1,2,3]});
117
+ // { foo: [ 1, 2, 3 ] }
118
+ ```
119
+
120
+ ## Stylized output
121
+
122
+ Pretty print a header, hash, and JSON
123
+ ```js
124
+ let cli = require('heroku-cli-util');
125
+ cli.styledHeader("MyApp");
126
+ cli.styledHash({name: "myapp", collaborators: ["user1@example.com", "user2@example.com"]});
127
+ cli.styledJSON({name: "myapp"});
128
+ ```
129
+
130
+ Produces
131
+
132
+ ```
133
+ === MyApp
134
+ Collaborators: user1@example.com
135
+ user1@example.com
136
+ Name: myapp
137
+
138
+ {
139
+ "name": "myapp"
140
+ }
141
+ ```
142
+
143
+ ## Table
144
+
145
+ ```js
146
+ cli.table([
147
+ {app: 'first-app', language: 'ruby', dyno_count: 3},
148
+ {app: 'second-app', language: 'node', dyno_count: 2},
149
+ ], {
150
+ columns: [
151
+ {key: 'app'},
152
+ {key: 'dyno_count', label: 'Dyno Count'},
153
+ {key: 'language', format: language => cli.color.red(language)},
154
+ ]
155
+ });
156
+ ```
157
+
158
+ Produces:
159
+
160
+ ```
161
+ app Dyno Count language
162
+ ────────── ────────── ────────
163
+ first-app 3 ruby
164
+ second-app 2 node
165
+ ```
166
+
167
+ ## Linewrap
168
+
169
+ Used to indent output with wrapping around words:
170
+
171
+ ```js
172
+ cli.log(cli.linewrap(2, 10, 'this is text is longer than 10 characters'));
173
+ // Outputs:
174
+ //
175
+ // this
176
+ // text is
177
+ // longer
178
+ // than 10
179
+ // characters`);
180
+ ```
181
+
182
+ Useful with `process.stdout.columns || 80`.
183
+
184
+ ## Open Web Browser
185
+
186
+ ```js
187
+ await cli.open('https://github.com');
188
+ ```
189
+
190
+ ## HTTP calls
191
+
192
+ `heroku-cli-util` includes an instance of [got](https://www.npmjs.com/package/got) that will correctly use HTTP proxies.
193
+
194
+ ```js
195
+ let cli = require('heroku-cli-util');
196
+ let rsp = await cli.got('https://google.com');
197
+ ```
198
+
199
+ ## Mocking
200
+
201
+ Mock stdout and stderr by using `cli.log()` and `cli.error()`.
202
+
203
+ ```js
204
+ let cli = require('heroku-cli-util');
205
+ cli.log('message 1'); // prints 'message 1'
206
+ cli.mockConsole();
207
+ cli.log('message 2'); // prints nothing
208
+ cli.stdout.should.eq('message 2\n');
209
+ ```
210
+
211
+ ## Command
212
+
213
+ Used for initializing a plugin command.
214
+ give you an auth'ed instance of `heroku-client` and cleanly handle API exceptions.
215
+
216
+ It expects you to return a promise chain. This is usually done with [co](https://github.com/tj/co).
217
+
218
+ ```js
219
+ let cli = require('heroku-cli-util');
220
+ let co = require('co');
221
+ module.exports.commands = [
222
+ {
223
+ topic: 'apps',
224
+ command: 'info',
225
+ needsAuth: true,
226
+ needsApp: true,
227
+ run: cli.command(async function (context, heroku) {
228
+ let app = await heroku.get(`/apps/${context.app}`);
229
+ console.dir(app);
230
+ })
231
+ }
232
+ ];
233
+ ```
234
+
235
+ With options:
236
+
237
+ ```js
238
+ let cli = require('heroku-cli-util');
239
+ let co = require('co');
240
+ module.exports.commands = [
241
+ {
242
+ topic: 'apps',
243
+ command: 'info',
244
+ needsAuth: true,
245
+ needsApp: true,
246
+ run: cli.command(
247
+ {preauth: true},
248
+ async function (context, heroku) {
249
+ let app = await heroku.get(`/apps/${context.app}`);
250
+ console.dir(app);
251
+ }
252
+ )
253
+ }
254
+ ];
255
+ ```
256
+
257
+ If the command has a `two_factor` API error, it will ask the user for a 2fa code and retry.
258
+ If you set `preauth: true` it will preauth against the current app instead of just setting the header on an app. (This is necessary if you need to do more than 1 API call that will require 2fa)
259
+
260
+ ## Tests
261
+
262
+ ```sh
263
+ npm install
264
+ npm test
265
+ ```
266
+
267
+ ## License
268
+
269
+ ISC
package/index.js ADDED
@@ -0,0 +1,40 @@
1
+ 'use strict'
2
+
3
+ exports.color = require('@heroku-cli/color').default
4
+
5
+ var console = require('./lib/console')
6
+ var errors = require('./lib/errors')
7
+ var prompt = require('./lib/prompt')
8
+ var styled = require('./lib/styled')
9
+
10
+ exports.hush = console.hush
11
+ exports.log = console.log.bind(console)
12
+ exports.formatDate = require('./lib/date').formatDate
13
+ exports.error = errors.error
14
+ exports.action = require('./lib/action')
15
+ exports.warn = exports.action.warn
16
+ exports.errorHandler = errors.errorHandler
17
+ exports.console = console
18
+ exports.yubikey = require('./lib/yubikey')
19
+ exports.prompt = prompt.prompt
20
+ exports.confirmApp = prompt.confirmApp
21
+ exports.preauth = require('./lib/preauth')
22
+ exports.command = require('./lib/command')
23
+ exports.debug = console.debug
24
+ exports.mockConsole = console.mock
25
+ exports.table = require('./lib/table')
26
+ exports.stdout = ''
27
+ exports.stderr = ''
28
+ exports.styledHeader = styled.styledHeader
29
+ exports.styledObject = styled.styledObject
30
+ exports.styledHash = styled.styledObject
31
+ exports.styledNameValues = styled.styledNameValues
32
+ exports.styledJSON = styled.styledJSON
33
+ exports.open = require('./lib/open')
34
+ exports.got = require('./lib/got')
35
+ exports.linewrap = require('./lib/linewrap')
36
+ exports.Spinner = require('./lib/spinner')
37
+ exports.exit = require('./lib/exit').exit
38
+ exports.auth = require('./lib/auth')
39
+ exports.login = exports.auth.login
40
+ exports.logout = exports.auth.logout
package/lib/action.js ADDED
@@ -0,0 +1,54 @@
1
+ 'use strict'
2
+
3
+ let cli = require('..')
4
+ let errors = require('./errors')
5
+
6
+ function start (message, options) {
7
+ if (!options) options = {}
8
+ module.exports.task = {
9
+ spinner: new cli.Spinner({ spinner: options.spinner, text: `${message}...` }),
10
+ stream: options.stream
11
+ }
12
+ module.exports.task.spinner.start()
13
+ }
14
+
15
+ function action (message, options, promise) {
16
+ if (options.then) [options, promise] = [{}, options]
17
+ start(message, options)
18
+ return promise.then(function (result) {
19
+ if (options.success !== false) done(options.success || 'done', options)
20
+ else done(null, options)
21
+ return result
22
+ }).catch(function (err) {
23
+ if (err.body && err.body.id === 'two_factor') done(cli.color.yellow.bold('!'), options)
24
+ else done(cli.color.red.bold('!'), options)
25
+ throw err
26
+ })
27
+ }
28
+
29
+ function warn (msg) {
30
+ let task = module.exports.task
31
+ if (task) task.spinner.warn(msg)
32
+ else errors.warn(msg)
33
+ }
34
+
35
+ function status (status) {
36
+ let task = module.exports.task
37
+ if (task) task.spinner.status = status
38
+ }
39
+
40
+ function done (msg, options) {
41
+ options = options || {}
42
+ let task = module.exports.task
43
+ if (task) {
44
+ task.spinner.stop(msg)
45
+ module.exports.task = null
46
+ if (options.clear) task.spinner.clear()
47
+ }
48
+ }
49
+
50
+ module.exports = action
51
+ module.exports.start = start
52
+ module.exports.warn = warn
53
+ module.exports.status = status
54
+ module.exports.done = done
package/lib/auth.js ADDED
@@ -0,0 +1,207 @@
1
+ 'use strict'
2
+
3
+ const cli = require('..')
4
+ const vars = require('./vars')
5
+
6
+ function basicAuth (username, password) {
7
+ let auth = [username, password].join(':')
8
+ auth = Buffer.from(auth).toString('base64')
9
+ return `Basic ${auth}`
10
+ }
11
+
12
+ function createOAuthToken (username, password, expiresIn, secondFactor) {
13
+ const os = require('os')
14
+
15
+ let headers = {
16
+ Authorization: basicAuth(username, password)
17
+ }
18
+
19
+ if (secondFactor) headers['Heroku-Two-Factor-Code'] = secondFactor
20
+
21
+ return cli.heroku.post('/oauth/authorizations', {
22
+ headers,
23
+ body: {
24
+ scope: ['global'],
25
+ description: `Heroku CLI login from ${os.hostname()} at ${new Date()}`,
26
+ expires_in: expiresIn || 60 * 60 * 24 * 365 // 1 year
27
+ }
28
+ }).then(function (auth) {
29
+ return { token: auth.access_token.token, email: auth.user.email, expires_in: auth.access_token.expires_in }
30
+ })
31
+ }
32
+
33
+ function saveToken ({ email, token }) {
34
+ const netrc = require('netrc-parser').default
35
+ netrc.loadSync()
36
+ const hosts = [vars.apiHost, vars.httpGitHost]
37
+ hosts.forEach(host => {
38
+ if (!netrc.machines[host]) netrc.machines[host] = {}
39
+ netrc.machines[host].login = email
40
+ netrc.machines[host].password = token
41
+ })
42
+ if (netrc.machines._tokens) {
43
+ netrc.machines._tokens.forEach(token => {
44
+ if (hosts.includes(token.host)) {
45
+ token.internalWhitespace = '\n '
46
+ }
47
+ })
48
+ }
49
+ netrc.saveSync()
50
+ }
51
+
52
+ async function loginUserPass ({ save, expires_in: expiresIn }) {
53
+ const { prompt } = require('./prompt')
54
+
55
+ cli.log('Enter your Heroku credentials:')
56
+ let email = await prompt('Email')
57
+ let password = await prompt('Password', { hide: true })
58
+
59
+ let auth
60
+ try {
61
+ auth = await createOAuthToken(email, password, expiresIn)
62
+ } catch (err) {
63
+ if (!err.body || err.body.id !== 'two_factor') throw err
64
+ let secondFactor = await prompt('Two-factor code', { mask: true })
65
+ auth = await createOAuthToken(email, password, expiresIn, secondFactor)
66
+ }
67
+ if (save) saveToken(auth)
68
+ return auth
69
+ }
70
+
71
+ async function loginSSO ({ save, browser }) {
72
+ const { prompt } = require('./prompt')
73
+
74
+ let url = process.env['SSO_URL']
75
+ if (!url) {
76
+ let org = process.env['HEROKU_ORGANIZATION']
77
+ if (!org) {
78
+ org = await prompt('Enter your organization name')
79
+ }
80
+ url = `https://sso.heroku.com/saml/${encodeURIComponent(org)}/init?cli=true`
81
+ }
82
+
83
+ const open = require('./open')
84
+
85
+ let openError
86
+ await cli.action('Opening browser for login', open(url, browser)
87
+ .catch(function (err) {
88
+ openError = err
89
+ })
90
+ )
91
+
92
+ if (openError) {
93
+ cli.console.error(openError.message)
94
+ }
95
+
96
+ let token = await prompt('Enter your access token (typing will be hidden)', { hide: true })
97
+
98
+ let account = await cli.heroku.get('/account', {
99
+ headers: {
100
+ Authorization: `Bearer ${token}`
101
+ }
102
+ })
103
+
104
+ if (save) saveToken({ token, email: account.email })
105
+ return { token: token, email: account.email }
106
+ }
107
+
108
+ async function logout () {
109
+ let token = cli.heroku.options.token
110
+ if (token) {
111
+ // for SSO logins we delete the session since those do not show up in
112
+ // authorizations because they are created a trusted client
113
+ let sessionsP = cli.heroku.delete('/oauth/sessions/~')
114
+ .catch(err => {
115
+ if (err.statusCode === 404 && err.body && err.body.id === 'not_found' && err.body.resource === 'session') {
116
+ return null
117
+ }
118
+ if (err.statusCode === 401 && err.body && err.body.id === 'unauthorized') {
119
+ return null
120
+ }
121
+ throw err
122
+ })
123
+
124
+ // grab the default authorization because that is the token shown in the
125
+ // dashboard as API Key and they may be using it for something else and we
126
+ // would unwittingly break an integration that they are depending on
127
+ let defaultAuthorizationP = cli.heroku.get('/oauth/authorizations/~')
128
+ .catch(err => {
129
+ if (err.statusCode === 404 && err.body && err.body.id === 'not_found' && err.body.resource === 'authorization') {
130
+ return null
131
+ }
132
+ if (err.statusCode === 401 && err.body && err.body.id === 'unauthorized') {
133
+ return null
134
+ }
135
+ throw err
136
+ })
137
+
138
+ // grab all the authorizations so that we can delete the token they are
139
+ // using in the CLI. we have to do this rather than delete ~ because
140
+ // the ~ is the API Key, not the authorization that is currently requesting
141
+ let authorizationsP = cli.heroku.get('/oauth/authorizations')
142
+ .catch(err => {
143
+ if (err.statusCode === 401 && err.body && err.body.id === 'unauthorized') {
144
+ return []
145
+ }
146
+ throw err
147
+ })
148
+
149
+ let [, defaultAuthorization, authorizations] = await Promise.all([sessionsP, defaultAuthorizationP, authorizationsP])
150
+
151
+ if (accessToken(defaultAuthorization) !== token) {
152
+ for (let authorization of authorizations) {
153
+ if (accessToken(authorization) === token) {
154
+ // remove the matching access token from core services
155
+ await cli.heroku.delete(`/oauth/authorizations/${authorization.id}`)
156
+ }
157
+ }
158
+ }
159
+ }
160
+
161
+ const netrc = require('netrc-parser').default
162
+ netrc.loadSync()
163
+ if (netrc.machines[vars.apiHost]) {
164
+ netrc.machines[vars.apiHost] = undefined
165
+ }
166
+ if (netrc.machines[vars.httpGitHost]) {
167
+ netrc.machines[vars.httpGitHost] = undefined
168
+ }
169
+ netrc.saveSync()
170
+ }
171
+
172
+ function accessToken (authorization) {
173
+ return authorization && authorization.access_token && authorization.access_token.token
174
+ }
175
+
176
+ async function login (options = {}) {
177
+ if (!options.skipLogout) await logout()
178
+
179
+ try {
180
+ if (options['sso']) {
181
+ return await loginSSO(options)
182
+ } else {
183
+ return await loginUserPass(options)
184
+ }
185
+ } catch (e) {
186
+ const { PromptMaskError } = require('./prompt')
187
+ const os = require('os')
188
+ if (e instanceof PromptMaskError && os.platform() === 'win32') {
189
+ throw new PromptMaskError('Login is currently incompatible with git bash/Cygwin/MinGW')
190
+ } else {
191
+ throw e
192
+ }
193
+ }
194
+ }
195
+
196
+ function token () {
197
+ const netrc = require('netrc-parser').default
198
+ netrc.loadSync()
199
+ if (process.env.HEROKU_API_KEY) return process.env.HEROKU_API_KEY
200
+ return netrc.machines[vars.apiHost] && netrc.machines[vars.apiHost].password
201
+ }
202
+
203
+ module.exports = {
204
+ login: login,
205
+ logout: logout,
206
+ token
207
+ }