@get-dx/cli 0.2.0 → 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 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
@@ -5,10 +5,13 @@ import { initCommand } from "./commands/init.js";
5
5
  import { scorecardsCommand } from "./commands/scorecards.js";
6
6
  import { studioCommand } from "./commands/studio.js";
7
7
  import { teamsCommand } from "./commands/teams.js";
8
+ import { workflowsCommand } from "./commands/workflows.js";
8
9
  import { handleError } from "./commandHelpers.js";
9
10
  import cliPackage from "../package.json" with { type: "json" };
11
+ import { handleTemporaryBaseUrlMigration } from "./config.js";
10
12
  export async function run(argv = process.argv) {
11
13
  try {
14
+ handleTemporaryBaseUrlMigration(argv);
12
15
  const program = createProgram();
13
16
  if (argv.length <= 2) {
14
17
  program.outputHelp();
@@ -35,6 +38,7 @@ function createProgram() {
35
38
  program.addCommand(scorecardsCommand());
36
39
  program.addCommand(studioCommand());
37
40
  program.addCommand(teamsCommand());
41
+ program.addCommand(workflowsCommand());
38
42
  applyExitOverride(program);
39
43
  return program;
40
44
  }
@@ -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;
@@ -21,6 +21,7 @@ export function entitiesCommand() {
21
21
  .option("--owner-team-ids <ids>", "Comma-separated owner team IDs")
22
22
  .option("--owner-user-ids <ids>", "Comma-separated owner user IDs")
23
23
  .option("--property <kv>", "Set a property value as key=value. Repeat for multiple properties.", (val, prev) => [...prev, val], [])
24
+ .option("--alias <kv>", "Set an alias value as key=value. Repeat for multiple aliases.", (val, prev) => [...prev, val], [])
24
25
  .addHelpText("afterAll", createExampleText([
25
26
  {
26
27
  label: "Create a new service entity",
@@ -38,6 +39,10 @@ export function entitiesCommand() {
38
39
  label: "Create with properties",
39
40
  command: 'dx catalog entities create --type service --identifier my-service --property tier=Tier-1 --property "languages=Ruby,TypeScript"',
40
41
  },
42
+ {
43
+ label: "Create with aliases",
44
+ command: "dx catalog entities create --type service --identifier my-service --alias github_repo=12345",
45
+ },
41
46
  ]))
42
47
  .action(wrapAction(async (options, command) => {
43
48
  if (!options.identifier) {
@@ -47,7 +52,7 @@ export function entitiesCommand() {
47
52
  throw new CliError("--type is required", EXIT_CODES.ARGUMENT_ERROR);
48
53
  }
49
54
  const runtime = buildRuntime(getContext(command));
50
- const properties = await resolvePropertiesForEntityType(runtime, options.type, options.property);
55
+ const resolvedValues = await resolveValuesForEntityType(runtime, options.type, options.property, options.alias);
51
56
  const response = await createEntity(runtime, options.identifier, {
52
57
  type: options.type,
53
58
  name: options.name,
@@ -58,13 +63,13 @@ export function entitiesCommand() {
58
63
  owner_user_ids: options.ownerUserIds
59
64
  ?.split(",")
60
65
  .map((s) => s.trim()),
61
- properties,
66
+ ...resolvedValues,
62
67
  });
63
68
  if (runtime.context.json) {
64
69
  renderJson(response);
65
70
  }
66
71
  else {
67
- renderEntity(response.entity);
72
+ renderEntity(response.entity, runtime);
68
73
  }
69
74
  }));
70
75
  entities
@@ -100,6 +105,7 @@ export function entitiesCommand() {
100
105
  .option("--owner-team-ids <ids>", "Comma-separated owner team IDs")
101
106
  .option("--owner-user-ids <ids>", "Comma-separated owner user IDs")
102
107
  .option("--property <kv>", "Set a property value as key=value. Repeat for multiple properties. Use value null to remove a property.", (val, prev) => [...prev, val], [])
108
+ .option("--alias <kv>", "Set an alias value as key=value. Repeat for multiple aliases. Use value null to remove an alias.", (val, prev) => [...prev, val], [])
103
109
  .addHelpText("afterAll", createExampleText([
104
110
  {
105
111
  label: "Rename an entity",
@@ -113,19 +119,23 @@ export function entitiesCommand() {
113
119
  label: "Update properties",
114
120
  command: 'dx catalog entities update my-service --property tier=Tier-1 --property "languages=Ruby,TypeScript"',
115
121
  },
122
+ {
123
+ label: "Update aliases",
124
+ command: "dx catalog entities update my-service --alias github_repo=12345",
125
+ },
116
126
  ]))
117
127
  .action(wrapAction(async (identifier, options, command) => {
118
128
  const runtime = buildRuntime(getContext(command));
119
- const properties = await resolvePropertiesForExistingEntity(runtime, identifier, options.property);
129
+ const resolvedValues = await resolveValuesForExistingEntity(runtime, identifier, options.property, options.alias);
120
130
  const response = await updateEntity(runtime, identifier, {
121
131
  ...getEntityMutationOptionValues(options),
122
- properties,
132
+ ...resolvedValues,
123
133
  });
124
134
  if (runtime.context.json) {
125
135
  renderJson(response);
126
136
  }
127
137
  else {
128
- renderEntity(response.entity, `${ui.success(ui.GLYPHS.CHECK)} Entity updated`);
138
+ renderEntity(response.entity, runtime, `${ui.success(ui.GLYPHS.CHECK)} Entity updated`);
129
139
  }
130
140
  }));
131
141
  entities
@@ -138,6 +148,7 @@ export function entitiesCommand() {
138
148
  .option("--owner-team-ids <ids>", "Comma-separated owner team IDs")
139
149
  .option("--owner-user-ids <ids>", "Comma-separated owner user IDs")
140
150
  .option("--property <kv>", "Set a property value as key=value. Repeat for multiple properties. Use value null to remove a property.", (val, prev) => [...prev, val], [])
151
+ .option("--alias <kv>", "Set an alias value as key=value. Repeat for multiple aliases. Use value null to remove an alias.", (val, prev) => [...prev, val], [])
141
152
  .addHelpText("afterAll", createExampleText([
142
153
  {
143
154
  label: "Create a new service entity if it does not exist",
@@ -151,6 +162,10 @@ export function entitiesCommand() {
151
162
  label: "Set properties while preserving omitted fields",
152
163
  command: 'dx catalog entities upsert --type service --identifier my-service --property tier=Tier-1 --property "languages=Ruby,TypeScript"',
153
164
  },
165
+ {
166
+ label: "Set aliases while preserving omitted fields",
167
+ command: "dx catalog entities upsert --type service --identifier my-service --alias github_repo=12345",
168
+ },
154
169
  ]))
155
170
  .action(wrapAction(async (options, command) => {
156
171
  if (!options.type) {
@@ -160,11 +175,11 @@ export function entitiesCommand() {
160
175
  throw new CliError("--identifier is required", EXIT_CODES.ARGUMENT_ERROR);
161
176
  }
162
177
  const runtime = buildRuntime(getContext(command));
163
- const properties = await resolvePropertiesForEntityType(runtime, options.type, options.property);
178
+ const resolvedValues = await resolveValuesForEntityType(runtime, options.type, options.property, options.alias);
164
179
  const response = await upsertEntity(runtime, options.identifier, {
165
180
  type: options.type,
166
181
  ...getEntityMutationOptionValues(options),
167
- properties,
182
+ ...resolvedValues,
168
183
  });
169
184
  if (runtime.context.json) {
170
185
  renderJson(response);
@@ -173,7 +188,7 @@ export function entitiesCommand() {
173
188
  const title = response.result === "created_new_entity"
174
189
  ? `${ui.success(ui.GLYPHS.CHECK)} Created new entity`
175
190
  : `${ui.success(ui.GLYPHS.CHECK)} Updated existing entity`;
176
- renderEntity(response.entity, title);
191
+ renderEntity(response.entity, runtime, title);
177
192
  }
178
193
  }));
179
194
  entities
@@ -202,7 +217,7 @@ export function entitiesCommand() {
202
217
  renderJson(processedResponse);
203
218
  }
204
219
  else {
205
- renderEntity(processedResponse.entity);
220
+ renderEntity(processedResponse.entity, runtime);
206
221
  }
207
222
  }));
208
223
  entities
@@ -245,7 +260,7 @@ export function entitiesCommand() {
245
260
  renderJson({ ...response, entities: processedEntities });
246
261
  }
247
262
  else {
248
- renderEntityList(processedEntities, response.response_metadata?.next_cursor ?? null);
263
+ renderEntityList(processedEntities, response.response_metadata?.next_cursor ?? null, runtime);
249
264
  }
250
265
  }));
251
266
  entities
@@ -311,7 +326,7 @@ export function entitiesCommand() {
311
326
  renderJson(response);
312
327
  }
313
328
  else {
314
- renderEntityScorecardList(response.scorecards, response.response_metadata?.next_cursor ?? null);
329
+ renderEntityScorecardList(response.scorecards, identifier, runtime, response.response_metadata?.next_cursor ?? null);
315
330
  }
316
331
  }));
317
332
  return entities;
@@ -459,6 +474,33 @@ export function parsePropertyValue(prop, rawValue) {
459
474
  return rawValue;
460
475
  }
461
476
  }
477
+ function parseEntityAliases(entityTypeIdentifier, rawAliases, knownAliases) {
478
+ const aliasKeys = Object.keys(knownAliases);
479
+ const aliasSet = new Set(aliasKeys);
480
+ const result = {};
481
+ for (const kv of rawAliases) {
482
+ const eqIndex = kv.indexOf("=");
483
+ if (eqIndex === -1) {
484
+ throw new CliError(`Invalid --alias "${kv}": expected format key=value`, EXIT_CODES.ARGUMENT_ERROR);
485
+ }
486
+ const key = kv.slice(0, eqIndex);
487
+ const rawValue = kv.slice(eqIndex + 1);
488
+ if (!aliasSet.has(key)) {
489
+ const availableAliases = aliasKeys
490
+ .sort()
491
+ .map((k) => `\`${k}\``)
492
+ .join(", ");
493
+ const availableAliasesText = availableAliases.length > 0 ? availableAliases : "(none)";
494
+ throw new CliError(`Unknown alias \`${key}\` for entity type \`${entityTypeIdentifier}\`. Available aliases: ${availableAliasesText}`, EXIT_CODES.ARGUMENT_ERROR);
495
+ }
496
+ if (rawValue === "null") {
497
+ result[key] = [];
498
+ continue;
499
+ }
500
+ result[key] = [...(result[key] ?? []), { identifier: rawValue }];
501
+ }
502
+ return result;
503
+ }
462
504
  // --- Include helpers ---
463
505
  const ENTITY_INCLUDE_SECTIONS = [
464
506
  "core",
@@ -487,22 +529,31 @@ function parseIncludeSections(include) {
487
529
  }
488
530
  return results;
489
531
  }
490
- async function resolvePropertiesForEntityType(runtime, entityTypeIdentifier, rawProperties) {
491
- if (rawProperties.length === 0) {
492
- return undefined;
532
+ async function resolveValuesForEntityType(runtime, entityTypeIdentifier, rawProperties, rawAliases) {
533
+ if (rawProperties.length === 0 && rawAliases.length === 0) {
534
+ return {};
493
535
  }
494
536
  let entityTypeResponse;
495
537
  try {
496
538
  entityTypeResponse = await getEntityType(runtime, entityTypeIdentifier);
497
539
  }
498
540
  catch (err) {
499
- throw wrapEntityTypeLookupError(entityTypeIdentifier, err);
541
+ throw wrapEntityTypeLookupError(entityTypeIdentifier, err, rawAliases.length > 0
542
+ ? "resolve property and alias values"
543
+ : "resolve property types");
544
+ }
545
+ const values = {};
546
+ if (rawProperties.length > 0) {
547
+ values.properties = parseEntityProperties(entityTypeIdentifier, rawProperties, entityTypeResponse.entity_type.properties);
500
548
  }
501
- return parseEntityProperties(entityTypeIdentifier, rawProperties, entityTypeResponse.entity_type.properties);
549
+ if (rawAliases.length > 0) {
550
+ values.aliases = parseEntityAliases(entityTypeIdentifier, rawAliases, entityTypeResponse.entity_type.aliases);
551
+ }
552
+ return values;
502
553
  }
503
- async function resolvePropertiesForExistingEntity(runtime, identifier, rawProperties) {
504
- if (rawProperties.length === 0) {
505
- return undefined;
554
+ async function resolveValuesForExistingEntity(runtime, identifier, rawProperties, rawAliases) {
555
+ if (rawProperties.length === 0 && rawAliases.length === 0) {
556
+ return {};
506
557
  }
507
558
  let entityResponse;
508
559
  try {
@@ -512,9 +563,23 @@ async function resolvePropertiesForExistingEntity(runtime, identifier, rawProper
512
563
  const exitCode = err instanceof HttpError && err.status !== undefined && err.status < 500
513
564
  ? EXIT_CODES.ARGUMENT_ERROR
514
565
  : EXIT_CODES.RETRY_RECOMMENDED;
515
- throw new CliError(`Failed to fetch entity "${identifier}" to resolve property types: ${err instanceof Error ? err.message : String(err)}`, exitCode);
566
+ throw new CliError(`Failed to fetch entity "${identifier}" to resolve property and alias values: ${err instanceof Error ? err.message : String(err)}`, exitCode);
567
+ }
568
+ let entityTypeResponse;
569
+ try {
570
+ entityTypeResponse = await getEntityType(runtime, entityResponse.entity.type);
571
+ }
572
+ catch (err) {
573
+ throw wrapEntityTypeLookupError(entityResponse.entity.type, err, "resolve property and alias values");
574
+ }
575
+ const values = {};
576
+ if (rawProperties.length > 0) {
577
+ values.properties = parseEntityProperties(entityResponse.entity.type, rawProperties, entityTypeResponse.entity_type.properties);
578
+ }
579
+ if (rawAliases.length > 0) {
580
+ values.aliases = parseEntityAliases(entityResponse.entity.type, rawAliases, entityTypeResponse.entity_type.aliases);
516
581
  }
517
- return resolvePropertiesForEntityType(runtime, entityResponse.entity.type, rawProperties);
582
+ return values;
518
583
  }
519
584
  function getEntityMutationOptionValues(options) {
520
585
  return {
@@ -549,13 +614,15 @@ function buildEntityMutationBody(identifier, params) {
549
614
  body.owner_user_ids = params.owner_user_ids;
550
615
  if (params.properties !== undefined)
551
616
  body.properties = params.properties;
617
+ if (params.aliases !== undefined)
618
+ body.aliases = params.aliases;
552
619
  return body;
553
620
  }
554
- function wrapEntityTypeLookupError(entityTypeIdentifier, err) {
621
+ function wrapEntityTypeLookupError(entityTypeIdentifier, err, purpose = "resolve property types") {
555
622
  const exitCode = err instanceof HttpError && err.status !== undefined && err.status < 500
556
623
  ? EXIT_CODES.ARGUMENT_ERROR
557
624
  : EXIT_CODES.RETRY_RECOMMENDED;
558
- return new CliError(`Failed to fetch entity type "${entityTypeIdentifier}" to resolve property types: ${err instanceof Error ? err.message : String(err)}`, exitCode);
625
+ return new CliError(`Failed to fetch entity type "${entityTypeIdentifier}" to ${purpose}: ${err instanceof Error ? err.message : String(err)}`, exitCode);
559
626
  }
560
627
  function uniqueSectionsInOrder(sections) {
561
628
  const seen = new Set();
@@ -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) {