@heroku/heroku-cli-util 9.0.1 → 10.0.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.
Files changed (38) hide show
  1. package/dist/index.d.ts +14 -26
  2. package/dist/index.js +27 -43
  3. package/dist/types/errors/ambiguous.js +7 -7
  4. package/dist/types/errors/not-found.js +4 -11
  5. package/dist/types/pg/data-api.js +1 -2
  6. package/dist/types/pg/tunnel.d.ts +1 -1
  7. package/dist/types/pg/tunnel.js +1 -2
  8. package/dist/utils/addons/resolve.d.ts +1 -1
  9. package/dist/utils/addons/resolve.js +5 -9
  10. package/dist/utils/pg/bastion.d.ts +3 -3
  11. package/dist/utils/pg/bastion.js +31 -33
  12. package/dist/utils/pg/config-vars.d.ts +1 -1
  13. package/dist/utils/pg/config-vars.js +8 -14
  14. package/dist/utils/pg/databases.d.ts +2 -2
  15. package/dist/utils/pg/databases.js +34 -43
  16. package/dist/utils/pg/host.js +2 -5
  17. package/dist/utils/pg/psql.d.ts +1 -1
  18. package/dist/utils/pg/psql.js +31 -37
  19. package/dist/ux/confirm.d.ts +9 -1
  20. package/dist/ux/confirm.js +37 -7
  21. package/dist/ux/prompt.d.ts +7 -2
  22. package/dist/ux/prompt.js +10 -6
  23. package/dist/ux/styled-header.js +4 -6
  24. package/dist/ux/styled-json.js +3 -6
  25. package/dist/ux/styled-object.d.ts +1 -1
  26. package/dist/ux/styled-object.js +48 -6
  27. package/dist/ux/table.d.ts +22 -2
  28. package/dist/ux/table.js +14 -6
  29. package/dist/ux/wait.js +4 -6
  30. package/package.json +16 -25
  31. package/dist/test-helpers/expect-output.d.ts +0 -2
  32. package/dist/test-helpers/expect-output.js +0 -16
  33. package/dist/test-helpers/init.d.ts +0 -1
  34. package/dist/test-helpers/init.js +0 -17
  35. package/dist/test-helpers/run-command.d.ts +0 -9
  36. package/dist/test-helpers/run-command.js +0 -43
  37. package/dist/test-helpers/stub-output.d.ts +0 -4
  38. package/dist/test-helpers/stub-output.js +0 -35
package/dist/index.d.ts CHANGED
@@ -1,28 +1,16 @@
1
- import { initCliTest } from './test-helpers/init';
2
- import { restoreStdoutStderr, setupStdoutStderr, stderr, stdout } from './test-helpers/stub-output';
3
- import { AmbiguousError } from './types/errors/ambiguous';
4
- import { NotFound } from './types/errors/not-found';
5
- import { AddOnAttachmentWithConfigVarsAndPlan, AddOnWithRelatedData, Link } from './types/pg/data-api';
6
- import { ConnectionDetails, ConnectionDetailsWithAttachment, TunnelConfig } from './types/pg/tunnel';
7
- import { getDatabase } from './utils/pg/databases';
8
- import getHost from './utils/pg/host';
9
- import { exec } from './utils/pg/psql';
10
- import { confirm } from './ux/confirm';
11
- import { prompt } from './ux/prompt';
12
- import { styledHeader } from './ux/styled-header';
13
- import { styledJSON } from './ux/styled-json';
14
- import { styledObject } from './ux/styled-object';
15
- import { table } from './ux/table';
16
- import { wait } from './ux/wait';
17
- export declare const testHelpers: {
18
- expectOutput: (actual: string, expected: string) => Chai.Assertion;
19
- initCliTest: typeof initCliTest;
20
- restoreStdoutStderr: typeof restoreStdoutStderr;
21
- runCommand: (Cmd: import("./test-helpers/run-command").GenericCmd, args?: string[], printStd?: boolean) => Promise<any>;
22
- setupStdoutStderr: typeof setupStdoutStderr;
23
- stderr: typeof stderr;
24
- stdout: typeof stdout;
25
- };
1
+ import { AmbiguousError } from './types/errors/ambiguous.js';
2
+ import { NotFound } from './types/errors/not-found.js';
3
+ import { AddOnAttachmentWithConfigVarsAndPlan, AddOnWithRelatedData, Link } from './types/pg/data-api.js';
4
+ import { ConnectionDetails, ConnectionDetailsWithAttachment, TunnelConfig } from './types/pg/tunnel.js';
5
+ import { getDatabase } from './utils/pg/databases.js';
6
+ import getHost from './utils/pg/host.js';
7
+ import { exec } from './utils/pg/psql.js';
8
+ import { prompt } from './ux/prompt.js';
9
+ import { styledHeader } from './ux/styled-header.js';
10
+ import { styledJSON } from './ux/styled-json.js';
11
+ import { styledObject } from './ux/styled-object.js';
12
+ import { table } from './ux/table.js';
13
+ import { wait } from './ux/wait.js';
26
14
  export declare const types: {
27
15
  errors: {
28
16
  AmbiguousError: typeof AmbiguousError;
@@ -47,7 +35,7 @@ export declare const utils: {
47
35
  };
48
36
  };
49
37
  export declare const hux: {
50
- confirm: typeof confirm;
38
+ confirm: (message: string, { defaultAnswer, ms, }?: import("./ux/confirm.js").PromptInputs<boolean>) => Promise<boolean>;
51
39
  prompt: typeof prompt;
52
40
  styledHeader: typeof styledHeader;
53
41
  styledJSON: typeof styledJSON;
package/dist/index.js CHANGED
@@ -1,35 +1,19 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.hux = exports.utils = exports.types = exports.testHelpers = void 0;
4
- const expect_output_1 = require("./test-helpers/expect-output");
5
- const init_1 = require("./test-helpers/init");
6
- const run_command_1 = require("./test-helpers/run-command");
7
- const stub_output_1 = require("./test-helpers/stub-output");
8
- const ambiguous_1 = require("./types/errors/ambiguous");
9
- const not_found_1 = require("./types/errors/not-found");
10
- const databases_1 = require("./utils/pg/databases");
11
- const host_1 = require("./utils/pg/host");
12
- const psql_1 = require("./utils/pg/psql");
13
- const confirm_1 = require("./ux/confirm");
14
- const prompt_1 = require("./ux/prompt");
15
- const styled_header_1 = require("./ux/styled-header");
16
- const styled_json_1 = require("./ux/styled-json");
17
- const styled_object_1 = require("./ux/styled-object");
18
- const table_1 = require("./ux/table");
19
- const wait_1 = require("./ux/wait");
20
- exports.testHelpers = {
21
- expectOutput: expect_output_1.default,
22
- initCliTest: init_1.initCliTest,
23
- restoreStdoutStderr: stub_output_1.restoreStdoutStderr,
24
- runCommand: run_command_1.runCommand,
25
- setupStdoutStderr: stub_output_1.setupStdoutStderr,
26
- stderr: stub_output_1.stderr,
27
- stdout: stub_output_1.stdout,
28
- };
29
- exports.types = {
1
+ import { AmbiguousError } from './types/errors/ambiguous.js';
2
+ import { NotFound } from './types/errors/not-found.js';
3
+ import { getDatabase } from './utils/pg/databases.js';
4
+ import getHost from './utils/pg/host.js';
5
+ import { exec } from './utils/pg/psql.js';
6
+ import { confirm } from './ux/confirm.js';
7
+ import { prompt } from './ux/prompt.js';
8
+ import { styledHeader } from './ux/styled-header.js';
9
+ import { styledJSON } from './ux/styled-json.js';
10
+ import { styledObject } from './ux/styled-object.js';
11
+ import { table } from './ux/table.js';
12
+ import { wait } from './ux/wait.js';
13
+ export const types = {
30
14
  errors: {
31
- AmbiguousError: ambiguous_1.AmbiguousError,
32
- NotFound: not_found_1.NotFound,
15
+ AmbiguousError,
16
+ NotFound,
33
17
  },
34
18
  pg: {
35
19
  AddOnAttachmentWithConfigVarsAndPlan: {},
@@ -40,21 +24,21 @@ exports.types = {
40
24
  TunnelConfig: {},
41
25
  },
42
26
  };
43
- exports.utils = {
27
+ export const utils = {
44
28
  pg: {
45
- databases: databases_1.getDatabase,
46
- host: host_1.default,
29
+ databases: getDatabase,
30
+ host: getHost,
47
31
  psql: {
48
- exec: psql_1.exec,
32
+ exec,
49
33
  },
50
34
  },
51
35
  };
52
- exports.hux = {
53
- confirm: confirm_1.confirm,
54
- prompt: prompt_1.prompt,
55
- styledHeader: styled_header_1.styledHeader,
56
- styledJSON: styled_json_1.styledJSON,
57
- styledObject: styled_object_1.styledObject,
58
- table: table_1.table,
59
- wait: wait_1.wait,
36
+ export const hux = {
37
+ confirm,
38
+ prompt,
39
+ styledHeader,
40
+ styledJSON,
41
+ styledObject,
42
+ table,
43
+ wait,
60
44
  };
@@ -1,14 +1,14 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.AmbiguousError = void 0;
4
- class AmbiguousError extends Error {
1
+ export class AmbiguousError extends Error {
2
+ matches;
3
+ type;
4
+ body;
5
+ message;
6
+ statusCode = 422;
5
7
  constructor(matches, type) {
6
8
  super();
7
9
  this.matches = matches;
8
10
  this.type = type;
9
- this.body = { id: 'multiple_matches', message: this.message };
10
- this.statusCode = 422;
11
11
  this.message = `Ambiguous identifier; multiple matching add-ons found: ${matches.map(match => match.name).join(', ')}.`;
12
+ this.body = { id: 'multiple_matches', message: this.message };
12
13
  }
13
14
  }
14
- exports.AmbiguousError = AmbiguousError;
@@ -1,12 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.NotFound = void 0;
4
- class NotFound extends Error {
5
- constructor() {
6
- super(...arguments);
7
- this.id = 'not_found';
8
- this.message = 'Couldn\'t find that addon.';
9
- this.statusCode = 404;
10
- }
1
+ export class NotFound extends Error {
2
+ id = 'not_found';
3
+ message = 'Couldn\'t find that addon.';
4
+ statusCode = 404;
11
5
  }
12
- exports.NotFound = NotFound;
@@ -1,2 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};
@@ -1,7 +1,7 @@
1
1
  import type { AddOnAttachment } from '@heroku-cli/schema';
2
2
  import { Server } from 'node:net';
3
3
  import * as createTunnel from 'tunnel-ssh';
4
- import type { AddOnAttachmentWithConfigVarsAndPlan } from './data-api';
4
+ import type { AddOnAttachmentWithConfigVarsAndPlan } from './data-api.js';
5
5
  export type ConnectionDetails = {
6
6
  _tunnel?: Server;
7
7
  bastionHost?: string;
@@ -1,2 +1 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
1
+ export {};
@@ -1,6 +1,6 @@
1
1
  import type { APIClient } from '@heroku-cli/command';
2
2
  import type { AddOnAttachment } from '@heroku-cli/schema';
3
- import type { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api';
3
+ import type { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api.js';
4
4
  export declare const appAttachment: (heroku: APIClient, app: string | undefined, id: string, options?: {
5
5
  addon_service?: string;
6
6
  namespace?: string;
@@ -1,16 +1,12 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.appAttachment = void 0;
4
- const ambiguous_1 = require("../../types/errors/ambiguous");
5
- const not_found_1 = require("../../types/errors/not-found");
6
- const appAttachment = async (heroku, app, id, options = {}) => {
1
+ import { AmbiguousError } from '../../types/errors/ambiguous.js';
2
+ import { NotFound } from '../../types/errors/not-found.js';
3
+ export const appAttachment = async (heroku, app, id, options = {}) => {
7
4
  const result = await heroku.post('/actions/addon-attachments/resolve', {
8
5
  // eslint-disable-next-line camelcase
9
6
  body: { addon_attachment: id, addon_service: options.addon_service, app }, headers: attachmentHeaders,
10
7
  });
11
8
  return singularize('addon_attachment', options.namespace)(result.body);
12
9
  };
13
- exports.appAttachment = appAttachment;
14
10
  const attachmentHeaders = {
15
11
  Accept: 'application/vnd.heroku+json; version=3.sdk',
16
12
  'Accept-Inclusion': 'addon:plan,config_vars',
@@ -26,13 +22,13 @@ function singularize(type, namespace) {
26
22
  }
27
23
  switch (matches.length) {
28
24
  case 0: {
29
- throw new not_found_1.NotFound();
25
+ throw new NotFound();
30
26
  }
31
27
  case 1: {
32
28
  return matches[0];
33
29
  }
34
30
  default: {
35
- throw new ambiguous_1.AmbiguousError(matches, type !== null && type !== void 0 ? type : '');
31
+ throw new AmbiguousError(matches, type ?? '');
36
32
  }
37
33
  }
38
34
  };
@@ -1,8 +1,8 @@
1
1
  import type { APIClient } from '@heroku-cli/command';
2
2
  import * as createTunnel from 'tunnel-ssh';
3
- import { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api';
4
- import { ConnectionDetails } from '../../types/pg/tunnel';
5
- import { TunnelConfig } from '../../types/pg/tunnel';
3
+ import { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api.js';
4
+ import { ConnectionDetails } from '../../types/pg/tunnel.js';
5
+ import { TunnelConfig } from '../../types/pg/tunnel.js';
6
6
  export declare const bastionKeyPlan: (a: AddOnAttachmentWithConfigVarsAndPlan) => boolean;
7
7
  export declare const env: (db: ConnectionDetails) => {
8
8
  TZ?: string;
@@ -1,21 +1,17 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getBastion = exports.env = exports.bastionKeyPlan = void 0;
4
- exports.fetchConfig = fetchConfig;
5
- exports.getConfigs = getConfigs;
6
- exports.sshTunnel = sshTunnel;
7
- exports.tunnelConfig = tunnelConfig;
8
- const core_1 = require("@oclif/core");
9
- const debug_1 = require("debug");
10
- const EventEmitter = require("node:events");
11
- const node_util_1 = require("node:util");
12
- const createTunnel = require("tunnel-ssh");
13
- const host_1 = require("./host");
14
- const pgDebug = (0, debug_1.default)('pg');
15
- const bastionKeyPlan = (a) => Boolean(/private/.test(a.addon.plan.name));
16
- exports.bastionKeyPlan = bastionKeyPlan;
17
- const env = (db) => {
18
- const baseEnv = Object.assign({ PGAPPNAME: 'psql non-interactive', PGSSLMODE: (!db.host || db.host === 'localhost') ? 'prefer' : 'require' }, process.env);
1
+ import { ux } from '@oclif/core';
2
+ import debug from 'debug';
3
+ import * as EventEmitter from 'node:events';
4
+ import { promisify } from 'node:util';
5
+ import * as createTunnel from 'tunnel-ssh';
6
+ import host from './host.js';
7
+ const pgDebug = debug('pg');
8
+ export const bastionKeyPlan = (a) => Boolean(/private/.test(a.addon.plan.name));
9
+ export const env = (db) => {
10
+ const baseEnv = {
11
+ PGAPPNAME: 'psql non-interactive',
12
+ PGSSLMODE: (!db.host || db.host === 'localhost') ? 'prefer' : 'require',
13
+ ...process.env,
14
+ };
19
15
  const mapping = {
20
16
  PGDATABASE: 'database',
21
17
  PGHOST: 'host',
@@ -31,13 +27,12 @@ const env = (db) => {
31
27
  }
32
28
  return baseEnv;
33
29
  };
34
- exports.env = env;
35
- async function fetchConfig(heroku, db) {
30
+ export async function fetchConfig(heroku, db) {
36
31
  return heroku.get(`/client/v11/databases/${encodeURIComponent(db.id)}/bastion`, {
37
- hostname: (0, host_1.default)(),
32
+ hostname: host(),
38
33
  });
39
34
  }
40
- const getBastion = function (config, baseName) {
35
+ export const getBastion = function (config, baseName) {
41
36
  // If there are bastions, extract a host and a key
42
37
  // otherwise, return an empty Object
43
38
  // If there are bastions:
@@ -50,9 +45,8 @@ const getBastion = function (config, baseName) {
50
45
  const bastionHost = bastions[Math.floor(Math.random() * bastions.length)];
51
46
  return (bastionKey && bastionHost) ? { bastionHost, bastionKey } : {};
52
47
  };
53
- exports.getBastion = getBastion;
54
- function getConfigs(db) {
55
- const dbEnv = (0, exports.env)(db);
48
+ export function getConfigs(db) {
49
+ const dbEnv = env(db);
56
50
  const dbTunnelConfig = tunnelConfig(db);
57
51
  if (db.bastionKey) {
58
52
  Object.assign(dbEnv, {
@@ -65,12 +59,12 @@ function getConfigs(db) {
65
59
  dbTunnelConfig,
66
60
  };
67
61
  }
68
- async function sshTunnel(db, dbTunnelConfig, timeout = 10000) {
62
+ export async function sshTunnel(db, dbTunnelConfig, timeout = 10_000) {
69
63
  if (!db.bastionKey) {
70
64
  return null;
71
65
  }
72
66
  const timeoutInstance = new Timeout(timeout, 'Establishing a secure tunnel timed out');
73
- const createSSHTunnel = (0, node_util_1.promisify)(createTunnel);
67
+ const createSSHTunnel = promisify(createTunnel.default);
74
68
  try {
75
69
  return await Promise.race([
76
70
  timeoutInstance.promise(),
@@ -79,15 +73,15 @@ async function sshTunnel(db, dbTunnelConfig, timeout = 10000) {
79
73
  }
80
74
  catch (error) {
81
75
  pgDebug(error);
82
- core_1.ux.error('Unable to establish a secure tunnel to your database.');
76
+ ux.error('Unable to establish a secure tunnel to your database.');
83
77
  }
84
78
  finally {
85
79
  timeoutInstance.cancel();
86
80
  }
87
81
  }
88
- function tunnelConfig(db) {
82
+ export function tunnelConfig(db) {
89
83
  const localHost = '127.0.0.1';
90
- const localPort = Math.floor((Math.random() * (65535 - 49152)) + 49152);
84
+ const localPort = Math.floor((Math.random() * (65_535 - 49_152)) + 49_152);
91
85
  return {
92
86
  dstHost: db.host || undefined,
93
87
  dstPort: (db.port && Number.parseInt(db.port, 10)) || undefined,
@@ -99,9 +93,11 @@ function tunnelConfig(db) {
99
93
  };
100
94
  }
101
95
  class Timeout {
96
+ events = new EventEmitter.EventEmitter();
97
+ message;
98
+ timeout;
99
+ timer;
102
100
  constructor(timeout, message) {
103
- // eslint-disable-next-line unicorn/prefer-event-target
104
- this.events = new EventEmitter();
105
101
  this.timeout = timeout;
106
102
  this.message = message;
107
103
  }
@@ -113,7 +109,9 @@ class Timeout {
113
109
  this.events.emit('error', new Error(this.message));
114
110
  }, this.timeout);
115
111
  try {
116
- await EventEmitter.once(this.events, 'cancelled');
112
+ await new Promise(resolve => {
113
+ this.events.once('cancelled', () => resolve());
114
+ });
117
115
  }
118
116
  finally {
119
117
  clearTimeout(this.timer);
@@ -1,6 +1,6 @@
1
1
  import type { APIClient } from '@heroku-cli/command';
2
2
  import type { AddOnAttachment } from '@heroku-cli/schema';
3
- import type { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api';
3
+ import type { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api.js';
4
4
  export declare function getConfig(heroku: APIClient, app: string): Promise<Record<string, string> | undefined>;
5
5
  export declare function getConfigVarName(configVars: string[]): string;
6
6
  export declare function getConfigVarNameFromAttachment(attachment: Required<{
@@ -1,30 +1,24 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getConfig = getConfig;
4
- exports.getConfigVarName = getConfigVarName;
5
- exports.getConfigVarNameFromAttachment = getConfigVarNameFromAttachment;
6
- const color_1 = require("@heroku-cli/color");
7
- const core_1 = require("@oclif/core");
1
+ import { color } from '@heroku-cli/color';
2
+ import { ux } from '@oclif/core';
8
3
  const responseByAppId = new Map();
9
- async function getConfig(heroku, app) {
4
+ export async function getConfig(heroku, app) {
10
5
  if (!responseByAppId.has(app)) {
11
6
  const promise = heroku.get(`/apps/${app}/config-vars`);
12
7
  responseByAppId.set(app, promise);
13
8
  }
14
9
  const result = await responseByAppId.get(app);
15
- return result === null || result === void 0 ? void 0 : result.body;
10
+ return result?.body;
16
11
  }
17
- function getConfigVarName(configVars) {
12
+ export function getConfigVarName(configVars) {
18
13
  const connStringVars = configVars.filter(cv => (cv.endsWith('_URL')));
19
14
  if (connStringVars.length === 0)
20
15
  throw new Error('Database URL not found for this addon');
21
16
  return connStringVars[0];
22
17
  }
23
- function getConfigVarNameFromAttachment(attachment, config = {}) {
24
- var _a, _b;
25
- const configVars = (_b = (_a = attachment.addon.config_vars) === null || _a === void 0 ? void 0 : _a.filter((cv) => { var _a; return (_a = config[cv]) === null || _a === void 0 ? void 0 : _a.startsWith('postgres://'); })) !== null && _b !== void 0 ? _b : [];
18
+ export function getConfigVarNameFromAttachment(attachment, config = {}) {
19
+ const configVars = attachment.addon.config_vars?.filter((cv) => config[cv]?.startsWith('postgres://')) ?? [];
26
20
  if (configVars.length === 0) {
27
- core_1.ux.error(`No config vars found for ${attachment.name}; perhaps they were removed as a side effect of ${color_1.default.cmd('heroku rollback')}? Use ${color_1.default.cmd('heroku addons:attach')} to create a new attachment and then ${color_1.default.cmd('heroku addons:detach')} to remove the current attachment.`);
21
+ ux.error(`No config vars found for ${attachment.name}; perhaps they were removed as a side effect of ${color.cmd('heroku rollback')}? Use ${color.cmd('heroku addons:attach')} to create a new attachment and then ${color.cmd('heroku addons:detach')} to remove the current attachment.`);
28
22
  }
29
23
  const configVarName = `${attachment.name}_URL`;
30
24
  if (configVars.includes(configVarName) && configVarName in config) {
@@ -1,7 +1,7 @@
1
1
  import type { AddOnAttachment } from '@heroku-cli/schema';
2
2
  import { APIClient } from '@heroku-cli/command';
3
- import type { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api';
4
- import type { ConnectionDetails, ConnectionDetailsWithAttachment } from '../../types/pg/tunnel';
3
+ import type { AddOnAttachmentWithConfigVarsAndPlan } from '../../types/pg/data-api.js';
4
+ import type { ConnectionDetails, ConnectionDetailsWithAttachment } from '../../types/pg/tunnel.js';
5
5
  export declare function getAttachment(heroku: APIClient, app: string, db?: string, namespace?: string): Promise<Required<{
6
6
  addon: AddOnAttachmentWithConfigVarsAndPlan;
7
7
  } & AddOnAttachment>>;
@@ -1,25 +1,19 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.parsePostgresConnectionString = exports.getConnectionDetails = void 0;
4
- exports.getAttachment = getAttachment;
5
- exports.getDatabase = getDatabase;
6
- const color_1 = require("@heroku-cli/color");
7
- const api_client_1 = require("@heroku-cli/command/lib/api-client");
8
- const debug_1 = require("debug");
9
- const node_process_1 = require("node:process");
10
- const ambiguous_1 = require("../../types/errors/ambiguous");
11
- const resolve_1 = require("../addons/resolve");
12
- const bastion_1 = require("./bastion");
13
- const config_vars_1 = require("./config-vars");
14
- const pgDebug = (0, debug_1.default)('pg');
1
+ import { color } from '@heroku-cli/color';
2
+ import { HerokuAPIError } from '@heroku-cli/command/lib/api-client.js';
3
+ import debug from 'debug';
4
+ import { env } from 'node:process';
5
+ import { AmbiguousError } from '../../types/errors/ambiguous.js';
6
+ import { appAttachment } from '../addons/resolve.js';
7
+ import { bastionKeyPlan, fetchConfig, getBastion } from './bastion.js';
8
+ import { getConfig, getConfigVarName, getConfigVarNameFromAttachment } from './config-vars.js';
9
+ const pgDebug = debug('pg');
15
10
  async function allAttachments(heroku, appId) {
16
11
  const { body: attachments } = await heroku.get(`/apps/${appId}/addon-attachments`, {
17
12
  headers: { 'Accept-Inclusion': 'addon:plan,config_vars' },
18
13
  });
19
- return attachments.filter((a) => { var _a, _b; return (_b = (_a = a.addon.plan) === null || _a === void 0 ? void 0 : _a.name) === null || _b === void 0 ? void 0 : _b.startsWith('heroku-postgresql'); });
14
+ return attachments.filter((a) => a.addon.plan?.name?.startsWith('heroku-postgresql'));
20
15
  }
21
- async function getAttachment(heroku, app, db = 'DATABASE_URL', namespace = '') {
22
- var _a;
16
+ export async function getAttachment(heroku, app, db = 'DATABASE_URL', namespace = '') {
23
17
  const matchesOrError = await matchesHelper(heroku, app, db, namespace);
24
18
  let { matches } = matchesOrError;
25
19
  const { error } = matchesOrError;
@@ -38,36 +32,36 @@ async function getAttachment(heroku, app, db = 'DATABASE_URL', namespace = '') {
38
32
  db += '_URL';
39
33
  }
40
34
  const [config = {}, attachments] = await Promise.all([
41
- (0, config_vars_1.getConfig)(heroku, app),
35
+ getConfig(heroku, app),
42
36
  allAttachments(heroku, app),
43
37
  ]);
44
38
  if (attachments.length === 0) {
45
- throw new Error(`${color_1.default.app(app)} has no databases`);
39
+ throw new Error(`${color.app(app)} has no databases`);
46
40
  }
47
- matches = attachments.filter(attachment => config[db] && config[db] === config[(0, config_vars_1.getConfigVarName)(attachment.config_vars)]);
41
+ matches = attachments.filter(attachment => config[db] && config[db] === config[getConfigVarName(attachment.config_vars)]);
48
42
  if (matches.length === 0) {
49
- const validOptions = attachments.map(attachment => (0, config_vars_1.getConfigVarName)(attachment.config_vars));
43
+ const validOptions = attachments.map(attachment => getConfigVarName(attachment.config_vars));
50
44
  throw new Error(`Unknown database: ${db}. Valid options are: ${validOptions.join(', ')}`);
51
45
  }
52
46
  }
53
47
  // case for multiple attachments with passedDb
54
48
  const first = matches[0];
55
49
  // case for 422 where there are ambiguous attachments that are equivalent
56
- if (matches.every(match => { var _a, _b, _c, _d; return ((_a = first.addon) === null || _a === void 0 ? void 0 : _a.id) === ((_b = match.addon) === null || _b === void 0 ? void 0 : _b.id) && ((_c = first.app) === null || _c === void 0 ? void 0 : _c.id) === ((_d = match.app) === null || _d === void 0 ? void 0 : _d.id); })) {
57
- const config = (_a = await (0, config_vars_1.getConfig)(heroku, first.app.name)) !== null && _a !== void 0 ? _a : {};
58
- if (matches.every(match => config[(0, config_vars_1.getConfigVarName)(first.addon.config_vars)] === config[(0, config_vars_1.getConfigVarName)(match.config_vars)])) {
50
+ if (matches.every(match => first.addon?.id === match.addon?.id && first.app?.id === match.app?.id)) {
51
+ const config = await getConfig(heroku, first.app.name) ?? {};
52
+ if (matches.every(match => config[getConfigVarName(first.addon.config_vars)] === config[getConfigVarName(match.config_vars)])) {
59
53
  return first;
60
54
  }
61
55
  }
62
56
  throw error;
63
57
  }
64
- const getConnectionDetails = (attachment, configVars = {}) => {
65
- const connStringVar = (0, config_vars_1.getConfigVarNameFromAttachment)(attachment, configVars);
58
+ export const getConnectionDetails = (attachment, configVars = {}) => {
59
+ const connStringVar = getConfigVarNameFromAttachment(attachment, configVars);
66
60
  // remove _URL from the end of the config var name
67
61
  const baseName = connStringVar.slice(0, -4);
68
62
  // build the default payload for non-bastion dbs
69
63
  pgDebug(`Using "${connStringVar}" to connect to your database…`);
70
- const conn = (0, exports.parsePostgresConnectionString)(configVars[connStringVar]);
64
+ const conn = parsePostgresConnectionString(configVars[connStringVar]);
71
65
  const payload = {
72
66
  attachment,
73
67
  database: conn.database,
@@ -79,22 +73,21 @@ const getConnectionDetails = (attachment, configVars = {}) => {
79
73
  user: conn.user,
80
74
  };
81
75
  // If bastion creds exist, graft it into the payload
82
- const bastion = (0, bastion_1.getBastion)(configVars, baseName);
76
+ const bastion = getBastion(configVars, baseName);
83
77
  if (bastion) {
84
78
  Object.assign(payload, bastion);
85
79
  }
86
80
  return payload;
87
81
  };
88
- exports.getConnectionDetails = getConnectionDetails;
89
- async function getDatabase(heroku, app, db, namespace) {
82
+ export async function getDatabase(heroku, app, db, namespace) {
90
83
  const attached = await getAttachment(heroku, app, db, namespace);
91
84
  // would inline this as well but in some cases attachment pulls down config
92
85
  // as well, and we would request twice at the same time but I did not want
93
86
  // to push this down into attachment because we do not always need config
94
- const config = await (0, config_vars_1.getConfig)(heroku, attached.app.name);
95
- const database = (0, exports.getConnectionDetails)(attached, config);
96
- if ((0, bastion_1.bastionKeyPlan)(attached.addon) && !database.bastionKey) {
97
- const { body: bastionConfig } = await (0, bastion_1.fetchConfig)(heroku, attached.addon);
87
+ const config = await getConfig(heroku, attached.app.name);
88
+ const database = getConnectionDetails(attached, config);
89
+ if (bastionKeyPlan(attached.addon) && !database.bastionKey) {
90
+ const { body: bastionConfig } = await fetchConfig(heroku, attached.addon);
98
91
  const bastionHost = bastionConfig.host;
99
92
  const bastionKey = bastionConfig.private_key;
100
93
  Object.assign(database, { bastionHost, bastionKey });
@@ -102,25 +95,24 @@ async function getDatabase(heroku, app, db, namespace) {
102
95
  return database;
103
96
  }
104
97
  async function matchesHelper(heroku, app, db, namespace) {
105
- var _a;
106
- (0, debug_1.default)(`fetching ${db} on ${app}`);
98
+ debug(`fetching ${db} on ${app}`);
107
99
  const addonService = process.env.HEROKU_POSTGRESQL_ADDON_NAME || 'heroku-postgresql';
108
- (0, debug_1.default)(`addon service: ${addonService}`);
100
+ debug(`addon service: ${addonService}`);
109
101
  try {
110
- const attached = await (0, resolve_1.appAttachment)(heroku, app, db, { addon_service: addonService, namespace });
102
+ const attached = await appAttachment(heroku, app, db, { addon_service: addonService, namespace });
111
103
  return ({ matches: [attached] });
112
104
  }
113
105
  catch (error) {
114
- if (error instanceof ambiguous_1.AmbiguousError && ((_a = error.body) === null || _a === void 0 ? void 0 : _a.id) === 'multiple_matches' && error.matches) {
106
+ if (error instanceof AmbiguousError && error.body?.id === 'multiple_matches' && error.matches) {
115
107
  return { error, matches: error.matches };
116
108
  }
117
- if (error instanceof api_client_1.HerokuAPIError && error.http.statusCode === 404 && error.body && error.body.id === 'not_found') {
109
+ if (error instanceof HerokuAPIError && error.http.statusCode === 404 && error.body && error.body.id === 'not_found') {
118
110
  return { error, matches: null };
119
111
  }
120
112
  throw error;
121
113
  }
122
114
  }
123
- const parsePostgresConnectionString = (db) => {
115
+ export const parsePostgresConnectionString = (db) => {
124
116
  const dbPath = /:\/\//.test(db) ? db : `postgres:///${db}`;
125
117
  const url = new URL(dbPath);
126
118
  const { hostname, password, pathname, port, username } = url;
@@ -129,9 +121,8 @@ const parsePostgresConnectionString = (db) => {
129
121
  host: hostname,
130
122
  password,
131
123
  pathname,
132
- port: port || node_process_1.env.PGPORT || (hostname && '5432'),
124
+ port: port || env.PGPORT || (hostname && '5432'),
133
125
  url: dbPath,
134
126
  user: username,
135
127
  };
136
128
  };
137
- exports.parsePostgresConnectionString = parsePostgresConnectionString;
@@ -1,7 +1,4 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.default = default_1;
4
- function default_1() {
1
+ export default function () {
5
2
  const host = process.env.HEROKU_DATA_HOST || process.env.HEROKU_POSTGRESQL_HOST;
6
- return host !== null && host !== void 0 ? host : 'api.data.heroku.com';
3
+ return host ?? 'api.data.heroku.com';
7
4
  }
@@ -2,7 +2,7 @@ import { type ChildProcess, type SpawnOptions, type SpawnOptionsWithStdioTuple }
2
2
  import { EventEmitter } from 'node:events';
3
3
  import { Server } from 'node:net';
4
4
  import { Stream } from 'node:stream';
5
- import { ConnectionDetails, TunnelConfig } from '../../types/pg/tunnel';
5
+ import { ConnectionDetails, TunnelConfig } from '../../types/pg/tunnel.js';
6
6
  export declare function consumeStream(inputStream: Stream): Promise<unknown>;
7
7
  export declare function exec(db: ConnectionDetails, query: string, cmdArgs?: string[]): Promise<string>;
8
8
  export declare function psqlQueryOptions(query: string, dbEnv: NodeJS.ProcessEnv, cmdArgs?: string[]): {
@@ -1,26 +1,17 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Tunnel = exports.trapAndForwardSignalsToChildProcess = void 0;
4
- exports.consumeStream = consumeStream;
5
- exports.exec = exec;
6
- exports.psqlQueryOptions = psqlQueryOptions;
7
- exports.execPSQL = execPSQL;
8
- exports.runWithTunnel = runWithTunnel;
9
- exports.waitForPSQLExit = waitForPSQLExit;
10
- const debug_1 = require("debug");
11
- const node_child_process_1 = require("node:child_process");
12
- const node_events_1 = require("node:events");
13
- const node_stream_1 = require("node:stream");
14
- const promises_1 = require("node:stream/promises");
15
- const bastion_1 = require("./bastion");
16
- const pgDebug = (0, debug_1.default)('pg');
17
- function consumeStream(inputStream) {
1
+ import debug from 'debug';
2
+ import { spawn, } from 'node:child_process';
3
+ import { EventEmitter, once } from 'node:events';
4
+ import { Stream } from 'node:stream';
5
+ import { finished } from 'node:stream/promises';
6
+ import { getConfigs, sshTunnel } from './bastion.js';
7
+ const pgDebug = debug('pg');
8
+ export function consumeStream(inputStream) {
18
9
  let result = '';
19
- const throughStream = new node_stream_1.Stream.PassThrough();
10
+ const throughStream = new Stream.PassThrough();
20
11
  // eslint-disable-next-line no-async-promise-executor
21
12
  const promise = new Promise(async (resolve, reject) => {
22
13
  try {
23
- await (0, promises_1.finished)(throughStream);
14
+ await finished(throughStream);
24
15
  resolve(result);
25
16
  }
26
17
  catch (error) {
@@ -32,12 +23,12 @@ function consumeStream(inputStream) {
32
23
  inputStream.pipe(throughStream);
33
24
  return promise;
34
25
  }
35
- async function exec(db, query, cmdArgs = []) {
36
- const configs = (0, bastion_1.getConfigs)(db);
26
+ export async function exec(db, query, cmdArgs = []) {
27
+ const configs = getConfigs(db);
37
28
  const options = psqlQueryOptions(query, configs.dbEnv, cmdArgs);
38
29
  return runWithTunnel(db, configs.dbTunnelConfig, options);
39
30
  }
40
- function psqlQueryOptions(query, dbEnv, cmdArgs = []) {
31
+ export function psqlQueryOptions(query, dbEnv, cmdArgs = []) {
41
32
  pgDebug('Running query: %s', query.trim());
42
33
  const psqlArgs = ['-c', query, '--set', 'sslmode=require', ...cmdArgs];
43
34
  const childProcessOptions = {
@@ -49,10 +40,13 @@ function psqlQueryOptions(query, dbEnv, cmdArgs = []) {
49
40
  psqlArgs,
50
41
  };
51
42
  }
52
- function execPSQL({ childProcessOptions, dbEnv, psqlArgs }) {
53
- const options = Object.assign({ env: dbEnv }, childProcessOptions);
43
+ export function execPSQL({ childProcessOptions, dbEnv, psqlArgs }) {
44
+ const options = {
45
+ env: dbEnv,
46
+ ...childProcessOptions,
47
+ };
54
48
  pgDebug('opening psql process');
55
- const psql = (0, node_child_process_1.spawn)('psql', psqlArgs, options);
49
+ const psql = spawn('psql', psqlArgs, options);
56
50
  psql.once('spawn', () => pgDebug('psql process spawned'));
57
51
  return psql;
58
52
  }
@@ -66,7 +60,7 @@ function kill(childProcess, signal) {
66
60
  childProcess.kill(signal);
67
61
  }
68
62
  }
69
- async function runWithTunnel(db, tunnelConfig, options) {
63
+ export async function runWithTunnel(db, tunnelConfig, options) {
70
64
  const tunnel = await Tunnel.connect(db, tunnelConfig);
71
65
  pgDebug('after create tunnel');
72
66
  const psql = execPSQL(options);
@@ -77,7 +71,7 @@ async function runWithTunnel(db, tunnelConfig, options) {
77
71
  // return a string for consistency but ideally we should return the child process from this function
78
72
  // and let the caller decide what to do with stdin/stdout/stderr
79
73
  const stdoutPromise = psql.stdout ? consumeStream(psql.stdout) : Promise.resolve('');
80
- const cleanupSignalTraps = (0, exports.trapAndForwardSignalsToChildProcess)(psql);
74
+ const cleanupSignalTraps = trapAndForwardSignalsToChildProcess(psql);
81
75
  try {
82
76
  pgDebug('waiting for psql or tunnel to exit');
83
77
  // wait for either psql or tunnel to exit;
@@ -108,7 +102,7 @@ async function runWithTunnel(db, tunnelConfig, options) {
108
102
  // This code is to stop the parent node process (heroku CLI)
109
103
  // from exiting. If the parent Heroku CLI node process exits, then psql will exit as it
110
104
  // is a child process of the Heroku CLI node process.
111
- const trapAndForwardSignalsToChildProcess = (childProcess) => {
105
+ export const trapAndForwardSignalsToChildProcess = (childProcess) => {
112
106
  const signalsToTrap = ['SIGINT'];
113
107
  const signalTraps = signalsToTrap.map(signal => {
114
108
  process.removeAllListeners(signal);
@@ -123,11 +117,10 @@ const trapAndForwardSignalsToChildProcess = (childProcess) => {
123
117
  }
124
118
  };
125
119
  };
126
- exports.trapAndForwardSignalsToChildProcess = trapAndForwardSignalsToChildProcess;
127
- async function waitForPSQLExit(psql) {
120
+ export async function waitForPSQLExit(psql) {
128
121
  let errorToThrow = null;
129
122
  try {
130
- const [exitCode] = await (0, node_events_1.once)(psql, 'close');
123
+ const [exitCode] = await once(psql, 'close');
131
124
  pgDebug(`psql exited with code ${exitCode}`);
132
125
  if (exitCode > 0) {
133
126
  errorToThrow = new Error(`psql exited with code ${exitCode}`);
@@ -147,14 +140,16 @@ async function waitForPSQLExit(psql) {
147
140
  // a small wrapper around tunnel-ssh
148
141
  // so that other code doesn't have to worry about
149
142
  // whether there is or is not a tunnel
150
- class Tunnel {
143
+ export class Tunnel {
144
+ bastionTunnel;
145
+ events;
151
146
  constructor(bastionTunnel) {
152
147
  this.bastionTunnel = bastionTunnel;
153
148
  // eslint-disable-next-line unicorn/prefer-event-target
154
- this.events = new node_events_1.EventEmitter();
149
+ this.events = new EventEmitter();
155
150
  }
156
151
  static async connect(db, tunnelConfig) {
157
- const tunnel = await (0, bastion_1.sshTunnel)(db, tunnelConfig);
152
+ const tunnel = await sshTunnel(db, tunnelConfig);
158
153
  return new Tunnel(tunnel);
159
154
  }
160
155
  close() {
@@ -171,7 +166,7 @@ class Tunnel {
171
166
  if (this.bastionTunnel) {
172
167
  try {
173
168
  pgDebug('wait for tunnel close');
174
- await (0, node_events_1.once)(this.bastionTunnel, 'close');
169
+ await once(this.bastionTunnel, 'close');
175
170
  pgDebug('tunnel closed');
176
171
  }
177
172
  catch (error) {
@@ -181,8 +176,7 @@ class Tunnel {
181
176
  }
182
177
  else {
183
178
  pgDebug('no bastion required; waiting for fake close event');
184
- await (0, node_events_1.once)(this.events, 'close');
179
+ await once(this.events, 'close');
185
180
  }
186
181
  }
187
182
  }
188
- exports.Tunnel = Tunnel;
@@ -1 +1,9 @@
1
- export declare function confirm(message: string): Promise<boolean>;
1
+ export type PromptInputs<T> = {
2
+ /**
3
+ * default value to offer to the user. Will be used if the user does not respond within the timeout period.
4
+ */
5
+ defaultAnswer?: T;
6
+ /** after this many ms, the prompt will time out. If a default value is provided, the default will be used. Otherwise the prompt will throw an error */
7
+ ms?: number;
8
+ };
9
+ export declare const confirm: (message: string, { defaultAnswer, ms, }?: PromptInputs<boolean>) => Promise<boolean>;
@@ -1,7 +1,37 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.confirm = confirm;
4
- const core_1 = require("@oclif/core");
5
- async function confirm(message) {
6
- return core_1.ux.confirm(message);
7
- }
1
+ import { ux } from '@oclif/core';
2
+ import { createPromptModule } from 'inquirer';
3
+ const prompt = createPromptModule();
4
+ export const confirm = async (message, { defaultAnswer = false, ms = 10_000, } = {}) => {
5
+ let timeoutId;
6
+ const promptPromise = prompt([{
7
+ default: defaultAnswer,
8
+ message,
9
+ name: 'answer',
10
+ type: 'confirm',
11
+ }]).then(({ answer }) => {
12
+ if (timeoutId)
13
+ clearTimeout(timeoutId);
14
+ return answer;
15
+ });
16
+ const timeoutPromise = new Promise((resolve, reject) => {
17
+ timeoutId = setTimeout(() => {
18
+ if (defaultAnswer === undefined) {
19
+ reject(ux.error('Prompt timed out'));
20
+ }
21
+ else {
22
+ // Force the process to continue with the default answer
23
+ process.stdin.push(null);
24
+ // Clean up stdin
25
+ process.stdin.pause();
26
+ resolve(defaultAnswer);
27
+ }
28
+ }, ms);
29
+ });
30
+ try {
31
+ return await Promise.race([promptPromise, timeoutPromise]);
32
+ }
33
+ finally {
34
+ if (timeoutId)
35
+ clearTimeout(timeoutId);
36
+ }
37
+ };
@@ -1,2 +1,7 @@
1
- import { ux } from '@oclif/core';
2
- export declare function prompt(name: string, options?: ux.IPromptOptions): Promise<string>;
1
+ type PromptOptions = {
2
+ default?: string;
3
+ required?: boolean;
4
+ type?: string;
5
+ };
6
+ export declare function prompt(name: string, options?: PromptOptions): Promise<string>;
7
+ export {};
package/dist/ux/prompt.js CHANGED
@@ -1,7 +1,11 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.prompt = prompt;
4
- const core_1 = require("@oclif/core");
5
- async function prompt(name, options) {
6
- return core_1.ux.prompt(name, options);
1
+ import inquirer from 'inquirer';
2
+ export async function prompt(name, options) {
3
+ const { answer } = await inquirer.prompt([{
4
+ default: options?.default,
5
+ message: name,
6
+ name: 'answer',
7
+ type: options?.type ?? 'input',
8
+ validate: options?.required ? (input) => input.length > 0 || 'This field is required' : undefined,
9
+ }]);
10
+ return answer;
7
11
  }
@@ -1,7 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.styledHeader = styledHeader;
4
- const core_1 = require("@oclif/core");
5
- function styledHeader(header) {
6
- return core_1.ux.styledHeader(header);
1
+ import { color } from '@heroku-cli/color';
2
+ import { ux } from '@oclif/core';
3
+ export function styledHeader(header) {
4
+ return ux.stdout(color.dim('=== ') + color.bold(header) + '\n');
7
5
  }
@@ -1,7 +1,4 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.styledJSON = styledJSON;
4
- const core_1 = require("@oclif/core");
5
- function styledJSON(obj) {
6
- return core_1.ux.styledJSON(obj);
1
+ import { ux } from '@oclif/core';
2
+ export function styledJSON(obj) {
3
+ ux.stdout(ux.colorizeJson(obj));
7
4
  }
@@ -1 +1 @@
1
- export declare function styledObject(obj: unknown, keys?: string[]): void;
1
+ export declare function styledObject(obj: unknown, keys?: string[]): string | undefined;
@@ -1,7 +1,49 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.styledObject = styledObject;
4
- const core_1 = require("@oclif/core");
5
- function styledObject(obj, keys) {
6
- return core_1.ux.styledObject(obj, keys);
1
+ import { color } from '@heroku-cli/color';
2
+ import { ux } from '@oclif/core';
3
+ import { inspect } from 'node:util';
4
+ function prettyPrint(obj) {
5
+ if (!obj)
6
+ return inspect(obj);
7
+ if (typeof obj === 'string')
8
+ return obj;
9
+ if (typeof obj === 'number')
10
+ return obj.toString();
11
+ if (typeof obj === 'boolean')
12
+ return obj.toString();
13
+ if (typeof obj === 'object') {
14
+ return Object.entries(obj)
15
+ .map(([key, value]) => `${key}: ${inspect(value)}`)
16
+ .join(', ');
17
+ }
18
+ return inspect(obj);
19
+ }
20
+ export function styledObject(obj, keys) {
21
+ if (!obj)
22
+ return inspect(obj);
23
+ if (typeof obj === 'string')
24
+ return obj;
25
+ if (typeof obj === 'number')
26
+ return obj.toString();
27
+ if (typeof obj === 'boolean')
28
+ return obj.toString();
29
+ const output = [];
30
+ const keyLengths = Object.keys(obj).map(key => key.toString().length);
31
+ const maxKeyLength = Math.max(...keyLengths) + 2;
32
+ const logKeyValue = (key, value) => `${color.cyan(key)}:` + ' '.repeat(maxKeyLength - key.length - 1) + prettyPrint(value);
33
+ for (const [key, value] of Object.entries(obj)) {
34
+ if (keys && !keys.includes(key))
35
+ continue;
36
+ if (Array.isArray(value)) {
37
+ if (value.length > 0) {
38
+ output.push(logKeyValue(key, value[0]));
39
+ for (const e of value.slice(1)) {
40
+ output.push(' '.repeat(maxKeyLength) + prettyPrint(e));
41
+ }
42
+ }
43
+ }
44
+ else if (value !== null && value !== undefined) {
45
+ output.push(logKeyValue(key, value));
46
+ }
47
+ }
48
+ ux.stdout(output.join('\n'));
7
49
  }
@@ -1,2 +1,22 @@
1
- import { ux } from '@oclif/core';
2
- export declare function table<T extends Record<string, unknown>>(data: T[], columns: ux.Table.table.Columns<T>, options?: ux.Table.table.Options): void;
1
+ type Column<T extends Record<string, unknown>> = {
2
+ extended: boolean;
3
+ get(row: T): unknown;
4
+ header: string;
5
+ minWidth: number;
6
+ };
7
+ type Columns<T extends Record<string, unknown>> = {
8
+ [key: string]: Partial<Column<T>>;
9
+ };
10
+ type Options = {
11
+ columns?: string;
12
+ extended?: boolean;
13
+ filter?: string;
14
+ 'no-header'?: boolean;
15
+ 'no-truncate'?: boolean;
16
+ printLine?(s: unknown): void;
17
+ rowStart?: string;
18
+ sort?: string;
19
+ title?: string;
20
+ };
21
+ export declare function table<T extends Record<string, unknown>>(data: T[], columns: Columns<T>, options?: Options): void;
22
+ export {};
package/dist/ux/table.js CHANGED
@@ -1,7 +1,15 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.table = table;
4
- const core_1 = require("@oclif/core");
5
- function table(data, columns, options) {
6
- return core_1.ux.table(data, columns, options);
1
+ import { printTable } from '@oclif/table';
2
+ export function table(data, columns, options) {
3
+ const cols = Object.entries(columns).map(([key, opts]) => {
4
+ if (opts.header)
5
+ return { key, name: opts.header };
6
+ return key;
7
+ });
8
+ const d = data.map(row => Object.fromEntries(Object.entries(columns).map(([key, { get }]) => [key, get ? get(row) : row[key]])));
9
+ printTable({
10
+ borderStyle: 'headers-only-with-underline',
11
+ columns: cols,
12
+ data: d,
13
+ title: options?.title,
14
+ });
7
15
  }
package/dist/ux/wait.js CHANGED
@@ -1,7 +1,5 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.wait = wait;
4
- const core_1 = require("@oclif/core");
5
- function wait(ms) {
6
- return core_1.ux.wait(ms);
1
+ export function wait(ms) {
2
+ return new Promise(resolve => {
3
+ setTimeout(resolve, ms);
4
+ });
7
5
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "type": "commonjs",
2
+ "type": "module",
3
3
  "name": "@heroku/heroku-cli-util",
4
- "version": "9.0.1",
4
+ "version": "10.0.0-beta.0",
5
5
  "description": "Set of helpful CLI utilities",
6
6
  "author": "Heroku",
7
7
  "license": "ISC",
@@ -11,13 +11,17 @@
11
11
  "dist"
12
12
  ],
13
13
  "devDependencies": {
14
+ "@heroku-cli/test-utils": "0.1.1",
14
15
  "@heroku-cli/schema": "^2.0.0",
16
+ "@types/chai": "^4.3.13",
15
17
  "@types/chai-as-promised": "^8.0.2",
16
18
  "@types/debug": "^4.1.12",
19
+ "@types/inquirer": "^9.0.8",
17
20
  "@types/mocha": "^10.0.10",
18
21
  "@types/node": "^22.15.3",
19
22
  "@types/sinon": "^17.0.4",
20
23
  "@types/tunnel-ssh": "4.1.1",
24
+ "chai": "^4.4.1",
21
25
  "chai-as-promised": "^8.0.1",
22
26
  "eslint": "^8.57.0",
23
27
  "eslint-config-oclif": "^5.0.0",
@@ -25,6 +29,8 @@
25
29
  "eslint-plugin-import": "^2.31.0",
26
30
  "eslint-plugin-mocha": "^10.4.3",
27
31
  "mocha": "^10.8.2",
32
+ "mock-stdin": "^1.0.0",
33
+ "nock": "^13.2.9",
28
34
  "nyc": "^17.1.0",
29
35
  "sinon": "^18.0.1",
30
36
  "strip-ansi": "^6",
@@ -34,40 +40,25 @@
34
40
  "typescript": "^5.4.0"
35
41
  },
36
42
  "dependencies": {
37
- "@types/chai": "^4.3.13",
38
43
  "@heroku-cli/color": "^2.0.4",
39
- "@heroku-cli/command": "^11.5.0",
44
+ "@heroku-cli/command": "^12.0.0",
40
45
  "@heroku/http-call": "^5.4.0",
41
- "@oclif/core": "^2.16.0",
42
- "chai": "^4.4.1",
46
+ "@oclif/core": "^4.3.0",
47
+ "@oclif/table": "0.4.8",
43
48
  "debug": "^4.4.0",
44
- "nock": "^13.2.9",
45
- "stdout-stderr": "^0.1.13",
49
+ "inquirer": "^12.6.1",
46
50
  "tunnel-ssh": "4.1.6"
47
51
  },
48
52
  "engines": {
49
53
  "node": ">=20"
50
54
  },
51
- "mocha": {
52
- "require": [
53
- "ts-node/register",
54
- "source-map-support/register",
55
- "test/hooks.ts"
56
- ],
57
- "file": [
58
- "src/test-helpers/stub-output.ts"
59
- ],
60
- "watch-extensions": "ts",
61
- "recursive": true,
62
- "reporter": "spec",
63
- "timeout": 360000
64
- },
65
55
  "scripts": {
66
56
  "build": "npm run clean && tsc",
67
57
  "clean": "rm -rf dist",
68
- "lint": "eslint . --ext .ts --config .eslintrc.js",
58
+ "example": "sh examples/run.sh",
59
+ "lint": "eslint . --ext .ts --config .eslintrc.cjs",
69
60
  "prepare": "npm run build",
70
- "test": "nyc mocha --forbid-only \"test/**/*.test.ts\"",
71
- "test:local": "mocha \"test/**/*.test.ts\""
61
+ "test": "nyc mocha --forbid-only \"test/**/*.test.ts\"",
62
+ "test:local": "nyc mocha \"${npm_config_file:-test/**/*.test.+(ts|tsx)}\""
72
63
  }
73
64
  }
@@ -1,2 +0,0 @@
1
- declare const expectOutput: (actual: string, expected: string) => Chai.Assertion;
2
- export default expectOutput;
@@ -1,16 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const chai_1 = require("chai");
4
- function stripIndents(str) {
5
- str = str.trim().replace(/\s+$/gm, '');
6
- const indent = (str.match(/^\s+[^$]/m) || [''])[0].length - 1;
7
- const regexp = new RegExp(`^s{${indent}}`, 'mg');
8
- return str.replace(regexp, '');
9
- }
10
- const expectOutput = function (actual, expected) {
11
- // it can be helpful to strip all hyphens & spaces when migrating tests before perfecting
12
- // use `.replace(/[\s─]/g, '')` on both actual & expected until tests pass, then remove, and paste actual into expected
13
- return (0, chai_1.expect)(actual.trim().replace(/\s+$/gm, ''))
14
- .to.equal(stripIndents(expected));
15
- };
16
- exports.default = expectOutput;
@@ -1 +0,0 @@
1
- export declare function initCliTest(): void;
@@ -1,17 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.initCliTest = initCliTest;
4
- const path = require('node:path');
5
- const { color } = require('@heroku-cli/color');
6
- const nock = require('nock');
7
- function initCliTest() {
8
- // eslint-disable-next-line no-multi-assign
9
- process.env.TS_NODE_PROJECT = path.resolve('test/tsconfig.json');
10
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
11
- global.columns = '120';
12
- color.enabled = false;
13
- nock.disableNetConnect();
14
- if (process.env.ENABLE_NET_CONNECT === 'true') {
15
- nock.enableNetConnect();
16
- }
17
- }
@@ -1,9 +0,0 @@
1
- import { Command } from '@heroku-cli/command';
2
- import { APIClient } from '@heroku-cli/command';
3
- import { Config } from '@oclif/core';
4
- type CmdConstructorParams = ConstructorParameters<typeof Command>;
5
- export type GenericCmd = new (...args: CmdConstructorParams) => Command;
6
- export declare const runCommand: (Cmd: GenericCmd, args?: string[], printStd?: boolean) => Promise<any>;
7
- export declare const getConfig: () => Promise<Config>;
8
- export declare const getHerokuAPI: () => Promise<APIClient>;
9
- export {};
@@ -1,43 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getHerokuAPI = exports.getConfig = exports.runCommand = void 0;
4
- const command_1 = require("@heroku-cli/command");
5
- const core_1 = require("@oclif/core");
6
- const stdout_stderr_1 = require("stdout-stderr");
7
- const stopMock = () => {
8
- stdout_stderr_1.stdout.stop();
9
- stdout_stderr_1.stderr.stop();
10
- };
11
- const runCommand = async (Cmd, args = [], printStd = false) => {
12
- const conf = await (0, exports.getConfig)();
13
- const instance = new Cmd(args, conf);
14
- if (printStd) {
15
- stdout_stderr_1.stdout.print = true;
16
- stdout_stderr_1.stderr.print = true;
17
- }
18
- stdout_stderr_1.stdout.start();
19
- stdout_stderr_1.stderr.start();
20
- return instance
21
- .run()
22
- .then(args => {
23
- stopMock();
24
- return args;
25
- })
26
- .catch((error) => {
27
- stopMock();
28
- throw error;
29
- });
30
- };
31
- exports.runCommand = runCommand;
32
- const getConfig = async () => {
33
- const pjsonPath = require.resolve('../../package.json');
34
- const conf = new core_1.Config({ root: pjsonPath });
35
- await conf.load();
36
- return conf;
37
- };
38
- exports.getConfig = getConfig;
39
- const getHerokuAPI = async () => {
40
- const conf = await (0, exports.getConfig)();
41
- return new command_1.APIClient(conf);
42
- };
43
- exports.getHerokuAPI = getHerokuAPI;
@@ -1,4 +0,0 @@
1
- export declare function setupStdoutStderr(): void;
2
- export declare function restoreStdoutStderr(): void;
3
- export declare function stdout(): string;
4
- export declare function stderr(): string;
@@ -1,35 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupStdoutStderr = setupStdoutStderr;
4
- exports.restoreStdoutStderr = restoreStdoutStderr;
5
- exports.stdout = stdout;
6
- exports.stderr = stderr;
7
- const sinon = require("sinon");
8
- let stdoutWriteStub;
9
- let stdoutOutput = '';
10
- let stderrWriteStub;
11
- let stderrOutput = '';
12
- function setupStdoutStderr() {
13
- stdoutOutput = '';
14
- stdoutWriteStub = sinon.stub(process.stdout, 'write').callsFake((str) => {
15
- stdoutOutput += str.toString();
16
- return true;
17
- });
18
- stderrOutput = '';
19
- stderrWriteStub = sinon.stub(process.stderr, 'write').callsFake((str) => {
20
- stderrOutput += str.toString();
21
- return true;
22
- });
23
- }
24
- function restoreStdoutStderr() {
25
- stdoutWriteStub.restore();
26
- stdoutOutput = '';
27
- stderrWriteStub.restore();
28
- stderrOutput = '';
29
- }
30
- function stdout() {
31
- return stdoutOutput;
32
- }
33
- function stderr() {
34
- return stderrOutput;
35
- }