@get-dx/cli 0.5.0 → 0.5.2

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,13 +18,6 @@ Interactively login and install the AI agent skill:
18
18
  dx init
19
19
  ```
20
20
 
21
- For CI, Docker/dev containers, or other headless environments, pass the token
22
- through the environment instead of storing it in the OS credential store:
23
-
24
- ```shell
25
- DX_API_TOKEN="$DX_TOKEN" dx auth status
26
- ```
27
-
28
21
  Show help text:
29
22
 
30
23
  ```shell
@@ -32,44 +25,9 @@ dx --help
32
25
  dx <subcommand> --help
33
26
  ```
34
27
 
35
- ### Customizing base URLs
36
-
37
- 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.
38
-
39
- | Value | How it is used | Env var | Default value |
40
- | ---------------- | -------------------------------------------- | ----------------- | ----------------------- |
41
- | **Web base URL** | Browser-based login and displaying web links | `DX_WEB_BASE_URL` | `https://app.getdx.com` |
42
- | **API base URL** | Making each API request to DX | `DX_API_BASE_URL` | `https://api.getdx.com` |
43
-
44
- #### For dedicated deployments
45
-
46
- Set the env vars once when initializing:
47
-
48
- ```shell
49
- # Interactive login
50
- DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" dx init
51
-
52
- # Non-interactive use for CI, containers, or remote agents
53
- DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" DX_API_TOKEN="$DX_TOKEN" dx auth status
54
- ```
55
-
56
- #### For managed deployments
57
-
58
- Set the env vars once when initializing:
59
-
60
- ```shell
61
- # Interactive login
62
- 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
63
-
64
- # Non-interactive use for CI, containers, or remote agents
65
- DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" DX_API_TOKEN="$DX_TOKEN" dx auth status
66
- ```
67
-
68
- ## Logging
69
-
70
- Set `DX_LOG_LEVEL` to one of `debug`, `info`, `warn`, or `error` to enable CLI logs.
28
+ ## Documentation
71
29
 
72
- Logs are always written to `stderr`. They are human-readable by default, and switch to JSON when `--json` is present or `stderr` is not a TTY.
30
+ For usage instructions and for more information about configuration, visit the [DX CLI documentation](https://docs.getdx.com/cli/).
73
31
 
74
32
  ## Developing the CLI
75
33
 
@@ -2,7 +2,7 @@ import { password, select } from "@inquirer/prompts";
2
2
  import { Command } from "commander";
3
3
  import { deleteToken, setToken } from "../secrets.js";
4
4
  import { renderJson } from "../renderers.js";
5
- import { renderAuthInfo, renderLoggedOut } from "./authRendering.js";
5
+ import { renderAuthInfo, renderAuthWhoami, renderLoggedOut, } from "./authRendering.js";
6
6
  import { createExampleText, getContext, wrapAction, } from "../commandHelpers.js";
7
7
  import { persistBaseUrls, resolveApiBaseUrl, deriveBaseUrlsFromEnv, } from "../config.js";
8
8
  import { CliError } from "../errors.js";
@@ -51,12 +51,12 @@ export function authCommand() {
51
51
  }
52
52
  else {
53
53
  token = await password({
54
- message: "Paste your account web API token here:",
54
+ message: "Paste your personal access token or organization token here:",
55
55
  mask: true,
56
56
  });
57
57
  }
58
58
  if (!token) {
59
- throw new CliError("Account web API token or personal access token is required");
59
+ throw new CliError("Personal access token or organization token is required");
60
60
  }
61
61
  }
62
62
  const runtime = await buildRuntime(context, {
@@ -128,6 +128,29 @@ export function authCommand() {
128
128
  renderAuthInfo(response, runtime.token, runtime.apiBaseUrl);
129
129
  }
130
130
  }));
131
+ auth
132
+ .command("whoami")
133
+ .description("Show user and team details for the authenticated token")
134
+ .addHelpText("afterAll", createExampleText([
135
+ {
136
+ label: "Fetch user and team details",
137
+ command: "dx auth whoami",
138
+ },
139
+ {
140
+ label: "Fetch user and team details as JSON",
141
+ command: "dx auth whoami --json",
142
+ },
143
+ ]))
144
+ .action(wrapAction(async (_options, command) => {
145
+ const runtime = await buildRuntime(getContext(command));
146
+ const response = await getAuthWhoami(runtime);
147
+ if (runtime.context.json) {
148
+ renderJson(response);
149
+ }
150
+ else {
151
+ renderAuthWhoami(response);
152
+ }
153
+ }));
131
154
  return auth;
132
155
  }
133
156
  // TODO: should we move this somewhere more central, since `init` needs it?
@@ -137,3 +160,9 @@ export async function getAuthInfo(runtime) {
137
160
  });
138
161
  return response.body;
139
162
  }
163
+ export async function getAuthWhoami(runtime) {
164
+ const response = await request(runtime, "/auth.whoami", {
165
+ method: "GET",
166
+ });
167
+ return response.body;
168
+ }
@@ -37,10 +37,51 @@ function scopesContent(scopes) {
37
37
  ];
38
38
  }
39
39
  }
40
+ export function renderAuthWhoami(response) {
41
+ const blocks = [];
42
+ // Auth section
43
+ blocks.push(ui.h3("Auth"));
44
+ blocks.push(ui.dl([
45
+ ui.dli("Account", response.account.name),
46
+ ui.dli("Token type", tokenTypeName(response.auth_token_type)),
47
+ ], { termWidth: 12 }));
48
+ // User section
49
+ blocks.push(ui.h3("Current user"));
50
+ if (response.user) {
51
+ blocks.push(ui.dl([
52
+ ui.dli("ID", ui.code(response.user.id)),
53
+ ui.dli("Name", response.user.name),
54
+ ui.dli("Email", response.user.email),
55
+ ], { termWidth: 7 }));
56
+ }
57
+ else {
58
+ blocks.push(ui.p(ui.dim("Not available for organization tokens")));
59
+ }
60
+ // Team section
61
+ blocks.push(ui.h3("Team"));
62
+ if (response.team) {
63
+ blocks.push(ui.dl([
64
+ ui.dli("ID", ui.code(response.team.id)),
65
+ ui.dli("Name", response.team.name),
66
+ ui.dli("Lead", formatUser(response.team.lead)),
67
+ ], { termWidth: 6 }));
68
+ if (response.team.contributors.length > 0) {
69
+ blocks.push(ui.p(ui.bold("Contributors:"), false));
70
+ blocks.push(ui.ul(response.team.contributors.map((c) => ui.li(formatUser(c)))));
71
+ }
72
+ }
73
+ else {
74
+ blocks.push(ui.p(ui.dim("Not available for organization tokens")));
75
+ }
76
+ renderRichText(blocks);
77
+ }
78
+ function formatUser(user) {
79
+ return `${user.name} ${ui.dim(`(${user.id}, ${user.email})`)}`;
80
+ }
40
81
  function tokenTypeName(tokenType) {
41
82
  switch (tokenType) {
42
83
  case "account_web_api_token":
43
- return "Account-level web API token";
84
+ return "Organization token";
44
85
  case "personal_access_token":
45
86
  return "Personal access token";
46
87
  default:
@@ -22,7 +22,7 @@ export function entitiesCommand() {
22
22
  .option("--owner-user-emails <emails>", "Comma-separated owner user emails")
23
23
  .option("--owner-user-ids <ids>", "Comma-separated owner user IDs")
24
24
  .option("--property <kv>", "Set a property value as key=value. Repeat for multiple properties.", (val, prev) => [...prev, val], [])
25
- .option("--alias <kv>", "Set an alias value as key=value. Repeat for multiple aliases.", (val, prev) => [...prev, val], [])
25
+ .option("--alias <kv>", "Set an alias as <type>=<identifier> (short form) or type=<type>,identifier=<id>[,instance_identifier=<instance_id>] (long form). Repeat for multiple aliases.", (val, prev) => [...prev, val], [])
26
26
  .addHelpText("afterAll", createExampleText([
27
27
  {
28
28
  label: "Create a new service entity",
@@ -41,9 +41,13 @@ export function entitiesCommand() {
41
41
  command: 'dx catalog entities create --type service --identifier my-service --property tier=Tier-1 --property "languages=Ruby,TypeScript"',
42
42
  },
43
43
  {
44
- label: "Create with aliases",
44
+ label: "Create with alias (short form)",
45
45
  command: "dx catalog entities create --type service --identifier my-service --alias github_repo=12345",
46
46
  },
47
+ {
48
+ label: "Create with alias (long form, includes instance identifier)",
49
+ command: 'dx catalog entities create --type service --identifier my-service --alias "type=github_repo,identifier=12345,instance_identifier=789"',
50
+ },
47
51
  ]))
48
52
  .action(wrapAction(async (options, command) => {
49
53
  if (!options.identifier) {
@@ -104,7 +108,7 @@ export function entitiesCommand() {
104
108
  .option("--owner-user-emails <emails>", "Comma-separated owner user emails")
105
109
  .option("--owner-user-ids <ids>", "Comma-separated owner user IDs")
106
110
  .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], [])
107
- .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], [])
111
+ .option("--alias <kv>", "Set an alias as <type>=<identifier> (short form) or type=<type>,identifier=<id>[,instance_identifier=<instance_id>] (long form). Repeat for multiple aliases. Use value null to remove an alias.", (val, prev) => [...prev, val], [])
108
112
  .addHelpText("afterAll", createExampleText([
109
113
  {
110
114
  label: "Rename an entity",
@@ -119,9 +123,13 @@ export function entitiesCommand() {
119
123
  command: 'dx catalog entities update my-service --property tier=Tier-1 --property "languages=Ruby,TypeScript"',
120
124
  },
121
125
  {
122
- label: "Update aliases",
126
+ label: "Update alias (short form)",
123
127
  command: "dx catalog entities update my-service --alias github_repo=12345",
124
128
  },
129
+ {
130
+ label: "Update alias (long form, includes instance identifier)",
131
+ command: 'dx catalog entities update my-service --alias "type=github_repo,identifier=12345,instance_identifier=789"',
132
+ },
125
133
  ]))
126
134
  .action(wrapAction(async (identifier, options, command) => {
127
135
  const runtime = await buildRuntime(getContext(command));
@@ -148,7 +156,7 @@ export function entitiesCommand() {
148
156
  .option("--owner-user-emails <emails>", "Comma-separated owner user emails")
149
157
  .option("--owner-user-ids <ids>", "Comma-separated owner user IDs")
150
158
  .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], [])
159
+ .option("--alias <kv>", "Set an alias as <type>=<identifier> (short form) or type=<type>,identifier=<id>[,instance_identifier=<instance_id>] (long form). Repeat for multiple aliases. Use value null to remove an alias.", (val, prev) => [...prev, val], [])
152
160
  .addHelpText("afterAll", createExampleText([
153
161
  {
154
162
  label: "Create a new service entity if it does not exist",
@@ -163,9 +171,13 @@ export function entitiesCommand() {
163
171
  command: 'dx catalog entities upsert --type service --identifier my-service --property tier=Tier-1 --property "languages=Ruby,TypeScript"',
164
172
  },
165
173
  {
166
- label: "Set aliases while preserving omitted fields",
174
+ label: "Set alias (short form) while preserving omitted fields",
167
175
  command: "dx catalog entities upsert --type service --identifier my-service --alias github_repo=12345",
168
176
  },
177
+ {
178
+ label: "Set alias (long form, includes instance identifier)",
179
+ command: 'dx catalog entities upsert --type service --identifier my-service --alias "type=github_repo,identifier=12345,instance_identifier=789"',
180
+ },
169
181
  ]))
170
182
  .action(wrapAction(async (options, command) => {
171
183
  if (!options.type) {
@@ -493,17 +505,47 @@ export function parsePropertyValue(prop, rawValue) {
493
505
  return rawValue;
494
506
  }
495
507
  }
508
+ function parseAliasInput(kv) {
509
+ // Long form: type=<alias_type>,identifier=<lookup>[,instance_identifier=<instance_id>]
510
+ if (kv.includes(",")) {
511
+ const pairs = {};
512
+ let validPairs = true;
513
+ for (const pair of kv.split(",")) {
514
+ const eqIdx = pair.indexOf("=");
515
+ if (eqIdx === -1) {
516
+ validPairs = false;
517
+ break;
518
+ }
519
+ pairs[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
520
+ }
521
+ if (validPairs &&
522
+ pairs.type !== undefined &&
523
+ pairs.identifier !== undefined) {
524
+ return {
525
+ key: pairs.type,
526
+ lookup: pairs.identifier,
527
+ ...(pairs.instance_identifier !== undefined
528
+ ? { instance_identifier: pairs.instance_identifier }
529
+ : {}),
530
+ };
531
+ }
532
+ }
533
+ // Short form: <alias_type>=<lookup>
534
+ const eqIndex = kv.indexOf("=");
535
+ if (eqIndex === -1)
536
+ return null;
537
+ return { key: kv.slice(0, eqIndex), lookup: kv.slice(eqIndex + 1) };
538
+ }
496
539
  function parseEntityAliases(entityTypeIdentifier, rawAliases, knownAliases) {
497
540
  const aliasKeys = Object.keys(knownAliases);
498
541
  const aliasSet = new Set(aliasKeys);
499
542
  const result = {};
500
543
  for (const kv of rawAliases) {
501
- const eqIndex = kv.indexOf("=");
502
- if (eqIndex === -1) {
503
- throw new CliError(`Invalid --alias "${kv}": expected format key=value`, EXIT_CODES.ARGUMENT_ERROR);
544
+ const parsed = parseAliasInput(kv);
545
+ if (!parsed) {
546
+ throw new CliError(`Invalid --alias "${kv}": expected format key=value or type=<type>,identifier=<id>[,instance_identifier=<instance_id>]`, EXIT_CODES.ARGUMENT_ERROR);
504
547
  }
505
- const key = kv.slice(0, eqIndex);
506
- const rawValue = kv.slice(eqIndex + 1);
548
+ const { key, lookup, instance_identifier } = parsed;
507
549
  if (!aliasSet.has(key)) {
508
550
  const availableAliases = aliasKeys
509
551
  .sort()
@@ -512,11 +554,15 @@ function parseEntityAliases(entityTypeIdentifier, rawAliases, knownAliases) {
512
554
  const availableAliasesText = availableAliases.length > 0 ? availableAliases : "(none)";
513
555
  throw new CliError(`Unknown alias \`${key}\` for entity type \`${entityTypeIdentifier}\`. Available aliases: ${availableAliasesText}`, EXIT_CODES.ARGUMENT_ERROR);
514
556
  }
515
- if (rawValue === "null") {
557
+ if (lookup === "null") {
516
558
  result[key] = [];
517
559
  continue;
518
560
  }
519
- result[key] = [...(result[key] ?? []), { lookup: rawValue }];
561
+ const aliasEntry = { lookup };
562
+ if (instance_identifier !== undefined) {
563
+ aliasEntry.instance_identifier = instance_identifier;
564
+ }
565
+ result[key] = [...(result[key] ?? []), aliasEntry];
520
566
  }
521
567
  return result;
522
568
  }
@@ -83,12 +83,12 @@ async function attemptLogin(apiBaseUrl, webBaseUrl, context) {
83
83
  }
84
84
  else {
85
85
  token = await password({
86
- message: "Paste your account web API token here:",
86
+ message: "Paste your personal access token or organization token here:",
87
87
  mask: true,
88
88
  });
89
89
  }
90
90
  if (!token) {
91
- throw new CliError("Account web API token is required");
91
+ throw new CliError("Personal access token or organization token is required");
92
92
  }
93
93
  const runtime = await buildRuntime(context, {
94
94
  apiBaseUrl,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@get-dx/cli",
3
- "version": "0.5.0",
3
+ "version": "0.5.2",
4
4
  "description": "DX CLI",
5
5
  "author": "DX <developers@getdx.com>",
6
6
  "bugs": {