heroku 10.8.0-beta.0 → 10.8.1-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.
- package/README.md +1 -0
- package/bin/heroku-prompts.js +235 -0
- package/bin/heroku-repl.js +682 -0
- package/bin/run +35 -7
- package/lib/analytics.js +3 -1
- package/lib/global_telemetry.d.ts +1 -1
- package/lib/global_telemetry.js +3 -1
- package/oclif.manifest.json +1 -1
- package/package.json +8 -5
package/README.md
CHANGED
|
@@ -36,6 +36,7 @@ For other issues, [submit a support ticket](https://help.heroku.com/).
|
|
|
36
36
|
* [`heroku access`](docs/access.md) - manage user access to apps
|
|
37
37
|
* [`heroku accounts`](docs/accounts.md) - list the Heroku accounts in your cache
|
|
38
38
|
* [`heroku addons`](docs/addons.md) - tools and services for developing, extending, and operating your app
|
|
39
|
+
* [`heroku ai`](docs/ai.md) - manage Heroku AI models
|
|
39
40
|
* [`heroku apps`](docs/apps.md) - manage apps on Heroku
|
|
40
41
|
* [`heroku auth`](docs/auth.md) - manage authentication for your Heroku account
|
|
41
42
|
* [`heroku authorizations`](docs/authorizations.md) - OAuth authorizations
|
|
@@ -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, name: flagOrArgName} = commandFlag
|
|
54
|
+
const {choices} = await choicesPrompt(description, [
|
|
55
|
+
{name: 'yes', value: true},
|
|
56
|
+
{name: 'no', value: false},
|
|
57
|
+
], defaultOption)
|
|
58
|
+
|
|
59
|
+
// user cancelled
|
|
60
|
+
if (choices === undefined || choices === 'Cancel') {
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (choices) {
|
|
65
|
+
userInputMap.set(flagOrArgName, {input: true})
|
|
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,682 @@
|
|
|
1
|
+
// do not use the older node:readline module
|
|
2
|
+
// else things will break
|
|
3
|
+
const readline = require('node:readline/promises')
|
|
4
|
+
const yargs = require('yargs-parser')
|
|
5
|
+
const util = require('util')
|
|
6
|
+
const path = require('node:path')
|
|
7
|
+
const fs = require('node:fs')
|
|
8
|
+
const {ux, run} = require('@oclif/core')
|
|
9
|
+
const os = require('node:os')
|
|
10
|
+
const historyFile = path.join(process.env.HOME || process.env.USERPROFILE || os.homedir(), '.heroku_repl_history')
|
|
11
|
+
const stateFile = path.join(process.env.HOME || process.env.USERPROFILE || os.homedir(), '.heroku_repl_state')
|
|
12
|
+
const shellQuote = require('shell-quote')
|
|
13
|
+
|
|
14
|
+
const maxHistory = 1000
|
|
15
|
+
const mcpMode = process.env.HEROKU_MCP_MODE === 'true'
|
|
16
|
+
/**
|
|
17
|
+
* Map of commands used to provide completion
|
|
18
|
+
* data. The key is the flag or arg name to
|
|
19
|
+
* get data for and the value is an array containing
|
|
20
|
+
* the command name and an array of arguments to
|
|
21
|
+
* pass to the command if needed.
|
|
22
|
+
*
|
|
23
|
+
* @example
|
|
24
|
+
* heroku > pipelines:create --app <tab><tab>
|
|
25
|
+
* heroku > spaces:create --team <tab><tab>
|
|
26
|
+
*/
|
|
27
|
+
const completionCommandByName = new Map([
|
|
28
|
+
['app', ['apps', ['--all', '--json']]],
|
|
29
|
+
['org', ['orgs', ['--json']]],
|
|
30
|
+
['team', ['teams', ['--json']]],
|
|
31
|
+
['space', ['spaces', ['--json']]],
|
|
32
|
+
['pipeline', ['pipelines', ['--json']]],
|
|
33
|
+
['addon', ['addons', ['--json']]],
|
|
34
|
+
['domain', ['domains', ['--json']]],
|
|
35
|
+
['dyno', ['ps', ['--json']]],
|
|
36
|
+
['release', ['releases', ['--json']]],
|
|
37
|
+
['stack', ['apps:stacks', ['--json']]],
|
|
38
|
+
])
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Map of completion data by flag or arg name.
|
|
42
|
+
* This is used as a cache for completion data
|
|
43
|
+
* that is retrieved from a remote source.
|
|
44
|
+
*
|
|
45
|
+
* No attempt is made to invalidate these caches
|
|
46
|
+
* at runtime but they are not preserved between
|
|
47
|
+
* sessions.
|
|
48
|
+
*/
|
|
49
|
+
const completionResultsByName = new Map()
|
|
50
|
+
|
|
51
|
+
class HerokuRepl {
|
|
52
|
+
/**
|
|
53
|
+
* The OClif config object containing
|
|
54
|
+
* the command metadata and the means
|
|
55
|
+
* to execute commands
|
|
56
|
+
*/
|
|
57
|
+
#config
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A map of key/value pairs used for
|
|
61
|
+
* the 'set' and 'unset' command
|
|
62
|
+
*/
|
|
63
|
+
#setValues = new Map()
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The history of the REPL commands used
|
|
67
|
+
*/
|
|
68
|
+
#history = []
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The write stream for the history file
|
|
72
|
+
*/
|
|
73
|
+
#historyStream
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* The readline interface used for the REPL
|
|
77
|
+
*/
|
|
78
|
+
#rl
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Constructs a new instance of the HerokuRepl class.
|
|
82
|
+
*
|
|
83
|
+
* @param {Config} config The oclif core config object
|
|
84
|
+
*/
|
|
85
|
+
constructor(config) {
|
|
86
|
+
this.#createInterface()
|
|
87
|
+
this.#config = config
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Prepares the REPL history by loading
|
|
92
|
+
* the previous history from the history file
|
|
93
|
+
* and opening a write stream for new entries.
|
|
94
|
+
*
|
|
95
|
+
* @returns {Promise<void>} a promise that resolves when the history has been loaded
|
|
96
|
+
*/
|
|
97
|
+
#prepareHistory() {
|
|
98
|
+
this.#historyStream = fs.createWriteStream(historyFile, {
|
|
99
|
+
flags: 'a',
|
|
100
|
+
encoding: 'utf8',
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// Load existing history first
|
|
104
|
+
if (fs.existsSync(historyFile)) {
|
|
105
|
+
this.#history = fs.readFileSync(historyFile, 'utf8')
|
|
106
|
+
.split('\n')
|
|
107
|
+
.filter(line => line.trim())
|
|
108
|
+
.reverse()
|
|
109
|
+
.splice(0, maxHistory)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Loads the previous session state from the state file.
|
|
115
|
+
* @returns {void}
|
|
116
|
+
*/
|
|
117
|
+
#loadState() {
|
|
118
|
+
if (fs.existsSync(stateFile)) {
|
|
119
|
+
try {
|
|
120
|
+
const state = JSON.parse(fs.readFileSync(stateFile, 'utf8'))
|
|
121
|
+
for (const entry of Object.entries(state)) {
|
|
122
|
+
this.#updateFlagsByName('set', entry, true)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
process.stdout.write('session restored')
|
|
126
|
+
} catch {
|
|
127
|
+
// noop
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Creates a new readline interface.
|
|
134
|
+
*
|
|
135
|
+
* @returns {readline.Interface} the readline interface
|
|
136
|
+
*/
|
|
137
|
+
#createInterface() {
|
|
138
|
+
this.#rl = readline.createInterface({
|
|
139
|
+
input: process.stdin,
|
|
140
|
+
output: process.stdout,
|
|
141
|
+
prompt: 'heroku > ',
|
|
142
|
+
removeHistoryDuplicates: true,
|
|
143
|
+
historySize: maxHistory,
|
|
144
|
+
completer: async line => {
|
|
145
|
+
if (mcpMode) {
|
|
146
|
+
return [[], line]
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Use shell-quote to tokenize the line for robust parsing
|
|
150
|
+
const tokens = shellQuote.parse(line)
|
|
151
|
+
const stringTokens = tokens.filter(t => typeof t === 'string')
|
|
152
|
+
const [command = '', ...parts] = stringTokens
|
|
153
|
+
if (command === 'set') {
|
|
154
|
+
return this.#buildSetCompletions(parts)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const commandMeta = this.#config.findCommand(command)
|
|
158
|
+
if (!commandMeta) {
|
|
159
|
+
const matches = this.#config.commands.filter(({id}) => id.startsWith(command))
|
|
160
|
+
return [matches.map(({id}) => id).sort(), line]
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return this.#buildCompletions(commandMeta, parts, line)
|
|
164
|
+
},
|
|
165
|
+
})
|
|
166
|
+
this.#prepareHistory()
|
|
167
|
+
this.#loadState()
|
|
168
|
+
|
|
169
|
+
this.#rl.history.push(...this.#history)
|
|
170
|
+
this.#rl.on('line', this.#processLine)
|
|
171
|
+
this.#rl.once('close', () => {
|
|
172
|
+
this.#historyStream?.close()
|
|
173
|
+
fs.writeFileSync(stateFile, JSON.stringify(Object.fromEntries(this.#setValues)), 'utf8')
|
|
174
|
+
})
|
|
175
|
+
this.#rl.prompt()
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Processes the line received from the terminal stdin
|
|
180
|
+
*
|
|
181
|
+
* @param {string} input the line to process
|
|
182
|
+
* @returns {Promise<void>} a promise that resolves when the command has been executed
|
|
183
|
+
*/
|
|
184
|
+
#processLine = async input => {
|
|
185
|
+
if (input.trim() === '') {
|
|
186
|
+
this.#rl.prompt()
|
|
187
|
+
return
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
this.#history.push(input)
|
|
191
|
+
this.#historyStream?.write(input + '\n')
|
|
192
|
+
|
|
193
|
+
const tokens = shellQuote.parse(input)
|
|
194
|
+
const stringTokens = tokens.filter(t => typeof t === 'string')
|
|
195
|
+
// flag/arg extraction
|
|
196
|
+
const {_: [command, ...positionalArgs], ...flags} = yargs(stringTokens, {
|
|
197
|
+
configuration: {
|
|
198
|
+
'camel-case-expansion': false,
|
|
199
|
+
'boolean-negation': false,
|
|
200
|
+
},
|
|
201
|
+
})
|
|
202
|
+
const args = Object.entries(flags).flatMap(([key, value]) => {
|
|
203
|
+
if (typeof value === 'string') {
|
|
204
|
+
return [`--${key}`, value]
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return [`--${key}`]
|
|
208
|
+
}).concat(positionalArgs)
|
|
209
|
+
|
|
210
|
+
if (command === 'exit') {
|
|
211
|
+
process.exit(0)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (command === 'history') {
|
|
215
|
+
process.stdout.write(this.#history.join('\n'))
|
|
216
|
+
this.#rl.prompt()
|
|
217
|
+
return
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (command === 'set' || command === 'unset') {
|
|
221
|
+
this.#updateFlagsByName(command, args)
|
|
222
|
+
this.#rl.prompt()
|
|
223
|
+
return
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const cmd = this.#config.findCommand(command)
|
|
227
|
+
|
|
228
|
+
if (!cmd) {
|
|
229
|
+
console.error(`"${command}" is not a valid command`)
|
|
230
|
+
this.#rl.prompt()
|
|
231
|
+
return
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
try {
|
|
235
|
+
const {flags} = cmd
|
|
236
|
+
for (const [key, value] of this.#setValues) {
|
|
237
|
+
if (Reflect.has(flags, key)) {
|
|
238
|
+
args.push(`--${key}`, value)
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Any commands that prompt the user will cause
|
|
243
|
+
// the REPL to enter an invalid state. We need
|
|
244
|
+
// to pause the readline interface and restore
|
|
245
|
+
// it when the command is done.
|
|
246
|
+
if (process.stdin.isTTY) {
|
|
247
|
+
process.stdin.setRawMode(false)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
this.#rl.close()
|
|
251
|
+
this.#rl.off('line', this.#processLine)
|
|
252
|
+
if (mcpMode) {
|
|
253
|
+
process.stdout.write('<<<BEGIN RESULTS>>>\n')
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
process.argv.length = 2
|
|
257
|
+
process.argv.push(command, ...args.filter(Boolean))
|
|
258
|
+
await run([command, ...args.filter(Boolean)], this.#config)
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (mcpMode) {
|
|
261
|
+
process.stderr.write(`<<<ERROR>>>\n${error.message}\n<<<END ERROR>>>\n`)
|
|
262
|
+
} else {
|
|
263
|
+
console.error(error.message)
|
|
264
|
+
}
|
|
265
|
+
} finally {
|
|
266
|
+
if (process.stdin.isTTY) {
|
|
267
|
+
process.stdin.setRawMode(true)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
if (mcpMode) {
|
|
271
|
+
process.stdout.write('<<<END RESULTS>>>\n')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
this.#createInterface()
|
|
275
|
+
this.start()
|
|
276
|
+
// Force readline to refresh the current line
|
|
277
|
+
this.#rl.write(null, {ctrl: true, name: 'u'})
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Updates the session state based on the command and args.
|
|
283
|
+
*
|
|
284
|
+
* @param {'set'|'unset'} command either 'set' or 'unset'
|
|
285
|
+
* @param {string[]} args an array of arg names
|
|
286
|
+
* @param {boolean} omitConfirmation when false. no confirmation is printed to stdout
|
|
287
|
+
* @returns {void}
|
|
288
|
+
*/
|
|
289
|
+
#updateFlagsByName(command, args, omitConfirmation) {
|
|
290
|
+
if (command === 'set') {
|
|
291
|
+
const [key, value] = args
|
|
292
|
+
if (key && value) {
|
|
293
|
+
this.#setValues.set(key, value)
|
|
294
|
+
|
|
295
|
+
if (!omitConfirmation) {
|
|
296
|
+
process.stdout.write(`setting --${key} to ${value}\n`)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
if (key === 'app') {
|
|
300
|
+
this.#rl.setPrompt(`${value} > `)
|
|
301
|
+
}
|
|
302
|
+
} else {
|
|
303
|
+
const values = []
|
|
304
|
+
for (const [flag, value] of this.#setValues) {
|
|
305
|
+
values.push({flag, value})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (values.length === 0) {
|
|
309
|
+
return console.info('no flags set')
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
ux.table(values, {
|
|
313
|
+
flag: {header: 'Flag'},
|
|
314
|
+
value: {header: 'Value'},
|
|
315
|
+
})
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (command === 'unset') {
|
|
320
|
+
const [key] = args
|
|
321
|
+
|
|
322
|
+
if (!omitConfirmation) {
|
|
323
|
+
process.stdout.write(`unsetting --${key}\n`)
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.#setValues.delete(key)
|
|
327
|
+
if (key === 'app') {
|
|
328
|
+
this.#rl.setPrompt('heroku > ')
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Build completions for a command.
|
|
335
|
+
* The completions are based on the
|
|
336
|
+
* metadata for the command and the
|
|
337
|
+
* user input.
|
|
338
|
+
*
|
|
339
|
+
* @param {Record<string, unknown>} commandMeta the metadata for the command
|
|
340
|
+
* @param {string[]} flagsOrArgs the flags or args for the command
|
|
341
|
+
* @param {string} line the current line
|
|
342
|
+
* @returns {Promise<[string[], string]>} the completions and the current input
|
|
343
|
+
*/
|
|
344
|
+
async #buildCompletions(commandMeta, flagsOrArgs = [], line = '') {
|
|
345
|
+
const {args, flags} = commandMeta
|
|
346
|
+
const {requiredInputs: requiredFlags, optionalInputs: optionalFlags} = this.#collectInputsFromManifest(flags)
|
|
347
|
+
const {requiredInputs: requiredArgs, optionalInputs: optionalArgs} = this.#collectInputsFromManifest(args)
|
|
348
|
+
|
|
349
|
+
const {_: userArgs, ...userFlags} = yargs(flagsOrArgs, {
|
|
350
|
+
configuration: {
|
|
351
|
+
'camel-case-expansion': false,
|
|
352
|
+
'boolean-negation': false,
|
|
353
|
+
},
|
|
354
|
+
})
|
|
355
|
+
const current = flagsOrArgs[flagsOrArgs.length - 1] ?? ''
|
|
356
|
+
|
|
357
|
+
// Order of precedence:
|
|
358
|
+
// 1. Required flags
|
|
359
|
+
// 2. Required args
|
|
360
|
+
// 3. Optional flags
|
|
361
|
+
// 4. Optional args
|
|
362
|
+
// 5. End of line
|
|
363
|
+
// Flags *must* occur first since they may influence
|
|
364
|
+
// the completions for args.
|
|
365
|
+
return await this.#getCompletionsForFlag(line, current, requiredFlags, userFlags, commandMeta) ||
|
|
366
|
+
await this.#getCompletionsForArg(current, requiredArgs, userArgs) ||
|
|
367
|
+
await this.#getCompletionsForFlag(line, current, optionalFlags, userFlags, commandMeta) ||
|
|
368
|
+
await this.#getCompletionsForArg(current, optionalArgs, userArgs) ||
|
|
369
|
+
this.#getCompletionsForEndOfLine(line, flags, userFlags)
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Get completions for a command.
|
|
374
|
+
* The completions are based on the
|
|
375
|
+
* metadata for the command and the
|
|
376
|
+
* user input.
|
|
377
|
+
*
|
|
378
|
+
* @param {[string, string]} parts the parts for a line to get completions for
|
|
379
|
+
* @returns {[string[], string]} the completions and the current input
|
|
380
|
+
*/
|
|
381
|
+
async #buildSetCompletions(parts) {
|
|
382
|
+
const [name, current] = parts
|
|
383
|
+
if (parts.length > 0 && completionCommandByName.has(name)) {
|
|
384
|
+
return [await this.#getCompletion(name, current), current]
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Critical to completions operating as expected;
|
|
388
|
+
// the completions must be filtered to omit keys
|
|
389
|
+
// that do not match our name (if a name exists).
|
|
390
|
+
const completions = [...completionCommandByName.keys()]
|
|
391
|
+
.filter(c => !name || c.startsWith(name))
|
|
392
|
+
|
|
393
|
+
return [completions, name ?? current]
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Get completions for the end of the line.
|
|
398
|
+
*
|
|
399
|
+
* @param {string} line the current line
|
|
400
|
+
* @param {Record<string, unknown>} flags the flags for the command
|
|
401
|
+
* @param {Record<string, unknown>} userFlags the flags that have already been used
|
|
402
|
+
* @returns {[string[], string]} the completions and the current input
|
|
403
|
+
*/
|
|
404
|
+
#getCompletionsForEndOfLine(line, flags, userFlags) {
|
|
405
|
+
const flagKeys = Object.keys(userFlags)
|
|
406
|
+
// If there are no more flags to complete,
|
|
407
|
+
// return an empty array.
|
|
408
|
+
return flagKeys.length < Object.keys(flags).length ? [[line.endsWith(' ') ? '--' : ' --'], ''] : [[], '']
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Get completions for a flag or flag value.
|
|
413
|
+
*
|
|
414
|
+
* @param {string} line the current line
|
|
415
|
+
* @param {string} current the current input
|
|
416
|
+
* @param {string[]} flags the flags for the command
|
|
417
|
+
* @param {string[]} userFlags the flags that have already been used
|
|
418
|
+
* @param {Record<string, unknown>} commandMeta the metadata for the command
|
|
419
|
+
* @return {Promise<[string[], string]>} the completions and the current input
|
|
420
|
+
*/
|
|
421
|
+
async #getCompletionsForFlag(line, current, flags, userFlags, commandMeta) {
|
|
422
|
+
const commandMetaWithCharKeys = {...commandMeta}
|
|
423
|
+
// make sure the commandMeta also contains keys for char fields
|
|
424
|
+
Object.keys(commandMeta.flags).forEach(key => {
|
|
425
|
+
const flag = commandMeta.flags[key]
|
|
426
|
+
if (flag.char) {
|
|
427
|
+
commandMetaWithCharKeys.flags[flag.char] = flag
|
|
428
|
+
}
|
|
429
|
+
})
|
|
430
|
+
// flag completion for long and short flags.
|
|
431
|
+
// flags that have already been used are
|
|
432
|
+
// not included in the completions.
|
|
433
|
+
const isFlag = current.startsWith('-')
|
|
434
|
+
const isLongFlag = current.startsWith('--')
|
|
435
|
+
if (isFlag) {
|
|
436
|
+
const rawFlag = isLongFlag ? current.slice(2) : current.slice(1)
|
|
437
|
+
const prop = isLongFlag ? 'long' : 'short'
|
|
438
|
+
const matched = flags
|
|
439
|
+
.filter(flag => {
|
|
440
|
+
return !Reflect.has(userFlags, flag.short) && !Reflect.has(userFlags, flag.long) &&
|
|
441
|
+
(!rawFlag || flag[prop]?.startsWith(rawFlag))
|
|
442
|
+
})
|
|
443
|
+
.map(f => isLongFlag ? f.long : f.short)
|
|
444
|
+
.filter(Boolean)
|
|
445
|
+
|
|
446
|
+
if (matched?.length > 0) {
|
|
447
|
+
return [matched, rawFlag]
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Does the flag have a value or is it
|
|
452
|
+
// expected to have a value?
|
|
453
|
+
const flagKeys = Object.keys(userFlags)
|
|
454
|
+
const flag = flagKeys[flagKeys.length - 1]
|
|
455
|
+
const isBooleanFlag = commandMetaWithCharKeys.flags[flag]?.type === 'boolean'
|
|
456
|
+
if (this.#isFlagValueComplete(line) || isBooleanFlag || current === '--' || current === '-') {
|
|
457
|
+
return null
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const {options, type, name} = commandMetaWithCharKeys.flags[flag] ?? {}
|
|
461
|
+
// Options are defined in the metadata
|
|
462
|
+
// for the command. If the flag has options
|
|
463
|
+
// defined, we will attempt to complete
|
|
464
|
+
// based on the options.
|
|
465
|
+
if (type === 'option') {
|
|
466
|
+
if (options?.length > 0) {
|
|
467
|
+
const optionComplete = options.includes(current)
|
|
468
|
+
const matched = options.filter(o => o.startsWith(current))
|
|
469
|
+
|
|
470
|
+
if (!optionComplete) {
|
|
471
|
+
return matched.length > 0 ? [matched, current] : [options, current]
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return [await this.#getCompletion(name, isFlag ? '' : current), current]
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/**
|
|
480
|
+
* Get completions for a flag.
|
|
481
|
+
*
|
|
482
|
+
* @param {string} flag the flag to get the completion for
|
|
483
|
+
* @param {string} startsWith the string to match against
|
|
484
|
+
* @returns {Promise<[string[]]>} the completions
|
|
485
|
+
*/
|
|
486
|
+
async #getCompletion(flag, startsWith) {
|
|
487
|
+
// attempt to retrieve the options from the
|
|
488
|
+
// Heroku API. If the options have already
|
|
489
|
+
// been retrieved, they will be cached.
|
|
490
|
+
if (completionCommandByName.has(flag)) {
|
|
491
|
+
let result
|
|
492
|
+
if (completionResultsByName.has(flag)) {
|
|
493
|
+
result = completionResultsByName.get(flag)
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (!result || result.length === 0) {
|
|
497
|
+
const [command, args] = completionCommandByName.get(flag)
|
|
498
|
+
const completionsStr = await this.#captureStdout(() => this.#config.runCommand(command, args)) ?? '[]'
|
|
499
|
+
result = JSON.parse(util.stripVTControlCharacters(completionsStr))
|
|
500
|
+
completionResultsByName.set(flag, result)
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const matched = result
|
|
504
|
+
.map(obj => obj.name ?? obj.id)
|
|
505
|
+
.filter(name => !startsWith || name.startsWith(startsWith))
|
|
506
|
+
.sort()
|
|
507
|
+
|
|
508
|
+
return matched
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
#isFlagValueComplete(input) {
|
|
513
|
+
const tokens = shellQuote.parse(input.trim())
|
|
514
|
+
const len = tokens.length
|
|
515
|
+
if (len === 0) {
|
|
516
|
+
return false
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
const lastToken = tokens[len - 1]
|
|
520
|
+
|
|
521
|
+
// "-" or "--" means the flag name is absent
|
|
522
|
+
if (lastToken === '-' || lastToken === '--') {
|
|
523
|
+
return false
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// back up to the last flag and store the index
|
|
527
|
+
let lastFlagIndex = -1
|
|
528
|
+
for (let i = len - 1; i >= 0; i--) {
|
|
529
|
+
if (typeof tokens[i] === 'string' && tokens[i].startsWith('-')) {
|
|
530
|
+
lastFlagIndex = i
|
|
531
|
+
break
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// No flag, nothing to complete
|
|
536
|
+
if (lastFlagIndex === -1) {
|
|
537
|
+
return true
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// If the last flag is the last token
|
|
541
|
+
// e.g., "run hello.sh --app"
|
|
542
|
+
if (lastFlagIndex === len - 1) {
|
|
543
|
+
return false
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// If the last flag has a value
|
|
547
|
+
if (lastFlagIndex === len - 2) {
|
|
548
|
+
// e.g., "run hello.sh --app heroku-vscode "
|
|
549
|
+
// If input ends with whitespace assume the value is complete
|
|
550
|
+
return /\s$/.test(input)
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// If the last flag is followed by more than one value, treat as complete
|
|
554
|
+
// since the last value is likely to be an argument
|
|
555
|
+
return true
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* Capture stdout by deflecting it to a
|
|
560
|
+
* trap function and returning the output.
|
|
561
|
+
*
|
|
562
|
+
* This is useful for silently capturing the output
|
|
563
|
+
* of a command that normally prints to stdout.
|
|
564
|
+
*
|
|
565
|
+
* @param {CallableFunction} fn the function to capture stdout for
|
|
566
|
+
* @returns {Promise<string>} the output from stdout
|
|
567
|
+
*/
|
|
568
|
+
async #captureStdout(fn) {
|
|
569
|
+
const output = []
|
|
570
|
+
const originalWrite = process.stdout.write
|
|
571
|
+
// Replace stdout.write temporarily
|
|
572
|
+
process.stdout.write = chunk => {
|
|
573
|
+
output.push(typeof chunk === 'string' ? chunk : chunk.toString())
|
|
574
|
+
return true
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
try {
|
|
578
|
+
await fn()
|
|
579
|
+
return output.join('')
|
|
580
|
+
} finally {
|
|
581
|
+
// Restore original stdout
|
|
582
|
+
process.stdout.write = originalWrite
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Get completions for an arg.
|
|
588
|
+
*
|
|
589
|
+
* @param {string} current the current input
|
|
590
|
+
* @param {({long: string}[])} args the args for the command
|
|
591
|
+
* @param {string[]} userArgs the args that have already been used
|
|
592
|
+
* @returns {Promise<[string[], string] | null>} the completions and the current input
|
|
593
|
+
*/
|
|
594
|
+
async #getCompletionsForArg(current, args = [], userArgs = []) {
|
|
595
|
+
if (userArgs.length <= args.length) {
|
|
596
|
+
const arg = args[userArgs.length]
|
|
597
|
+
if (arg) {
|
|
598
|
+
const {long} = arg
|
|
599
|
+
if (completionCommandByName.has(long)) {
|
|
600
|
+
const completions = await this.#getCompletion(long, current)
|
|
601
|
+
if (completions.length > 0) {
|
|
602
|
+
return [completions, current]
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
return [[`<${long}>`], current]
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
return null
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* Collect inputs from the command manifest and sorts
|
|
615
|
+
* them by type and then by required status.
|
|
616
|
+
*
|
|
617
|
+
* @param {Record<string, unknown>} commandMeta the metadata from the command manifest
|
|
618
|
+
* @returns {{requiredInputs: {long: string, short: string}[], optionalInputs: {long: string, short: string}[]}} the inputs from the command manifest
|
|
619
|
+
*/
|
|
620
|
+
#collectInputsFromManifest(commandMeta) {
|
|
621
|
+
const requiredInputs = []
|
|
622
|
+
const optionalInputs = []
|
|
623
|
+
|
|
624
|
+
// Prioritize options over booleans
|
|
625
|
+
const keysByType = Object.keys(commandMeta).sort((a, b) => {
|
|
626
|
+
const {type: aType} = commandMeta[a]
|
|
627
|
+
const {type: bType} = commandMeta[b]
|
|
628
|
+
if (aType === bType) {
|
|
629
|
+
return 0
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
if (aType === 'option') {
|
|
633
|
+
return -1
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
if (bType === 'option') {
|
|
637
|
+
return 1
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
return 0
|
|
641
|
+
})
|
|
642
|
+
const includedFlags = new Set()
|
|
643
|
+
for (const key of keysByType) {
|
|
644
|
+
const {required: isRequired, char: short, name: long} = commandMeta[key]
|
|
645
|
+
if (includedFlags.has(long)) {
|
|
646
|
+
continue
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
includedFlags.add(long)
|
|
650
|
+
if (isRequired) {
|
|
651
|
+
requiredInputs.push({long, short})
|
|
652
|
+
continue
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
optionalInputs.push({long, short})
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
// Prioritize required inputs
|
|
659
|
+
// over optional inputs
|
|
660
|
+
// required inputs are sorted
|
|
661
|
+
// alphabetically. optional
|
|
662
|
+
// inputs are sorted alphabetically
|
|
663
|
+
// and then pushed to the end of
|
|
664
|
+
// the list.
|
|
665
|
+
requiredInputs.sort((a, b) => {
|
|
666
|
+
if (a.long < b.long) {
|
|
667
|
+
return -1
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (a.long > b.long) {
|
|
671
|
+
return 1
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
return 0
|
|
675
|
+
})
|
|
676
|
+
|
|
677
|
+
return {requiredInputs, optionalInputs}
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
module.exports.herokuRepl = async function (config) {
|
|
681
|
+
return new HerokuRepl(config)
|
|
682
|
+
}
|
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,36 @@ 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 (flags.repl && args.length === 0 && Object.keys(flags).length === 1) {
|
|
56
|
+
return herokuRepl(config)
|
|
57
|
+
}
|
|
47
58
|
|
|
48
|
-
|
|
49
|
-
|
|
59
|
+
if (flags.prompt) {
|
|
60
|
+
delete flags.prompt
|
|
61
|
+
await promptUser(config, commandName, args, flags)
|
|
62
|
+
await oclif.run([commandName, ...args], config)
|
|
63
|
+
} else {
|
|
64
|
+
await oclif.run(undefined, config)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
await oclifFlush()
|
|
68
|
+
} catch (error) {
|
|
69
|
+
// capture any errors raised by oclif
|
|
70
|
+
const cliError = error
|
|
71
|
+
cliError.cliRunDuration = globalTelemetry.computeDuration(cliStartTime)
|
|
72
|
+
await globalTelemetry.sendTelemetry(cliError)
|
|
73
|
+
|
|
74
|
+
oclifError(error)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
50
77
|
|
|
78
|
+
main()
|
package/lib/analytics.js
CHANGED
|
@@ -15,6 +15,8 @@ class AnalyticsCommand {
|
|
|
15
15
|
}
|
|
16
16
|
async record(opts) {
|
|
17
17
|
await this.initialize;
|
|
18
|
+
const mcpMode = process.env.HEROKU_MCP_MODE === 'true';
|
|
19
|
+
const mcpServerVersion = process.env.HEROKU_MCP_SERVER_VERSION || 'unknown';
|
|
18
20
|
const plugin = opts.Command.plugin;
|
|
19
21
|
if (!plugin) {
|
|
20
22
|
debug('no plugin found for analytics');
|
|
@@ -29,7 +31,7 @@ class AnalyticsCommand {
|
|
|
29
31
|
cli: this.config.name,
|
|
30
32
|
command: opts.Command.id,
|
|
31
33
|
completion: await this._acAnalytics(opts.Command.id),
|
|
32
|
-
version: this.config.version
|
|
34
|
+
version: `${this.config.version}${mcpMode ? ` (MCP ${mcpServerVersion})` : ''}`,
|
|
33
35
|
plugin: plugin.name,
|
|
34
36
|
plugin_version: plugin.version,
|
|
35
37
|
os: this.config.platform,
|
|
@@ -25,7 +25,7 @@ export declare function initializeInstrumentation(): void;
|
|
|
25
25
|
export declare function setupTelemetry(config: any, opts: any): {
|
|
26
26
|
command: any;
|
|
27
27
|
os: any;
|
|
28
|
-
version:
|
|
28
|
+
version: string;
|
|
29
29
|
exitCode: number;
|
|
30
30
|
exitState: string;
|
|
31
31
|
cliRunDuration: number;
|
package/lib/global_telemetry.js
CHANGED
|
@@ -57,10 +57,12 @@ function setupTelemetry(config, opts) {
|
|
|
57
57
|
const now = new Date();
|
|
58
58
|
const cmdStartTime = now.getTime();
|
|
59
59
|
const isRegularCmd = Boolean(opts.Command);
|
|
60
|
+
const mcpMode = process.env.HEROKU_MCP_MODE === 'true';
|
|
61
|
+
const mcpServerVersion = process.env.HEROKU_MCP_SERVER_VERSION || 'unknown';
|
|
60
62
|
const irregularTelemetryObject = {
|
|
61
63
|
command: opts.id,
|
|
62
64
|
os: config.platform,
|
|
63
|
-
version: config.version
|
|
65
|
+
version: `${config.version}${mcpMode ? ` (MCP ${mcpServerVersion})` : ''}`,
|
|
64
66
|
exitCode: 0,
|
|
65
67
|
exitState: 'successful',
|
|
66
68
|
cliRunDuration: 0,
|
package/oclif.manifest.json
CHANGED
|
@@ -14929,5 +14929,5 @@
|
|
|
14929
14929
|
]
|
|
14930
14930
|
}
|
|
14931
14931
|
},
|
|
14932
|
-
"version": "10.8.
|
|
14932
|
+
"version": "10.8.1-alpha.0"
|
|
14933
14933
|
}
|
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.8.
|
|
4
|
+
"version": "10.8.1-alpha.0",
|
|
5
5
|
"author": "Heroku",
|
|
6
6
|
"bin": "./bin/run",
|
|
7
7
|
"bugs": "https://github.com/heroku/cli/issues",
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
"@heroku/eventsource": "^1.0.7",
|
|
16
16
|
"@heroku/heroku-cli-util": "^9.0.2",
|
|
17
17
|
"@heroku/http-call": "^5.4.0",
|
|
18
|
+
"@heroku/plugin-ai": "^1.0.1",
|
|
18
19
|
"@inquirer/prompts": "^5.0.5",
|
|
19
20
|
"@oclif/core": "^2.16.0",
|
|
20
21
|
"@oclif/plugin-commands": "2.2.28",
|
|
@@ -79,7 +80,8 @@
|
|
|
79
80
|
"validator": "^13.7.0",
|
|
80
81
|
"word-wrap": "^1.2.5",
|
|
81
82
|
"ws": "^6.2.2",
|
|
82
|
-
"yaml": "^2.0.1"
|
|
83
|
+
"yaml": "^2.0.1",
|
|
84
|
+
"yargs-parser": "18.1.3"
|
|
83
85
|
},
|
|
84
86
|
"devDependencies": {
|
|
85
87
|
"@heroku-cli/schema": "^1.0.25",
|
|
@@ -185,7 +187,8 @@
|
|
|
185
187
|
"@oclif/plugin-update",
|
|
186
188
|
"@oclif/plugin-version",
|
|
187
189
|
"@oclif/plugin-warn-if-update-available",
|
|
188
|
-
"@oclif/plugin-which"
|
|
190
|
+
"@oclif/plugin-which",
|
|
191
|
+
"@heroku/plugin-ai"
|
|
189
192
|
],
|
|
190
193
|
"bin": "heroku",
|
|
191
194
|
"dirname": "heroku",
|
|
@@ -388,12 +391,12 @@
|
|
|
388
391
|
"test:acceptance": "yarn pretest && mocha --forbid-only \"test/**/*.acceptance.test.ts\" && node ./bin/bats-test-runner",
|
|
389
392
|
"test:integration": "yarn pretest && mocha --forbid-only \"test/**/*.integration.test.ts\"",
|
|
390
393
|
"test:smoke": "yarn pretest && mocha --forbid-only \"test/**/smoke.acceptance.test.ts\"",
|
|
391
|
-
"test:unit:justTest:local": "
|
|
394
|
+
"test:unit:justTest:local": "mocha \"test/**/*.unit.test.ts\"",
|
|
392
395
|
"test:unit:justTest:ci": "nyc --reporter=lcov --reporter=text-summary mocha --forbid-only \"test/**/*.unit.test.ts\"",
|
|
393
396
|
"test": "yarn pretest && yarn test:unit:justTest:ci",
|
|
394
397
|
"test:local": "yarn pretest && yarn test:unit:justTest:local",
|
|
395
398
|
"version": "oclif readme --multi && git add README.md ../../docs"
|
|
396
399
|
},
|
|
397
400
|
"types": "lib/index.d.ts",
|
|
398
|
-
"gitHead": "
|
|
401
|
+
"gitHead": "9bc0afca647ef869c379b2ac054e798652d5de63"
|
|
399
402
|
}
|