Skip to content

Commit

Permalink
feat(cli): Adding disclaimer for plugin AI install (#3065)
Browse files Browse the repository at this point in the history
* Adding a prerun hook to show AI plugin installation disclaimer

* Moving logic to a plugins:preinstall hook

* Adding tests for the feature

* Updating conditional and adding tests for repo use case

* Adding requested prompt
  • Loading branch information
sbosio authored Oct 30, 2024
1 parent e4aa1c6 commit 80ab352
Show file tree
Hide file tree
Showing 4 changed files with 163 additions and 0 deletions.
1 change: 1 addition & 0 deletions cspell-dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -286,6 +286,7 @@ searchpath
secrettoken
segv
selfsigned
sfdc
shellescape
showenvs
sigints
Expand Down
3 changes: 3 additions & 0 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -317,6 +317,9 @@
"./lib/hooks/init/terms-of-service",
"./lib/hooks/init/performance_analytics"
],
"plugins:preinstall": [
"./lib/hooks/plugins/preinstall/disclaimers"
],
"prerun": [
"./lib/hooks/prerun/analytics"
],
Expand Down
30 changes: 30 additions & 0 deletions packages/cli/src/hooks/plugins/preinstall/disclaimers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import {Hook, ux} from '@oclif/core'

const hook: Hook<'plugins:preinstall'> = async function (options) {
const npmPackageNames = ['@heroku/plugin-ai', '@heroku-cli/plugin-ai']

if (options.plugin.type !== 'npm' || !npmPackageNames.includes(options.plugin.name)) return

ux.warn(
'\n\nThis pilot feature is a Beta Service. You may opt to try such Beta Service in your sole discretion. ' +
'Any use of the Beta Service is subject to the applicable Beta Services Terms provided at ' +
'https://www.salesforce.com/company/legal/customer-agreements/. While use of the pilot feature itself is free, ' +
'to the extent such use consumes a generally available Service, you may be charged for that consumption as set ' +
'forth in the Documentation. Your continued use of this pilot feature constitutes your acceptance of the foregoing.\n\n' +
'For clarity and without limitation, the various third-party machine learning and generative artificial intelligence ' +
'(AI) models and applications (each a “Platform”) integrated with the Beta Service are Non-SFDC Applications, ' +
'as that term is defined in the Beta Services Terms. Note that these third-party Platforms include features that use ' +
'generative AI technology. Due to the nature of generative AI, the output that a Platform generates may be ' +
'unpredictable, and may include inaccurate or harmful responses. Before using any generative AI output, Customer is ' +
'solely responsible for reviewing the output for accuracy, safety, and compliance with applicable laws and third-party ' +
'acceptable use policies. In addition, Customer’s use of each Platform may be subject to the Platform’s own terms and ' +
'conditions, compliance with which Customer is solely responsible.\n',
)

const response = await ux.prompt('Continue? (Y/N)')
if (response.toUpperCase() !== 'Y') {
ux.error('Canceled', {exit: 1})
}
}

export default hook
129 changes: 129 additions & 0 deletions packages/cli/test/unit/hooks/disclaimers.unit.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import {Config, ux} from '@oclif/core'
import {CLIError} from '@oclif/core/lib/errors'
import {expect} from 'chai'
import {join} from 'path'
import * as sinon from 'sinon'
import {stderr} from 'stdout-stderr'

describe('disclaimers ‘plugins:preinstall’ hook', function () {
let config: Config
let sandbox: sinon.SinonSandbox

before(async function () {
config = await Config.load({root: join(__dirname, '../../..')})
sandbox = sinon.createSandbox()
})

afterEach(function () {
sandbox.restore()
})

context('when installing from a Github repository', function () {
it('doesn’t show the disclaimer', async function () {
stderr.start()
config.runHook('plugins:preinstall', {
plugin: {
url: 'https://github.com/heroku/heroku-api-plugin',
type: 'repo',
},
})
stderr.stop()

expect(stderr.output).not.to.include('This pilot feature is a Beta Service.')
})
})

context('when installing a plugin different from ‘@heroku/plugin-ai’ or ‘@heroku-cli/plugin-ai’', function () {
it('doesn’t show the disclaimer', async function () {
stderr.start()
config.runHook('plugins:preinstall', {
plugin: {
name: '@heroku-cli/plugin-events',
tag: 'latest',
type: 'npm',
},
})
stderr.stop()

expect(stderr.output).not.to.include('This pilot feature is a Beta Service.')
})
})

context('when installing the ‘@heroku/plugin-ai’ plugin', function () {
it('shows the disclaimer and prompts the user', async function () {
const promptStub = sandbox.stub(ux, 'prompt').onFirstCall().resolves('y')

stderr.start()
await config.runHook('plugins:preinstall', {
plugin: {
name: '@heroku/plugin-ai',
tag: 'latest',
type: 'npm',
},
})
stderr.stop()

expect(stderr.output).to.include('This pilot feature is a Beta Service.')
expect(promptStub.calledOnce).to.be.true
})

it('cancels installation if customer doesn’t accepts the prompt', async function () {
sandbox.stub(ux, 'prompt').onFirstCall().resolves('n')

stderr.start()
try {
await config.runHook('plugins:preinstall', {
plugin: {
name: '@heroku/plugin-ai',
tag: 'latest',
type: 'npm',
},
})
} catch (error: unknown) {
stderr.stop()
const {message, oclif} = error as CLIError
expect(message).to.equal('Canceled')
expect(oclif.exit).to.equal(1)
}
})
})

context('when installing the ‘@heroku-cli/plugin-ai’ plugin', function () {
it('shows the disclaimer and prompts the user', async function () {
const promptStub = sandbox.stub(ux, 'prompt').onFirstCall().resolves('y')

stderr.start()
await config.runHook('plugins:preinstall', {
plugin: {
name: '@heroku-cli/plugin-ai',
tag: 'latest',
type: 'npm',
},
})
stderr.stop()

expect(stderr.output).to.include('This pilot feature is a Beta Service.')
expect(promptStub.calledOnce).to.be.true
})

it('cancels installation if customer doesn’t accepts the prompt', async function () {
sandbox.stub(ux, 'prompt').onFirstCall().resolves('n')

stderr.start()
try {
await config.runHook('plugins:preinstall', {
plugin: {
name: '@heroku-cli/plugin-ai',
tag: 'latest',
type: 'npm',
},
})
} catch (error: unknown) {
stderr.stop()
const {message, oclif} = error as CLIError
expect(message).to.equal('Canceled')
expect(oclif.exit).to.equal(1)
}
})
})
})

0 comments on commit 80ab352

Please sign in to comment.