heroku 11.4.1-beta.0 → 11.5.0-alpha.5

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 CHANGED
@@ -4,21 +4,42 @@ 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.4.1-beta.0](https://github.com/heroku/cli/compare/v11.4.0...v11.4.1-beta.0) (2026-05-27)
7
+ ## [11.5.0-alpha.5](https://github.com/heroku/cli/compare/v11.4.0...v11.5.0-alpha.5) (2026-06-02)
8
+
9
+
10
+ ### Features
11
+
12
+ * 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))
13
+ * remove cached netrc account on logout ([#3710](https://github.com/heroku/cli/issues/3710)) ([b07137b](https://github.com/heroku/cli/commit/b07137bed5282a1618ae7e2b1e7f603eeb9a70be))
14
+ * 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))
15
+ * update accounts:add to use the credential manager ([#3699](https://github.com/heroku/cli/issues/3699)) ([49e3938](https://github.com/heroku/cli/commit/49e3938f44ec52451870cf24c692d440a7a69701))
16
+ * update accounts:remove to work with credential manager ([#3701](https://github.com/heroku/cli/issues/3701)) ([89d7b14](https://github.com/heroku/cli/commit/89d7b148df950429f902691c8348afc5bba07454))
17
+ * update accounts:set to work with keychain managers ([#3696](https://github.com/heroku/cli/issues/3696)) ([1f896aa](https://github.com/heroku/cli/commit/1f896aab4b23fb312d444420a06ed9d839ec8cf4))
8
18
 
9
19
 
10
20
  ### Bug Fixes
11
21
 
12
22
  * 'run:inside' args ordering (W-22693654) ([#3727](https://github.com/heroku/cli/issues/3727)) ([355113e](https://github.com/heroku/cli/commit/355113e8253b8547ea86be4ef540287ea263af80))
13
23
  * add missing warning to 'data:pg:migrate' (W-22544849) ([#3716](https://github.com/heroku/cli/issues/3716)) ([400fc6e](https://github.com/heroku/cli/commit/400fc6e26eb7b7fdd634e0e4465fc5c19762303a))
24
+ * 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))
14
25
  * inherit secrets in reusable workflow ([#3711](https://github.com/heroku/cli/issues/3711)) ([5dd58af](https://github.com/heroku/cli/commit/5dd58af6c14f55ab85fe8584598718ebee03ae4b))
15
26
  * pass empty string to rl.write in repl finally block (W-22295448) ([#3721](https://github.com/heroku/cli/issues/3721)) ([ebdf082](https://github.com/heroku/cli/commit/ebdf082ed317cb74a59945c60dc274615b735b81))
27
+ * restore git configuration on logout ([#3697](https://github.com/heroku/cli/issues/3697)) ([5479fa5](https://github.com/heroku/cli/commit/5479fa53c75b152aece718999deee50b94b1f5ce))
16
28
 
17
29
 
18
30
  ### Miscellaneous Chores
19
31
 
20
32
  * add CLAUDE.md and Copilot instructions pointing to AGENTS.md ([#3724](https://github.com/heroku/cli/issues/3724)) ([8abed2f](https://github.com/heroku/cli/commit/8abed2faad739a1897734fe414cc2e1b70ea245a))
33
+ * fix linting errors ([7df4bf0](https://github.com/heroku/cli/commit/7df4bf076497d70c4c0ab3ae672598853f761247))
34
+ * merge main ([#3737](https://github.com/heroku/cli/issues/3737)) ([cb751e6](https://github.com/heroku/cli/commit/cb751e6a23d11baf44cd6111f7a1741e8477c06d))
21
35
  * remove unused workflow file ([#3712](https://github.com/heroku/cli/issues/3712)) ([8860aa4](https://github.com/heroku/cli/commit/8860aa412261f05fae3df19438cfd50a5f4ae67f))
36
+ * update CLI analytics to use heroku credential manager ([#3685](https://github.com/heroku/cli/issues/3685)) ([5f86e4c](https://github.com/heroku/cli/commit/5f86e4cf9a6b8ca74d6ef0650abbae1f940fab7d))
37
+
38
+
39
+ ### Tests
40
+
41
+ * fix accounts, apps, auth, buildpacks, container, git, and ps-exec tests ([391b6a6](https://github.com/heroku/cli/commit/391b6a6cff1cb754697db642f1c11464a8de6de3))
42
+ * update analytics tests to use the credential manager ([#3688](https://github.com/heroku/cli/issues/3688)) ([4fde394](https://github.com/heroku/cli/commit/4fde394ac2351504a727829fd86fd647136afbc8))
22
43
 
23
44
  ## [11.4.0](https://github.com/heroku/cli/compare/v11.3.0...v11.4.0) (2026-05-13)
24
45
 
@@ -11,19 +11,13 @@ export default class Add extends Command {
11
11
  async run() {
12
12
  const { args } = await this.parse(Add);
13
13
  const { name } = args;
14
- const logInMessage = 'You must be logged in to run this command.';
15
- if (AccountsModule.list().some(a => a.name === name)) {
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 token = this.heroku.auth || '';
21
- if (token === '') {
22
- ux.error(logInMessage);
23
- }
24
- if (email === '') {
25
- ux.error(logInMessage);
26
- }
19
+ const email = account.email;
20
+ const token = this.heroku.auth;
27
21
  AccountsModule.add(name, email, token);
28
22
  }
29
23
  }
@@ -8,7 +8,7 @@ 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
  }
@@ -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 === await accountsModule.current()) {
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,12 @@ export default class Remove extends Command {
11
11
  async run() {
12
12
  const { args } = await this.parse(Remove);
13
13
  const { name } = args;
14
- if (!AccountsModule.list().some(a => a.name === name)) {
14
+ if (!(await AccountsModule.list()).some(account => account.name === name || account.username === name)) {
15
15
  ux.error(`${name} doesn't exist in your accounts cache.`);
16
16
  }
17
- if (await AccountsModule.current() === name) {
18
- ux.error(`${name} is the current account.`);
17
+ if (await AccountsModule.current(this.heroku) === name) {
18
+ ux.error(`${name} is the current account. To log out, run ${color.command('heroku logout')}.`);
19
19
  }
20
- AccountsModule.remove(name);
20
+ await AccountsModule.remove(name);
21
21
  }
22
22
  }
@@ -4,16 +4,20 @@ 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 or system keychain';
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
- if (!AccountsModule.list().some(a => a.name === name)) {
15
- ux.error(`${name} does not exist in your accounts cache.`);
14
+ const accounts = await AccountsModule.list();
15
+ const netrcAccount = accounts.find(account => account.name === name);
16
+ const keychainAccount = accounts.find(account => !account.name && account.username === name);
17
+ if (!netrcAccount && !keychainAccount) {
18
+ ux.error(`${name} does not exist in your accounts cache or system keychain.`);
16
19
  }
17
- AccountsModule.set(name);
20
+ const account = netrcAccount ?? keychainAccount;
21
+ await AccountsModule.set(account, this.config.dataDir);
18
22
  }
19
23
  }
@@ -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
+ AccountsModule.removeNetrc(cachedNetrcAccount);
25
+ }
12
26
  await this.config.runHook('recache', { type: 'logout' });
13
27
  ux.action.stop();
14
28
  }
@@ -17,7 +17,7 @@ export default class AuthWhoami extends Command {
17
17
  this.log(account.email);
18
18
  }
19
19
  catch (error) {
20
- if (error.statusCode === 401)
20
+ if (error.http.statusCode === 401)
21
21
  this.notloggedin();
22
22
  throw error;
23
23
  }
@@ -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
  }
@@ -24,5 +24,6 @@ remote: Counting objects: 42, done.
24
24
  const directory = args.DIRECTORY || app.name;
25
25
  const remote = flags.remote || 'heroku';
26
26
  await git.spawn(['clone', '-o', remote, git.url(app.name), directory]);
27
+ await git.configureCredentialHelper();
27
28
  }
28
29
  }
@@ -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
- if (!this.heroku.auth)
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=git.heroku.com
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,18 +1,32 @@
1
- import * as Heroku from '@heroku-cli/schema';
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
- list(): [] | Heroku.Account[];
8
+ current(heroku: APIClient): Promise<null | string>;
9
+ currentNetrc(): Promise<null | string>;
10
+ getStorageConfig(): ReturnType<typeof getStorageConfig>;
11
+ list(): Promise<AccountEntry[]>;
6
12
  remove(name: string): void;
7
- set(name: string): Promise<void>;
13
+ removeNetrc(name: string): void;
14
+ set(account: AccountEntry, dataDir: string): Promise<void>;
15
+ writeLoginState(dataDir: string, name: string): Promise<void>;
8
16
  }
9
17
  export declare class AccountsWrapper implements IAccountsWrapper {
10
18
  private netrc;
11
19
  add(name: string, username: string, password: string): void;
12
- current(): Promise<null | string>;
13
- list(): [] | Heroku.Account[];
14
- remove(name: string): void;
15
- set(name: string): Promise<void>;
20
+ current(heroku: APIClient): Promise<null | string>;
21
+ currentNetrc(): Promise<null | string>;
22
+ getKeychainAccounts(): Promise<(null | string | undefined)[]>;
23
+ getStorageConfig(): import("@heroku-cli/command").StorageConfig;
24
+ list(): Promise<AccountEntry[]>;
25
+ listNetrc(): AccountEntry[];
26
+ remove(name: string): Promise<void>;
27
+ removeNetrc(name: string): void;
28
+ set(account: AccountEntry, dataDir: string): Promise<void>;
29
+ writeLoginState(dataDir: string, name: string): Promise<void>;
16
30
  private account;
17
31
  private configDir;
18
32
  private initNetrc;
@@ -1,3 +1,5 @@
1
+ import { getStorageConfig, listKeychainAccounts, 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,41 +7,86 @@ import { parse, stringify } from 'yaml';
5
7
  export class AccountsWrapper {
6
8
  netrc;
7
9
  add(name, username, password) {
8
- const basedir = path.join(this.configDir(), 'accounts');
9
- fs.mkdirSync(basedir, { recursive: true });
10
- fs.writeFileSync(path.join(basedir, name),
11
- // eslint-disable-next-line perfectionist/sort-objects
12
- stringify({ username, password }), 'utf8');
13
- fs.chmodSync(path.join(basedir, name), 0o600);
10
+ const config = this.getStorageConfig();
11
+ if (config.useNetrc) {
12
+ const basedir = path.join(this.configDir(), 'accounts');
13
+ fs.mkdirSync(basedir, { recursive: true });
14
+ fs.writeFileSync(path.join(basedir, name),
15
+ // eslint-disable-next-line perfectionist/sort-objects
16
+ stringify({ username, password }), 'utf8');
17
+ fs.chmodSync(path.join(basedir, name), 0o600);
18
+ }
19
+ }
20
+ async current(heroku) {
21
+ const config = this.getStorageConfig();
22
+ if (config.credentialStore) {
23
+ const authEntry = await heroku.getAuthEntry();
24
+ return authEntry?.account ?? null;
25
+ }
26
+ return this.currentNetrc();
14
27
  }
15
- async current() {
28
+ async currentNetrc() {
16
29
  const netrcInstance = await this.initNetrc();
17
30
  if (netrcInstance.machines['api.heroku.com']) {
18
- const current = this.list().find(a => a.username === netrcInstance.machines['api.heroku.com'].login);
31
+ const current = this.listNetrc().find(a => a.username === netrcInstance.machines['api.heroku.com'].login);
19
32
  return current && current.name ? current.name : null;
20
33
  }
21
34
  return null;
22
35
  }
23
- list() {
36
+ async getKeychainAccounts() {
37
+ return listKeychainAccounts();
38
+ }
39
+ getStorageConfig() {
40
+ return getStorageConfig();
41
+ }
42
+ async list() {
43
+ const config = this.getStorageConfig();
44
+ if (config.credentialStore) {
45
+ const accounts = await this.getKeychainAccounts();
46
+ return accounts
47
+ .filter((account) => account !== null && account !== undefined)
48
+ .map(account => ({ username: account }));
49
+ }
50
+ return this.listNetrc();
51
+ }
52
+ listNetrc() {
24
53
  const basedir = path.join(this.configDir(), 'accounts');
25
54
  try {
26
55
  return fs.readdirSync(basedir)
27
- .map(name => Object.assign(this.account(name), { name }));
56
+ .map(name => ({ name, username: this.account(name).username ?? '' }));
28
57
  }
29
58
  catch {
30
59
  return [];
31
60
  }
32
61
  }
33
- remove(name) {
62
+ async remove(name) {
63
+ const config = this.getStorageConfig();
64
+ if (config.credentialStore) {
65
+ await removeAuth(name, ['api.heroku.com', 'git.heroku.com']);
66
+ return;
67
+ }
68
+ this.removeNetrc(name);
69
+ }
70
+ removeNetrc(name) {
34
71
  const basedir = path.join(this.configDir(), 'accounts');
35
72
  fs.unlinkSync(path.join(basedir, name));
36
73
  }
37
- async set(name) {
38
- const netrcInstance = await this.initNetrc();
39
- const current = this.account(name);
40
- netrcInstance.machines['git.heroku.com'] = { login: current.username, password: current.password };
41
- netrcInstance.machines['api.heroku.com'] = { login: current.username, password: current.password };
42
- await netrcInstance.save();
74
+ async set(account, dataDir) {
75
+ const config = this.getStorageConfig();
76
+ if (config.credentialStore && !account.name) {
77
+ await this.writeLoginState(dataDir, account.username);
78
+ return;
79
+ }
80
+ if (config.useNetrc && account.name) {
81
+ const netrcInstance = await this.initNetrc();
82
+ const current = this.account(account.name);
83
+ netrcInstance.machines['git.heroku.com'] = { login: current.username, password: current.password };
84
+ netrcInstance.machines['api.heroku.com'] = { login: current.username, password: current.password };
85
+ await netrcInstance.save();
86
+ }
87
+ }
88
+ async writeLoginState(dataDir, name) {
89
+ return writeLoginState(dataDir, name);
43
90
  }
44
91
  account(name) {
45
92
  const basedir = path.join(this.configDir(), 'accounts');
@@ -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;
@@ -1,4 +1,4 @@
1
- import { vars } from '@heroku-cli/command';
1
+ import { APIClient } from '@heroku-cli/command';
2
2
  import { HTTP } from '@heroku/http-call';
3
3
  import fs from 'fs-extra';
4
4
  import path from 'node:path';
@@ -8,8 +8,8 @@ export default class BackboardHerokulyticsClient {
8
8
  config;
9
9
  http;
10
10
  userConfig;
11
+ heroku;
11
12
  isInitialized = false;
12
- netrc;
13
13
  constructor(config) {
14
14
  this.config = config;
15
15
  this.http = HTTP.create({
@@ -17,26 +17,11 @@ export default class BackboardHerokulyticsClient {
17
17
  });
18
18
  }
19
19
  get authorizationToken() {
20
- return process.env.HEROKU_API_KEY || this.netrcToken;
21
- }
22
- get netrcLogin() {
23
- return this.netrc?.machines[vars.apiHost]?.login;
24
- }
25
- get netrcToken() {
26
- return this.netrc?.machines[vars.apiHost]?.password;
20
+ return this.heroku.auth;
27
21
  }
28
22
  get url() {
29
23
  return process.env.HEROKU_ANALYTICS_URL || 'https://backboard.heroku.com/hamurai';
30
24
  }
31
- get user() {
32
- if (this.usingHerokuAPIKey)
33
- return undefined;
34
- return this.netrcLogin;
35
- }
36
- get usingHerokuAPIKey() {
37
- const k = process.env.HEROKU_API_KEY;
38
- return Boolean(k && k.length > 0);
39
- }
40
25
  async _acAnalytics(id) {
41
26
  if (id === 'autocomplete:options')
42
27
  return 0;
@@ -110,10 +95,8 @@ export default class BackboardHerokulyticsClient {
110
95
  }
111
96
  telemetryDebug('Initializing Herokulytics client...');
112
97
  this.isInitialized = true;
113
- const NetrcModule = await import('netrc-parser');
114
- const NetrcClass = NetrcModule.Netrc || NetrcModule.default.constructor;
115
- this.netrc = new NetrcClass();
116
- await this.netrc.load();
98
+ this.heroku = new APIClient(this.config);
99
+ await this.heroku.getAuth();
117
100
  this.userConfig = new HerokulyticsConfig(this.config);
118
101
  await this.userConfig.init();
119
102
  telemetryDebug('Herokulytics client initialized (install_id: %s)', this.userConfig.install);
@@ -1,12 +1,20 @@
1
+ import cp from 'node:child_process';
1
2
  export default class Git {
3
+ private readonly execFile;
4
+ /** Configures `heroku git:credentials` as a Git credential helper
5
+ * that is URL-scoped to Heroku Git operations only.
6
+ */
7
+ configureCredentialHelper(): Promise<void>;
2
8
  createRemote(remote: string, url: string): Promise<string | null>;
9
+ /** Erases stored credentials for the Heroku Git host */
10
+ eraseCredentials(): Promise<void>;
3
11
  exec(args: string[]): Promise<string>;
4
12
  getBranch(symbolicRef: string): Promise<string>;
5
13
  getCommitTitle(ref: string): Promise<string>;
6
14
  getRef(branch: string): Promise<string>;
7
15
  hasGitRemote(remote: string): Promise<boolean>;
8
16
  httpGitUrl(app: string): string;
9
- inGitRepo(): true | undefined;
17
+ inGitRepo(): boolean;
10
18
  readCommit(commit: string): Promise<{
11
19
  branch: string;
12
20
  message: string;
@@ -14,6 +22,11 @@ export default class Git {
14
22
  }>;
15
23
  remoteFromGitConfig(): Promise<string | void>;
16
24
  remoteUrl(name: string): Promise<string>;
17
- spawn(args: string[]): Promise<unknown>;
25
+ /** Removes `heroku git:credentials` from the global config */
26
+ removeCredentialHelper(): Promise<void>;
27
+ spawn(args: string[], options?: {
28
+ input?: string;
29
+ stdio?: cp.StdioOptions;
30
+ }): Promise<unknown>;
18
31
  url(app: string): string;
19
32
  }