heroku 11.5.0-beta.0 → 11.6.0-beta.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +68 -1
- package/dist/commands/accounts/add.js +9 -11
- package/dist/commands/accounts/current.js +3 -2
- package/dist/commands/accounts/index.js +5 -4
- package/dist/commands/accounts/remove.js +8 -4
- package/dist/commands/accounts/set.js +7 -5
- package/dist/commands/apps/create.js +1 -0
- package/dist/commands/auth/login.js +9 -0
- package/dist/commands/auth/logout.js +15 -1
- package/dist/commands/auth/whoami.js +1 -1
- package/dist/commands/ci/config/unset.js +1 -1
- package/dist/commands/git/clone.js +1 -0
- package/dist/commands/git/credentials.d.ts +6 -0
- package/dist/commands/git/credentials.js +38 -3
- package/dist/commands/git/remote.js +1 -0
- package/dist/lib/accounts/accounts.d.ts +22 -8
- package/dist/lib/accounts/accounts.js +88 -26
- package/dist/lib/analytics-telemetry/backboard-herokulytics-client.d.ts +1 -5
- package/dist/lib/analytics-telemetry/backboard-herokulytics-client.js +5 -22
- package/dist/lib/git/git.d.ts +15 -2
- package/dist/lib/git/git.js +41 -5
- package/npm-shrinkwrap.json +312 -2102
- package/oclif.manifest.json +731 -731
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,74 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
6
|
|
|
7
|
-
## [11.
|
|
7
|
+
## [11.6.0-beta.0](https://github.com/heroku/cli/compare/v11.5.0...v11.6.0-beta.0) (2026-06-15)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
### Features
|
|
11
|
+
|
|
12
|
+
* add alias files for keychain accounts ([e1c572c](https://github.com/heroku/cli/commit/e1c572c942386b519b6ab63aac52c5ed56060d05))
|
|
13
|
+
* add getAliasEmail helper method ([bb53b87](https://github.com/heroku/cli/commit/bb53b87479d3009518c02420027da1dc7b8ddace))
|
|
14
|
+
* add listAliasFiles helper method ([01da5e4](https://github.com/heroku/cli/commit/01da5e44ae9c7257fddb27566fb3b3c02f7c8444))
|
|
15
|
+
* adds alias support for keychain-based accounts ([#3752](https://github.com/heroku/cli/issues/3752)) ([c5d1a45](https://github.com/heroku/cli/commit/c5d1a450601405b44b23648593b498bf61de2b20))
|
|
16
|
+
* adds credential manager functionality to the CLI ([#3758](https://github.com/heroku/cli/issues/3758)) ([ad0466e](https://github.com/heroku/cli/commit/ad0466e1512b902ee454fc55d4a5abdf87eaab12))
|
|
17
|
+
* implement heroku git:credentials as a git credential helper ([#3683](https://github.com/heroku/cli/issues/3683)) ([781e99e](https://github.com/heroku/cli/commit/781e99e1b9c56339b1245e30087aa9b312c10e69))
|
|
18
|
+
* prevent duplicate aliases for same email in accounts:add ([c094aa6](https://github.com/heroku/cli/commit/c094aa61670a0beaa15d3c8a31e0839f082c50d5))
|
|
19
|
+
* remove alias files for keychain and netrc accounts ([0d3b417](https://github.com/heroku/cli/commit/0d3b4175391b48ae84a712f9f86743a85ff2efbd))
|
|
20
|
+
* remove cached netrc account on logout ([#3710](https://github.com/heroku/cli/issues/3710)) ([b07137b](https://github.com/heroku/cli/commit/b07137bed5282a1618ae7e2b1e7f603eeb9a70be))
|
|
21
|
+
* update accounts and accounts:current commands to use the credential manager ([#3689](https://github.com/heroku/cli/issues/3689)) ([e753d06](https://github.com/heroku/cli/commit/e753d06407fe8ebf6f87b25131d6198d78e824e8))
|
|
22
|
+
* update accounts:add description for clarity ([a34d687](https://github.com/heroku/cli/commit/a34d687ecee13d4db831ef39011c9698d676bcb3))
|
|
23
|
+
* update accounts:add to use the credential manager ([#3699](https://github.com/heroku/cli/issues/3699)) ([49e3938](https://github.com/heroku/cli/commit/49e3938f44ec52451870cf24c692d440a7a69701))
|
|
24
|
+
* update accounts:remove to work with credential manager ([#3701](https://github.com/heroku/cli/issues/3701)) ([89d7b14](https://github.com/heroku/cli/commit/89d7b148df950429f902691c8348afc5bba07454))
|
|
25
|
+
* update accounts:set to work with keychain managers ([#3696](https://github.com/heroku/cli/issues/3696)) ([1f896aa](https://github.com/heroku/cli/commit/1f896aab4b23fb312d444420a06ed9d839ec8cf4))
|
|
26
|
+
* update list() to merge keychain accounts with alias files ([940bb6f](https://github.com/heroku/cli/commit/940bb6f89326e26e176b199db8fe0defa141c29d))
|
|
27
|
+
* update remove() to resolve alias before keychain removal ([92e2fe1](https://github.com/heroku/cli/commit/92e2fe133ca2d52f4c0b744c0096be2196fb106b))
|
|
28
|
+
* update set() to handle aliased keychain accounts ([114d3a7](https://github.com/heroku/cli/commit/114d3a7740c7f1c10e1425c5d9c7613811182658))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
### Bug Fixes
|
|
32
|
+
|
|
33
|
+
* allow switching to netrc mode with keychain-created aliases ([386bd16](https://github.com/heroku/cli/commit/386bd163b31cb2899d7d17332e79b0e68d63a5c0))
|
|
34
|
+
* fix linting errors in accounts lib file ([a8368d6](https://github.com/heroku/cli/commit/a8368d696dc91552ffef901da286e003a3091596))
|
|
35
|
+
* fix whoami and update heroku-cli-command and heroku-cli-util ([#3719](https://github.com/heroku/cli/issues/3719)) ([7db768f](https://github.com/heroku/cli/commit/7db768f21f0dbe04d8d422f93919bc4f6f9156c5))
|
|
36
|
+
* improves consistency between keychain and netrc accounts actions ([#3762](https://github.com/heroku/cli/issues/3762)) ([76d24b8](https://github.com/heroku/cli/commit/76d24b88247091325a59d37d77683a512e6f1724))
|
|
37
|
+
* make getAliasEmail test cross-platform compatible ([0f5b958](https://github.com/heroku/cli/commit/0f5b958f4782cf2ec5b5cb53812bdd636cb436e9))
|
|
38
|
+
* prevent cross-mode account removal with helpful error messages ([efa00a3](https://github.com/heroku/cli/commit/efa00a3e473eb2bb9be5a96f0412dced0851ec89))
|
|
39
|
+
* prevent password from being written to keychain alias files ([4b0e92d](https://github.com/heroku/cli/commit/4b0e92d1045dc649c5b5d92b649fa9d8d613e267))
|
|
40
|
+
* remove support for non-aliased accounts from heroku accounts and accounts:current commands ([9509171](https://github.com/heroku/cli/commit/950917138576dfae869bf5ef2930d9a159e7808b))
|
|
41
|
+
* restore git configuration on logout ([#3697](https://github.com/heroku/cli/issues/3697)) ([5479fa5](https://github.com/heroku/cli/commit/5479fa53c75b152aece718999deee50b94b1f5ce))
|
|
42
|
+
* update accounts:remove to check for email or alias to validate current account ([969afab](https://github.com/heroku/cli/commit/969afabbfaa46bfdaf37cac7eab854440fbcd601))
|
|
43
|
+
* update error text for accounts commands ([a15d8bd](https://github.com/heroku/cli/commit/a15d8bd1f8b86b802b9c74efccf3de1136cc3483))
|
|
44
|
+
* update error text for accounts commands ([#3756](https://github.com/heroku/cli/issues/3756)) ([35350e8](https://github.com/heroku/cli/commit/35350e89b86080fd415ad0c4c20928695fbac42b))
|
|
45
|
+
* use HEROKU_CLI_CHANNEL in Direwolf test trigger ([#3765](https://github.com/heroku/cli/issues/3765)) ([62be38b](https://github.com/heroku/cli/commit/62be38b378e29a97246cc41c500d633fa8bf407c))
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
### Miscellaneous Chores
|
|
49
|
+
|
|
50
|
+
* fix linting errors ([7df4bf0](https://github.com/heroku/cli/commit/7df4bf076497d70c4c0ab3ae672598853f761247))
|
|
51
|
+
* merge in main to feature branch ([#3747](https://github.com/heroku/cli/issues/3747)) ([8c100fd](https://github.com/heroku/cli/commit/8c100fd4667a88e414a4c9379ff896792ef07498))
|
|
52
|
+
* merge main ([#3737](https://github.com/heroku/cli/issues/3737)) ([cb751e6](https://github.com/heroku/cli/commit/cb751e6a23d11baf44cd6111f7a1741e8477c06d))
|
|
53
|
+
* merge main ([#3757](https://github.com/heroku/cli/issues/3757)) ([07a78a3](https://github.com/heroku/cli/commit/07a78a3d8023bd56c9b2935d98ffad61925be803))
|
|
54
|
+
* update CLI analytics to use heroku credential manager ([#3685](https://github.com/heroku/cli/issues/3685)) ([5f86e4c](https://github.com/heroku/cli/commit/5f86e4cf9a6b8ca74d6ef0650abbae1f940fab7d))
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
### Code Refactoring
|
|
58
|
+
|
|
59
|
+
* add accountsDir() helper to reduce code duplication ([efff0f5](https://github.com/heroku/cli/commit/efff0f5acd9cd4554ba74eb96d611abd9f44c830))
|
|
60
|
+
* always write to netrc and remove non-aliased account support from accounts:set ([1a6fdb1](https://github.com/heroku/cli/commit/1a6fdb1fd70d37fb40bd44e8761b1868cc7df59f))
|
|
61
|
+
* clean up accounts commands and lib function ([b2619ac](https://github.com/heroku/cli/commit/b2619ac45d8ee85469851c90274105515c7c9f09))
|
|
62
|
+
* extract Ruby symbol conversion to helper method ([dae3901](https://github.com/heroku/cli/commit/dae390196a0c3654cc8be22126bb48bedb022ed0))
|
|
63
|
+
* remove cross-mode validation from accounts management ([aba7fa5](https://github.com/heroku/cli/commit/aba7fa5cb213ef6b54a078b1d28beafefe9b79b3))
|
|
64
|
+
* simplify account lookup in set command ([c540bec](https://github.com/heroku/cli/commit/c540bec1ab5829e683f5296baa0806b427439605))
|
|
65
|
+
* simplify accounts add to always write username and password ([5be6ed5](https://github.com/heroku/cli/commit/5be6ed5987146486e18bda5570d92ea461686b80))
|
|
66
|
+
* simplify add() method with writeAccountFile helper ([b15db3a](https://github.com/heroku/cli/commit/b15db3ae0abf1ad92b20d14fb62c72039b1488c2))
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
### Tests
|
|
70
|
+
|
|
71
|
+
* fix accounts, apps, auth, buildpacks, container, git, and ps-exec tests ([391b6a6](https://github.com/heroku/cli/commit/391b6a6cff1cb754697db642f1c11464a8de6de3))
|
|
72
|
+
* update analytics tests to use the credential manager ([#3688](https://github.com/heroku/cli/issues/3688)) ([4fde394](https://github.com/heroku/cli/commit/4fde394ac2351504a727829fd86fd647136afbc8))
|
|
73
|
+
|
|
74
|
+
## [11.5.0](https://github.com/heroku/cli/compare/v11.4.0...v11.5.0) (2026-06-11)
|
|
8
75
|
|
|
9
76
|
|
|
10
77
|
### Features
|
|
@@ -4,26 +4,24 @@ import { Args, ux } from '@oclif/core';
|
|
|
4
4
|
import AccountsModule from '../../lib/accounts/accounts.js';
|
|
5
5
|
export default class Add extends Command {
|
|
6
6
|
static args = {
|
|
7
|
-
name: Args.string({ description: '
|
|
7
|
+
name: Args.string({ description: 'alias for Heroku account to add', required: true }),
|
|
8
8
|
};
|
|
9
|
-
static description = 'add
|
|
9
|
+
static description = 'add the current Heroku account to your accounts cache';
|
|
10
10
|
static example = `${color.command('heroku accounts:add my-account')}`;
|
|
11
11
|
async run() {
|
|
12
12
|
const { args } = await this.parse(Add);
|
|
13
13
|
const { name } = args;
|
|
14
|
-
const
|
|
15
|
-
if (
|
|
14
|
+
const accounts = await AccountsModule.list();
|
|
15
|
+
if (accounts.some(account => account.name === name)) {
|
|
16
16
|
ux.error(`${name} already exists`);
|
|
17
17
|
}
|
|
18
18
|
const { body: account } = await this.heroku.get('/account');
|
|
19
|
-
const email = account.email
|
|
20
|
-
const
|
|
21
|
-
if (
|
|
22
|
-
ux.error(
|
|
23
|
-
}
|
|
24
|
-
if (email === '') {
|
|
25
|
-
ux.error(logInMessage);
|
|
19
|
+
const email = account.email;
|
|
20
|
+
const existingAlias = accounts.find(account => account.name && account.username === email);
|
|
21
|
+
if (existingAlias) {
|
|
22
|
+
ux.error(`Account ${email} already has an alias of ${existingAlias.name}.`);
|
|
26
23
|
}
|
|
24
|
+
const token = this.heroku.auth;
|
|
27
25
|
AccountsModule.add(name, email, token);
|
|
28
26
|
}
|
|
29
27
|
}
|
|
@@ -8,12 +8,13 @@ export default class Current extends Command {
|
|
|
8
8
|
static example = `${color.command('heroku accounts:current')}`;
|
|
9
9
|
static promptFlagActive = false;
|
|
10
10
|
async run() {
|
|
11
|
-
const accountName = await AccountsModule.current();
|
|
11
|
+
const accountName = await AccountsModule.current(this.heroku);
|
|
12
12
|
if (accountName) {
|
|
13
13
|
hux.styledHeader(`Current account is ${color.user(accountName)}`);
|
|
14
14
|
}
|
|
15
15
|
else {
|
|
16
|
-
ux.error(`You haven't set an account
|
|
16
|
+
ux.error(`You haven't set an account.\n
|
|
17
|
+
Run ${color.code('heroku accounts:add <account-name>')} to add an account to your cache or ${color.code('heroku accounts:set <account-name>')} to set the current account.`);
|
|
17
18
|
}
|
|
18
19
|
}
|
|
19
20
|
}
|
|
@@ -8,16 +8,17 @@ export default class AccountsIndex extends Command {
|
|
|
8
8
|
static example = `${color.command('heroku accounts')}`;
|
|
9
9
|
static promptFlagActive = false;
|
|
10
10
|
async run() {
|
|
11
|
-
const accounts = accountsModule.list();
|
|
11
|
+
const accounts = await accountsModule.list();
|
|
12
12
|
if (accounts.length === 0) {
|
|
13
13
|
ux.error('You don\'t have any accounts in your cache.');
|
|
14
14
|
}
|
|
15
|
+
const current = await accountsModule.current(this.heroku);
|
|
15
16
|
for (const account of accounts) {
|
|
16
|
-
if (account.name ===
|
|
17
|
-
ux.stdout(`* ${account.name}`);
|
|
17
|
+
if (account.name === current || account.username === current) {
|
|
18
|
+
ux.stdout(`* ${account.name ?? account.username}`);
|
|
18
19
|
}
|
|
19
20
|
else {
|
|
20
|
-
ux.stdout(` ${account.name}`);
|
|
21
|
+
ux.stdout(` ${account.name ?? account.username}`);
|
|
21
22
|
}
|
|
22
23
|
}
|
|
23
24
|
}
|
|
@@ -11,12 +11,16 @@ export default class Remove extends Command {
|
|
|
11
11
|
async run() {
|
|
12
12
|
const { args } = await this.parse(Remove);
|
|
13
13
|
const { name } = args;
|
|
14
|
-
|
|
14
|
+
const accounts = await AccountsModule.list();
|
|
15
|
+
const account = accounts.find(a => a.name === name || a.username === name);
|
|
16
|
+
if (!account) {
|
|
15
17
|
ux.error(`${name} doesn't exist in your accounts cache.`);
|
|
16
18
|
}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
+
const currentAccount = await AccountsModule.current(this.heroku);
|
|
20
|
+
// Check both alias (name) and email (username) against current account
|
|
21
|
+
if (currentAccount === name || currentAccount === account.username) {
|
|
22
|
+
ux.error(`${name} is the current account. To log out, run ${color.command('heroku logout')}.`);
|
|
19
23
|
}
|
|
20
|
-
AccountsModule.remove(name);
|
|
24
|
+
await AccountsModule.remove(name);
|
|
21
25
|
}
|
|
22
26
|
}
|
|
@@ -4,16 +4,18 @@ import { Args, ux } from '@oclif/core';
|
|
|
4
4
|
import AccountsModule from '../../lib/accounts/accounts.js';
|
|
5
5
|
export default class Set extends Command {
|
|
6
6
|
static args = {
|
|
7
|
-
name: Args.string({ description: 'name of account to set', required: true }),
|
|
7
|
+
name: Args.string({ description: 'name or username of account to set', required: true }),
|
|
8
8
|
};
|
|
9
|
-
static description = 'set the current Heroku account from your cache';
|
|
9
|
+
static description = 'set the current Heroku account from your accounts cache';
|
|
10
10
|
static example = `${color.command('heroku accounts:set my-account')}`;
|
|
11
11
|
async run() {
|
|
12
12
|
const { args } = await this.parse(Set);
|
|
13
13
|
const { name } = args;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
const accounts = await AccountsModule.list();
|
|
15
|
+
const account = accounts.find(account => account.name === name || account.username === name);
|
|
16
|
+
if (!account) {
|
|
17
|
+
ux.error(`${name} doesn't exist in your accounts cache.`);
|
|
16
18
|
}
|
|
17
|
-
AccountsModule.set(
|
|
19
|
+
await AccountsModule.set(account, this.config.dataDir);
|
|
18
20
|
}
|
|
19
21
|
}
|
|
@@ -69,6 +69,7 @@ async function configureGitRemote(context, app) {
|
|
|
69
69
|
const remoteUrl = git.httpGitUrl(app.name || '');
|
|
70
70
|
if (!context.flags['no-remote'] && git.inGitRepo()) {
|
|
71
71
|
await git.createRemote(context.flags.remote || 'heroku', remoteUrl);
|
|
72
|
+
await git.configureCredentialHelper();
|
|
72
73
|
}
|
|
73
74
|
return remoteUrl;
|
|
74
75
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Command, flags } from '@heroku-cli/command';
|
|
2
2
|
import * as color from '@heroku/heroku-cli-util/color';
|
|
3
|
+
import Git from '../../lib/git/git.js';
|
|
3
4
|
export default class Login extends Command {
|
|
4
5
|
static aliases = ['login'];
|
|
5
6
|
static description = 'login with your Heroku credentials';
|
|
@@ -17,6 +18,14 @@ export default class Login extends Command {
|
|
|
17
18
|
await this.heroku.login({ browser: flags.browser, expiresIn: flags['expires-in'], method });
|
|
18
19
|
const { body: account } = await this.heroku.get('/account', { retryAuth: false });
|
|
19
20
|
this.log(`Logged in as ${color.user(account.email)}`);
|
|
21
|
+
const git = new Git();
|
|
22
|
+
try {
|
|
23
|
+
await git.configureCredentialHelper();
|
|
24
|
+
await git.eraseCredentials();
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
// ignore
|
|
28
|
+
}
|
|
20
29
|
await this.config.runHook('recache', { type: 'login' });
|
|
21
30
|
}
|
|
22
31
|
}
|
|
@@ -1,14 +1,28 @@
|
|
|
1
1
|
import { Command } from '@heroku-cli/command';
|
|
2
2
|
import { ux } from '@oclif/core/ux';
|
|
3
|
+
import AccountsModule from '../../lib/accounts/accounts.js';
|
|
4
|
+
import Git from '../../lib/git/git.js';
|
|
3
5
|
export default class Logout extends Command {
|
|
4
6
|
static aliases = ['logout'];
|
|
5
7
|
static baseFlags = Command.baseFlagsWithoutPrompt();
|
|
6
8
|
static description = 'clears local login credentials and invalidates API session';
|
|
7
9
|
static promptFlagActive = false;
|
|
8
10
|
async run() {
|
|
9
|
-
this.parse(Logout);
|
|
11
|
+
await this.parse(Logout);
|
|
10
12
|
ux.action.start('Logging out');
|
|
13
|
+
const cachedNetrcAccount = await AccountsModule.currentNetrc();
|
|
11
14
|
await this.heroku.logout();
|
|
15
|
+
const git = new Git();
|
|
16
|
+
try {
|
|
17
|
+
await git.removeCredentialHelper();
|
|
18
|
+
await git.eraseCredentials();
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// ignore
|
|
22
|
+
}
|
|
23
|
+
if (cachedNetrcAccount) {
|
|
24
|
+
await AccountsModule.remove(cachedNetrcAccount);
|
|
25
|
+
}
|
|
12
26
|
await this.config.runHook('recache', { type: 'logout' });
|
|
13
27
|
ux.action.stop();
|
|
14
28
|
}
|
|
@@ -27,7 +27,7 @@ export default class CiConfigUnset extends Command {
|
|
|
27
27
|
vars[iAmStr] = null;
|
|
28
28
|
}
|
|
29
29
|
await ux.action.start(`Unsetting ${Object.keys(vars).join(', ')}`);
|
|
30
|
-
setPipelineConfigVars(this.heroku, pipeline.id, vars);
|
|
30
|
+
await setPipelineConfigVars(this.heroku, pipeline.id, vars);
|
|
31
31
|
ux.action.stop();
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -6,4 +6,10 @@ export declare class GitCredentials extends Command {
|
|
|
6
6
|
static description: string;
|
|
7
7
|
static hidden: boolean;
|
|
8
8
|
run(): Promise<void>;
|
|
9
|
+
/**
|
|
10
|
+
* Reads git-credential input from stdin
|
|
11
|
+
* Format: key=value pairs, one per line, terminated by blank line
|
|
12
|
+
* Returns parsed object with protocol, host, username, and path
|
|
13
|
+
*/
|
|
14
|
+
private readInput;
|
|
9
15
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { Command } from '@heroku-cli/command';
|
|
1
|
+
import { Command, vars } from '@heroku-cli/command';
|
|
2
2
|
import { Args, ux } from '@oclif/core';
|
|
3
|
+
import * as readline from 'node:readline';
|
|
3
4
|
export class GitCredentials extends Command {
|
|
4
5
|
static args = {
|
|
5
6
|
command: Args.string({ description: 'command name of the git credentials', required: true }),
|
|
@@ -16,10 +17,16 @@ export class GitCredentials extends Command {
|
|
|
16
17
|
break;
|
|
17
18
|
}
|
|
18
19
|
case 'get': {
|
|
19
|
-
|
|
20
|
+
const { host, protocol } = await this.readInput();
|
|
21
|
+
const { httpGitHost } = vars;
|
|
22
|
+
if (protocol !== 'https' || host !== httpGitHost) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!this.heroku.auth) {
|
|
20
26
|
throw new Error('not logged in');
|
|
27
|
+
}
|
|
21
28
|
ux.stdout(`protocol=https
|
|
22
|
-
host
|
|
29
|
+
host=${httpGitHost}
|
|
23
30
|
username=heroku
|
|
24
31
|
password=${this.heroku.auth}`);
|
|
25
32
|
break;
|
|
@@ -29,4 +36,32 @@ password=${this.heroku.auth}`);
|
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
38
|
}
|
|
39
|
+
/**
|
|
40
|
+
* Reads git-credential input from stdin
|
|
41
|
+
* Format: key=value pairs, one per line, terminated by blank line
|
|
42
|
+
* Returns parsed object with protocol, host, username, and path
|
|
43
|
+
*/
|
|
44
|
+
async readInput() {
|
|
45
|
+
return new Promise(resolve => {
|
|
46
|
+
const rl = readline.createInterface({
|
|
47
|
+
input: process.stdin,
|
|
48
|
+
terminal: false,
|
|
49
|
+
});
|
|
50
|
+
const input = {};
|
|
51
|
+
rl.on('line', (line) => {
|
|
52
|
+
if (!line.trim()) {
|
|
53
|
+
rl.close();
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const [key, value] = line.split('=', 2);
|
|
57
|
+
if (key && value) {
|
|
58
|
+
input[key] = value;
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
rl.on('close', () => {
|
|
62
|
+
process.stdin.pause();
|
|
63
|
+
resolve(input);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
32
67
|
}
|
|
@@ -34,5 +34,6 @@ ${color.command('heroku git:remote --remote heroku-staging -a example-staging')}
|
|
|
34
34
|
: git.exec(['remote', 'add', remote, url].concat(argv)));
|
|
35
35
|
const newRemote = await git.remoteUrl(remote);
|
|
36
36
|
this.log(`set git remote ${color.cyan(remote)} to ${color.cyan(newRemote)}`);
|
|
37
|
+
await git.configureCredentialHelper();
|
|
37
38
|
}
|
|
38
39
|
}
|
|
@@ -1,21 +1,35 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { APIClient, getStorageConfig } from '@heroku-cli/command';
|
|
2
|
+
export interface AccountEntry {
|
|
3
|
+
name?: string;
|
|
4
|
+
username: string;
|
|
5
|
+
}
|
|
2
6
|
export interface IAccountsWrapper {
|
|
3
7
|
add(name: string, username: string, password: string): void;
|
|
4
|
-
current(): Promise<null | string>;
|
|
5
|
-
|
|
8
|
+
current(heroku: APIClient): Promise<null | string>;
|
|
9
|
+
currentNetrc(): Promise<null | string>;
|
|
10
|
+
getStorageConfig(): ReturnType<typeof getStorageConfig>;
|
|
11
|
+
list(): AccountEntry[];
|
|
6
12
|
remove(name: string): void;
|
|
7
|
-
set(
|
|
13
|
+
set(account: AccountEntry, dataDir: string): Promise<void>;
|
|
14
|
+
writeLoginState(dataDir: string, name: string): Promise<void>;
|
|
8
15
|
}
|
|
9
16
|
export declare class AccountsWrapper implements IAccountsWrapper {
|
|
10
17
|
private netrc;
|
|
11
18
|
add(name: string, username: string, password: string): void;
|
|
12
|
-
current(): Promise<null | string>;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
19
|
+
current(heroku: APIClient): Promise<null | string>;
|
|
20
|
+
currentNetrc(): Promise<null | string>;
|
|
21
|
+
getStorageConfig(): import("@heroku-cli/command").StorageConfig;
|
|
22
|
+
list(): AccountEntry[];
|
|
23
|
+
remove(name: string): Promise<void>;
|
|
24
|
+
set(account: AccountEntry, dataDir: string): Promise<void>;
|
|
25
|
+
writeLoginState(dataDir: string, name: string): Promise<void>;
|
|
16
26
|
private account;
|
|
27
|
+
private accountsDir;
|
|
17
28
|
private configDir;
|
|
29
|
+
private convertRubySymbols;
|
|
30
|
+
private getAliasEmail;
|
|
18
31
|
private initNetrc;
|
|
32
|
+
private writeAccountFile;
|
|
19
33
|
}
|
|
20
34
|
declare const _default: AccountsWrapper;
|
|
21
35
|
export default _default;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { getStorageConfig, writeLoginState, } from '@heroku-cli/command';
|
|
2
|
+
import { removeAuth } from '@heroku-cli/command/lib/credential-manager.js';
|
|
1
3
|
import fs from 'node:fs';
|
|
2
4
|
import os from 'node:os';
|
|
3
5
|
import path from 'node:path';
|
|
@@ -5,14 +7,20 @@ import { parse, stringify } from 'yaml';
|
|
|
5
7
|
export class AccountsWrapper {
|
|
6
8
|
netrc;
|
|
7
9
|
add(name, username, password) {
|
|
8
|
-
|
|
9
|
-
fs.mkdirSync(basedir, { recursive: true });
|
|
10
|
-
fs.writeFileSync(path.join(basedir, name),
|
|
10
|
+
fs.mkdirSync(this.accountsDir(), { recursive: true });
|
|
11
11
|
// eslint-disable-next-line perfectionist/sort-objects
|
|
12
|
-
|
|
13
|
-
fs.chmodSync(path.join(basedir, name), 0o600);
|
|
12
|
+
this.writeAccountFile(name, { username, password });
|
|
14
13
|
}
|
|
15
|
-
async current() {
|
|
14
|
+
async current(heroku) {
|
|
15
|
+
const config = this.getStorageConfig();
|
|
16
|
+
if (config.credentialStore) {
|
|
17
|
+
const authEntry = await heroku.getAuthEntry();
|
|
18
|
+
const current = this.list().find(a => a.username === authEntry?.account);
|
|
19
|
+
return current && current.name ? current.name : null;
|
|
20
|
+
}
|
|
21
|
+
return this.currentNetrc();
|
|
22
|
+
}
|
|
23
|
+
async currentNetrc() {
|
|
16
24
|
const netrcInstance = await this.initNetrc();
|
|
17
25
|
if (netrcInstance.machines['api.heroku.com']) {
|
|
18
26
|
const current = this.list().find(a => a.username === netrcInstance.machines['api.heroku.com'].login);
|
|
@@ -20,40 +28,66 @@ export class AccountsWrapper {
|
|
|
20
28
|
}
|
|
21
29
|
return null;
|
|
22
30
|
}
|
|
31
|
+
getStorageConfig() {
|
|
32
|
+
return getStorageConfig();
|
|
33
|
+
}
|
|
23
34
|
list() {
|
|
24
|
-
const basedir =
|
|
35
|
+
const basedir = this.accountsDir();
|
|
25
36
|
try {
|
|
26
37
|
return fs.readdirSync(basedir)
|
|
27
|
-
.
|
|
38
|
+
.filter(name => this.account(name).username)
|
|
39
|
+
.map(name => ({ name, username: this.account(name).username }));
|
|
28
40
|
}
|
|
29
41
|
catch {
|
|
30
42
|
return [];
|
|
31
43
|
}
|
|
32
44
|
}
|
|
33
|
-
remove(name) {
|
|
34
|
-
const
|
|
35
|
-
|
|
45
|
+
async remove(name) {
|
|
46
|
+
const config = this.getStorageConfig();
|
|
47
|
+
if (config.credentialStore) {
|
|
48
|
+
// Keychain mode
|
|
49
|
+
const email = this.getAliasEmail(name);
|
|
50
|
+
await removeAuth(email, ['api.heroku.com', 'git.heroku.com']);
|
|
51
|
+
fs.unlinkSync(path.join(this.accountsDir(), name));
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
// Netrc mode
|
|
55
|
+
fs.unlinkSync(path.join(this.accountsDir(), name));
|
|
36
56
|
}
|
|
37
|
-
async set(
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
57
|
+
async set(account, dataDir) {
|
|
58
|
+
const config = this.getStorageConfig();
|
|
59
|
+
if (account.name) {
|
|
60
|
+
if (config.credentialStore) {
|
|
61
|
+
const email = this.getAliasEmail(account.name);
|
|
62
|
+
if (email) {
|
|
63
|
+
await this.writeLoginState(dataDir, email);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
const netrcInstance = await this.initNetrc();
|
|
67
|
+
let current;
|
|
68
|
+
try {
|
|
69
|
+
current = this.account(account.name);
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
throw new Error(`We can't find the alias file for ${account.name}.`);
|
|
73
|
+
}
|
|
74
|
+
netrcInstance.machines['git.heroku.com'] = { login: current.username, password: current.password || '' };
|
|
75
|
+
netrcInstance.machines['api.heroku.com'] = { login: current.username, password: current.password || '' };
|
|
76
|
+
await netrcInstance.save();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async writeLoginState(dataDir, name) {
|
|
80
|
+
return writeLoginState(dataDir, name);
|
|
43
81
|
}
|
|
44
82
|
account(name) {
|
|
45
|
-
const
|
|
46
|
-
const file = fs.readFileSync(path.join(basedir, name), 'utf8');
|
|
83
|
+
const file = fs.readFileSync(path.join(this.accountsDir(), name), 'utf8');
|
|
47
84
|
const account = parse(file);
|
|
48
|
-
|
|
49
|
-
// convert from ruby symbols
|
|
50
|
-
account.username = account[':username'];
|
|
51
|
-
account.password = account[':password'];
|
|
52
|
-
delete account[':username'];
|
|
53
|
-
delete account[':password'];
|
|
54
|
-
}
|
|
85
|
+
this.convertRubySymbols(account);
|
|
55
86
|
return account;
|
|
56
87
|
}
|
|
88
|
+
accountsDir() {
|
|
89
|
+
return path.join(this.configDir(), 'accounts');
|
|
90
|
+
}
|
|
57
91
|
configDir() {
|
|
58
92
|
const legacyDir = path.join(os.homedir(), '.heroku');
|
|
59
93
|
if (fs.existsSync(legacyDir)) {
|
|
@@ -61,6 +95,29 @@ export class AccountsWrapper {
|
|
|
61
95
|
}
|
|
62
96
|
return path.join(os.homedir(), '.config', 'heroku');
|
|
63
97
|
}
|
|
98
|
+
convertRubySymbols(account) {
|
|
99
|
+
if (account[':username']) {
|
|
100
|
+
account.username = account[':username'];
|
|
101
|
+
account.password = account[':password'];
|
|
102
|
+
delete account[':username'];
|
|
103
|
+
delete account[':password'];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
getAliasEmail(alias) {
|
|
107
|
+
try {
|
|
108
|
+
const filePath = path.join(this.accountsDir(), alias);
|
|
109
|
+
if (!fs.existsSync(filePath)) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
const file = fs.readFileSync(filePath, 'utf8');
|
|
113
|
+
const account = parse(file);
|
|
114
|
+
this.convertRubySymbols(account);
|
|
115
|
+
return account.username ?? undefined;
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return undefined;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
64
121
|
async initNetrc() {
|
|
65
122
|
if (!this.netrc) {
|
|
66
123
|
const NetrcModule = await import('netrc-parser');
|
|
@@ -70,6 +127,11 @@ export class AccountsWrapper {
|
|
|
70
127
|
}
|
|
71
128
|
return this.netrc;
|
|
72
129
|
}
|
|
130
|
+
writeAccountFile(name, content) {
|
|
131
|
+
const filePath = path.join(this.accountsDir(), name);
|
|
132
|
+
fs.writeFileSync(filePath, stringify(content), 'utf8');
|
|
133
|
+
fs.chmodSync(filePath, 0o600);
|
|
134
|
+
}
|
|
73
135
|
}
|
|
74
136
|
// Default export for convenience
|
|
75
137
|
export default new AccountsWrapper();
|
|
@@ -26,15 +26,11 @@ export default class BackboardHerokulyticsClient {
|
|
|
26
26
|
config: Interfaces.Config;
|
|
27
27
|
http: ReturnType<typeof HTTP.create>;
|
|
28
28
|
userConfig: HerokulyticsConfig;
|
|
29
|
+
private heroku;
|
|
29
30
|
private isInitialized;
|
|
30
|
-
private netrc;
|
|
31
31
|
constructor(config: Interfaces.Config);
|
|
32
32
|
get authorizationToken(): string | undefined;
|
|
33
|
-
get netrcLogin(): string | undefined;
|
|
34
|
-
get netrcToken(): string | undefined;
|
|
35
33
|
get url(): string;
|
|
36
|
-
get user(): string | undefined;
|
|
37
|
-
get usingHerokuAPIKey(): boolean;
|
|
38
34
|
_acAnalytics(id: string): Promise<number>;
|
|
39
35
|
send(opts: RecordOpts): Promise<void>;
|
|
40
36
|
private ensureInitialized;
|