From 63d71af64df11cd483d5d621d46f838af074dcba Mon Sep 17 00:00:00 2001 From: Eric Black Date: Fri, 16 Jan 2026 11:03:23 -0800 Subject: [PATCH 1/3] feat: migrate to color.space from @heroku/heroku-cli-util MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrates all space name references to use the new semantic color.space() function from @heroku/heroku-cli-util instead of generic colors like color.cyan() or color.green(). Changes: - Updated 15 command files in packages/cli/src/commands/spaces/ - Updated apps/index.ts to use color.space() for space references - Changed all imports from @heroku-cli/color to @heroku/heroku-cli-util - Updated color.cmd() to color.command() where affected by import changes - Updated corresponding test files to match new color output The new color.space() function provides consistent bold blue formatting with the ⬡ symbol prefix for Private Space names throughout the CLI. Part of the color system migration plan (PR 2 of 17). --- .github/PULL_REQUEST_TEMPLATE.md | 47 +++++++++++--- packages/cli/src/commands/apps/index.ts | 21 ++++--- packages/cli/src/commands/spaces/create.ts | 48 +++++++------- packages/cli/src/commands/spaces/destroy.ts | 26 ++++---- .../cli/src/commands/spaces/drains/set.ts | 17 ++--- packages/cli/src/commands/spaces/index.ts | 54 ++++++++-------- packages/cli/src/commands/spaces/rename.ts | 13 ++-- packages/cli/src/commands/spaces/transfer.ts | 11 ++-- .../src/commands/spaces/trusted-ips/add.ts | 22 ++++--- .../src/commands/spaces/trusted-ips/remove.ts | 22 ++++--- .../cli/src/commands/spaces/vpn/connect.ts | 25 ++++---- .../cli/src/commands/spaces/vpn/destroy.ts | 30 ++++----- .../cli/src/commands/spaces/vpn/update.ts | 35 ++++++----- packages/cli/src/commands/spaces/wait.ts | 63 ++++++++++--------- .../unit/commands/apps/index.unit.test.ts | 6 +- .../unit/commands/spaces/destroy.unit.test.ts | 40 ++++++------ .../commands/spaces/drains/set.unit.test.ts | 2 +- .../unit/commands/spaces/index.unit.test.ts | 49 +++++++-------- .../unit/commands/spaces/rename.unit.test.ts | 5 +- .../spaces/trusted-ips/add.unit.test.ts | 6 +- .../spaces/trusted-ips/remove.unit.test.ts | 24 +++---- .../commands/spaces/vpn/connect.unit.test.ts | 27 +++++--- .../commands/spaces/vpn/destroy.unit.test.ts | 7 ++- .../commands/spaces/vpn/update.unit.test.ts | 12 ++-- .../unit/commands/spaces/wait.unit.test.ts | 40 ++++++------ 25 files changed, 362 insertions(+), 290 deletions(-) diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 0640efa794..600d795b00 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,13 +1,46 @@ -`feat: add growl notification to spaces:wait` +## Summary + -`fix: handle special characters in app names` +## Type of Change +### Breaking Changes (major semver update) +- [ ] Add a `!` after your change type to denote a change that breaks current behavior -`chore: refactor tests` +### Feature Additions (minor semver update) +- [ ] **feat**: Introduces a new feature to the codebase -Learn more about [Conventional Commits](https://www.conventionalcommits.org/). ---> +### Patch Updates (patch semver update) +- [ ] **fix**: Bug fix +- [ ] **perf**: Performance improvement +- [ ] **deps**: Dependency upgrade +- [ ] **revert**: Revert a previous commit +- [ ] **docs**: Documentation change +- [ ] **style**: Styling update +- [ ] **chore**: Change that does not affect production code +- [ ] **refactor**: Refactoring existing code without changing behavior +- [ ] **tests**: Add/update/remove tests +- [ ] **build**: Change to the build system +- [ ] **ci**: Continuous integration workflow update + +## Testing +**Notes**: + + +**Steps**: +1. Replace this text with a list of steps used to validate changes or type 'Passing CI suffices'. +2. ... + +## Screenshots (if applicable) + +## Related Issues +GitHub issue: #[GitHub issue number] +GUS work item: [WI number](WI link) diff --git a/packages/cli/src/commands/apps/index.ts b/packages/cli/src/commands/apps/index.ts index d233e05969..915aed0af6 100644 --- a/packages/cli/src/commands/apps/index.ts +++ b/packages/cli/src/commands/apps/index.ts @@ -33,13 +33,13 @@ function listApps(apps: Heroku.App) { apps.forEach((app: App) => ux.stdout(regionizeAppName(app))) } -function print(apps: Heroku.App, user: Heroku.Account, space?: string, team?: string | null) { +function print(apps: Heroku.App, user: Heroku.Account, space?: string, team?: null | string) { if (apps.length === 0) { - if (space) ux.stdout(`There are no apps in space ${color.green(space)}.`) + if (space) ux.stdout(`There are no apps in space ${color.space(space)}.`) else if (team) ux.stdout(`There are no apps in team ${color.magenta(team)}.`) else ux.stdout('You have no apps.') } else if (space) { - hux.styledHeader(`Apps in space ${color.green(space)}`) + hux.styledHeader(`Apps in space ${color.space(space)}`) listApps(apps) } else if (team) { hux.styledHeader(`Apps in team ${color.magenta(team)}`) @@ -53,6 +53,7 @@ function print(apps: Heroku.App, user: Heroku.Account, space?: string, team?: st const columns = { Name: {get: regionizeAppName}, + // eslint-disable-next-line perfectionist/sort-objects Email: {get: ({owner}: any) => owner.email}, } @@ -65,26 +66,28 @@ function print(apps: Heroku.App, user: Heroku.Account, space?: string, team?: st export default class AppsIndex extends Command { static description = 'list your apps' - static topic = 'apps' - static hiddenAliases = ['list', 'apps:list'] - static examples = [ '$ heroku apps', ] static flags = { all: flags.boolean({char: 'A', description: 'include apps in all teams'}), + 'internal-routing': flags.boolean({char: 'i', description: 'filter to Internal Web Apps', hidden: true}), json: flags.boolean({char: 'j', description: 'output in json format'}), + + personal: flags.boolean({char: 'p', description: 'list apps in personal account when a default team is set'}), space: flags.string({ char: 's', - description: 'filter by space', completion: SpaceCompletion, + description: 'filter by space', }), - personal: flags.boolean({char: 'p', description: 'list apps in personal account when a default team is set'}), - 'internal-routing': flags.boolean({char: 'i', description: 'filter to Internal Web Apps', hidden: true}), team: flags.team(), } + static hiddenAliases = ['list', 'apps:list'] + + static topic = 'apps' + async run() { const {flags} = await this.parse(AppsIndex) diff --git a/packages/cli/src/commands/spaces/create.ts b/packages/cli/src/commands/spaces/create.ts index b20044e5eb..1ebb1d5902 100644 --- a/packages/cli/src/commands/spaces/create.ts +++ b/packages/cli/src/commands/spaces/create.ts @@ -1,18 +1,21 @@ -import {color} from '@heroku-cli/color' +import {color, hux} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' +import {RegionCompletion} from '@heroku-cli/command/lib/completions.js' import {Args, ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' -import {Space} from '../../lib/types/fir.js' import tsheredoc from 'tsheredoc' -import {displayShieldState} from '../../lib/spaces/spaces.js' -import {RegionCompletion} from '@heroku-cli/command/lib/completions.js' -import {splitCsv} from '../../lib/spaces/parsers.js' + import {getGeneration} from '../../lib/apps/generation.js' +import {splitCsv} from '../../lib/spaces/parsers.js' +import {displayShieldState} from '../../lib/spaces/spaces.js' +import {Space} from '../../lib/types/fir.js' const heredoc = tsheredoc.default export default class Create extends Command { - static topic = 'spaces' + static args = { + space: Args.string({hidden: true}), + } + static description = heredoc` create a new space ` @@ -36,23 +39,21 @@ export default class Create extends Command { channel: flags.string({hidden: true}), cidr: flags.string({description: 'RFC-1918 CIDR the space will use'}), 'data-cidr': flags.string({description: 'RFC-1918 CIDR used by Heroku Data resources for the space'}), - features: flags.string({hidden: true, description: 'a list of features separated by commas'}), - generation: flags.string({description: 'generation for space', default: 'cedar', options: ['cedar', 'fir']}), - 'kpi-url': flags.string({hidden: true, description: 'self-managed KPI endpoint to use'}), - 'log-drain-url': flags.string({hidden: true, description: 'direct log drain url'}), - region: flags.string({description: 'region name', completion: RegionCompletion}), - shield: flags.boolean({hidden: true, description: 'create a Shield space'}), + features: flags.string({description: 'a list of features separated by commas', hidden: true}), + generation: flags.string({default: 'cedar', description: 'generation for space', options: ['cedar', 'fir']}), + 'kpi-url': flags.string({description: 'self-managed KPI endpoint to use', hidden: true}), + 'log-drain-url': flags.string({description: 'direct log drain url', hidden: true}), + region: flags.string({completion: RegionCompletion, description: 'region name'}), + shield: flags.boolean({description: 'create a Shield space', hidden: true}), space: flags.string({char: 's', description: 'name of space to create'}), team: flags.team({required: true}), } - static args = { - space: Args.string({hidden: true}), - } + static topic = 'spaces' public async run(): Promise { - const {flags, args} = await this.parse(Create) - const {channel, region, features, generation, 'log-drain-url': logDrainUrl, shield, cidr, 'kpi-url': kpiUrl, 'data-cidr': dataCidr, team} = flags + const {args, flags} = await this.parse(Create) + const {channel, cidr, 'data-cidr': dataCidr, features, generation, 'kpi-url': kpiUrl, 'log-drain-url': logDrainUrl, region, shield, team} = flags const spaceName = flags.space || args.space if (!spaceName) { @@ -66,11 +67,8 @@ export default class Create extends Command { const dollarAmountHourly = shield ? '$4.17' : '$1.39' const spaceType = shield ? 'Shield' : 'Standard' - ux.action.start(`Creating space ${color.green(spaceName as string)} in team ${color.cyan(team as string)}`) + ux.action.start(`Creating space ${color.space(spaceName as string)} in team ${color.cyan(team as string)}`) const {body: space} = await this.heroku.post>('/spaces', { - headers: { - Accept: 'application/vnd.heroku+json; version=3.sdk', - }, body: { channel_name: channel, cidr, @@ -84,14 +82,18 @@ export default class Create extends Command { shield, team, }, + headers: { + Accept: 'application/vnd.heroku+json; version=3.sdk', + }, }) ux.action.stop() ux.warn(`${color.bold('Spend Alert.')} Each Heroku ${spaceType} Private Space costs ~${dollarAmountHourly}/hour (max ${dollarAmountMonthly}/month), pro-rated to the second.`) - ux.warn(`Use ${color.cmd('heroku spaces:wait')} to track allocation.`) + ux.warn(`Use ${color.command('heroku spaces:wait')} to track allocation.`) hux.styledHeader(space.name) hux.styledObject({ + // eslint-disable-next-line perfectionist/sort-objects ID: space.id, Team: space.team.name, Region: space.region.name, CIDR: space.cidr, 'Data CIDR': space.data_cidr, State: space.state, Shield: displayShieldState(space), Generation: getGeneration(space), 'Created at': space.created_at, }, ['ID', 'Team', 'Region', 'CIDR', 'Data CIDR', 'State', 'Shield', 'Generation', 'Created at']) } diff --git a/packages/cli/src/commands/spaces/destroy.ts b/packages/cli/src/commands/spaces/destroy.ts index bd250a4645..bf6315c189 100644 --- a/packages/cli/src/commands/spaces/destroy.ts +++ b/packages/cli/src/commands/spaces/destroy.ts @@ -1,19 +1,23 @@ -import {Args, ux} from '@oclif/core' +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' +import {Args, ux} from '@oclif/core' import tsheredoc from 'tsheredoc' + +import {getGeneration} from '../../lib/apps/generation.js' import ConfirmCommand from '../../lib/confirmCommand.js' import {displayNat} from '../../lib/spaces/spaces.js' -import {color} from '@heroku-cli/color' import {Space} from '../../lib/types/fir.js' -import {getGeneration} from '../../lib/apps/generation.js' const heredoc = tsheredoc.default -type RequiredSpaceWithNat = Required & {outbound_ips?: Required} +type RequiredSpaceWithNat = {outbound_ips?: Required} & Required export default class Destroy extends Command { - static topic = 'spaces' + static args = { + space: Args.string({hidden: true}), + } + static description = heredoc` destroy a space ` @@ -23,16 +27,14 @@ export default class Destroy extends Command { `] static flags = { - space: flags.string({char: 's', description: 'space to destroy'}), confirm: flags.string({description: 'set to space name to bypass confirm prompt', hasValue: true}), + space: flags.string({char: 's', description: 'space to destroy'}), } - static args = { - space: Args.string({hidden: true}), - } + static topic = 'spaces' public async run(): Promise { - const {flags, args} = await this.parse(Destroy) + const {args, flags} = await this.parse(Destroy) const {confirm} = flags const spaceName = flags.space || args.space if (!spaceName) { @@ -68,10 +70,10 @@ export default class Destroy extends Command { await new ConfirmCommand().confirm( spaceName as string, confirm, - `Destructive Action\nThis command will destroy the space ${color.bold.red(spaceName as string)}\n${natWarning}\n`, + `Destructive Action\nThis command will destroy the space ${color.space(spaceName as string)}\n${natWarning}\n`, ) - ux.action.start(`Destroying space ${color.cyan(spaceName as string)}`) + ux.action.start(`Destroying space ${color.space(spaceName as string)}`) await this.heroku.delete(`/spaces/${spaceName}`) ux.action.stop() } diff --git a/packages/cli/src/commands/spaces/drains/set.ts b/packages/cli/src/commands/spaces/drains/set.ts index ca6167a7a4..6f1dd39b4c 100644 --- a/packages/cli/src/commands/spaces/drains/set.ts +++ b/packages/cli/src/commands/spaces/drains/set.ts @@ -1,29 +1,30 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' 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 Set extends Command { - static topic = 'spaces' static aliases = ['drains:set'] + static args = { + url: Args.string({description: 'URL to replace the log drain with', required: true}), + } + static description = 'replaces the log drain for a space' static flags = { space: flags.string({char: 's', description: 'space for which to set log drain', required: true}), } - static args = { - url: Args.string({required: true, description: 'URL to replace the log drain with'}), - } + static topic = 'spaces' public async run(): Promise { - const {flags, args} = await this.parse(Set) + const {args, flags} = await this.parse(Set) const {url} = args const {space} = flags const {body: drain} = await this.heroku.put(`/spaces/${space}/log-drain`, { body: {url}, headers: {Accept: 'application/vnd.heroku+json; version=3.dogwood'}, }) - ux.stdout(`Successfully set drain ${color.cyan(drain.url)} for ${color.cyan.bold(space)}.`) + ux.stdout(`Successfully set drain ${color.cyan(drain.url)} for ${color.space(space)}.`) ux.warn('It may take a few moments for the changes to take effect.') } } diff --git a/packages/cli/src/commands/spaces/index.ts b/packages/cli/src/commands/spaces/index.ts index 3b6f2aa665..3062dbec43 100644 --- a/packages/cli/src/commands/spaces/index.ts +++ b/packages/cli/src/commands/spaces/index.ts @@ -1,23 +1,46 @@ -import {color} from '@heroku-cli/color' +/* eslint-disable perfectionist/sort-objects */ +import {color, hux} from '@heroku/heroku-cli-util' import {Command, flags as Flags} from '@heroku-cli/command' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' -import {Space} from '../../lib/types/fir.js' + import {getGeneration} from '../../lib/apps/generation.js' +import {Space} from '../../lib/types/fir.js' type SpaceArray = Array> export default class Index extends Command { - static topic = 'spaces' static description = 'list available spaces' static flags = { json: Flags.boolean({description: 'output in json format'}), team: Flags.team(), } + static topic = 'spaces' + + protected display(spaces: SpaceArray) { + hux.table( + spaces, + { + Name: {get: space => color.space(space.name)}, + Team: {get: space => color.cyan(space.team.name)}, + Region: {get: space => space.region.name}, + State: {get: space => space.state}, + Generation: {get: space => getGeneration(space)}, + createdAt: { + header: 'Created At', + get: space => space.created_at, + }, + }, + ) + } + + protected displayJSON(spaces: SpaceArray) { + ux.stdout(JSON.stringify(spaces, null, 2)) + } + public async run(): Promise { const {flags} = await this.parse(Index) - const {team, json} = flags + const {json, team} = flags let {body: spaces} = await this.heroku.get('/spaces', { headers: { Accept: 'application/vnd.heroku+json; version=3.sdk', @@ -45,25 +68,4 @@ export default class Index extends Command { spaces.sort((a, b) => a.name === b.name ? 0 : (a.name < b.name ? -1 : 1)) return spaces } - - protected displayJSON(spaces: SpaceArray) { - ux.stdout(JSON.stringify(spaces, null, 2)) - } - - protected display(spaces: SpaceArray) { - hux.table( - spaces, - { - Name: {get: space => space.name}, - Team: {get: space => space.team.name}, - Region: {get: space => space.region.name}, - State: {get: space => space.state}, - Generation: {get: space => getGeneration(space)}, - createdAt: { - header: 'Created At', - get: space => space.created_at, - }, - }, - ) - } } diff --git a/packages/cli/src/commands/spaces/rename.ts b/packages/cli/src/commands/spaces/rename.ts index cbdff65e9c..936c2d696b 100644 --- a/packages/cli/src/commands/spaces/rename.ts +++ b/packages/cli/src/commands/spaces/rename.ts @@ -1,4 +1,4 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {ux} from '@oclif/core' import tsheredoc from 'tsheredoc' @@ -6,7 +6,6 @@ import tsheredoc from 'tsheredoc' const heredoc = tsheredoc.default export default class Rename extends Command { - static topic = 'spaces' static description = 'renames a space' static example = heredoc(` $ heroku spaces:rename --from old-space-name --to new-space-name @@ -14,14 +13,16 @@ export default class Rename extends Command { `) static flags = { - from: flags.string({required: true, description: 'current name of space'}), - to: flags.string({required: true, description: 'desired name of space'}), + from: flags.string({description: 'current name of space', required: true}), + to: flags.string({description: 'desired name of space', required: true}), } + static topic = 'spaces' + public async run(): Promise { const {flags} = await this.parse(Rename) - const {to, from} = flags - ux.action.start(`Renaming space from ${color.cyan(from)} to ${color.green(to)}`) + const {from, to} = flags + ux.action.start(`Renaming space from ${color.space(from)} to ${color.info(to)}`) await this.heroku.patch(`/spaces/${from}`, {body: {name: to}}) ux.action.stop() } diff --git a/packages/cli/src/commands/spaces/transfer.ts b/packages/cli/src/commands/spaces/transfer.ts index e0033714b3..834bb06739 100644 --- a/packages/cli/src/commands/spaces/transfer.ts +++ b/packages/cli/src/commands/spaces/transfer.ts @@ -1,4 +1,4 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {ux} from '@oclif/core' import tsheredoc from 'tsheredoc' @@ -6,7 +6,6 @@ import tsheredoc from 'tsheredoc' const heredoc = tsheredoc.default export default class Transfer extends Command { - static topic = 'spaces' static description = 'transfer a space to another team' static examples = [heredoc(` $ heroku spaces:transfer --space=space-name --team=team-name @@ -14,17 +13,19 @@ export default class Transfer extends Command { `)] static flags = { - space: flags.string({required: true, char: 's', description: 'name of space'}), - team: flags.string({required: true, char: 't', description: 'desired owner of space'}), + space: flags.string({char: 's', description: 'name of space', required: true}), + team: flags.string({char: 't', description: 'desired owner of space', required: true}), } + static topic = 'spaces' + public async run(): Promise { const {flags} = await this.parse(Transfer) const {space} = flags const {team} = flags try { - ux.action.start(`Transferring space ${color.yellow(space)} to team ${color.green(team)}`) + ux.action.start(`Transferring space ${color.space(space)} to team ${color.green(team)}`) await this.heroku.post(`/spaces/${space}/transfer`, {body: {new_owner: team}}) } catch (error) { const {body: {message}} = error as {body: {message: string}} diff --git a/packages/cli/src/commands/spaces/trusted-ips/add.ts b/packages/cli/src/commands/spaces/trusted-ips/add.ts index d99d4af6c6..1c5f0ff592 100644 --- a/packages/cli/src/commands/spaces/trusted-ips/add.ts +++ b/packages/cli/src/commands/spaces/trusted-ips/add.ts @@ -1,14 +1,16 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' 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 tsheredoc from 'tsheredoc' const heredoc = tsheredoc.default export default class Add extends Command { - static topic = 'spaces' - static hiddenAliases = ['trusted-ips:add'] + static args = { + source: Args.string({description: 'IP address in CIDR notation', required: true}), + } + static description = heredoc(` Add one range to the list of trusted IP ranges Uses CIDR notation.`) @@ -18,16 +20,16 @@ export default class Add extends Command { Added 192.168.0.1/24 to trusted IP ranges on my-space`)] static flags = { - space: flags.string({char: 's', description: 'space to add rule to', required: true}), confirm: flags.string({description: 'set to space name to bypass confirm prompt'}), + space: flags.string({char: 's', description: 'space to add rule to', required: true}), } - static args = { - source: Args.string({required: true, description: 'IP address in CIDR notation'}), - } + static hiddenAliases = ['trusted-ips:add'] + + static topic = 'spaces' public async run(): Promise { - const {flags, args} = await this.parse(Add) + const {args, flags} = await this.parse(Add) const {space} = flags const url = `/spaces/${space}/inbound-ruleset` const {body: ruleset} = await this.heroku.get(url) @@ -37,7 +39,7 @@ export default class Add extends Command { ruleset.rules.push({action: 'allow', source: args.source}) await this.heroku.put(url, {body: ruleset}) - ux.stdout(`Added ${color.cyan.bold(args.source)} to trusted IP ranges on ${color.cyan.bold(space)}`) + ux.stdout(`Added ${color.cyan.bold(args.source)} to trusted IP ranges on ${color.space(space)}`) // Fetch updated ruleset to check applied status const {body: updatedRuleset} = await this.heroku.get(url) diff --git a/packages/cli/src/commands/spaces/trusted-ips/remove.ts b/packages/cli/src/commands/spaces/trusted-ips/remove.ts index f69ef37258..80e3a398bd 100644 --- a/packages/cli/src/commands/spaces/trusted-ips/remove.ts +++ b/packages/cli/src/commands/spaces/trusted-ips/remove.ts @@ -1,14 +1,16 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' 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 tsheredoc from 'tsheredoc' const heredoc = tsheredoc.default export default class Remove extends Command { - static topic = 'spaces' - static hiddenAliases = ['trusted-ips:remove'] + static args = { + source: Args.string({description: 'IP address in CIDR notation', required: true}), + } + static description = heredoc(` Remove a range from the list of trusted IP ranges Uses CIDR notation.`) @@ -19,16 +21,16 @@ export default class Remove extends Command { `)] static flags = { - space: flags.string({required: true, char: 's', description: 'space to remove rule from'}), confirm: flags.string({description: 'set to space name to bypass confirm prompt'}), + space: flags.string({char: 's', description: 'space to remove rule from', required: true}), } - static args = { - source: Args.string({required: true, description: 'IP address in CIDR notation'}), - } + static hiddenAliases = ['trusted-ips:remove'] + + static topic = 'spaces' public async run(): Promise { - const {flags, args} = await this.parse(Remove) + const {args, flags} = await this.parse(Remove) const {space} = flags const url = `/spaces/${space}/inbound-ruleset` const {body: rules} = await this.heroku.get(url) @@ -43,7 +45,7 @@ export default class Remove extends Command { } await this.heroku.put(url, {body: rules}) - ux.stdout(`Removed ${color.cyan.bold(args.source)} from trusted IP ranges on ${color.cyan.bold(space)}`) + ux.stdout(`Removed ${color.cyan.bold(args.source)} from trusted IP ranges on ⬡ ${color.space(space)}`) // Fetch updated ruleset to check applied status const {body: updatedRuleset} = await this.heroku.get(url) diff --git a/packages/cli/src/commands/spaces/vpn/connect.ts b/packages/cli/src/commands/spaces/vpn/connect.ts index 086e32d429..a610ddea53 100644 --- a/packages/cli/src/commands/spaces/vpn/connect.ts +++ b/packages/cli/src/commands/spaces/vpn/connect.ts @@ -1,13 +1,21 @@ +import {color as newColor} from '@heroku/heroku-cli-util' import {color} from '@heroku-cli/color' import {Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' import tsheredoc from 'tsheredoc' + import {splitCsv} from '../../../lib/spaces/parsers.js' const heredoc = tsheredoc.default export default class Connect extends Command { - static topic = 'spaces' + static args = { + name: Args.string({ + description: 'name or id of the VPN connection to create', + required: true, + }), + } + static description = heredoc` create VPN Private Spaces can be connected to another private network via an IPSec VPN connection allowing dynos to connect to hosts on your private networks and vice versa. @@ -20,25 +28,20 @@ export default class Connect extends Command { `] static flags = { - ip: flags.string({char: 'i', description: 'public IP of customer gateway', required: true}), cidrs: flags.string({char: 'c', description: 'a list of routable CIDRs separated by commas', required: true}), + ip: flags.string({char: 'i', description: 'public IP of customer gateway', required: true}), space: flags.string({char: 's', description: 'space name', required: true}), } - static args = { - name: Args.string({ - required: true, - description: 'name or id of the VPN connection to create', - }), - } + static topic = 'spaces' public async run(): Promise { - const {flags, args} = await this.parse(Connect) - const {space, cidrs, ip} = flags + const {args, flags} = await this.parse(Connect) + const {cidrs, ip, space} = flags const {name} = args const parsed_cidrs = splitCsv(cidrs) - ux.action.start(`Creating VPN Connection in space ${color.green(space)}`) + ux.action.start(`Creating VPN Connection in space ${newColor.space(space)}`) await this.heroku.post(`/spaces/${space}/vpn-connections`, { body: { name, diff --git a/packages/cli/src/commands/spaces/vpn/destroy.ts b/packages/cli/src/commands/spaces/vpn/destroy.ts index 2d2ac763b5..38edc359a9 100644 --- a/packages/cli/src/commands/spaces/vpn/destroy.ts +++ b/packages/cli/src/commands/spaces/vpn/destroy.ts @@ -1,13 +1,20 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' -import ConfirmCommand from '../../../lib/confirmCommand.js' import tsheredoc from 'tsheredoc' +import ConfirmCommand from '../../../lib/confirmCommand.js' + const heredoc = tsheredoc.default export default class Destroy extends Command { - static topic = 'spaces' + static args = { + name: Args.string({ + description: 'name or id of the VPN connection to destroy', + required: true, + }), + } + static description = 'destroys VPN in a private space' static examples = [heredoc` $ heroku spaces:vpn:destroy vpn-connection-name --space example-space --confirm vpn-connection-name @@ -15,20 +22,15 @@ export default class Destroy extends Command { `] static flags = { - space: flags.string({char: 's', description: 'space name', required: true}), confirm: flags.string({description: 'set to VPN connection name to bypass confirm prompt', hidden: true}), + space: flags.string({char: 's', description: 'space name', required: true}), } - static args = { - name: Args.string({ - required: true, - description: 'name or id of the VPN connection to destroy', - }), - } + static topic = 'spaces' public async run(): Promise { - const {flags, args} = await this.parse(Destroy) - const {space, confirm} = flags + const {args, flags} = await this.parse(Destroy) + const {confirm, space} = flags const {name} = args await new ConfirmCommand().confirm( @@ -36,11 +38,11 @@ export default class Destroy extends Command { confirm, heredoc` Destructive Action - This command will attempt to destroy the specified VPN Connection in space ${color.green(space)} + This command will attempt to destroy the specified VPN Connection in space ${color.space(space)} `, ) - ux.action.start(`Tearing down VPN Connection ${color.cyan(name)} in space ${color.cyan(space)}`) + ux.action.start(`Tearing down VPN Connection ${color.cyan(name)} in space ${color.space(space)}`) await this.heroku.delete(`/spaces/${space}/vpn-connections/${name}`) ux.action.stop() } diff --git a/packages/cli/src/commands/spaces/vpn/update.ts b/packages/cli/src/commands/spaces/vpn/update.ts index 535d5a5d6b..8679562801 100644 --- a/packages/cli/src/commands/spaces/vpn/update.ts +++ b/packages/cli/src/commands/spaces/vpn/update.ts @@ -1,41 +1,44 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' -import {splitCsv} from '../../../lib/spaces/parsers.js' import tsheredoc from 'tsheredoc' +import {splitCsv} from '../../../lib/spaces/parsers.js' + const heredoc = tsheredoc.default export default class Update extends Command { - static topic = 'spaces' + static args = { + name: Args.string({ + description: 'name or id of the VPN connection to update', + required: true, + }), + } + static description = heredoc` update VPN Private Spaces can be connected to another private network via an IPSec VPN connection allowing dynos to connect to hosts on your private networks and vice versa. The connection is established over the public Internet but all traffic is encrypted using IPSec. ` - static flags = { - cidrs: flags.string({char: 'c', description: 'a list of routable CIDRs separated by commas', required: true}), - space: flags.string({char: 's', description: 'space name', required: true}), - } - static example = heredoc` $ heroku spaces:vpn:update vpn-connection-name --space my-space --cidrs 172.16.0.0/16,10.0.0.0/24 Updating VPN Connection in space my-space... done ` - static args = { - name: Args.string({ - required: true, - description: 'name or id of the VPN connection to update', - }), + + static flags = { + cidrs: flags.string({char: 'c', description: 'a list of routable CIDRs separated by commas', required: true}), + space: flags.string({char: 's', description: 'space name', required: true}), } + static topic = 'spaces' + public async run(): Promise { - const {flags, args} = await this.parse(Update) - const {space, cidrs} = flags + const {args, flags} = await this.parse(Update) + const {cidrs, space} = flags const {name} = args const parsedCidrs = splitCsv(cidrs) - ux.action.start(`Updating VPN Connection in space ${color.green(space)}`) + ux.action.start(`Updating VPN Connection in space ${color.space(space)}`) await this.heroku.patch( `/spaces/${space}/vpn-connections/${name}`, {body: {routable_cidrs: parsedCidrs}}, diff --git a/packages/cli/src/commands/spaces/wait.ts b/packages/cli/src/commands/spaces/wait.ts index 53cbafdea0..fcbe57084d 100644 --- a/packages/cli/src/commands/spaces/wait.ts +++ b/packages/cli/src/commands/spaces/wait.ts @@ -1,12 +1,12 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' +import {Notification, notify} from '@heroku-cli/notifications' import {Args, ux} from '@oclif/core' -import tsheredoc from 'tsheredoc' -import {action} from '@oclif/core/ux' import debug from 'debug' -import {renderInfo} from '../../lib/spaces/spaces.js' -import {Notification, notify} from '@heroku-cli/notifications' import {IncomingHttpHeaders} from 'node:http' +import tsheredoc from 'tsheredoc' + +import {renderInfo} from '../../lib/spaces/spaces.js' import {SpaceNat} from '../../lib/types/fir.js' import {SpaceWithOutboundIps} from '../../lib/types/spaces.js' @@ -15,29 +15,46 @@ const heredoc = tsheredoc.default const spacesDebug = debug('spaces:wait') export default class Wait extends Command { - static topic = 'spaces' + static args = { + space: Args.string({hidden: true}), + } + static description = 'wait for a space to be created' static flags = { - space: flags.string({char: 's', description: 'space to get info of'}), - json: flags.boolean({description: 'output in json format'}), interval: flags.integer({ char: 'i', - description: 'seconds to wait between poll intervals', default: 30, + description: 'seconds to wait between poll intervals', }), + json: flags.boolean({description: 'output in json format'}), + space: flags.string({char: 's', description: 'space to get info of'}), timeout: flags.integer({ char: 't', - description: 'maximum number of seconds to wait', default: 25 * 60, + description: 'maximum number of seconds to wait', }), } - static args = { - space: Args.string({hidden: true}), + static topic = 'spaces' + + protected notify(spaceName: string) { + try { + const notification: { + message?: string, sound?: boolean, subtitle?: string, title?: string + } & Notification = { + message: 'space was successfully created', + sound: true, + subtitle: `heroku spaces:wait ${spaceName}`, + title: spaceName, + } + notify(notification) + } catch (error: any) { + ux.warn(error) + } } public async run(): Promise { - const {flags, args} = await this.parse(Wait) + const {args, flags} = await this.parse(Wait) const spaceName = flags.space || args.space if (!spaceName) { ux.error(heredoc(` @@ -50,7 +67,7 @@ export default class Wait extends Command { const interval = flags.interval * 1000 const timeout = flags.timeout * 1000 const deadline = new Date(Date.now() + timeout) - action.start(`Waiting for space ${color.green(spaceName as string)} to allocate`) + ux.action.start(`Waiting for space ${color.space(spaceName as string)} to allocate`) const headers: IncomingHttpHeaders = { Accept: 'application/vnd.heroku+json; version=3.fir', @@ -77,7 +94,7 @@ export default class Wait extends Command { spacesDebug(`Retrieving NAT details for the space failed with ${error}`) } - action.stop() + ux.action.stop() renderInfo(space, flags.json) this.notify(spaceName as string) } @@ -85,20 +102,4 @@ export default class Wait extends Command { protected wait(ms: number) { return new Promise(resolve => setTimeout(resolve, ms)) } - - protected notify(spaceName: string) { - try { - const notification: Notification & { - sound?: boolean, message?: string, title?: string, subtitle?: string - } = { - title: spaceName, - subtitle: `heroku spaces:wait ${spaceName}`, - message: 'space was successfully created', - sound: true, - } - notify(notification) - } catch (error: any) { - ux.warn(error) - } - } } diff --git a/packages/cli/test/unit/commands/apps/index.unit.test.ts b/packages/cli/test/unit/commands/apps/index.unit.test.ts index d3d46e88ef..518336fc42 100644 --- a/packages/cli/test/unit/commands/apps/index.unit.test.ts +++ b/packages/cli/test/unit/commands/apps/index.unit.test.ts @@ -297,7 +297,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps', '--space', 'test-space']) expect(stderr).to.equal('') - expect(stdout).to.equal('There are no apps in space test-space.\n') + expect(stdout).to.equal('There are no apps in space ⬡ test-space.\n') }) it('lists only apps in spaces by name', async function () { @@ -312,7 +312,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps', '--space', 'test-space']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== Apps in space test-space\n\n⬢ space-app-1\n⬢ space-app-2\n') + expect(stdout).to.equal('=== Apps in space ⬡ test-space\n\n⬢ space-app-1\n⬢ space-app-2\n') }) it('lists only internal apps in spaces by name', async function () { @@ -327,7 +327,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps', '--space', 'test-space', '--internal-routing']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== Apps in space test-space\n\n⬢ space-internal-app [internal]\n') + expect(stdout).to.equal('=== Apps in space ⬡ test-space\n\n⬢ space-internal-app [internal]\n') }) }) }) diff --git a/packages/cli/test/unit/commands/spaces/destroy.unit.test.ts b/packages/cli/test/unit/commands/spaces/destroy.unit.test.ts index 28865fa24e..ed5ffb38f2 100644 --- a/packages/cli/test/unit/commands/spaces/destroy.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/destroy.unit.test.ts @@ -1,15 +1,13 @@ +import {hux} from '@heroku/heroku-cli-util' +import {expect} from 'chai' +import nock from 'nock' +import * as sinon from 'sinon' import {stderr} from 'stdout-stderr' + import Cmd from '../../../../src/commands/spaces/destroy.js' import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' -import {expect} from 'chai' -import tsheredoc from 'tsheredoc' -import {hux} from '@heroku/heroku-cli-util' -import * as sinon from 'sinon' import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js' -const heredoc = tsheredoc.default - describe('spaces:destroy', function () { const now = new Date() @@ -26,24 +24,24 @@ describe('spaces:destroy', function () { const api = nock('https://api.heroku.com') .get('/spaces/my-space') .reply(200, { + created_at: now, + generation: 'fir', name: 'my-space', - team: {name: 'my-team'}, region: {name: 'my-region'}, state: 'allocated', - created_at: now, - generation: 'fir', + team: {name: 'my-team'}, }) .get('/spaces/my-space/nat') - .reply(200, {state: 'enabled', sources: ['1.1.1.1', '2.2.2.2']}) + .reply(200, {sources: ['1.1.1.1', '2.2.2.2'], state: 'enabled'}) .delete('/spaces/my-space') .reply(200) await runCommand(Cmd, ['--space', 'my-space']) api.done() const replacer = /([»›])/g - const actual = removeAllWhitespace(stderr.output.replace(replacer, '')) + const actual = removeAllWhitespace(stderr.output.replaceAll(replacer, '')) expect(actual).to.include(removeAllWhitespace('Warning: Destructive Action')) - expect(actual).to.include(removeAllWhitespace('This command will destroy the space my-space')) + expect(actual).to.include(removeAllWhitespace('This command will destroy the space ⬡ my-space')) expect(actual).to.include(removeAllWhitespace('=== WARNING: Outbound IPs Will Be Reused')) expect(actual).to.include(removeAllWhitespace('⚠️ Deleting this space frees up the following outbound IPv4 and IPv6 IPs for reuse:')) expect(actual).to.include(removeAllWhitespace('1.1.1.1, 2.2.2.2')) @@ -53,31 +51,31 @@ describe('spaces:destroy', function () { expect(actual).to.include(removeAllWhitespace('= Security group configurations')) expect(actual).to.include(removeAllWhitespace('= Network ACLs')) expect(actual).to.include(removeAllWhitespace('Ensure that you remove the listed IPv4 and IPv6 addresses from your security configurations.')) - expect(actual).to.include(removeAllWhitespace('Destroying space my-space... done')) + expect(actual).to.include(removeAllWhitespace('Destroying space ⬡ my-space... done')) }) it('shows simple NAT warning for non-fir generation space', async function () { const api = nock('https://api.heroku.com') .get('/spaces/my-space') .reply(200, { + created_at: now, + generation: 'cedar', name: 'my-space', - team: {name: 'my-team'}, region: {name: 'my-region'}, state: 'allocated', - created_at: now, - generation: 'cedar', + team: {name: 'my-team'}, }) .get('/spaces/my-space/nat') - .reply(200, {state: 'enabled', sources: ['1.1.1.1', '2.2.2.2']}) + .reply(200, {sources: ['1.1.1.1', '2.2.2.2'], state: 'enabled'}) .delete('/spaces/my-space') .reply(200) await runCommand(Cmd, ['--space', 'my-space']) api.done() const replacer = /([»›])/g - const actual = removeAllWhitespace(stderr.output.replace(replacer, '')) + const actual = removeAllWhitespace(stderr.output.replaceAll(replacer, '')) expect(actual).to.include(removeAllWhitespace('Warning: Destructive Action')) - expect(actual).to.include(removeAllWhitespace('This command will destroy the space my-space')) + expect(actual).to.include(removeAllWhitespace('This command will destroy the space ⬡ my-space')) expect(actual).to.include(removeAllWhitespace('=== WARNING: Outbound IPs Will Be Reused')) expect(actual).to.include(removeAllWhitespace('⚠️ Deleting this space frees up the following outbound IPv4 IPs for reuse:')) expect(actual).to.include(removeAllWhitespace('1.1.1.1, 2.2.2.2')) @@ -87,6 +85,6 @@ describe('spaces:destroy', function () { expect(actual).to.include(removeAllWhitespace('= Security group configurations')) expect(actual).to.include(removeAllWhitespace('= Network ACLs')) expect(actual).to.include(removeAllWhitespace('Ensure that you remove the listed IPv4 addresses from your security configurations.')) - expect(actual).to.include(removeAllWhitespace('Destroying space my-space... done')) + expect(actual).to.include(removeAllWhitespace('Destroying space ⬡ my-space... done')) }) }) diff --git a/packages/cli/test/unit/commands/spaces/drains/set.unit.test.ts b/packages/cli/test/unit/commands/spaces/drains/set.unit.test.ts index bcc1b7ea8f..4099422a0e 100644 --- a/packages/cli/test/unit/commands/spaces/drains/set.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/drains/set.unit.test.ts @@ -30,6 +30,6 @@ describe('spaces:drains:set', function () { const {stdout} = await runCommand(['spaces:drains:set', 'https://example.com', '--space', 'my-space']) - expect(stdout).to.equal('Successfully set drain https://example.com for my-space.\n') + expect(stdout).to.equal('Successfully set drain https://example.com for ⬡ my-space.\n') }) }) diff --git a/packages/cli/test/unit/commands/spaces/index.unit.test.ts b/packages/cli/test/unit/commands/spaces/index.unit.test.ts index db4c6509ca..68dfaf2425 100644 --- a/packages/cli/test/unit/commands/spaces/index.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/index.unit.test.ts @@ -1,74 +1,77 @@ +import {Errors} from '@oclif/core' +import {expect} from 'chai' +import nock from 'nock' import {stdout} from 'stdout-stderr' +import stripAnsi from 'strip-ansi' + import Cmd from '../../../../src/commands/spaces/index.js' import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' -import {expect} from 'chai' -import {Errors} from '@oclif/core' -import stripAnsi from 'strip-ansi' import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js' describe('spaces', function () { const now = new Date() const spaces = [{ + created_at: now.toISOString(), + generation: 'cedar', name: 'my-space', - team: {name: 'my-team'}, region: {name: 'my-region'}, state: 'allocated', - created_at: now.toISOString(), - generation: 'cedar', + team: {name: 'my-team'}, }] + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) afterEach(function () { + api.done() nock.cleanAll() }) it('shows spaces', async function () { - const api = nock('https://api.heroku.com') + api .get('/spaces') .reply(200, spaces) await runCommand(Cmd, []) - api.done() - const actual = removeAllWhitespace(stdout.output) expect(actual).to.include(removeAllWhitespace('Name Team Region State Generation Created At')) - expect(actual).to.include(removeAllWhitespace(`my-space my-team my-region allocated cedar ${now.toISOString()}`)) + expect(actual).to.include(removeAllWhitespace(`⬡ my-space my-team my-region allocated cedar ${now.toISOString()}`)) }) it('shows spaces with --json', async function () { - const api = nock('https://api.heroku.com') + api .get('/spaces') .reply(200, spaces) await runCommand(Cmd, ['--json']) - api.done() expect(JSON.parse(stdout.output)).to.deep.eq(spaces) }) it('shows spaces scoped by teams', async function () { - const api = nock('https://api.heroku.com') + api .get('/spaces') .reply(200, spaces.concat([{ + created_at: now.toISOString(), + generation: 'cedar', name: 'other-space', - team: {name: 'other-team'}, region: {name: 'my-region'}, state: 'allocated', - created_at: now.toISOString(), - generation: 'cedar', + team: {name: 'other-team'}, }])) await runCommand(Cmd, ['--team', 'my-team']) - api.done() const actual = removeAllWhitespace(stdout.output) expect(actual).to.include(removeAllWhitespace('Name Team Region State Generation Created At')) - expect(actual).to.include(removeAllWhitespace(`my-space my-team my-region allocated cedar ${now.toISOString()}`)) + expect(actual).to.include(removeAllWhitespace(`⬡ my-space my-team my-region allocated cedar ${now.toISOString()}`)) }) it('shows spaces team error message', async function () { - const api = nock('https://api.heroku.com') + api .get('/spaces') .reply(200, spaces) @@ -78,12 +81,10 @@ describe('spaces', function () { const {message} = error as Errors.CLIError expect(stripAnsi(message)).to.eq('No spaces in other-team.') } - - api.done() }) it('shows spaces error message', async function () { - const api = nock('https://api.heroku.com') + api .get('/spaces') .reply(200, []) @@ -93,7 +94,5 @@ describe('spaces', function () { const {message} = error as Errors.CLIError expect(message).to.eq('You do not have access to any spaces.') } - - api.done() }) }) diff --git a/packages/cli/test/unit/commands/spaces/rename.unit.test.ts b/packages/cli/test/unit/commands/spaces/rename.unit.test.ts index f80bcd5d1b..f0ac0cee04 100644 --- a/packages/cli/test/unit/commands/spaces/rename.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/rename.unit.test.ts @@ -1,7 +1,8 @@ +import nock from 'nock' import {stderr} from 'stdout-stderr' + import Cmd from '../../../../src/commands/spaces/rename.js' import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' import expectOutput from '../../../helpers/utils/expectOutput.js' describe('spaces:rename', function () { @@ -16,6 +17,6 @@ describe('spaces:rename', function () { '--to', 'new-space-name', ]) - expectOutput(stderr.output, 'Renaming space from old-space-name to new-space-name... done') + expectOutput(stderr.output, 'Renaming space from ⬡ old-space-name to new-space-name... done') }) }) diff --git a/packages/cli/test/unit/commands/spaces/trusted-ips/add.unit.test.ts b/packages/cli/test/unit/commands/spaces/trusted-ips/add.unit.test.ts index 2769b64e08..549f60cabc 100644 --- a/packages/cli/test/unit/commands/spaces/trusted-ips/add.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/trusted-ips/add.unit.test.ts @@ -43,7 +43,7 @@ describe('trusted-ips:add', function () { const {stdout} = await runCommand(['spaces:trusted-ips:add', '127.0.0.1/20', '--space', 'my-space', '--confirm', 'my-space']) - expect(stdout).to.eq('Added 127.0.0.1/20 to trusted IP ranges on my-space\nTrusted IP rules are applied to this space.\n') + expect(stdout).to.eq('Added 127.0.0.1/20 to trusted IP ranges on ⬡ my-space\nTrusted IP rules are applied to this space.\n') }) it('shows message when applied is false after add', async function () { @@ -75,7 +75,7 @@ describe('trusted-ips:add', function () { const {stdout} = await runCommand(['spaces:trusted-ips:add', '127.0.0.1/20', '--space', 'my-space', '--confirm', 'my-space']) - expect(stdout).to.include('Added 127.0.0.1/20 to trusted IP ranges on my-space') + expect(stdout).to.include('Added 127.0.0.1/20 to trusted IP ranges on ⬡ my-space') expect(stdout).to.include('Trusted IP rules are not applied to this space. Update your Trusted IP list to trigger a re-application of the rules.') }) @@ -107,6 +107,6 @@ describe('trusted-ips:add', function () { const {stdout} = await runCommand(['spaces:trusted-ips:add', '127.0.0.1/20', '--space', 'my-space', '--confirm', 'my-space']) - expect(stdout).to.eq('Added 127.0.0.1/20 to trusted IP ranges on my-space\n') + expect(stdout).to.eq('Added 127.0.0.1/20 to trusted IP ranges on ⬡ my-space\n') }) }) diff --git a/packages/cli/test/unit/commands/spaces/trusted-ips/remove.unit.test.ts b/packages/cli/test/unit/commands/spaces/trusted-ips/remove.unit.test.ts index ed4e5f9666..5d6ab79cb1 100644 --- a/packages/cli/test/unit/commands/spaces/trusted-ips/remove.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/trusted-ips/remove.unit.test.ts @@ -21,7 +21,7 @@ describe('trusted-ips:remove', function () { api .get('/spaces/my-space/inbound-ruleset') .reply(200, { - created_by: 'dickeyxxx', + created_by: 'gandalf', rules: [ {action: 'allow', source: '128.0.0.1/20'}, {action: 'allow', source: '127.0.0.1/20'}, @@ -29,7 +29,7 @@ describe('trusted-ips:remove', function () { }, ) .put('/spaces/my-space/inbound-ruleset', { - created_by: 'dickeyxxx', + created_by: 'gandalf', rules: [ {action: 'allow', source: '128.0.0.1/20'}, ], @@ -38,7 +38,7 @@ describe('trusted-ips:remove', function () { .get('/spaces/my-space/inbound-ruleset') .reply(200, { applied: true, - created_by: 'dickeyxxx', + created_by: 'gandalf', rules: [ {action: 'allow', source: '128.0.0.1/20'}, ], @@ -47,7 +47,7 @@ describe('trusted-ips:remove', function () { const {stdout} = await runCommand(['spaces:trusted-ips:remove', '127.0.0.1/20', '--space', 'my-space']) expect(stdout).to.eq(heredoc(` - Removed 127.0.0.1/20 from trusted IP ranges on my-space + Removed 127.0.0.1/20 from trusted IP ranges on ⬡ my-space Trusted IP rules are applied to this space. `)) }) @@ -56,7 +56,7 @@ describe('trusted-ips:remove', function () { api .get('/spaces/my-space/inbound-ruleset') .reply(200, { - created_by: 'dickeyxxx', + created_by: 'gandalf', rules: [ {action: 'allow', source: '128.0.0.1/20'}, {action: 'allow', source: '127.0.0.1/20'}, @@ -64,7 +64,7 @@ describe('trusted-ips:remove', function () { }, ) .put('/spaces/my-space/inbound-ruleset', { - created_by: 'dickeyxxx', + created_by: 'gandalf', rules: [ {action: 'allow', source: '128.0.0.1/20'}, ], @@ -73,7 +73,7 @@ describe('trusted-ips:remove', function () { .get('/spaces/my-space/inbound-ruleset') .reply(200, { applied: false, - created_by: 'dickeyxxx', + created_by: 'gandalf', rules: [ {action: 'allow', source: '128.0.0.1/20'}, ], @@ -81,7 +81,7 @@ describe('trusted-ips:remove', function () { const {stdout} = await runCommand(['spaces:trusted-ips:remove', '127.0.0.1/20', '--space', 'my-space']) - expect(stdout).to.include('Removed 127.0.0.1/20 from trusted IP ranges on my-space') + expect(stdout).to.include('Removed 127.0.0.1/20 from trusted IP ranges on ⬡ my-space') expect(stdout).to.include('Trusted IP rules are not applied to this space. Update your Trusted IP list to trigger a re-application of the rules.') }) @@ -89,7 +89,7 @@ describe('trusted-ips:remove', function () { api .get('/spaces/my-space/inbound-ruleset') .reply(200, { - created_by: 'dickeyxxx', + created_by: 'gandalf', rules: [ {action: 'allow', source: '128.0.0.1/20'}, {action: 'allow', source: '127.0.0.1/20'}, @@ -97,7 +97,7 @@ describe('trusted-ips:remove', function () { }, ) .put('/spaces/my-space/inbound-ruleset', { - created_by: 'dickeyxxx', + created_by: 'gandalf', rules: [ {action: 'allow', source: '128.0.0.1/20'}, ], @@ -105,7 +105,7 @@ describe('trusted-ips:remove', function () { .reply(200, {rules: []}) .get('/spaces/my-space/inbound-ruleset') .reply(200, { - created_by: 'dickeyxxx', + created_by: 'gandalf', rules: [ {action: 'allow', source: '128.0.0.1/20'}, ], @@ -114,7 +114,7 @@ describe('trusted-ips:remove', function () { const {stdout} = await runCommand(['spaces:trusted-ips:remove', '127.0.0.1/20', '--space', 'my-space']) expect(stdout).to.eq(heredoc(` - Removed 127.0.0.1/20 from trusted IP ranges on my-space + Removed 127.0.0.1/20 from trusted IP ranges on ⬡ my-space `)) }) }) diff --git a/packages/cli/test/unit/commands/spaces/vpn/connect.unit.test.ts b/packages/cli/test/unit/commands/spaces/vpn/connect.unit.test.ts index 0e45c0b758..1b01d98432 100644 --- a/packages/cli/test/unit/commands/spaces/vpn/connect.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/vpn/connect.unit.test.ts @@ -1,16 +1,29 @@ +import ansis from 'ansis' +import {expect} from 'chai' +import nock from 'nock' import {stderr} from 'stdout-stderr' +import stripAnsi from 'strip-ansi' +import tsheredoc from 'tsheredoc' + import Cmd from '../../../../../src/commands/spaces/vpn/connect.js' import runCommand from '../../../../helpers/runCommand.js' -import nock from 'nock' -import tsheredoc from 'tsheredoc' -import {expect} from 'chai' -import stripAnsi from 'strip-ansi' const heredoc = tsheredoc.default describe('spaces:vpn:connect', function () { + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) + it('creates a VPN', async function () { - const api = nock('https://api.heroku.com') + api .post('/spaces/my-space/vpn-connections', { name: 'office', public_ip: '192.168.0.1', @@ -29,8 +42,8 @@ describe('spaces:vpn:connect', function () { ]) api.done() - expect(stderr.output).to.contain('Creating VPN Connection in space my-space... done\n') - expect(stripAnsi(stderr.output)).to.contain(heredoc` + expect(stderr.output).to.contain('Creating VPN Connection in space ⬡ my-space... done\n') + expect(ansis.strip(stderr.output)).to.contain(heredoc` Use heroku spaces:vpn:wait to track allocation. `) diff --git a/packages/cli/test/unit/commands/spaces/vpn/destroy.unit.test.ts b/packages/cli/test/unit/commands/spaces/vpn/destroy.unit.test.ts index 638a06d049..d9f79f23d3 100644 --- a/packages/cli/test/unit/commands/spaces/vpn/destroy.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/vpn/destroy.unit.test.ts @@ -1,8 +1,9 @@ -import {stderr} from 'stdout-stderr' import {expect} from 'chai' +import nock from 'nock' +import {stderr} from 'stdout-stderr' + import Cmd from '../../../../../src/commands/spaces/vpn/destroy.js' import runCommand from '../../../../helpers/runCommand.js' -import nock from 'nock' describe('spaces:vpn:destroy', function () { it('destroys a VPN Connection when name is specified', async function () { @@ -19,7 +20,7 @@ describe('spaces:vpn:destroy', function () { ]) api.done() - expect(stderr.output).to.eq('Tearing down VPN Connection my-vpn-connection in space my-space... done\n') + expect(stderr.output).to.eq('Tearing down VPN Connection my-vpn-connection in space ⬡ my-space... done\n') nock.cleanAll() }) diff --git a/packages/cli/test/unit/commands/spaces/vpn/update.unit.test.ts b/packages/cli/test/unit/commands/spaces/vpn/update.unit.test.ts index 34d8de218c..921083bf98 100644 --- a/packages/cli/test/unit/commands/spaces/vpn/update.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/vpn/update.unit.test.ts @@ -1,11 +1,9 @@ -import {stderr, stdout} from 'stdout-stderr' -import Cmd from '../../../../../src/commands/spaces/vpn/update.js' -import runCommand from '../../../../helpers/runCommand.js' -import nock from 'nock' -import tsheredoc from 'tsheredoc' import {expect} from 'chai' +import nock from 'nock' +import {stderr} from 'stdout-stderr' -const heredoc = tsheredoc.default +import Cmd from '../../../../../src/commands/spaces/vpn/update.js' +import runCommand from '../../../../helpers/runCommand.js' describe('spaces:vpn:update', function () { it('updates VPN', async function () { @@ -24,7 +22,7 @@ describe('spaces:vpn:update', function () { ]) api.done() - expect(stderr.output).to.eq('Updating VPN Connection in space my-space... done\n') + expect(stderr.output).to.eq('Updating VPN Connection in space ⬡ my-space... done\n') nock.cleanAll() }) diff --git a/packages/cli/test/unit/commands/spaces/wait.unit.test.ts b/packages/cli/test/unit/commands/spaces/wait.unit.test.ts index 2a9ae74f0a..c98322ce66 100644 --- a/packages/cli/test/unit/commands/spaces/wait.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/wait.unit.test.ts @@ -1,14 +1,15 @@ +import {expect} from 'chai' +import nock from 'nock' +import * as sinon from 'sinon' import {stderr, stdout} from 'stdout-stderr' +import tsheredoc from 'tsheredoc' + import Cmd from '../../../../src/commands/spaces/wait.js' +import {getGeneration} from '../../../../src/lib/apps/generation.js' +import {SpaceWithOutboundIps} from '../../../../src/lib/types/spaces.js' +import * as fixtures from '../../../fixtures/spaces/fixtures.js' import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' -import tsheredoc from 'tsheredoc' -import {expect} from 'chai' import expectOutput from '../../../helpers/utils/expectOutput.js' -import * as fixtures from '../../../fixtures/spaces/fixtures.js' -import * as sinon from 'sinon' -import {SpaceWithOutboundIps} from '../../../../src/lib/types/spaces.js' -import {getGeneration} from '../../../../src/lib/apps/generation.js' const heredoc = tsheredoc.default @@ -17,8 +18,10 @@ describe('spaces:wait', function () { let allocatedSpace: SpaceWithOutboundIps let sandbox: sinon.SinonSandbox let notifyStub: sinon.SinonStub + let api: nock.Scope beforeEach(function () { + api = nock('https://api.heroku.com') sandbox = sinon.createSandbox() notifyStub = sandbox.stub(Cmd.prototype, 'notify' as any) allocatingSpace = fixtures.spaces['allocating-space'] @@ -26,6 +29,8 @@ describe('spaces:wait', function () { }) afterEach(function () { + nock.cleanAll() + api.done() sandbox.restore() }) @@ -46,7 +51,7 @@ describe('spaces:wait', function () { '0', ]) expectOutput(stderr.output, heredoc(` - Waiting for space ${allocatedSpace.name} to allocate... done + Waiting for space ⬡ ${allocatedSpace.name} to allocate... done `)) expectOutput(stdout.output, heredoc(` === ${allocatedSpace.name} @@ -66,14 +71,14 @@ describe('spaces:wait', function () { }) it('waits for space with --json', async function () { - nock('https://api.heroku.com') + api .get(`/spaces/${allocatingSpace.name}`) .reply(200, allocatingSpace) .get(`/spaces/${allocatedSpace.name}`) .reply(200, allocatedSpace) - nock('https://api.heroku.com') + api .get(`/spaces/${allocatedSpace.name}/nat`) - .reply(200, {state: 'enabled', sources: ['123.456.789.123']}) + .reply(200, {sources: ['123.456.789.123'], state: 'enabled'}) await runCommand(Cmd, [ '--space', @@ -82,13 +87,12 @@ describe('spaces:wait', function () { '--interval', '0', ]) - const allocatedSpaceWithOutboundIPs = Object.assign( - {}, - allocatedSpace, - {outbound_ips: {state: 'enabled', sources: ['123.456.789.123']}}, - ) + const allocatedSpaceWithOutboundIPs = { + ...allocatedSpace, + outbound_ips: {sources: ['123.456.789.123'], state: 'enabled'}, + } expectOutput(stderr.output, heredoc(` - Waiting for space ${allocatedSpace.name} to allocate... done + Waiting for space ⬡ ${allocatedSpace.name} to allocate... done `)) expectOutput(stdout.output, JSON.stringify(allocatedSpaceWithOutboundIPs, null, 2)) }) @@ -97,7 +101,7 @@ describe('spaces:wait', function () { nock('https://api.heroku.com', {reqheaders: {'Accept-Expansion': 'region'}}) .get(`/spaces/${allocatedSpace.name}`) .reply(200, allocatedSpace) - nock('https://api.heroku.com') + api .get(`/spaces/${allocatedSpace.name}/nat`) .reply(503, {}) From e6aad9b8d6fea0e106391023f56bdd801749d7ab Mon Sep 17 00:00:00 2001 From: Eric Black Date: Fri, 16 Jan 2026 12:00:45 -0800 Subject: [PATCH 2/3] update a few more commands to use space color --- packages/cli/src/commands/apps/info.ts | 15 +-- packages/cli/src/commands/spaces/create.ts | 2 +- packages/cli/src/lib/spaces/spaces.ts | 7 +- packages/cli/src/lib/telemetry/util.ts | 17 +-- .../test/unit/commands/apps/info.unit.test.ts | 18 ++- .../unit/commands/spaces/create.unit.test.ts | 113 +++++++++--------- .../unit/commands/telemetry/info.unit.test.ts | 35 +++--- .../commands/telemetry/update.unit.test.ts | 23 ++-- 8 files changed, 115 insertions(+), 115 deletions(-) diff --git a/packages/cli/src/commands/apps/info.ts b/packages/cli/src/commands/apps/info.ts index 207939ecd0..6f727237bd 100644 --- a/packages/cli/src/commands/apps/info.ts +++ b/packages/cli/src/commands/apps/info.ts @@ -1,10 +1,11 @@ -import {Args, ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' +import {color, hux} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' -import * as util from 'util' -import _ from 'lodash' +import {Args, ux} from '@oclif/core' import {filesize} from 'filesize' +import _ from 'lodash' +import * as util from 'util' + import {getGeneration} from '../../lib/apps/generation.js' const {countBy, snakeCase} = _ @@ -38,8 +39,8 @@ async function getInfo(app: string, client: Command, extended: boolean) { const data: Heroku.App = { addons, app: appWithMoreInfo, - dynos, collaborators, + dynos, pipeline_coupling: pipelineCouplings, } @@ -66,7 +67,7 @@ function print(info: Heroku.App, addons: Heroku.AddOn[], collaborators: Heroku.C if (info.app.cron_next_run) data['Cron Next Run'] = formatDate(new Date(info.app.cron_next_run)) if (info.app.database_size) data['Database Size'] = filesize(info.app.database_size, {standard: 'jedec', round: 0}) if (info.app.create_status !== 'complete') data['Create Status'] = info.app.create_status - if (info.app.space) data.Space = info.app.space.name + if (info.app.space) data.Space = color.space(info.app.space.name) if (info.app.space && info.app.internal_routing) data['Internal Routing'] = info.app.internal_routing if (info.pipeline_coupling) data.Pipeline = `${info.pipeline_coupling.pipeline.name} - ${info.pipeline_coupling.stage}` @@ -87,7 +88,7 @@ function print(info: Heroku.App, addons: Heroku.AddOn[], collaborators: Heroku.C return stack })(info.app) - hux.styledHeader(info.app.name) + hux.styledHeader(color.app(info.app.name)) hux.styledObject(data) if (extended) { diff --git a/packages/cli/src/commands/spaces/create.ts b/packages/cli/src/commands/spaces/create.ts index 1ebb1d5902..2dbb100c71 100644 --- a/packages/cli/src/commands/spaces/create.ts +++ b/packages/cli/src/commands/spaces/create.ts @@ -91,7 +91,7 @@ export default class Create extends Command { ux.warn(`${color.bold('Spend Alert.')} Each Heroku ${spaceType} Private Space costs ~${dollarAmountHourly}/hour (max ${dollarAmountMonthly}/month), pro-rated to the second.`) ux.warn(`Use ${color.command('heroku spaces:wait')} to track allocation.`) - hux.styledHeader(space.name) + hux.styledHeader(color.space(space.name)) hux.styledObject({ // eslint-disable-next-line perfectionist/sort-objects ID: space.id, Team: space.team.name, Region: space.region.name, CIDR: space.cidr, 'Data CIDR': space.data_cidr, State: space.state, Shield: displayShieldState(space), Generation: getGeneration(space), 'Created at': space.created_at, diff --git a/packages/cli/src/lib/spaces/spaces.ts b/packages/cli/src/lib/spaces/spaces.ts index 94f119a2e8..a72a63a855 100644 --- a/packages/cli/src/lib/spaces/spaces.ts +++ b/packages/cli/src/lib/spaces/spaces.ts @@ -1,9 +1,10 @@ +import {color, hux} from '@heroku/heroku-cli-util' import * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' + +import {getGeneration} from '../apps/generation.js' import {SpaceNat} from '../types/fir.js' import {SpaceWithOutboundIps} from '../types/spaces.js' -import {getGeneration} from '../apps/generation.js' export function displayShieldState(space: Heroku.Space) { return space.shield ? 'on' : 'off' @@ -19,7 +20,7 @@ export function renderInfo(space: SpaceWithOutboundIps, json: boolean) { if (json) { ux.stdout(JSON.stringify(space, null, 2)) } else { - hux.styledHeader(space.name || '') + hux.styledHeader(color.space(space.name || '')) hux.styledObject( { ID: space.id, diff --git a/packages/cli/src/lib/telemetry/util.ts b/packages/cli/src/lib/telemetry/util.ts index 45b2e30c6c..0f1664b296 100644 --- a/packages/cli/src/lib/telemetry/util.ts +++ b/packages/cli/src/lib/telemetry/util.ts @@ -1,16 +1,17 @@ +import {color, hux} from '@heroku/heroku-cli-util' +import {APIClient} from '@heroku-cli/command' +import * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' + import {TelemetryDrain} from '../types/telemetry.js' -import * as Heroku from '@heroku-cli/schema' -import {APIClient} from '@heroku-cli/command' interface TelemetryDisplayObject { App?: string - Space?: string - Signals: string Endpoint: string - Transport: string Headers?: string + Signals: string + Space?: string + Transport: string } export function validateAndFormatSignals(signalInput: string | undefined): string[] { @@ -38,14 +39,14 @@ export async function displayTelemetryDrain(telemetryDrain: TelemetryDrain, hero Accept: 'application/vnd.heroku+json; version=3.sdk', }, }) - displayObject.Space = space.name + displayObject.Space = color.space(space.name || '') } else { const {body: app} = await heroku.get(`/apps/${telemetryDrain.owner.id}`, { headers: { Accept: 'application/vnd.heroku+json; version=3.sdk', }, }) - displayObject.App = app.name + displayObject.App = color.app(app.name || '') } // Add the other properties after App/Space diff --git a/packages/cli/test/unit/commands/apps/info.unit.test.ts b/packages/cli/test/unit/commands/apps/info.unit.test.ts index 8311ebec84..e35e468540 100644 --- a/packages/cli/test/unit/commands/apps/info.unit.test.ts +++ b/packages/cli/test/unit/commands/apps/info.unit.test.ts @@ -2,8 +2,6 @@ import {runCommand} from '@oclif/test' import {expect} from 'chai' import nock from 'nock' -import type {App} from '../../../../src/lib/types/fir.js' - import {unwrap} from '../../../helpers/utils/unwrap.js' describe('apps:info', function () { @@ -61,13 +59,13 @@ describe('apps:info', function () { {user: {email: 'foo2@foo.com'}}, ] - const BASE_INFO = `=== myapp + const BASE_INFO = `=== ⬢ myapp Addons: heroku-redis papertrail Collaborators: foo2@foo.com Database Size: 1000 B -Space: myspace +Space: ⬡ myspace Internal Routing: true Auto Cert Mgmt: true Git URL: https://git.heroku.com/myapp @@ -80,13 +78,13 @@ Dynos: web: 1 Stack: cedar-14 ` - const BASE_INFO_FIR = `=== myapp + const BASE_INFO_FIR = `=== ⬢ myapp Addons: heroku-redis papertrail Collaborators: foo2@foo.com Database Size: 1000 B -Space: myspace +Space: ⬡ myspace Internal Routing: true Auto Cert Mgmt: true Git URL: https://git.heroku.com/myapp @@ -201,13 +199,13 @@ Stack: cedar-14 const {stderr, stdout} = await runCommand(['apps:info', 'myapp']) - expect(stdout).to.equal(`=== myapp + expect(stdout).to.equal(`=== ⬢ myapp Addons: heroku-redis papertrail Collaborators: foo2@foo.com Database Size: 1000 B -Space: myspace +Space: ⬡ myspace Internal Routing: true Pipeline: my-pipeline - production Auto Cert Mgmt: true @@ -331,13 +329,13 @@ stack=cedar-14 const {stderr, stdout} = await runCommand(['apps:info', 'myapp']) - expect(stdout).to.equal(`=== myapp + expect(stdout).to.equal(`=== ⬢ myapp Addons: heroku-redis papertrail Collaborators: foo2@foo.com Database Size: 1000 B -Space: myspace +Space: ⬡ myspace Internal Routing: true Git URL: https://git.heroku.com/myapp Web URL: https://myapp.herokuapp.com diff --git a/packages/cli/test/unit/commands/spaces/create.unit.test.ts b/packages/cli/test/unit/commands/spaces/create.unit.test.ts index b53ebcb553..44f0d40e12 100644 --- a/packages/cli/test/unit/commands/spaces/create.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/create.unit.test.ts @@ -1,10 +1,11 @@ -import {stdout, stderr} from 'stdout-stderr' -import Cmd from '../../../../src/commands/spaces/create.js' -import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' import {expect} from 'chai' +import nock from 'nock' +import {stderr, stdout} from 'stdout-stderr' import tsheredoc from 'tsheredoc' + +import Cmd from '../../../../src/commands/spaces/create.js' import {getGeneration} from '../../../../src/lib/apps/generation.js' +import runCommand from '../../../helpers/runCommand.js' import {unwrap} from '../../../helpers/utils/unwrap.js' const heredoc = tsheredoc.default @@ -12,13 +13,19 @@ const heredoc = tsheredoc.default describe('spaces:create', function () { const now = new Date() const features = ['one', 'two'] + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) + }) afterEach(function () { + api.done() nock.cleanAll() }) it('creates a Standard space', async function () { - const api = nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) + api .post('/spaces', { features, generation: 'cedar', @@ -27,16 +34,16 @@ describe('spaces:create', function () { team: 'my-team', }) .reply(201, { - shield: false, - name: 'my-space', - team: {name: 'my-team'}, - region: {name: 'my-region'}, + cidr: '10.0.0.0/16', + created_at: now, + data_cidr: '172.23.0.0/20', features: ['one', 'two'], generation: 'cedar', + name: 'my-space', + region: {name: 'my-region'}, + shield: false, state: 'allocated', - created_at: now, - cidr: '10.0.0.0/16', - data_cidr: '172.23.0.0/20', + team: {name: 'my-team'}, }) await runCommand(Cmd, [ @@ -46,10 +53,8 @@ describe('spaces:create', function () { '--features=one, two', ]) - api.done() - expect(stdout.output).to.eq(heredoc` - === my-space + === ⬡ my-space Team: my-team Region: my-region @@ -63,7 +68,7 @@ describe('spaces:create', function () { }) it('shows Standard Private Space Add-on cost warning', async function () { - const api = nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) + api .post('/spaces', { features, generation: 'cedar', @@ -72,16 +77,16 @@ describe('spaces:create', function () { team: 'my-team', }) .reply(201, { - shield: false, - name: 'my-space', - team: {name: 'my-team'}, - region: {name: 'my-region'}, + cidr: '10.0.0.0/16', + created_at: now, + data_cidr: '172.23.0.0/20', features: ['one', 'two'], generation: 'cedar', + name: 'my-space', + region: {name: 'my-region'}, + shield: false, state: 'allocated', - created_at: now, - cidr: '10.0.0.0/16', - data_cidr: '172.23.0.0/20', + team: {name: 'my-team'}, }) await runCommand(Cmd, [ @@ -91,13 +96,11 @@ describe('spaces:create', function () { '--features=one, two', ]) - api.done() - expect(unwrap(stderr.output)).to.include('Warning: Spend Alert. Each Heroku Standard Private Space costs ~$1.39/hour (max $1000/month), pro-rated to the second.') }) it('creates a Shield space', async function () { - const api = nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) + api .post('/spaces', { features, generation: 'cedar', @@ -107,16 +110,16 @@ describe('spaces:create', function () { team: 'my-team', }) .reply(201, { - shield: true, - name: 'my-space', - team: {name: 'my-team'}, - region: {name: 'my-region'}, + cidr: '10.0.0.0/16', + created_at: now, + data_cidr: '172.23.0.0/20', features: ['one', 'two'], generation: 'cedar', + name: 'my-space', + region: {name: 'my-region'}, + shield: true, state: 'allocated', - created_at: now, - cidr: '10.0.0.0/16', - data_cidr: '172.23.0.0/20', + team: {name: 'my-team'}, }) await runCommand(Cmd, [ @@ -127,10 +130,8 @@ describe('spaces:create', function () { '--shield', ]) - api.done() - expect(stdout.output).to.eq(heredoc` - === my-space + === ⬡ my-space Team: my-team Region: my-region @@ -144,7 +145,7 @@ describe('spaces:create', function () { }) it('shows Shield Private Space Add-on cost warning', async function () { - const api = nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) + api .post('/spaces', { features, generation: 'cedar', @@ -154,16 +155,16 @@ describe('spaces:create', function () { team: 'my-team', }) .reply(201, { - shield: true, - name: 'my-space', - team: {name: 'my-team'}, - region: {name: 'my-region'}, + cidr: '10.0.0.0/16', + created_at: now, + data_cidr: '172.23.0.0/20', features: ['one', 'two'], generation: 'cedar', + name: 'my-space', + region: {name: 'my-region'}, + shield: true, state: 'allocated', - created_at: now, - cidr: '10.0.0.0/16', - data_cidr: '172.23.0.0/20', + team: {name: 'my-team'}, }) await runCommand(Cmd, [ @@ -174,13 +175,11 @@ describe('spaces:create', function () { '--shield', ]) - api.done() - expect(unwrap(stderr.output)).to.include('Warning: Spend Alert. Each Heroku Shield Private Space costs ~$4.17/hour (max $3000/month), pro-rated to the second.') }) it('creates a space with custom cidr and data cidr', async function () { - const api = nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) + api .post('/spaces', { cidr: '10.0.0.0/24', data_cidr: '172.23.0.0/28', @@ -191,15 +190,15 @@ describe('spaces:create', function () { team: 'my-team', }) .reply(201, { - shield: false, + cidr: '10.0.0.0/24', + created_at: now, + data_cidr: '172.23.0.0/28', + features: ['one', 'two'], name: 'my-space', - team: {name: 'my-team'}, region: {name: 'my-region'}, - features: ['one', 'two'], + shield: false, state: 'allocated', - created_at: now, - cidr: '10.0.0.0/24', - data_cidr: '172.23.0.0/28', + team: {name: 'my-team'}, }) await runCommand(Cmd, [ @@ -211,10 +210,8 @@ describe('spaces:create', function () { '--data-cidr=172.23.0.0/28', ]) - api.done() - expect(stdout.output).to.eq(heredoc` - === my-space + === ⬡ my-space Team: my-team Region: my-region @@ -239,7 +236,7 @@ describe('spaces:create', function () { state: 'allocated', team: {name: 'my-team'}, } - nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) + api .post('/spaces', { features: firSpace.features, generation: getGeneration(firSpace), @@ -262,7 +259,7 @@ describe('spaces:create', function () { getGeneration(firSpace)!, ]) expect(stdout.output).to.eq(heredoc` - === ${firSpace.name} + === ⬡ ${firSpace.name} Team: ${firSpace.team.name} Region: ${firSpace.region.name} diff --git a/packages/cli/test/unit/commands/telemetry/info.unit.test.ts b/packages/cli/test/unit/commands/telemetry/info.unit.test.ts index ddc6959da6..20ccc7f3a9 100644 --- a/packages/cli/test/unit/commands/telemetry/info.unit.test.ts +++ b/packages/cli/test/unit/commands/telemetry/info.unit.test.ts @@ -1,10 +1,11 @@ +import nock from 'nock' import {stdout} from 'stdout-stderr' +import tsheredoc from 'tsheredoc' + import Cmd from '../../../../src/commands/telemetry/info.js' +import {TelemetryDrain} from '../../../../src/lib/types/telemetry.js' import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' import expectOutput from '../../../helpers/utils/expectOutput.js' -import tsheredoc from 'tsheredoc' -import {TelemetryDrain} from '../../../../src/lib/types/telemetry.js' const heredoc = tsheredoc.default @@ -16,30 +17,30 @@ describe('telemetry:info', function () { beforeEach(function () { spaceTelemetryDrain = { - id: '44444321-5717-4562-b3fc-2c963f66afa6', - owner: {id: spaceId, type: 'space'}, - signals: ['traces', 'metrics', 'logs'], exporter: { - type: 'otlphttp', endpoint: 'https://api.honeycomb.io/', headers: { - 'x-honeycomb-team': 'your-api-key', 'x-honeycomb-dataset': 'your-dataset', + 'x-honeycomb-team': 'your-api-key', }, + type: 'otlphttp', }, + id: '44444321-5717-4562-b3fc-2c963f66afa6', + owner: {id: spaceId, type: 'space'}, + signals: ['traces', 'metrics', 'logs'], } appTelemetryDrain = { - id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', - owner: {id: appId, type: 'app'}, - signals: ['traces', 'metrics'], exporter: { - type: 'otlphttp', endpoint: 'https://api.honeycomb.io/', headers: { - 'x-honeycomb-team': 'your-api-key', 'x-honeycomb-dataset': 'your-dataset', + 'x-honeycomb-team': 'your-api-key', }, + type: 'otlphttp', }, + id: '3fa85f64-5717-4562-b3fc-2c963f66afa6', + owner: {id: appId, type: 'app'}, + signals: ['traces', 'metrics'], } }) @@ -61,11 +62,11 @@ describe('telemetry:info', function () { ]) expectOutput(stdout.output, heredoc(` === ${spaceTelemetryDrain.id} - Space: myspace + Space: ⬡ myspace Signals: ${spaceTelemetryDrain.signals.join(', ')} Endpoint: ${spaceTelemetryDrain.exporter.endpoint} Transport: HTTP - Headers: {"x-honeycomb-team":"your-api-key","x-honeycomb-dataset":"your-dataset"} + Headers: {"x-honeycomb-dataset":"your-dataset","x-honeycomb-team":"your-api-key"} `)) }) @@ -83,11 +84,11 @@ describe('telemetry:info', function () { ]) expectOutput(stdout.output, heredoc(` === ${appTelemetryDrain.id} - App: myapp + App: ⬢ myapp Signals: ${appTelemetryDrain.signals.join(', ')} Endpoint: ${appTelemetryDrain.exporter.endpoint} Transport: HTTP - Headers: {"x-honeycomb-team":"your-api-key","x-honeycomb-dataset":"your-dataset"} + Headers: {"x-honeycomb-dataset":"your-dataset","x-honeycomb-team":"your-api-key"} `)) }) }) diff --git a/packages/cli/test/unit/commands/telemetry/update.unit.test.ts b/packages/cli/test/unit/commands/telemetry/update.unit.test.ts index 96d5aa95c3..91fedf175b 100644 --- a/packages/cli/test/unit/commands/telemetry/update.unit.test.ts +++ b/packages/cli/test/unit/commands/telemetry/update.unit.test.ts @@ -1,11 +1,12 @@ +import {expect} from 'chai' +import nock from 'nock' import {stderr, stdout} from 'stdout-stderr' +import tsheredoc from 'tsheredoc' + import Cmd from '../../../../src/commands/telemetry/update.js' +import {appTelemetryDrain1} from '../../../fixtures/telemetry/fixtures.js' import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' import expectOutput from '../../../helpers/utils/expectOutput.js' -import tsheredoc from 'tsheredoc' -import {expect} from 'chai' -import {appTelemetryDrain1} from '../../../fixtures/telemetry/fixtures.js' const heredoc = tsheredoc.default @@ -34,7 +35,7 @@ describe('telemetry:update', function () { `)) expectOutput(stdout.output, heredoc(` === ${updatedAppTelemetryDrain.id} - App: myapp + App: ⬢ myapp Signals: ${updatedAppTelemetryDrain.signals.join(', ')} Endpoint: ${updatedAppTelemetryDrain.exporter.endpoint} Transport: HTTP @@ -45,23 +46,23 @@ describe('telemetry:update', function () { it('updates a telemetry drain with multiple fields', async function () { const updatedAppTelemetryDrain = { ...appTelemetryDrain1, - signals: ['logs'], exporter: { endpoint: 'https://api-new.honeycomb.io/', - type: 'otlp', headers: { - 'x-honeycomb-team': 'your-api-key', 'x-honeycomb-dataset': 'your-dataset', + 'x-honeycomb-team': 'your-api-key', }, + type: 'otlp', }, + signals: ['logs'], } nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) .patch(`/telemetry-drains/${appTelemetryDrain1.id}`, { - signals: ['logs'], exporter: { endpoint: 'https://api-new.honeycomb.io/', type: 'otlp', }, + signals: ['logs'], }) .reply(200, updatedAppTelemetryDrain) @@ -83,11 +84,11 @@ describe('telemetry:update', function () { `)) expectOutput(stdout.output, heredoc(` === ${updatedAppTelemetryDrain.id} - App: myapp + App: ⬢ myapp Signals: ${updatedAppTelemetryDrain.signals.join(', ')} Endpoint: ${updatedAppTelemetryDrain.exporter.endpoint} Transport: gRPC - Headers: {"x-honeycomb-team":"your-api-key","x-honeycomb-dataset":"your-dataset"} + Headers: {"x-honeycomb-dataset":"your-dataset","x-honeycomb-team":"your-api-key"} `)) }) From 9878b570ecc4328f2dba445a3b7fab77ca205f4f Mon Sep 17 00:00:00 2001 From: Eric Black Date: Fri, 16 Jan 2026 12:23:56 -0800 Subject: [PATCH 3/3] fix tests --- .../src/commands/spaces/trusted-ips/remove.ts | 2 +- .../unit/commands/spaces/info.unit.test.ts | 46 +++++++++++-------- .../unit/commands/spaces/wait.unit.test.ts | 8 ++-- .../test/unit/lib/spaces/spaces.unit.test.ts | 33 ++++++------- 4 files changed, 49 insertions(+), 40 deletions(-) diff --git a/packages/cli/src/commands/spaces/trusted-ips/remove.ts b/packages/cli/src/commands/spaces/trusted-ips/remove.ts index 80e3a398bd..7eaa4e27b5 100644 --- a/packages/cli/src/commands/spaces/trusted-ips/remove.ts +++ b/packages/cli/src/commands/spaces/trusted-ips/remove.ts @@ -45,7 +45,7 @@ export default class Remove extends Command { } await this.heroku.put(url, {body: rules}) - ux.stdout(`Removed ${color.cyan.bold(args.source)} from trusted IP ranges on ⬡ ${color.space(space)}`) + ux.stdout(`Removed ${color.cyan.bold(args.source)} from trusted IP ranges on ${color.space(space)}`) // Fetch updated ruleset to check applied status const {body: updatedRuleset} = await this.heroku.get(url) diff --git a/packages/cli/test/unit/commands/spaces/info.unit.test.ts b/packages/cli/test/unit/commands/spaces/info.unit.test.ts index e3def5b96a..ffa9701d49 100644 --- a/packages/cli/test/unit/commands/spaces/info.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/info.unit.test.ts @@ -1,22 +1,30 @@ +import nock from 'nock' import {stdout} from 'stdout-stderr' +import tsheredoc from 'tsheredoc' + import Cmd from '../../../../src/commands/spaces/info.js' +import {getGeneration} from '../../../../src/lib/apps/generation.js' +import {SpaceWithOutboundIps} from '../../../../src/lib/types/spaces.js' +import * as fixtures from '../../../fixtures/spaces/fixtures.js' import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' -import tsheredoc from 'tsheredoc' import expectOutput from '../../../helpers/utils/expectOutput.js' -import * as fixtures from '../../../fixtures/spaces/fixtures.js' -import {SpaceWithOutboundIps} from '../../../../src/lib/types/spaces.js' -import {getGeneration} from '../../../../src/lib/apps/generation.js' const heredoc = tsheredoc.default describe('spaces:info', function () { let space: SpaceWithOutboundIps let shieldSpace: SpaceWithOutboundIps + let api: nock.Scope beforeEach(function () { space = fixtures.spaces['non-shield-space'] shieldSpace = fixtures.spaces['shield-space'] + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() }) it('shows space info', async function () { @@ -29,7 +37,7 @@ describe('spaces:info', function () { space.name, ]) expectOutput(stdout.output, heredoc(` - === ${space.name} + === ⬡ ${space.name} ID: ${space.id} Team: ${space.team.name} Region: ${space.region.description} @@ -43,7 +51,7 @@ describe('spaces:info', function () { }) it('shows space info --json', async function () { - nock('https://api.heroku.com:443') + api .get(`/spaces/${space.name}`) .reply(200, space) @@ -59,16 +67,16 @@ describe('spaces:info', function () { nock('https://api.heroku.com', {reqheaders: {'Accept-Expansion': 'region'}}) .get(`/spaces/${space.name}`) .reply(200, space) - nock('https://api.heroku.com') + api .get(`/spaces/${space.name}/nat`) - .reply(200, {state: 'enabled', sources: ['123.456.789.123']}) + .reply(200, {sources: ['123.456.789.123'], state: 'enabled'}) await runCommand(Cmd, [ '--space', space.name, ]) expectOutput(stdout.output, heredoc(` - === ${space.name} + === ⬡ ${space.name} ID: ${space.id} Team: ${space.team.name} Region: ${space.region.description} @@ -86,16 +94,16 @@ describe('spaces:info', function () { nock('https://api.heroku.com', {reqheaders: {'Accept-Expansion': 'region'}}) .get(`/spaces/${space.name}`) .reply(200, space) - nock('https://api.heroku.com') + api .get(`/spaces/${space.name}/nat`) - .reply(200, {state: 'disabled', sources: ['123.456.789.123']}) + .reply(200, {sources: ['123.456.789.123'], state: 'disabled'}) await runCommand(Cmd, [ '--space', space.name, ]) expectOutput(stdout.output, heredoc(` - === ${space.name} + === ⬡ ${space.name} ID: ${space.id} Team: ${space.team.name} Region: ${space.region.description} @@ -110,7 +118,7 @@ describe('spaces:info', function () { }) it('shows a space with Shield turned off', async function () { - nock('https://api.heroku.com:443') + api .get(`/spaces/${space.name}`) .reply(200, space) @@ -119,7 +127,7 @@ describe('spaces:info', function () { space.name, ]) expectOutput(stdout.output, heredoc(` - === ${space.name} + === ⬡ ${space.name} ID: ${space.id} Team: ${space.team.name} Region: ${space.region.description} @@ -133,7 +141,7 @@ describe('spaces:info', function () { }) it('shows a space with Shield turned on', async function () { - nock('https://api.heroku.com') + api .get(`/spaces/${shieldSpace.name}`) .reply(200, shieldSpace) await runCommand(Cmd, [ @@ -142,7 +150,7 @@ describe('spaces:info', function () { ]) expectOutput(stdout.output, heredoc(` - === ${shieldSpace.name} + === ⬡ ${shieldSpace.name} ID: ${shieldSpace.id} Team: ${shieldSpace.team.name} Region: ${shieldSpace.region.description} @@ -156,7 +164,7 @@ describe('spaces:info', function () { }) it('test if nat API call fails ', async function () { - nock('https://api.heroku.com') + api .get(`/spaces/${space.name}`) .reply(200, space) await runCommand(Cmd, [ @@ -164,7 +172,7 @@ describe('spaces:info', function () { space.name, ]) expectOutput(stdout.output, heredoc(` - === ${space.name} + === ⬡ ${space.name} ID: ${space.id} Team: ${space.team.name} Region: ${space.region.description} diff --git a/packages/cli/test/unit/commands/spaces/wait.unit.test.ts b/packages/cli/test/unit/commands/spaces/wait.unit.test.ts index c98322ce66..a0896c6bb6 100644 --- a/packages/cli/test/unit/commands/spaces/wait.unit.test.ts +++ b/packages/cli/test/unit/commands/spaces/wait.unit.test.ts @@ -40,9 +40,9 @@ describe('spaces:wait', function () { .reply(200, allocatingSpace) .get(`/spaces/${allocatingSpace.name}`) .reply(200, allocatedSpace) - nock('https://api.heroku.com') + api .get(`/spaces/${allocatedSpace.name}/nat`) - .reply(200, {state: 'enabled', sources: ['123.456.789.123']}) + .reply(200, {sources: ['123.456.789.123'], state: 'enabled'}) await runCommand(Cmd, [ '--space', @@ -54,7 +54,7 @@ describe('spaces:wait', function () { Waiting for space ⬡ ${allocatedSpace.name} to allocate... done `)) expectOutput(stdout.output, heredoc(` - === ${allocatedSpace.name} + === ⬡ ${allocatedSpace.name} ID: ${allocatedSpace.id} Team: ${allocatedSpace.team.name} Region: ${allocatedSpace.region.description} @@ -112,7 +112,7 @@ describe('spaces:wait', function () { '0', ]) expectOutput(stdout.output, heredoc(` - === ${allocatedSpace.name} + === ⬡ ${allocatedSpace.name} ID: ${allocatedSpace.id} Team: ${allocatedSpace.team.name} Region: ${allocatedSpace.region.description} diff --git a/packages/cli/test/unit/lib/spaces/spaces.unit.test.ts b/packages/cli/test/unit/lib/spaces/spaces.unit.test.ts index 2afe697f62..fecf716f7a 100644 --- a/packages/cli/test/unit/lib/spaces/spaces.unit.test.ts +++ b/packages/cli/test/unit/lib/spaces/spaces.unit.test.ts @@ -1,10 +1,11 @@ -import {expect} from 'chai' import * as Heroku from '@heroku-cli/schema' -import {displayShieldState, displayNat, renderInfo} from '../../../../src/lib/spaces/spaces.js' -import {SpaceNat} from '../../../../src/lib/types/fir.js' -import * as fixtures from '../../../fixtures/spaces/fixtures.js' +import {expect} from 'chai' import {stdout} from 'stdout-stderr' import tsheredoc from 'tsheredoc' + +import {displayNat, displayShieldState, renderInfo} from '../../../../src/lib/spaces/spaces.js' +import {SpaceNat} from '../../../../src/lib/types/fir.js' +import * as fixtures from '../../../fixtures/spaces/fixtures.js' import expectOutput from '../../../helpers/utils/expectOutput.js' const heredoc = tsheredoc.default @@ -33,9 +34,9 @@ describe('displayNat', function () { it('returns state when NAT state is updating', function () { const nat: SpaceNat = { - state: 'updating', - sources: [], created_at: '2024-01-01T00:00:00Z', + sources: [], + state: 'updating', updated_at: '2024-01-01T00:00:00Z', } expect(displayNat(nat)).to.equal('updating') @@ -43,9 +44,9 @@ describe('displayNat', function () { it('returns state when NAT state is disabled', function () { const nat: SpaceNat = { - state: 'disabled', - sources: [], created_at: '2024-01-01T00:00:00Z', + sources: [], + state: 'disabled', updated_at: '2024-01-01T00:00:00Z', } expect(displayNat(nat)).to.equal('disabled') @@ -53,9 +54,9 @@ describe('displayNat', function () { it('returns empty string when NAT is enabled with no IPs', function () { const nat: SpaceNat = { - state: 'enabled', - sources: [], created_at: '2024-01-01T00:00:00Z', + sources: [], + state: 'enabled', updated_at: '2024-01-01T00:00:00Z', } expect(displayNat(nat)).to.equal('') @@ -63,9 +64,9 @@ describe('displayNat', function () { it('returns a single IP when NAT is enabled with one IP', function () { const nat: SpaceNat = { - state: 'enabled', - sources: ['1.2.3.4'], created_at: '2024-01-01T00:00:00Z', + sources: ['1.2.3.4'], + state: 'enabled', updated_at: '2024-01-01T00:00:00Z', } expect(displayNat(nat)).to.equal('1.2.3.4') @@ -73,9 +74,9 @@ describe('displayNat', function () { it('returns comma-separated IPs when NAT is enabled with multiple IPs', function () { const nat: SpaceNat = { - state: 'enabled', - sources: ['1.2.3.4', '5.6.7.8', '9.10.11.12'], created_at: '2024-01-01T00:00:00Z', + sources: ['1.2.3.4', '5.6.7.8', '9.10.11.12'], + state: 'enabled', updated_at: '2024-01-01T00:00:00Z', } expect(displayNat(nat)).to.equal('1.2.3.4, 5.6.7.8, 9.10.11.12') @@ -83,7 +84,7 @@ describe('displayNat', function () { }) describe('renderInfo', function () { - const space = Object.assign({}, fixtures.spaces['non-shield-space'], {outbound_ips: {state: 'enabled', sources: ['123.456.789.123']}}) + const space = Object.assign({}, fixtures.spaces['non-shield-space'], {outbound_ips: {sources: ['123.456.789.123'], state: 'enabled'}}) it('outputs space info in JSON format when json flag is true', function () { stdout.start() @@ -98,7 +99,7 @@ describe('renderInfo', function () { stdout.stop() expectOutput(stdout.output, heredoc(` - === ${space.name} + === ⬡ ${space.name} ID: ${space.id} Team: ${space.team.name} Region: ${space.region.description}