Skip to content

Commit

Permalink
Merge pull request #7 from heroku/sbosio/ai-docs
Browse files Browse the repository at this point in the history
Adding command 'ai:docs'
  • Loading branch information
sbosio authored Sep 19, 2024
2 parents cc7f4f0 + 513159a commit bf83d07
Show file tree
Hide file tree
Showing 4 changed files with 207 additions and 0 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,22 @@ USAGE
```
# Commands
<!-- commands -->
* [`heroku ai:docs`](#heroku-aidocs)

## `heroku ai:docs`

Opens documentation for Heroku AI in your web browser.

```
USAGE
$ heroku ai:docs [--browser <value>]
FLAGS
--browser=<value> browser to open docs with (example: "firefox", "safari")
DESCRIPTION
Opens documentation for Heroku AI in your web browser.
```

_See code: [dist/commands/ai/docs.ts](https://github.com/heroku/heroku-cli-plugin-integration/blob/v0.0.0/dist/commands/ai/docs.ts)_
<!-- commandsstop -->
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"@heroku-cli/schema": "^1.0.25",
"@oclif/core": "^2.16.0",
"@oclif/plugin-help": "^5",
"open": "^8.4.2",
"tsheredoc": "^1"
},
"devDependencies": {
Expand Down
50 changes: 50 additions & 0 deletions src/commands/ai/docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import color from '@heroku-cli/color'
import {flags} from '@heroku-cli/command'
import {ux} from '@oclif/core'
import {CLIError} from '@oclif/core/lib/errors'
import open from 'open'
import Command from '../../lib/base'

export default class Docs extends Command {
static defaultUrl = 'https://devcenter.heroku.com/articles/ai'
static description = 'Opens documentation for Heroku AI in your web browser.'
static flags = {
browser: flags.string({description: 'browser to open docs with (example: "firefox", "safari")'}),
}

static urlOpener: (...args: Parameters<typeof open>) => ReturnType<typeof open> = open

public async run(): Promise<void> {
const {flags} = await this.parse(Docs)
const browser = flags.browser
const url = process.env.HEROKU_AI_DOCS_URL || Docs.defaultUrl

let browserErrorShown = false
const showBrowserError = (browser?: string) => {
if (browserErrorShown) return

ux.warn(`Unable to open ${browser ? browser : 'your default'} browser. Please visit ${color.cyan(url)} to view the documentation.`)
browserErrorShown = true
}

ux.log(`Opening ${color.cyan(url)} in ${browser ? browser : 'your default'} browser…`)

try {
await ux.anykey(
`Press any key to open up the browser to show Heroku AI documentation, or ${color.yellow('q')} to exit`
)
} catch (error) {
const {message, oclif} = error as CLIError
ux.error(message, {exit: oclif?.exit || 1})
}

const cp = await Docs.urlOpener(url, {wait: false, ...(browser ? {app: {name: browser}} : {})})
cp.on('error', (err: Error) => {
ux.warn(err)
showBrowserError(browser)
})
cp.on('close', (code: number) => {
if (code !== 0) showBrowserError(browser)
})
}
}
139 changes: 139 additions & 0 deletions test/commands/ai/docs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import {ux} from '@oclif/core'
import {expect} from 'chai'
import childProcess from 'node:child_process'
import sinon, {SinonSandbox, SinonStub} from 'sinon'
import {stderr, stdout} from 'stdout-stderr'
import Cmd from '../../../src/commands/ai/docs'
import stripAnsi from '../../helpers/strip-ansi'
import {runCommand} from '../../run-command'

describe('ai:docs', function () {
const {env} = process
let sandbox: SinonSandbox
let urlOpener: SinonStub
let spawnMock: () => any

beforeEach(function () {
process.env = {}
sandbox = sinon.createSandbox()
})

afterEach(function () {
process.env = env
sandbox.restore()
})

context('when the user accepts the prompt to open the browser', function () {
beforeEach(function () {
sandbox.stub(ux, 'anykey').onFirstCall().resolves()
})

describe('attempting to open the browser', function () {
beforeEach(function () {
urlOpener = sandbox.stub(Cmd, 'urlOpener').onFirstCall().resolves({
on: (_: string, _cb: ErrorCallback) => {},
} as unknown as childProcess.ChildProcess)
})

context('without --browser option', function () {
it('shows the URL that will be opened for in the default browser', async function () {
await runCommand(Cmd)

expect(stdout.output).to.contain(`Opening ${Cmd.defaultUrl} in your default browser…`)
})

it('attempts to open the default browser to the Dev Center AI article', async function () {
await runCommand(Cmd)

expect(urlOpener.calledWith(Cmd.defaultUrl, {wait: false})).to.equal(true)
})
})

context('with --browser option', function () {
it('shows the URL that will be opened in the specified browser', async function () {
await runCommand(Cmd, [
'--browser=firefox',
])

expect(stdout.output).to.contain(`Opening ${Cmd.defaultUrl} in firefox browser…`)
})

it('attempts to open the specified browser to the Dev Center AI article', async function () {
await runCommand(Cmd, [
'--browser=firefox',
])

expect(urlOpener.calledWith(Cmd.defaultUrl, {wait: false, app: {name: 'firefox'}})).to.equal(true)
})
})

it('respects HEROKU_AI_DOCS_URL', async function () {
const customUrl = 'https://devcenter.heroku.com/articles/custom-article-url'

process.env = {
HEROKU_AI_DOCS_URL: customUrl,
}

await runCommand(Cmd)

expect(urlOpener.calledWith(customUrl, {wait: false})).to.equal(true)
})
})

context('when there’s an error opening the browser', function () {
beforeEach(function () {
spawnMock = sandbox.stub().returns({
on: (event: string, cb: CallableFunction) => {
if (event === 'error') cb(new Error('error'))
}, unref: () => {},
})
})

it('shows a warning', async function () {
const spawnStub = sandbox.stub(childProcess, 'spawn').callsFake(spawnMock)

await runCommand(Cmd)

expect(spawnStub.calledOnce).to.be.true
expect(stripAnsi(stderr.output)).to.contain('Error: error')
expect(stripAnsi(stderr.output)).to.contain('Warning: Unable to open your default browser.')
expect(stripAnsi(stderr.output)).to.contain(Cmd.defaultUrl)
})
})

context('when the browser closes with a non-zero exit status', function () {
beforeEach(function () {
spawnMock = sandbox.stub().returns({
on: (event: string, cb: CallableFunction) => {
if (event === 'close') cb(1)
}, unref: () => {},
})
})

it('shows a warning', async function () {
const spawnStub = sandbox.stub(childProcess, 'spawn').callsFake(spawnMock)

await runCommand(Cmd)

expect(spawnStub.calledOnce).to.be.true
expect(stripAnsi(stderr.output)).to.contain('Warning: Unable to open your default browser.')
expect(stripAnsi(stderr.output)).to.contain(Cmd.defaultUrl)
})
})
})

context('when the user rejects the prompt to open the browser', function () {
beforeEach(function () {
urlOpener = sandbox.stub(Cmd, 'urlOpener')
sandbox.stub(ux, 'anykey').onFirstCall().rejects(new Error('quit'))
})

it('doesn’t attempt to open the browser', async function () {
try {
await runCommand(Cmd)
} catch {}

expect(urlOpener.notCalled).to.equal(true)
})
})
})

0 comments on commit bf83d07

Please sign in to comment.