@heroku/heroku-cli-util 9.0.2 → 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.
- package/dist/index.d.ts +14 -15
- package/dist/index.js +27 -30
- package/dist/types/errors/ambiguous.js +7 -7
- package/dist/types/errors/not-found.js +4 -11
- package/dist/types/pg/data-api.js +1 -2
- package/dist/types/pg/tunnel.d.ts +1 -1
- package/dist/types/pg/tunnel.js +1 -2
- package/dist/utils/addons/resolve.d.ts +1 -1
- package/dist/utils/addons/resolve.js +5 -9
- package/dist/utils/pg/bastion.d.ts +3 -3
- package/dist/utils/pg/bastion.js +31 -33
- package/dist/utils/pg/config-vars.d.ts +1 -1
- package/dist/utils/pg/config-vars.js +8 -14
- package/dist/utils/pg/databases.d.ts +2 -2
- package/dist/utils/pg/databases.js +34 -43
- package/dist/utils/pg/host.js +2 -5
- package/dist/utils/pg/psql.d.ts +1 -1
- package/dist/utils/pg/psql.js +31 -37
- package/dist/ux/confirm.d.ts +9 -1
- package/dist/ux/confirm.js +37 -7
- package/dist/ux/prompt.d.ts +7 -2
- package/dist/ux/prompt.js +10 -6
- package/dist/ux/styled-header.js +4 -6
- package/dist/ux/styled-json.js +3 -6
- package/dist/ux/styled-object.d.ts +1 -1
- package/dist/ux/styled-object.js +48 -6
- package/dist/ux/table.d.ts +22 -2
- package/dist/ux/table.js +14 -6
- package/dist/ux/wait.js +4 -6
- package/package.json +12 -19
package/dist/index.d.ts
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
import { AmbiguousError } from './types/errors/ambiguous';
|
|
2
|
-
import { NotFound } from './types/errors/not-found';
|
|
3
|
-
import { AddOnAttachmentWithConfigVarsAndPlan, AddOnWithRelatedData, Link } from './types/pg/data-api';
|
|
4
|
-
import { ConnectionDetails, ConnectionDetailsWithAttachment, TunnelConfig } from './types/pg/tunnel';
|
|
5
|
-
import { getDatabase } from './utils/pg/databases';
|
|
6
|
-
import getHost from './utils/pg/host';
|
|
7
|
-
import { exec } from './utils/pg/psql';
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
-
import {
|
|
13
|
-
import {
|
|
14
|
-
import { wait } from './ux/wait';
|
|
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';
|
|
15
14
|
export declare const types: {
|
|
16
15
|
errors: {
|
|
17
16
|
AmbiguousError: typeof AmbiguousError;
|
|
@@ -36,7 +35,7 @@ export declare const utils: {
|
|
|
36
35
|
};
|
|
37
36
|
};
|
|
38
37
|
export declare const hux: {
|
|
39
|
-
confirm:
|
|
38
|
+
confirm: (message: string, { defaultAnswer, ms, }?: import("./ux/confirm.js").PromptInputs<boolean>) => Promise<boolean>;
|
|
40
39
|
prompt: typeof prompt;
|
|
41
40
|
styledHeader: typeof styledHeader;
|
|
42
41
|
styledJSON: typeof styledJSON;
|
package/dist/index.js
CHANGED
|
@@ -1,22 +1,19 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
const
|
|
14
|
-
const table_1 = require("./ux/table");
|
|
15
|
-
const wait_1 = require("./ux/wait");
|
|
16
|
-
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 = {
|
|
17
14
|
errors: {
|
|
18
|
-
AmbiguousError
|
|
19
|
-
NotFound
|
|
15
|
+
AmbiguousError,
|
|
16
|
+
NotFound,
|
|
20
17
|
},
|
|
21
18
|
pg: {
|
|
22
19
|
AddOnAttachmentWithConfigVarsAndPlan: {},
|
|
@@ -27,21 +24,21 @@ exports.types = {
|
|
|
27
24
|
TunnelConfig: {},
|
|
28
25
|
},
|
|
29
26
|
};
|
|
30
|
-
|
|
27
|
+
export const utils = {
|
|
31
28
|
pg: {
|
|
32
|
-
databases:
|
|
33
|
-
host:
|
|
29
|
+
databases: getDatabase,
|
|
30
|
+
host: getHost,
|
|
34
31
|
psql: {
|
|
35
|
-
exec
|
|
32
|
+
exec,
|
|
36
33
|
},
|
|
37
34
|
},
|
|
38
35
|
};
|
|
39
|
-
|
|
40
|
-
confirm
|
|
41
|
-
prompt
|
|
42
|
-
styledHeader
|
|
43
|
-
styledJSON
|
|
44
|
-
styledObject
|
|
45
|
-
table
|
|
46
|
-
wait
|
|
36
|
+
export const hux = {
|
|
37
|
+
confirm,
|
|
38
|
+
prompt,
|
|
39
|
+
styledHeader,
|
|
40
|
+
styledJSON,
|
|
41
|
+
styledObject,
|
|
42
|
+
table,
|
|
43
|
+
wait,
|
|
47
44
|
};
|
|
@@ -1,14 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
-
|
|
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;
|
package/dist/types/pg/tunnel.js
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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
|
|
25
|
+
throw new NotFound();
|
|
30
26
|
}
|
|
31
27
|
case 1: {
|
|
32
28
|
return matches[0];
|
|
33
29
|
}
|
|
34
30
|
default: {
|
|
35
|
-
throw new
|
|
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;
|
package/dist/utils/pg/bastion.js
CHANGED
|
@@ -1,21 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
const
|
|
9
|
-
const
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
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: (
|
|
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
|
-
|
|
54
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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() * (
|
|
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
|
|
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
|
-
|
|
2
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
const
|
|
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) =>
|
|
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
|
-
|
|
35
|
+
getConfig(heroku, app),
|
|
42
36
|
allAttachments(heroku, app),
|
|
43
37
|
]);
|
|
44
38
|
if (attachments.length === 0) {
|
|
45
|
-
throw new Error(`${
|
|
39
|
+
throw new Error(`${color.app(app)} has no databases`);
|
|
46
40
|
}
|
|
47
|
-
matches = attachments.filter(attachment => config[db] && config[db] === config[
|
|
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 =>
|
|
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 =>
|
|
57
|
-
const config =
|
|
58
|
-
if (matches.every(match => config[
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
95
|
-
const database =
|
|
96
|
-
if (
|
|
97
|
-
const { body: bastionConfig } = await
|
|
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
|
-
|
|
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
|
-
(
|
|
100
|
+
debug(`addon service: ${addonService}`);
|
|
109
101
|
try {
|
|
110
|
-
const attached = await
|
|
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
|
|
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
|
|
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 ||
|
|
124
|
+
port: port || env.PGPORT || (hostname && '5432'),
|
|
133
125
|
url: dbPath,
|
|
134
126
|
user: username,
|
|
135
127
|
};
|
|
136
128
|
};
|
|
137
|
-
exports.parsePostgresConnectionString = parsePostgresConnectionString;
|
package/dist/utils/pg/host.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
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
|
|
3
|
+
return host ?? 'api.data.heroku.com';
|
|
7
4
|
}
|
package/dist/utils/pg/psql.d.ts
CHANGED
|
@@ -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[]): {
|
package/dist/utils/pg/psql.js
CHANGED
|
@@ -1,26 +1,17 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
127
|
-
async function waitForPSQLExit(psql) {
|
|
120
|
+
export async function waitForPSQLExit(psql) {
|
|
128
121
|
let errorToThrow = null;
|
|
129
122
|
try {
|
|
130
|
-
const [exitCode] = await
|
|
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
|
|
149
|
+
this.events = new EventEmitter();
|
|
155
150
|
}
|
|
156
151
|
static async connect(db, tunnelConfig) {
|
|
157
|
-
const tunnel = await
|
|
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
|
|
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
|
|
179
|
+
await once(this.events, 'close');
|
|
185
180
|
}
|
|
186
181
|
}
|
|
187
182
|
}
|
|
188
|
-
exports.Tunnel = Tunnel;
|
package/dist/ux/confirm.d.ts
CHANGED
|
@@ -1 +1,9 @@
|
|
|
1
|
-
export
|
|
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>;
|
package/dist/ux/confirm.js
CHANGED
|
@@ -1,7 +1,37 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
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
|
+
};
|
package/dist/ux/prompt.d.ts
CHANGED
|
@@ -1,2 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
}
|
package/dist/ux/styled-header.js
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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
|
}
|
package/dist/ux/styled-json.js
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
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[]):
|
|
1
|
+
export declare function styledObject(obj: unknown, keys?: string[]): string | undefined;
|
package/dist/ux/styled-object.js
CHANGED
|
@@ -1,7 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
}
|
package/dist/ux/table.d.ts
CHANGED
|
@@ -1,2 +1,22 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
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": "
|
|
2
|
+
"type": "module",
|
|
3
3
|
"name": "@heroku/heroku-cli-util",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "10.0.0-beta.0",
|
|
5
5
|
"description": "Set of helpful CLI utilities",
|
|
6
6
|
"author": "Heroku",
|
|
7
7
|
"license": "ISC",
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
"@types/chai": "^4.3.13",
|
|
17
17
|
"@types/chai-as-promised": "^8.0.2",
|
|
18
18
|
"@types/debug": "^4.1.12",
|
|
19
|
+
"@types/inquirer": "^9.0.8",
|
|
19
20
|
"@types/mocha": "^10.0.10",
|
|
20
21
|
"@types/node": "^22.15.3",
|
|
21
22
|
"@types/sinon": "^17.0.4",
|
|
@@ -28,10 +29,10 @@
|
|
|
28
29
|
"eslint-plugin-import": "^2.31.0",
|
|
29
30
|
"eslint-plugin-mocha": "^10.4.3",
|
|
30
31
|
"mocha": "^10.8.2",
|
|
32
|
+
"mock-stdin": "^1.0.0",
|
|
31
33
|
"nock": "^13.2.9",
|
|
32
34
|
"nyc": "^17.1.0",
|
|
33
35
|
"sinon": "^18.0.1",
|
|
34
|
-
"stdout-stderr": "^0.1.13",
|
|
35
36
|
"strip-ansi": "^6",
|
|
36
37
|
"ts-node": "^10.9.2",
|
|
37
38
|
"tsconfig-paths": "^4.2.0",
|
|
@@ -40,32 +41,24 @@
|
|
|
40
41
|
},
|
|
41
42
|
"dependencies": {
|
|
42
43
|
"@heroku-cli/color": "^2.0.4",
|
|
43
|
-
"@heroku-cli/command": "^
|
|
44
|
+
"@heroku-cli/command": "^12.0.0",
|
|
44
45
|
"@heroku/http-call": "^5.4.0",
|
|
45
|
-
"@oclif/core": "^
|
|
46
|
+
"@oclif/core": "^4.3.0",
|
|
47
|
+
"@oclif/table": "0.4.8",
|
|
46
48
|
"debug": "^4.4.0",
|
|
49
|
+
"inquirer": "^12.6.1",
|
|
47
50
|
"tunnel-ssh": "4.1.6"
|
|
48
51
|
},
|
|
49
52
|
"engines": {
|
|
50
53
|
"node": ">=20"
|
|
51
54
|
},
|
|
52
|
-
"mocha": {
|
|
53
|
-
"require": [
|
|
54
|
-
"ts-node/register",
|
|
55
|
-
"source-map-support/register",
|
|
56
|
-
"test/hooks.ts"
|
|
57
|
-
],
|
|
58
|
-
"watch-extensions": "ts",
|
|
59
|
-
"recursive": true,
|
|
60
|
-
"reporter": "spec",
|
|
61
|
-
"timeout": 360000
|
|
62
|
-
},
|
|
63
55
|
"scripts": {
|
|
64
56
|
"build": "npm run clean && tsc",
|
|
65
57
|
"clean": "rm -rf dist",
|
|
66
|
-
"
|
|
58
|
+
"example": "sh examples/run.sh",
|
|
59
|
+
"lint": "eslint . --ext .ts --config .eslintrc.cjs",
|
|
67
60
|
"prepare": "npm run build",
|
|
68
|
-
"test": "nyc mocha
|
|
69
|
-
"test:local": "mocha \"test/**/*.test
|
|
61
|
+
"test": "nyc mocha --forbid-only \"test/**/*.test.ts\"",
|
|
62
|
+
"test:local": "nyc mocha \"${npm_config_file:-test/**/*.test.+(ts|tsx)}\""
|
|
70
63
|
}
|
|
71
64
|
}
|