@get-dx/cli 0.2.1 → 0.3.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/README.md +28 -15
- package/dist/cli.js +2 -0
- package/dist/commands/auth.js +27 -22
- package/dist/commands/catalog/entities.js +6 -6
- package/dist/commands/catalog/entitiesRendering.js +13 -9
- package/dist/commands/catalog/entityTypes.js +4 -4
- package/dist/commands/catalog/entityTypesRendering.js +15 -21
- package/dist/commands/init.js +15 -60
- package/dist/commands/scorecards.js +4 -4
- package/dist/commands/scorecardsRendering.js +8 -5
- package/dist/commands/studio/query.js +2 -2
- package/dist/config.js +171 -29
- package/dist/http.js +1 -1
- package/dist/loginViaBrowser.js +5 -5
- package/dist/runtime.js +11 -7
- package/dist/ui.js +3 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,31 +18,44 @@ Interactively login and install the AI agent skill:
|
|
|
18
18
|
dx init
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
Show help text:
|
|
22
|
+
|
|
23
|
+
```shell
|
|
24
|
+
dx --help
|
|
25
|
+
dx <subcommand> --help
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Customizing base URLs
|
|
22
29
|
|
|
23
|
-
|
|
30
|
+
The CLI requires two base URLs to be configured. The default values are used for DX **cloud** deployments. Users of **dedicated** and **managed** deployments will need to specify these values explicitly when logging in.
|
|
24
31
|
|
|
25
|
-
|
|
32
|
+
| Value | How it is used | Env var | Default value |
|
|
33
|
+
| ---------------- | -------------------------------------------- | ----------------- | ----------------------- |
|
|
34
|
+
| **Web base URL** | Browser-based login and displaying web links | `DX_WEB_BASE_URL` | `https://app.getdx.com` |
|
|
35
|
+
| **API base URL** | Making each API request to DX | `DX_API_BASE_URL` | `https://api.getdx.com` |
|
|
36
|
+
|
|
37
|
+
#### For dedicated deployments
|
|
38
|
+
|
|
39
|
+
Set the env vars once when initializing:
|
|
26
40
|
|
|
27
41
|
```shell
|
|
28
|
-
#
|
|
29
|
-
|
|
42
|
+
# Interactive login
|
|
43
|
+
DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" dx init
|
|
30
44
|
|
|
31
|
-
#
|
|
32
|
-
|
|
45
|
+
# Non-interactive login for CI or remote agents
|
|
46
|
+
DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" dx auth login --token $DX_TOKEN
|
|
33
47
|
```
|
|
34
48
|
|
|
35
|
-
|
|
49
|
+
#### For managed deployments
|
|
36
50
|
|
|
37
|
-
|
|
38
|
-
- `<account>.getdx.io` — DX dedicated (derives `https://api.<account>.getdx.io` automatically)
|
|
39
|
-
- Any custom domain — you will be prompted for the separate API base URL
|
|
40
|
-
|
|
41
|
-
Show help text:
|
|
51
|
+
Set the env vars once when initializing:
|
|
42
52
|
|
|
43
53
|
```shell
|
|
44
|
-
|
|
45
|
-
dx
|
|
54
|
+
# Interactive login
|
|
55
|
+
DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" dx init
|
|
56
|
+
|
|
57
|
+
# Non-interactive login for CI or remote agents
|
|
58
|
+
DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" dx auth login --token $DX_TOKEN
|
|
46
59
|
```
|
|
47
60
|
|
|
48
61
|
## Logging
|
package/dist/cli.js
CHANGED
|
@@ -8,8 +8,10 @@ import { teamsCommand } from "./commands/teams.js";
|
|
|
8
8
|
import { workflowsCommand } from "./commands/workflows.js";
|
|
9
9
|
import { handleError } from "./commandHelpers.js";
|
|
10
10
|
import cliPackage from "../package.json" with { type: "json" };
|
|
11
|
+
import { handleTemporaryBaseUrlMigration } from "./config.js";
|
|
11
12
|
export async function run(argv = process.argv) {
|
|
12
13
|
try {
|
|
14
|
+
handleTemporaryBaseUrlMigration(argv);
|
|
13
15
|
const program = createProgram();
|
|
14
16
|
if (argv.length <= 2) {
|
|
15
17
|
program.outputHelp();
|
package/dist/commands/auth.js
CHANGED
|
@@ -4,12 +4,11 @@ import { deleteToken, setToken } from "../secrets.js";
|
|
|
4
4
|
import { renderJson } from "../renderers.js";
|
|
5
5
|
import { renderAuthInfo, renderLoggedOut } from "./authRendering.js";
|
|
6
6
|
import { getContext, wrapAction } from "../commandHelpers.js";
|
|
7
|
-
import {
|
|
7
|
+
import { persistBaseUrls, resolveApiBaseUrl, deriveBaseUrlsFromEnv, } from "../config.js";
|
|
8
8
|
import { CliError } from "../errors.js";
|
|
9
9
|
import { request } from "../http.js";
|
|
10
10
|
import { loginViaBrowser } from "../loginViaBrowser.js";
|
|
11
|
-
import {
|
|
12
|
-
import cliPackage from "../../package.json" with { type: "json" };
|
|
11
|
+
import { buildRuntime } from "../runtime.js";
|
|
13
12
|
import { maskToken } from "../ui.js";
|
|
14
13
|
export function authCommand() {
|
|
15
14
|
const auth = new Command()
|
|
@@ -20,7 +19,7 @@ export function authCommand() {
|
|
|
20
19
|
.option("--token <token>", "Account web API token or personal access token. Omit this option to login interactively by web browser or pasting.")
|
|
21
20
|
.action(wrapAction(async (commandOptions, command) => {
|
|
22
21
|
const context = getContext(command);
|
|
23
|
-
const
|
|
22
|
+
const { apiBaseUrl, webBaseUrl } = deriveBaseUrlsFromEnv();
|
|
24
23
|
let token = commandOptions.token;
|
|
25
24
|
if (!token) {
|
|
26
25
|
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
@@ -34,7 +33,7 @@ export function authCommand() {
|
|
|
34
33
|
],
|
|
35
34
|
});
|
|
36
35
|
if (method === "browser") {
|
|
37
|
-
token = await loginViaBrowser(
|
|
36
|
+
token = await loginViaBrowser(webBaseUrl);
|
|
38
37
|
}
|
|
39
38
|
else {
|
|
40
39
|
token = await password({
|
|
@@ -46,45 +45,51 @@ export function authCommand() {
|
|
|
46
45
|
throw new CliError("Account web API token or personal access token is required");
|
|
47
46
|
}
|
|
48
47
|
}
|
|
49
|
-
const runtime = {
|
|
50
|
-
|
|
48
|
+
const runtime = buildRuntime(context, {
|
|
49
|
+
apiBaseUrl,
|
|
51
50
|
token,
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
logger: buildLogger(context),
|
|
55
|
-
};
|
|
51
|
+
webBaseUrl,
|
|
52
|
+
});
|
|
56
53
|
const response = await getAuthInfo(runtime);
|
|
57
|
-
|
|
58
|
-
setToken(
|
|
54
|
+
persistBaseUrls(apiBaseUrl, webBaseUrl);
|
|
55
|
+
setToken(apiBaseUrl, token);
|
|
59
56
|
if (context.json) {
|
|
60
|
-
renderJson({
|
|
57
|
+
renderJson({
|
|
58
|
+
...response,
|
|
59
|
+
api_base_url: apiBaseUrl,
|
|
60
|
+
web_base_url: webBaseUrl,
|
|
61
|
+
});
|
|
61
62
|
return;
|
|
62
63
|
}
|
|
63
|
-
renderAuthInfo(response, token,
|
|
64
|
+
renderAuthInfo(response, token, apiBaseUrl);
|
|
64
65
|
}));
|
|
65
66
|
auth.command("logout").action(wrapAction(async (_options, command) => {
|
|
66
67
|
const context = getContext(command);
|
|
67
|
-
const
|
|
68
|
-
deleteToken(
|
|
68
|
+
const apiBaseUrl = resolveApiBaseUrl();
|
|
69
|
+
deleteToken(apiBaseUrl);
|
|
69
70
|
if (context.json) {
|
|
70
|
-
renderJson({ ok: true,
|
|
71
|
+
renderJson({ ok: true, api_base_url: apiBaseUrl, logged_out: true });
|
|
71
72
|
}
|
|
72
73
|
else {
|
|
73
|
-
renderLoggedOut(
|
|
74
|
+
renderLoggedOut(apiBaseUrl);
|
|
74
75
|
}
|
|
75
76
|
}));
|
|
76
|
-
auth
|
|
77
|
+
auth
|
|
78
|
+
.command("status")
|
|
79
|
+
.alias("info")
|
|
80
|
+
.action(wrapAction(async (_options, command) => {
|
|
77
81
|
const runtime = buildRuntime(getContext(command));
|
|
78
82
|
const response = await getAuthInfo(runtime);
|
|
79
83
|
if (runtime.context.json) {
|
|
80
84
|
renderJson({
|
|
81
85
|
...response,
|
|
82
86
|
token: maskToken(runtime.token),
|
|
83
|
-
|
|
87
|
+
api_base_url: runtime.apiBaseUrl,
|
|
88
|
+
web_base_url: runtime.webBaseUrl,
|
|
84
89
|
});
|
|
85
90
|
}
|
|
86
91
|
else {
|
|
87
|
-
renderAuthInfo(response, runtime.token, runtime.
|
|
92
|
+
renderAuthInfo(response, runtime.token, runtime.apiBaseUrl);
|
|
88
93
|
}
|
|
89
94
|
}));
|
|
90
95
|
return auth;
|
|
@@ -69,7 +69,7 @@ export function entitiesCommand() {
|
|
|
69
69
|
renderJson(response);
|
|
70
70
|
}
|
|
71
71
|
else {
|
|
72
|
-
renderEntity(response.entity);
|
|
72
|
+
renderEntity(response.entity, runtime);
|
|
73
73
|
}
|
|
74
74
|
}));
|
|
75
75
|
entities
|
|
@@ -135,7 +135,7 @@ export function entitiesCommand() {
|
|
|
135
135
|
renderJson(response);
|
|
136
136
|
}
|
|
137
137
|
else {
|
|
138
|
-
renderEntity(response.entity, `${ui.success(ui.GLYPHS.CHECK)} Entity updated`);
|
|
138
|
+
renderEntity(response.entity, runtime, `${ui.success(ui.GLYPHS.CHECK)} Entity updated`);
|
|
139
139
|
}
|
|
140
140
|
}));
|
|
141
141
|
entities
|
|
@@ -188,7 +188,7 @@ export function entitiesCommand() {
|
|
|
188
188
|
const title = response.result === "created_new_entity"
|
|
189
189
|
? `${ui.success(ui.GLYPHS.CHECK)} Created new entity`
|
|
190
190
|
: `${ui.success(ui.GLYPHS.CHECK)} Updated existing entity`;
|
|
191
|
-
renderEntity(response.entity, title);
|
|
191
|
+
renderEntity(response.entity, runtime, title);
|
|
192
192
|
}
|
|
193
193
|
}));
|
|
194
194
|
entities
|
|
@@ -217,7 +217,7 @@ export function entitiesCommand() {
|
|
|
217
217
|
renderJson(processedResponse);
|
|
218
218
|
}
|
|
219
219
|
else {
|
|
220
|
-
renderEntity(processedResponse.entity);
|
|
220
|
+
renderEntity(processedResponse.entity, runtime);
|
|
221
221
|
}
|
|
222
222
|
}));
|
|
223
223
|
entities
|
|
@@ -260,7 +260,7 @@ export function entitiesCommand() {
|
|
|
260
260
|
renderJson({ ...response, entities: processedEntities });
|
|
261
261
|
}
|
|
262
262
|
else {
|
|
263
|
-
renderEntityList(processedEntities, response.response_metadata?.next_cursor ?? null);
|
|
263
|
+
renderEntityList(processedEntities, response.response_metadata?.next_cursor ?? null, runtime);
|
|
264
264
|
}
|
|
265
265
|
}));
|
|
266
266
|
entities
|
|
@@ -326,7 +326,7 @@ export function entitiesCommand() {
|
|
|
326
326
|
renderJson(response);
|
|
327
327
|
}
|
|
328
328
|
else {
|
|
329
|
-
renderEntityScorecardList(response.scorecards, response.response_metadata?.next_cursor ?? null);
|
|
329
|
+
renderEntityScorecardList(response.scorecards, identifier, runtime, response.response_metadata?.next_cursor ?? null);
|
|
330
330
|
}
|
|
331
331
|
}));
|
|
332
332
|
return entities;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import dayjs from "dayjs";
|
|
2
2
|
import { renderRichText } from "../../renderers.js";
|
|
3
3
|
import * as ui from "../../ui.js";
|
|
4
|
-
export function renderEntityList(entities, nextCursor) {
|
|
4
|
+
export function renderEntityList(entities, nextCursor, runtime) {
|
|
5
5
|
const blocks = [ui.h1("Entities")];
|
|
6
6
|
blocks.push(ui.p(`Displaying ${ui.bold(entities.length.toString())} entities.`));
|
|
7
7
|
if (nextCursor) {
|
|
@@ -19,7 +19,7 @@ export function renderEntityList(entities, nextCursor) {
|
|
|
19
19
|
}
|
|
20
20
|
if (entity.identifier) {
|
|
21
21
|
blocks.push(ui.h3("Core attributes"));
|
|
22
|
-
blocks.push(...coreContent(entity));
|
|
22
|
+
blocks.push(...coreContent(entity, runtime));
|
|
23
23
|
}
|
|
24
24
|
if (entity.owner_teams || entity.owner_users) {
|
|
25
25
|
blocks.push(ui.h3("Owners"));
|
|
@@ -36,11 +36,11 @@ export function renderEntityList(entities, nextCursor) {
|
|
|
36
36
|
}
|
|
37
37
|
renderRichText(blocks);
|
|
38
38
|
}
|
|
39
|
-
export function renderEntity(entity, title = "Entity Information") {
|
|
39
|
+
export function renderEntity(entity, runtime, title = "Entity Information") {
|
|
40
40
|
renderRichText([
|
|
41
41
|
ui.h1(title),
|
|
42
42
|
entity.identifier
|
|
43
|
-
? [ui.h2("Core attributes"), ...coreContent(entity)]
|
|
43
|
+
? [ui.h2("Core attributes"), ...coreContent(entity, runtime)]
|
|
44
44
|
: null,
|
|
45
45
|
entity.owner_teams ? [ui.h2("Owners"), ...ownersContent(entity)] : null,
|
|
46
46
|
entity.properties
|
|
@@ -55,7 +55,7 @@ export function renderEntityDeleted(entity) {
|
|
|
55
55
|
ui.p(`The entity ${ui.code(entity.identifier)} has been deleted.`),
|
|
56
56
|
]);
|
|
57
57
|
}
|
|
58
|
-
export function renderEntityScorecardList(scorecards, nextCursor) {
|
|
58
|
+
export function renderEntityScorecardList(scorecards, entityIdentifier, runtime, nextCursor) {
|
|
59
59
|
const blocks = [ui.h1("Scorecards")];
|
|
60
60
|
blocks.push(ui.p(`Displaying ${ui.bold(scorecards.length.toString())} scorecards.`));
|
|
61
61
|
if (nextCursor) {
|
|
@@ -63,7 +63,7 @@ export function renderEntityScorecardList(scorecards, nextCursor) {
|
|
|
63
63
|
}
|
|
64
64
|
for (const scorecard of scorecards) {
|
|
65
65
|
blocks.push(ui.h2(scorecard.name));
|
|
66
|
-
blocks.push(...scorecardReportContent(scorecard));
|
|
66
|
+
blocks.push(...scorecardReportContent(scorecard, entityIdentifier, runtime));
|
|
67
67
|
}
|
|
68
68
|
renderRichText(blocks);
|
|
69
69
|
}
|
|
@@ -79,8 +79,9 @@ export function renderEntityTaskList(tasks, nextCursor) {
|
|
|
79
79
|
}
|
|
80
80
|
renderRichText(blocks);
|
|
81
81
|
}
|
|
82
|
-
function scorecardReportContent(scorecard) {
|
|
82
|
+
function scorecardReportContent(scorecard, entityIdentifier, runtime) {
|
|
83
83
|
const blocks = [];
|
|
84
|
+
blocks.push(ui.p(ui.link(ui.webLink(`/catalog/${entityIdentifier}/scorecards?expanded=${scorecard.id}`, runtime))));
|
|
84
85
|
if (scorecard.type === "POINTS") {
|
|
85
86
|
const total = scorecard.points_meta?.points_total ?? 0;
|
|
86
87
|
const achieved = scorecard.points_meta?.points_achieved ?? 0;
|
|
@@ -179,7 +180,7 @@ function dueDateText(completeBy) {
|
|
|
179
180
|
return formattedDate;
|
|
180
181
|
}
|
|
181
182
|
}
|
|
182
|
-
function coreContent(entity) {
|
|
183
|
+
function coreContent(entity, runtime) {
|
|
183
184
|
return [
|
|
184
185
|
ui.dl([
|
|
185
186
|
ui.dli("Name", [ui.p(entity.name ?? ui.dim("(None)"), false)]),
|
|
@@ -188,7 +189,10 @@ function coreContent(entity) {
|
|
|
188
189
|
ui.dli("Created", [
|
|
189
190
|
ui.p(ui.timestampSummary(entity.created_at), false),
|
|
190
191
|
]),
|
|
191
|
-
ui.dli("Last updated",
|
|
192
|
+
ui.dli("Last updated", ui.timestampSummary(entity.updated_at)),
|
|
193
|
+
ui.dli("Web link", [
|
|
194
|
+
ui.p(ui.link(ui.webLink(`/catalog/${entity.identifier}`, runtime))),
|
|
195
|
+
]),
|
|
192
196
|
ui.dli("Description", [
|
|
193
197
|
ui.p(entity.description ?? ui.dim("(None)"), false),
|
|
194
198
|
]),
|
|
@@ -53,7 +53,7 @@ export function entityTypesCommand() {
|
|
|
53
53
|
renderJson({ ok: true, entity_type: response.entity_type });
|
|
54
54
|
}
|
|
55
55
|
else {
|
|
56
|
-
renderEntityType(response.entity_type, `${ui.success(ui.GLYPHS.CHECK)} Entity type created`);
|
|
56
|
+
renderEntityType(response.entity_type, runtime, `${ui.success(ui.GLYPHS.CHECK)} Entity type created`);
|
|
57
57
|
}
|
|
58
58
|
}));
|
|
59
59
|
entityTypes
|
|
@@ -106,7 +106,7 @@ export function entityTypesCommand() {
|
|
|
106
106
|
renderJson(processedEntityType);
|
|
107
107
|
}
|
|
108
108
|
else {
|
|
109
|
-
renderEntityType(processedEntityType);
|
|
109
|
+
renderEntityType(processedEntityType, runtime);
|
|
110
110
|
}
|
|
111
111
|
}));
|
|
112
112
|
entityTypes
|
|
@@ -200,7 +200,7 @@ export function entityTypesCommand() {
|
|
|
200
200
|
renderJson({ ...response, entity_types: processedEntityTypes });
|
|
201
201
|
}
|
|
202
202
|
else {
|
|
203
|
-
renderEntityTypeList(processedEntityTypes, response.response_metadata?.next_cursor ?? null);
|
|
203
|
+
renderEntityTypeList(processedEntityTypes, response.response_metadata?.next_cursor ?? null, runtime);
|
|
204
204
|
}
|
|
205
205
|
}));
|
|
206
206
|
entityTypes
|
|
@@ -245,7 +245,7 @@ export function entityTypesCommand() {
|
|
|
245
245
|
renderJson({ ok: true, entity_type: response.entity_type });
|
|
246
246
|
}
|
|
247
247
|
else {
|
|
248
|
-
renderEntityType(response.entity_type, `${ui.success(ui.GLYPHS.CHECK)} Entity type updated`);
|
|
248
|
+
renderEntityType(response.entity_type, runtime, `${ui.success(ui.GLYPHS.CHECK)} Entity type updated`);
|
|
249
249
|
}
|
|
250
250
|
}));
|
|
251
251
|
return entityTypes;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderRichText } from "../../renderers.js";
|
|
2
2
|
import * as ui from "../../ui.js";
|
|
3
|
-
export function renderEntityTypeList(entityTypes, nextCursor) {
|
|
3
|
+
export function renderEntityTypeList(entityTypes, nextCursor, runtime) {
|
|
4
4
|
const blocks = [ui.h1("Entity Types")];
|
|
5
5
|
blocks.push(ui.p(`Displaying ${ui.bold(entityTypes.length.toString())} entity types.`));
|
|
6
6
|
if (nextCursor) {
|
|
@@ -18,7 +18,7 @@ export function renderEntityTypeList(entityTypes, nextCursor) {
|
|
|
18
18
|
}
|
|
19
19
|
if (entityType.identifier) {
|
|
20
20
|
blocks.push(ui.h3("Core attributes"));
|
|
21
|
-
blocks.push(...coreContent(entityType));
|
|
21
|
+
blocks.push(...coreContent(entityType, runtime));
|
|
22
22
|
}
|
|
23
23
|
if (entityType.properties) {
|
|
24
24
|
blocks.push(ui.h3("Properties"));
|
|
@@ -31,11 +31,11 @@ export function renderEntityTypeList(entityTypes, nextCursor) {
|
|
|
31
31
|
}
|
|
32
32
|
renderRichText(blocks);
|
|
33
33
|
}
|
|
34
|
-
export function renderEntityType(entityType, title = "Entity Type Information") {
|
|
34
|
+
export function renderEntityType(entityType, runtime, title = "Entity Type Information") {
|
|
35
35
|
renderRichText([
|
|
36
36
|
ui.h1(title),
|
|
37
37
|
entityType.identifier
|
|
38
|
-
? [ui.h2("Core attributes"), ...coreContent(entityType)]
|
|
38
|
+
? [ui.h2("Core attributes"), ...coreContent(entityType, runtime)]
|
|
39
39
|
: null,
|
|
40
40
|
entityType.properties
|
|
41
41
|
? [ui.h2("Properties"), ...propertiesContent(entityType.properties)]
|
|
@@ -51,25 +51,19 @@ export function renderEntityTypeDeleted(entityType) {
|
|
|
51
51
|
ui.p(`The entity type ${ui.code(entityType.identifier)} has been deleted.`),
|
|
52
52
|
]);
|
|
53
53
|
}
|
|
54
|
-
function coreContent(entityType) {
|
|
54
|
+
function coreContent(entityType, runtime) {
|
|
55
55
|
return [
|
|
56
56
|
ui.dl([
|
|
57
|
-
ui.dli("Name",
|
|
58
|
-
ui.dli("Identifier",
|
|
59
|
-
ui.dli("Description",
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
ui.dli("
|
|
63
|
-
ui.dli("
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
ui.p(ui.timestampSummary(entityType.created_at), false),
|
|
68
|
-
]),
|
|
69
|
-
ui.dli("Last updated", [
|
|
70
|
-
ui.p(ui.timestampSummary(entityType.updated_at)),
|
|
71
|
-
]),
|
|
72
|
-
], { termWidth: 14 }),
|
|
57
|
+
ui.dli("Name", entityType.name ?? ui.dim("(None)")),
|
|
58
|
+
ui.dli("Identifier", ui.code(entityType.identifier)),
|
|
59
|
+
ui.dli("Description", entityType.description || ui.dim("(None)")),
|
|
60
|
+
ui.dli("Icon", entityType.icon ?? ui.dim("(None)")),
|
|
61
|
+
ui.dli("Ordering", entityType.ordering?.toString() ?? ui.dim("(None)")),
|
|
62
|
+
ui.dli("Created", ui.timestampSummary(entityType.created_at)),
|
|
63
|
+
ui.dli("Last updated", ui.timestampSummary(entityType.updated_at)),
|
|
64
|
+
ui.dli("Entities link", ui.link(ui.webLink(`/catalog?type=${entityType.identifier}`, runtime))),
|
|
65
|
+
ui.dli("Settings link", ui.link(ui.webLink(`/catalog/manage/entitytypes/${entityType.identifier}`, runtime))),
|
|
66
|
+
], { termWidth: 15 }),
|
|
73
67
|
];
|
|
74
68
|
}
|
|
75
69
|
function propertiesContent(properties) {
|
package/dist/commands/init.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { access } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import {
|
|
4
|
+
import { password, confirm, select } from "@inquirer/prompts";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
import { execa } from "execa";
|
|
7
|
-
import { buildRuntime, buildRuntimeSafe } from "../runtime.js";
|
|
7
|
+
import { buildLogger, buildRuntime, buildRuntimeSafe } from "../runtime.js";
|
|
8
8
|
import { getAuthInfo } from "./auth.js";
|
|
9
9
|
import { renderAuthInfo } from "./authRendering.js";
|
|
10
10
|
import { loginViaBrowser } from "../loginViaBrowser.js";
|
|
@@ -13,29 +13,8 @@ import { wrapAction } from "../commandHelpers.js";
|
|
|
13
13
|
import { renderRichText } from "../renderers.js";
|
|
14
14
|
import * as ui from "../ui.js";
|
|
15
15
|
import { CliError } from "../errors.js";
|
|
16
|
-
import {
|
|
16
|
+
import { deriveBaseUrlsFromEnv, persistBaseUrls } from "../config.js";
|
|
17
17
|
import { setToken } from "../secrets.js";
|
|
18
|
-
function parseHostname(raw) {
|
|
19
|
-
const normalized = raw.trim().replace(/\/$/, "");
|
|
20
|
-
if (!normalized || normalized === "app.getdx.com") {
|
|
21
|
-
return { type: "cloud" };
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
const url = new URL(normalized.startsWith("http") ? normalized : `https://${normalized}`);
|
|
25
|
-
const host = url.hostname;
|
|
26
|
-
const dedicatedMatch = host.match(/^(.+)\.getdx\.io$/);
|
|
27
|
-
if (dedicatedMatch) {
|
|
28
|
-
return { type: "dedicated", accountName: dedicatedMatch[1] };
|
|
29
|
-
}
|
|
30
|
-
if (host) {
|
|
31
|
-
return { type: "managed", uiBaseUrl: url.origin };
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
// fall through to invalid
|
|
36
|
-
}
|
|
37
|
-
return { type: "invalid" };
|
|
38
|
-
}
|
|
39
18
|
// FIXME: make this WAY more glam
|
|
40
19
|
// Choose a more interesting ASCII art font from https://patorjk.com/software/taag/
|
|
41
20
|
const WELCOME_BANNER = ui.indent(`\
|
|
@@ -51,14 +30,12 @@ export function initCommand() {
|
|
|
51
30
|
const init = new Command()
|
|
52
31
|
.name("init")
|
|
53
32
|
.description("Initialize the DX CLI")
|
|
54
|
-
.option("--host <hostname>", "DX hostname for dedicated or managed deployments (e.g. mycompany.getdx.io)")
|
|
55
33
|
.action(wrapAction(async (commandOptions, command) => {
|
|
56
34
|
const context = getContext(command);
|
|
57
35
|
let runtime = buildRuntimeSafe(context);
|
|
58
36
|
ensureInteractive();
|
|
59
37
|
showWelcomeBanner();
|
|
60
|
-
|
|
61
|
-
runtime = await ensureLoggedIn(runtime, host);
|
|
38
|
+
runtime = await ensureLoggedIn(context, runtime);
|
|
62
39
|
await optionallySetupSkill(runtime);
|
|
63
40
|
renderRichText([
|
|
64
41
|
ui.blankLine(),
|
|
@@ -75,7 +52,7 @@ function ensureInteractive() {
|
|
|
75
52
|
function showWelcomeBanner() {
|
|
76
53
|
renderRichText([ui.p(ui.success(WELCOME_BANNER))]);
|
|
77
54
|
}
|
|
78
|
-
async function ensureLoggedIn(
|
|
55
|
+
async function ensureLoggedIn(context, runtime) {
|
|
79
56
|
renderRichText([ui.h1(`Checking if you are logged in...`), ui.blankLine()]);
|
|
80
57
|
if (runtime) {
|
|
81
58
|
let authInfo = null;
|
|
@@ -86,34 +63,17 @@ async function ensureLoggedIn(runtime, host) {
|
|
|
86
63
|
// Do nothing, we don't mind if this fails
|
|
87
64
|
}
|
|
88
65
|
if (authInfo) {
|
|
89
|
-
renderAuthInfo(authInfo, runtime.token, runtime.
|
|
66
|
+
renderAuthInfo(authInfo, runtime.token, runtime.apiBaseUrl);
|
|
90
67
|
return runtime;
|
|
91
68
|
}
|
|
92
69
|
}
|
|
93
70
|
renderRichText([ui.p(`You are not logged in yet.`), ui.blankLine()]);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
throw new CliError(`Could not recognize hostname "${host}". Expected app.getdx.com, <account>.getdx.io, or a custom domain.`);
|
|
97
|
-
}
|
|
98
|
-
switch (parsed.type) {
|
|
99
|
-
case "cloud":
|
|
100
|
-
return await attemptLogin("https://api.getdx.com", "https://app.getdx.com");
|
|
101
|
-
case "dedicated": {
|
|
102
|
-
const { accountName } = parsed;
|
|
103
|
-
return await attemptLogin(`https://api.${accountName}.getdx.io`, `https://${accountName}.getdx.io`);
|
|
104
|
-
}
|
|
105
|
-
case "managed": {
|
|
106
|
-
const apiBaseUrl = await input({
|
|
107
|
-
message: "What is your API base URL?",
|
|
108
|
-
});
|
|
109
|
-
if (!apiBaseUrl) {
|
|
110
|
-
throw new CliError("API base URL is required");
|
|
111
|
-
}
|
|
112
|
-
return await attemptLogin(apiBaseUrl, parsed.uiBaseUrl);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
71
|
+
const { apiBaseUrl, webBaseUrl } = deriveBaseUrlsFromEnv();
|
|
72
|
+
return await attemptLogin(apiBaseUrl, webBaseUrl, context);
|
|
115
73
|
}
|
|
116
|
-
async function attemptLogin(apiBaseUrl,
|
|
74
|
+
async function attemptLogin(apiBaseUrl, webBaseUrl, context) {
|
|
75
|
+
const logger = buildLogger(context);
|
|
76
|
+
logger.debug("Attempting login", { apiBaseUrl, webBaseUrl });
|
|
117
77
|
const method = await select({
|
|
118
78
|
message: "How would you like to log in?",
|
|
119
79
|
choices: [
|
|
@@ -123,7 +83,7 @@ async function attemptLogin(apiBaseUrl, uiBaseUrl) {
|
|
|
123
83
|
});
|
|
124
84
|
let token;
|
|
125
85
|
if (method === "browser") {
|
|
126
|
-
token = await loginViaBrowser(
|
|
86
|
+
token = await loginViaBrowser(webBaseUrl);
|
|
127
87
|
}
|
|
128
88
|
else {
|
|
129
89
|
token = await password({
|
|
@@ -134,17 +94,17 @@ async function attemptLogin(apiBaseUrl, uiBaseUrl) {
|
|
|
134
94
|
if (!token) {
|
|
135
95
|
throw new CliError("Account web API token is required");
|
|
136
96
|
}
|
|
137
|
-
const context = createEmptyContext();
|
|
138
97
|
const runtime = buildRuntime(context, {
|
|
139
|
-
|
|
98
|
+
apiBaseUrl,
|
|
140
99
|
token,
|
|
100
|
+
webBaseUrl,
|
|
141
101
|
});
|
|
142
102
|
renderRichText([ui.blankLine(), ui.p(`Attempting login...`), ui.blankLine()]);
|
|
143
103
|
const response = await getAuthInfo(runtime);
|
|
144
104
|
if (!response.ok) {
|
|
145
105
|
throw new CliError(`Login failed`);
|
|
146
106
|
}
|
|
147
|
-
|
|
107
|
+
persistBaseUrls(apiBaseUrl, webBaseUrl);
|
|
148
108
|
setToken(apiBaseUrl, token);
|
|
149
109
|
renderAuthInfo(response, token, apiBaseUrl);
|
|
150
110
|
return runtime;
|
|
@@ -195,8 +155,3 @@ async function optionallySetupSkill(runtime) {
|
|
|
195
155
|
throw new CliError(`Failed to setup the DX skill: ${result.stderr}`);
|
|
196
156
|
}
|
|
197
157
|
}
|
|
198
|
-
function createEmptyContext() {
|
|
199
|
-
return {
|
|
200
|
-
json: false,
|
|
201
|
-
};
|
|
202
|
-
}
|
|
@@ -53,7 +53,7 @@ export function scorecardsCommand() {
|
|
|
53
53
|
renderJson({ ok: true, scorecard: response.scorecard });
|
|
54
54
|
}
|
|
55
55
|
else {
|
|
56
|
-
renderScorecard(response.scorecard, null, `${ui.success(ui.GLYPHS.CHECK)} Scorecard created`);
|
|
56
|
+
renderScorecard(response.scorecard, null, runtime, `${ui.success(ui.GLYPHS.CHECK)} Scorecard created`);
|
|
57
57
|
}
|
|
58
58
|
}));
|
|
59
59
|
scorecards
|
|
@@ -110,7 +110,7 @@ export function scorecardsCommand() {
|
|
|
110
110
|
renderJson({ ok: true, scorecard: processedScorecard });
|
|
111
111
|
}
|
|
112
112
|
else {
|
|
113
|
-
renderScorecard(response.scorecard, includeSections);
|
|
113
|
+
renderScorecard(response.scorecard, includeSections, runtime);
|
|
114
114
|
}
|
|
115
115
|
}));
|
|
116
116
|
scorecards
|
|
@@ -211,7 +211,7 @@ export function scorecardsCommand() {
|
|
|
211
211
|
renderJson({ ...response, scorecards: processedScorecards });
|
|
212
212
|
}
|
|
213
213
|
else {
|
|
214
|
-
renderScorecardList(response.scorecards, response.response_metadata?.next_cursor ?? null, includeSections);
|
|
214
|
+
renderScorecardList(response.scorecards, response.response_metadata?.next_cursor ?? null, includeSections, runtime);
|
|
215
215
|
}
|
|
216
216
|
}));
|
|
217
217
|
scorecards
|
|
@@ -256,7 +256,7 @@ export function scorecardsCommand() {
|
|
|
256
256
|
renderJson({ ok: true, scorecard: response.scorecard });
|
|
257
257
|
}
|
|
258
258
|
else {
|
|
259
|
-
renderScorecard(response.scorecard, null, `${ui.success(ui.GLYPHS.CHECK)} Scorecard updated`);
|
|
259
|
+
renderScorecard(response.scorecard, null, runtime, `${ui.success(ui.GLYPHS.CHECK)} Scorecard updated`);
|
|
260
260
|
}
|
|
261
261
|
}));
|
|
262
262
|
return scorecards;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { renderRichText } from "../renderers.js";
|
|
2
2
|
import * as ui from "../ui.js";
|
|
3
|
-
export function renderScorecardList(scorecards, nextCursor, includeSections) {
|
|
3
|
+
export function renderScorecardList(scorecards, nextCursor, includeSections, runtime) {
|
|
4
4
|
const shouldIncludeAll = includeSections === null;
|
|
5
5
|
const blocks = [ui.h1("Scorecards")];
|
|
6
6
|
blocks.push(ui.p(`Displaying ${ui.bold(scorecards.length.toString())} scorecards.`));
|
|
@@ -11,7 +11,7 @@ export function renderScorecardList(scorecards, nextCursor, includeSections) {
|
|
|
11
11
|
blocks.push(ui.h2(`${scorecard.name} (${ui.code(scorecard.id)})`));
|
|
12
12
|
if (shouldIncludeAll || includeSections.includes("core")) {
|
|
13
13
|
blocks.push(ui.h3("Basic details"));
|
|
14
|
-
blocks.push(...basicDetailsContent(scorecard));
|
|
14
|
+
blocks.push(...basicDetailsContent(scorecard, runtime));
|
|
15
15
|
blocks.push(ui.h3("Entity filter"));
|
|
16
16
|
blocks.push(...entityFilterContent(scorecard));
|
|
17
17
|
if (scorecard.type === "LEVEL") {
|
|
@@ -34,12 +34,12 @@ export function renderScorecardList(scorecards, nextCursor, includeSections) {
|
|
|
34
34
|
}
|
|
35
35
|
renderRichText(blocks);
|
|
36
36
|
}
|
|
37
|
-
export function renderScorecard(scorecard, includeSections, title = "Scorecard Information") {
|
|
37
|
+
export function renderScorecard(scorecard, includeSections, runtime, title = "Scorecard Information") {
|
|
38
38
|
const shouldIncludeAll = includeSections === null;
|
|
39
39
|
const blocks = [ui.h1(title)];
|
|
40
40
|
if (shouldIncludeAll || includeSections.includes("core")) {
|
|
41
41
|
blocks.push(ui.h2("Basic details"));
|
|
42
|
-
blocks.push(...basicDetailsContent(scorecard));
|
|
42
|
+
blocks.push(...basicDetailsContent(scorecard, runtime));
|
|
43
43
|
blocks.push(ui.h2("Entity filter"));
|
|
44
44
|
blocks.push(...entityFilterContent(scorecard));
|
|
45
45
|
if (scorecard.type === "LEVEL") {
|
|
@@ -61,7 +61,7 @@ export function renderScorecard(scorecard, includeSections, title = "Scorecard I
|
|
|
61
61
|
}
|
|
62
62
|
renderRichText(blocks);
|
|
63
63
|
}
|
|
64
|
-
function basicDetailsContent(scorecard) {
|
|
64
|
+
function basicDetailsContent(scorecard, runtime) {
|
|
65
65
|
const items = [
|
|
66
66
|
ui.dli("ID", [ui.p(ui.code(scorecard.id), false)]),
|
|
67
67
|
ui.dli("Name", [ui.p(scorecard.name, false)]),
|
|
@@ -74,6 +74,9 @@ function basicDetailsContent(scorecard) {
|
|
|
74
74
|
ui.dli("Published", [
|
|
75
75
|
ui.p(scorecard.published ? ui.success("Yes") : ui.dim("No (draft)"), false),
|
|
76
76
|
]),
|
|
77
|
+
ui.dli("Web link", [
|
|
78
|
+
ui.p(ui.link(ui.webLink(`/scorecards/${scorecard.id}`, runtime)), false),
|
|
79
|
+
]),
|
|
77
80
|
];
|
|
78
81
|
if (scorecard.tags && scorecard.tags.length > 0) {
|
|
79
82
|
items.push(ui.dli("Tags", [
|
|
@@ -144,13 +144,13 @@ async function getStudioQueryResults(runtime, id) {
|
|
|
144
144
|
return response.body;
|
|
145
145
|
}
|
|
146
146
|
async function downloadStudioQueryResultsCsv(runtime, id, filename) {
|
|
147
|
-
const response = await fetchWithApiHeaders(runtime, `${runtime.
|
|
147
|
+
const response = await fetchWithApiHeaders(runtime, `${runtime.apiBaseUrl}/studio.queryRuns.results?id=${encodeURIComponent(id)}&format=csv`, { redirect: "manual" });
|
|
148
148
|
if (response.status >= 300 && response.status < 400) {
|
|
149
149
|
const location = response.headers.get("location");
|
|
150
150
|
if (!location) {
|
|
151
151
|
throw new HttpError("CSV download redirect did not include a Location header.", response.status);
|
|
152
152
|
}
|
|
153
|
-
const downloadResponse = await fetchResponse(new URL(location, runtime.
|
|
153
|
+
const downloadResponse = await fetchResponse(new URL(location, runtime.apiBaseUrl).toString(), {
|
|
154
154
|
method: "GET",
|
|
155
155
|
headers: buildDownloadHeaders(runtime, "text/csv"),
|
|
156
156
|
});
|
package/dist/config.js
CHANGED
|
@@ -1,14 +1,10 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
return path.join(xdg, "dx");
|
|
9
|
-
}
|
|
10
|
-
return path.join(os.homedir(), ".config", "dx");
|
|
11
|
-
}
|
|
4
|
+
import { CliError, EXIT_CODES } from "./errors.js";
|
|
5
|
+
const DEFAULT_API_BASE_URL = "https://api.getdx.com";
|
|
6
|
+
const DEFAULT_WEB_BASE_URL = "https://app.getdx.com";
|
|
7
|
+
// --- Config file I/O ---------------------------------------------------------
|
|
12
8
|
export function getConfigPath() {
|
|
13
9
|
return path.join(getConfigDir(), "config.json");
|
|
14
10
|
}
|
|
@@ -18,38 +14,184 @@ export function readConfig() {
|
|
|
18
14
|
return {};
|
|
19
15
|
}
|
|
20
16
|
const content = fs.readFileSync(configPath, "utf8");
|
|
21
|
-
|
|
17
|
+
const raw = JSON.parse(content);
|
|
18
|
+
const api = raw.apiBaseUrl;
|
|
19
|
+
const web = raw.webBaseUrl;
|
|
20
|
+
const stored = {};
|
|
21
|
+
if (web) {
|
|
22
|
+
stored.webBaseUrl = web;
|
|
23
|
+
}
|
|
24
|
+
if (api) {
|
|
25
|
+
stored.apiBaseUrl = api;
|
|
26
|
+
}
|
|
27
|
+
return stored;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Responsible for handling the migration from `baseUrl` to `apiBaseUrl` and `webBaseUrl`,
|
|
31
|
+
* so the upgrade to 0.3.x will be non-breaking for `cloud` and `dedicated` deployments.
|
|
32
|
+
*
|
|
33
|
+
* `managed` deployments will encounter an error and will have to reauthenticate.
|
|
34
|
+
*
|
|
35
|
+
* TODO: delete after 2026-07-01.
|
|
36
|
+
*/
|
|
37
|
+
export function handleTemporaryBaseUrlMigration(argv) {
|
|
38
|
+
if (!process.env.VITEST) {
|
|
39
|
+
// Running tests - we don't want our permanent tests to exercise this logic
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
const COMMANDS_TO_SKIP_MIGRATION = ["login", "logout", "init"];
|
|
43
|
+
if (COMMANDS_TO_SKIP_MIGRATION.some((command) => argv.includes(command))) {
|
|
44
|
+
// Logging in or logging out: we need to ignore the current config file's contents
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const configPath = getConfigPath();
|
|
48
|
+
if (!fs.existsSync(configPath)) {
|
|
49
|
+
// No config file, no need to migrate
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const content = fs.readFileSync(configPath, "utf8");
|
|
53
|
+
const raw = JSON.parse(content);
|
|
54
|
+
const api = raw.apiBaseUrl;
|
|
55
|
+
const web = raw.webBaseUrl;
|
|
56
|
+
if (api !== undefined && web !== undefined) {
|
|
57
|
+
// Already migrated, no need to do anything
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (raw.baseUrl) {
|
|
61
|
+
// Legacy baseUrl is present, migrate to apiBaseUrl and webBaseUrl
|
|
62
|
+
const apiBaseUrl = raw.baseUrl;
|
|
63
|
+
let webBaseUrl;
|
|
64
|
+
if (apiBaseUrl.endsWith(".getdx.com")) {
|
|
65
|
+
// Migrate `cloud`
|
|
66
|
+
webBaseUrl = "https://app.getdx.com";
|
|
67
|
+
writeConfig({
|
|
68
|
+
apiBaseUrl: normalizeUrl(apiBaseUrl),
|
|
69
|
+
webBaseUrl: normalizeUrl(webBaseUrl),
|
|
70
|
+
});
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const dedicatedMatch = apiBaseUrl.match(/^api\.(.+)\.getdx\.io$/);
|
|
74
|
+
if (dedicatedMatch) {
|
|
75
|
+
// Migrate `dedicated`
|
|
76
|
+
webBaseUrl = `https://${dedicatedMatch[1]}.getdx.io`;
|
|
77
|
+
writeConfig({
|
|
78
|
+
apiBaseUrl: normalizeUrl(apiBaseUrl),
|
|
79
|
+
webBaseUrl: normalizeUrl(webBaseUrl),
|
|
80
|
+
});
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
// Unable to automatically migrate `managed`
|
|
84
|
+
throw new CliError("Your on-disk DX CLI config is missing apiBaseUrl and webBaseUrl. Run `dx auth logout`, then login again via `dx init` or `dx auth login`.");
|
|
85
|
+
}
|
|
22
86
|
}
|
|
23
87
|
export function writeConfig(config) {
|
|
24
88
|
fs.mkdirSync(getConfigDir(), { recursive: true });
|
|
25
89
|
fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf8");
|
|
26
90
|
}
|
|
27
|
-
export function
|
|
28
|
-
|
|
29
|
-
|
|
91
|
+
export function persistBaseUrls(apiBaseUrl, webBaseUrl) {
|
|
92
|
+
writeConfig({
|
|
93
|
+
...readConfig(),
|
|
94
|
+
apiBaseUrl: normalizeUrl(apiBaseUrl),
|
|
95
|
+
webBaseUrl: normalizeUrl(webBaseUrl),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
/** Strips trailing slashes from an HTTP(S) base URL string. */
|
|
99
|
+
export function normalizeUrl(url) {
|
|
100
|
+
return url.replace(/\/+$/, "");
|
|
101
|
+
}
|
|
102
|
+
function getConfigDir() {
|
|
103
|
+
const xdg = process.env.XDG_CONFIG_HOME;
|
|
104
|
+
if (xdg) {
|
|
105
|
+
return path.join(xdg, "dx");
|
|
30
106
|
}
|
|
31
|
-
return
|
|
107
|
+
return path.join(os.homedir(), ".config", "dx");
|
|
32
108
|
}
|
|
109
|
+
// --- Resolving URLs at command runtime ------------------------------------------------------
|
|
33
110
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
111
|
+
* Resolved API base URL for HTTP requests.
|
|
112
|
+
* Precedence: `DX_API_BASE_URL`, then persisted `apiBaseUrl`, then DX Cloud default.
|
|
36
113
|
*/
|
|
37
|
-
export function
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const host = url.hostname;
|
|
41
|
-
if (host === "api.getdx.com") {
|
|
42
|
-
return "https://app.getdx.com";
|
|
114
|
+
export function resolveApiBaseUrl() {
|
|
115
|
+
if (process.env.DX_API_BASE_URL) {
|
|
116
|
+
return normalizeUrl(process.env.DX_API_BASE_URL);
|
|
43
117
|
}
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
118
|
+
return normalizeUrl(readConfig().apiBaseUrl || DEFAULT_API_BASE_URL);
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Resolved web-app base URL for CLI deep links and browser OAuth.
|
|
122
|
+
* Precedence: `DX_WEB_BASE_URL`, persisted `webBaseUrl`, then DX Cloud default.
|
|
123
|
+
*/
|
|
124
|
+
export function resolveWebBaseUrl() {
|
|
125
|
+
if (process.env.DX_WEB_BASE_URL) {
|
|
126
|
+
return normalizeUrl(process.env.DX_WEB_BASE_URL);
|
|
47
127
|
}
|
|
48
|
-
return
|
|
128
|
+
return normalizeUrl(readConfig().webBaseUrl || DEFAULT_WEB_BASE_URL);
|
|
49
129
|
}
|
|
50
|
-
|
|
51
|
-
|
|
130
|
+
// --- Deriving base URLs at login time -------------------------------------
|
|
131
|
+
/**
|
|
132
|
+
* Derive the appropriate `apiBaseUrl` and `webBaseUrl` to persist into config,
|
|
133
|
+
* considering the `DX_API_BASE_URL` and `DX_WEB_BASE_URL` env vars if they are present.
|
|
134
|
+
*
|
|
135
|
+
* If the env vars are not set, we assume the user is configuring for a `cloud` deployment.
|
|
136
|
+
*
|
|
137
|
+
* If they are set, we parse them to determine whether the deployment is `cloud`, `dedicated`, or `managed`.
|
|
138
|
+
*
|
|
139
|
+
* If we fail to derive the base URLs, a `CliError` is thrown.
|
|
140
|
+
*/
|
|
141
|
+
export function deriveBaseUrlsFromEnv() {
|
|
142
|
+
const apiBaseUrlFromEnv = process.env.DX_API_BASE_URL;
|
|
143
|
+
const webBaseUrlFromEnv = process.env.DX_WEB_BASE_URL;
|
|
144
|
+
if (apiBaseUrlFromEnv && !webBaseUrlFromEnv) {
|
|
145
|
+
throw new CliError("DX_WEB_BASE_URL must be set when DX_API_BASE_URL is set", EXIT_CODES.ARGUMENT_ERROR);
|
|
146
|
+
}
|
|
147
|
+
const parsed = webBaseUrlFromEnv
|
|
148
|
+
? parseWebBaseUrl(webBaseUrlFromEnv)
|
|
149
|
+
: { type: "cloud" };
|
|
150
|
+
switch (parsed.type) {
|
|
151
|
+
case "cloud":
|
|
152
|
+
return {
|
|
153
|
+
apiBaseUrl: "https://api.getdx.com",
|
|
154
|
+
webBaseUrl: "https://app.getdx.com",
|
|
155
|
+
};
|
|
156
|
+
case "dedicated": {
|
|
157
|
+
const { accountName } = parsed;
|
|
158
|
+
return {
|
|
159
|
+
apiBaseUrl: `https://api.${accountName}.getdx.io`,
|
|
160
|
+
webBaseUrl: `https://${accountName}.getdx.io`,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
case "managed": {
|
|
164
|
+
const apiBaseUrl = process.env.DX_API_BASE_URL;
|
|
165
|
+
if (!apiBaseUrl) {
|
|
166
|
+
throw new CliError("DX_API_BASE_URL must be specified when authenticating with a managed deployment");
|
|
167
|
+
}
|
|
168
|
+
return {
|
|
169
|
+
apiBaseUrl,
|
|
170
|
+
webBaseUrl: parsed.webAppUrl,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
case "invalid":
|
|
174
|
+
throw new CliError(`Could not recognize web base URL "${parsed.input}". Expected https://app.getdx.com, https://<account>.getdx.io, or a custom domain.`);
|
|
175
|
+
}
|
|
52
176
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
177
|
+
function parseWebBaseUrl(raw) {
|
|
178
|
+
const normalized = raw.trim().replace(/\/$/, "");
|
|
179
|
+
if (!normalized || normalized === "app.getdx.com") {
|
|
180
|
+
return { type: "cloud" };
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const url = new URL(normalized.startsWith("http") ? normalized : `https://${normalized}`);
|
|
184
|
+
const host = url.hostname;
|
|
185
|
+
const dedicatedMatch = host.match(/^(.+)\.getdx\.io$/);
|
|
186
|
+
if (dedicatedMatch) {
|
|
187
|
+
return { type: "dedicated", accountName: dedicatedMatch[1] };
|
|
188
|
+
}
|
|
189
|
+
if (host) {
|
|
190
|
+
return { type: "managed", webAppUrl: url.origin };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
// fall through to invalid
|
|
195
|
+
}
|
|
196
|
+
return { type: "invalid", input: raw };
|
|
55
197
|
}
|
package/dist/http.js
CHANGED
|
@@ -23,7 +23,7 @@ export async function request(runtime, route, options = {}) {
|
|
|
23
23
|
query.set(key, String(value));
|
|
24
24
|
}
|
|
25
25
|
});
|
|
26
|
-
const url = `${runtime.
|
|
26
|
+
const url = `${runtime.apiBaseUrl}${route}${query.size > 0 ? `?${query.toString()}` : ""}`;
|
|
27
27
|
const requestBody = options.body !== undefined ? JSON.stringify(options.body) : undefined;
|
|
28
28
|
runtime.logger.debug("Sending HTTP request", {
|
|
29
29
|
body: options.body ?? null,
|
package/dist/loginViaBrowser.js
CHANGED
|
@@ -4,7 +4,7 @@ import { AuthCodeCallbackServer, } from "./authCodeCallbackServer.js";
|
|
|
4
4
|
import { CliError } from "./errors.js";
|
|
5
5
|
import { renderRichText } from "./renderers.js";
|
|
6
6
|
import * as ui from "./ui.js";
|
|
7
|
-
export async function loginViaBrowser(
|
|
7
|
+
export async function loginViaBrowser(webBaseUrl) {
|
|
8
8
|
const server = new AuthCodeCallbackServer();
|
|
9
9
|
await server.start();
|
|
10
10
|
const callbackUri = server.getAddress();
|
|
@@ -17,7 +17,7 @@ export async function loginViaBrowser(uiBaseUrl) {
|
|
|
17
17
|
code_challenge: codeChallenge,
|
|
18
18
|
code_challenge_method: "S256",
|
|
19
19
|
});
|
|
20
|
-
const authUrl = `${
|
|
20
|
+
const authUrl = `${webBaseUrl}/cli/auth?${params.toString()}`;
|
|
21
21
|
renderRichText([
|
|
22
22
|
ui.p(`Opening your browser to complete authentication...`),
|
|
23
23
|
ui.blankLine(),
|
|
@@ -42,7 +42,7 @@ export async function loginViaBrowser(uiBaseUrl) {
|
|
|
42
42
|
error: new CliError("Authentication failed: state mismatch"),
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
const tokenExchangeResponse = await exchangeCodeForToken(
|
|
45
|
+
const tokenExchangeResponse = await exchangeCodeForToken(webBaseUrl, code, codeVerifier);
|
|
46
46
|
if (!tokenExchangeResponse.ok) {
|
|
47
47
|
return {
|
|
48
48
|
type: "ERROR",
|
|
@@ -64,8 +64,8 @@ export async function loginViaBrowser(uiBaseUrl) {
|
|
|
64
64
|
function validateState(stateFromResponse, originalState) {
|
|
65
65
|
return stateFromResponse === originalState;
|
|
66
66
|
}
|
|
67
|
-
async function exchangeCodeForToken(
|
|
68
|
-
return fetch(`${
|
|
67
|
+
async function exchangeCodeForToken(webBaseUrl, code, codeVerifier) {
|
|
68
|
+
return fetch(`${webBaseUrl}/cli/token`, {
|
|
69
69
|
method: "POST",
|
|
70
70
|
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
71
71
|
body: new URLSearchParams({ code, code_verifier: codeVerifier }),
|
package/dist/runtime.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { resolveApiBaseUrl, resolveWebBaseUrl } from "./config.js";
|
|
2
2
|
import { CliError } from "./errors.js";
|
|
3
3
|
import { getToken } from "./secrets.js";
|
|
4
4
|
import { createLogger } from "./logger.js";
|
|
@@ -7,13 +7,15 @@ export function buildLogger(context) {
|
|
|
7
7
|
return createLogger({ json: context.json || !process.stderr.isTTY });
|
|
8
8
|
}
|
|
9
9
|
export function buildRuntime(context, overrides) {
|
|
10
|
-
const
|
|
11
|
-
const
|
|
10
|
+
const apiBaseUrl = overrides?.apiBaseUrl ?? resolveApiBaseUrl();
|
|
11
|
+
const webBaseUrl = overrides?.webBaseUrl ?? resolveWebBaseUrl();
|
|
12
|
+
const token = overrides?.token ?? getToken(apiBaseUrl);
|
|
12
13
|
if (!token) {
|
|
13
14
|
throw new CliError("No API token configured. Run `dx auth login --token <token>` or set DX_API_TOKEN.");
|
|
14
15
|
}
|
|
15
16
|
return {
|
|
16
|
-
|
|
17
|
+
apiBaseUrl,
|
|
18
|
+
webBaseUrl,
|
|
17
19
|
token,
|
|
18
20
|
context,
|
|
19
21
|
version: cliPackage.version,
|
|
@@ -21,13 +23,15 @@ export function buildRuntime(context, overrides) {
|
|
|
21
23
|
};
|
|
22
24
|
}
|
|
23
25
|
export function buildRuntimeSafe(context, overrides) {
|
|
24
|
-
const
|
|
25
|
-
const
|
|
26
|
+
const apiBaseUrl = overrides?.apiBaseUrl ?? resolveApiBaseUrl();
|
|
27
|
+
const webBaseUrl = overrides?.webBaseUrl ?? resolveWebBaseUrl();
|
|
28
|
+
const token = overrides?.token ?? getToken(apiBaseUrl);
|
|
26
29
|
if (!token) {
|
|
27
30
|
return null;
|
|
28
31
|
}
|
|
29
32
|
return {
|
|
30
|
-
|
|
33
|
+
apiBaseUrl,
|
|
34
|
+
webBaseUrl,
|
|
31
35
|
token,
|
|
32
36
|
context,
|
|
33
37
|
version: cliPackage.version,
|
package/dist/ui.js
CHANGED