diff --git a/packages/cli/package.json b/packages/cli/package.json index d4deefab9e..59effb5357 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", diff --git a/packages/cli/src/commands/doctor/ask.ts b/packages/cli/src/commands/doctor/ask.ts new file mode 100644 index 0000000000..83e91598c9 --- /dev/null +++ b/packages/cli/src/commands/doctor/ask.ts @@ -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('/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) + } + } +} diff --git a/packages/cli/src/commands/doctor/diagnose.ts b/packages/cli/src/commands/doctor/diagnose.ts new file mode 100644 index 0000000000..6d6e0ec72d --- /dev/null +++ b/packages/cli/src/commands/doctor/diagnose.ts @@ -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}`) + } +} diff --git a/packages/cli/src/commands/doctor/recommend.ts b/packages/cli/src/commands/doctor/recommend.ts new file mode 100644 index 0000000000..f63c3c0c8b --- /dev/null +++ b/packages/cli/src/commands/doctor/recommend.ts @@ -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 ', + '$ 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') + } +} diff --git a/packages/cli/src/commands/doctor/vitals.ts b/packages/cli/src/commands/doctor/vitals.ts new file mode 100644 index 0000000000..0bfceaf426 --- /dev/null +++ b/packages/cli/src/commands/doctor/vitals.ts @@ -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) + } +} diff --git a/packages/cli/src/lib/doctor/font/README.md b/packages/cli/src/lib/doctor/font/README.md new file mode 100644 index 0000000000..8203fbd72a --- /dev/null +++ b/packages/cli/src/lib/doctor/font/README.md @@ -0,0 +1 @@ +This is just temporary proof of concept. Actual font should be imported via heroku's cdn. \ No newline at end of file diff --git a/packages/cli/src/lib/doctor/font/heroku.otf b/packages/cli/src/lib/doctor/font/heroku.otf new file mode 100644 index 0000000000..68c339480a Binary files /dev/null and b/packages/cli/src/lib/doctor/font/heroku.otf differ diff --git a/yarn.lock b/yarn.lock index 3abdab80d1..85a0282d33 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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" @@ -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 @@ -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: