diff --git a/package-lock.json b/package-lock.json index 6767efc93c..5cb433df4b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2778,6 +2778,78 @@ "node": ">=0.12.0" } }, + "node_modules/@heroku/heroku-cli-util": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@heroku/heroku-cli-util/-/heroku-cli-util-10.3.0.tgz", + "integrity": "sha512-amhimMWwzceu84i1/Yj872iuGnyLg2nkuZx51EVyP6sjBxoOSyhNzfbB7i4T7+hGShFRtE3dw8PeFcAPb+uo8g==", + "license": "ISC", + "dependencies": { + "@heroku-cli/command": "^12.0.0", + "@heroku/http-call": "^5.5.0", + "@oclif/core": "^4.3.0", + "@oclif/table": "0.4.14", + "ansis": "^4.1.0", + "debug": "^4.4.0", + "inquirer": "^12.6.1", + "tunnel-ssh": "5.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@heroku/heroku-cli-util/node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/@heroku/heroku-cli-util/node_modules/inquirer": { + "version": "12.11.1", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", + "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", + "license": "MIT", + "dependencies": { + "@inquirer/ansi": "^1.0.2", + "@inquirer/core": "^10.3.2", + "@inquirer/prompts": "^7.10.1", + "@inquirer/type": "^3.0.10", + "mute-stream": "^2.0.0", + "run-async": "^4.0.6", + "rxjs": "^7.8.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/@heroku/heroku-cli-util/node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@heroku/heroku-cli-util/node_modules/run-async": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", + "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, "node_modules/@heroku/http-call": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/@heroku/http-call/-/http-call-5.5.0.tgz", @@ -3284,7 +3356,6 @@ "version": "7.10.1", "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/checkbox": "^4.3.2", @@ -3314,7 +3385,6 @@ "version": "5.1.21", "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.21.tgz", "integrity": "sha512-KR8edRkIsUayMXV+o3Gv+q4jlhENF9nMYUZs9PA2HzrXeHI8M5uDag70U7RJn9yyiMZSbtF5/UexBtAVtZGSbQ==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -3336,7 +3406,6 @@ "version": "4.3.1", "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.3.1.tgz", "integrity": "sha512-kN0pAM4yPrLjJ1XJBjDxyfDduXOuQHrBB8aLDMueuwUGn+vNpF7Gq7TvyVxx8u4SHlFFj4trmj+a2cbpG4Jn1g==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/core": "^10.3.2", @@ -3358,7 +3427,6 @@ "version": "4.4.2", "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", - "dev": true, "license": "MIT", "dependencies": { "@inquirer/ansi": "^1.0.2", @@ -5050,18 +5118,6 @@ "node": ">=18.0.0" } }, - "node_modules/@oclif/table/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/@oclif/table/node_modules/ansi-styles": { "version": "6.2.3", "license": "MIT", @@ -10433,18 +10489,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/cli-truncate/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/cli-truncate/node_modules/emoji-regex": { "version": "10.5.0", "license": "MIT" @@ -14015,18 +14059,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/ink/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, "node_modules/ink/node_modules/ansi-styles": { "version": "6.2.3", "license": "MIT", @@ -20110,7 +20142,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", @@ -20216,6 +20248,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", @@ -21595,166 +21628,6 @@ "version": "2.2.3", "license": "MIT" }, - "packages/cli/node_modules/@heroku/heroku-cli-util": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/@heroku/heroku-cli-util/-/heroku-cli-util-10.2.0.tgz", - "integrity": "sha512-s6An3U8C0CYkzUroAPnHW/+26P1mTEzbkBiEcuVidc5oz5709bpzXRNLfVMVFesW+7IS6K23aAaD+wXJRxKJ/g==", - "license": "ISC", - "dependencies": { - "@heroku-cli/color": "^2.0.4", - "@heroku-cli/command": "^12.0.0", - "@heroku/http-call": "^5.5.0", - "@oclif/core": "^4.3.0", - "@oclif/table": "0.4.14", - "debug": "^4.4.0", - "inquirer": "^12.6.1", - "tunnel-ssh": "5.2.0" - }, - "engines": { - "node": ">=20" - } - }, - "packages/cli/node_modules/@heroku/heroku-cli-util/node_modules/@heroku-cli/color": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/@heroku-cli/color/-/color-2.0.4.tgz", - "integrity": "sha512-CjG4Mlj8B5ZELk8mzgJLUEb1fH+zckrqtufgxeKG9jLMyUCRyHNhEEBT9XHadPE6HbKTXMzW941QsmK6qVgNng==", - "license": "ISC", - "dependencies": { - "ansi-styles": "^4.3.0", - "chalk": "^4.1.2", - "supports-color": "^7.2.0", - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "packages/cli/node_modules/@heroku/heroku-cli-util/node_modules/@inquirer/prompts": { - "version": "7.10.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.10.1.tgz", - "integrity": "sha512-Dx/y9bCQcXLI5ooQ5KyvA4FTgeo2jYj/7plWfV5Ak5wDPKQZgudKez2ixyfz7tKXzcJciTxqLeK7R9HItwiByg==", - "license": "MIT", - "dependencies": { - "@inquirer/checkbox": "^4.3.2", - "@inquirer/confirm": "^5.1.21", - "@inquirer/editor": "^4.2.23", - "@inquirer/expand": "^4.0.23", - "@inquirer/input": "^4.3.1", - "@inquirer/number": "^3.0.23", - "@inquirer/password": "^4.0.23", - "@inquirer/rawlist": "^4.1.11", - "@inquirer/search": "^3.2.2", - "@inquirer/select": "^4.4.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "packages/cli/node_modules/@heroku/heroku-cli-util/node_modules/@inquirer/select": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.4.2.tgz", - "integrity": "sha512-l4xMuJo55MAe+N7Qr4rX90vypFwCajSakx59qe/tMaC1aEHWLyw68wF4o0A4SLAY4E0nd+Vt+EyskeDIqu1M6w==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/figures": "^1.0.15", - "@inquirer/type": "^3.0.10", - "yoctocolors-cjs": "^2.1.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "packages/cli/node_modules/@heroku/heroku-cli-util/node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "packages/cli/node_modules/@heroku/heroku-cli-util/node_modules/inquirer": { - "version": "12.11.1", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-12.11.1.tgz", - "integrity": "sha512-9VF7mrY+3OmsAfjH3yKz/pLbJ5z22E23hENKw3/LNSaA/sAt3v49bDRY+Ygct1xwuKT+U+cBfTzjCPySna69Qw==", - "license": "MIT", - "dependencies": { - "@inquirer/ansi": "^1.0.2", - "@inquirer/core": "^10.3.2", - "@inquirer/prompts": "^7.10.1", - "@inquirer/type": "^3.0.10", - "mute-stream": "^2.0.0", - "run-async": "^4.0.6", - "rxjs": "^7.8.2" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@types/node": ">=18" - }, - "peerDependenciesMeta": { - "@types/node": { - "optional": true - } - } - }, - "packages/cli/node_modules/@heroku/heroku-cli-util/node_modules/mute-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", - "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", - "license": "ISC", - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "packages/cli/node_modules/@heroku/heroku-cli-util/node_modules/run-async": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-4.0.6.tgz", - "integrity": "sha512-IoDlSLTs3Yq593mb3ZoKWKXMNu3UpObxhgA/Xuid5p4bbfi2jdY1Hj0m1K+0/tEuQTxIGMhQDqGjKb7RuxGpAQ==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "packages/cli/node_modules/@heroku/heroku-cli-util/node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, "packages/cli/node_modules/@heroku/mcp-server": { "version": "1.0.7-alpha.1", "license": "Apache-2.0", @@ -22667,6 +22540,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/cli/node_modules/@oclif/plugin-not-found/node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "packages/cli/node_modules/@oclif/plugin-not-found/node_modules/type-fest": { "version": "0.21.3", "license": "(MIT OR CC0-1.0)", @@ -22697,6 +22579,15 @@ "node": ">=18.0.0" } }, + "packages/cli/node_modules/@oclif/plugin-plugins/node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "packages/cli/node_modules/@oclif/plugin-plugins/node_modules/debug": { "version": "4.4.0", "license": "MIT", @@ -22801,6 +22692,15 @@ "node": ">=18.0.0" } }, + "packages/cli/node_modules/@oclif/plugin-update/node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "packages/cli/node_modules/@oclif/plugin-update/node_modules/debug": { "version": "4.4.1", "license": "MIT", @@ -22834,6 +22734,15 @@ "node": ">=18.0.0" } }, + "packages/cli/node_modules/@oclif/plugin-version/node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "packages/cli/node_modules/@oclif/plugin-which": { "version": "3.2.35", "license": "MIT", @@ -22845,6 +22754,15 @@ "node": ">=18.0.0" } }, + "packages/cli/node_modules/@oclif/plugin-which/node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "packages/cli/node_modules/@oclif/screen": { "version": "1.0.4", "license": "MIT", @@ -24000,6 +23918,16 @@ "node": ">=4" } }, + "packages/cli/node_modules/ansis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.2.0.tgz", + "integrity": "sha512-HqZ5rWlFjGiV0tDm3UxxgNRqsOTniqoKZu0pIAfh7TZQMGuZK+hH0drySty0si0QXj1ieop4+SkSfPZBPPkHig==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "packages/cli/node_modules/anymatch": { "version": "3.1.3", "dev": true, @@ -29054,6 +28982,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "packages/cli/node_modules/oclif/node_modules/ansis": { + "version": "3.17.0", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-3.17.0.tgz", + "integrity": "sha512-0qWUglt9JEqLFr3w1I1pbrChn1grhaiAR2ocX1PP/flRmxgtwTzPFFFnfIlD6aMOLQZgSuCRlidD70lvx8yhzg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, "packages/cli/node_modules/oclif/node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", diff --git a/packages/cli/package.json b/packages/cli/package.json index 2f5af4b666..f978858e69 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -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", @@ -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", diff --git a/packages/cli/src/commands/access/add.ts b/packages/cli/src/commands/access/add.ts index e21b4b3438..a6fe3c9ccb 100644 --- a/packages/cli/src/commands/access/add.ts +++ b/packages/cli/src/commands/access/add.ts @@ -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 { - 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(`/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) @@ -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(`/teams/apps/${appName}/collaborators`, { - body: {user: email, permissions: permissionsArraySorted}, + body: {permissions: permissionsArraySorted, user: email}, }) ux.action.stop() } else { diff --git a/packages/cli/src/commands/access/remove.ts b/packages/cli/src/commands/access/remove.ts index 91c3ab19fd..69a73aca3e 100644 --- a/packages/cli/src/commands/access/remove.ts +++ b/packages/cli/src/commands/access/remove.ts @@ -1,13 +1,12 @@ 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'}), @@ -15,12 +14,14 @@ export default class AccessRemove extends Command { static strict = false + static topic = 'access' + public async run(): Promise { - 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(`/apps/${appName}/collaborators/${email}`) ux.action.stop() } diff --git a/packages/cli/src/commands/addons/attach.ts b/packages/cli/src/commands/addons/attach.ts index 216d5e46b2..07465be040 100644 --- a/packages/cli/src/commands/addons/attach.ts +++ b/packages/cli/src/commands/addons/attach.ts @@ -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 { - 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(`/addons/${encodeURIComponent(args.addon_name)}`) const createAttachment = async (confirmed?: string) => { let namespace: string | undefined @@ -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('/addon-attachments', {body}) ux.action.stop() return attachments @@ -47,9 +50,9 @@ export default class Attach extends Command { } const attachment = await trapConfirmationRequired(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(`/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}`) } diff --git a/packages/cli/src/commands/addons/destroy.ts b/packages/cli/src/commands/addons/destroy.ts index d3debb5d4f..50edc9a1f9 100644 --- a/packages/cli/src/commands/addons/destroy.ts +++ b/packages/cli/src/commands/addons/destroy.ts @@ -1,36 +1,40 @@ 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 { - 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))) @@ -38,7 +42,7 @@ export default class Destroy extends Command { // 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)}`) } } diff --git a/packages/cli/src/commands/addons/detach.ts b/packages/cli/src/commands/addons/detach.ts index 9e773013ac..0c290349f6 100644 --- a/packages/cli/src/commands/addons/detach.ts +++ b/packages/cli/src/commands/addons/detach.ts @@ -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 { - const {flags, args} = await this.parse(Detach) + const {args, flags} = await this.parse(Detach) const {app} = flags const {body: attachment} = await this.heroku.get(`/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(`/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 || ''}`) diff --git a/packages/cli/src/commands/addons/index.ts b/packages/cli/src/commands/addons/index.ts index c9d4c9f3b1..c7101eb812 100644 --- a/packages/cli/src/commands/addons/index.ts +++ b/packages/cli/src/commands/addons/index.ts @@ -1,13 +1,14 @@ +/* eslint-disable perfectionist/sort-objects */ +import {hux, color as newColor} from '@heroku/heroku-cli-util' import {color} from '@heroku-cli/color' import {APIClient, Command, flags} from '@heroku-cli/command' -import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' import * as Heroku from '@heroku-cli/schema' -import {formatPrice, grandfatheredPrice, formatState} from '../../lib/addons/util.js' +import {ux} from '@oclif/core' import _ from 'lodash' - import printf from 'printf' +import {formatPrice, formatState, grandfatheredPrice} from '../../lib/addons/util.js' + const topic = 'addons' async function addonGetter(api: APIClient, app?: string) { @@ -16,8 +17,8 @@ async function addonGetter(api: APIClient, app?: string) { if (app) { // don't display attachments globally addonsResponse = api.get(`/apps/${app}/addons`, { headers: { - 'Accept-Expansion': 'addon_service,plan', Accept: 'application/vnd.heroku+json; version=3.sdk', + 'Accept-Expansion': 'addon_service,plan', }, }) const sudoHeaders = JSON.parse(process.env.HEROKU_HEADERS || '{}') @@ -34,8 +35,8 @@ async function addonGetter(api: APIClient, app?: string) { } else { addonsResponse = api.get('/addons', { headers: { - 'Accept-Expansion': 'addon_service,plan', Accept: 'application/vnd.heroku+json; version=3.sdk', + 'Accept-Expansion': 'addon_service,plan', }, }) } @@ -67,7 +68,7 @@ async function addonGetter(api: APIClient, app?: string) { _.values(groupedAttachments) .forEach(atts => { const inaccessibleAddon = { - app: atts[0].addon.app, name: atts[0].addon.name, addon_service: {}, plan: {}, attachments: atts, + addon_service: {}, app: atts[0].addon.app, attachments: atts, name: atts[0].addon.name, plan: {}, } if (isRelevantToApp(inaccessibleAddon)) { addons.push(inaccessibleAddon) @@ -88,7 +89,7 @@ function displayAll(addons: Heroku.AddOn[]) { addons, { 'Owning App': { - get: ({app}) => color.cyan(app?.name || ''), + get: ({app}) => newColor.app(app?.name || ''), }, 'Add-on': { get: ({name}) => color.magenta(name || ''), @@ -104,16 +105,17 @@ function displayAll(addons: Heroku.AddOn[]) { get({plan}) { if (plan?.price === undefined) return color.dim('?') - return formatPrice({price: plan?.price, hourly: true}) + return formatPrice({hourly: true, price: plan?.price}) }, }, 'Max Price': { get({plan}) { if (plan?.price === undefined) return color.dim('?') - return formatPrice({price: plan?.price, hourly: false}) + return formatPrice({hourly: false, price: plan?.price}) }, }, + State: { get({state}) { let result: string = state || '' @@ -147,7 +149,7 @@ function formatAttachment(attachment: Heroku.AddOnAttachment, showApp = true) { const attName = color.green(attachment.name || '') const output = [color.dim('as'), attName] if (showApp) { - const appInfo = `on ${color.cyan(attachment.app?.name || '')} app` + const appInfo = `on ${newColor.app(attachment.app?.name || '')} app` output.push(color.dim(appInfo)) } @@ -199,19 +201,20 @@ function displayForApp(app: string, addons: Heroku.AddOn[]) { Price: { get(addon) { if (addon.app?.name === app) { - return formatPrice({price: addon.plan?.price, hourly: true}) + return formatPrice({hourly: true, price: addon.plan?.price}) } - return color.dim(printf('(billed to %s app)', color.cyan(addon.app?.name || ''))) + return color.dim(printf('(billed to %s app)', newColor.app(addon.app?.name || ''))) }, }, + // eslint-disable-next-line perfectionist/sort-objects 'Max Price': { get(addon) { if (addon.app?.name === app) { - return formatPrice({price: addon.plan?.price, hourly: false}) + return formatPrice({hourly: false, price: addon.plan?.price}) } - return color.dim(printf('(billed to %s app)', color.cyan(addon.app?.name || ''))) + return color.dim(printf('(billed to %s app)', newColor.app(addon.app?.name || ''))) }, }, State: { @@ -222,7 +225,7 @@ function displayForApp(app: string, addons: Heroku.AddOn[]) { overflow: 'wrap', }, ) - ux.stdout(`The table above shows ${color.magenta('add-ons')} and the ${color.green('attachments')} to the current app (${app}) or other ${color.cyan('apps')}.\n `) + ux.stdout(`The table above shows ${color.magenta('add-ons')} and the ${color.green('attachments')} to the current app (${newColor.app(app)}) or other ${color.cyan('apps')}.\n `) } function displayJSON(addons: Heroku.AddOn[]) { @@ -230,8 +233,6 @@ function displayJSON(addons: Heroku.AddOn[]) { } export default class Addons extends Command { - static topic = topic - static usage = 'addons [--all|--app APP]' static description = `Lists your add-ons and attachments. The default filter applied depends on whether you are in a Heroku app @@ -239,21 +240,25 @@ export default class Addons extends Command { is implied. Explicitly providing either flag overrides the default behavior. ` + static examples = [ + `$ heroku ${topic} --all`, + `$ heroku ${topic} --app acme-inc-www`, + ] + static flags = { all: flags.boolean({char: 'A', description: 'show add-ons and attachments for all accessible apps'}), - json: flags.boolean({description: 'return add-ons in json format'}), app: flags.app(), + json: flags.boolean({description: 'return add-ons in json format'}), remote: flags.remote(), } - static examples = [ - `$ heroku ${topic} --all`, - `$ heroku ${topic} --app acme-inc-www`, - ] + static topic = topic + + static usage = 'addons [--all|--app APP]' public async run(): Promise { const {flags} = await this.parse(Addons) - const {app, all, json} = flags + const {all, app, json} = flags if (!all && app) { const addons = await addonGetter(this.heroku, app) diff --git a/packages/cli/src/commands/apps/create.ts b/packages/cli/src/commands/apps/create.ts index a7a4a44599..46a9ed28f1 100644 --- a/packages/cli/src/commands/apps/create.ts +++ b/packages/cli/src/commands/apps/create.ts @@ -1,16 +1,16 @@ -import {parse} from 'yaml' -import fs from 'fs-extra' -import {APIClient, flags, Command} from '@heroku-cli/command' +import {color, hux} from '@heroku/heroku-cli-util' +import {APIClient, Command, flags} from '@heroku-cli/command' import { BuildpackCompletion, RegionCompletion, SpaceCompletion, StackCompletion, } from '@heroku-cli/command/lib/completions.js' -import {Args, Interfaces, ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' -import {color} from '@heroku-cli/color' import * as Heroku from '@heroku-cli/schema' +import {Args, Interfaces, ux} from '@oclif/core' +import fs from 'fs-extra' +import {parse} from 'yaml' + import Git from '../../lib/git/git.js' const git = new Git() @@ -27,15 +27,15 @@ function createText(name: string, space: string) { async function createApp(context: Interfaces.ParserOutput, heroku: APIClient, name: string, stack: string) { const {flags} = context const params = { + feature_flags: flags.features, + internal_routing: flags['internal-routing'], + kernel: flags.kernel, + locked: flags.locked, name, - team: flags.team, region: flags.region, space: flags.space, stack, - internal_routing: flags['internal-routing'], - feature_flags: flags.features, - kernel: flags.kernel, - locked: flags.locked, + team: flags.team, } const requestPath = (params.space || params.team) ? '/teams/apps' : '/apps' @@ -57,11 +57,11 @@ async function createApp(context: Interfaces.ParserOutput, heroku: APIClient, na return app } -async function addAddons(heroku: APIClient, app: Heroku.App, addons: { plan: string, as?: string }[]) { +async function addAddons(heroku: APIClient, app: Heroku.App, addons: { as?: string, plan: string }[]) { for (const addon of addons) { const body = { - plan: addon.plan, attachment: addon.as ? {name: addon.as} : undefined, + plan: addon.plan, } ux.action.start(`Adding ${color.green(addon.plan)}`) @@ -104,7 +104,7 @@ function printAppSummary(context: Interfaces.ParserOutput, app: Heroku.App, remo } async function runFromFlags(context: Interfaces.ParserOutput, heroku: APIClient, config: Interfaces.Config) { - const {flags, args} = context + const {args, flags} = context if (flags['internal-routing'] && !flags.space) { throw new Error('Space name required.\nInternal Web Apps are only available for Private Spaces.\nUSAGE: heroku apps:create --space my-space --internal-routing') } @@ -136,14 +136,16 @@ async function runFromFlags(context: Interfaces.ParserOutput, heroku: APIClient, const remoteUrl = await configureGitRemote(context, app) - await config.runHook('recache', {type: 'app', app: app.name}) + await config.runHook('recache', {app: app.name, type: 'app'}) printAppSummary(context, app, remoteUrl) } export default class Create extends Command { - static description = 'creates a new app' + static args = { + app: Args.string({description: 'name of app to create', required: false}), + } - static hiddenAliases = ['create'] + static description = 'creates a new app' static examples = [ `$ heroku apps:create @@ -169,43 +171,52 @@ $ heroku apps:create example-staging --remote staging $ heroku apps:create --region eu`, ] - static args = { - app: Args.string({description: 'name of app to create', required: false}), - } - static flags = { + addons: flags.string({description: 'comma-delimited list of addons to install'}), // `app` set to `flags.string` instead of `flags.app` to maintain original v5 functionality and avoid a default value from the git remote set when used without an app app: flags.string({hidden: true}), - addons: flags.string({description: 'comma-delimited list of addons to install'}), buildpack: flags.string({ char: 'b', - description: 'buildpack url to use for this app', completion: BuildpackCompletion, + description: 'buildpack url to use for this app', }), - manifest: flags.boolean({char: 'm', description: 'use heroku.yml settings for this app', hidden: true}), - 'no-remote': flags.boolean({char: 'n', description: 'do not create a git remote'}), - remote: flags.remote({description: 'the git remote to create, default "heroku"', default: 'heroku'}), - stack: flags.string({char: 's', description: 'the stack to create the app on', completion: StackCompletion}), - space: flags.string({description: 'the private space to create the app in', completion: SpaceCompletion}), - region: flags.string({description: 'specify region for the app to run in', completion: RegionCompletion}), + features: flags.string({hidden: true}), 'internal-routing': flags.boolean({ - hidden: true, description: 'private space-only. create as an Internal Web App that is only routable in the local network.', + hidden: true, }), - features: flags.string({hidden: true}), + json: flags.boolean({description: 'output in json format'}), kernel: flags.string({hidden: true}), locked: flags.boolean({hidden: true}), - json: flags.boolean({description: 'output in json format'}), + manifest: flags.boolean({char: 'm', description: 'use heroku.yml settings for this app', hidden: true}), + 'no-remote': flags.boolean({char: 'n', description: 'do not create a git remote'}), + region: flags.string({completion: RegionCompletion, description: 'specify region for the app to run in'}), + remote: flags.remote({default: 'heroku', description: 'the git remote to create, default "heroku"'}), + space: flags.string({completion: SpaceCompletion, description: 'the private space to create the app in'}), + stack: flags.string({char: 's', completion: StackCompletion, description: 'the stack to create the app on'}), team: flags.team(), } + static hiddenAliases = ['create'] + async readManifest() { const buffer = await fs.readFile('heroku.yml') return parse(buffer.toString()) } + async run() { + const context = await this.parse(Create) + const {flags} = context + + if (flags.manifest) { + return this.runFromManifest(context, this.heroku) + } + + await runFromFlags(context, this.heroku, this.config) + } + async runFromManifest(context: Interfaces.ParserOutput, heroku: APIClient) { - const {flags, args} = context + const {args, flags} = context const name = flags.app || args.app || process.env.HEROKU_APP ux.action.start('Reading heroku.yml manifest') @@ -226,15 +237,4 @@ $ heroku apps:create --region eu`, printAppSummary(context, app, remoteUrl) } - - async run() { - const context = await this.parse(Create) - const {flags} = context - - if (flags.manifest) { - return this.runFromManifest(context, this.heroku) - } - - await runFromFlags(context, this.heroku, this.config) - } } diff --git a/packages/cli/src/commands/apps/destroy.ts b/packages/cli/src/commands/apps/destroy.ts index 60abc044e0..9bbefb0649 100644 --- a/packages/cli/src/commands/apps/destroy.ts +++ b/packages/cli/src/commands/apps/destroy.ts @@ -1,25 +1,29 @@ -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 * as git from '../../lib/ci/git.js' +import ConfirmCommand from '../../lib/confirmCommand.js' export default class Destroy extends Command { + static args = { + app: Args.string({hidden: true}), + } + static description = 'permanently destroy an app' - static help = 'This will also destroy all add-ons on the app.' - static hiddenAliases = ['destroy', 'apps:delete'] + static flags = { app: flags.app(), - remote: flags.remote(), confirm: flags.string({char: 'c'}), + remote: flags.remote(), } - static args = { - app: Args.string({hidden: true}), - } + static help = 'This will also destroy all add-ons on the app.' + + static hiddenAliases = ['destroy', 'apps:delete'] async run() { - const {flags, args} = await this.parse(Destroy) + const {args, flags} = await this.parse(Destroy) const app = args.app || flags.app if (!app) throw new Error('No app specified.\nUSAGE: heroku apps:destroy APPNAME') diff --git a/packages/cli/src/commands/apps/errors.ts b/packages/cli/src/commands/apps/errors.ts index d920e6f702..898d9f8d3b 100644 --- a/packages/cli/src/commands/apps/errors.ts +++ b/packages/cli/src/commands/apps/errors.ts @@ -1,11 +1,12 @@ -import {color} from '@heroku-cli/color' +/* eslint-disable perfectionist/sort-objects */ +import {color, hux} from '@heroku/heroku-cli-util' +import {HTTP} from '@heroku/http-call' import {Command, flags} from '@heroku-cli/command' +import * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' -import {HTTP} from '@heroku/http-call' import _ from 'lodash' + import errorInfo from '../../lib/apps/error_info.js' -import * as Heroku from '@heroku-cli/schema' import {AppErrors} from '../../lib/types/app_errors.js' type ErrorSummary = Record @@ -35,10 +36,22 @@ function buildErrorTable(errors: ErrorSummary, source: string) { const count = errors[name] const info = errorInfo.find(e => e.name === name) if (info) { - return {name, count, source, level: info.level, title: info.title} + return { + count, + level: info.level, + name, + source, + title: info.title, + } } - return {name, count, source, level: 'critical', title: 'unknown error'} + return { + count, + level: 'critical', + name, + source, + title: 'unknown error', + } }) } @@ -55,11 +68,11 @@ export default class Errors extends Command { static flags = { app: flags.app({required: true}), - remote: flags.remote(), + dyno: flags.boolean({description: 'show only dyno errors'}), + hours: flags.string({default: '24', description: 'number of hours to look back (default 24)'}), json: flags.boolean({description: 'output in json format'}), - hours: flags.string({description: 'number of hours to look back (default 24)', default: '24'}), + remote: flags.remote(), router: flags.boolean({description: 'show only router errors'}), - dyno: flags.boolean({description: 'show only dyno errors'}), } async run() { diff --git a/packages/cli/src/commands/apps/favorites/add.ts b/packages/cli/src/commands/apps/favorites/add.ts index 7e33bdd62d..f31e14b99b 100644 --- a/packages/cli/src/commands/apps/favorites/add.ts +++ b/packages/cli/src/commands/apps/favorites/add.ts @@ -1,16 +1,18 @@ -import {color} from '@heroku-cli/color' -import {ux} from '@oclif/core' +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' +import {ux} from '@oclif/core' + import {Favorites} from '../../../lib/types/favorites.js' export default class Add extends Command { static description = 'favorites an app' - static topic = 'apps' static flags = { app: flags.app({required: true}), remote: flags.remote(), } + static topic = 'apps' + async run() { const {flags} = await this.parse(Add) const {app} = flags @@ -28,8 +30,8 @@ export default class Add extends Command { try { await this.heroku.post('/favorites', { + body: {resource_id: app, type: 'app'}, hostname: 'particleboard.heroku.com', - body: {type: 'app', resource_id: app}, }) } catch (error: any) { if (error.statusCode === 404) { diff --git a/packages/cli/src/commands/apps/favorites/index.ts b/packages/cli/src/commands/apps/favorites/index.ts index db0410af8a..569af3e1e2 100644 --- a/packages/cli/src/commands/apps/favorites/index.ts +++ b/packages/cli/src/commands/apps/favorites/index.ts @@ -1,15 +1,17 @@ -import {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 {ux} from '@oclif/core' + import {Favorites} from '../../../lib/types/favorites.js' export default class Index extends Command { static description = 'list favorited apps' - static topic = 'apps' static flags = { json: flags.boolean({char: 'j', description: 'output in json format'}), } + static topic = 'apps' + async run() { const {flags} = await this.parse(Index) @@ -23,7 +25,7 @@ export default class Index extends Command { } else { hux.styledHeader('Favorited Apps') for (const f of favorites) { - ux.stdout(f.resource_name) + ux.stdout(color.app(f.resource_name)) } } } diff --git a/packages/cli/src/commands/apps/favorites/remove.ts b/packages/cli/src/commands/apps/favorites/remove.ts index cd72a91805..efa3dfd691 100644 --- a/packages/cli/src/commands/apps/favorites/remove.ts +++ b/packages/cli/src/commands/apps/favorites/remove.ts @@ -1,16 +1,18 @@ -import {color} from '@heroku-cli/color' -import {ux} from '@oclif/core' +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' +import {ux} from '@oclif/core' + import {Favorites} from '../../../lib/types/favorites.js' export default class Remove extends Command { static description = 'unfavorites an app' - static topic = 'apps' static flags = { app: flags.app({required: true}), remote: flags.remote(), } + static topic = 'apps' + async run() { const {flags} = await this.parse(Remove) const {app} = flags diff --git a/packages/cli/src/commands/apps/index.ts b/packages/cli/src/commands/apps/index.ts index c0a81909b0..d233e05969 100644 --- a/packages/cli/src/commands/apps/index.ts +++ b/packages/cli/src/commands/apps/index.ts @@ -1,20 +1,20 @@ -import {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 {SpaceCompletion} from '@heroku-cli/command/lib/completions.js' import * as Heroku from '@heroku-cli/schema' +import {ux} from '@oclif/core' import _ from 'lodash' -import {color} from '@heroku-cli/color' -import {SpaceCompletion} from '@heroku-cli/command/lib/completions.js' + import {App} from '../../lib/types/app.js' function annotateAppName(app: App) { - let name = `${app.name}` + let name = color.app(app.name) if (app.locked && app.internal_routing) { - name = `${app.name} [internal/locked]` + name = `${color.app(app.name)} [internal/locked]` } else if (app.locked) { - name = `${app.name} [locked]` + name = `${color.app(app.name)} [locked]` } else if (app.internal_routing) { - name = `${app.name} [internal]` + name = `${color.app(app.name)} [internal]` } return name diff --git a/packages/cli/src/commands/apps/join.ts b/packages/cli/src/commands/apps/join.ts index 380c4205d2..c70535b681 100644 --- a/packages/cli/src/commands/apps/join.ts +++ b/packages/cli/src/commands/apps/join.ts @@ -1,17 +1,18 @@ -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 * as Heroku from '@heroku-cli/schema' +import {ux} from '@oclif/core' export default class AppsJoin extends Command { - static topic = 'apps' - static description = 'add yourself to a team app' static aliases = ['join'] + static description = 'add yourself to a team app' static flags = { app: flags.app({required: true}), remote: flags.remote({char: 'r'}), } + static topic = 'apps' + public async run(): Promise { const {flags} = await this.parse(AppsJoin) const {app} = flags diff --git a/packages/cli/src/commands/apps/leave.ts b/packages/cli/src/commands/apps/leave.ts index 24e7bfde6d..d977263bb3 100644 --- a/packages/cli/src/commands/apps/leave.ts +++ b/packages/cli/src/commands/apps/leave.ts @@ -1,10 +1,9 @@ -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 * as Heroku from '@heroku-cli/schema' +import {ux} from '@oclif/core' export default class Leave extends Command { - static topic = 'apps' static aliases = ['leave'] static description = 'remove yourself from a team app' static example = 'heroku apps:leave -a APP' @@ -13,6 +12,8 @@ export default class Leave extends Command { remote: flags.remote(), } + static topic = 'apps' + public async run(): Promise { const {flags} = await this.parse(Leave) const {app} = flags diff --git a/packages/cli/src/commands/apps/transfer.ts b/packages/cli/src/commands/apps/transfer.ts index 1fce04050c..1b88eda669 100644 --- a/packages/cli/src/commands/apps/transfer.ts +++ b/packages/cli/src/commands/apps/transfer.ts @@ -1,28 +1,20 @@ 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 inquirer from 'inquirer' -import {getOwner, isTeamApp, isValidEmail} from '../../lib/teamUtils.js' -import AppsLock from './lock.js' + import {appTransfer} from '../../lib/apps/app-transfer.js' import ConfirmCommand from '../../lib/confirmCommand.js' +import {getOwner, isTeamApp, isValidEmail} from '../../lib/teamUtils.js' +import AppsLock from './lock.js' export default class AppsTransfer extends Command { - static topic = 'apps' - static description = 'transfer applications to another user or team' - static flags = { - locked: flags.boolean({char: 'l', required: false, description: 'lock the app upon transfer'}), - bulk: flags.boolean({required: false, description: 'transfer applications in bulk'}), - app: flags.app(), - remote: flags.remote({char: 'r'}), - confirm: flags.string({char: 'c', hidden: true}), - } - static args = { recipient: Args.string({description: 'user or team to transfer applications to', required: true}), } + static description = 'transfer applications to another user or team' static examples = [`$ heroku apps:transfer collaborator@example.com Transferring example to collaborator@example.com... done @@ -32,21 +24,31 @@ Transferring example to acme-widgets... done $ heroku apps:transfer --bulk acme-widgets ...`] + static flags = { + app: flags.app(), + bulk: flags.boolean({description: 'transfer applications in bulk', required: false}), + confirm: flags.string({char: 'c', hidden: true}), + locked: flags.boolean({char: 'l', description: 'lock the app upon transfer', required: false}), + remote: flags.remote({char: 'r'}), + } + + static topic = 'apps' + getAppsToTransfer(apps: Heroku.App[]) { return inquirer.prompt([{ - type: 'checkbox', - name: 'choices', - pageSize: 20, - message: 'Select applications you would like to transfer', choices: apps.map(app => ({ - name: `${app.name} (${getOwner(app.owner?.email)})`, value: {name: app.name, owner: app.owner?.email}, + name: `${color.app(app.name)} (${getOwner(app.owner?.email)})`, value: {name: app.name, owner: app.owner?.email}, })), + message: 'Select applications you would like to transfer', + name: 'choices', + pageSize: 20, + type: 'checkbox', }]) } public async run() { - const {flags, args} = await this.parse(AppsTransfer) - const {app, bulk, locked, confirm} = flags + const {args, flags} = await this.parse(AppsTransfer) + const {app, bulk, confirm, locked} = flags const {recipient} = args if (bulk) { const {body: allApps} = await this.heroku.get('/apps') @@ -55,11 +57,11 @@ $ heroku apps:transfer --bulk acme-widgets for (const app of selectedApps.choices) { try { await appTransfer({ - heroku: this.heroku, appName: app.name, - recipient, - personalToPersonal: isValidEmail(recipient) && !isTeamApp(app.owner), bulk: true, + heroku: this.heroku, + personalToPersonal: isValidEmail(recipient) && !isTeamApp(app.owner), + recipient, }) } catch (error) { const {message} = error as {message: string} @@ -74,11 +76,11 @@ $ heroku apps:transfer --bulk acme-widgets } await appTransfer({ - heroku: this.heroku, appName, - recipient, - personalToPersonal: isValidEmail(recipient) && !isTeamApp(appInfo.owner?.email), bulk, + heroku: this.heroku, + personalToPersonal: isValidEmail(recipient) && !isTeamApp(appInfo.owner?.email), + recipient, }) if (locked) { await AppsLock.run(['--app', appName], this.config) diff --git a/packages/cli/src/commands/apps/unlock.ts b/packages/cli/src/commands/apps/unlock.ts index 9fd63de466..4b5a83e14e 100644 --- a/packages/cli/src/commands/apps/unlock.ts +++ b/packages/cli/src/commands/apps/unlock.ts @@ -1,17 +1,18 @@ -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 * as Heroku from '@heroku-cli/schema' +import {ux} from '@oclif/core' export default class Unlock extends Command { - static topic = 'apps' - static description = 'unlock an app so any team member can join' static aliases = ['unlock'] + static description = 'unlock an app so any team member can join' static flags = { app: flags.app({required: true}), remote: flags.remote(), } + static topic = 'apps' + public async run(): Promise { const {flags} = await this.parse(Unlock) const {app} = flags diff --git a/packages/cli/src/commands/buildpacks/index.ts b/packages/cli/src/commands/buildpacks/index.ts index 1f8a8991e4..b48dc3501c 100644 --- a/packages/cli/src/commands/buildpacks/index.ts +++ b/packages/cli/src/commands/buildpacks/index.ts @@ -1,10 +1,9 @@ +import {color, hux} from '@heroku/heroku-cli-util' import {Command, flags as Flags} from '@heroku-cli/command' -import {hux} from '@heroku/heroku-cli-util' -import {App} from '../../lib/types/fir.js' -import {color} from '@heroku-cli/color' -import {BuildpackCommand} from '../../lib/buildpacks/buildpacks.js' import {getGeneration} from '../../lib/apps/generation.js' +import {BuildpackCommand} from '../../lib/buildpacks/buildpacks.js' +import {App} from '../../lib/types/fir.js' export default class Index extends Command { static description = 'list the buildpacks on an app' diff --git a/packages/cli/src/commands/buildpacks/remove.ts b/packages/cli/src/commands/buildpacks/remove.ts index af27c8438d..a7c39b4764 100644 --- a/packages/cli/src/commands/buildpacks/remove.ts +++ b/packages/cli/src/commands/buildpacks/remove.ts @@ -1,24 +1,25 @@ +import {color} from '@heroku/heroku-cli-util' import {Command, flags as Flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' import {BuildpackCommand} from '../../lib/buildpacks/buildpacks.js' export default class Remove extends Command { + static args = { + buildpack: Args.string({ + description: 'namespace/name of the buildpack', + }), + } + static description = 'remove a buildpack set on the app' static flags = { app: Flags.app({required: true}), - remote: Flags.remote(), index: Flags.integer({ - description: 'the 1-based index of the URL to remove from the list of URLs', char: 'i', + description: 'the 1-based index of the URL to remove from the list of URLs', }), - } - - static args = { - buildpack: Args.string({ - description: 'namespace/name of the buildpack', - }), + remote: Flags.remote(), } async run() { @@ -35,7 +36,7 @@ export default class Remove extends Command { const buildpacks = await buildpackCommand.fetch(flags.app) if (buildpacks.length === 0) { - ux.error(`No buildpacks were found. Next release on ${flags.app} will detect buildpack normally.`, {exit: 1}) + ux.error(`No buildpacks were found. Next release on ${color.app(flags.app)} will detect buildpack normally.`, {exit: 1}) } let spliceIndex: number diff --git a/packages/cli/src/commands/certs/add.ts b/packages/cli/src/commands/certs/add.ts index a88177c17e..7db642d30d 100644 --- a/packages/cli/src/commands/certs/add.ts +++ b/packages/cli/src/commands/certs/add.ts @@ -1,20 +1,24 @@ +import {hux} from '@heroku/heroku-cli-util' import {color} from '@heroku-cli/color' import {APIClient, Command, flags} from '@heroku-cli/command' -import {Args, ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' import * as Heroku from '@heroku-cli/schema' -import {waitForDomains} from '../../lib/certs/domains.js' +import {Args, ux} from '@oclif/core' import inquirer from 'inquirer' -import {CertAndKeyManager} from '../../lib/certs/get_cert_and_key.js' import tsheredoc from 'tsheredoc' -import {SniEndpoint} from '../../lib/types/sni_endpoint.js' + import {displayCertificateDetails} from '../../lib/certs/certificate_details.js' +import {waitForDomains} from '../../lib/certs/domains.js' +import {CertAndKeyManager} from '../../lib/certs/get_cert_and_key.js' +import {SniEndpoint} from '../../lib/types/sni_endpoint.js' const heredoc = tsheredoc.default export default class Add extends Command { - static topic = 'certs' - static strict = true + static args = { + CRT: Args.string({description: 'absolute path of the certificate file on disk', required: true}), + KEY: Args.string({description: 'absolute path of the key file on disk', required: true}), + } + static description = `Add an SSL certificate to an app. Note: certificates with PEM encoding are also valid. @@ -30,19 +34,9 @@ export default class Add extends Command { remote: flags.remote(), } - static args = { - CRT: Args.string({required: true, description: 'absolute path of the certificate file on disk'}), - KEY: Args.string({required: true, description: 'absolute path of the key file on disk'}), - } + static strict = true - getDomainsToAssociate(sniEndpoint: SniEndpoint) { - return inquirer.prompt<{domains: string[]}>([{ - type: 'checkbox', - name: 'domains', - message: 'Select domains', - choices: sniEndpoint.ssl_cert.cert_domains, - }]) - } + static topic = 'certs' async configureDomains(app: string, heroku: APIClient, cert: SniEndpoint) { const certDomains = cert.ssl_cert.cert_domains @@ -58,21 +52,21 @@ export default class Add extends Command { } } - async selectDomains(domainOptions: string[]) { + getDomainsToAssociate(sniEndpoint: SniEndpoint) { return inquirer.prompt<{domains: string[]}>([{ - type: 'checkbox', - name: 'domains', + choices: sniEndpoint.ssl_cert.cert_domains, message: 'Select domains', - choices: domainOptions, + name: 'domains', + type: 'checkbox', }]) } public async run(): Promise { - const {flags, args} = await this.parse(Add) + const {args, flags} = await this.parse(Add) const {app} = flags const certManager = new CertAndKeyManager() const files = await certManager.getCertAndKey(args) - ux.action.start(`Adding SSL certificate to ${color.magenta(app)}`) + ux.action.start(`Adding SSL certificate to ${color.app(app)}`) const {body: sniEndpoint} = await this.heroku.post(`/apps/${app}/sni-endpoints`, { body: { certificate_chain: files.crt.toString(), @@ -84,6 +78,15 @@ export default class Add extends Command { displayCertificateDetails(sniEndpoint) await this.configureDomains(app, this.heroku, sniEndpoint) } + + async selectDomains(domainOptions: string[]) { + return inquirer.prompt<{domains: string[]}>([{ + choices: domainOptions, + message: 'Select domains', + name: 'domains', + type: 'checkbox', + }]) + } } function splitDomains(domains: string[]): [string, string][] { @@ -98,7 +101,7 @@ function createMatcherFromSplitDomain([firstChar, rest]: [string, string]) { matcherContents.push(firstChar) } - const escapedRest = rest.replace(/\./g, '\\.') + const escapedRest = rest.replaceAll('.', '\\.') matcherContents.push(escapedRest) diff --git a/packages/cli/src/commands/certs/auto/disable.ts b/packages/cli/src/commands/certs/auto/disable.ts index 94917a2edf..b637741974 100644 --- a/packages/cli/src/commands/certs/auto/disable.ts +++ b/packages/cli/src/commands/certs/auto/disable.ts @@ -1,20 +1,22 @@ -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 ConfirmCommand from '../../../lib/confirmCommand.js' import tsheredoc from 'tsheredoc' +import ConfirmCommand from '../../../lib/confirmCommand.js' + const heredoc = tsheredoc.default export default class Disable extends Command { - static topic = 'certs' static description = 'disable ACM for an app' static flags = { - confirm: flags.string({char: 'c', hidden: true}), app: flags.app({required: true}), + confirm: flags.string({char: 'c', hidden: true}), remote: flags.remote(), } + static topic = 'certs' + public async run(): Promise { const {flags} = await this.parse(Disable) const {app, confirm} = flags diff --git a/packages/cli/src/commands/certs/auto/index.ts b/packages/cli/src/commands/certs/auto/index.ts index b45ad4f1b3..ede65e2213 100644 --- a/packages/cli/src/commands/certs/auto/index.ts +++ b/packages/cli/src/commands/certs/auto/index.ts @@ -1,18 +1,19 @@ +import {hux, color as newColor} from '@heroku/heroku-cli-util' import {color} from '@heroku-cli/color' import {Command, flags} from '@heroku-cli/command' -import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' import * as Heroku from '@heroku-cli/schema' +import {ux} from '@oclif/core' +import {formatDistanceToNow} from 'date-fns' +import tsheredoc from 'tsheredoc' + import {displayCertificateDetails} from '../../../lib/certs/certificate_details.js' import {waitForCertIssuedOnDomains} from '../../../lib/domains/domains.js' -import {formatDistanceToNow} from 'date-fns' -import {SniEndpoint} from '../../../lib/types/sni_endpoint.js' import {Domain} from '../../../lib/types/domain.js' -import tsheredoc from 'tsheredoc' +import {SniEndpoint} from '../../../lib/types/sni_endpoint.js' const heredoc = tsheredoc.default -function humanize(value: string | null) { +function humanize(value: null | string) { if (!value) { return color.yellow('Waiting') } @@ -39,15 +40,16 @@ function humanize(value: string | null) { } export default class Index extends Command { - static topic = 'certs' static command: 'auto' static description = 'show ACM status for an app' static flags = { - wait: flags.boolean({description: 'watch ACM status and display the status when complete'}), app: flags.app({required: true}), remote: flags.remote(), + wait: flags.boolean({description: 'watch ACM status and display the status when complete'}), } + static topic = 'certs' + public async run(): Promise { const {flags} = await this.parse(Index) const [{body: app}, {body: sniEndpoints}] = await Promise.all([ @@ -56,11 +58,11 @@ export default class Index extends Command { ]) if (!app.acm) { - hux.styledHeader(`Automatic Certificate Management is ${color.yellow('disabled')} on ${flags.app}`) + hux.styledHeader(`Automatic Certificate Management is ${color.yellow('disabled')} on ${newColor.app(flags.app)}`) return } - hux.styledHeader(`Automatic Certificate Management is ${color.green('enabled')} on ${flags.app}`) + hux.styledHeader(`Automatic Certificate Management is ${color.green('enabled')} on ${newColor.app(flags.app)}`) if (sniEndpoints.length === 1 && sniEndpoints[0].ssl_cert.acm) { displayCertificateDetails(sniEndpoints[0]) @@ -104,8 +106,8 @@ export default class Index extends Command { }, } : {}), lastUpdated: { - header: 'Last Updated', get: (domain: Domain) => formatDistanceToNow(new Date(domain.updated_at)), + header: 'Last Updated', }, }, ) diff --git a/packages/cli/src/commands/certs/index.ts b/packages/cli/src/commands/certs/index.ts index 4a5adfeaf3..520c8e3cc5 100644 --- a/packages/cli/src/commands/certs/index.ts +++ b/packages/cli/src/commands/certs/index.ts @@ -1,23 +1,26 @@ +import {color as newColor} from '@heroku/heroku-cli-util' import {color} from '@heroku-cli/color' import {Command, flags} from '@heroku-cli/command' import {ux} from '@oclif/core' + import displayTable from '../../lib/certs/display_table.js' import {SniEndpoint} from '../../lib/types/sni_endpoint.js' export default class Index extends Command { - static topic = 'certs' static description = 'list SSL certificates for an app' static flags = { app: flags.app({required: true}), remote: flags.remote(), } + static topic = 'certs' + public async run(): Promise { const {flags} = await this.parse(Index) const {body: certs} = await this.heroku.get(`/apps/${flags.app}/sni-endpoints`) if (certs.length === 0) { - ux.stdout(`${color.app(flags.app)} has no SSL certificates.\nUse ${color.cmd('heroku certs:add CRT KEY')} to add one.`) + ux.stdout(`${newColor.app(flags.app)} has no SSL certificates.\nUse ${color.cmd('heroku certs:add CRT KEY')} to add one.`) } else { const sortedCerts = certs.sort((a, b) => a.name > b.name ? 1 : (b.name > a.name ? -1 : 0)) displayTable(sortedCerts) diff --git a/packages/cli/src/commands/certs/info.ts b/packages/cli/src/commands/certs/info.ts index d965803686..1113f388c6 100644 --- a/packages/cli/src/commands/certs/info.ts +++ b/packages/cli/src/commands/certs/info.ts @@ -1,22 +1,24 @@ -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 getEndpoint from '../../lib/certs/flags.js' + import {displayCertificateDetails} from '../../lib/certs/certificate_details.js' -import {SniEndpoint} from '../../lib/types/sni_endpoint.js' +import getEndpoint from '../../lib/certs/flags.js' import {Domain} from '../../lib/types/domain.js' +import {SniEndpoint} from '../../lib/types/sni_endpoint.js' export default class Info extends Command { - static topic = 'certs' static description = 'show certificate information for an SSL certificate' static flags = { - name: flags.string({description: 'name to check info on'}), - endpoint: flags.string({description: 'endpoint to check info on'}), - 'show-domains': flags.boolean({description: 'show associated domains'}), app: flags.app({required: true}), + endpoint: flags.string({description: 'endpoint to check info on'}), + name: flags.string({description: 'name to check info on'}), remote: flags.remote(), + 'show-domains': flags.boolean({description: 'show associated domains'}), } + static topic = 'certs' + public async run(): Promise { const {flags} = await this.parse(Info) const {app} = flags diff --git a/packages/cli/src/commands/certs/remove.ts b/packages/cli/src/commands/certs/remove.ts index 406553f5e4..de9fd04b06 100644 --- a/packages/cli/src/commands/certs/remove.ts +++ b/packages/cli/src/commands/certs/remove.ts @@ -1,22 +1,25 @@ -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' + import getEndpoint from '../../lib/certs/flags.js' import ConfirmCommand from '../../lib/confirmCommand.js' -import tsheredoc from 'tsheredoc' + const heredoc = tsheredoc.default export default class Remove extends Command { - static topic = 'certs' static description = 'remove an SSL certificate from an app' static flags = { + app: flags.app({required: true}), confirm: flags.string({hidden: true}), - name: flags.string({description: 'name to remove'}), endpoint: flags.string({description: 'endpoint to remove'}), - app: flags.app({required: true}), + name: flags.string({description: 'name to remove'}), remote: flags.remote(), } + static topic = 'certs' + public async run(): Promise { const {flags} = await this.parse(Remove) const {app, confirm} = flags diff --git a/packages/cli/src/commands/certs/update.ts b/packages/cli/src/commands/certs/update.ts index 0f318ed790..feeb023c2a 100644 --- a/packages/cli/src/commands/certs/update.ts +++ b/packages/cli/src/commands/certs/update.ts @@ -1,34 +1,26 @@ -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 {displayCertificateDetails} from '../../lib/certs/certificate_details.js' -import {CertAndKeyManager} from '../../lib/certs/get_cert_and_key.js' import tsheredoc from 'tsheredoc' + +import {displayCertificateDetails} from '../../lib/certs/certificate_details.js' import getEndpoint from '../../lib/certs/flags.js' +import {CertAndKeyManager} from '../../lib/certs/get_cert_and_key.js' import ConfirmCommand from '../../lib/confirmCommand.js' import {SniEndpoint} from '../../lib/types/sni_endpoint.js' const heredoc = tsheredoc.default export default class Update extends Command { - static topic = 'certs' + static args = { + CRT: Args.string({description: 'absolute path of the certificate file on disk', required: true}), + KEY: Args.string({description: 'absolute path of the key file on disk', required: true}), + } + static description = heredoc` update an SSL certificate on an app Note: certificates with PEM encoding are also valid ` - static flags = { - confirm: flags.string({hidden: true}), - name: flags.string({description: 'name to update'}), - endpoint: flags.string({description: 'endpoint to update'}), - app: flags.app({required: true}), - remote: flags.remote(), - } - - static args = { - CRT: Args.string({required: true, description: 'absolute path of the certificate file on disk'}), - KEY: Args.string({required: true, description: 'absolute path of the key file on disk'}), - } - static examples = [heredoc` $ heroku certs:update example.com.crt example.com.key @@ -36,8 +28,18 @@ export default class Update extends Command { https://help.salesforce.com/s/articleView?id=000333504&type=1 `] + static flags = { + app: flags.app({required: true}), + confirm: flags.string({hidden: true}), + endpoint: flags.string({description: 'endpoint to update'}), + name: flags.string({description: 'name to update'}), + remote: flags.remote(), + } + + static topic = 'certs' + public async run(): Promise { - const {flags, args} = await this.parse(Update) + const {args, flags} = await this.parse(Update) const {app, confirm} = flags let sniEndpoint = await getEndpoint(flags, this.heroku) const files = await new CertAndKeyManager().getCertAndKey(args) @@ -47,11 +49,11 @@ export default class Update extends Command { confirm, heredoc` Potentially Destructive Action - This command will change the certificate of endpoint ${sniEndpoint.name} from ${color.magenta(app)}. + This command will change the certificate of endpoint ${sniEndpoint.name} from ${color.app(app)}. `, ) - ux.action.start(`Updating SSL certificate ${sniEndpoint.name} for ${color.magenta(app)}`); + ux.action.start(`Updating SSL certificate ${sniEndpoint.name} for ${color.app(app)}`); ({body: sniEndpoint} = await this.heroku.patch(`/apps/${app}/sni-endpoints/${sniEndpoint.name}`, { body: { certificate_chain: files.crt, diff --git a/packages/cli/src/commands/config/edit.ts b/packages/cli/src/commands/config/edit.ts index 0d2e756ae3..29aa667a9e 100644 --- a/packages/cli/src/commands/config/edit.ts +++ b/packages/cli/src/commands/config/edit.ts @@ -1,9 +1,7 @@ -import {color} from '@heroku-cli/color' +import {color, hux} 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 {hux} from '@heroku/heroku-cli-util' - import _ from 'lodash' import {parse, quote} from '../../lib/config/quote.js' @@ -64,6 +62,10 @@ function showDiff(from: Config, to: Config) { } export default class ConfigEdit extends Command { + static args = { + key: Args.string({description: 'edit a single key', optional: true}), + } + static description = `interactively edit config vars This command opens the app config in a text editor set by $VISUAL or $EDITOR. Any variables added/removed/changed will be updated on the app after saving and closing the file.` @@ -84,14 +86,10 @@ $ VISUAL="atom --wait" heroku config:edit`, remote: flags.remote(), } - static args = { - key: Args.string({optional: true, description: 'edit a single key'}), - } - app!: string async run() { - const {flags: {app}, args: {key}} = await this.parse(ConfigEdit) + const {args: {key}, flags: {app}} = await this.parse(ConfigEdit) this.app = app ux.action.start('Fetching config') const original = await this.fetchLatestConfig() @@ -103,7 +101,7 @@ $ VISUAL="atom --wait" heroku config:edit`, newConfig[key] = await editor.edit(original[key], {prefix}) if (!original[key].endsWith('\n') && newConfig[key].endsWith('\n')) newConfig[key] = newConfig[key].slice(0, -1) } else { - const s = await editor.edit(configToString(original), {prefix, postfix: '.sh'}) + const s = await editor.edit(configToString(original), {postfix: '.sh', prefix}) newConfig = stringToConfig(s) } @@ -116,11 +114,6 @@ $ VISUAL="atom --wait" heroku config:edit`, ux.action.stop() } - private async fetchLatestConfig() { - const {body: original} = await this.heroku.get(`/apps/${this.app}/config-vars`) - return original - } - private async diffPrompt(original: Config, newConfig: Config): Promise { if (_.isEqual(original, newConfig)) { this.warn('no changes to config') @@ -134,11 +127,9 @@ $ VISUAL="atom --wait" heroku config:edit`, return hux.confirm(`Update config on ${color.app(this.app)} with these values?`) } - private async verifyUnchanged(original: Config) { - const latest = await this.fetchLatestConfig() - if (!_.isEqual(original, latest)) { - throw new Error('Config changed on server. Refusing to update.') - } + private async fetchLatestConfig() { + const {body: original} = await this.heroku.get(`/apps/${this.app}/config-vars`) + return original } private async updateConfig(newConfig: UploadConfig) { @@ -146,4 +137,11 @@ $ VISUAL="atom --wait" heroku config:edit`, body: newConfig, }) } + + private async verifyUnchanged(original: Config) { + const latest = await this.fetchLatestConfig() + if (!_.isEqual(original, latest)) { + throw new Error('Config changed on server. Refusing to update.') + } + } } diff --git a/packages/cli/src/commands/config/index.ts b/packages/cli/src/commands/config/index.ts index 66699389e4..fb0f540643 100644 --- a/packages/cli/src/commands/config/index.ts +++ b/packages/cli/src/commands/config/index.ts @@ -1,8 +1,8 @@ +import {hux, color as newColor} from '@heroku/heroku-cli-util' import {color} from '@heroku-cli/color' import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' import {quote} from '../../lib/config/quote.js' @@ -11,9 +11,9 @@ export class ConfigIndex extends Command { static flags = { app: flags.app({required: true}), + json: flags.boolean({char: 'j', description: 'output config vars in json format'}), remote: flags.remote(), shell: flags.boolean({char: 's', description: 'output config vars in shell format'}), - json: flags.boolean({char: 'j', description: 'output config vars in json format'}), } async run() { @@ -25,7 +25,7 @@ export class ConfigIndex extends Command { } else if (flags.json) { hux.styledJSON(config) } else { - hux.styledHeader(`${flags.app} Config Vars`) + hux.styledHeader(`${newColor.app(flags.app)} Config Vars`) const coloredConfig = Object.fromEntries( Object.entries(config).map(([key, value]) => [color.configVar(key), value]), ) diff --git a/packages/cli/src/commands/domains/clear.ts b/packages/cli/src/commands/domains/clear.ts index 1a1e397390..4b0be06f70 100644 --- a/packages/cli/src/commands/domains/clear.ts +++ b/packages/cli/src/commands/domains/clear.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 * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' diff --git a/packages/cli/src/commands/domains/index.ts b/packages/cli/src/commands/domains/index.ts index c003c49962..9439008f88 100644 --- a/packages/cli/src/commands/domains/index.ts +++ b/packages/cli/src/commands/domains/index.ts @@ -1,17 +1,18 @@ -import {Command, flags} from '@heroku-cli/command' +import {hux, color as newColor} from '@heroku/heroku-cli-util' import {color} from '@heroku-cli/color' +import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' -import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' -import Uri from 'urijs' import {confirm} from '@inquirer/prompts' +import {ux} from '@oclif/core' import {orderBy} from 'natural-orderby' -import {paginateRequest} from '../../lib/utils/paginator.js' +import Uri from 'urijs' + import parseKeyValue from '../../lib/utils/keyValueParser.js' +import {paginateRequest} from '../../lib/utils/paginator.js' function isApexDomain(hostname: string) { if (hostname.includes('*')) return false - const a = new Uri({protocol: 'http', hostname}) + const a = new Uri({hostname, protocol: 'http'}) return a.subdomain() === '' } @@ -32,70 +33,16 @@ www.example.com CNAME www.example.herokudns.com static flags = { app: flags.app({required: true}), columns: flags.string({description: 'only show provided columns (comma-separated)'}), - extended: flags.boolean({description: 'show extra columns', char: 'x'}), + csv: flags.boolean({char: 'c', description: 'output in csv format'}), + extended: flags.boolean({char: 'x', description: 'show extra columns'}), filter: flags.string({description: 'filter property by partial string matching, ex: name=foo'}), - json: flags.boolean({description: 'output in json format', char: 'j'}), - csv: flags.boolean({description: 'output in csv format', char: 'c'}), + json: flags.boolean({char: 'j', description: 'output in json format'}), remote: flags.remote(), sort: flags.string({description: 'sort by property'}), } - tableConfig = (needsEndpoints: boolean, extended: boolean, requestedColumns?: string[]) => { - const tableConfig: Record = { - hostname: { - header: 'Domain Name', - }, - kind: { - get(domain: Heroku.Domain) { - if (domain.hostname) { - return isApexDomain(domain.hostname) ? 'ALIAS or ANAME' : 'CNAME' - } - }, - header: 'DNS Record Type', - }, - cname: {header: 'DNS Target'}, - } - - if (extended) { - tableConfig.acm_status = {header: 'ACM Status'} - tableConfig.acm_status_reason = {header: 'ACM Status'} - } - - const sniConfig = { - sni_endpoint: { - header: 'SNI Endpoint', - get(domain: Heroku.Domain) { - if (domain.sni_endpoint) { - return domain.sni_endpoint.name - } - }, - }, - } - - let fullConfig = tableConfig - if (needsEndpoints) { - fullConfig = { - ...tableConfig, - ...sniConfig, - } - } - - // If specific columns are requested, filter the configuration - if (requestedColumns && requestedColumns.length > 0) { - const filteredConfig: Record = {} - requestedColumns.forEach(columnKey => { - if (fullConfig[columnKey]) { - filteredConfig[columnKey] = fullConfig[columnKey] - } - }) - return filteredConfig - } - - return fullConfig - } - getFilteredDomains = (filterKeyValue: string, domains: Array) => { - const filteredInfo = {size: 0, filteredDomains: domains} + const filteredInfo = {filteredDomains: domains, size: 0} const {key: filterName, value} = parseKeyValue(filterKeyValue) if (!value) { @@ -128,28 +75,28 @@ www.example.com CNAME www.example.herokudns.com return filteredInfo } - mapSortFieldToProperty = (sortField: string): string => { - const headerToPropertyMap: Record = { - 'Domain Name': 'hostname', + mapColumnHeadersToKeys = (columnHeaders: string[]): string[] => { + const headerToKeyMap: Record = { + 'ACM Status': 'acm_status', 'DNS Record Type': 'kind', 'DNS Target': 'cname', + 'Domain Name': 'hostname', 'SNI Endpoint': 'sni_endpoint', - 'ACM Status': 'acm_status', } - return headerToPropertyMap[sortField] || sortField + return columnHeaders.map(header => headerToKeyMap[header.trim()] || header.trim()) } - mapColumnHeadersToKeys = (columnHeaders: string[]): string[] => { - const headerToKeyMap: Record = { - 'Domain Name': 'hostname', + mapSortFieldToProperty = (sortField: string): string => { + const headerToPropertyMap: Record = { + 'ACM Status': 'acm_status', 'DNS Record Type': 'kind', 'DNS Target': 'cname', + 'Domain Name': 'hostname', 'SNI Endpoint': 'sni_endpoint', - 'ACM Status': 'acm_status', } - return columnHeaders.map(header => headerToKeyMap[header.trim()] || header.trim()) + return headerToPropertyMap[sortField] || sortField } outputCSV = (customDomains: Heroku.Domain[], tableConfig: Record, sortProperty?: string) => { @@ -178,6 +125,61 @@ www.example.com CNAME www.example.herokudns.com } } + tableConfig = (needsEndpoints: boolean, extended: boolean, requestedColumns?: string[]) => { + const tableConfig: Record = { + hostname: { + header: 'Domain Name', + }, + kind: { + get(domain: Heroku.Domain) { + if (domain.hostname) { + return isApexDomain(domain.hostname) ? 'ALIAS or ANAME' : 'CNAME' + } + }, + header: 'DNS Record Type', + }, + // eslint-disable-next-line perfectionist/sort-objects + cname: {header: 'DNS Target'}, + } + + if (extended) { + tableConfig.acm_status = {header: 'ACM Status'} + tableConfig.acm_status_reason = {header: 'ACM Status'} + } + + const sniConfig = { + sni_endpoint: { + get(domain: Heroku.Domain) { + if (domain.sni_endpoint) { + return domain.sni_endpoint.name + } + }, + header: 'SNI Endpoint', + }, + } + + let fullConfig = tableConfig + if (needsEndpoints) { + fullConfig = { + ...tableConfig, + ...sniConfig, + } + } + + // If specific columns are requested, filter the configuration + if (requestedColumns && requestedColumns.length > 0) { + const filteredConfig: Record = {} + requestedColumns.forEach(columnKey => { + if (fullConfig[columnKey]) { + filteredConfig[columnKey] = fullConfig[columnKey] + } + }) + return filteredConfig + } + + return fullConfig + } + async confirmDisplayAllDomains(customDomains: Heroku.Domain[]) { return confirm({default: false, message: `Display all ${customDomains.length} domains?`, theme: {prefix: '', style: {defaultAnswer: () => '(Y/N)'}}}) } @@ -196,7 +198,7 @@ www.example.com CNAME www.example.herokudns.com if (flags.json) { hux.styledJSON(domains) } else { - hux.styledHeader(`${flags.app} Heroku Domain`) + hux.styledHeader(`${newColor.app(flags.app)} Heroku Domain`) ux.stdout(herokuDomain && herokuDomain.hostname) if (customDomains && customDomains.length > 0) { ux.stdout() @@ -210,7 +212,7 @@ www.example.com CNAME www.example.herokudns.com } ux.stdout() - hux.styledHeader(`${flags.app} Custom Domains`) + hux.styledHeader(`${newColor.app(flags.app)} Custom Domains`) const tableConfig = this.tableConfig(true, flags.extended, flags.columns ? this.mapColumnHeadersToKeys(flags.columns.split(',')) : undefined) const sortProperty = this.mapSortFieldToProperty(flags.sort) diff --git a/packages/cli/src/commands/domains/remove.ts b/packages/cli/src/commands/domains/remove.ts index b526b74230..b0ce4d9d63 100644 --- a/packages/cli/src/commands/domains/remove.ts +++ b/packages/cli/src/commands/domains/remove.ts @@ -1,8 +1,12 @@ -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' export default class DomainsRemove extends Command { + static args = { + hostname: Args.string({description: 'unique identifier of the domain or full hostname', required: true}), + } + static description = 'remove a domain from an app' static examples = ['heroku domains:remove www.example.com'] @@ -12,10 +16,6 @@ export default class DomainsRemove extends Command { remote: flags.remote(), } - static args = { - hostname: Args.string({required: true, description: 'unique identifier of the domain or full hostname'}), - } - async run() { const {args, flags} = await this.parse(DomainsRemove) diff --git a/packages/cli/src/commands/maintenance/off.ts b/packages/cli/src/commands/maintenance/off.ts index ba43097109..d072a761bf 100644 --- a/packages/cli/src/commands/maintenance/off.ts +++ b/packages/cli/src/commands/maintenance/off.ts @@ -1,17 +1,17 @@ +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' -import {color} from '@heroku-cli/color' import {ux} from '@oclif/core' export default class MaintenanceOff extends Command { static description = 'take the app out of maintenance mode' - static topic = 'maintenance' - static flags = { app: flags.app({required: true}), remote: flags.remote(), } + static topic = 'maintenance' + async run() { const {flags} = await this.parse(MaintenanceOff) ux.action.start(`Disabling maintenance mode for ${color.app(flags.app)}`) diff --git a/packages/cli/src/commands/maintenance/on.ts b/packages/cli/src/commands/maintenance/on.ts index b3a9d65243..44ba9a6137 100644 --- a/packages/cli/src/commands/maintenance/on.ts +++ b/packages/cli/src/commands/maintenance/on.ts @@ -1,17 +1,17 @@ +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' -import {color} from '@heroku-cli/color' import {ux} from '@oclif/core' export default class MaintenanceOn extends Command { static description = 'put the app into maintenance mode' - static topic = 'maintenance' - static flags = { app: flags.app({required: true}), remote: flags.remote(), } + static topic = 'maintenance' + async run() { const {flags} = await this.parse(MaintenanceOn) ux.action.start(`Enabling maintenance mode for ${color.app(flags.app)}`) diff --git a/packages/cli/src/commands/pg/backups/delete.ts b/packages/cli/src/commands/pg/backups/delete.ts index 4ac132ed3c..9d0c5b20cd 100644 --- a/packages/cli/src/commands/pg/backups/delete.ts +++ b/packages/cli/src/commands/pg/backups/delete.ts @@ -1,29 +1,31 @@ -import {color} from '@heroku-cli/color' +import {color, utils} 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 {utils} from '@heroku/heroku-cli-util' import backupsFactory from '../../../lib/pg/backups.js' export default class Delete extends Command { - static topic = 'pg' - static description = 'delete a backup' - static flags = { - confirm: flags.string({char: 'c', hidden: true}), - app: flags.app({required: true}), - remote: flags.remote(), - } - static args = { - backup_id: Args.string({required: true, description: 'ID of the backup'}), + backup_id: Args.string({description: 'ID of the backup', required: true}), } + static description = 'delete a backup' + static examples = [ '$ heroku pg:backup:delete --app APP_ID BACKUP_ID', ] + static flags = { + app: flags.app({required: true}), + confirm: flags.string({char: 'c', hidden: true}), + remote: flags.remote(), + } + + static topic = 'pg' + public async run(): Promise { - const {flags, args} = await this.parse(Delete) + const {args, flags} = await this.parse(Delete) const {app, confirm} = flags const {backup_id} = args const pgbackups = backupsFactory(app, this.heroku) diff --git a/packages/cli/src/commands/pg/backups/download.ts b/packages/cli/src/commands/pg/backups/download.ts index c5e9838170..ea2ebf3fe4 100644 --- a/packages/cli/src/commands/pg/backups/download.ts +++ b/packages/cli/src/commands/pg/backups/download.ts @@ -1,12 +1,13 @@ -import {color} from '@heroku-cli/color' +import {color, utils} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' -import {utils} from '@heroku/heroku-cli-util' -import pgBackupsApi from '../../../lib/pg/backups.js' -import download from '../../../lib/pg/download.js' import fs from 'fs-extra' + import type {BackupTransfer, PublicUrlResponse} from '../../../lib/pg/types.js' +import pgBackupsApi from '../../../lib/pg/backups.js' +import download from '../../../lib/pg/download.js' + function defaultFilename() { let f = 'latest.dump' if (!fs.existsSync(f)) @@ -19,25 +20,27 @@ function defaultFilename() { } export default class Download extends Command { - static topic = 'pg' + static args = { + backup_id: Args.string({description: 'ID of the backup. If omitted, we use the last backup ID.'}), + } + static description = 'downloads database backup' + static flags = { - output: flags.string({char: 'o', description: 'location to download to. Defaults to latest.dump'}), app: flags.app({required: true}), + output: flags.string({char: 'o', description: 'location to download to. Defaults to latest.dump'}), remote: flags.remote(), } - static args = { - backup_id: Args.string({description: 'ID of the backup. If omitted, we use the last backup ID.'}), - } + static topic = 'pg' public async run(): Promise { - const {flags, args} = await this.parse(Download) + const {args, flags} = await this.parse(Download) const {backup_id} = args const {app} = flags const output = flags.output || defaultFilename() let num - ux.action.start(`Getting backup from ${color.magenta(app)}`) + ux.action.start(`Getting backup from ${color.app(app)}`) if (backup_id) { num = await pgBackupsApi(app, this.heroku).num(backup_id) if (!num) @@ -48,7 +51,7 @@ export default class Download extends Command { .filter(t => t.succeeded && t.to_type === 'gof3r') .sort((a, b) => b.created_at.localeCompare(a.created_at))[0] if (!lastBackup) - throw new Error(`No backups on ${color.magenta(app)}. Capture one with ${color.cyan.bold('heroku pg:backups:capture')}`) + throw new Error(`No backups on ${color.app(app)}. Capture one with ${color.cyan.bold('heroku pg:backups:capture')}`) num = lastBackup.num } diff --git a/packages/cli/src/commands/pg/backups/restore.ts b/packages/cli/src/commands/pg/backups/restore.ts index ee95cf9340..9628fc815e 100644 --- a/packages/cli/src/commands/pg/backups/restore.ts +++ b/packages/cli/src/commands/pg/backups/restore.ts @@ -1,11 +1,12 @@ -import {color} from '@heroku-cli/color' +import {color, utils} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' import tsheredoc from 'tsheredoc' + +import type {BackupTransfer} from '../../../lib/pg/types.js' + import ConfirmCommand from '../../../lib/confirmCommand.js' import backupsFactory from '../../../lib/pg/backups.js' -import {utils} from '@heroku/heroku-cli-util' -import type {BackupTransfer} from '../../../lib/pg/types.js' import {nls} from '../../../nls.js' const heredoc = tsheredoc.default @@ -24,29 +25,13 @@ function dropboxURL(url: string) { } export default class Restore extends Command { - static topic = 'pg' - static description = 'restore a backup (default latest) to a database' - static flags = { - 'wait-interval': flags.integer({default: 3}), - extensions: flags.string({ - char: 'e', - description: heredoc(` - comma-separated list of extensions to pre-install in the default - public schema or an optional custom schema - (for example: hstore or myschema.hstore) - `), - }), - verbose: flags.boolean({char: 'v'}), - confirm: flags.string({char: 'c'}), - app: flags.app({required: true}), - remote: flags.remote(), - } - static args = { backup: Args.string({description: 'URL or backup ID from another app'}), database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:default:suffix')}`}), } + static description = 'restore a backup (default latest) to a database' + static examples = [ heredoc(` # Basic Restore from Backup ID @@ -74,9 +59,31 @@ export default class Restore extends Command { `), ] + static flags = { + app: flags.app({required: true}), + confirm: flags.string({char: 'c'}), + extensions: flags.string({ + char: 'e', + description: heredoc(` + comma-separated list of extensions to pre-install in the default + public schema or an optional custom schema + (for example: hstore or myschema.hstore) + `), + }), + remote: flags.remote(), + verbose: flags.boolean({char: 'v'}), + 'wait-interval': flags.integer({default: 3}), + } + + static topic = 'pg' + + protected getSortedExtensions(extensions: null | string | undefined): string[] | undefined { + return extensions?.split(',').map(ext => ext.trim().toLowerCase()).sort() + } + public async run(): Promise { - const {flags, args} = await this.parse(Restore) - const {app, 'wait-interval': waitInterval, extensions, confirm, verbose} = flags + const {args, flags} = await this.parse(Restore) + const {app, confirm, extensions, verbose, 'wait-interval': waitInterval} = flags const interval = Math.max(3, waitInterval) const dbResolver = new utils.pg.DatabaseResolver(this.heroku) const {addon: db} = await dbResolver.getAttachment(app as string, args.database) @@ -141,9 +148,5 @@ export default class Restore extends Command { ux.action.stop() await pgbackups.wait('Restoring', restore.uuid, interval, verbose, db.app.id as string) } - - protected getSortedExtensions(extensions: string | null | undefined): string[] | undefined { - return extensions?.split(',').map(ext => ext.trim().toLowerCase()).sort() - } } diff --git a/packages/cli/src/commands/pg/backups/schedules.ts b/packages/cli/src/commands/pg/backups/schedules.ts index c2113c6c00..7b71a30631 100644 --- a/packages/cli/src/commands/pg/backups/schedules.ts +++ b/packages/cli/src/commands/pg/backups/schedules.ts @@ -1,18 +1,18 @@ -import {color} from '@heroku-cli/color' +import {color, hux, utils} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' -import {utils} from '@heroku/heroku-cli-util' + import type {TransferSchedule} from '../../../lib/pg/types.js' export default class Schedules extends Command { - static topic = 'pg' static description = 'list backup schedule' static flags = { app: flags.app({required: true}), remote: flags.remote(), } + static topic = 'pg' + public async run(): Promise { const {flags} = await this.parse(Schedules) const {app} = flags diff --git a/packages/cli/src/commands/pg/backups/unschedule.ts b/packages/cli/src/commands/pg/backups/unschedule.ts index 920220c2a1..1976a55633 100644 --- a/packages/cli/src/commands/pg/backups/unschedule.ts +++ b/packages/cli/src/commands/pg/backups/unschedule.ts @@ -1,24 +1,25 @@ -import {color} from '@heroku-cli/color' +import {color, utils} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' -import {utils} from '@heroku/heroku-cli-util' + import {TransferSchedule} from '../../../lib/pg/types.js' import {nls} from '../../../nls.js' export default class Unschedule extends Command { - static topic = 'pg' + static args = { + database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:arbitrary:suffix')}`}), + } + static description = 'stop daily backups' static flags = { app: flags.app({required: true}), remote: flags.remote(), } - static args = { - database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:arbitrary:suffix')}`}), - } + static topic = 'pg' public async run(): Promise { - const {flags, args} = await this.parse(Unschedule) + const {args, flags} = await this.parse(Unschedule) const {app} = flags const {database} = args let db = database diff --git a/packages/cli/src/commands/pg/backups/url.ts b/packages/cli/src/commands/pg/backups/url.ts index ebdaecd8f1..afeea92307 100644 --- a/packages/cli/src/commands/pg/backups/url.ts +++ b/packages/cli/src/commands/pg/backups/url.ts @@ -1,24 +1,27 @@ -import {color} from '@heroku-cli/color' +import {color, utils} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' -import {utils} from '@heroku/heroku-cli-util' -import pgBackupsApi from '../../../lib/pg/backups.js' + import type {BackupTransfer, PublicUrlResponse} from '../../../lib/pg/types.js' +import pgBackupsApi from '../../../lib/pg/backups.js' + export default class Url extends Command { - static topic = 'pg' + static args = { + backup_id: Args.string({description: 'ID of the backup. If omitted, we use the last backup ID.'}), + } + static description = 'get secret but publicly accessible URL of a backup' + static flags = { app: flags.app({required: true}), remote: flags.remote(), } - static args = { - backup_id: Args.string({description: 'ID of the backup. If omitted, we use the last backup ID.'}), - } + static topic = 'pg' public async run(): Promise { - const {flags, args} = await this.parse(Url) + const {args, flags} = await this.parse(Url) const {backup_id} = args const {app} = flags diff --git a/packages/cli/src/commands/pg/connection-pooling/attach.ts b/packages/cli/src/commands/pg/connection-pooling/attach.ts index dcaf63904d..d6f3bc1656 100644 --- a/packages/cli/src/commands/pg/connection-pooling/attach.ts +++ b/packages/cli/src/commands/pg/connection-pooling/attach.ts @@ -1,50 +1,52 @@ -import {color} from '@heroku-cli/color' +import {color, utils} 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' + import {essentialPlan} from '../../../lib/pg/util.js' -import {utils} from '@heroku/heroku-cli-util' import {nls} from '../../../nls.js' const heredoc = tsheredoc.default export default class Attach extends Command { - static topic = 'pg' + static args = { + database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:default:suffix')}`}), + } + static description = 'add an attachment to a database using connection pooling' + static examples = [heredoc` $ heroku pg:connection-pooling:attach postgresql-something-12345 `] static flags = { - as: flags.string({description: 'name for add-on attachment'}), app: flags.app({required: true}), + as: flags.string({description: 'name for add-on attachment'}), remote: flags.remote(), } - static args = { - database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:default:suffix')}`}), - } + static topic = 'pg' public async run(): Promise { - const {flags, args} = await this.parse(Attach) + const {args, flags} = await this.parse(Attach) const {app} = flags const dbResolver = new utils.pg.DatabaseResolver(this.heroku) const {addon: db} = await dbResolver.getAttachment(app, args.database) if (essentialPlan(db)) - ux.error('You can’t perform this operation on Essential-tier databases.') + ux.error('You can\'t perform this operation on Essential-tier databases.') - ux.action.start(`Enabling Connection Pooling on ${color.yellow(db.name)} to ${color.magenta(app)}`) + ux.action.start(`Enabling Connection Pooling on ${color.yellow(db.name)} to ${color.app(app)}`) const {body: attachment} = await this.heroku.post>(`/client/v11/databases/${encodeURIComponent(db.name)}/connection-pooling`, { - body: {name: flags.as, credential: 'default', app}, hostname: utils.pg.host(), + body: {app, credential: 'default', name: flags.as}, hostname: utils.pg.host(), }) ux.action.stop() - 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[]>( `/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}`) } diff --git a/packages/cli/src/commands/pg/credentials/destroy.ts b/packages/cli/src/commands/pg/credentials/destroy.ts index 20758f4bfa..3f4fd7bbd3 100644 --- a/packages/cli/src/commands/pg/credentials/destroy.ts +++ b/packages/cli/src/commands/pg/credentials/destroy.ts @@ -1,31 +1,33 @@ -import {color} from '@heroku-cli/color' +import {color, utils} 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 {essentialPlan} from '../../../lib/pg/util.js' -import {utils} from '@heroku/heroku-cli-util' +import {Args, ux} from '@oclif/core' + import ConfirmCommand from '../../../lib/confirmCommand.js' +import {essentialPlan} from '../../../lib/pg/util.js' import {nls} from '../../../nls.js' export default class Destroy extends Command { - static topic = 'pg' + static args = { + database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:default:suffix')}`}), + } + static description = 'destroy credential within database' static example = '$ heroku pg:credentials:destroy postgresql-transparent-56874 --name cred-name -a woodstock-production' + static flags = { - name: flags.string({char: 'n', required: true, description: 'unique identifier for the credential'}), - confirm: flags.string({char: 'c', description: 'set to app name to bypass confirm prompt'}), app: flags.app({required: true}), + confirm: flags.string({char: 'c', description: 'set to app name to bypass confirm prompt'}), + name: flags.string({char: 'n', description: 'unique identifier for the credential', required: true}), remote: flags.remote(), } - static args = { - database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:default:suffix')}`}), - } + static topic = 'pg' public async run(): Promise { - const {flags, args} = await this.parse(Destroy) + const {args, flags} = await this.parse(Destroy) const {database} = args - const {app, name, confirm} = flags + const {app, confirm, name} = flags if (name === 'default') { throw new Error('Default credential cannot be destroyed.') } diff --git a/packages/cli/src/commands/pg/credentials/rotate.ts b/packages/cli/src/commands/pg/credentials/rotate.ts index f0136d72d1..5fed9d11cd 100644 --- a/packages/cli/src/commands/pg/credentials/rotate.ts +++ b/packages/cli/src/commands/pg/credentials/rotate.ts @@ -1,34 +1,36 @@ -import {color} from '@heroku-cli/color' -import {Command, flags} from '@heroku-cli/command' -import {APIClient} from '@heroku-cli/command' import type {AddOnAttachment} from '@heroku-cli/schema' + +import {color, utils} from '@heroku/heroku-cli-util' +import {APIClient, Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' + import ConfirmCommand from '../../../lib/confirmCommand.js' -import {utils} from '@heroku/heroku-cli-util' import {nls} from '../../../nls.js' export default class Rotate extends Command { - static topic = 'pg' + static args = { + database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:default:suffix')}`}), + } + static description = 'rotate the database credentials' + static flags = { + all: flags.boolean({description: 'rotate all credentials', exclusive: ['name']}), + app: flags.app({required: true}), + confirm: flags.string({char: 'c', description: 'set to app name to bypass confirm prompt'}), + force: flags.boolean({description: 'forces rotating the targeted credentials'}), name: flags.string({ char: 'n', description: 'which credential to rotate (default credentials if not specified and --all is not used)', }), - all: flags.boolean({description: 'rotate all credentials', exclusive: ['name']}), - confirm: flags.string({char: 'c', description: 'set to app name to bypass confirm prompt'}), - force: flags.boolean({description: 'forces rotating the targeted credentials'}), - app: flags.app({required: true}), remote: flags.remote(), } - static args = { - database: Args.string({description: `${nls('pg:database:arg:description')} ${nls('pg:database:arg:description:default:suffix')}`}), - } + static topic = 'pg' public async run(): Promise { - const {flags, args} = await this.parse(Rotate) - const {app, all, confirm, name, force} = flags + const {args, flags} = await this.parse(Rotate) + const {all, app, confirm, force, name} = flags const dbResolver = new utils.pg.DatabaseResolver(this.heroku) const {addon: db} = await dbResolver.getAttachment(app, args.database) const warnings: string[] = [] @@ -74,11 +76,11 @@ export default class Rotate extends Command { await new ConfirmCommand().confirm(app, confirm, `Destructive Action\n${warnings.join('\n')}`) const options: APIClient.Options = { - hostname: utils.pg.host(), body: {forced: force ?? undefined}, headers: { Authorization: `Basic ${Buffer.from(`:${this.heroku.auth}`).toString('base64')}`, }, + hostname: utils.pg.host(), } if (all) { ux.action.start(`Rotating all credentials on ${color.yellow(db.name)}`) diff --git a/packages/cli/src/commands/pipelines/add.ts b/packages/cli/src/commands/pipelines/add.ts index c2775fe6af..99f6adc7de 100644 --- a/packages/cli/src/commands/pipelines/add.ts +++ b/packages/cli/src/commands/pipelines/add.ts @@ -1,6 +1,7 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' import {StageCompletion} from '@heroku-cli/command/lib/completions.js' +import * as Heroku from '@heroku-cli/schema' import {Args, ux} from '@oclif/core' import inquirer from 'inquirer' @@ -10,6 +11,13 @@ import infer from '../../lib/pipelines/infer.js' import {inferrableStageNames as stageNames} from '../../lib/pipelines/stages.js' export default class PipelinesAdd extends Command { + static args = { + pipeline: Args.string({ + description: 'name of pipeline', + required: true, + }), + } + static description = `add this app to a pipeline The app and pipeline names must be specified. The stage of the app will be guessed based on its name if not specified.` @@ -23,15 +31,8 @@ The stage of the app will be guessed based on its name if not specified.` remote: flags.remote(), stage: flags.string({ char: 's', - description: 'stage of first app in pipeline', completion: StageCompletion, - }), - } - - static args = { - pipeline: Args.string({ - description: 'name of pipeline', - required: true, + description: 'stage of first app in pipeline', }), } @@ -43,24 +44,24 @@ The stage of the app will be guessed based on its name if not specified.` const guesses = infer(app) const questions = [] - const pipeline: any = await disambiguate(this.heroku, args.pipeline) + const pipeline: Heroku.Pipeline = await disambiguate(this.heroku, args.pipeline) if (flags.stage) { stage = flags.stage } else { questions.push({ - type: 'list', - name: 'stage', - message: `Stage of ${app}`, choices: stageNames, default: guesses[1], + message: `Stage of ${app}`, + name: 'stage', + type: 'list', }) } const answers: any = await inquirer.prompt(questions) if (answers.stage) stage = answers.stage - ux.action.start(`Adding ${color.app(app)} to ${color.pipeline(pipeline.name)} pipeline as ${stage}`) + ux.action.start(`Adding ${color.app(app)} to ${color.pipeline(pipeline.name || '')} pipeline as ${stage}`) await createCoupling(this.heroku, pipeline, app, stage) ux.action.stop() } diff --git a/packages/cli/src/commands/pipelines/diff.ts b/packages/cli/src/commands/pipelines/diff.ts index 97aeed168b..ce77f4a5d0 100644 --- a/packages/cli/src/commands/pipelines/diff.ts +++ b/packages/cli/src/commands/pipelines/diff.ts @@ -1,19 +1,25 @@ -import {color} from '@heroku-cli/color' +import {color, hux} from '@heroku/heroku-cli-util' +import {HTTP} from '@heroku/http-call' import {Command, flags} from '@heroku-cli/command' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' -import {HTTP} from '@heroku/http-call' -import {getCoupling, getPipeline, getReleases, listPipelineApps, SDK_HEADER} from '../../lib/api.js' -import KolkrabbiAPI from '../../lib/pipelines/kolkrabbi-api.js' -import type {OciImage, Slug, PipelineCoupling} from '../../lib/types/fir.js' +import type {OciImage, PipelineCoupling, Slug} from '../../lib/types/fir.js' import type {Commit, GitHubDiff} from '../../lib/types/github.js' + +import { + SDK_HEADER, + getCoupling, + getPipeline, + getReleases, + listPipelineApps, +} from '../../lib/api.js' import {GenerationKind, getGeneration} from '../../lib/apps/generation.js' +import KolkrabbiAPI from '../../lib/pipelines/kolkrabbi-api.js' interface AppInfo { + hash?: string; name: string; repo?: string; - hash?: string; } const PROMOTION_ORDER = ['development', 'staging', 'production'] @@ -39,9 +45,9 @@ async function diff(targetApp: AppInfo, downstreamApp: AppInfo, githubToken: str try { const path = `${targetApp.repo}/compare/${downstreamApp.hash}...${targetApp.hash}` const headers = { - authorization: 'token ' + githubToken, 'Content-Type': 'application/vnd.github+json', 'X-GitHub-Api-Version': '2022-11-28', + authorization: 'token ' + githubToken, } if (herokuUserAgent) { @@ -85,12 +91,10 @@ export default class PipelinesDiff extends Command { remote: flags.remote(), } - kolkrabbi: KolkrabbiAPI = new KolkrabbiAPI(this.config.userAgent, () => this.heroku.auth) - getAppInfo = async (appName: string, appId: string, generation: GenerationKind): Promise => { // Find GitHub connection for the app const githubApp = await this.kolkrabbi.getAppLink(appId) - .catch(() => ({name: appName, repo: null, hash: null})) + .catch(() => ({hash: null, name: appName, repo: null})) // Find the commit hash of the latest release for this app let slug: Slug @@ -116,12 +120,14 @@ export default class PipelinesDiff extends Command { commit = ociImages[0]?.commit } } catch { - return {name: appName, repo: githubApp.repo, hash: undefined} + return {hash: undefined, name: appName, repo: githubApp.repo} } - return {name: appName, repo: githubApp.repo, hash: commit} + return {hash: commit, name: appName, repo: githubApp.repo} } + kolkrabbi: KolkrabbiAPI = new KolkrabbiAPI(this.config.userAgent, () => this.heroku.auth) + async run() { const {flags} = await this.parse(PipelinesDiff) const targetAppName = flags.app diff --git a/packages/cli/src/commands/pipelines/promote.ts b/packages/cli/src/commands/pipelines/promote.ts index c59c27d6ba..5db5ed1355 100644 --- a/packages/cli/src/commands/pipelines/promote.ts +++ b/packages/cli/src/commands/pipelines/promote.ts @@ -1,8 +1,7 @@ -import {color} from '@heroku-cli/color' +import {color, hux} from '@heroku/heroku-cli-util' import {APIClient, Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' import assert from 'assert' import fetch from 'node-fetch' import * as Stream from 'stream' @@ -68,12 +67,12 @@ async function getCoupling(heroku: APIClient, app: string): Promise, secondFactor?: string): Promise { const options = { - headers: {}, body: { pipeline: {id}, source: {app: {id: sourceAppId}}, targets: targetApps.map(app => ({app: {id: app.id}})), }, + headers: {}, } if (secondFactor) { diff --git a/packages/cli/src/commands/pipelines/remove.ts b/packages/cli/src/commands/pipelines/remove.ts index ae4d09119d..be284301bd 100644 --- a/packages/cli/src/commands/pipelines/remove.ts +++ b/packages/cli/src/commands/pipelines/remove.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' diff --git a/packages/cli/src/commands/pipelines/setup.ts b/packages/cli/src/commands/pipelines/setup.ts index 62611bb653..75fe08c223 100644 --- a/packages/cli/src/commands/pipelines/setup.ts +++ b/packages/cli/src/commands/pipelines/setup.ts @@ -1,8 +1,8 @@ -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 debug from 'debug' import openBrowser from 'open' -import Debug from 'debug' import {createPipeline, getAccountInfo, getTeam} from '../../lib/api.js' import GitHubAPI from '../../lib/pipelines/github-api.js' @@ -15,13 +15,22 @@ import getRepo from '../../lib/pipelines/setup/get-repo.js' import getSettings from '../../lib/pipelines/setup/get-settings.js' import pollAppSetups from '../../lib/pipelines/setup/poll-app-setups.js' import setupPipeline from '../../lib/pipelines/setup/setup-pipeline.js' -import {nameAndRepo, STAGING_APP_INDICATOR} from '../../lib/pipelines/setup/validate.js' +import {STAGING_APP_INDICATOR, nameAndRepo} from '../../lib/pipelines/setup/validate.js' -// eslint-disable-next-line new-cap -const debug = Debug('pipelines:setup') +const pipelineDebug = debug('pipelines:setup') export default class Setup extends Command { - static open = openBrowser + static args = { + name: Args.string({ + description: 'name of pipeline', + required: false, + }), + repo: Args.string({ + description: 'a GitHub repository to connect the pipeline to', + required: false, + }), + } + static description = 'bootstrap a new pipeline with common settings and create a production and staging app (requires a fully formed app.json in the repo)' @@ -38,16 +47,7 @@ export default class Setup extends Command { }), } - static args = { - name: Args.string({ - description: 'name of pipeline', - required: false, - }), - repo: Args.string({ - description: 'a GitHub repository to connect the pipeline to', - required: false, - }), - } + static open = openBrowser async run() { const {args, flags} = await this.parse(Setup) @@ -106,7 +106,7 @@ export default class Setup extends Command { await setup await Setup.open(`https://dashboard.heroku.com/pipelines/${pipeline.id}`) } catch (error: any) { - debug(error) + pipelineDebug(error) ux.error(error) } finally { ux.action.stop() diff --git a/packages/cli/src/commands/pipelines/update.ts b/packages/cli/src/commands/pipelines/update.ts index f238d06c8a..f32967fe0d 100644 --- a/packages/cli/src/commands/pipelines/update.ts +++ b/packages/cli/src/commands/pipelines/update.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 {StageCompletion} from '@heroku-cli/command/lib/completions.js' import {ux} from '@oclif/core' @@ -17,8 +17,8 @@ export default class PipelinesUpdate extends Command { remote: flags.remote(), stage: flags.string({ char: 's', - description: 'new stage of app', completion: StageCompletion, + description: 'new stage of app', required: true, }), } diff --git a/packages/cli/src/commands/ps/index.ts b/packages/cli/src/commands/ps/index.ts index d7a63f981e..6a6a317947 100644 --- a/packages/cli/src/commands/ps/index.ts +++ b/packages/cli/src/commands/ps/index.ts @@ -1,14 +1,14 @@ -import {color} from '@heroku-cli/color' -import {Command, flags} from '@heroku-cli/command' +import {color, hux} from '@heroku/heroku-cli-util' +import {APIClient, Command, flags} from '@heroku-cli/command' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' +import tsheredoc from 'tsheredoc' + import {ago} from '../../lib/time.js' -import {APIClient} from '@heroku-cli/command' import {AccountQuota} from '../../lib/types/account_quota.js' import {AppProcessTier} from '../../lib/types/app_process_tier.js' import {DynoExtended} from '../../lib/types/dyno_extended.js' import {Account} from '../../lib/types/fir.js' -import tsheredoc from 'tsheredoc' + const heredoc = tsheredoc.default function getProcessNumber(s: string) : number { @@ -174,11 +174,7 @@ function printDynos(dynos: DynoExtended[]) : void { } export default class Index extends Command { - static topic = 'ps' static description = 'list dynos for an app' - static strict = false - static usage = 'ps [TYPE [TYPE ...]]' - static examples = [heredoc` $ heroku ps === run: one-off dyno @@ -193,24 +189,30 @@ export default class Index extends Command { static flags = { app: flags.app({required: true}), - remote: flags.remote(), - json: flags.boolean({description: 'display as json'}), extended: flags.boolean({char: 'x', hidden: true}), // only works with sudo privileges + json: flags.boolean({description: 'display as json'}), + remote: flags.remote(), } + static strict = false + + static topic = 'ps' + + static usage = 'ps [TYPE [TYPE ...]]' + public async run(): Promise { const {flags, ...restParse} = await this.parse(Index) - const {app, json, extended} = flags + const {app, extended, json} = flags const types = restParse.argv as string[] const suffix = extended ? '?extended=true' : '' const promises = { - dynos: this.heroku.request(`/apps/${app}/dynos${suffix}`, { + accountInfo: this.heroku.request('/account', { headers: {Accept: 'application/vnd.heroku+json; version=3.sdk'}, }), appInfo: this.heroku.request(`/apps/${app}`, { headers: {Accept: 'application/vnd.heroku+json; version=3.sdk'}, }), - accountInfo: this.heroku.request('/account', { + dynos: this.heroku.request(`/apps/${app}/dynos${suffix}`, { headers: {Accept: 'application/vnd.heroku+json; version=3.sdk'}, }), } @@ -229,7 +231,7 @@ export default class Index extends Command { selectedDynos = selectedDynos.filter(dyno => types.find((t: string) => dyno.type === t)) types.forEach(t => { if (!selectedDynos.some(d => d.type === t)) { - throw new Error(`No ${color.cyan(t)} dynos on ${color.magenta(app)}`) + throw new Error(`No ${color.cyan(t)} dynos on ${color.app(app)}`) } }) } @@ -243,7 +245,7 @@ export default class Index extends Command { else { await printAccountQuota(this.heroku, appInfo, accountInfo) if (selectedDynos.length === 0) - ux.stdout(`No dynos on ${color.magenta(app)}`) + ux.stdout(`No dynos on ${color.app(app)}`) else printDynos(selectedDynos) } diff --git a/packages/cli/src/commands/ps/restart.ts b/packages/cli/src/commands/ps/restart.ts index 51178ff6ff..f349fcdbc9 100644 --- a/packages/cli/src/commands/ps/restart.ts +++ b/packages/cli/src/commands/ps/restart.ts @@ -1,47 +1,51 @@ +import {color as newColor} from '@heroku/heroku-cli-util' +import {color} from '@heroku-cli/color' import {Command, flags} from '@heroku-cli/command' +import {ProcessTypeCompletion} from '@heroku-cli/command/lib/completions.js' import * as Heroku from '@heroku-cli/schema' -import {color} from '@heroku-cli/color' import {Args, ux} from '@oclif/core' -import {ProcessTypeCompletion} from '@heroku-cli/command/lib/completions.js' import tsheredoc from 'tsheredoc' + const heredoc = tsheredoc.default export default class Restart extends Command { + static aliases = ['dyno:restart'] + + static args = { + dyno: Args.string({deprecated: true, description: 'name of the dyno to restart', required: false}), + } + static description = heredoc(` restart an app dyno or process type if neither --dyno nor --type are specified, restarts all dynos on app `) - static topic = 'ps' - static aliases = ['dyno:restart'] - static hiddenAliases = ['restart'] - static examples = [ '$ heroku ps:restart --app myapp --dyno-name web.1', '$ heroku ps:restart --app myapp --process-type web', '$ heroku ps:restart --app myapp', ] - static args = { - dyno: Args.string({description: 'name of the dyno to restart', required: false, deprecated: true}), - } - static flags = { app: flags.app({required: true}), - remote: flags.remote(), 'dyno-name': flags.string({ char: 'd', description: 'name of the dyno to restart', }), 'process-type': flags.string({ char: 'p', - description: 'name of the process type to restart', completion: ProcessTypeCompletion, + description: 'name of the process type to restart', exclusive: ['dyno-name'], }), + remote: flags.remote(), } + static hiddenAliases = ['restart'] + + static topic = 'ps' + async run() { const {args, flags} = await this.parse(Restart) const {app} = flags @@ -65,7 +69,7 @@ export default class Restart extends Command { restartUrl = `/apps/${app}/dynos` } - msg += ` on ${color.app(app)}` + msg += ` on ${newColor.app(app)}` ux.action.start(msg) await this.heroku.delete(restartUrl, { diff --git a/packages/cli/src/commands/ps/scale.ts b/packages/cli/src/commands/ps/scale.ts index 0c2188cf12..9d136db950 100644 --- a/packages/cli/src/commands/ps/scale.ts +++ b/packages/cli/src/commands/ps/scale.ts @@ -1,17 +1,18 @@ -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 * as Heroku from '@heroku-cli/schema' +import {ux} from '@oclif/core' import _ from 'lodash' import tsheredoc from 'tsheredoc' + const heredoc = tsheredoc.default const emptyFormationErr = (app: string) => ( - new Error(`No process types on ${color.magenta(app)}.\nUpload a Procfile to add process types.\nhttps://devcenter.heroku.com/articles/procfile`) + new Error(`No process types on ${color.app(app)}.\nUpload a Procfile to add process types.\nhttps://devcenter.heroku.com/articles/procfile`) ) export default class Scale extends Command { - static strict = false + static aliases = ['dyno:scale'] static description = heredoc` scale dyno quantity up or down Appending a size (eg. web=2:Standard-2X) allows simultaneous scaling and resizing. @@ -27,13 +28,14 @@ export default class Scale extends Command { web=3:Standard-2X worker=1:Standard-1X `] - static aliases = ['dyno:scale'] - static hiddenAliases = ['scale'] static flags = { app: flags.app({required: true}), remote: flags.remote(), } + static hiddenAliases = ['scale'] + static strict = false + public async run(): Promise { const {flags, ...restParse} = await this.parse(Scale) const argv = restParse.argv as string[] diff --git a/packages/cli/src/commands/ps/stop.ts b/packages/cli/src/commands/ps/stop.ts index b927b1d0b5..5098d565c1 100644 --- a/packages/cli/src/commands/ps/stop.ts +++ b/packages/cli/src/commands/ps/stop.ts @@ -1,41 +1,44 @@ +import {color as newColor} from '@heroku/heroku-cli-util' +import {color} from '@heroku-cli/color' import {Command, flags} from '@heroku-cli/command' +import {ProcessTypeCompletion} from '@heroku-cli/command/lib/completions.js' import * as Heroku from '@heroku-cli/schema' -import {color} from '@heroku-cli/color' import {Args, ux} from '@oclif/core' -import {ProcessTypeCompletion} from '@heroku-cli/command/lib/completions.js' import tsheredoc from 'tsheredoc' + const heredoc = tsheredoc.default export default class Stop extends Command { - static description = 'stop an app dyno or process type' - static topic = 'ps' static aliases = ['dyno:stop', 'ps:kill', 'dyno:kill'] - static hiddenAliases = ['stop', 'kill'] + static args = { + dyno: Args.string({deprecated: true, description: 'name of the dyno to stop', required: false}), + } + static description = 'stop an app dyno or process type' static examples = [ '$ heroku ps:stop --app myapp --dyno-name run.1828', '$ heroku ps:stop --app myapp --process-type run', ] - static args = { - dyno: Args.string({description: 'name of the dyno to stop', required: false, deprecated: true}), - } - static flags = { app: flags.app({required: true}), - remote: flags.remote(), 'dyno-name': flags.string({ char: 'd', description: 'name of the dyno to stop', }), 'process-type': flags.string({ char: 'p', - description: 'name of the process type to stop', completion: ProcessTypeCompletion, + description: 'name of the process type to stop', exclusive: ['dyno-name'], }), + remote: flags.remote(), } + static hiddenAliases = ['stop', 'kill'] + + static topic = 'ps' + async run() { const {args, flags} = await this.parse(Stop) @@ -62,7 +65,7 @@ export default class Stop extends Command { `)) } - msg += ` on ${color.app(app)}` + msg += ` on ${newColor.app(app)}` ux.action.start(msg) await this.heroku.post(stopUrl, {headers: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) diff --git a/packages/cli/src/commands/ps/type.ts b/packages/cli/src/commands/ps/type.ts index d5583acccb..a45270ff84 100644 --- a/packages/cli/src/commands/ps/type.ts +++ b/packages/cli/src/commands/ps/type.ts @@ -1,57 +1,56 @@ -import {color} from '@heroku-cli/color' +import {color, hux} from '@heroku/heroku-cli-util' import {APIClient, Command, flags} from '@heroku-cli/command' -import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' import * as Heroku from '@heroku-cli/schema' +import {ux} from '@oclif/core' import _ from 'lodash' import tsheredoc from 'tsheredoc' const heredoc = tsheredoc.default const COST_MONTHLY: Record = { - Free: 0, + '1X': 36, + '2X': 72, + Basic: 7, Eco: 0, + Free: 0, Hobby: 7, - Basic: 7, - 'Standard-1X': 25, - 'Standard-2X': 50, - 'Performance-M': 250, + PX: 576, Performance: 500, + 'Performance-2XL': 1500, 'Performance-L': 500, - '1X': 36, - '2X': 72, - PX: 576, 'Performance-L-RAM': 500, + 'Performance-M': 250, 'Performance-XL': 750, - 'Performance-2XL': 1500, - 'Private-S': 225, - 'Private-M': 450, 'Private-L': 900, - 'Shield-M': 540, - 'Shield-L': 1080, - 'Shield-S': 270, + 'Private-M': 450, + 'Private-Memory-2XL': 1500, 'Private-Memory-L': 500, 'Private-Memory-XL': 750, - 'Private-Memory-2XL': 1500, + 'Private-S': 225, + 'Shield-L': 1080, + 'Shield-M': 540, + 'Shield-Memory-2XL': 1800, 'Shield-Memory-L': 600, 'Shield-Memory-XL': 900, - 'Shield-Memory-2XL': 1800, + 'Shield-S': 270, + 'Standard-1X': 25, + 'Standard-2X': 50, 'dyno-1c-0.5gb': 25, - 'dyno-2c-1gb': 50, 'dyno-1c-4gb': 80, - 'dyno-2c-8gb': 160, - 'dyno-4c-16gb': 320, - 'dyno-8c-32gb': 640, - 'dyno-16c-64gb': 1000, - 'dyno-2c-4gb': 150, - 'dyno-4c-8gb': 300, - 'dyno-8c-16gb': 600, - 'dyno-16c-32gb': 1200, - 'dyno-32c-64gb': 2400, 'dyno-1c-8gb': 100, + 'dyno-2c-1gb': 50, + 'dyno-2c-4gb': 150, + 'dyno-2c-8gb': 160, 'dyno-2c-16gb': 250, + 'dyno-4c-8gb': 300, + 'dyno-4c-16gb': 320, 'dyno-4c-32gb': 500, + 'dyno-8c-16gb': 600, + 'dyno-8c-32gb': 640, 'dyno-8c-64gb': 750, + 'dyno-16c-32gb': 1200, + 'dyno-16c-64gb': 1000, 'dyno-16c-128gb': 1500, + 'dyno-32c-64gb': 2400, } const calculateHourly = (size: string) => COST_MONTHLY[size] / 720 @@ -69,7 +68,7 @@ const displayFormation = async (heroku: APIClient, app: string) => { const formationTableData = _.sortBy(formation, 'type') // this filter shouldn't be necessary, but it makes TS happy - .filter((f): f is Heroku.Formation & {size: string, quantity: number} => typeof f.size === 'string' && typeof f.quantity === 'number') + .filter((f): f is {quantity: number, size: string} & Heroku.Formation => typeof f.size === 'string' && typeof f.quantity === 'number') .map((d => { if (d.size === 'Eco') { isShowingEcoCostMessage = true @@ -129,7 +128,7 @@ const displayFormation = async (heroku: APIClient, app: string) => { } export default class Type extends Command { - static strict = false + static aliases = ['ps:resize', 'dyno:resize'] static description = heredoc` manage dyno sizes Called with no arguments shows the current dyno size. @@ -139,13 +138,14 @@ export default class Type extends Command { Called with 1..n TYPE=SIZE arguments sets the quantity per type. ` - static aliases = ['ps:resize', 'dyno:resize'] - static hiddenAliases = ['resize', 'dyno:type'] static flags = { app: flags.app({required: true}), remote: flags.remote(), } + static hiddenAliases = ['resize', 'dyno:type'] + static strict = false + public async run(): Promise { const {flags, ...restParse} = await this.parse(Type) const argv = restParse.argv as string[] @@ -175,7 +175,7 @@ export default class Type extends Command { const changes = await parse() if (changes.length > 0) { - ux.action.start(`Scaling dynos on ${color.magenta(app)}`) + ux.action.start(`Scaling dynos on ${color.app(app)}`) await this.heroku.patch(`/apps/${app}/formation`, {body: {updates: changes}}) ux.action.stop() } diff --git a/packages/cli/src/commands/ps/wait.ts b/packages/cli/src/commands/ps/wait.ts index 18bf6fa23c..31664a162d 100644 --- a/packages/cli/src/commands/ps/wait.ts +++ b/packages/cli/src/commands/ps/wait.ts @@ -1,13 +1,10 @@ +import {color, hux} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' -import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' - import {Dyno, Release} from '@heroku-cli/schema' +import {ux} from '@oclif/core' export default class Wait extends Command { static description = 'wait for all dynos to be running latest version after a release' - static topic = 'ps' - static flags = { app: flags.app({required: true}), remote: flags.remote(), @@ -17,6 +14,7 @@ export default class Wait extends Command { }), 'wait-interval': flags.integer({ char: 'w', + default: 10, description: 'how frequently to poll in seconds (to avoid hitting Heroku API rate limits)', async parse(input) { const w = Number.parseInt(input, 10) @@ -26,7 +24,6 @@ export default class Wait extends Command { return w }, - default: 10, }), 'with-run': flags.boolean({ char: 'R', @@ -35,18 +32,20 @@ export default class Wait extends Command { }), } + static topic = 'ps' + async run() { const {flags} = await this.parse(Wait) const {body: releases} = await this.heroku.request(`/apps/${flags.app}/releases`, { - partial: true, headers: { Range: 'version ..; max=1, order=desc', }, + partial: true, }) if (releases.length === 0) { - this.warn(`App ${flags.app} has no releases`) + this.warn(`App ${color.app(flags.app)} has no releases`) return } diff --git a/packages/cli/src/commands/releases/index.ts b/packages/cli/src/commands/releases/index.ts index c5f015985c..462e414292 100644 --- a/packages/cli/src/commands/releases/index.ts +++ b/packages/cli/src/commands/releases/index.ts @@ -1,18 +1,18 @@ +import {color as newColor, hux} from '@heroku/heroku-cli-util' import {color} from '@heroku-cli/color' import {Command, flags} from '@heroku-cli/command' +import * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' import _ from 'lodash' -import * as Heroku from '@heroku-cli/schema' +import stripAnsi from 'strip-ansi' import * as statusHelper from '../../lib/releases/status_helper.js' import * as time from '../../lib/time.js' -import stripAnsi from 'strip-ansi' type ColumnConfig = { - get?: (row: Heroku.Release) => string | number | undefined - header?: string extended?: boolean + get?: (row: Heroku.Release) => number | string | undefined + header?: string } const getDescriptionTruncation = function (releases: Heroku.Release[], columns: Record, optimizeKey: string) { @@ -70,7 +70,6 @@ const getDescriptionTruncation = function (releases: Heroku.Release[], columns: } export default class Index extends Command { - static topic = 'releases' static description = 'display the releases for an app' static examples = [ 'v1 Config add FOO_BAR email@example.com 2015/11/17 17:37:41 (~ 1h ago)', @@ -79,25 +78,27 @@ export default class Index extends Command { ] static flags = { - num: flags.string({char: 'n', description: 'number of releases to show'}), - json: flags.boolean({description: 'output releases in json format'}), + app: flags.app({required: true}), extended: flags.boolean({char: 'x', hidden: true}), + json: flags.boolean({description: 'output releases in json format'}), + num: flags.string({char: 'n', description: 'number of releases to show'}), remote: flags.remote(), - app: flags.app({required: true}), } + static topic = 'releases' + public async run(): Promise { const {flags} = await this.parse(Index) - const {app, num, json, extended} = flags + const {app, extended, json, num} = flags const url = `/apps/${app}/releases${extended ? '?extended=true' : ''}` const {body: releases} = await this.heroku.request(url, { - partial: true, headers: { - Range: `version ..; max=${num || 15}, order=desc`, Accept: 'application/vnd.heroku+json; version=3.sdk', + Range: `version ..; max=${num || 15}, order=desc`, }, + partial: true, }) let optimizationWidth = 0 @@ -173,9 +174,9 @@ export default class Index extends Command { if (json) { hux.styledJSON(releases) } else if (releases.length === 0) { - ux.stdout(`${app} has no releases.`) + ux.stdout(`${newColor.app(app)} has no releases.`) } else { - let header = `${app} Releases` + let header = `${newColor.app(app)} Releases` const currentRelease = releases.find(r => r.current === true) if (currentRelease) { header += ' - ' + color.cyan(`Current: v${currentRelease.version}`) diff --git a/packages/cli/src/commands/releases/retry.ts b/packages/cli/src/commands/releases/retry.ts index 75de1fbe10..7a0a135c85 100644 --- a/packages/cli/src/commands/releases/retry.ts +++ b/packages/cli/src/commands/releases/retry.ts @@ -1,19 +1,23 @@ -import {Command, flags} from '@heroku-cli/command' -import {ux} from '@oclif/core' +import {color as newColor} from '@heroku/heroku-cli-util' import {color} from '@heroku-cli/color' +import {Command, flags} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' +import {ux} from '@oclif/core' + import {stream} from '../../lib/releases/output.js' import {findByLatestOrId} from '../../lib/releases/releases.js' export default class Retry extends Command { - static topic = 'releases' static description = 'retry the latest release-phase command' static examples = ['heroku releases:retry --app happy-samurai-42'] - static help = 'Copies the latest release into a new release and retries the latest release-phase command. App must have a release-phase command.' static flags = { app: flags.app({required: true}), } + static help = 'Copies the latest release into a new release and retries the latest release-phase command. App must have a release-phase command.' + + static topic = 'releases' + public async run(): Promise { const {flags} = await this.parse(Retry) const {app} = flags @@ -22,19 +26,19 @@ export default class Retry extends Command { const releasePhase = formations.filter(formation => formation.type === 'release') if (!release) { - return ux.error('No release found for this app.') + return ux.error(`No release found for ${newColor.app(app)}.`) } if (releasePhase.length === 0) { return ux.error('App must have a release-phase command to use this command.') } - ux.action.start(`Retrying ${color.green('v' + release.version)} on ${color.app(app)}`) + ux.action.start(`Retrying ${color.green('v' + release.version)} on ${newColor.app(app)}`) const {body: retry} = await this.heroku.post(`/apps/${app}/releases`, { body: { - slug: release?.slug?.id, description: `Retry of v${release.version}: ${release.description}`, + slug: release?.slug?.id, }, }) diff --git a/packages/cli/src/commands/releases/rollback.ts b/packages/cli/src/commands/releases/rollback.ts index dae3a8cf50..347faa1230 100644 --- a/packages/cli/src/commands/releases/rollback.ts +++ b/packages/cli/src/commands/releases/rollback.ts @@ -1,28 +1,31 @@ -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 {findByPreviousOrId} from '../../lib/releases/releases.js' +import {Args, ux} from '@oclif/core' + import {stream} from '../../lib/releases/output.js' +import {findByPreviousOrId} from '../../lib/releases/releases.js' export default class Rollback extends Command { - static topic = 'releases' - static hiddenAliases = ['rollback'] + static args = { + release: Args.string({description: 'ID of the release. If omitted, we use the last eligible release.'}), + } + static description = `Roll back to a previous release. If RELEASE is not specified, it will roll back to the last eligible release. ` static flags = { - remote: flags.remote(), app: flags.app({required: true}), + remote: flags.remote(), } - static args = { - release: Args.string({description: 'ID of the release. If omitted, we use the last eligible release.'}), - } + static hiddenAliases = ['rollback'] + + static topic = 'releases' public async run(): Promise { - const {flags, args} = await this.parse(Rollback) + const {args, flags} = await this.parse(Rollback) const {app} = flags const release = await findByPreviousOrId(this.heroku, app, args.release) diff --git a/packages/cli/src/commands/usage/addons.ts b/packages/cli/src/commands/usage/addons.ts index a4ecf042f6..f13ed89ba8 100644 --- a/packages/cli/src/commands/usage/addons.ts +++ b/packages/cli/src/commands/usage/addons.ts @@ -1,8 +1,7 @@ +import {color, hux} from '@heroku/heroku-cli-util' import {Command, flags} from '@heroku-cli/command' -import {ux} from '@oclif/core' -import {hux} from '@heroku/heroku-cli-util' import * as Heroku from '@heroku-cli/schema' -import {color} from '@heroku-cli/color' +import {ux} from '@oclif/core' interface AppUsage { addons: Array<{ @@ -17,8 +16,8 @@ interface AppUsage { interface TeamUsage { apps: Array<{ - id: string; addons: AppUsage['addons']; + id: string; }>; } @@ -28,13 +27,28 @@ interface AppInfo extends Record { } export default class UsageAddons extends Command { - static topic = 'usage' static description = 'list usage for metered add-ons attached to an app or apps within a team' static flags = { app: flags.string({char: 'a', description: 'app to list metered add-ons usage for'}), team: flags.team({description: 'team to list metered add-ons usage for'}), } + static topic = 'usage' + + public async run(): Promise { + const {flags} = await this.parse(UsageAddons) + const {app, team} = flags + if (!app && !team) { + ux.error('Specify an app with --app or a team with --team') + } + + if (app) { + await this.fetchAndDisplayAppUsageData(app, team) + } else if (team) { + await this.fetchAndDisplayTeamUsageData(team) + } + } + private displayAppUsage(app: string, usageAddons: AppUsage['addons'], appAddons: Heroku.AddOn[]): void { const metersArray = usageAddons.flatMap(addon => Object.entries(addon.meters).map(([label, data]) => ({ @@ -119,7 +133,7 @@ export default class UsageAddons extends Command { const appInfoArray = this.getAppInfoFromTeamAddons(teamAddons) // Display usage for each app - usageData.apps.forEach((app: { id: string; addons: any[] }) => { + usageData.apps.forEach((app: { addons: any[]; id: string }) => { const appInfo = appInfoArray.find(info => info.id === app.id) this.displayAppUsage(appInfo?.name || app.id, app.addons, teamAddons) ux.stdout() @@ -139,18 +153,4 @@ export default class UsageAddons extends Command { name, })) } - - public async run(): Promise { - const {flags} = await this.parse(UsageAddons) - const {app, team} = flags - if (!app && !team) { - ux.error('Specify an app with --app or a team with --team') - } - - if (app) { - await this.fetchAndDisplayAppUsageData(app, team) - } else if (team) { - await this.fetchAndDisplayTeamUsageData(team) - } - } } diff --git a/packages/cli/src/lib/apps/app-transfer.ts b/packages/cli/src/lib/apps/app-transfer.ts index 9865eec970..96dfb864ab 100644 --- a/packages/cli/src/lib/apps/app-transfer.ts +++ b/packages/cli/src/lib/apps/app-transfer.ts @@ -1,4 +1,4 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' import {APIClient} from '@heroku-cli/command' import * as Heroku from '@heroku-cli/schema' import {ux} from '@oclif/core' diff --git a/packages/cli/src/lib/pipelines/setup/poll-app-setups.ts b/packages/cli/src/lib/pipelines/setup/poll-app-setups.ts index 84bcb4ee0b..9718d966df 100644 --- a/packages/cli/src/lib/pipelines/setup/poll-app-setups.ts +++ b/packages/cli/src/lib/pipelines/setup/poll-app-setups.ts @@ -1,4 +1,4 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' import {ux} from '@oclif/core' import * as api from '../../api.js' diff --git a/packages/cli/src/lib/run/dyno.ts b/packages/cli/src/lib/run/dyno.ts index 291271bac0..3335bfb700 100644 --- a/packages/cli/src/lib/run/dyno.ts +++ b/packages/cli/src/lib/run/dyno.ts @@ -1,6 +1,6 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ +import {color} from '@heroku/heroku-cli-util' import {HTTP} from '@heroku/http-call' -import {color} from '@heroku-cli/color' import {APIClient} from '@heroku-cli/command' import {Notification, notify} from '@heroku-cli/notifications' import {Dyno as APIDyno} from '@heroku-cli/schema' diff --git a/packages/cli/src/lib/webhooks/base.ts b/packages/cli/src/lib/webhooks/base.ts index d09a75da14..bb2baf0cc0 100644 --- a/packages/cli/src/lib/webhooks/base.ts +++ b/packages/cli/src/lib/webhooks/base.ts @@ -1,6 +1,5 @@ -import {color} from '@heroku-cli/color' +import {color} from '@heroku/heroku-cli-util' import {APIClient, Command} from '@heroku-cli/command' - import {Config} from '@oclif/core' export default abstract class extends Command { @@ -18,18 +17,18 @@ export default abstract class extends Command { this.webhooksClient = client } - webhookType(context: {pipeline?: string; app?: string}): {path: string; display: string} { + webhookType(context: {app?: string; pipeline?: string}): {display: string; path: string} { if (context.pipeline) { return { - path: `/pipelines/${context.pipeline}`, display: context.pipeline, + path: `/pipelines/${context.pipeline}`, } } if (context.app) { return { - path: `/apps/${context.app}`, display: color.app(context.app), + path: `/apps/${context.app}`, } } diff --git a/packages/cli/src/oldCommands/pg/copy.ts b/packages/cli/src/oldCommands/pg/copy.ts index d94edbc62b..d2601d0cab 100644 --- a/packages/cli/src/oldCommands/pg/copy.ts +++ b/packages/cli/src/oldCommands/pg/copy.ts @@ -1,5 +1,5 @@ /* -import color from '@heroku-cli/color' +import color from '@heroku/heroku-cli-util' import {APIClient, Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' import * as Heroku from '@heroku-cli/schema' @@ -23,7 +23,7 @@ const getAttachmentInfo = async function (heroku: APIClient, db: string, app: st const attachment = await dbResolver.getAttachment(app, db) if (!attachment) - throw new Error(`${db} not found on ${color.magenta(app)}`) + throw new Error(`${db} not found on ${color.app(app)}`) const {body: addon} = await heroku.get(`/addons/${attachment.addon.name}`) const {body: config} = await heroku.get(`/apps/${attachment.app.name}/config-vars`) diff --git a/packages/cli/src/oldCommands/pg/info.ts b/packages/cli/src/oldCommands/pg/info.ts index 09f888acba..2abd774faf 100644 --- a/packages/cli/src/oldCommands/pg/info.ts +++ b/packages/cli/src/oldCommands/pg/info.ts @@ -2,12 +2,12 @@ import color from '@heroku-cli/color' import {Command, flags} from '@heroku-cli/command' import {Args, ux} from '@oclif/core' -import {ExtendedAddonAttachment, hux} from '@heroku/heroku-cli-util' +import {ExtendedAddonAttachment, hux} from '@heroku-cli/color' import * as Heroku from '@heroku-cli/schema' import {configVarNamesFromValue, databaseNameFromUrl} from '../../lib/pg/util' import {PgDatabaseTenant} from '../../lib/pg/types' import {nls} from '../../nls' -import {utils} from '@heroku/heroku-cli-util' +import {utils} from '@heroku-cli/color' type DBObject = { addon: ExtendedAddonAttachment | ExtendedAddonAttachment['addon'] & {attachment_names?: string[]}, @@ -77,7 +77,7 @@ export default class Info extends Command { const dbResolver = new utils.pg.DatabaseResolver(this.heroku) addons = await dbResolver.getAllLegacyDatabases(app) if (addons.length === 0) { - ux.log(`${color.magenta(app)} has no heroku-postgresql databases.`) + ux.log(`${color.app(app)} has no heroku-postgresql databases.`) return } } diff --git a/packages/cli/src/oldCommands/pg/links/index.ts b/packages/cli/src/oldCommands/pg/links/index.ts index ee1c938bf2..522d9803f3 100644 --- a/packages/cli/src/oldCommands/pg/links/index.ts +++ b/packages/cli/src/oldCommands/pg/links/index.ts @@ -1,5 +1,5 @@ /* -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 {hux, utils} from '@heroku/heroku-cli-util' diff --git a/packages/cli/src/oldCommands/pg/promote.ts b/packages/cli/src/oldCommands/pg/promote.ts index 6b1075e0d4..3050c23f7d 100644 --- a/packages/cli/src/oldCommands/pg/promote.ts +++ b/packages/cli/src/oldCommands/pg/promote.ts @@ -1,6 +1,6 @@ /* eslint-disable complexity */ /* -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' diff --git a/packages/cli/test/helpers/init.mjs b/packages/cli/test/helpers/init.mjs index bdaf0fd25d..265c41e4d7 100644 --- a/packages/cli/test/helpers/init.mjs +++ b/packages/cli/test/helpers/init.mjs @@ -22,3 +22,6 @@ if (process.env.ENABLE_NET_CONNECT === 'true') { } chai.use(chaiAsPromised) + +// Disable truncation of assertion error messages +chai.config.truncateThreshold = 0 diff --git a/packages/cli/test/unit/commands/access/add.unit.test.ts b/packages/cli/test/unit/commands/access/add.unit.test.ts index 1f017851d9..a049d5901a 100644 --- a/packages/cli/test/unit/commands/access/add.unit.test.ts +++ b/packages/cli/test/unit/commands/access/add.unit.test.ts @@ -1,6 +1,7 @@ -import {stderr, stdout} from 'stdout-stderr' -import nock from 'nock' import {expect} from 'chai' +import nock from 'nock' +import {stderr, stdout} from 'stdout-stderr' + import Cmd from '../../../../src/commands/access/add.js' import runCommand from '../../../helpers/runCommand.js' import {personalApp, teamApp, teamFeatures} from '../../../helpers/stubs/get.js' @@ -34,7 +35,7 @@ describe('heroku access:add', function () { apiGetOrgFeatures.done() apiPost.done() expect('').to.eq(stdout.output) - expect(stderr.output).to.equal('Adding gandalf@heroku.com access to the app myapp with deploy, view permissions... done\n') + expect(stderr.output).to.equal('Adding gandalf@heroku.com access to the app ⬢ myapp with deploy, view permissions... done\n') }) it('adds user to the app with permissions, and view is implicit', async function () { @@ -49,7 +50,7 @@ describe('heroku access:add', function () { apiGetOrgFeatures.done() apiPost.done() expect('').to.eq(stdout.output) - expect(stderr.output).to.equal('Adding gandalf@heroku.com access to the app myapp with deploy, view permissions... done\n') + expect(stderr.output).to.equal('Adding gandalf@heroku.com access to the app ⬢ myapp with deploy, view permissions... done\n') }) it('raises an error when permissions are not specified', function () { @@ -88,7 +89,7 @@ describe('heroku access:add', function () { apiGetOrgFeatures.done() apiPost.done() expect('').to.eq(stdout.output) - expect(stderr.output).to.equal('Adding gandalf@heroku.com access to the app myapp... done\n') + expect(stderr.output).to.equal('Adding gandalf@heroku.com access to the app ⬢ myapp... done\n') }) }) @@ -111,7 +112,7 @@ describe('heroku access:add', function () { apiGet.done() apiPost.done() expect('').to.eq(stdout.output) - expect(stderr.output).to.equal('Adding gandalf@heroku.com access to the app myapp... done\n') + expect(stderr.output).to.equal('Adding gandalf@heroku.com access to the app ⬢ myapp... done\n') }) }) }) diff --git a/packages/cli/test/unit/commands/access/remove.unit.test.ts b/packages/cli/test/unit/commands/access/remove.unit.test.ts index 3fb0e53845..837b3a41b0 100644 --- a/packages/cli/test/unit/commands/access/remove.unit.test.ts +++ b/packages/cli/test/unit/commands/access/remove.unit.test.ts @@ -1,6 +1,7 @@ -import {stdout, stderr} from 'stdout-stderr' -import nock from 'nock' import {expect} from 'chai' +import nock from 'nock' +import {stderr, stdout} from 'stdout-stderr' + import Cmd from '../../../../src/commands/access/remove.js' import runCommand from '../../../helpers/runCommand.js' import {collaboratorsPersonalApp} from '../../../helpers/stubs/delete.js' @@ -26,7 +27,7 @@ describe('heroku access:remove', function () { ]) apiDelete.done() expect('').to.eq(stdout.output) - expectOutput(stderr.output, 'Removing gandalf@heroku.com access from the app myapp... done\n') + expectOutput(stderr.output, 'Removing gandalf@heroku.com access from the app ⬢ myapp... done\n') }) }) }) diff --git a/packages/cli/test/unit/commands/addons/attach.unit.test.ts b/packages/cli/test/unit/commands/addons/attach.unit.test.ts index 5dc717c11d..f00069cf5a 100644 --- a/packages/cli/test/unit/commands/addons/attach.unit.test.ts +++ b/packages/cli/test/unit/commands/addons/attach.unit.test.ts @@ -1,29 +1,34 @@ -import {stdout, stderr} from 'stdout-stderr' -import Cmd from '../../../../src/commands/addons/attach.js' -import runCommand from '../../../helpers/runCommand.js' import {expect} from 'chai' import nock from 'nock' -import ConfirmCommand from '../../../../src/lib/confirmCommand.js' import sinon from 'sinon' +import {stderr, stdout} from 'stdout-stderr' + +import Cmd from '../../../../src/commands/addons/attach.js' +import ConfirmCommand from '../../../../src/lib/confirmCommand.js' +import runCommand from '../../../helpers/runCommand.js' let confirmStub: sinon.SinonStub describe('addons:attach', function () { + let api: nock.Scope + beforeEach(function () { + api = nock('https://api.heroku.com') confirmStub = sinon.stub(ConfirmCommand.prototype, 'confirm').resolves() }) afterEach(function () { confirmStub.restore() + api.done() nock.cleanAll() sinon.restore() }) it('attaches an add-on', async function () { - const api = nock('https://api.heroku.com:443') + api .get('/addons/redis-123') .reply(200, {name: 'redis-123'}) - .post('/addon-attachments', {app: {name: 'myapp'}, addon: {name: 'redis-123'}}) + .post('/addon-attachments', {addon: {name: 'redis-123'}, app: {name: 'myapp'}}) .reply(201, {name: 'REDIS'}) .get('/apps/myapp/releases') .reply(200, [{version: 10}]) @@ -35,16 +40,15 @@ describe('addons:attach', function () { ]) expect(stdout.output).to.equal('') - expect(stderr.output).to.contain('Attaching redis-123 to myapp... done') - expect(stderr.output).to.contain('\nSetting REDIS config vars and restarting myapp... done, v10') - return api.done() + expect(stderr.output).to.contain('Attaching redis-123 to ⬢ myapp... done') + expect(stderr.output).to.contain('\nSetting REDIS config vars and restarting ⬢ myapp... done, v10') }) it('attaches an add-on as foo', function () { - const api = nock('https://api.heroku.com:443') + api .get('/addons/redis-123') .reply(200, {name: 'redis-123'}) - .post('/addon-attachments', {name: 'foo', app: {name: 'myapp'}, addon: {name: 'redis-123'}}) + .post('/addon-attachments', {addon: {name: 'redis-123'}, app: {name: 'myapp'}, name: 'foo'}) .reply(201, {name: 'foo'}) .get('/apps/myapp/releases') .reply(200, [{version: 10}]) @@ -58,19 +62,18 @@ describe('addons:attach', function () { ]) .then(() => { expect(stdout.output).to.equal('') - expect(stderr.output).to.contain('Attaching redis-123 as foo to myapp... done') - expect(stderr.output).to.contain('\nSetting foo config vars and restarting myapp... done, v10') - api.done() + expect(stderr.output).to.contain('Attaching redis-123 as foo to ⬢ myapp... done') + expect(stderr.output).to.contain('\nSetting foo config vars and restarting ⬢ myapp... done, v10') }) }) it('overwrites an add-on as foo when confirmation is set', function () { - const api = nock('https://api.heroku.com:443') + api .get('/addons/redis-123') .reply(200, {name: 'redis-123'}) - .post('/addon-attachments', {name: 'foo', app: {name: 'myapp'}, addon: {name: 'redis-123'}}) + .post('/addon-attachments', {addon: {name: 'redis-123'}, app: {name: 'myapp'}, name: 'foo'}) .reply(400, {id: 'confirmation_required'}) - .post('/addon-attachments', {name: 'foo', app: {name: 'myapp'}, addon: {name: 'redis-123'}, confirm: 'myapp'}) + .post('/addon-attachments', {addon: {name: 'redis-123'}, app: {name: 'myapp'}, confirm: 'myapp', name: 'foo'}) .reply(201, {name: 'foo'}) .get('/apps/myapp/releases') .reply(200, [{version: 10}]) @@ -84,18 +87,17 @@ describe('addons:attach', function () { ]) .then(() => { expect(stdout.output).to.equal('') - expect(stderr.output).to.contain('Attaching redis-123 as foo to myapp...') - expect(stderr.output).to.contain('Attaching redis-123 as foo to myapp... done') - expect(stderr.output).to.contain('Setting foo config vars and restarting myapp... done, v10') - api.done() + expect(stderr.output).to.contain('Attaching redis-123 as foo to ⬢ myapp...') + expect(stderr.output).to.contain('Attaching redis-123 as foo to ⬢ myapp... done') + expect(stderr.output).to.contain('Setting foo config vars and restarting ⬢ myapp... done, v10') }) }) it('attaches an addon without a namespace if the credential flag is set to default', function () { - const api = nock('https://api.heroku.com:443') + api .get('/addons/postgres-123') .reply(200, {name: 'postgres-123'}) - .post('/addon-attachments', {app: {name: 'myapp'}, addon: {name: 'postgres-123'}}) + .post('/addon-attachments', {addon: {name: 'postgres-123'}, app: {name: 'myapp'}}) .reply(201, {name: 'POSTGRES_HELLO'}) .get('/apps/myapp/releases') .reply(200, [{version: 10}]) @@ -109,19 +111,18 @@ describe('addons:attach', function () { ]) .then(() => { expect(stdout.output).to.equal('') - expect(stderr.output).to.contain('Attaching default of postgres-123 to myapp... done') - expect(stderr.output).to.contain('Setting POSTGRES_HELLO config vars and restarting myapp... done, v10') - api.done() + expect(stderr.output).to.contain('Attaching default of postgres-123 to ⬢ myapp... done') + expect(stderr.output).to.contain('Setting POSTGRES_HELLO config vars and restarting ⬢ myapp... done, v10') }) }) it('attaches in the credential namespace if the credential flag is specified', function () { - const api = nock('https://api.heroku.com:443') + api .get('/addons/postgres-123') .reply(200, {name: 'postgres-123'}) .get('/addons/postgres-123/config/credential:hello') .reply(200, [{some: 'config'}]) - .post('/addon-attachments', {app: {name: 'myapp'}, addon: {name: 'postgres-123'}, namespace: 'credential:hello'}) + .post('/addon-attachments', {addon: {name: 'postgres-123'}, app: {name: 'myapp'}, namespace: 'credential:hello'}) .reply(201, {name: 'POSTGRES_HELLO'}) .get('/apps/myapp/releases') .reply(200, [{version: 10}]) @@ -135,22 +136,17 @@ describe('addons:attach', function () { ]) .then(() => { expect(stdout.output).to.equal('') - expect(stderr.output).to.contain('Attaching hello of postgres-123 to myapp... done') - expect(stderr.output).to.contain('Setting POSTGRES_HELLO config vars and restarting myapp... done, v10') - api.done() + expect(stderr.output).to.contain('Attaching hello of postgres-123 to ⬢ myapp... done') + expect(stderr.output).to.contain('Setting POSTGRES_HELLO config vars and restarting ⬢ myapp... done, v10') }) }) it('errors if the credential flag is specified but that credential does not exist for that addon', function () { - nock('https://api.heroku.com:443') + api .get('/addons/postgres-123') .reply(200, {name: 'postgres-123'}) .get('/addons/postgres-123/config/credential:hello') .reply(200, []) - .post('/addon-attachments', {app: {name: 'myapp'}, addon: {name: 'postgres-123'}}) - .reply(201, {name: 'POSTGRES_DEFAULT'}) - .get('/apps/myapp/releases') - .reply(200, [{version: 10}]) return runCommand(Cmd, [ '--app', diff --git a/packages/cli/test/unit/commands/addons/destroy.unit.test.ts b/packages/cli/test/unit/commands/addons/destroy.unit.test.ts index ee19b35eec..d3a2ddba27 100644 --- a/packages/cli/test/unit/commands/addons/destroy.unit.test.ts +++ b/packages/cli/test/unit/commands/addons/destroy.unit.test.ts @@ -1,26 +1,39 @@ -import {stdout, stderr} from 'stdout-stderr' -import Cmd from '../../../../src/commands/addons/destroy.js' -import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' import {expect} from 'chai' import lolex from 'lolex' +import nock from 'nock' import sinon from 'sinon' +import {stderr, stdout} from 'stdout-stderr' import stripAnsi from 'strip-ansi' +import Cmd from '../../../../src/commands/addons/destroy.js' +import runCommand from '../../../helpers/runCommand.js' + /* WARNING!!!! this file is a minefield because packages/cli/src/lib/addons/resolve.ts resolveAddon uses memoization * You MUST change requests to have different params, or they won't be made and nock will not be satisfied */ describe('addons:destroy', function () { + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + afterEach(function () { - return nock.cleanAll() + api.done() + nock.cleanAll() }) + context('when an add-on implements sync deprovisioning', function () { it('destroys the add-on synchronously', async function () { const addon = { - id: 201, name: 'db3-swiftly-123', addon_service: {name: 'heroku-db3'}, app: {name: 'myapp', id: 101}, state: 'provisioned', + addon_service: {name: 'heroku-db3'}, + app: {id: 101, name: 'myapp'}, + id: 201, + name: 'db3-swiftly-123', + state: 'provisioned', } - const api = nock('https://api.heroku.com:443') - .post('/actions/addons/resolve', {app: 'myapp', addon: 'heroku-db3'}) + api + .post('/actions/addons/resolve', {addon: 'heroku-db3', app: 'myapp'}) .reply(200, [addon]) .delete('/apps/101/addons/201', {force: false}) .reply(200, {...addon, state: 'deprovisioned'}) @@ -34,16 +47,19 @@ describe('addons:destroy', function () { ]) expect(stdout.output).to.equal('') expect(stderr.output).to.contain('Destroying db3-swiftly-123 on ⬢ myapp... done\n') - api.done() }) }) context('when an add-on implements async deprovisioning', function () { it('destroys the add-on asynchronously', async function () { const addon = { - id: 201, name: 'db4-swiftly-123', addon_service: {name: 'heroku-db4'}, app: {name: 'myapp', id: 101}, state: 'provisioned', + addon_service: {name: 'heroku-db4'}, + app: {id: 101, name: 'myapp'}, + id: 201, + name: 'db4-swiftly-123', + state: 'provisioned', } - const api = nock('https://api.heroku.com:443') - .post('/actions/addons/resolve', {app: 'myapp', addon: 'heroku-db4'}) + api + .post('/actions/addons/resolve', {addon: 'heroku-db4', app: 'myapp'}) .reply(200, [addon]) .delete('/apps/101/addons/201', {force: false}) .reply(202, {...addon, state: 'deprovisioning'}) @@ -56,7 +72,6 @@ describe('addons:destroy', function () { ]) expect(stdout.output).to.equal('db4-swiftly-123 is being destroyed in the background. The app will restart when complete...\nUse heroku addons:info db4-swiftly-123 to check destruction progress\n') expect(stripAnsi(stderr.output)).to.contain(stripAnsi('Destroying db4-swiftly-123 on ⬢ myapp... pending')) - api.done() }) context('--wait', function () { let clock: ReturnType @@ -75,11 +90,11 @@ describe('addons:destroy', function () { }) it('waits for response and notifies', async function () { const addon = { - id: 201, name: 'db5-swiftly-123', addon_service: {name: 'heroku-db5'}, app: {name: 'myapp', id: 101}, state: 'provisioned', + addon_service: {name: 'heroku-db5'}, app: {id: 101, name: 'myapp'}, id: 201, name: 'db5-swiftly-123', state: 'provisioned', } const notifySpy = sandbox.spy(Cmd, 'notifier') - const api = nock('https://api.heroku.com:443') - .post('/actions/addons/resolve', {app: 'myapp', addon: 'heroku-db5'}) + api + .post('/actions/addons/resolve', {addon: 'heroku-db5', app: 'myapp'}) .reply(200, [addon]) .delete('/apps/101/addons/201', {force: false}) .reply(202, {...addon, state: 'deprovisioning'}) @@ -95,7 +110,6 @@ describe('addons:destroy', function () { '--wait', 'heroku-db5', ]) - api.done() expect(notifySpy.called).to.equal(true) expect(notifySpy.calledOnce).to.equal(true) expect(stripAnsi(stderr.output)).to.contain('Destroying db5-swiftly-123 on ⬢ myapp... pending') @@ -107,10 +121,10 @@ describe('addons:destroy', function () { it('fails when addon app is not the app specified', async function () { const addon_in_other_app = { - id: 201, name: 'db6-swiftly-123', addon_service: {name: 'heroku-db6'}, app: {name: 'myotherapp', id: 102}, state: 'provisioned', + addon_service: {name: 'heroku-db6'}, app: {id: 102, name: 'myotherapp'}, id: 201, name: 'db6-swiftly-123', state: 'provisioned', } - const api = nock('https://api.heroku.com:443') - .post('/actions/addons/resolve', {app: 'myapp', addon: 'heroku-db6'}) + api + .post('/actions/addons/resolve', {addon: 'heroku-db6', app: 'myapp'}) .reply(200, [addon_in_other_app]) try { await runCommand(Cmd, [ @@ -122,17 +136,20 @@ describe('addons:destroy', function () { ]) throw new Error('unreachable') } catch (error: any) { - api.done() - expect(stripAnsi(error.message)).to.equal('db6-swiftly-123 is on myotherapp not myapp') + expect(stripAnsi(error.message)).to.equal('db6-swiftly-123 is on ⬢ myotherapp not ⬢ myapp') } }) it('shows that it failed to deprovision when there are errors returned', async function () { const addon = { - id: 201, name: 'db7-swiftly-123', addon_service: {name: 'heroku-db7'}, app: {name: 'myapp', id: 101}, state: 'suspended', + addon_service: {name: 'heroku-db7'}, + app: {id: 101, name: 'myapp'}, + id: 201, + name: 'db7-swiftly-123', + state: 'suspended', } - const api = nock('https://api.heroku.com:443') - .post('/actions/addons/resolve', {app: 'myapp', addon: 'heroku-db7'}) + api + .post('/actions/addons/resolve', {addon: 'heroku-db7', app: 'myapp'}) .reply(200, [addon]) .delete('/apps/101/addons/201', {force: false}) .reply(403, {id: 'forbidden', message: 'Cannot delete a suspended addon'}) @@ -146,31 +163,30 @@ describe('addons:destroy', function () { ]) throw new Error('unreachable') } catch (error: any) { - api.done() expect(error.message).to.equal('The add-on was unable to be destroyed: Cannot delete a suspended addon.') } }) context('when an multiple add-ons provided', function () { it('destroys them all', async function () { const addon = { + addon_service: {name: 'heroku-db23'}, + app: {id: 101, name: 'myapp'}, id: 201, name: 'db23-swiftly-123', - addon_service: {name: 'heroku-db23'}, - app: {name: 'myapp', id: 101}, state: 'provisioned', } const addon1 = { + addon_service: {name: 'heroku-db24'}, + app: {id: 101, name: 'myapp'}, id: 301, name: 'db24-swiftly-123', - addon_service: {name: 'heroku-db24'}, - app: {name: 'myapp', id: 101}, state: 'provisioned', } - const api = nock('https://api.heroku.com:443') - .post('/actions/addons/resolve', {app: 'myapp', addon: 'heroku-db23'}).reply(200, [addon]) + api + .post('/actions/addons/resolve', {addon: 'heroku-db23', app: 'myapp'}).reply(200, [addon]) .delete('/apps/101/addons/201', {force: false}) .reply(200, {...addon, state: 'deprovisioned'}) - .post('/actions/addons/resolve', {app: 'myapp', addon: 'heroku-db24'}).reply(200, [addon1]) + .post('/actions/addons/resolve', {addon: 'heroku-db24', app: 'myapp'}).reply(200, [addon1]) .delete('/apps/101/addons/301', {force: false}) .reply(200, {...addon, state: 'deprovisioned'}) @@ -185,27 +201,26 @@ describe('addons:destroy', function () { expect(stdout.output).to.equal('') expect(stderr.output).to.contain('Destroying db23-swiftly-123 on ⬢ myapp... done\n') expect(stderr.output).to.contain('Destroying db24-swiftly-123 on ⬢ myapp... done\n') - api.done() }) it('fails when additional addon app is not the app specified', async function () { const addon = { + addon_service: {name: 'heroku-db13'}, + app: {id: 101, name: 'myapp'}, id: 201, name: 'db13-swiftly-123', - addon_service: {name: 'heroku-db13'}, - app: {name: 'myapp', id: 101}, state: 'provisioned', } const addon1 = { + addon_service: {name: 'heroku-db14'}, + app: {id: 444, name: 'myapp2'}, id: 301, name: 'db14-swiftly-123', - addon_service: {name: 'heroku-db14'}, - app: {name: 'myapp2', id: 444}, state: 'provisioned', } - const api = nock('https://api.heroku.com:443') - .post('/actions/addons/resolve', {app: 'myapp', addon: 'heroku-db13'}).reply(200, [addon]) - .post('/actions/addons/resolve', {app: 'myapp', addon: 'heroku-db14'}).reply(200, [addon1]) + api + .post('/actions/addons/resolve', {addon: 'heroku-db13', app: 'myapp'}).reply(200, [addon]) + .post('/actions/addons/resolve', {addon: 'heroku-db14', app: 'myapp'}).reply(200, [addon1]) try { await runCommand(Cmd, [ @@ -218,10 +233,8 @@ describe('addons:destroy', function () { ]) throw new Error('unreachable') } catch (error: any) { - expect(stripAnsi(error.message)).to.equal('db14-swiftly-123 is on myapp2 not myapp') + expect(stripAnsi(error.message)).to.equal('db14-swiftly-123 is on ⬢ myapp2 not ⬢ myapp') } - - api.done() }) }) }) diff --git a/packages/cli/test/unit/commands/addons/detach.unit.test.ts b/packages/cli/test/unit/commands/addons/detach.unit.test.ts index dda5bb2b08..53ff3948a2 100644 --- a/packages/cli/test/unit/commands/addons/detach.unit.test.ts +++ b/packages/cli/test/unit/commands/addons/detach.unit.test.ts @@ -1,16 +1,26 @@ -import {stdout, stderr} from 'stdout-stderr' +import {expect} from 'chai' +import nock from 'nock' +import {stderr, stdout} from 'stdout-stderr' + import Cmd from '../../../../src/commands/addons/detach.js' import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' -import {expect} from 'chai' describe('addons:detach', function () { - afterEach(nock.cleanAll) + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) it('detaches an add-on', function () { - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/addon-attachments/redis-123') - .reply(200, {id: 100, name: 'redis-123', addon: {name: 'redis'}}) + .reply(200, {addon: {name: 'redis'}, id: 100, name: 'redis-123'}) .delete('/addon-attachments/100') .reply(200) .get('/apps/myapp/releases') @@ -19,9 +29,8 @@ describe('addons:detach', function () { return runCommand(Cmd, ['--app', 'myapp', 'redis-123']) .then(() => { expect(stdout.output).to.equal('') - expect(stderr.output).to.contain('Detaching redis-123 to redis from myapp... done') - expect(stderr.output).to.contain('Unsetting redis-123 config vars and restarting myapp... done, v10') + expect(stderr.output).to.contain('Detaching redis-123 to redis from ⬢ myapp... done') + expect(stderr.output).to.contain('Unsetting redis-123 config vars and restarting ⬢ myapp... done, v10') }) - .then(() => api.done()) }) }) diff --git a/packages/cli/test/unit/commands/addons/index.unit.test.ts b/packages/cli/test/unit/commands/addons/index.unit.test.ts index 3f08e68325..f9888bacf4 100644 --- a/packages/cli/test/unit/commands/addons/index.unit.test.ts +++ b/packages/cli/test/unit/commands/addons/index.unit.test.ts @@ -1,15 +1,32 @@ /* eslint-disable max-nested-callbacks */ +import * as Heroku from '@heroku-cli/schema' +import {expect} from 'chai' +import nock from 'nock' import {stdout} from 'stdout-stderr' + import Cmd from '../../../../src/commands/addons/index.js' -import runCommand from '../../../helpers/runCommand.js' import * as fixtures from '../../../fixtures/addons/fixtures.js' -import nock from 'nock' -import {expect} from 'chai' +import runCommand from '../../../helpers/runCommand.js' import expectOutput from '../../../helpers/utils/expectOutput.js' -import * as Heroku from '@heroku-cli/schema' import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js' describe('addons', function () { + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com', { + reqheaders: { + Accept: 'application/vnd.heroku+json; version=3.sdk', + 'Accept-Expansion': 'addon_service,plan', + }, + }) + }) + + afterEach(function () { + api.done() + nock.cleanAll() + }) + describe('--all', function () { let addons: Heroku.AddOn[] @@ -21,22 +38,20 @@ describe('addons', function () { context('with add-ons', function () { beforeEach(function () { - nock('https://api.heroku.com', {reqheaders: { - 'Accept-Expansion': 'addon_service,plan', - Accept: 'application/vnd.heroku+json; version=3.sdk', - }}) + api .get('/addons') .reply(200, addons) }) + it('prints add-ons in a table', async function () { await runCommand(Cmd, []) const actual = removeAllWhitespace(stdout.output) const expectedHeader = removeAllWhitespace(` Owning App Add-on Plan Price Max Price State`) const expected = removeAllWhitespace(` - acme-inc-api api-redis heroku-redis:premium-2 ~$0.083/hour $60/month created - acme-inc-www www-db heroku-postgresql:mini ~$0.007/hour $5/month created - acme-inc-www www-redis heroku-redis:premium-2 ~$0.083/hour $60/month creating`) + ⬢ acme-inc-api api-redis heroku-redis:premium-2 ~$0.083/hour $60/month created + ⬢ acme-inc-www www-db heroku-postgresql:mini ~$0.007/hour $5/month created + ⬢ acme-inc-www www-redis heroku-redis:premium-2 ~$0.083/hour $60/month creating`) expect(actual).to.include(expectedHeader) expect(actual).to.include(expected) }) @@ -58,10 +73,7 @@ describe('addons', function () { beforeEach(function () { const addon = fixtures.addons['dwh-db'] addon.billed_price = {cents: 10000} - nock('https://api.heroku.com', {reqheaders: { - 'Accept-Expansion': 'addon_service,plan', - Accept: 'application/vnd.heroku+json; version=3.sdk', - }}) + api .get('/addons') .reply(200, [addon]) }) @@ -71,7 +83,7 @@ describe('addons', function () { const expectedHeader = removeAllWhitespace(` Owning App Add-on Plan Price Max Price State`) const expected = removeAllWhitespace(` - acme-inc-dwh dwh-db heroku-postgresql:standard-2 ~$0.139/hour $100/month created`) + ⬢ acme-inc-dwh dwh-db heroku-postgresql:standard-2 ~$0.139/hour $100/month created`) expect(actual).to.include(expectedHeader) expect(actual).to.include(expected) }) @@ -80,10 +92,7 @@ describe('addons', function () { beforeEach(function () { const addon = fixtures.addons['dwh-db'] addon.billed_price = {cents: 0, contract: true} - nock('https://api.heroku.com', {reqheaders: { - 'Accept-Expansion': 'addon_service,plan', - Accept: 'application/vnd.heroku+json; version=3.sdk', - }}) + api .get('/addons') .reply(200, [addon]) }) @@ -93,7 +102,7 @@ describe('addons', function () { const expectedHeader = removeAllWhitespace(` Owning App Add-on Plan Price Max Price State`) const expected = removeAllWhitespace(` - acme-inc-dwh dwh-db heroku-postgresql:standard-2 contract contract created`) + ⬢ acme-inc-dwh dwh-db heroku-postgresql:standard-2 contract contract created`) expect(actual).to.include(expectedHeader) expect(actual).to.include(expected) }) @@ -110,10 +119,7 @@ describe('addons', function () { describe('--app', function () { function mockAPI(appName: string, addons: Heroku.AddOn[] = [], attachments: Heroku.AddOnAttachment[] = []) { - nock('https://api.heroku.com', {reqheaders: { - 'Accept-Expansion': 'addon_service,plan', - Accept: 'application/vnd.heroku+json; version=3.sdk', - }}) + api .get(`/apps/${appName}/addons`) .reply(200, addons) nock('https://api.heroku.com') @@ -168,8 +174,8 @@ describe('addons', function () { expect(actual).to.include(removeAllWhitespace('heroku-postgresql (www-db)')) expect(actual).to.include(removeAllWhitespace('mini ~$0.007/hour $5/month created')) expect(actual).to.include(removeAllWhitespace('as DATABASE')) - expect(actual).to.include(removeAllWhitespace('as WWW_DB on acme-inc-dwh app')) - expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (acme-inc-www) or other apps.')) + expect(actual).to.include(removeAllWhitespace('as WWW_DB on ⬢ acme-inc-dwh app')) + expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (⬢ acme-inc-www) or other apps.')) }) }) it('shows add-ons owned by foreign apps if attached to targeted app', function () { @@ -182,10 +188,10 @@ describe('addons', function () { Add-on Plan Price Max Price State`) expect(actual).to.include(expectedHeader) expect(actual).to.include(removeAllWhitespace('heroku-postgresql (www-db)')) - expect(actual).to.include(removeAllWhitespace('mini (billed to acme-inc-www app) (billed to acme-inc-www app) created')) + expect(actual).to.include(removeAllWhitespace('mini (billed to ⬢ acme-inc-www app) (billed to ⬢ acme-inc-www app) created')) expect(actual).to.include(removeAllWhitespace('as WWW_DB')) - expect(actual).to.include(removeAllWhitespace('as DATABASE on acme-inc-www app')) - expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (acme-inc-dwh) or other apps.')) + expect(actual).to.include(removeAllWhitespace('as DATABASE on ⬢ acme-inc-www app')) + expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (⬢ acme-inc-dwh) or other apps.')) }) }) it("doesn't show attachments that are not related to the targeted app", function () { @@ -209,7 +215,7 @@ describe('addons', function () { it('includes app name for foreign attachments', function () { return run('acme-inc-dwh', function () { - expect(stdout.output).to.match(/└─ as DATABASE on acme-inc-www app\s*$/m) + expect(stdout.output).to.match(/└─ as DATABASE on ⬢ acme-inc-www app\s*$/m) }) }) }) @@ -264,7 +270,7 @@ describe('addons', function () { fixtures.attachments['acme-inc-www::DATABASE'], fixtures.attachments['acme-inc-dwh::WWW_DB'], ]) return run('acme-inc-dwh', function () { - expect(stdout.output.indexOf('as WWW_DB')).to.be.lt(stdout.output.indexOf('as DATABASE on acme-inc-www app')) + expect(stdout.output.indexOf('as WWW_DB')).to.be.lt(stdout.output.indexOf('as DATABASE on ⬢ acme-inc-www app')) }) }) it('sorts local attachments by name', function () { @@ -280,7 +286,7 @@ describe('addons', function () { fixtures.attachments['acme-inc-api::WWW_DB'], fixtures.attachments['acme-inc-dwh::WWW_DB'], fixtures.attachments['acme-inc-www::DATABASE'], ]) return run('acme-inc-api', function () { - expect(stdout.output.indexOf('as WWW_DB on acme-inc-dwh')).to.be.lt(stdout.output.indexOf('as DATABASE on acme-inc-www')) + expect(stdout.output.indexOf('as WWW_DB on ⬢ acme-inc-dwh')).to.be.lt(stdout.output.indexOf('as DATABASE on ⬢ acme-inc-www')) }) }) it('sorts foreign attachments for same app by name', function () { @@ -288,7 +294,7 @@ describe('addons', function () { fixtures.attachments['acme-inc-api::WWW_DB'], fixtures.attachments['acme-inc-www::DATABASE'], fixtures.attachments['acme-inc-www::HEROKU_POSTGRESQL_RED'], ]) return run('acme-inc-api', function () { - expect(stdout.output.indexOf('as DATABASE on acme-inc-www')).to.be.lt(stdout.output.indexOf('as HEROKU_POSTGRESQL_RED on acme-inc-www')) + expect(stdout.output.indexOf('as DATABASE on ⬢ acme-inc-www')).to.be.lt(stdout.output.indexOf('as HEROKU_POSTGRESQL_RED on ⬢ acme-inc-www')) }) }) }) @@ -313,7 +319,7 @@ describe('addons', function () { expect(actual).to.include(removeAllWhitespace('heroku-postgresql (dwh-db)')) expect(actual).to.include(removeAllWhitespace('standard-2 ~$0.139/hour $100/month created')) expect(actual).to.include(removeAllWhitespace('as DATABASE')) - expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (acme-inc-dwh) or other apps.')) + expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (⬢ acme-inc-dwh) or other apps.')) }) }) }) @@ -336,7 +342,7 @@ describe('addons', function () { expect(actual).to.include(removeAllWhitespace('heroku-postgresql (dwh-db)')) expect(actual).to.include(removeAllWhitespace('standard-2 contract contract created')) expect(actual).to.include(removeAllWhitespace('as DATABASE')) - expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (acme-inc-dwh) or other apps.')) + expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (⬢ acme-inc-dwh) or other apps.')) }) }) }) @@ -349,9 +355,9 @@ describe('addons', function () { Add-on Plan Price Max Price State`) expect(actual).to.include(expectedHeader) expect(actual).to.include(removeAllWhitespace('? (www-db)')) - expect(actual).to.include(removeAllWhitespace('? (billed to acme-inc-www app) (billed to acme-inc-www app)')) + expect(actual).to.include(removeAllWhitespace('? (billed to ⬢ acme-inc-www app) (billed to ⬢ acme-inc-www app)')) expect(actual).to.include(removeAllWhitespace('as WWW_DB')) - expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (acme-inc-api) or other apps.')) + expect(actual).to.include(removeAllWhitespace('The table above shows add-ons and the attachments to the current app (⬢ acme-inc-api) or other apps.')) }) }) }) diff --git a/packages/cli/test/unit/commands/apps/favorites/index.unit.test.ts b/packages/cli/test/unit/commands/apps/favorites/index.unit.test.ts index 96935de072..5920c225f8 100644 --- a/packages/cli/test/unit/commands/apps/favorites/index.unit.test.ts +++ b/packages/cli/test/unit/commands/apps/favorites/index.unit.test.ts @@ -23,7 +23,7 @@ describe('apps:favorites', function () { const {stderr, stdout} = await runCommand(['apps:favorites']) - expect(stdout).to.contain('=== Favorited Apps\n\nmyapp\nmyotherapp\n') + expect(stdout).to.contain('=== Favorited Apps\n\n⬢ myapp\n⬢ myotherapp\n') expect(stderr).to.equal('') }) 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 e9f9bee869..d3d46e88ef 100644 --- a/packages/cli/test/unit/commands/apps/index.unit.test.ts +++ b/packages/cli/test/unit/commands/apps/index.unit.test.ts @@ -116,7 +116,7 @@ describe('apps', function () { const actual = removeAllWhitespace(stdout) const expectedPersonalApps = removeAllWhitespace(` === foo@bar.com Apps - example`) + ⬢ example`) const expectedCollaboratedAppsHeader = removeAllWhitespace('Collaborated Apps') const expectedCollaboratedApps = removeAllWhitespace('collab-app someone-else@bar.com') expect(actual).to.include(expectedPersonalApps) @@ -137,7 +137,7 @@ describe('apps', function () { const actual = removeAllWhitespace(stdout) const expectedPersonalApps = removeAllWhitespace(` === foo@bar.com Apps - example`) + ⬢ example`) const expectedCollaboratedAppsHeader = removeAllWhitespace('Collaborated Apps') const expectedCollaboratedApps = removeAllWhitespace('collab-app someone-else@bar.com') expect(actual).to.include(expectedPersonalApps) @@ -168,7 +168,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== foo@bar.com Apps\n\nexample\nexample-eu (eu)\n') + expect(stdout).to.equal('=== foo@bar.com Apps\n\n⬢ example\n⬢ example-eu (eu)\n') }) it('shows locked app', async function () { @@ -181,7 +181,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== foo@bar.com Apps\n\nexample\nexample-eu (eu)\nlocked-app [locked]\n') + expect(stdout).to.equal('=== foo@bar.com Apps\n\n⬢ example\n⬢ example-eu (eu)\n⬢ locked-app [locked]\n') }) it('shows locked eu app', async function () { @@ -196,7 +196,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== foo@bar.com Apps\n\nexample\nexample-eu (eu)\nlocked-app [locked] (eu)\n') + expect(stdout).to.equal('=== foo@bar.com Apps\n\n⬢ example\n⬢ example-eu (eu)\n⬢ locked-app [locked] (eu)\n') }) it('shows internal app', async function () { @@ -209,7 +209,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== foo@bar.com Apps\n\nexample\nexample-eu (eu)\ninternal-app [internal]\n') + expect(stdout).to.equal('=== foo@bar.com Apps\n\n⬢ example\n⬢ example-eu (eu)\n⬢ internal-app [internal]\n') }) it('shows internal locked app', async function () { @@ -222,7 +222,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== foo@bar.com Apps\n\nexample\nexample-eu (eu)\ninternal-app [internal/locked]\n') + expect(stdout).to.equal('=== foo@bar.com Apps\n\n⬢ example\n⬢ example-eu (eu)\n⬢ internal-app [internal/locked]\n') }) it('shows internal eu app', async function () { @@ -237,7 +237,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== foo@bar.com Apps\n\nexample\nexample-eu (eu)\ninternal-app [internal] (eu)\n') + expect(stdout).to.equal('=== foo@bar.com Apps\n\n⬢ example\n⬢ example-eu (eu)\n⬢ internal-app [internal] (eu)\n') }) it('shows internal locked eu app', async function () { @@ -252,7 +252,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== foo@bar.com Apps\n\nexample\nexample-eu (eu)\ninternal-app [internal/locked] (eu)\n') + expect(stdout).to.equal('=== foo@bar.com Apps\n\n⬢ example\n⬢ example-eu (eu)\n⬢ internal-app [internal/locked] (eu)\n') }) }) @@ -280,7 +280,7 @@ describe('apps', function () { const {stderr, stdout} = await runCommand(['apps', '--team', 'test-team']) expect(stderr).to.equal('') - expect(stdout).to.equal('=== Apps in team test-team\n\nteam-app-1\nteam-app-2\n') + expect(stdout).to.equal('=== Apps in team test-team\n\n⬢ team-app-1\n⬢ team-app-2\n') }) }) @@ -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\nspace-app-1\nspace-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\nspace-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/buildpacks/remove.unit.test.ts b/packages/cli/test/unit/commands/buildpacks/remove.unit.test.ts index 6f38ae6a45..febdee6898 100644 --- a/packages/cli/test/unit/commands/buildpacks/remove.unit.test.ts +++ b/packages/cli/test/unit/commands/buildpacks/remove.unit.test.ts @@ -133,7 +133,7 @@ Run git push heroku main to create a new release using these buildpacks. const {error} = await runCommand(['buildpacks:remove', '-i', '1', '-a', 'example']) - expect(error?.message).to.include('No buildpacks were found. Next release on example will detect buildpack normally.') + expect(error?.message).to.include('No buildpacks were found. Next release on ⬢ example will detect buildpack normally.') }) it('# returns an error when the index > 1 and the size is one', async function () { @@ -222,7 +222,7 @@ Run git push heroku main to create a new release using this buildpack. const {error} = await runCommand(['buildpacks:remove', 'https://github.com/bar/bar', '-a', 'example']) - expect(error?.message).to.include('No buildpacks were found. Next release on example will detect buildpack normally.') + expect(error?.message).to.include('No buildpacks were found. Next release on ⬢ example will detect buildpack normally.') }) it('# returns an error when the url is not found', async function () { diff --git a/packages/cli/test/unit/commands/certs/add.unit.test.ts b/packages/cli/test/unit/commands/certs/add.unit.test.ts index 7d341bf8a0..656b312876 100644 --- a/packages/cli/test/unit/commands/certs/add.unit.test.ts +++ b/packages/cli/test/unit/commands/certs/add.unit.test.ts @@ -64,7 +64,7 @@ describe('heroku certs:add', function () { 'pem_file', 'key_file', ]) - expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done\n') expect(stdout.output).to.equal(`Certificate details:\n${heredoc(certificateDetails)}`) }) @@ -81,7 +81,7 @@ describe('heroku certs:add', function () { 'pem_file', 'key_file', ]) - expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done\n') expect(stdout.output).to.eq(`Certificate details:\n${heredoc(certificateDetails)}`) }) @@ -100,7 +100,7 @@ describe('heroku certs:add', function () { 'pem_file', 'key_file', ]) - expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done\n') expect(stdout.output).to.eq(`Certificate details:\n${heredoc(certificateDetails)}=== Almost done! Which of these domains on this application would you like this certificate associated with?\n\n`) }) @@ -140,7 +140,7 @@ describe('heroku certs:add', function () { expect(stubbedSelectDomains.firstCall.args[0]).to.eql([ 'biz.example.com', ]) - expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done\n') expect(stdout.output.trim()).to.equal('Certificate details:\nCommon Name(s): foo.example.org\n bar.example.org\n biz.example.com\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nSSL certificate is self signed.\n=== Almost done! Which of these domains on this application would you like this certificate associated with?') }) @@ -171,7 +171,7 @@ describe('heroku certs:add', function () { expect(stubbedSelectDomains.firstCall.args[0]).to.eql([ 'tokyo-1050.herokuapp.com', ]) - expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done\n') expect(stdout.output.trim()).to.equal('Certificate details:\nCommon Name(s): tokyo-1050.herokuapp.com\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=heroku.com\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=tokyo-1050.herokuapp.com\nSSL certificate is not trusted.\n=== Almost done! Which of these domains on this application would you like this certificate associated with?') }) @@ -200,7 +200,7 @@ describe('heroku certs:add', function () { expect(stubbedSelectDomains.called).to.be.false mock.done() domainsMock.done() - expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done\n') expect(stdout.output.trim()).to.equal('Certificate details:\nCommon Name(s): foo.example.org\n bar.example.org\n biz.example.com\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nSSL certificate is self signed.') }) @@ -224,7 +224,7 @@ describe('heroku certs:add', function () { expect(stubbedSelectDomains.called).to.be.false mock.done() domainsMock.done() - expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done\n') expect(stdout.output.trim()).to.equal('Certificate details:\nCommon Name(s): *.example.org\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nSSL certificate is self signed.') }) @@ -260,7 +260,7 @@ describe('heroku certs:add', function () { mock.done() domainsMock.done() domainsMockPatch.done() - expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done\n') expect(stdout.output.trim()).to.equal('Certificate details:\nCommon Name(s): *.example.org\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nSSL certificate is self signed.\n=== Almost done! Which of these domains on this application would you like this certificate associated with?') }) @@ -392,7 +392,7 @@ describe('heroku certs:add', function () { domainsCreateFoo.done() domainsCreateBar.done() domainsCreateBiz.done() - expect(stderr.output).to.contain('Adding SSL certificate to example... done\n') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done\n') expect(stderr.output).to.contain('Waiting for stable domains to be created... done\n') expect(stdout.output.trim()).to.equal('Certificate details:\nCommon Name(s): foo.example.org\n bar.example.org\n biz.example.com\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nSSL certificate is self signed.\n=== Almost done! Which of these domains on this application would you like this certificate associated with?') }) @@ -441,7 +441,7 @@ describe('heroku certs:add', function () { expect(message).to.contain('Timed out while waiting for stable domains to be created') } - expect(stderr.output).to.contain('Adding SSL certificate to example... done') + expect(stderr.output).to.contain('Adding SSL certificate to ⬢ example... done') expect(stderr.output).to.contain('Waiting for stable domains to be created... !') expect(stdout.output).to.equal('Certificate details:\nCommon Name(s): foo.example.org\n bar.example.org\n biz.example.com\nExpires At: 2013-08-01 21:34 UTC\nIssuer: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nStarts At: 2012-08-01 21:34 UTC\nSubject: /C=US/ST=California/L=San Francisco/O=Heroku by Salesforce/CN=secure.example.org\nSSL certificate is self signed.\n') }) diff --git a/packages/cli/test/unit/commands/certs/auto/index.unit.test.ts b/packages/cli/test/unit/commands/certs/auto/index.unit.test.ts index 59cdf14530..e3c2c3d134 100644 --- a/packages/cli/test/unit/commands/certs/auto/index.unit.test.ts +++ b/packages/cli/test/unit/commands/certs/auto/index.unit.test.ts @@ -1,10 +1,11 @@ -import {stdout, stderr} from 'stdout-stderr' -import Cmd from '../../../../../src/commands/certs/auto/index.js' -import runCommand from '../../../../helpers/runCommand.js' -import nock from 'nock' import {expect} from 'chai' -import tsheredoc from 'tsheredoc' +import nock from 'nock' import sinon from 'sinon' +import {stderr, stdout} from 'stdout-stderr' +import tsheredoc from 'tsheredoc' + +import Cmd from '../../../../../src/commands/certs/auto/index.js' +import runCommand from '../../../../helpers/runCommand.js' import removeAllWhitespace from '../../../../helpers/utils/remove-whitespaces.js' const heredoc = tsheredoc.default @@ -12,12 +13,12 @@ const sandbox = sinon.createSandbox() const letsEncrypt = { domains: [], ssl_cert: { + acm: true, cert_domains: ['heroku-acm.heroku-cli-sni-test.com', 'heroku-san-test.heroku-cli-sni-test.com'], - issuer: "/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3", expires_at: '2012-08-01T21:34:23Z', + issuer: "/C=US/O=Let's Encrypt/CN=Let's Encrypt Authority X3", starts_at: '2013-08-01T21:34:23Z', subject: '/CN=heroku-acm.heroku-cli-sni-test.com', - acm: true, }, } @@ -29,34 +30,41 @@ const selfSigned = { } describe('heroku certs:auto', function () { + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + afterEach(function () { + api.done() nock.cleanAll() }) it('displays enabled status message', async function () { const now = new Date().toISOString() - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, [letsEncrypt]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-san-test.heroku-cli-sni-test.com', - cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-san-test.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) @@ -65,10 +73,8 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') - expect(stdout.output).to.include('=== Automatic Certificate Management is enabled on example') + expect(stdout.output).to.include('=== Automatic Certificate Management is enabled on ⬢ example') expect(stdout.output).to.include(heredoc` Certificate details: Common Name(s): heroku-acm.heroku-cli-sni-test.com @@ -90,56 +96,56 @@ describe('heroku certs:auto', function () { it('displays partially enabled status message', async function () { const now = new Date().toISOString() - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, [letsEncrypt]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-san-test.heroku-cli-sni-test.com', - cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-san-test.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-in-prog.heroku-cli-sni-test.com', - cname: 'heroku-in-prog.heroku-cli-sni-test.com.herokudns.com', acm_status: 'in-progress', + cname: 'heroku-in-prog.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-in-prog.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', hostname: 'heroku-verified.heroku-cli-sni-test.com', - cname: 'heroku-verified.heroku-cli-sni-test.com.herokudns.com', - acm_status: 'verified', + acm_status: 'verified', cname: 'heroku-verified.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-verified.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-dns-verified.heroku-cli-sni-test.com', - cname: 'heroku-dns-verified.heroku-cli-sni-test.com.herokudns.com', acm_status: 'dns-verified', + cname: 'heroku-dns-verified.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-dns-verified.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-missing.heroku-cli-sni-test.com', - cname: 'heroku-missing.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failing', + cname: 'heroku-missing.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-missing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', hostname: 'heroku-unknown.heroku-cli-sni-test.com', - cname: 'heroku-unknown.heroku-cli-sni-test.com.herokudns.com', - acm_status: null, + acm_status: null, cname: 'heroku-unknown.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-unknown.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) @@ -148,10 +154,8 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stdout.output).to.include(heredoc` - === Automatic Certificate Management is enabled on example + === Automatic Certificate Management is enabled on ⬢ example Certificate details: Common Name(s): heroku-acm.heroku-cli-sni-test.com @@ -185,40 +189,40 @@ describe('heroku certs:auto', function () { const now = new Date().toISOString() const sslCert = {...letsEncrypt.ssl_cert, acm: false} const acmFalse = {...letsEncrypt, ssl_cert: sslCert} - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, [acmFalse]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-san-test.heroku-cli-sni-test.com', - cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-san-test.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-missing.heroku-cli-sni-test.com', - cname: 'heroku-missing.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failing', + cname: 'heroku-missing.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-missing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-unknown.heroku-cli-sni-test.com', - cname: 'heroku-unknown.heroku-cli-sni-test.com.herokudns.com', acm_status: null, + cname: 'heroku-unknown.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-unknown.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) @@ -227,8 +231,6 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') const actual = removeAllWhitespace(stdout.output) const expectedHeader = removeAllWhitespace('Domain Status Last Updated') @@ -247,34 +249,34 @@ describe('heroku certs:auto', function () { it('displays partially enabled status with failed message', async function () { const now = new Date().toISOString() - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, [letsEncrypt]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-san-test.heroku-cli-sni-test.com', - cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-san-test.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failed.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failed', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failed.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) @@ -283,11 +285,9 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') expect(stdout.output).to.include(heredoc` - === Automatic Certificate Management is enabled on example + === Automatic Certificate Management is enabled on ⬢ example Certificate details: Common Name(s): heroku-acm.heroku-cli-sni-test.com @@ -314,34 +314,34 @@ describe('heroku certs:auto', function () { it('displays partially enabled status with failing message', async function () { const now = new Date().toISOString() - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, [letsEncrypt]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-san-test.heroku-cli-sni-test.com', - cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-san-test.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-san-test.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failed.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failing', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failed.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now}]) await runCommand(Cmd, [ @@ -349,11 +349,9 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') expect(stdout.output).to.include(heredoc` - === Automatic Certificate Management is enabled on example + === Automatic Certificate Management is enabled on ⬢ example Certificate details: Common Name(s): heroku-acm.heroku-cli-sni-test.com @@ -379,7 +377,7 @@ describe('heroku certs:auto', function () { }) it('displays disabled status message', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: false}) .get('/apps/example/sni-endpoints') @@ -390,36 +388,34 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') - expect(stdout.output).to.equal('=== Automatic Certificate Management is disabled on example\n\n') + expect(stdout.output).to.equal('=== Automatic Certificate Management is disabled on ⬢ example\n\n') }) it('displays message that there are no certificates', async function () { const now = new Date().toISOString() - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, []) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failing.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failing', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) @@ -428,10 +424,8 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') - expect(stdout.output).to.include('=== Automatic Certificate Management is enabled on example') + expect(stdout.output).to.include('=== Automatic Certificate Management is enabled on ⬢ example') const actual = removeAllWhitespace(stdout.output) const expectedHeader = removeAllWhitespace('Domain Status Last Updated') const expected = removeAllWhitespace(heredoc(` @@ -443,14 +437,14 @@ describe('heroku certs:auto', function () { }) it('displays message that there are no domains', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, []) .get('/apps/example/domains') .reply(200, [ - {kind: 'heroku', hostname: 'tokyo-1050.herokuapp.com', cname: null, acm_status: null}, + {acm_status: null, cname: null, hostname: 'tokyo-1050.herokuapp.com', kind: 'heroku'}, ]) await runCommand(Cmd, [ @@ -458,18 +452,16 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') expect(stdout.output).to.equal(heredoc` - === Automatic Certificate Management is enabled on example + === Automatic Certificate Management is enabled on ⬢ example === Add a custom domain to your app by running: heroku domains:add \n `) }) it('does not displays message that there are no certificates', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: false}) .get('/apps/example/sni-endpoints') @@ -480,14 +472,12 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') - expect(stdout.output).to.equal('=== Automatic Certificate Management is disabled on example\n\n') + expect(stdout.output).to.equal('=== Automatic Certificate Management is disabled on ⬢ example\n\n') }) it('displays message that there are no ACM certificates', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') @@ -500,18 +490,16 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') expect(stdout.output).to.equal(heredoc` - === Automatic Certificate Management is enabled on example + === Automatic Certificate Management is enabled on ⬢ example === Add a custom domain to your app by running: heroku domains:add \n `) }) it('does not displays message that there are not acm certificates', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: false}) .get('/apps/example/sni-endpoints') @@ -522,32 +510,30 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') - expect(stdout.output).to.equal('=== Automatic Certificate Management is disabled on example\n\n') + expect(stdout.output).to.equal('=== Automatic Certificate Management is disabled on ⬢ example\n\n') }) it('shows acm_status_reason', async function () { const now = new Date().toISOString() - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, [letsEncrypt]) .get('/apps/example/domains') .reply(200, [{ - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'ok', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failed.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failed', acm_status_reason: 'uh oh something failed', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failed.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) @@ -556,11 +542,9 @@ describe('heroku certs:auto', function () { 'example', ]) - api.done() - expect(stderr.output).to.equal('') expect(stdout.output).to.include(heredoc` - === Automatic Certificate Management is enabled on example + === Automatic Certificate Management is enabled on ⬢ example Certificate details: Common Name(s): heroku-acm.heroku-cli-sni-test.com @@ -600,85 +584,85 @@ describe('heroku certs:auto', function () { it('waits until certs are issued and displays the domains details', async function () { const now = commandExecutedTime - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, []) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failing.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failing', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failing.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failing', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failing.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failing.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) @@ -688,12 +672,10 @@ describe('heroku certs:auto', function () { '--wait', ]) - api.done() - expect(stderr.output).to.equal(heredoc` Waiting until the certificate is issued to all domains... done `) - expect(stdout.output).to.include('=== Automatic Certificate Management is enabled on example') + expect(stdout.output).to.include('=== Automatic Certificate Management is enabled on ⬢ example') const expectedHeader = removeAllWhitespace('Domain Status Last Updated') const expected = removeAllWhitespace(heredoc(` heroku-acm.heroku-cli-sni-test.com Cert issued less than a minute @@ -706,85 +688,85 @@ describe('heroku certs:auto', function () { it('waits until certs are issued or failed and displays the domains details ignoring errors while waiting', async function () { const now = new Date().toISOString() - const api = nock('https://api.heroku.com') + api .get('/apps/example') .reply(200, {acm: true}) .get('/apps/example/sni-endpoints') .reply(200, []) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failing.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failing', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failing.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failing', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failing.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failed', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) .get('/apps/example/domains') .reply(200, [{ - kind: 'heroku', - hostname: 'tokyo-1050.herokuapp.com', - cname: null, acm_status: null, + cname: null, + hostname: 'tokyo-1050.herokuapp.com', + kind: 'heroku', }, { - kind: 'custom', - hostname: 'heroku-acm.heroku-cli-sni-test.com', - cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', acm_status: 'cert issued', + cname: 'heroku-acm.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-acm.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }, { - kind: 'custom', - hostname: 'heroku-failing.heroku-cli-sni-test.com', - cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', acm_status: 'failed', + cname: 'heroku-failed.heroku-cli-sni-test.com.herokudns.com', + hostname: 'heroku-failing.heroku-cli-sni-test.com', + kind: 'custom', updated_at: now, }]) @@ -794,12 +776,10 @@ describe('heroku certs:auto', function () { '--wait', ]) - api.done() - expect(stderr.output).to.equal(heredoc` Waiting until the certificate is issued to all domains... ! `) - expect(stdout.output).to.include('=== Automatic Certificate Management is enabled on example') + expect(stdout.output).to.include('=== Automatic Certificate Management is enabled on ⬢ example') const actual = removeAllWhitespace(stdout.output) const expectedHeader = removeAllWhitespace('Domain Status Last Updated') const expected = removeAllWhitespace(heredoc(` diff --git a/packages/cli/test/unit/commands/certs/remove.unit.test.ts b/packages/cli/test/unit/commands/certs/remove.unit.test.ts index 92d38a4182..45dbc09bba 100644 --- a/packages/cli/test/unit/commands/certs/remove.unit.test.ts +++ b/packages/cli/test/unit/commands/certs/remove.unit.test.ts @@ -1,13 +1,14 @@ -import {stdout, stderr} from 'stdout-stderr' +import {expect} from 'chai' +import nock from 'nock' +import {stderr, stdout} from 'stdout-stderr' +import stripAnsi from 'strip-ansi' +import tsheredoc from 'tsheredoc' + import Cmd from '../../../../src/commands/certs/remove.js' +import {SniEndpoint} from '../../../../src/lib/types/sni_endpoint.js' import runCommand from '../../../helpers/runCommand.js' -import tsheredoc from 'tsheredoc' -import nock from 'nock' import {endpoint} from '../../../helpers/stubs/sni-endpoints.js' import * as sharedSni from './shared_sni.unit.test.js' -import {SniEndpoint} from '../../../../src/lib/types/sni_endpoint.js' -import {expect} from 'chai' -import stripAnsi from 'strip-ansi' const heredoc = tsheredoc.default diff --git a/packages/cli/test/unit/commands/certs/update.unit.test.ts b/packages/cli/test/unit/commands/certs/update.unit.test.ts index c6ee50b133..467bd23e7b 100644 --- a/packages/cli/test/unit/commands/certs/update.unit.test.ts +++ b/packages/cli/test/unit/commands/certs/update.unit.test.ts @@ -1,17 +1,18 @@ -import {stdout, stderr} from 'stdout-stderr' -import Cmd from '../../../../src/commands/certs/update.js' -import runCommand from '../../../helpers/runCommand.js' -import tsheredoc from 'tsheredoc' -import nock from 'nock' -import {endpoint, certificateDetails} from '../../../helpers/stubs/sni-endpoints.js' -import * as sharedSni from './shared_sni.unit.test.js' -import {SniEndpoint} from '../../../../src/lib/types/sni_endpoint.js' +import {Errors} from '@oclif/core' import {expect} from 'chai' -import stripAnsi from 'strip-ansi' +import nock from 'nock' import * as sinon from 'sinon' -import {Errors} from '@oclif/core' -import {CertAndKeyManager} from '../../../../src/lib/certs/get_cert_and_key.js' import {SinonStub} from 'sinon' +import {stderr, stdout} from 'stdout-stderr' +import stripAnsi from 'strip-ansi' +import tsheredoc from 'tsheredoc' + +import Cmd from '../../../../src/commands/certs/update.js' +import {CertAndKeyManager} from '../../../../src/lib/certs/get_cert_and_key.js' +import {SniEndpoint} from '../../../../src/lib/types/sni_endpoint.js' +import runCommand from '../../../helpers/runCommand.js' +import {certificateDetails, endpoint} from '../../../helpers/stubs/sni-endpoints.js' +import * as sharedSni from './shared_sni.unit.test.js' const heredoc = tsheredoc.default @@ -90,7 +91,7 @@ describe('heroku certs:update', function () { api.done() expect(stderr.output).to.equal(heredoc` - Updating SSL certificate tokyo-1050 for example... done + Updating SSL certificate tokyo-1050 for ⬢ example... done `) expect(stdout.output).to.equal(`Updated certificate details:\n${heredoc(certificateDetails)}`) }) @@ -140,7 +141,7 @@ describe('shared', function () { const stderr = function (endpoint: Partial) { return heredoc` - Updating SSL certificate ${endpoint.name} for example... done\n + Updating SSL certificate ${endpoint.name} for ⬢ example... done\n ` } @@ -149,5 +150,10 @@ describe('shared', function () { return `Updated certificate details:\n${heredoc(certificateDetails)}\n` } - sharedSni.shouldHandleArgs('certs:update', Cmd, callback, {stdout, stderr, flags: {confirm: 'example'}, args: ['pem_file', 'key_file']}) + sharedSni.shouldHandleArgs('certs:update', Cmd, callback, { + args: ['pem_file', 'key_file'], + flags: {confirm: 'example'}, + stderr, + stdout, + }) }) diff --git a/packages/cli/test/unit/commands/config/index.unit.test.ts b/packages/cli/test/unit/commands/config/index.unit.test.ts index 20a583216d..e541ffdbe8 100644 --- a/packages/cli/test/unit/commands/config/index.unit.test.ts +++ b/packages/cli/test/unit/commands/config/index.unit.test.ts @@ -21,7 +21,7 @@ describe('config', function () { const {stdout} = await runCommand(['config', '--app=myapp']) - expect(stdout).to.equal('=== myapp Config Vars\n\nLANG: en_US.UTF-8\nRACK_ENV: production\n') + expect(stdout).to.equal('=== ⬢ myapp Config Vars\n\nLANG: en_US.UTF-8\nRACK_ENV: production\n') }) it('--json', async function () { diff --git a/packages/cli/test/unit/commands/domains/index.unit.test.ts b/packages/cli/test/unit/commands/domains/index.unit.test.ts index 528e1de529..c3b2e143d8 100644 --- a/packages/cli/test/unit/commands/domains/index.unit.test.ts +++ b/packages/cli/test/unit/commands/domains/index.unit.test.ts @@ -119,9 +119,9 @@ describe('domains', function () { await runCommand(DomainsIndex, ['--app', 'myapp']) - expect(stdout.output).to.contain('=== myapp Heroku Domain\n\nmyapp.herokuapp.com') + expect(stdout.output).to.contain('=== ⬢ myapp Heroku Domain\n\nmyapp.herokuapp.com') expect(stdout.output).to.contain('myapp.herokuapp.com') - expect(stdout.output).to.not.contain('=== myapp Custom Domains') + expect(stdout.output).to.not.contain('=== ⬢ myapp Custom Domains') }) it('shows a list of domains and their DNS targets when there are custom domains', async function () { @@ -132,9 +132,9 @@ describe('domains', function () { await runCommand(DomainsIndex, ['--app', 'myapp']) const actual = removeAllWhitespace(stdout.output) - expect(stdout.output).to.contain('=== myapp Heroku Domain\n\nmyapp.herokuapp.com') + expect(stdout.output).to.contain('=== ⬢ myapp Heroku Domain\n\nmyapp.herokuapp.com') expect(stdout.output).to.contain('myapp.herokuapp.com') - expect(stdout.output).to.contain('=== myapp Custom Domains') + expect(stdout.output).to.contain('=== ⬢ myapp Custom Domains') expect(actual).to.contain(removeAllWhitespace('Domain Name DNS Record Type DNS Target')) expect(actual).to.contain(removeAllWhitespace('example.com ALIAS or ANAME foo.herokudns.com')) expect(actual).to.contain(removeAllWhitespace('www.example.com CNAME bar.herokudns.com')) @@ -178,7 +178,7 @@ describe('domains', function () { await runCommand(DomainsIndex, ['--app', 'myapp']) - expect(stdout.output).to.contain('=== myapp Heroku Domain') + expect(stdout.output).to.contain('=== ⬢ myapp Heroku Domain') expect(unwrap(stderr.output)).to.contain('Warning: This app has over 100 domains. Your terminal may not be configured to display the total amount of domains.') }) }) diff --git a/packages/cli/test/unit/commands/pg/connection-pooling/attach.unit.test.ts b/packages/cli/test/unit/commands/pg/connection-pooling/attach.unit.test.ts index f20b72cfca..439fa9efbc 100644 --- a/packages/cli/test/unit/commands/pg/connection-pooling/attach.unit.test.ts +++ b/packages/cli/test/unit/commands/pg/connection-pooling/attach.unit.test.ts @@ -1,14 +1,15 @@ -import {stdout, stderr} from 'stdout-stderr' -import runCommand from '../../../../helpers/runCommand.js' import {expect} from 'chai' import nock from 'nock' +import {stderr, stdout} from 'stdout-stderr' + import Cmd from '../../../../../src/commands/pg/connection-pooling/attach.js' import {resolvedAttachments} from '../../../../fixtures/addons/fixtures.js' +import runCommand from '../../../../helpers/runCommand.js' describe('pg:connection-pooling:attach', function () { const addon = { - name: 'postgres-1', id: '1234', + name: 'postgres-1', plan: {name: 'heroku-postgresql:standard-0'}, } let api: nock.Scope @@ -36,7 +37,7 @@ describe('pg:connection-pooling:attach', function () { context('includes an attachment name', function () { beforeEach(function () { pg.post(`/client/v11/databases/${addon.name}/connection-pooling`, { - credential: defaultCredential, name: attachmentName, app: 'myapp', + app: 'myapp', credential: defaultCredential, name: attachmentName, }).reply(201, {name: attachmentName}) }) @@ -51,14 +52,14 @@ describe('pg:connection-pooling:attach', function () { expect(stdout.output).to.equal('') expect(stderr.output).to.contain('Enabling Connection Pooling on') - expect(stderr.output).to.contain(`Setting ${attachmentName} config vars and restarting myapp... done, v0`) + expect(stderr.output).to.contain(`Setting ${attachmentName} config vars and restarting ⬢ myapp... done, v0`) }) }) context('base command with no credential or attachment name', function () { beforeEach(function () { pg.post(`/client/v11/databases/${addon.name}/connection-pooling`, { - credential: defaultCredential, app: 'myapp', + app: 'myapp', credential: defaultCredential, }).reply(201, {name: 'HEROKU_COLOR'}) }) @@ -71,7 +72,7 @@ describe('pg:connection-pooling:attach', function () { expect(stdout.output).to.equal('') expect(stderr.output).to.contain('Enabling Connection Pooling on') - expect(stderr.output).to.contain('Setting HEROKU_COLOR config vars and restarting myapp... done, v0') + expect(stderr.output).to.contain('Setting HEROKU_COLOR config vars and restarting ⬢ myapp... done, v0') }) }) }) diff --git a/packages/cli/test/unit/commands/pipelines/setup.unit.test.ts b/packages/cli/test/unit/commands/pipelines/setup.unit.test.ts index d6f1cb8b99..ff82843cb9 100644 --- a/packages/cli/test/unit/commands/pipelines/setup.unit.test.ts +++ b/packages/cli/test/unit/commands/pipelines/setup.unit.test.ts @@ -1,6 +1,5 @@ /* eslint-disable max-nested-callbacks */ -import {hux} from '@heroku/heroku-cli-util' -import {color} from '@heroku-cli/color' +import {color, hux} from '@heroku/heroku-cli-util' import {runCommand} from '@oclif/test' import {expect} from 'chai' import nock from 'nock' diff --git a/packages/cli/test/unit/commands/ps/index.unit.test.ts b/packages/cli/test/unit/commands/ps/index.unit.test.ts index 856dfc4013..ac8662e622 100644 --- a/packages/cli/test/unit/commands/ps/index.unit.test.ts +++ b/packages/cli/test/unit/commands/ps/index.unit.test.ts @@ -1,12 +1,14 @@ -import {stdout, stderr} from 'stdout-stderr' -import Cmd from '../../../../src/commands/ps/index.js' -import runCommand from '../../../helpers/runCommand.js' -import normalizeTableOutput from '../../../helpers/utils/normalizeTableOutput.js' -import nock from 'nock' import {expect} from 'chai' +import nock from 'nock' +import {stderr, stdout} from 'stdout-stderr' import strftime from 'strftime' import stripAnsi from 'strip-ansi' import tsheredoc from 'tsheredoc' + +import Cmd from '../../../../src/commands/ps/index.js' +import runCommand from '../../../helpers/runCommand.js' +import normalizeTableOutput from '../../../helpers/utils/normalizeTableOutput.js' + const heredoc = tsheredoc.default const hourAgo = new Date(Date.now() - (60 * 60 * 1000)) @@ -15,10 +17,10 @@ const hourAgoStr = strftime('%Y/%m/%d %H:%M:%S %z', hourAgo) function stubAccountQuota(code: number, body: Record) { nock('https://api.heroku.com', {reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp') - .reply(200, {process_tier: 'eco', owner: {id: '1234'}, id: '6789'}) + .reply(200, {id: '6789', owner: {id: '1234'}, process_tier: 'eco'}) nock('https://api.heroku.com', {reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp/dynos') - .reply(200, [{command: 'bash', size: 'Eco', name: 'run.1', type: 'run', updated_at: hourAgo, state: 'up'}]) + .reply(200, [{command: 'bash', name: 'run.1', size: 'Eco', state: 'up', type: 'run', updated_at: hourAgo}]) nock('https://api.heroku.com', {reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/account') .reply(200, {id: '1234'}) @@ -30,7 +32,7 @@ function stubAccountQuota(code: number, body: Record) { function stubAppAndAccount() { nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp') - .reply(200, {process_tier: 'basic', owner: {id: '1234'}, id: '6789'}) + .reply(200, {id: '6789', owner: {id: '1234'}, process_tier: 'basic'}) nock('https://api.heroku.com', {reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/account') .reply(200, {id: '1234'}) @@ -45,8 +47,22 @@ describe('ps', function () { const api = nock('https://api.heroku.com', {reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp/dynos') .reply(200, [ - {command: 'npm start', size: 'Eco', name: 'web.1', type: 'web', updated_at: hourAgo, state: 'up'}, - {command: 'bash', size: 'Eco', name: 'run.1', type: 'run', updated_at: hourAgo, state: 'up'}, + { + command: 'npm start', + name: 'web.1', + size: 'Eco', + state: 'up', + type: 'web', + updated_at: hourAgo, + }, + { + command: 'bash', + name: 'run.1', + size: 'Eco', + state: 'up', + type: 'run', + updated_at: hourAgo, + }, ]) stubAppAndAccount() @@ -74,9 +90,9 @@ describe('ps', function () { const api = nock('https://api.heroku.com', {reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp/dynos') .reply(200, [ - {command: 'npm start', size: '1X-Classic', name: 'web.4ed720fa31-ur8z1', type: 'web', updated_at: hourAgo, state: 'up'}, - {command: 'npm start', size: '1X-Classic', name: 'web.4ed720fa31-5om2v', type: 'web', updated_at: hourAgo, state: 'up'}, - {command: 'npm start ./worker.js', size: '2X-Compute', name: 'node-worker.4ed720fa31-w4llb', type: 'node-worker', updated_at: hourAgo, state: 'up'}, + {command: 'npm start', name: 'web.4ed720fa31-ur8z1', size: '1X-Classic', state: 'up', type: 'web', updated_at: hourAgo}, + {command: 'npm start', name: 'web.4ed720fa31-5om2v', size: '1X-Classic', state: 'up', type: 'web', updated_at: hourAgo}, + {command: 'npm start ./worker.js', name: 'node-worker.4ed720fa31-w4llb', size: '2X-Compute', state: 'up', type: 'node-worker', updated_at: hourAgo}, ]) stubAppAndAccount() @@ -107,8 +123,8 @@ describe('ps', function () { .reply(200, {space: {shield: true}}) .get('/apps/myapp/dynos') .reply(200, [ - {command: 'npm start', size: 'Private-M', name: 'web.1', type: 'web', updated_at: hourAgo, state: 'up'}, - {command: 'bash', size: 'Private-L', name: 'run.1', type: 'run', updated_at: hourAgo, state: 'up'}, + {command: 'npm start', name: 'web.1', size: 'Private-M', state: 'up', type: 'web', updated_at: hourAgo}, + {command: 'bash', name: 'run.1', size: 'Private-L', state: 'up', type: 'run', updated_at: hourAgo}, ]) stubAppAndAccount() @@ -137,8 +153,8 @@ describe('ps', function () { const api = nock('https://api.heroku.com', {reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp/dynos') .reply(200, [ - {command: 'npm start', size: 'Eco', name: 'web.1', type: 'web', updated_at: hourAgo, state: 'up'}, - {command: 'bash', size: 'Eco', name: 'run.1', type: 'run', updated_at: hourAgo, state: 'up'}, + {command: 'npm start', name: 'web.1', size: 'Eco', state: 'up', type: 'web', updated_at: hourAgo}, + {command: 'bash', name: 'run.1', size: 'Eco', state: 'up', type: 'run', updated_at: hourAgo}, ]) stubAppAndAccount() @@ -150,7 +166,7 @@ describe('ps', function () { 'myapp', ]) } catch (error: any) { - expect(stripAnsi(error.message)).to.include('No foo dynos on myapp') + expect(stripAnsi(error.message)).to.include('No foo dynos on ⬢ myapp') } api.done() @@ -166,7 +182,7 @@ describe('ps', function () { .reply(200, {name: 'myapp'}) .get('/apps/myapp/dynos') .reply(200, [ - {command: 'npm start', size: 'Eco', name: 'web.1', type: 'web', updated_at: hourAgo, state: 'up'}, + {command: 'npm start', name: 'web.1', size: 'Eco', state: 'up', type: 'web', updated_at: hourAgo}, ]) await runCommand(Cmd, [ @@ -188,25 +204,43 @@ describe('ps', function () { .reply(200, {name: 'myapp'}) .get('/apps/myapp/dynos?extended=true') .reply(200, [{ - id: '100', command: 'npm start', - size: 'Eco', + extended: { + az: 'us-east', + execution_plane: 'execution_plane', + fleet: 'fleet', + instance: 'instance', + ip: '10.0.0.1', + port: 8000, + region: 'us', + route: 'da route', + }, + id: '100', name: 'web.1', + release: {id: '10', version: '40'}, + size: 'Eco', + state: 'up', type: 'web', updated_at: hourAgo, - state: 'up', - release: {id: '10', version: '40'}, - extended: {region: 'us', execution_plane: 'execution_plane', fleet: 'fleet', instance: 'instance', ip: '10.0.0.1', port: 8000, az: 'us-east', route: 'da route'}, }, { - id: '101', command: 'bash', - size: 'Eco', + extended: { + az: 'us-east', + execution_plane: 'execution_plane', + fleet: 'fleet', + instance: 'instance', + ip: '10.0.0.2', + port: 8000, + region: 'us', + route: 'da route', + }, + id: '101', name: 'run.1', + release: {id: '10', version: '40'}, + size: 'Eco', + state: 'up', type: 'run', updated_at: hourAgo, - state: 'up', - release: {id: '10', version: '40'}, - extended: {region: 'us', execution_plane: 'execution_plane', fleet: 'fleet', instance: 'instance', ip: '10.0.0.2', port: 8000, az: 'us-east', route: 'da route'}, }]) await runCommand(Cmd, [ @@ -235,14 +269,7 @@ describe('ps', function () { .reply(200, {name: 'myapp'}) .get('/apps/myapp/dynos?extended=true') .reply(200, [{ - id: '100', command: 'npm start', - size: 'Eco', - name: 'web.1', - type: 'web', - updated_at: hourAgo, - state: 'up', - release: {id: '10', version: '40'}, extended: { az: null, execution_plane: null, @@ -253,15 +280,15 @@ describe('ps', function () { region: 'us', route: null, }, - }, { - id: '101', - command: 'bash', + id: '100', + name: 'web.1', + release: {id: '10', version: '40'}, size: 'Eco', - name: 'run.1', - type: 'run', - updated_at: hourAgo, state: 'up', - release: {id: '10', version: '40'}, + type: 'web', + updated_at: hourAgo, + }, { + command: 'bash', extended: { az: null, execution_plane: null, @@ -272,6 +299,13 @@ describe('ps', function () { region: 'us', route: null, }, + id: '101', + name: 'run.1', + release: {id: '10', version: '40'}, + size: 'Eco', + state: 'up', + type: 'run', + updated_at: hourAgo, }]) await runCommand(Cmd, [ @@ -299,25 +333,25 @@ describe('ps', function () { .reply(200, {space: {shield: true}}) .get('/apps/myapp/dynos?extended=true') .reply(200, [{ - id: 100, command: 'npm start', - size: 'Private-M', + extended: {region: 'us', execution_plane: 'execution_plane', fleet: 'fleet', instance: 'instance', ip: '10.0.0.1', port: 8000, az: 'us-east', route: 'da route'}, + id: 100, name: 'web.1', + release: {id: '10', version: '40'}, + size: 'Private-M', + state: 'up', type: 'web', updated_at: hourAgo, - state: 'up', - release: {id: '10', version: '40'}, - extended: {region: 'us', execution_plane: 'execution_plane', fleet: 'fleet', instance: 'instance', ip: '10.0.0.1', port: 8000, az: 'us-east', route: 'da route'}, }, { - id: 101, command: 'bash', - size: 'Private-L', + extended: {region: 'us', execution_plane: 'execution_plane', fleet: 'fleet', instance: 'instance', ip: '10.0.0.2', port: 8000, az: 'us-east', route: 'da route'}, + id: 101, name: 'run.1', + release: {id: '10', version: '40'}, + size: 'Private-L', + state: 'up', type: 'run', updated_at: hourAgo, - state: 'up', - release: {id: '10', version: '40'}, - extended: {region: 'us', execution_plane: 'execution_plane', fleet: 'fleet', instance: 'instance', ip: '10.0.0.2', port: 8000, az: 'us-east', route: 'da route'}, }]) await runCommand(Cmd, [ @@ -349,7 +383,7 @@ describe('ps', function () { run.1 (Eco): up ${hourAgoStr} (~ 1h ago): bash ` - stubAccountQuota(200, {account_quota: 1000, quota_used: 1, apps: []}) + stubAccountQuota(200, {account_quota: 1000, apps: [], quota_used: 1}) await runCommand(Cmd, [ '--app', @@ -372,7 +406,7 @@ describe('ps', function () { run.1 (Eco): up ${hourAgoStr} (~ 1h ago): bash ` - stubAccountQuota(200, {account_quota: 3600000, quota_used: 178200, apps: []}) + stubAccountQuota(200, {account_quota: 3600000, apps: [], quota_used: 178200}) await runCommand(Cmd, [ '--app', @@ -395,7 +429,7 @@ describe('ps', function () { run.1 (Eco): up ${hourAgoStr} (~ 1h ago): bash ` - stubAccountQuota(200, {account_quota: 3600000, quota_used: 178200, apps: [{app_uuid: '6789', quota_used: 178200}]}) + stubAccountQuota(200, {account_quota: 3600000, apps: [{app_uuid: '6789', quota_used: 178200}], quota_used: 178200}) await runCommand(Cmd, [ '--app', @@ -418,7 +452,7 @@ describe('ps', function () { run.1 (Eco): up ${hourAgoStr} (~ 1h ago): bash ` - stubAccountQuota(200, {account_quota: 0, quota_used: 0, apps: []}) + stubAccountQuota(200, {account_quota: 0, apps: [], quota_used: 0}) await runCommand(Cmd, [ '--app', @@ -472,14 +506,14 @@ describe('ps', function () { nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp') .reply(200, { - process_tier: 'eco', owner: {id: '5678'}, + owner: {id: '5678'}, process_tier: 'eco', }) nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.account-quotas'}}) .get('/accounts/1234/actions/get-quota') - .reply(200, {account_quota: 1000, quota_used: 1, apps: []}) + .reply(200, {account_quota: 1000, apps: [], quota_used: 1}) const dynos = nock('https://api.heroku.com', {reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp/dynos') - .reply(200, [{command: 'bash', size: 'Eco', name: 'run.1', type: 'run', updated_at: hourAgo, state: 'up'}]) + .reply(200, [{command: 'bash', name: 'run.1', size: 'Eco', state: 'up', type: 'run', updated_at: hourAgo}]) const ecoExpression = heredoc` === run: one-off processes (1) @@ -504,10 +538,10 @@ describe('ps', function () { .reply(200, {id: '1234'}) nock('https://api.heroku.com', {reqheaders: {Accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp') - .reply(200, {process_tier: 'eco', owner: {id: 1234}}) + .reply(200, {owner: {id: 1234}, process_tier: 'eco'}) const dynos = nock('https://api.heroku.com', {reqheaders: {accept: 'application/vnd.heroku+json; version=3.sdk'}}) .get('/apps/myapp/dynos') - .reply(200, [{command: 'bash', size: 'Eco', name: 'run.1', type: 'run', updated_at: hourAgo, state: 'up'}]) + .reply(200, [{command: 'bash', name: 'run.1', size: 'Eco', state: 'up', type: 'run', updated_at: hourAgo}]) const ecoExpression = heredoc` === run: one-off processes (1) @@ -557,7 +591,7 @@ describe('ps', function () { dynos.done() - expect(stdout.output).to.equal('No dynos on myapp\n') + expect(stdout.output).to.equal('No dynos on ⬢ myapp\n') expect(stderr.output).to.equal('') }) }) diff --git a/packages/cli/test/unit/commands/ps/scale.unit.test.ts b/packages/cli/test/unit/commands/ps/scale.unit.test.ts index 81b7bb042a..8e443629ad 100644 --- a/packages/cli/test/unit/commands/ps/scale.unit.test.ts +++ b/packages/cli/test/unit/commands/ps/scale.unit.test.ts @@ -1,19 +1,27 @@ -import {stdout, stderr} from 'stdout-stderr' -import Cmd from '../../../../src/commands/ps/scale.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 stripAnsi from 'strip-ansi' +import Cmd from '../../../../src/commands/ps/scale.js' +import runCommand from '../../../helpers/runCommand.js' + describe('ps:scale', function () { + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + afterEach(function () { - return nock.cleanAll() + api.done() + nock.cleanAll() }) it('shows formation with no args', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/myapp/formation') - .reply(200, [{type: 'web', quantity: 1, size: 'Free'}, {type: 'worker', quantity: 2, size: 'Free'}]) + .reply(200, [{quantity: 1, size: 'Free', type: 'web'}, {quantity: 2, size: 'Free', type: 'worker'}]) .get('/apps/myapp') .reply(200, {name: 'myapp'}) @@ -24,13 +32,12 @@ describe('ps:scale', function () { expect(stdout.output).to.equal('web=1:Free worker=2:Free\n') expect(stderr.output).to.equal('') - api.done() }) it('shows formation with shield dynos for apps in a shielded private space', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/myapp/formation') - .reply(200, [{type: 'web', quantity: 1, size: 'Private-L'}, {type: 'worker', quantity: 2, size: 'Private-M'}]) + .reply(200, [{quantity: 1, size: 'Private-L', type: 'web'}, {quantity: 2, size: 'Private-M', type: 'worker'}]) .get('/apps/myapp') .reply(200, {name: 'myapp', space: {shield: true}}) @@ -41,11 +48,10 @@ describe('ps:scale', function () { expect(stdout.output).to.equal('web=1:Shield-L worker=2:Shield-M\n') expect(stderr.output).to.equal('') - api.done() }) it('errors with no process types', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/myapp/formation') .reply(200, []) .get('/apps/myapp') @@ -57,18 +63,17 @@ describe('ps:scale', function () { 'myapp', ]) } catch (error: any) { - expect(stripAnsi(error.message)).to.include('No process types on myapp.') + expect(stripAnsi(error.message)).to.include('No process types on ⬢ myapp.') } expect(stdout.output).to.equal('') expect(stderr.output).to.equal('') - api.done() }) it('scales web=1 worker=2', async function () { - const api = nock('https://api.heroku.com:443') - .patch('/apps/myapp/formation', {updates: [{type: 'web', quantity: '1'}, {type: 'worker', quantity: '2'}]}) - .reply(200, [{type: 'web', quantity: 1, size: 'Free'}, {type: 'worker', quantity: 2, size: 'Free'}]) + api + .patch('/apps/myapp/formation', {updates: [{quantity: '1', type: 'web'}, {quantity: '2', type: 'worker'}]}) + .reply(200, [{quantity: 1, size: 'Free', type: 'web'}, {quantity: 2, size: 'Free', type: 'worker'}]) .get('/apps/myapp') .reply(200, {name: 'myapp'}) @@ -81,13 +86,12 @@ describe('ps:scale', function () { expect(stdout.output).to.equal('') expect(stderr.output).to.contain('Scaling dynos... done, now running web at 1:Free, worker at 2:Free\n') - api.done() }) it('scales up a shield dyno if the app is in a shielded private space', async function () { - const api = nock('https://api.heroku.com:443') - .patch('/apps/myapp/formation', {updates: [{type: 'web', quantity: '1', size: 'Private-L'}]}) - .reply(200, [{type: 'web', quantity: 1, size: 'Private-L'}]) + api + .patch('/apps/myapp/formation', {updates: [{quantity: '1', size: 'Private-L', type: 'web'}]}) + .reply(200, [{quantity: 1, size: 'Private-L', type: 'web'}]) .get('/apps/myapp') .reply(200, {name: 'myapp', space: {shield: true}}) @@ -99,13 +103,12 @@ describe('ps:scale', function () { expect(stdout.output).to.equal('') expect(stderr.output).to.contain('Scaling dynos... done, now running web at 1:Shield-L\n') - api.done() }) it('scales web-1', async function () { - const api = nock('https://api.heroku.com:443') - .patch('/apps/myapp/formation', {updates: [{type: 'web', quantity: '+1'}]}) - .reply(200, [{type: 'web', quantity: 2, size: 'Free'}]) + api + .patch('/apps/myapp/formation', {updates: [{quantity: '+1', type: 'web'}]}) + .reply(200, [{quantity: 2, size: 'Free', type: 'web'}]) .get('/apps/myapp') .reply(200, {name: 'myapp'}) @@ -117,6 +120,5 @@ describe('ps:scale', function () { expect(stdout.output).to.equal('') expect(stderr.output).to.contain('Scaling dynos... done, now running web at 2:Free\n') - api.done() }) }) diff --git a/packages/cli/test/unit/commands/ps/type.unit.test.ts b/packages/cli/test/unit/commands/ps/type.unit.test.ts index b94798f440..f766539c5a 100644 --- a/packages/cli/test/unit/commands/ps/type.unit.test.ts +++ b/packages/cli/test/unit/commands/ps/type.unit.test.ts @@ -1,67 +1,73 @@ -import {stdout, stderr} from 'stdout-stderr' +import {expect} from 'chai' +import nock from 'nock' +import {stderr, stdout} from 'stdout-stderr' + import Cmd from '../../../../src/commands/ps/type.js' import runCommand from '../../../helpers/runCommand.js' -import nock from 'nock' -import {expect} from 'chai' import expectOutput from '../../../helpers/utils/expectOutput.js' import normalizeTableOutput from '../../../helpers/utils/normalizeTableOutput.js' -import tsheredoc from 'tsheredoc' -const heredoc = tsheredoc.default describe('ps:type', function () { - function app(args = {}) { - const base = {name: 'myapp'} - return Object.assign(base, args) - } + let api: nock.Scope beforeEach(function () { + api = nock('https://api.heroku.com') + }) + + afterEach(function () { + api.done() nock.cleanAll() }) + function app(args = {}) { + const base = {name: 'myapp'} + return Object.assign(base, args) + } + it('displays cost/hour and max cost/month for all individually-priced dyno sizes', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/myapp') .reply(200, app()) .get('/apps/myapp/formation') .reply(200, [ - {type: 'web', quantity: 1, size: 'Eco'}, - {type: 'web', quantity: 1, size: 'Basic'}, - {type: 'web', quantity: 1, size: 'Standard-1X'}, - {type: 'web', quantity: 1, size: 'Standard-2X'}, - {type: 'web', quantity: 1, size: 'Performance-M'}, - {type: 'web', quantity: 1, size: 'Performance-L'}, - {type: 'web', quantity: 1, size: 'Performance-L-RAM'}, - {type: 'web', quantity: 1, size: 'Performance-XL'}, - {type: 'web', quantity: 1, size: 'Performance-2XL'}, - {type: 'web', quantity: 1, size: 'Private-S'}, - {type: 'web', quantity: 1, size: 'Private-M'}, - {type: 'web', quantity: 1, size: 'Private-L'}, - {type: 'web', quantity: 1, size: 'Shield-M'}, - {type: 'web', quantity: 1, size: 'Shield-L'}, - {type: 'web', quantity: 1, size: 'Shield-S'}, - {type: 'web', quantity: 1, size: 'Private-Memory-L'}, - {type: 'web', quantity: 1, size: 'Private-Memory-XL'}, - {type: 'web', quantity: 1, size: 'Private-Memory-2XL'}, - {type: 'web', quantity: 1, size: 'Shield-Memory-L'}, - {type: 'web', quantity: 1, size: 'Shield-Memory-XL'}, - {type: 'web', quantity: 1, size: 'Shield-Memory-2XL'}, - {type: 'web', quantity: 1, size: 'dyno-1c-0.5gb'}, - {type: 'web', quantity: 1, size: 'dyno-2c-1gb'}, - {type: 'web', quantity: 1, size: 'dyno-1c-4gb'}, - {type: 'web', quantity: 1, size: 'dyno-2c-8gb'}, - {type: 'web', quantity: 1, size: 'dyno-4c-16gb'}, - {type: 'web', quantity: 1, size: 'dyno-8c-32gb'}, - {type: 'web', quantity: 1, size: 'dyno-16c-64gb'}, - {type: 'web', quantity: 1, size: 'dyno-2c-4gb'}, - {type: 'web', quantity: 1, size: 'dyno-4c-8gb'}, - {type: 'web', quantity: 1, size: 'dyno-8c-16gb'}, - {type: 'web', quantity: 1, size: 'dyno-16c-32gb'}, - {type: 'web', quantity: 1, size: 'dyno-32c-64gb'}, - {type: 'web', quantity: 1, size: 'dyno-1c-8gb'}, - {type: 'web', quantity: 1, size: 'dyno-2c-16gb'}, - {type: 'web', quantity: 1, size: 'dyno-4c-32gb'}, - {type: 'web', quantity: 1, size: 'dyno-8c-64gb'}, - {type: 'web', quantity: 1, size: 'dyno-16c-128gb'}, + {quantity: 1, size: 'Eco', type: 'web'}, + {quantity: 1, size: 'Basic', type: 'web'}, + {quantity: 1, size: 'Standard-1X', type: 'web'}, + {quantity: 1, size: 'Standard-2X', type: 'web'}, + {quantity: 1, size: 'Performance-M', type: 'web'}, + {quantity: 1, size: 'Performance-L', type: 'web'}, + {quantity: 1, size: 'Performance-L-RAM', type: 'web'}, + {quantity: 1, size: 'Performance-XL', type: 'web'}, + {quantity: 1, size: 'Performance-2XL', type: 'web'}, + {quantity: 1, size: 'Private-S', type: 'web'}, + {quantity: 1, size: 'Private-M', type: 'web'}, + {quantity: 1, size: 'Private-L', type: 'web'}, + {quantity: 1, size: 'Shield-M', type: 'web'}, + {quantity: 1, size: 'Shield-L', type: 'web'}, + {quantity: 1, size: 'Shield-S', type: 'web'}, + {quantity: 1, size: 'Private-Memory-L', type: 'web'}, + {quantity: 1, size: 'Private-Memory-XL', type: 'web'}, + {quantity: 1, size: 'Private-Memory-2XL', type: 'web'}, + {quantity: 1, size: 'Shield-Memory-L', type: 'web'}, + {quantity: 1, size: 'Shield-Memory-XL', type: 'web'}, + {quantity: 1, size: 'Shield-Memory-2XL', type: 'web'}, + {quantity: 1, size: 'dyno-1c-0.5gb', type: 'web'}, + {quantity: 1, size: 'dyno-2c-1gb', type: 'web'}, + {quantity: 1, size: 'dyno-1c-4gb', type: 'web'}, + {quantity: 1, size: 'dyno-2c-8gb', type: 'web'}, + {quantity: 1, size: 'dyno-4c-16gb', type: 'web'}, + {quantity: 1, size: 'dyno-8c-32gb', type: 'web'}, + {quantity: 1, size: 'dyno-16c-64gb', type: 'web'}, + {quantity: 1, size: 'dyno-2c-4gb', type: 'web'}, + {quantity: 1, size: 'dyno-4c-8gb', type: 'web'}, + {quantity: 1, size: 'dyno-8c-16gb', type: 'web'}, + {quantity: 1, size: 'dyno-16c-32gb', type: 'web'}, + {quantity: 1, size: 'dyno-32c-64gb', type: 'web'}, + {quantity: 1, size: 'dyno-1c-8gb', type: 'web'}, + {quantity: 1, size: 'dyno-2c-16gb', type: 'web'}, + {quantity: 1, size: 'dyno-4c-32gb', type: 'web'}, + {quantity: 1, size: 'dyno-8c-64gb', type: 'web'}, + {quantity: 1, size: 'dyno-16c-128gb', type: 'web'}, ]) await runCommand(Cmd, [ @@ -156,19 +162,18 @@ describe('ps:type', function () { dyno-16c-128gb 1 $5 (flat monthly fee, shared across all Eco dynos) `)) - api.done() }) it('switches to performance-l-ram dyno', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/myapp') .reply(200, app()) .get('/apps/myapp/formation') - .reply(200, [{type: 'web', quantity: 1, size: 'Eco'}]) - .patch('/apps/myapp/formation', {updates: [{type: 'web', size: 'performance-l-ram'}]}) - .reply(200, [{type: 'web', quantity: 1, size: 'Performance-L-RAM'}]) + .reply(200, [{quantity: 1, size: 'Eco', type: 'web'}]) + .patch('/apps/myapp/formation', {updates: [{size: 'performance-l-ram', type: 'web'}]}) + .reply(200, [{quantity: 1, size: 'Performance-L-RAM', type: 'web'}]) .get('/apps/myapp/formation') - .reply(200, [{type: 'web', quantity: 1, size: 'Performance-L-RAM'}]) + .reply(200, [{quantity: 1, size: 'Performance-L-RAM', type: 'web'}]) await runCommand(Cmd, [ '--app', @@ -176,8 +181,6 @@ describe('ps:type', function () { 'web=performance-l-ram', ]) - api.done() - expect(normalizeTableOutput(stdout.output)).to.eq(normalizeTableOutput(` === Process Types type size qty cost/hour max cost/month @@ -189,19 +192,19 @@ describe('ps:type', function () { ─────────────────────────── Performance-L-RAM 1 `)) - expectOutput(stderr.output, 'Scaling dynos on myapp... done') + expectOutput(stderr.output, 'Scaling dynos on ⬢ myapp... done') }) it('switches to hobby dynos', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/myapp') .reply(200, app()) .get('/apps/myapp/formation') - .reply(200, [{type: 'web', quantity: 1, size: 'Eco'}, {type: 'worker', quantity: 2, size: 'Eco'}]) - .patch('/apps/myapp/formation', {updates: [{type: 'web', size: 'basic'}, {type: 'worker', size: 'basic'}]}) - .reply(200, [{type: 'web', quantity: 1, size: 'Basic'}, {type: 'worker', quantity: 2, size: 'Basic'}]) + .reply(200, [{quantity: 1, size: 'Eco', type: 'web'}, {quantity: 2, size: 'Eco', type: 'worker'}]) + .patch('/apps/myapp/formation', {updates: [{size: 'basic', type: 'web'}, {size: 'basic', type: 'worker'}]}) + .reply(200, [{quantity: 1, size: 'Basic', type: 'web'}, {quantity: 2, size: 'Basic', type: 'worker'}]) .get('/apps/myapp/formation') - .reply(200, [{type: 'web', quantity: 1, size: 'Basic'}, {type: 'worker', quantity: 2, size: 'Basic'}]) + .reply(200, [{quantity: 1, size: 'Basic', type: 'web'}, {quantity: 2, size: 'Basic', type: 'worker'}]) await runCommand(Cmd, [ '--app', @@ -221,20 +224,19 @@ describe('ps:type', function () { ─────────────── Basic 3 `)) - expect(stderr.output).to.include('Scaling dynos on myapp... done\n') - api.done() + expect(stderr.output).to.include('Scaling dynos on ⬢ myapp... done\n') }) it('switches to standard-1x and standard-2x dynos', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/myapp') .reply(200, app()) .get('/apps/myapp/formation') - .reply(200, [{type: 'web', quantity: 1, size: 'Eco'}, {type: 'worker', quantity: 2, size: 'Eco'}]) - .patch('/apps/myapp/formation', {updates: [{type: 'web', size: 'standard-1x'}, {type: 'worker', size: 'standard-2x'}]}) - .reply(200, [{type: 'web', quantity: 1, size: 'Standard-1X'}, {type: 'worker', quantity: 2, size: 'Standard-2X'}]) + .reply(200, [{quantity: 1, size: 'Eco', type: 'web'}, {quantity: 2, size: 'Eco', type: 'worker'}]) + .patch('/apps/myapp/formation', {updates: [{size: 'standard-1x', type: 'web'}, {size: 'standard-2x', type: 'worker'}]}) + .reply(200, [{quantity: 1, size: 'Standard-1X', type: 'web'}, {quantity: 2, size: 'Standard-2X', type: 'worker'}]) .get('/apps/myapp/formation') - .reply(200, [{type: 'web', quantity: 1, size: 'Standard-1X'}, {type: 'worker', quantity: 2, size: 'Standard-2X'}]) + .reply(200, [{quantity: 1, size: 'Standard-1X', type: 'web'}, {quantity: 2, size: 'Standard-2X', type: 'worker'}]) await runCommand(Cmd, [ '--app', @@ -256,16 +258,15 @@ describe('ps:type', function () { Standard-1X 1 Standard-2X 2 `)) - expect(stderr.output).to.include('Scaling dynos on myapp... done\n') - api.done() + expect(stderr.output).to.include('Scaling dynos on ⬢ myapp... done\n') }) it('displays Shield dynos for apps in shielded spaces', async function () { - const api = nock('https://api.heroku.com') + api .get('/apps/myapp') .reply(200, app({space: {shield: true}})) .get('/apps/myapp/formation') - .reply(200, [{type: 'web', quantity: 0, size: 'Private-M'}, {type: 'web', quantity: 0, size: 'Private-L'}]) + .reply(200, [{quantity: 0, size: 'Private-M', type: 'web'}, {quantity: 0, size: 'Private-L', type: 'web'}]) await runCommand(Cmd, [ '--app', @@ -285,6 +286,5 @@ describe('ps:type', function () { Shield-M 0 Shield-L 0 `)) - api.done() }) }) diff --git a/packages/cli/test/unit/commands/ps/wait.unit.test.ts b/packages/cli/test/unit/commands/ps/wait.unit.test.ts index fb54615ed8..874be2f535 100644 --- a/packages/cli/test/unit/commands/ps/wait.unit.test.ts +++ b/packages/cli/test/unit/commands/ps/wait.unit.test.ts @@ -34,7 +34,7 @@ describe('heroku ps:wait', function () { const {stderr} = await runCommand(['ps:wait', '--app', APP_NAME]) - expect(stderr).to.include(`Warning: App ${APP_NAME} has no releases`) + expect(stderr).to.include(`Warning: App ⬢ ${APP_NAME} has no releases`) }) it('exits with no output if app is already on the latest release', async function () { diff --git a/packages/cli/test/unit/commands/releases/index.unit.test.ts b/packages/cli/test/unit/commands/releases/index.unit.test.ts index ffa534ef15..c6fa5b1edf 100644 --- a/packages/cli/test/unit/commands/releases/index.unit.test.ts +++ b/packages/cli/test/unit/commands/releases/index.unit.test.ts @@ -9,6 +9,7 @@ import removeAllWhitespace from '../../../helpers/utils/remove-whitespaces.js' describe('releases', function () { let originalColumns: number | undefined let originalIsTTY: boolean | undefined + let api: nock.Scope before(function () { process.env.TZ = 'UTC' // Use UTC time always @@ -17,6 +18,7 @@ describe('releases', function () { beforeEach(function () { originalColumns = process.stdout.columns originalIsTTY = process.stdout.isTTY + api = nock('https://api.heroku.com') }) afterEach(function () { @@ -32,6 +34,8 @@ describe('releases', function () { } else { process.stdout.columns = originalColumns } + + api.done() }) const releases = [ @@ -123,7 +127,7 @@ describe('releases', function () { it('shows releases', async function () { process.stdout.isTTY = true process.stdout.columns = 80 - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, releases) @@ -133,19 +137,18 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases - Current: v37')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases - Current: v37')) expect(actual).to.include(removeAllWhitespace('v description user created_at')) expect(actual).to.include(removeAllWhitespace('v41')) expect(actual).to.include(removeAllWhitespace('releas…')) expect(actual).to.include(removeAllWhitespace('v40 Set foo co… rdagg@heroku.com')) expect(actual).to.include(removeAllWhitespace('v37 first comm… rdagg@heroku.com')) - api.done() }) it('shows successful releases', async function () { process.stdout.isTTY = true process.stdout.columns = 80 - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, onlySuccessfulReleases) @@ -155,18 +158,17 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases - Current: v37')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases - Current: v37')) expect(actual).to.include(removeAllWhitespace('v description user created_at')) expect(actual).to.include(removeAllWhitespace('v41 third comm… rdagg@heroku.com')) expect(actual).to.include(removeAllWhitespace('v40 Set foo co… rdagg@heroku.com')) expect(actual).to.include(removeAllWhitespace('v37 first comm… rdagg@heroku.com')) - api.done() }) it('shows releases in wider terminal', async function () { process.stdout.isTTY = true process.stdout.columns = 100 - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, releases) @@ -176,20 +178,19 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases - Current: v37')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases - Current: v37')) expect(actual).to.include(removeAllWhitespace('v description user created_at')) // cspell:ignore releas expect(actual).to.include(removeAllWhitespace('v41')) expect(actual).to.include(removeAllWhitespace('releas…')) expect(actual).to.include(removeAllWhitespace('v40 Set foo config vars rdagg@heroku.com')) expect(actual).to.include(removeAllWhitespace('v37 first commit rdagg@heroku.com')) - api.done() }) it('shows successful releases in wider terminal', async function () { process.stdout.isTTY = true process.stdout.columns = 100 - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, onlySuccessfulReleases) @@ -199,18 +200,17 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases - Current: v37')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases - Current: v37')) expect(actual).to.include(removeAllWhitespace('v description user created_at')) expect(actual).to.include(removeAllWhitespace('v41 third commit rdagg@heroku.com')) expect(actual).to.include(removeAllWhitespace('v40 Set foo config vars rdagg@heroku.com')) expect(actual).to.include(removeAllWhitespace('v37 first commit rdagg@heroku.com')) - api.done() }) it('shows releases in narrow terminal', async function () { process.stdout.isTTY = true process.stdout.columns = 65 - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, releases) @@ -220,20 +220,19 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases - Current: v37')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases - Current: v37')) expect(actual).to.include(removeAllWhitespace('v')) expect(actual).to.include(removeAllWhitespace('description')) expect(actual).to.include(removeAllWhitespace('v41')) expect(actual).to.include(removeAllWhitespace('v40')) expect(actual).to.include(removeAllWhitespace('v37')) expect(actual).to.include(removeAllWhitespace('rdagg')) - api.done() }) it('shows pending releases without release phase', async function () { process.stdout.isTTY = true process.stdout.columns = 80 - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, releases) @@ -243,20 +242,19 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases - Current: v37')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases - Current: v37')) expect(actual).to.include(removeAllWhitespace('v')) expect(actual).to.include(removeAllWhitespace('description')) expect(actual).to.include(removeAllWhitespace('v41')) expect(actual).to.include(removeAllWhitespace('v40')) expect(actual).to.include(removeAllWhitespace('v37')) expect(actual).to.include(removeAllWhitespace('rdagg@heroku.com')) - api.done() }) it('shows pending releases without a slug', async function () { process.stdout.isTTY = true process.stdout.columns = 80 - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, releasesNoSlug) @@ -266,16 +264,15 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases')) expect(actual).to.include(removeAllWhitespace('v')) expect(actual).to.include(removeAllWhitespace('description')) expect(actual).to.include(removeAllWhitespace('v1')) expect(actual).to.include(removeAllWhitespace('rdagg@heroku.com')) - api.done() }) it('shows releases as json', async function () { - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, releases) @@ -287,11 +284,10 @@ describe('releases', function () { expect(JSON.parse(stdout.output)[0]).to.have.nested.include({version: 41}) // stderr may contain warnings from other plugins in test environment - api.done() }) it('shows message if no releases', async function () { - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, []) @@ -300,13 +296,12 @@ describe('releases', function () { 'myapp', ]) - expect(stdout.output).to.equal('myapp has no releases.\n') + expect(stdout.output).to.equal('⬢ myapp has no releases.\n') // stderr may contain warnings from other plugins in test environment - api.done() }) it('shows extended info', async function () { - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases?extended=true') .reply(200, extended) @@ -317,7 +312,7 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases')) expect(actual).to.include(removeAllWhitespace('v')) expect(actual).to.include(removeAllWhitespace('description')) expect(actual).to.include(removeAllWhitespace('slug_id')) @@ -327,13 +322,12 @@ describe('releases', function () { expect(actual).to.include(removeAllWhitespace('1')) expect(actual).to.include(removeAllWhitespace('uuid')) // stderr may contain warnings from other plugins in test environment - api.done() }) it('shows extended info in wider terminal', async function () { process.stdout.isTTY = true process.stdout.columns = 100 - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases?extended=true') .reply(200, extended) @@ -344,7 +338,7 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases')) expect(actual).to.include(removeAllWhitespace('v')) expect(actual).to.include(removeAllWhitespace('description')) expect(actual).to.include(removeAllWhitespace('slug_id')) @@ -355,7 +349,6 @@ describe('releases', function () { expect(actual).to.include(removeAllWhitespace('1')) expect(actual).to.include(removeAllWhitespace('uuid')) // stderr may contain warnings from other plugins in test environment - api.done() }) it('shows no current release', async function () { @@ -364,7 +357,7 @@ describe('releases', function () { // Create a copy to avoid mutating the shared releases array const releasesCopy = releases.map(r => ({...r})) releasesCopy.at(-1)!.current = false - const api = nock('https://api.heroku.com:443') + api .get('/apps/myapp/releases') .reply(200, releasesCopy) @@ -374,13 +367,12 @@ describe('releases', function () { ]) const actual = removeAllWhitespace(stdout.output) - expect(actual).to.include(removeAllWhitespace('=== myapp Releases')) + expect(actual).to.include(removeAllWhitespace('=== ⬢ myapp Releases')) expect(actual).to.include(removeAllWhitespace('v')) expect(actual).to.include(removeAllWhitespace('description')) expect(actual).to.include(removeAllWhitespace('v41')) expect(actual).to.include(removeAllWhitespace('v40')) expect(actual).to.include(removeAllWhitespace('v37')) expect(actual).to.include(removeAllWhitespace('rdagg@heroku.com')) - api.done() }) }) diff --git a/packages/cli/test/unit/commands/releases/retry.unit.test.ts b/packages/cli/test/unit/commands/releases/retry.unit.test.ts index 5abd64d520..7de67befe6 100644 --- a/packages/cli/test/unit/commands/releases/retry.unit.test.ts +++ b/packages/cli/test/unit/commands/releases/retry.unit.test.ts @@ -1,3 +1,4 @@ +import ansis from 'ansis' import {expect} from 'chai' import nock from 'nock' import {stderr, stdout} from 'stdout-stderr' @@ -6,8 +7,15 @@ import Cmd from '../../../../src/commands/releases/retry.js' import runCommand from '../../../helpers/runCommand.js' describe('releases:retry', function () { + let api: nock.Scope + + beforeEach(function () { + api = nock('https://api.heroku.com') + }) + afterEach(function () { - return nock.cleanAll() + api.done() + nock.cleanAll() }) const release = [{ @@ -35,7 +43,7 @@ describe('releases:retry', function () { } it('errors when there are no releases yet', async function () { - nock('https://api.heroku.com') + api .get('/apps/myapp/releases') .reply(200, []) .get('/apps/myapp/formation') @@ -45,12 +53,12 @@ describe('releases:retry', function () { '--app', 'myapp', ]).catch((error: Error) => { - expect(error.message).to.eq('No release found for this app.') + expect(ansis.strip(error.message)).to.eq('No release found for ⬢ myapp.') }) }) it('retries the release', async function () { - nock('https://api.heroku.com') + api .get('/apps/myapp/releases') .reply(200, release) .get('/apps/myapp/formation') @@ -69,7 +77,7 @@ describe('releases:retry', function () { .get('/streams/release.log') .reply(200, 'Release Output Content') - const api = nock('https://api.heroku.com') + api .get('/apps/myapp/releases') .reply(200, release) .get('/apps/myapp/formation') @@ -82,7 +90,6 @@ describe('releases:retry', function () { 'myapp', ]) - api.done() busl.done() expect(stderr.output).to.contain('Retrying v40 on') expect(stderr.output).to.contain('myapp') @@ -90,7 +97,7 @@ describe('releases:retry', function () { }) it('errors if app does not use release-phase', async function () { - nock('https://api.heroku.com') + api .get('/apps/myapp/releases') .reply(200, release) .get('/apps/myapp/formation')