heroku 10.0.2 → 10.0.3-alpha.0

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.
@@ -0,0 +1,235 @@
1
+ const fs = require('node:fs')
2
+ const inquirer = require('inquirer')
3
+
4
+ function choicesPrompt(description, choices, required, defaultValue) {
5
+ return inquirer.prompt([{
6
+ type: 'list',
7
+ name: 'choices',
8
+ message: description,
9
+ choices,
10
+ default: defaultValue,
11
+ validate(input) {
12
+ if (!required || input) {
13
+ return true
14
+ }
15
+
16
+ return `${description} is required`
17
+ },
18
+ }])
19
+ }
20
+
21
+ function prompt(description, required) {
22
+ return inquirer.prompt([{
23
+ type: 'input',
24
+ name: 'input',
25
+ message: description,
26
+ validate(input) {
27
+ if (!required || input.trim()) {
28
+ return true
29
+ }
30
+
31
+ return `${description} is required`
32
+ },
33
+ }])
34
+ }
35
+
36
+ function filePrompt(description, defaultPath) {
37
+ return inquirer.prompt([{
38
+ type: 'input',
39
+ name: 'path',
40
+ message: description,
41
+ default: defaultPath,
42
+ validate(input) {
43
+ if (fs.existsSync(input)) {
44
+ return true
45
+ }
46
+
47
+ return 'File does not exist. Please enter a valid file path.'
48
+ },
49
+ }])
50
+ }
51
+
52
+ const showBooleanPrompt = async (commandFlag, userInputMap, defaultOption) => {
53
+ const {description, default: defaultValue, name: flagOrArgName} = commandFlag
54
+ const choice = await choicesPrompt(description, [
55
+ {name: 'yes', value: true},
56
+ {name: 'no', value: false},
57
+ ], defaultOption)
58
+
59
+ // user cancelled
60
+ if (choice === undefined || choice === 'Cancel') {
61
+ return true
62
+ }
63
+
64
+ if (choice === 'Yes') {
65
+ userInputMap.set(flagOrArgName, defaultValue)
66
+ }
67
+
68
+ return false
69
+ }
70
+
71
+ const showOtherDialog = async (commandFlagOrArg, userInputMap) => {
72
+ const {description, default: defaultValue, options, required, name: flagOrArgName} = commandFlagOrArg
73
+
74
+ let input
75
+ const isFileInput = description?.includes('absolute path')
76
+ if (isFileInput) {
77
+ input = await filePrompt(description, '')
78
+ } else if (options) {
79
+ const choices = options.map(option => ({name: option, value: option}))
80
+ input = await choicesPrompt(`Select the ${description}`, choices, required, defaultValue)
81
+ } else {
82
+ input = await prompt(`${description.slice(0, 1).toUpperCase()}${description.slice(1)} (${required ? 'required' : 'optional - press "Enter" to bypass'})`, required)
83
+ }
84
+
85
+ if (input === undefined) {
86
+ return true
87
+ }
88
+
89
+ if (input !== '') {
90
+ userInputMap.set(flagOrArgName, input)
91
+ }
92
+
93
+ return false
94
+ }
95
+
96
+ function collectInputsFromManifest(flagsOrArgsManifest, omitOptional) {
97
+ const requiredInputs = []
98
+ const optionalInputs = []
99
+
100
+ // Prioritize options over booleans to
101
+ // prevent the user from yo-yo back and
102
+ // forth between the different input dialogs
103
+ const keysByType = Object.keys(flagsOrArgsManifest).sort((a, b) => {
104
+ const {type: aType} = flagsOrArgsManifest[a]
105
+ const {type: bType} = flagsOrArgsManifest[b]
106
+ if (aType === bType) {
107
+ return 0
108
+ }
109
+
110
+ if (aType === 'option') {
111
+ return -1
112
+ }
113
+
114
+ if (bType === 'option') {
115
+ return 1
116
+ }
117
+
118
+ return 0
119
+ })
120
+
121
+ keysByType.forEach(key => {
122
+ const isRequired = Reflect.get(flagsOrArgsManifest[key], 'required');
123
+ (isRequired ? requiredInputs : optionalInputs).push(key)
124
+ })
125
+ // Prioritize required inputs
126
+ // over optional inputs when
127
+ // prompting the user.
128
+ // required inputs are sorted
129
+ // alphabetically. optional
130
+ // inputs are sorted alphabetically
131
+ // and then pushed to the end of
132
+ // the list.
133
+ requiredInputs.sort((a, b) => {
134
+ if (a < b) {
135
+ return -1
136
+ }
137
+
138
+ if (a > b) {
139
+ return 1
140
+ }
141
+
142
+ return 0
143
+ })
144
+ // Include optional only when not explicitly omitted
145
+ return omitOptional ? requiredInputs : [...requiredInputs, ...optionalInputs]
146
+ }
147
+
148
+ async function getInput(flagsOrArgsManifest, userInputMap, omitOptional) {
149
+ const flagsOrArgs = collectInputsFromManifest(flagsOrArgsManifest, omitOptional)
150
+
151
+ for (const flagOrArg of flagsOrArgs) {
152
+ const {name, description, type, hidden} = flagsOrArgsManifest[flagOrArg]
153
+ if (userInputMap.has(name)) {
154
+ continue
155
+ }
156
+
157
+ // hidden args and flags may be exposed later
158
+ // based on the user type. For now, skip them.
159
+ if (!description || hidden) {
160
+ continue
161
+ }
162
+
163
+ const cancelled = await (type === 'boolean' ? showBooleanPrompt : showOtherDialog)(flagsOrArgsManifest[flagOrArg], userInputMap)
164
+ if (cancelled) {
165
+ return true
166
+ }
167
+ }
168
+
169
+ return false
170
+ }
171
+
172
+ async function promptForInputs(commandName, commandManifest, userArgs, userFlags) {
173
+ const {args, flags} = commandManifest
174
+
175
+ const userInputByArg = new Map()
176
+ Object.keys(args).forEach((argKey, index) => {
177
+ if (userArgs[index]) {
178
+ userInputByArg.set(argKey, userArgs[index])
179
+ }
180
+ })
181
+
182
+ let cancelled = await getInput(args, userInputByArg)
183
+ if (cancelled) {
184
+ return {userInputByArg}
185
+ }
186
+
187
+ const userInputByFlag = new Map()
188
+ Object.keys(flags).forEach(flagKey => {
189
+ const {name, char} = flags[flagKey]
190
+ if (userFlags[name] || userFlags[char]) {
191
+ userInputByFlag.set(flagKey, userFlags[flagKey])
192
+ }
193
+ })
194
+ cancelled = await getInput(flags, userInputByFlag)
195
+ if (cancelled) {
196
+ return
197
+ }
198
+
199
+ return {userInputByArg, userInputByFlag}
200
+ }
201
+
202
+ module.exports.promptUser = async (config, commandName, args, flags) => {
203
+ const commandMeta = config.findCommand(commandName)
204
+ if (!commandMeta) {
205
+ process.stderr.write(`"${commandName}" not a valid command\n$ `)
206
+ return
207
+ }
208
+
209
+ const {userInputByArg, userInputByFlag} = await promptForInputs(commandName, commandMeta, args, flags)
210
+
211
+ try {
212
+ for (const [, {input: argValue}] of userInputByArg) {
213
+ if (argValue) {
214
+ args.push(argValue)
215
+ }
216
+ }
217
+
218
+ for (const [flagName, {input: flagValue}] of userInputByFlag) {
219
+ if (!flagValue) {
220
+ continue
221
+ }
222
+
223
+ if (flagValue === true) {
224
+ args.push(`--${flagName}`)
225
+ continue
226
+ }
227
+
228
+ args.push(`--${flagName}`, flagValue)
229
+ }
230
+
231
+ return args
232
+ } catch (error) {
233
+ process.stderr.write(error.message)
234
+ }
235
+ }
@@ -0,0 +1,44 @@
1
+ const {Config} = require('@oclif/core')
2
+ const root = require.resolve('../package.json')
3
+ const config = new Config({root})
4
+ const flagsByName = new Map()
5
+ async function * commandGenerator() {
6
+ while (true) {
7
+ const argv = await new Promise(resolve => {
8
+ process.stdin.once('data', resolve)
9
+ })
10
+ yield argv.toString().trim().split(' ')
11
+ }
12
+ }
13
+
14
+ module.exports.herokuRepl = async function (config) {
15
+ process.stderr.write('Welcome to the Heroku Terminal!\n$ ')
16
+
17
+ for await (const input of commandGenerator()) {
18
+ const [command, ...argv] = input
19
+ if (command.startsWith('set')) {
20
+ flagsByName.set(argv[0], argv[1])
21
+ process.stderr.write(`setting --app to "${argv[1]}"\n$ `)
22
+ continue
23
+ }
24
+
25
+ const commandMeta = config.findCommand(command)
26
+ if (!commandMeta) {
27
+ process.stderr.write(`"${command}" not a valid command\n$ `)
28
+ continue
29
+ }
30
+
31
+ try {
32
+ const {flags} = commandMeta
33
+ if (flags.app && flagsByName.has('app') && !argv?.includes('--app')) {
34
+ argv.push('--app', flagsByName.get('app'))
35
+ }
36
+
37
+ await config.runCommand(command, argv)
38
+ } catch (error) {
39
+ process.stderr.write(error.message)
40
+ }
41
+
42
+ process.stderr.write('\n$ ')
43
+ }
44
+ }
package/bin/run CHANGED
@@ -1,10 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
 
3
+ const {Config} = require('@oclif/core')
4
+ const root = require.resolve('../package.json')
5
+ const config = new Config({root})
6
+
3
7
  process.env.HEROKU_UPDATE_INSTRUCTIONS = process.env.HEROKU_UPDATE_INSTRUCTIONS || 'update with: "npm update -g heroku"'
4
8
 
5
9
  const now = new Date()
6
10
  const cliStartTime = now.getTime()
7
11
  const globalTelemetry = require('../lib/global_telemetry')
12
+ const yargs = require('yargs-parser')(process.argv.slice(2))
8
13
 
9
14
  process.once('beforeExit', async code => {
10
15
  // capture as successful exit
@@ -38,13 +43,32 @@ process.on('SIGTERM', async () => {
38
43
  globalTelemetry.initializeInstrumentation()
39
44
 
40
45
  const oclif = require('@oclif/core')
46
+ const oclifFlush = require('@oclif/core/flush')
47
+ const oclifError = require('@oclif/core/handle')
48
+ const { promptUser } = require('./heroku-prompts')
49
+ const { herokuRepl } = require('./heroku-repl')
41
50
 
42
- oclif.run().then(require('@oclif/core/flush')).catch(async error => {
43
- // capture any errors raised by oclif
44
- const cliError = error
45
- cliError.cliRunDuration = globalTelemetry.computeDuration(cliStartTime)
46
- await globalTelemetry.sendTelemetry(cliError)
51
+ const main = async () => {
52
+ try {
53
+ await config.load()
54
+ const {_: [commandName, ...args], ...flags} = yargs
55
+ if (!args.length && !Object.keys(flags).length) {
56
+ return await herokuRepl(config)
57
+ }
58
+ if (flags.prompt) {
59
+ delete flags.prompt
60
+ await promptUser(config, commandName, args, flags)
61
+ }
62
+ await oclif.run([commandName, ...args], config)
63
+ await oclifFlush()
64
+ } catch (error) {
65
+ // capture any errors raised by oclif
66
+ const cliError = error
67
+ cliError.cliRunDuration = globalTelemetry.computeDuration(cliStartTime)
68
+ await globalTelemetry.sendTelemetry(cliError)
47
69
 
48
- return require('@oclif/core/handle')(error)
49
- })
70
+ oclifError(error)
71
+ }
72
+ };
50
73
 
74
+ void main();
@@ -25,6 +25,7 @@ export default class AnalyticsCommand {
25
25
  config: Interfaces.Config;
26
26
  userConfig: typeof deps.UserConfig.prototype;
27
27
  http: typeof deps.HTTP;
28
+ initialize: Promise<void>;
28
29
  constructor(config: Interfaces.Config);
29
30
  record(opts: RecordOpts): Promise<any>;
30
31
  get url(): string;
package/lib/analytics.js CHANGED
@@ -11,9 +11,10 @@ class AnalyticsCommand {
11
11
  this.http = deps_1.default.HTTP.create({
12
12
  headers: { 'user-agent': config.userAgent },
13
13
  });
14
+ this.initialize = this.init();
14
15
  }
15
16
  async record(opts) {
16
- await this.init();
17
+ await this.initialize;
17
18
  const plugin = opts.Command.plugin;
18
19
  if (!plugin) {
19
20
  debug('no plugin found for analytics');
@@ -53,7 +53,7 @@ class Open extends command_1.Command {
53
53
  const sso = await this.heroku.request(`/apps/${app}/addons/${args.addon}/sso`, {
54
54
  method: 'GET',
55
55
  headers: {
56
- Accept: 'application/vnd.heroku+json; version=3.add-ons-sso',
56
+ Accept: 'application/vnd.heroku+json; version=3.sdk',
57
57
  },
58
58
  });
59
59
  const { method, action } = sso.body;
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const core_1 = require("@oclif/core");
4
- const color_1 = require("@heroku-cli/color");
5
4
  const command_1 = require("@heroku-cli/command");
6
5
  const tsheredoc_1 = require("tsheredoc");
7
6
  const confirmCommand_1 = require("../../lib/confirmCommand");
8
7
  const spaces_1 = require("../../lib/spaces/spaces");
8
+ const color_1 = require("@heroku-cli/color");
9
9
  class Destroy extends command_1.Command {
10
10
  async run() {
11
+ var _a, _b;
11
12
  const { flags, args } = await this.parse(Destroy);
12
13
  const { confirm } = flags;
13
14
  const spaceName = flags.space || args.space;
@@ -22,7 +23,20 @@ class Destroy extends command_1.Command {
22
23
  if (space.state === 'allocated') {
23
24
  ({ body: space.outbound_ips } = await this.heroku.get(`/spaces/${spaceName}/nat`));
24
25
  if (space.outbound_ips && space.outbound_ips.state === 'enabled') {
25
- natWarning = `The Outbound IPs for this space will be reused!\nEnsure that external services no longer allow these Outbound IPs: ${(0, spaces_1.displayNat)(space.outbound_ips)}\n`;
26
+ const ipv6 = ((_a = space.generation) === null || _a === void 0 ? void 0 : _a.name) === 'fir' ? ' and IPv6' : '';
27
+ natWarning = (0, tsheredoc_1.default) `
28
+ ${color_1.default.dim('===')} ${color_1.default.bold('WARNING: Outbound IPs Will Be Reused')}
29
+ ${color_1.default.yellow(`⚠️ Deleting this space frees up the following outbound IPv4${ipv6} IPs for reuse:`)}
30
+ ${color_1.default.bold((_b = (0, spaces_1.displayNat)(space.outbound_ips)) !== null && _b !== void 0 ? _b : '')}
31
+
32
+ ${color_1.default.dim('Update the following configurations:')}
33
+ ${color_1.default.dim('=')} IP allowlists
34
+ ${color_1.default.dim('=')} Firewall rules
35
+ ${color_1.default.dim('=')} Security group configurations
36
+ ${color_1.default.dim('=')} Network ACLs
37
+
38
+ ${color_1.default.yellow(`Ensure that you remove the listed IPv4${ipv6} addresses from your security configurations.`)}
39
+ `;
26
40
  }
27
41
  }
28
42
  await (0, confirmCommand_1.default)(spaceName, confirm, `Destructive Action\nThis command will destroy the space ${color_1.default.bold.red(spaceName)}\n${natWarning}\n`);
@@ -29,7 +29,7 @@ class Add extends command_1.Command {
29
29
  signals: (0, util_1.validateAndFormatSignals)(signals),
30
30
  exporter: {
31
31
  endpoint,
32
- type: `otlp${transport}`,
32
+ type: (transport === 'grpc') ? 'otlp' : 'otlphttp',
33
33
  headers: JSON.parse(exporterHeaders),
34
34
  },
35
35
  };
@@ -44,6 +44,6 @@ Index.topic = 'telemetry';
44
44
  Index.description = 'list telemetry drains';
45
45
  Index.flags = {
46
46
  space: command_1.flags.string({ char: 's', description: 'filter by space name', exactlyOne: ['app', 'space'] }),
47
- app: command_1.flags.app({ description: 'filter by app name' }),
47
+ app: command_1.flags.string({ description: 'filter by app name' }),
48
48
  };
49
49
  Index.example = '$ heroku telemetry';
@@ -8,5 +8,6 @@ const performance_analytics = async function () {
8
8
  const cmdStartTime = global.cliTelemetry.commandRunDuration;
9
9
  global.cliTelemetry.commandRunDuration = telemetry.computeDuration(cmdStartTime);
10
10
  global.cliTelemetry.lifecycleHookCompletion.postrun = true;
11
+ await Reflect.get(globalThis, 'recordPromise');
11
12
  };
12
13
  exports.default = performance_analytics;
@@ -8,6 +8,6 @@ const analytics = async function (options) {
8
8
  }
9
9
  global.cliTelemetry = telemetry.setupTelemetry(this.config, options);
10
10
  const analytics = new analytics_1.default(this.config);
11
- await analytics.record(options);
11
+ Reflect.set(globalThis, 'recordPromise', analytics.record(options));
12
12
  };
13
13
  exports.default = analytics;
@@ -10078,7 +10078,6 @@
10078
10078
  "type": "option"
10079
10079
  },
10080
10080
  "app": {
10081
- "char": "a",
10082
10081
  "description": "filter by app name",
10083
10082
  "name": "app",
10084
10083
  "hasDynamicHelp": false,
@@ -14451,5 +14450,5 @@
14451
14450
  ]
14452
14451
  }
14453
14452
  },
14454
- "version": "10.0.2"
14453
+ "version": "10.0.3-alpha.0"
14455
14454
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "heroku",
3
3
  "description": "CLI to interact with Heroku",
4
- "version": "10.0.2",
4
+ "version": "10.0.3-alpha.0",
5
5
  "author": "Heroku",
6
6
  "bin": "./bin/run",
7
7
  "bugs": "https://github.com/heroku/cli/issues",
@@ -78,7 +78,8 @@
78
78
  "urijs": "^1.19.11",
79
79
  "validator": "^13.7.0",
80
80
  "word-wrap": "^1.2.5",
81
- "ws": "^6.2.2"
81
+ "ws": "^6.2.2",
82
+ "yargs-parser": "18.1.3"
82
83
  },
83
84
  "devDependencies": {
84
85
  "@heroku-cli/schema": "^1.0.25",
@@ -92,9 +93,9 @@
92
93
  "@types/glob": "^7.1.1",
93
94
  "@types/inquirer": "^8.2.10",
94
95
  "@types/lodash": "^4.14.123",
95
- "@types/mocha": "^10.0.6",
96
+ "@types/mocha": "^10.0.10",
96
97
  "@types/node": "20.14.8",
97
- "@types/node-fetch": "^2.1.6",
98
+ "@types/node-fetch": "^2.6.11",
98
99
  "@types/phoenix": "^1.4.0",
99
100
  "@types/proxyquire": "^1.3.28",
100
101
  "@types/psl": "^1.1.3",
@@ -117,7 +118,7 @@
117
118
  "globby": "^10.0.2",
118
119
  "lodash": "^4.17.11",
119
120
  "lolex": "^3.1.0",
120
- "mocha": "^9.2.2",
121
+ "mocha": "^10.8.2",
121
122
  "nock": "^13.5.1",
122
123
  "nyc": "^15.1.0",
123
124
  "oclif": "4.14.36",
@@ -390,5 +391,5 @@
390
391
  "version": "oclif readme --multi && git add README.md ../../docs"
391
392
  },
392
393
  "types": "lib/index.d.ts",
393
- "gitHead": "7947ef4d8b09b952f87632711df6675e41d14c98"
394
+ "gitHead": "6ea0baa82814848385ba6fd07363c39a748f285e"
394
395
  }