@get-dx/cli 0.1.5 → 0.1.7
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 +20 -0
- package/dist/commands/authRendering.js +7 -0
- package/dist/commands/init.js +8 -14
- package/dist/commands/teams.js +198 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,26 @@ 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.
|
|
22
|
+
|
|
23
|
+
### Dedicated or managed deployments
|
|
24
|
+
|
|
25
|
+
If your organization is on a dedicated or managed DX deployment, pass your custom hostname at init time:
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
# Flag (dedicated instance at mycompany.getdx.io)
|
|
29
|
+
dx init --host mycompany.getdx.io
|
|
30
|
+
|
|
31
|
+
# Env var — useful for CI or scripted setups
|
|
32
|
+
DX_HOST=mycompany.getdx.io dx init
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The `DX_HOST` and `--host` values accept:
|
|
36
|
+
|
|
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
|
+
|
|
21
41
|
Show help text:
|
|
22
42
|
|
|
23
43
|
```shell
|
|
@@ -9,6 +9,13 @@ export function renderAuthInfo(authInfo, token, baseUrl) {
|
|
|
9
9
|
ui.dli("Token type", tokenTypeName(authInfo.auth.token_type)),
|
|
10
10
|
ui.dli("Token name", authInfo.auth.token_name),
|
|
11
11
|
ui.dli("Token created at", ui.timestampSummary(authInfo.auth.created_at)),
|
|
12
|
+
...(authInfo.auth.token_type === "personal_access_token"
|
|
13
|
+
? [
|
|
14
|
+
ui.dli("Token expires at", authInfo.auth.expires_at
|
|
15
|
+
? ui.timestampSummary(authInfo.auth.expires_at)
|
|
16
|
+
: ui.dim("(no expiration)")),
|
|
17
|
+
]
|
|
18
|
+
: []),
|
|
12
19
|
], { termWidth: 18 }),
|
|
13
20
|
ui.p(ui.bold("Token scopes:"), false),
|
|
14
21
|
...scopesContent(authInfo.auth.scopes),
|
package/dist/commands/init.js
CHANGED
|
@@ -50,12 +50,14 @@ export function initCommand() {
|
|
|
50
50
|
const init = new Command()
|
|
51
51
|
.name("init")
|
|
52
52
|
.description("Initialize the DX CLI")
|
|
53
|
-
.
|
|
53
|
+
.option("--host <hostname>", "DX hostname for dedicated or managed deployments (e.g. mycompany.getdx.io)")
|
|
54
|
+
.action(wrapAction(async (commandOptions, command) => {
|
|
54
55
|
const context = getContext(command);
|
|
55
56
|
let runtime = buildRuntimeSafe(context);
|
|
56
57
|
ensureInteractive();
|
|
57
58
|
showWelcomeBanner();
|
|
58
|
-
|
|
59
|
+
const host = commandOptions.host || process.env.DX_HOST;
|
|
60
|
+
runtime = await ensureLoggedIn(runtime, host);
|
|
59
61
|
await optionallySetupSkill(runtime);
|
|
60
62
|
renderRichText([
|
|
61
63
|
ui.blankLine(),
|
|
@@ -72,7 +74,7 @@ function ensureInteractive() {
|
|
|
72
74
|
function showWelcomeBanner() {
|
|
73
75
|
renderRichText([ui.p(ui.success(WELCOME_BANNER))]);
|
|
74
76
|
}
|
|
75
|
-
async function ensureLoggedIn(runtime) {
|
|
77
|
+
async function ensureLoggedIn(runtime, host) {
|
|
76
78
|
renderRichText([ui.h1(`Checking if you are logged in...`), ui.blankLine()]);
|
|
77
79
|
if (runtime) {
|
|
78
80
|
let authInfo = null;
|
|
@@ -88,17 +90,9 @@ async function ensureLoggedIn(runtime) {
|
|
|
88
90
|
}
|
|
89
91
|
}
|
|
90
92
|
renderRichText([ui.p(`You are not logged in yet.`), ui.blankLine()]);
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
message: "What is your DX hostname? (leave blank for app.getdx.com)",
|
|
95
|
-
});
|
|
96
|
-
parsed = parseHostname(raw);
|
|
97
|
-
if (parsed.type === "invalid") {
|
|
98
|
-
renderRichText([
|
|
99
|
-
ui.p(ui.error(`Could not recognise that hostname. Please try again.`)),
|
|
100
|
-
]);
|
|
101
|
-
}
|
|
93
|
+
const parsed = host ? parseHostname(host) : { type: "cloud" };
|
|
94
|
+
if (parsed.type === "invalid") {
|
|
95
|
+
throw new CliError(`Could not recognize hostname "${host}". Expected app.getdx.com, <account>.getdx.io, or a custom domain.`);
|
|
102
96
|
}
|
|
103
97
|
switch (parsed.type) {
|
|
104
98
|
case "cloud":
|
package/dist/commands/teams.js
CHANGED
|
@@ -1,11 +1,67 @@
|
|
|
1
1
|
import { Command } from "commander";
|
|
2
2
|
import { createExampleText, getContext, wrapAction, } from "../commandHelpers.js";
|
|
3
|
+
import { CliError, EXIT_CODES, HttpError } from "../errors.js";
|
|
3
4
|
import { request } from "../http.js";
|
|
4
5
|
import { renderJson, renderRichText } from "../renderers.js";
|
|
5
6
|
import { buildRuntime } from "../runtime.js";
|
|
6
7
|
import * as ui from "../ui.js";
|
|
7
8
|
export function teamsCommand() {
|
|
8
9
|
const teams = new Command().name("teams").description("Manage DX teams");
|
|
10
|
+
teams
|
|
11
|
+
.command("findByMembers")
|
|
12
|
+
.description("Find a team by member email addresses")
|
|
13
|
+
.option("--team-emails <emails>", "Comma-separated list of team member email addresses")
|
|
14
|
+
.addHelpText("afterAll", createExampleText([
|
|
15
|
+
{
|
|
16
|
+
label: "Find the team for a group of members",
|
|
17
|
+
command: "dx teams findByMembers --team-emails person@example.com,another@example.com",
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
label: "Find a team and return JSON",
|
|
21
|
+
command: "dx --json teams findByMembers --team-emails person@example.com,another@example.com",
|
|
22
|
+
},
|
|
23
|
+
]))
|
|
24
|
+
.action(wrapAction(async (options, command) => {
|
|
25
|
+
const teamEmails = parseTeamEmails(options.teamEmails);
|
|
26
|
+
const runtime = buildRuntime(getContext(command));
|
|
27
|
+
const response = await findTeamByMembers(runtime, teamEmails);
|
|
28
|
+
if (runtime.context.json) {
|
|
29
|
+
renderJson(response);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
renderTeamByMembers(response.team);
|
|
33
|
+
}
|
|
34
|
+
}));
|
|
35
|
+
teams
|
|
36
|
+
.command("info")
|
|
37
|
+
.description("Retrieve details for an individual team")
|
|
38
|
+
.option("--team-id <id>", "DX team ID")
|
|
39
|
+
.option("--reference-id <id>", "Team ID that is internal to your organization")
|
|
40
|
+
.addHelpText("afterAll", createExampleText([
|
|
41
|
+
{
|
|
42
|
+
label: "Fetch info for a team by DX team ID",
|
|
43
|
+
command: "dx teams info --team-id NTA4Nzc",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
label: "Fetch info for a team by your organization's reference ID",
|
|
47
|
+
command: "dx teams info --reference-id 06BEC4E0-5A61-354E-08A6-C39D756058AB",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
label: "Fetch team info and return JSON",
|
|
51
|
+
command: "dx --json teams info --team-id NTA4Nzc",
|
|
52
|
+
},
|
|
53
|
+
]))
|
|
54
|
+
.action(wrapAction(async (options, command) => {
|
|
55
|
+
const lookup = parseTeamInfoLookup(options);
|
|
56
|
+
const runtime = buildRuntime(getContext(command));
|
|
57
|
+
const response = await getTeamInfo(runtime, lookup);
|
|
58
|
+
if (runtime.context.json) {
|
|
59
|
+
renderJson(response);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
renderTeamInfo(response.team);
|
|
63
|
+
}
|
|
64
|
+
}));
|
|
9
65
|
teams
|
|
10
66
|
.command("list")
|
|
11
67
|
.description("List all teams in DX")
|
|
@@ -31,12 +87,74 @@ export function teamsCommand() {
|
|
|
31
87
|
}));
|
|
32
88
|
return teams;
|
|
33
89
|
}
|
|
90
|
+
async function findTeamByMembers(runtime, teamEmails) {
|
|
91
|
+
try {
|
|
92
|
+
const response = await request(runtime, "/teams.findByMembers", {
|
|
93
|
+
method: "GET",
|
|
94
|
+
query: { team_emails: teamEmails },
|
|
95
|
+
});
|
|
96
|
+
return response.body;
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
if (runtime.context.json) {
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
throw buildFindByMembersError(error, teamEmails);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function getTeamInfo(runtime, lookup) {
|
|
106
|
+
const response = await request(runtime, "/teams.info", {
|
|
107
|
+
method: "GET",
|
|
108
|
+
query: lookup,
|
|
109
|
+
});
|
|
110
|
+
return response.body;
|
|
111
|
+
}
|
|
34
112
|
async function listTeams(runtime) {
|
|
35
113
|
const response = await request(runtime, "/teams.list", {
|
|
36
114
|
method: "GET",
|
|
37
115
|
});
|
|
38
116
|
return response.body;
|
|
39
117
|
}
|
|
118
|
+
function renderTeamByMembers(team) {
|
|
119
|
+
const blocks = [
|
|
120
|
+
ui.h1("Team"),
|
|
121
|
+
ui.h2(`${team.name} (${ui.code(team.id)})`),
|
|
122
|
+
ui.dl([
|
|
123
|
+
ui.dli("Manager", formatTeamMember(team.manager)),
|
|
124
|
+
ui.dli("Members", team.members.length > 0
|
|
125
|
+
? [
|
|
126
|
+
ui.ul(team.members.map((member) => ui.li(formatTeamMember(member)))),
|
|
127
|
+
]
|
|
128
|
+
: ui.dim("(None)")),
|
|
129
|
+
], { termWidth: 8 }),
|
|
130
|
+
];
|
|
131
|
+
renderRichText(blocks);
|
|
132
|
+
}
|
|
133
|
+
function renderTeamInfo(team) {
|
|
134
|
+
const blocks = [ui.h1("Team Information")];
|
|
135
|
+
const heading = formatTeamHeading(team);
|
|
136
|
+
if (heading) {
|
|
137
|
+
blocks.push(ui.h2(heading));
|
|
138
|
+
}
|
|
139
|
+
const details = [];
|
|
140
|
+
pushDetail(details, "ID", formatText(team.id), team.id !== undefined);
|
|
141
|
+
pushDetail(details, "Name", team.name ?? ui.dim("(None)"), team.name !== undefined);
|
|
142
|
+
pushDetail(details, "Reference ID", formatText(team.reference_id), team.reference_id !== undefined);
|
|
143
|
+
pushDetail(details, "Parent ID", formatText(team.parent_id), team.parent_id !== undefined);
|
|
144
|
+
pushDetail(details, "Manager ID", formatText(team.manager_id), team.manager_id !== undefined);
|
|
145
|
+
pushDetail(details, "Parent team", typeof team.parent === "boolean" ? (team.parent ? "Yes" : "No") : "", team.parent !== undefined);
|
|
146
|
+
pushDetail(details, "Last changed", team.last_changed_at
|
|
147
|
+
? ui.timestampSummary(team.last_changed_at)
|
|
148
|
+
: ui.dim("(None)"), team.last_changed_at !== undefined);
|
|
149
|
+
pushDetail(details, "Lead", formatTeamMember(team.lead), team.lead !== undefined);
|
|
150
|
+
pushDetail(details, "Manager", formatTeamMember(team.manager), team.manager !== undefined);
|
|
151
|
+
if (details.length > 0) {
|
|
152
|
+
blocks.push(ui.dl(details, { termWidth: 14 }));
|
|
153
|
+
}
|
|
154
|
+
blocks.push(ui.h2("Contributors"));
|
|
155
|
+
blocks.push(formatTeamMemberList(team.contributors));
|
|
156
|
+
renderRichText(blocks);
|
|
157
|
+
}
|
|
40
158
|
function renderTeams(teams) {
|
|
41
159
|
const blocks = [ui.h1("Teams")];
|
|
42
160
|
blocks.push(ui.p(`Displaying ${ui.bold(teams.length.toString())} teams.`));
|
|
@@ -67,3 +185,83 @@ function renderTeams(teams) {
|
|
|
67
185
|
function formatText(value) {
|
|
68
186
|
return value ? ui.code(value) : ui.dim("(None)");
|
|
69
187
|
}
|
|
188
|
+
function formatTeamHeading(team) {
|
|
189
|
+
if (team.name && team.id) {
|
|
190
|
+
return `${team.name} (${ui.code(team.id)})`;
|
|
191
|
+
}
|
|
192
|
+
if (team.name) {
|
|
193
|
+
return team.name;
|
|
194
|
+
}
|
|
195
|
+
if (team.id) {
|
|
196
|
+
return ui.code(team.id);
|
|
197
|
+
}
|
|
198
|
+
if (team.reference_id) {
|
|
199
|
+
return ui.code(team.reference_id);
|
|
200
|
+
}
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
function formatTeamMember(member) {
|
|
204
|
+
if (!member) {
|
|
205
|
+
return ui.dim("(None)");
|
|
206
|
+
}
|
|
207
|
+
return `${member.name} <${member.email}> (${ui.code(member.id)})`;
|
|
208
|
+
}
|
|
209
|
+
function formatTeamMemberList(members) {
|
|
210
|
+
if (!members || members.length === 0) {
|
|
211
|
+
return ui.p(ui.dim("(None)"));
|
|
212
|
+
}
|
|
213
|
+
return ui.ul(members.map((member) => ui.li(formatTeamMember(member))));
|
|
214
|
+
}
|
|
215
|
+
function pushDetail(details, term, value, shouldInclude) {
|
|
216
|
+
if (shouldInclude) {
|
|
217
|
+
details.push(ui.dli(term, value));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
function parseTeamInfoLookup(options) {
|
|
221
|
+
const teamId = parseOptionalTextOption(options.teamId);
|
|
222
|
+
const referenceId = parseOptionalTextOption(options.referenceId);
|
|
223
|
+
if (!teamId && !referenceId) {
|
|
224
|
+
throw new CliError("One of --team-id or --reference-id is required", EXIT_CODES.ARGUMENT_ERROR);
|
|
225
|
+
}
|
|
226
|
+
if (teamId && referenceId) {
|
|
227
|
+
throw new CliError("--team-id and --reference-id are mutually exclusive", EXIT_CODES.ARGUMENT_ERROR);
|
|
228
|
+
}
|
|
229
|
+
return teamId ? { team_id: teamId } : { reference_id: referenceId };
|
|
230
|
+
}
|
|
231
|
+
function parseOptionalTextOption(value) {
|
|
232
|
+
if (typeof value !== "string") {
|
|
233
|
+
return undefined;
|
|
234
|
+
}
|
|
235
|
+
const trimmed = value.trim();
|
|
236
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
237
|
+
}
|
|
238
|
+
function parseTeamEmails(value) {
|
|
239
|
+
if (typeof value !== "string") {
|
|
240
|
+
throw new CliError("--team-emails is required", EXIT_CODES.ARGUMENT_ERROR);
|
|
241
|
+
}
|
|
242
|
+
const emails = value
|
|
243
|
+
.split(",")
|
|
244
|
+
.map((email) => email.trim())
|
|
245
|
+
.filter((email) => email.length > 0);
|
|
246
|
+
if (emails.length === 0) {
|
|
247
|
+
throw new CliError("--team-emails must include at least one email address", EXIT_CODES.ARGUMENT_ERROR);
|
|
248
|
+
}
|
|
249
|
+
return emails.join(",");
|
|
250
|
+
}
|
|
251
|
+
function buildFindByMembersError(error, teamEmails) {
|
|
252
|
+
if (!(error instanceof HttpError)) {
|
|
253
|
+
return error instanceof Error ? error : new Error(String(error));
|
|
254
|
+
}
|
|
255
|
+
const apiError = getApiErrorCode(error.body);
|
|
256
|
+
if (apiError === "no_users_found") {
|
|
257
|
+
return new CliError(`No active DX users were found for the provided email addresses: ${teamEmails}.\n\nCheck the spelling and make sure these users exist and are active in the DX account for your API token.`, EXIT_CODES.ARGUMENT_ERROR);
|
|
258
|
+
}
|
|
259
|
+
return error;
|
|
260
|
+
}
|
|
261
|
+
function getApiErrorCode(body) {
|
|
262
|
+
if (!body || typeof body !== "object") {
|
|
263
|
+
return undefined;
|
|
264
|
+
}
|
|
265
|
+
const error = body.error;
|
|
266
|
+
return typeof error === "string" ? error : undefined;
|
|
267
|
+
}
|