@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 +2 -44
- package/dist/commands/auth.js +32 -3
- package/dist/commands/authRendering.js +42 -1
- package/dist/commands/catalog/entities.js +59 -13
- package/dist/commands/init.js +2 -2
- package/package.json +1 -1
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
|
-
|
|
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
|
-
|
|
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
|
|
package/dist/commands/auth.js
CHANGED
|
@@ -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
|
|
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("
|
|
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 "
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
502
|
-
if (
|
|
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 =
|
|
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 (
|
|
557
|
+
if (lookup === "null") {
|
|
516
558
|
result[key] = [];
|
|
517
559
|
continue;
|
|
518
560
|
}
|
|
519
|
-
|
|
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
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -83,12 +83,12 @@ async function attemptLogin(apiBaseUrl, webBaseUrl, context) {
|
|
|
83
83
|
}
|
|
84
84
|
else {
|
|
85
85
|
token = await password({
|
|
86
|
-
message: "Paste your
|
|
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("
|
|
91
|
+
throw new CliError("Personal access token or organization token is required");
|
|
92
92
|
}
|
|
93
93
|
const runtime = await buildRuntime(context, {
|
|
94
94
|
apiBaseUrl,
|