Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
340 changes: 139 additions & 201 deletions package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
"@heroku-cli/schema": "^1.0.25",
"@heroku/buildpack-registry": "^1.0.1",
"@heroku/eventsource": "^1.0.7",
"@heroku/heroku-cli-util": "^10.2.0",
"@heroku/heroku-cli-util": "10.3.0",
"@heroku/http-call": "^5.5.0",
"@heroku/mcp-server": "1.0.7-alpha.1",
"@heroku/plugin-ai": "^1.0.1",
Expand Down Expand Up @@ -116,6 +116,7 @@
"@types/uuid": "^8.3.0",
"@types/write-json-file": "^3.2.1",
"@types/ws": "^6.0.1",
"ansis": "^4",
"bats": "^1.1.0",
"chai": "^4.4.1",
"chai-as-promised": "^7.1.1",
Expand Down
26 changes: 14 additions & 12 deletions packages/cli/src/commands/access/add.ts
Original file line number Diff line number Diff line change
@@ -1,32 +1,34 @@
import {color} from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import {isTeamApp, getOwner} from '../../lib/teamUtils.js'
import {Args, ux} from '@oclif/core'

import {getOwner, isTeamApp} from '../../lib/teamUtils.js'

export default class AccessAdd extends Command {
static description = 'add new users to your app'
static flags = {
app: flags.app({required: true}),
remote: flags.remote({char: 'r'}),
permissions: flags.string({char: 'p', description: 'list of permissions comma separated'}),
static args = {
email: Args.string({description: 'email address of the team member', required: true}),
}

static description = 'add new users to your app'

static examples = [
'$ heroku access:add user@email.com --app APP # add a collaborator to your app',
'$ heroku access:add user@email.com --app APP --permissions deploy,manage,operate # permissions must be comma separated',
]

static args = {
email: Args.string({required: true, description: 'email address of the team member'}),
static flags = {
app: flags.app({required: true}),
permissions: flags.string({char: 'p', description: 'list of permissions comma separated'}),
remote: flags.remote({char: 'r'}),
}

public async run(): Promise<void> {
const {flags, args} = await this.parse(AccessAdd)
const {args, flags} = await this.parse(AccessAdd)
const {email} = args
const {app: appName, permissions} = flags
const {body: appInfo} = await this.heroku.get<Heroku.App>(`/apps/${appName}`)
let output = `Adding ${color.cyan(email)} access to the app ${color.magenta(appName)}`
let output = `Adding ${color.cyan(email)} access to the app ${color.app(appName)}`
let teamFeatures: Heroku.TeamFeature[] = []
if (isTeamApp(appInfo?.owner?.email)) {
const teamName = getOwner(appInfo?.owner?.email)
Expand All @@ -43,7 +45,7 @@ export default class AccessAdd extends Command {
output += ` with ${color.green(permissionsArraySorted.join(', '))} permissions`
ux.action.start(output)
await this.heroku.post<Heroku.TeamAppCollaborator[]>(`/teams/apps/${appName}/collaborators`, {
body: {user: email, permissions: permissionsArraySorted},
body: {permissions: permissionsArraySorted, user: email},
})
ux.action.stop()
} else {
Expand Down
9 changes: 5 additions & 4 deletions packages/cli/src/commands/access/remove.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,27 @@

import {color} from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import {ux} from '@oclif/core'

export default class AccessRemove extends Command {
static description = 'remove users from a team app'
static example = '$ heroku access:remove user@email.com --app APP'
static topic = 'access'
static flags = {
app: flags.app({required: true}),
remote: flags.remote({char: 'r'}),
}

static strict = false

static topic = 'access'

public async run(): Promise<void> {
const {flags, argv} = await this.parse(AccessRemove)
const {argv, flags} = await this.parse(AccessRemove)
const {app} = flags
const email = argv[0] as string
const appName = app
ux.action.start(`Removing ${color.cyan(email)} access from the app ${color.magenta(appName)}`)
ux.action.start(`Removing ${color.cyan(email)} access from the app ${color.app(appName)}`)
await this.heroku.delete<Heroku.Collaborator>(`/apps/${appName}/collaborators/${email}`)
ux.action.stop()
}
Expand Down
29 changes: 16 additions & 13 deletions packages/cli/src/commands/addons/attach.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,30 @@
import {color} from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'

import {trapConfirmationRequired} from '../../lib/addons/util.js'

export default class Attach extends Command {
static topic = 'addons'
static args = {
addon_name: Args.string({description: 'unique identifier or globally unique name of the add-on', required: true}),
}

static description = 'attach an existing add-on resource to an app'

static flags = {
app: flags.app({required: true}),
as: flags.string({description: 'name for add-on attachment'}),
credential: flags.string({description: 'credential name for scoped access to Heroku Postgres'}),
confirm: flags.string({description: 'overwrite existing add-on attachment with same name'}),
app: flags.app({required: true}),
credential: flags.string({description: 'credential name for scoped access to Heroku Postgres'}),
remote: flags.remote(),
}

static args = {
addon_name: Args.string({required: true, description: 'unique identifier or globally unique name of the add-on'}),
}
static topic = 'addons'

public async run(): Promise<void> {
const {flags, args} = await this.parse(Attach)
const {app, credential, as, confirm} = flags
const {args, flags} = await this.parse(Attach)
const {app, as, confirm, credential} = flags
const {body: addon} = await this.heroku.get<Heroku.AddOn>(`/addons/${encodeURIComponent(args.addon_name)}`)
const createAttachment = async (confirmed?: string) => {
let namespace: string | undefined
Expand All @@ -30,10 +33,10 @@ export default class Attach extends Command {
}

const body = {
name: as, app: {name: app}, addon: {name: addon.name}, confirm: confirmed, namespace,
addon: {name: addon.name}, app: {name: app}, confirm: confirmed, name: as, namespace,
}

ux.action.start(`Attaching ${credential ? color.yellow(credential) + ' of ' : ''}${color.yellow(addon.name || '')}${as ? ' as ' + color.cyan(as) : ''} to ${color.magenta(app)}`)
ux.action.start(`Attaching ${credential ? color.yellow(credential) + ' of ' : ''}${color.yellow(addon.name || '')}${as ? ' as ' + color.cyan(as) : ''} to ${color.app(app)}`)
const {body: attachments} = await this.heroku.post<Heroku.AddOnAttachment>('/addon-attachments', {body})
ux.action.stop()
return attachments
Expand All @@ -47,9 +50,9 @@ export default class Attach extends Command {
}

const attachment = await trapConfirmationRequired<Heroku.AddOnAttachment>(app, confirm, (confirmed?: string) => createAttachment(confirmed))
ux.action.start(`Setting ${color.cyan(attachment.name || '')} config vars and restarting ${color.magenta(app)}`)
ux.action.start(`Setting ${color.cyan(attachment.name || '')} config vars and restarting ${color.app(app)}`)
const {body: releases} = await this.heroku.get<Heroku.Release[]>(`/apps/${app}/releases`, {
partial: true, headers: {Range: 'version ..; max=1, order=desc'},
headers: {Range: 'version ..; max=1, order=desc'}, partial: true,
})
ux.action.stop(`done, v${releases[0].version}`)
}
Expand Down
38 changes: 21 additions & 17 deletions packages/cli/src/commands/addons/destroy.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,48 @@
import {color} from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import notify from '../../lib/notify.js'
import ConfirmCommand from '../../lib/confirmCommand.js'
import {Args} from '@oclif/core'
import _ from 'lodash'

import destroyAddon from '../../lib/addons/destroy_addon.js'
import {resolveAddon} from '../../lib/addons/resolve.js'
import _ from 'lodash'
import ConfirmCommand from '../../lib/confirmCommand.js'
import notify from '../../lib/notify.js'

export default class Destroy extends Command {
static topic = 'addons'
static args = {
addonName: Args.string({description: 'unique identifier or globally unique name of the add-on', required: true}),
}

static description = 'permanently destroy an add-on resource'
static strict = false
static examples = ['addons:destroy [ADDON]... [flags]']
static hiddenAliases = ['addons:remove']

static flags = {
force: flags.boolean({char: 'f', description: 'allow destruction even if connected to other apps'}),
confirm: flags.string({char: 'c'}),
wait: flags.boolean({description: 'watch add-on destruction status and exit when complete'}),
app: flags.app(),
confirm: flags.string({char: 'c'}),
force: flags.boolean({char: 'f', description: 'allow destruction even if connected to other apps'}),
remote: flags.remote(),
wait: flags.boolean({description: 'watch add-on destruction status and exit when complete'}),
}

static args = {
addonName: Args.string({required: true, description: 'unique identifier or globally unique name of the add-on'}),
}

static hiddenAliases = ['addons:remove']
public static notifier: (subtitle: string, message: string, success?: boolean) => void = notify

static strict = false

static topic = 'addons'

public async run(): Promise<void> {
const {flags, argv} = await this.parse(Destroy)
const {app, wait, confirm} = flags
const {argv, flags} = await this.parse(Destroy)
const {app, confirm, wait} = flags
const force = flags.force || process.env.HEROKU_FORCE === '1'

const addons = await Promise.all(argv.map((name: string) => resolveAddon(this.heroku, app, name as string)))
for (const addon of addons) {
// prevent deletion of add-on when context.app is set but the addon is attached to a different app
const addonApp = addon.app?.name
if (app && addonApp !== app) {
throw new Error(`${color.yellow(addon.name ?? '')} is on ${color.magenta(addonApp ?? '')} not ${color.magenta(app)}`)
throw new Error(`${color.yellow(addon.name ?? '')} is on ${color.app(addonApp ?? '')} not ${color.app(app)}`)
}
}

Expand Down
20 changes: 11 additions & 9 deletions packages/cli/src/commands/addons/detach.ts
Original file line number Diff line number Diff line change
@@ -1,35 +1,37 @@
import {color} from '@heroku-cli/color'
import {Command, flags} from '@heroku-cli/command'
import {Args, ux} from '@oclif/core'
import * as Heroku from '@heroku-cli/schema'
import {Args, ux} from '@oclif/core'

export default class Detach extends Command {
static topic = 'addons'
static args = {
attachment_name: Args.string({description: 'unique identifier of the add-on attachment', required: true}),
}

static description = 'detach an existing add-on resource from an app'

static flags = {
app: flags.app({required: true}),
remote: flags.remote(),
}

static args = {
attachment_name: Args.string({required: true, description: 'unique identifier of the add-on attachment'}),
}
static topic = 'addons'

public async run(): Promise<void> {
const {flags, args} = await this.parse(Detach)
const {args, flags} = await this.parse(Detach)
const {app} = flags
const {body: attachment} = await this.heroku.get<Heroku.AddOnAttachment>(`/apps/${app}/addon-attachments/${args.attachment_name}`)

ux.action.start(`Detaching ${color.cyan(attachment.name || '')} to ${color.yellow(attachment.addon?.name || '')} from ${color.magenta(app)}`)
ux.action.start(`Detaching ${color.cyan(attachment.name || '')} to ${color.yellow(attachment.addon?.name || '')} from ${color.app(app)}`)

await this.heroku.delete(`/addon-attachments/${attachment.id}`)

ux.action.stop()

ux.action.start(`Unsetting ${color.cyan(attachment.name || '')} config vars and restarting ${color.magenta(app)}`)
ux.action.start(`Unsetting ${color.cyan(attachment.name || '')} config vars and restarting ${color.app(app)}`)

const {body: releases} = await this.heroku.get<Heroku.Release[]>(`/apps/${app}/releases`, {
partial: true, headers: {Range: 'version ..; max=1, order=desc'},
headers: {Range: 'version ..; max=1, order=desc'}, partial: true,
})

ux.action.stop(`done, v${releases[0]?.version || ''}`)
Expand Down
Loading
Loading