Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): add cli doctor #2564

Draft
wants to merge 26 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
eeed11b
WIP create vitals command
zwhitfield3 Nov 29, 2023
9039368
WIP add color scheme to output
zwhitfield3 Nov 29, 2023
958a3d7
WIP add os & cli version
zwhitfield3 Nov 29, 2023
09b8be6
WIP get node version locally
zwhitfield3 Nov 29, 2023
9d9600f
WIP add functions to retrieve necessary information
zwhitfield3 Nov 29, 2023
55b8dad
WIP add install path
zwhitfield3 Nov 30, 2023
696c554
Add install path logic
zwhitfield3 Nov 30, 2023
f68b04d
Add getInstalledPlugins logic
zwhitfield3 Nov 30, 2023
3dad51c
Remove console.log noise
zwhitfield3 Nov 30, 2023
a6e17eb
Update herokuUp variable naming
zwhitfield3 Nov 30, 2023
bd7caad
Code clean up
zwhitfield3 Nov 30, 2023
930469a
Bold heroku status
zwhitfield3 Nov 30, 2023
fd7f67b
Add proxy logic to getLocalProxySettings
zwhitfield3 Nov 30, 2023
1887903
Update masking logic
zwhitfield3 Nov 30, 2023
b0b79fd
Clean up code
zwhitfield3 Nov 30, 2023
da6ef3c
Add copy-results flag
zwhitfield3 Nov 30, 2023
81732a2
Add logic for copying results to clipboard
zwhitfield3 Nov 30, 2023
69ee2de
Add logic for copying results to clipboard part 2
zwhitfield3 Dec 1, 2023
dd3a36b
Successfully add copyToClipboard functionality
zwhitfield3 Dec 1, 2023
900dd16
Update unmask flag description
zwhitfield3 Dec 1, 2023
75d18ac
Add ask, diagnose, and recommend commands w/ temp font lib
zwhitfield3 Dec 1, 2023
8755225
WIP add json logic
zwhitfield3 Dec 1, 2023
dc1ecf6
Update herokAI responses
zwhitfield3 Dec 1, 2023
c3f5548
WIP add logic for first pass on diagnose command
zwhitfield3 Dec 1, 2023
f7219f9
WIP add logic for first pass on recommend command
zwhitfield3 Dec 1, 2023
7519cc6
Update copy-results output
zwhitfield3 Dec 1, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"ansi-escapes": "3.2.0",
"async-file": "^2.0.2",
"chalk": "^2.4.2",
"copy-paste": "^1.5.3",
"date-fns": "^2.30.0",
"debug": "4.1.1",
"dotenv": "^16.3.1",
Expand Down
39 changes: 39 additions & 0 deletions packages/cli/src/commands/doctor/ask.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'
// const Font = require('ascii-art-font')
// Font.fontPath = '../../../lib/doctor/font'

export default class DoctorAsk extends Command {
static description = 'recieve responses from HerokAI'
static topic = 'doctor'

static flags = {
interactive: flags.boolean({description: 'use interactive mode with HerokAI', required: false}),
json: flags.boolean({description: 'display doctor:ask input/output as json', required: false}),
}

static args = {
question: Args.string({description: 'question to ask HerokAI', required: true}),
}

async run() {
const {args, flags} = await this.parse(DoctorAsk)
const {body: user} = await this.heroku.get<Heroku.Account>('/account', {retryAuth: false})
const userName = (user && user.name) ? ` ${user.name}` : ''
const herokAIResponse = `${color.heroku(`${color.bold(`Hi${userName},`)} \n\nI'm just a concept right now. Remember? Maybe you can get some buy in during the demo?`)}`
const herokAIJsonResponse = `Hi${userName}, I'm just a concept right now. Remember? Maybe you can get some buy in during the demo?`

const dialogue = {
question: args.question,
response: herokAIJsonResponse,
}

if (flags.json) {
ux.styledJSON(dialogue)
} else {
ux.log(herokAIResponse)
}
}
}
42 changes: 42 additions & 0 deletions packages/cli/src/commands/doctor/diagnose.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'
import * as lodash from 'lodash'
const clipboard = require('copy-paste')
const {exec} = require('child_process')
const {promisify} = require('util')
const execAsync = promisify(exec)

export default class DoctorDiagnose extends Command {
static description = 'check the heroku cli build for errors'
static topic = 'doctor'

static args = {
command: Args.string({description: 'command to check for errors', required: false}),
}

static flags = {
all: flags.boolean({description: 'check all commands for errors', required: false, char: 'A'}),
build: flags.boolean({description: 'check entire heroku cli build for errors', required: false, char: 'b'}),
'copy-results': flags.boolean({description: 'copies results to clipboard', required: false, char: 'p'}),
json: flags.boolean({description: 'display as json', required: false}),
}

async run() {
const {args, flags} = await this.parse(DoctorDiagnose)
const errorMessage = 'H23'
const stackMessage = 'some/crazy/looking/stack/message'

ux.action.start(`${color.heroku(`Running diagnostics on ${color.cyan(`${args.command}`)}`)}`)
ux.action.stop()
ux.action.start(`${color.heroku(`Writing up report on ${color.cyan(`${args.command}`)}`)}`)
ux.action.stop()

ux.log('\n')
ux.log(`${color.bold(`${color.heroku('Report')}`)}`)
ux.log(`${color.bold(`${color.heroku('-------------------------------------------')}`)}`)
ux.log(`${color.cyan('Error:')} ${errorMessage}`)
ux.log(`${color.cyan('Stack:')} ${stackMessage}`)
}
}
42 changes: 42 additions & 0 deletions packages/cli/src/commands/doctor/recommend.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'
import * as lodash from 'lodash'
const clipboard = require('copy-paste')
const {exec} = require('child_process')
const {promisify} = require('util')
const execAsync = promisify(exec)

export default class DoctorRecommend extends Command {
static description = 'recieve the latest tips, general playbooks, and resources when encountering cli issues'
static topic = 'doctor'
static examples = [
'$ heroku doctor:recommend <command>',
'$ heroku doctor:recommend --type command "Command will not show output"',
'$ heroku doctor:recommend --type install "Cli is erroring during install"',
'$ heroku doctor:recommend --type error "I get an error when running..."',
'$ heroku doctor:recommend --type network "I can not push my latest release"',
'$ heroku doctor:recommend --type permissions "I cannot get access to..."',
]

static args = {
statement: Args.string({description: 'statement of problem user is enountering', required: true}),
}

static flags = {
type: flags.string({description: 'type of help required', required: false}),
'copy-results': flags.boolean({description: 'copies results to clipboard', required: false}),
}

async run() {
const {flags} = await this.parse(DoctorRecommend)
ux.log(`${color.bold(`${color.heroku('Recommendations')}`)}`)
ux.log(`${color.bold(`${color.heroku('-------------------------------------------')}`)}`)
ux.log(`- Visit ${color.cyan('"https://devcenter.heroku.com/articles/heroku-cli"')} for more install information`)
ux.log('- Try reinstalling the heroku cli')
ux.log(`- Try running ${color.cyan('"$ heroku doctor:diagnose"')}`)
ux.log(`- Try running ${color.cyan('"$ heroku doctor:ask"')}`)
ux.log('- Check which version of the heroku cli your are running')
}
}
127 changes: 127 additions & 0 deletions packages/cli/src/commands/doctor/vitals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import color from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import * as Heroku from '@heroku-cli/schema'
import {ux} from '@oclif/core'
import * as lodash from 'lodash'
const clipboard = require('copy-paste')
const {exec} = require('child_process')
const {promisify} = require('util')
const execAsync = promisify(exec)

const getLocalNodeVersion = async () => {
const {stdout} = await execAsync('node -v')
return stdout
}

const getInstallMethod = () => {
return 'brew'
}

const getInstallLocation = async () => {
const {stdout} = await execAsync('which heroku')
const formattedOutput = stdout.replace(/\n/g, '')
return formattedOutput
}

const getLocalProxySettings = async (unmasked = false) => {
const command = `httpsProxy=$(scutil --proxy | awk -F': ' '/HTTPSProxy/ {print $2}')

# Check if HTTPSProxy has a value
if [ -n "$httpsProxy" ]; then
echo "$httpsProxy"
else
echo "no proxy set"
fi`

const {stdout} = await execAsync(command)
const hasProxySet = !stdout.includes('no proxy set')

if (unmasked) {
return stdout
}

return hasProxySet ? 'xxxxx\n' : stdout
}

const getInstalledPLugins = async () => {
const {stdout} = await execAsync('heroku plugins')
return stdout
}

const getHerokuStatus = async () => {
const {stdout} = await execAsync('heroku status')
return stdout
}

const copyToClipboard = async (value: any) => {
clipboard.copy(value)
}

export default class DoctorVitals extends Command {
static description = 'list local user setup for debugging'
static topic = 'doctor'

static flags = {
unmask: flags.boolean({description: 'unmasks fields heroku has deemed potentially sensitive', required: false}),
'copy-results': flags.boolean({description: 'copies results to clipboard', required: false}),
json: flags.boolean({description: 'display as json', required: false}),
}

async run() {
const {flags} = await this.parse(DoctorVitals)
const copyResults = flags['copy-results']
const time = new Date()
const dateChecked = time.toISOString().split('T')[0]
const cliInstallMethod = getInstallMethod()
const cliInstallLocation = await getInstallLocation()
const os = this.config.platform
const cliVersion = `v${this.config.version}`
const nodeVersion = await getLocalNodeVersion()
const networkConfig = {
httpsProxy: await getLocalProxySettings(flags.unmask),
}
const installedPlugins = await getInstalledPLugins()
const herokuStatus = await getHerokuStatus()

const isHerokuUp = true
let copiedResults = ''

ux.styledHeader(`${color.heroku('Heroku CLI Doctor')} · ${color.cyan(`User Local Setup on ${dateChecked}`)}`)
ux.log(`${color.cyan('CLI Install Method:')} ${cliInstallMethod}`)
ux.log(`${color.cyan('CLI Install Location:')} ${cliInstallLocation}`)
ux.log(`${color.cyan('OS:')} ${os}`)
ux.log(`${color.cyan('Heroku CLI Version:')} ${cliVersion}`)
ux.log(`${color.cyan('Node Version:')} ${nodeVersion}`)

ux.log(`${color.cyan('Network Config')}`)
ux.log(`HTTPSProxy: ${networkConfig.httpsProxy}`)

ux.log(`${color.cyan('Installed Plugins')}`)
ux.log(`${installedPlugins}`)

ux.log(`${color.bold(color.heroku('Heroku Status'))}`)
ux.log(`${color.bold(color.heroku('----------------------------------------'))}`)
ux.log(isHerokuUp ? color.green(herokuStatus) : color.red(herokuStatus))

if (copyResults) {
// copy results to clipboard here
copiedResults += `Heroku CLI Doctor · User Local Setup on ${dateChecked}\n`
copiedResults += `CLI Install Method: ${cliInstallMethod}\n`
copiedResults += `CLI Install Location: ${cliInstallLocation}\n`
copiedResults += `OS: ${os}\n`
copiedResults += `Heroku CLI Version: ${cliVersion}\n`
copiedResults += `Node Version: ${nodeVersion}\n`
copiedResults += 'Network Config\n'
copiedResults += `HTTPSProxy: ${networkConfig.httpsProxy}\n`
copiedResults += 'Installed Plugins\n'
copiedResults += `${installedPlugins}\n`
copiedResults += 'Heroku Status\n'
copiedResults += '----------------------------------------\n'
copiedResults += herokuStatus

ux.log(`\n${color.bold(`${color.heroku('Results copied to clipboard!')}`)}`)
}

await copyToClipboard(copiedResults)
}
}
1 change: 1 addition & 0 deletions packages/cli/src/lib/doctor/font/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is just temporary proof of concept. Actual font should be imported via heroku's cdn.
Binary file added packages/cli/src/lib/doctor/font/heroku.otf
Binary file not shown.
12 changes: 11 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6215,6 +6215,15 @@ __metadata:
languageName: node
linkType: hard

"copy-paste@npm:^1.5.3":
version: 1.5.3
resolution: "copy-paste@npm:1.5.3"
dependencies:
iconv-lite: ^0.4.8
checksum: 97fac26ea222478e93f2177fdb29f4e90687698a600a128032aa96f15956afe45d03b4c226756264d9266ddee39926f177992cc1809cbfe659c622275095eddd
languageName: node
linkType: hard

"core-util-is@npm:1.0.2, core-util-is@npm:~1.0.0":
version: 1.0.2
resolution: "core-util-is@npm:1.0.2"
Expand Down Expand Up @@ -9317,6 +9326,7 @@ __metadata:
bats: ^1.1.0
chai: ^4.2.0
chalk: ^2.4.2
copy-paste: ^1.5.3
date-fns: ^2.30.0
debug: 4.1.1
dotenv: ^16.3.1
Expand Down Expand Up @@ -9564,7 +9574,7 @@ __metadata:
languageName: node
linkType: hard

"iconv-lite@npm:^0.4.17, iconv-lite@npm:^0.4.24":
"iconv-lite@npm:^0.4.17, iconv-lite@npm:^0.4.24, iconv-lite@npm:^0.4.8":
version: 0.4.24
resolution: "iconv-lite@npm:0.4.24"
dependencies:
Expand Down
Loading