@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 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),
@@ -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
- .action(wrapAction(async (_commandOptions, command) => {
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
- runtime = await ensureLoggedIn(runtime);
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
- let parsed = { type: "invalid" };
92
- while (parsed.type === "invalid") {
93
- const raw = await input({
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":
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@get-dx/cli",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "DX CLI",
5
5
  "author": "DX <developers@getdx.com>",
6
6
  "bugs": {