heroku 10.0.2 → 10.0.3-alpha.1
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/bin/heroku-prompts.js +235 -0
- package/bin/heroku-repl.js +48 -0
- package/bin/run +31 -7
- package/lib/analytics.d.ts +1 -0
- package/lib/analytics.js +2 -1
- package/lib/commands/addons/open.js +1 -1
- package/lib/commands/spaces/destroy.js +16 -2
- package/lib/commands/telemetry/add.js +1 -1
- package/lib/commands/telemetry/index.js +1 -1
- package/lib/hooks/postrun/performance_analytics.js +1 -0
- package/lib/hooks/prerun/analytics.js +1 -1
- package/oclif.manifest.json +675 -676
- package/package.json +7 -6
|
@@ -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,48 @@
|
|
|
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 === '.exit') {
|
|
20
|
+
process.exit(0)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (command.startsWith('set')) {
|
|
24
|
+
flagsByName.set(argv[0], argv[1])
|
|
25
|
+
process.stderr.write(`setting --app to "${argv[1]}"\n$ `)
|
|
26
|
+
continue
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const commandMeta = config.findCommand(command)
|
|
30
|
+
if (!commandMeta) {
|
|
31
|
+
process.stderr.write(`"${command}" not a valid command\n$ `)
|
|
32
|
+
continue
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const {flags} = commandMeta
|
|
37
|
+
if (flags.app && flagsByName.has('app') && !argv?.includes('--app')) {
|
|
38
|
+
argv.push('--app', flagsByName.get('app'))
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
await config.runCommand(command, argv)
|
|
42
|
+
} catch (error) {
|
|
43
|
+
process.stderr.write(error.message)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
process.stderr.write('\n$ ')
|
|
47
|
+
}
|
|
48
|
+
}
|
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
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
51
|
+
const main = async () => {
|
|
52
|
+
try {
|
|
53
|
+
await config.load()
|
|
54
|
+
const {_: [commandName, ...args], ...flags} = yargs
|
|
55
|
+
if (!commandName && !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(undefined, 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
|
-
|
|
49
|
-
}
|
|
70
|
+
oclifError(error)
|
|
71
|
+
}
|
|
72
|
+
};
|
|
50
73
|
|
|
74
|
+
void main();
|
package/lib/analytics.d.ts
CHANGED
|
@@ -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.
|
|
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.
|
|
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
|
-
|
|
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:
|
|
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.
|
|
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
|
-
|
|
11
|
+
Reflect.set(globalThis, 'recordPromise', analytics.record(options));
|
|
12
12
|
};
|
|
13
13
|
exports.default = analytics;
|