@get-dx/cli 0.6.2 → 0.7.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/dist/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ import { scorecardsCommand } from "./commands/scorecards.js";
|
|
|
6
6
|
import { snapshotsCommand } from "./commands/snapshots.js";
|
|
7
7
|
import { studioCommand } from "./commands/studio.js";
|
|
8
8
|
import { teamsCommand } from "./commands/teams.js";
|
|
9
|
+
import { usersCommand } from "./commands/users.js";
|
|
9
10
|
import { workflowRunsCommand } from "./commands/workflowRuns.js";
|
|
10
11
|
import { workflowsCommand } from "./commands/workflows.js";
|
|
11
12
|
import { handleError } from "./commandHelpers.js";
|
|
@@ -54,6 +55,7 @@ function createProgram() {
|
|
|
54
55
|
program.addCommand(snapshotsCommand());
|
|
55
56
|
program.addCommand(studioCommand());
|
|
56
57
|
program.addCommand(teamsCommand());
|
|
58
|
+
program.addCommand(usersCommand());
|
|
57
59
|
program.addCommand(workflowRunsCommand());
|
|
58
60
|
program.addCommand(workflowsCommand());
|
|
59
61
|
applyExitOverride(program);
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createExampleText, getContext, parsePositiveIntOption, wrapAction, } from "../../commandHelpers.js";
|
|
3
|
+
import { CliError, EXIT_CODES } from "../../errors.js";
|
|
4
|
+
import { requestWithMessages } from "../../http.js";
|
|
5
|
+
import { renderJson, renderRichText } from "../../renderers.js";
|
|
6
|
+
import { buildRuntime } from "../../runtime.js";
|
|
7
|
+
import * as ui from "../../ui.js";
|
|
8
|
+
import { SOURCES } from "./sources.js";
|
|
9
|
+
export function linkableAccountsCommand() {
|
|
10
|
+
const linkableAccounts = new Command()
|
|
11
|
+
.name("linkableAccounts")
|
|
12
|
+
.description("Search accounts that can be linked to DX users");
|
|
13
|
+
linkableAccounts
|
|
14
|
+
.command("list")
|
|
15
|
+
.description("Search the accounts DX has imported from a source, and get the account IDs to pass to `dx users links` commands")
|
|
16
|
+
.requiredOption("--source <source>", `Source to search (${SOURCES})`)
|
|
17
|
+
.option("--query <query>", "Case-insensitive text to find in the account's name, email, or username")
|
|
18
|
+
.option("--external-id <id>", "Only return the account with this ID from the tool itself")
|
|
19
|
+
.option("--instance-id <id>", "Only return accounts from this instance, organization, workspace, or connection")
|
|
20
|
+
.option("--linked", "Only return accounts linked to a DX user")
|
|
21
|
+
.option("--unlinked", "Only return accounts not linked to a DX user")
|
|
22
|
+
.option("--page <n>", "Page number to return (default is 1)", (value) => parsePositiveIntOption(value, "--page"))
|
|
23
|
+
.option("--page-size <n>", "Max accounts per page (default and maximum is 100)", (value) => parsePositiveIntOption(value, "--page-size"))
|
|
24
|
+
.addHelpText("afterAll", createExampleText([
|
|
25
|
+
{
|
|
26
|
+
label: "Search unlinked GitHub accounts matching a name",
|
|
27
|
+
command: "dx users linkableAccounts list --source github --query jane --unlinked",
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
label: "Find a Jira account by its Jira ID and return JSON",
|
|
31
|
+
command: "dx users linkableAccounts list --source jira --external-id 5b10ac8d82e05b22cc7d4ef5 --json",
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
label: "Fetch the next page of results",
|
|
35
|
+
command: "dx users linkableAccounts list --source github --page 2",
|
|
36
|
+
},
|
|
37
|
+
]))
|
|
38
|
+
.action(wrapAction(async (options, command) => {
|
|
39
|
+
if (options.linked && options.unlinked) {
|
|
40
|
+
throw new CliError("--linked and --unlinked are mutually exclusive", EXIT_CODES.ARGUMENT_ERROR);
|
|
41
|
+
}
|
|
42
|
+
const runtime = await buildRuntime(getContext(command));
|
|
43
|
+
const response = await listLinkableAccounts(runtime, {
|
|
44
|
+
source: options.source,
|
|
45
|
+
query: options.query,
|
|
46
|
+
external_id: options.externalId,
|
|
47
|
+
instance_id: options.instanceId,
|
|
48
|
+
linked: options.unlinked ? false : options.linked,
|
|
49
|
+
page: options.page,
|
|
50
|
+
page_size: options.pageSize,
|
|
51
|
+
});
|
|
52
|
+
if (runtime.context.json) {
|
|
53
|
+
renderJson(response);
|
|
54
|
+
}
|
|
55
|
+
else {
|
|
56
|
+
renderLinkableAccounts(response);
|
|
57
|
+
}
|
|
58
|
+
}));
|
|
59
|
+
return linkableAccounts;
|
|
60
|
+
}
|
|
61
|
+
const ERROR_MESSAGES = {
|
|
62
|
+
query_timeout: "The search timed out. Narrow it with --instance-id, --external-id, or a longer --query, then try again.",
|
|
63
|
+
};
|
|
64
|
+
async function listLinkableAccounts(runtime, params) {
|
|
65
|
+
return requestWithMessages(runtime, "/users.linkableAccounts.list", {
|
|
66
|
+
method: "GET",
|
|
67
|
+
query: params,
|
|
68
|
+
}, ERROR_MESSAGES);
|
|
69
|
+
}
|
|
70
|
+
function renderLinkableAccounts(response) {
|
|
71
|
+
const blocks = [ui.h1("Linkable Accounts")];
|
|
72
|
+
blocks.push(ui.p(`Displaying ${ui.bold(response.accounts.length.toString())} of ${ui.bold(response.total.toString())} accounts.`));
|
|
73
|
+
if (response.next_page) {
|
|
74
|
+
blocks.push(ui.p(`Next page: ${ui.code(response.next_page.toString())}`));
|
|
75
|
+
}
|
|
76
|
+
for (const account of response.accounts) {
|
|
77
|
+
blocks.push(ui.h2(`${account.label} (${ui.code(account.id)})`));
|
|
78
|
+
blocks.push(ui.dl([
|
|
79
|
+
...accountDetailItems(account),
|
|
80
|
+
ui.dli("Linked user", account.linked_user_id
|
|
81
|
+
? ui.code(account.linked_user_id)
|
|
82
|
+
: ui.dim("(Not linked)")),
|
|
83
|
+
], { termWidth: 13 }));
|
|
84
|
+
}
|
|
85
|
+
renderRichText(blocks);
|
|
86
|
+
}
|
|
87
|
+
export function accountDetailItems(account) {
|
|
88
|
+
return [
|
|
89
|
+
ui.dli("Source", account.source),
|
|
90
|
+
ui.dli("External ID", formatCode(account.external_id)),
|
|
91
|
+
ui.dli("Instance ID", formatCode(account.instance_id)),
|
|
92
|
+
ui.dli("Name", account.name ?? ui.dim("(None)")),
|
|
93
|
+
ui.dli("Email", account.email ?? ui.dim("(None)")),
|
|
94
|
+
ui.dli("Username", account.username ?? ui.dim("(None)")),
|
|
95
|
+
];
|
|
96
|
+
}
|
|
97
|
+
function formatCode(value) {
|
|
98
|
+
return value ? ui.code(value) : ui.dim("(None)");
|
|
99
|
+
}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createExampleText, getContext, parsePositiveIntOption, wrapAction, } from "../../commandHelpers.js";
|
|
3
|
+
import { requestWithMessages } from "../../http.js";
|
|
4
|
+
import { renderJson, renderRichText } from "../../renderers.js";
|
|
5
|
+
import { buildRuntime } from "../../runtime.js";
|
|
6
|
+
import * as ui from "../../ui.js";
|
|
7
|
+
import { accountDetailItems } from "./linkableAccounts.js";
|
|
8
|
+
import { SOURCES } from "./sources.js";
|
|
9
|
+
export function linksCommand() {
|
|
10
|
+
const links = new Command()
|
|
11
|
+
.name("links")
|
|
12
|
+
.description("Manage the accounts linked to DX users");
|
|
13
|
+
links
|
|
14
|
+
.command("create")
|
|
15
|
+
.description("Link one account to a user, keeping the user's other linked accounts for that source")
|
|
16
|
+
.argument("<user-id>", "DX user ID")
|
|
17
|
+
.requiredOption("--source <source>", `Source the account comes from (${SOURCES})`)
|
|
18
|
+
.requiredOption("--account-id <id>", "Account ID to link, as returned by `dx users linkableAccounts list`")
|
|
19
|
+
.addHelpText("afterAll", createExampleText([
|
|
20
|
+
{
|
|
21
|
+
label: "Link a GitHub account to a user",
|
|
22
|
+
command: "dx users links create NTEyMDUw --source github --account-id 4812",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
label: "Link an account and return JSON",
|
|
26
|
+
command: "dx users links create NTEyMDUw --source github --account-id 4812 --json",
|
|
27
|
+
},
|
|
28
|
+
]))
|
|
29
|
+
.action(wrapAction(async (userId, options, command) => {
|
|
30
|
+
const runtime = await buildRuntime(getContext(command));
|
|
31
|
+
const response = await createLink(runtime, {
|
|
32
|
+
user_id: userId,
|
|
33
|
+
source: options.source,
|
|
34
|
+
account_id: options.accountId,
|
|
35
|
+
});
|
|
36
|
+
if (runtime.context.json) {
|
|
37
|
+
renderJson(response);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
renderRichText([
|
|
41
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Linked ${options.source} account ${ui.code(options.accountId)} to user ${ui.code(userId)}.`),
|
|
42
|
+
]);
|
|
43
|
+
}
|
|
44
|
+
}));
|
|
45
|
+
links
|
|
46
|
+
.command("delete")
|
|
47
|
+
.description("Unlink one account from a user, keeping the user's other linked accounts for that source")
|
|
48
|
+
.argument("<user-id>", "DX user ID")
|
|
49
|
+
.requiredOption("--source <source>", `Source the account comes from (${SOURCES})`)
|
|
50
|
+
.requiredOption("--account-id <id>", "Account ID to unlink")
|
|
51
|
+
.addHelpText("afterAll", createExampleText([
|
|
52
|
+
{
|
|
53
|
+
label: "Unlink a GitHub account from a user",
|
|
54
|
+
command: "dx users links delete NTEyMDUw --source github --account-id 4812",
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
label: "Unlink an account and return JSON",
|
|
58
|
+
command: "dx users links delete NTEyMDUw --source github --account-id 4812 --json",
|
|
59
|
+
},
|
|
60
|
+
]))
|
|
61
|
+
.action(wrapAction(async (userId, options, command) => {
|
|
62
|
+
const runtime = await buildRuntime(getContext(command));
|
|
63
|
+
const response = await deleteLink(runtime, {
|
|
64
|
+
user_id: userId,
|
|
65
|
+
source: options.source,
|
|
66
|
+
account_id: options.accountId,
|
|
67
|
+
});
|
|
68
|
+
if (runtime.context.json) {
|
|
69
|
+
renderJson(response);
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
renderRichText([
|
|
73
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Unlinked ${options.source} account ${ui.code(options.accountId)} from user ${ui.code(userId)}.`),
|
|
74
|
+
]);
|
|
75
|
+
}
|
|
76
|
+
}));
|
|
77
|
+
links
|
|
78
|
+
.command("list")
|
|
79
|
+
.description("List the accounts linked to a user")
|
|
80
|
+
.argument("<user-id>", "DX user ID")
|
|
81
|
+
.option("--source <source>", `Only return accounts from this source (${SOURCES})`)
|
|
82
|
+
.option("--page <n>", "Page number to return (default is 1)", (value) => parsePositiveIntOption(value, "--page"))
|
|
83
|
+
.option("--page-size <n>", "Max links per page (default and maximum is 100)", (value) => parsePositiveIntOption(value, "--page-size"))
|
|
84
|
+
.addHelpText("afterAll", createExampleText([
|
|
85
|
+
{
|
|
86
|
+
label: "List every account linked to a user",
|
|
87
|
+
command: "dx users links list NTEyMDUw",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
label: "List a user's GitHub accounts and return JSON",
|
|
91
|
+
command: "dx users links list NTEyMDUw --source github --json",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
label: "Fetch the next page of links",
|
|
95
|
+
command: "dx users links list NTEyMDUw --page 2",
|
|
96
|
+
},
|
|
97
|
+
]))
|
|
98
|
+
.action(wrapAction(async (userId, options, command) => {
|
|
99
|
+
const runtime = await buildRuntime(getContext(command));
|
|
100
|
+
const response = await listLinks(runtime, {
|
|
101
|
+
user_id: userId,
|
|
102
|
+
source: options.source,
|
|
103
|
+
page: options.page,
|
|
104
|
+
page_size: options.pageSize,
|
|
105
|
+
});
|
|
106
|
+
if (runtime.context.json) {
|
|
107
|
+
renderJson(response);
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
renderLinks(userId, response);
|
|
111
|
+
}
|
|
112
|
+
}));
|
|
113
|
+
links
|
|
114
|
+
.command("set")
|
|
115
|
+
.description("Replace every account linked to a user for one source. Accounts not in the list are unlinked.")
|
|
116
|
+
.argument("<user-id>", "DX user ID")
|
|
117
|
+
.requiredOption("--source <source>", `Source the accounts come from (${SOURCES})`)
|
|
118
|
+
.requiredOption("--account-ids <ids>", 'Comma-separated account IDs to link, up to 100. Pass "" to unlink every account for the source.')
|
|
119
|
+
.addHelpText("afterAll", createExampleText([
|
|
120
|
+
{
|
|
121
|
+
label: "Link exactly these two GitHub accounts to a user",
|
|
122
|
+
command: "dx users links set NTEyMDUw --source github --account-ids 4812,4813",
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
label: "Unlink every GitHub account from a user",
|
|
126
|
+
command: 'dx users links set NTEyMDUw --source github --account-ids ""',
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
label: "Replace a user's Jira accounts and return JSON",
|
|
130
|
+
command: "dx users links set NTEyMDUw --source jira --account-ids 991 --json",
|
|
131
|
+
},
|
|
132
|
+
]))
|
|
133
|
+
.action(wrapAction(async (userId, options, command) => {
|
|
134
|
+
const accountIds = options.accountIds
|
|
135
|
+
.split(",")
|
|
136
|
+
.map((id) => id.trim())
|
|
137
|
+
.filter((id) => id.length > 0);
|
|
138
|
+
const runtime = await buildRuntime(getContext(command));
|
|
139
|
+
const response = await setLinks(runtime, {
|
|
140
|
+
user_id: userId,
|
|
141
|
+
source: options.source,
|
|
142
|
+
account_ids: accountIds,
|
|
143
|
+
});
|
|
144
|
+
if (runtime.context.json) {
|
|
145
|
+
renderJson(response);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
renderRichText([
|
|
149
|
+
ui.p(accountIds.length > 0
|
|
150
|
+
? `${ui.success(ui.GLYPHS.CHECK)} Set the ${options.source} accounts linked to user ${ui.code(userId)} to ${accountIds.map((id) => ui.code(id)).join(", ")}.`
|
|
151
|
+
: `${ui.success(ui.GLYPHS.CHECK)} Unlinked every ${options.source} account from user ${ui.code(userId)}.`),
|
|
152
|
+
]);
|
|
153
|
+
}
|
|
154
|
+
}));
|
|
155
|
+
return links;
|
|
156
|
+
}
|
|
157
|
+
const NOT_SYNCED = "This user hasn't synced to Data Cloud yet. Try again once their next sync finishes.";
|
|
158
|
+
const ERROR_MESSAGES = {
|
|
159
|
+
link_update_in_progress: "Another link update for this user and source is still running. Try again in a few seconds.",
|
|
160
|
+
manual_link_conflict: "An account is manually linked to another user. Remove that link before linking it to this user.",
|
|
161
|
+
not_found: "The user or account wasn't found. Check the user ID, and look up account IDs with `dx users linkableAccounts list`.",
|
|
162
|
+
user_not_in_datacloud: NOT_SYNCED,
|
|
163
|
+
// users.links.set passes Data Cloud's own error string through instead of a code.
|
|
164
|
+
"User not found in datacloud": NOT_SYNCED,
|
|
165
|
+
};
|
|
166
|
+
async function createLink(runtime, params) {
|
|
167
|
+
return requestWithMessages(runtime, "/users.links.create", {
|
|
168
|
+
method: "POST",
|
|
169
|
+
body: params,
|
|
170
|
+
}, ERROR_MESSAGES);
|
|
171
|
+
}
|
|
172
|
+
async function deleteLink(runtime, params) {
|
|
173
|
+
return requestWithMessages(runtime, "/users.links.delete", {
|
|
174
|
+
method: "POST",
|
|
175
|
+
body: params,
|
|
176
|
+
}, ERROR_MESSAGES);
|
|
177
|
+
}
|
|
178
|
+
async function listLinks(runtime, params) {
|
|
179
|
+
return requestWithMessages(runtime, "/users.links.list", {
|
|
180
|
+
method: "GET",
|
|
181
|
+
query: params,
|
|
182
|
+
}, ERROR_MESSAGES);
|
|
183
|
+
}
|
|
184
|
+
async function setLinks(runtime, params) {
|
|
185
|
+
return requestWithMessages(runtime, "/users.links.set", {
|
|
186
|
+
method: "POST",
|
|
187
|
+
body: params,
|
|
188
|
+
}, ERROR_MESSAGES);
|
|
189
|
+
}
|
|
190
|
+
function renderLinks(userId, response) {
|
|
191
|
+
const blocks = [ui.h1("Linked Accounts")];
|
|
192
|
+
blocks.push(ui.p(`Displaying ${ui.bold(response.links.length.toString())} of ${ui.bold(response.total.toString())} links for user ${ui.code(userId)}.`));
|
|
193
|
+
if (response.next_page) {
|
|
194
|
+
blocks.push(ui.p(`Next page: ${ui.code(response.next_page.toString())}`));
|
|
195
|
+
}
|
|
196
|
+
for (const link of response.links) {
|
|
197
|
+
blocks.push(ui.h2(link.account
|
|
198
|
+
? `${link.account.label} (${ui.code(link.account_id)})`
|
|
199
|
+
: `${link.source} account (${ui.code(link.account_id)})`));
|
|
200
|
+
blocks.push(ui.dl(link.account
|
|
201
|
+
? accountDetailItems(link.account)
|
|
202
|
+
: [ui.dli("Source", link.source)], { termWidth: 13 }));
|
|
203
|
+
}
|
|
204
|
+
renderRichText(blocks);
|
|
205
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const SOURCES = "ado, amazon_kiro, bitbucket, bitbucket_server, claude_code, codex, cursor, github, gitlab, jira, linear, windsurf";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { linkableAccountsCommand } from "./users/linkableAccounts.js";
|
|
3
|
+
import { linksCommand } from "./users/links.js";
|
|
4
|
+
export function usersCommand() {
|
|
5
|
+
const users = new Command().name("users").description("Manage DX users");
|
|
6
|
+
users.addCommand(linkableAccountsCommand());
|
|
7
|
+
users.addCommand(linksCommand());
|
|
8
|
+
return users;
|
|
9
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { HttpError } from "./errors.js";
|
|
1
|
+
import { CliError, HttpError } from "./errors.js";
|
|
2
2
|
export async function request(runtime, route, options = {}) {
|
|
3
3
|
const method = options.method || "GET";
|
|
4
4
|
const headers = new Headers({
|
|
@@ -72,6 +72,24 @@ export async function request(runtime, route, options = {}) {
|
|
|
72
72
|
const body = parsedResponseBody;
|
|
73
73
|
return retryAfterMs === undefined ? { body } : { body, retryAfterMs };
|
|
74
74
|
}
|
|
75
|
+
export async function requestWithMessages(runtime, route, options, messages) {
|
|
76
|
+
try {
|
|
77
|
+
const response = await request(runtime, route, options);
|
|
78
|
+
return response.body;
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
throw runtime.context.json ? error : readableError(error, messages);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function readableError(error, messages) {
|
|
85
|
+
if (!(error instanceof HttpError)) {
|
|
86
|
+
return error;
|
|
87
|
+
}
|
|
88
|
+
const code = error.body?.error;
|
|
89
|
+
return typeof code === "string" && Object.hasOwn(messages, code)
|
|
90
|
+
? new CliError(messages[code], error.exitCode)
|
|
91
|
+
: error;
|
|
92
|
+
}
|
|
75
93
|
/**
|
|
76
94
|
* Flatten an error thrown by `fetch` into a single readable message.
|
|
77
95
|
*
|