@get-dx/cli 0.2.1 → 0.3.1

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 CHANGED
@@ -18,31 +18,44 @@ Interactively login and install the AI agent skill:
18
18
  dx init
19
19
  ```
20
20
 
21
- The CLI uses `app.getdx.com` as the hostname and `api.getdx.com` as the api base url by default.
21
+ Show help text:
22
+
23
+ ```shell
24
+ dx --help
25
+ dx <subcommand> --help
26
+ ```
27
+
28
+ ### Customizing base URLs
22
29
 
23
- ### Dedicated or managed deployments
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
- If your organization is on a dedicated or managed DX deployment, pass your custom hostname at init time:
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
- # Flag (dedicated instance at mycompany.getdx.io)
29
- dx init --host mycompany.getdx.io
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
- # Env var — useful for CI or scripted setups
32
- DX_HOST=mycompany.getdx.io dx init
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
- The `DX_HOST` and `--host` values accept:
49
+ #### For managed deployments
36
50
 
37
- - `app.getdx.com` — DX Cloud (default)
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
- dx --help
45
- dx <subcommand> --help
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
@@ -3,13 +3,16 @@ import { authCommand } from "./commands/auth.js";
3
3
  import { catalogCommand } from "./commands/catalog.js";
4
4
  import { initCommand } from "./commands/init.js";
5
5
  import { scorecardsCommand } from "./commands/scorecards.js";
6
+ import { snapshotsCommand } from "./commands/snapshots.js";
6
7
  import { studioCommand } from "./commands/studio.js";
7
8
  import { teamsCommand } from "./commands/teams.js";
8
9
  import { workflowsCommand } from "./commands/workflows.js";
9
10
  import { handleError } from "./commandHelpers.js";
10
11
  import cliPackage from "../package.json" with { type: "json" };
12
+ import { handleTemporaryBaseUrlMigration } from "./config.js";
11
13
  export async function run(argv = process.argv) {
12
14
  try {
15
+ handleTemporaryBaseUrlMigration(argv);
13
16
  const program = createProgram();
14
17
  if (argv.length <= 2) {
15
18
  program.outputHelp();
@@ -34,6 +37,7 @@ function createProgram() {
34
37
  program.addCommand(catalogCommand());
35
38
  program.addCommand(initCommand());
36
39
  program.addCommand(scorecardsCommand());
40
+ program.addCommand(snapshotsCommand());
37
41
  program.addCommand(studioCommand());
38
42
  program.addCommand(teamsCommand());
39
43
  program.addCommand(workflowsCommand());
@@ -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 { persistBaseUrl, resolveBaseUrl, resolveUiUrl } from "../config.js";
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 { buildLogger, buildRuntime } from "../runtime.js";
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 baseUrl = resolveBaseUrl();
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(resolveUiUrl(baseUrl));
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
- baseUrl,
48
+ const runtime = buildRuntime(context, {
49
+ apiBaseUrl,
51
50
  token,
52
- context,
53
- version: cliPackage.version,
54
- logger: buildLogger(context),
55
- };
51
+ webBaseUrl,
52
+ });
56
53
  const response = await getAuthInfo(runtime);
57
- persistBaseUrl(baseUrl);
58
- setToken(baseUrl, token);
54
+ persistBaseUrls(apiBaseUrl, webBaseUrl);
55
+ setToken(apiBaseUrl, token);
59
56
  if (context.json) {
60
- renderJson({ ...response, base_url: baseUrl });
57
+ renderJson({
58
+ ...response,
59
+ api_base_url: apiBaseUrl,
60
+ web_base_url: webBaseUrl,
61
+ });
61
62
  return;
62
63
  }
63
- renderAuthInfo(response, token, baseUrl);
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 baseUrl = resolveBaseUrl();
68
- deleteToken(baseUrl);
68
+ const apiBaseUrl = resolveApiBaseUrl();
69
+ deleteToken(apiBaseUrl);
69
70
  if (context.json) {
70
- renderJson({ ok: true, base_url: baseUrl, logged_out: true });
71
+ renderJson({ ok: true, api_base_url: apiBaseUrl, logged_out: true });
71
72
  }
72
73
  else {
73
- renderLoggedOut(baseUrl);
74
+ renderLoggedOut(apiBaseUrl);
74
75
  }
75
76
  }));
76
- auth.command("status").action(wrapAction(async (_options, command) => {
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
- base_url: runtime.baseUrl,
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.baseUrl);
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", [ui.p(ui.timestampSummary(entity.updated_at))]),
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", [ui.p(entityType.name ?? ui.dim("(None)"), false)]),
58
- ui.dli("Identifier", [ui.p(ui.code(entityType.identifier), false)]),
59
- ui.dli("Description", [
60
- ui.p(entityType.description || ui.dim("(None)"), false),
61
- ]),
62
- ui.dli("Icon", [ui.p(entityType.icon ?? ui.dim("(None)"), false)]),
63
- ui.dli("Ordering", [
64
- ui.p(entityType.ordering?.toString() ?? ui.dim("(None)"), false),
65
- ]),
66
- ui.dli("Created", [
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) {
@@ -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 { input, password, confirm, select } from "@inquirer/prompts";
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 { persistBaseUrl } from "../config.js";
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
- const host = commandOptions.host || process.env.DX_HOST;
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(runtime, host) {
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.baseUrl);
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 parsed = host ? parseHostname(host) : { type: "cloud" };
95
- if (parsed.type === "invalid") {
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, uiBaseUrl) {
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(uiBaseUrl);
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
- baseUrl: apiBaseUrl,
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
- persistBaseUrl(apiBaseUrl);
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", [