@get-dx/cli 0.1.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/README.md +44 -0
- package/dist/cli.js +50 -0
- package/dist/commandHelpers.js +110 -0
- package/dist/commands/auth.js +70 -0
- package/dist/commands/authRendering.js +40 -0
- package/dist/commands/catalog/entities.js +596 -0
- package/dist/commands/catalog/entitiesRendering.js +286 -0
- package/dist/commands/catalog/entity-type-blank-template.yaml +51 -0
- package/dist/commands/catalog/entityTypes.js +402 -0
- package/dist/commands/catalog/entityTypesRendering.js +159 -0
- package/dist/commands/catalog.js +11 -0
- package/dist/commands/scorecard-blank-template.yaml +82 -0
- package/dist/commands/scorecards.js +480 -0
- package/dist/commands/scorecardsRendering.js +219 -0
- package/dist/commands/studio/query.js +270 -0
- package/dist/commands/studio/queryRendering.js +99 -0
- package/dist/commands/studio.js +9 -0
- package/dist/config.js +38 -0
- package/dist/errors.js +24 -0
- package/dist/http.js +128 -0
- package/dist/index.js +3 -0
- package/dist/logger.js +70 -0
- package/dist/renderers.js +29 -0
- package/dist/runtime.js +22 -0
- package/dist/secrets.js +80 -0
- package/dist/types.js +1 -0
- package/dist/ui/blocks.js +94 -0
- package/dist/ui/glyphs.js +5 -0
- package/dist/ui.js +109 -0
- package/package.json +56 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# dx-cli
|
|
2
|
+
|
|
3
|
+
Standalone DX CLI for existing token-authenticated DX web API endpoints.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install --global @get-dx/cli
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Commands
|
|
12
|
+
|
|
13
|
+
- `dx auth login --token <account-web-api-token>`
|
|
14
|
+
- `dx auth logout`
|
|
15
|
+
- `dx auth status`
|
|
16
|
+
- `dx catalog entities info <identifier>`
|
|
17
|
+
|
|
18
|
+
## Agent Provenance
|
|
19
|
+
|
|
20
|
+
The CLI can automatically attach agent metadata from:
|
|
21
|
+
|
|
22
|
+
- `DX_AGENT_NAME`
|
|
23
|
+
- `DX_AGENT_SESSION_ID`
|
|
24
|
+
|
|
25
|
+
Explicit `--agent` and `--agent-session-id` flags take precedence over the environment.
|
|
26
|
+
|
|
27
|
+
## Logging
|
|
28
|
+
|
|
29
|
+
Set `DX_LOG_LEVEL` to one of `debug`, `info`, `warn`, or `error` to enable CLI logs.
|
|
30
|
+
|
|
31
|
+
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.
|
|
32
|
+
|
|
33
|
+
Deferred in this repo version:
|
|
34
|
+
|
|
35
|
+
- `dx init`
|
|
36
|
+
- `dx scorecards list|get|create|update`
|
|
37
|
+
- `dx catalog entities list|create|update`
|
|
38
|
+
- `dx workflows list`
|
|
39
|
+
- `dx workflows trigger`
|
|
40
|
+
- `dx studio query`
|
|
41
|
+
|
|
42
|
+
## Developing the CLI
|
|
43
|
+
|
|
44
|
+
See [CONTRIBUTING.md](./CONTRIBUTING.md).
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Command, Option } from "commander";
|
|
2
|
+
import { authCommand } from "./commands/auth.js";
|
|
3
|
+
import { catalogCommand } from "./commands/catalog.js";
|
|
4
|
+
import { scorecardsCommand } from "./commands/scorecards.js";
|
|
5
|
+
import { studioCommand } from "./commands/studio.js";
|
|
6
|
+
import { handleError } from "./commandHelpers.js";
|
|
7
|
+
import cliPackage from "../package.json" with { type: "json" };
|
|
8
|
+
export async function run(argv = process.argv) {
|
|
9
|
+
try {
|
|
10
|
+
const program = createProgram();
|
|
11
|
+
if (argv.length <= 2) {
|
|
12
|
+
program.outputHelp();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
await program.parseAsync(argv);
|
|
16
|
+
}
|
|
17
|
+
catch (error) {
|
|
18
|
+
handleError(error, undefined, argv);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function createProgram() {
|
|
22
|
+
const program = new Command();
|
|
23
|
+
program
|
|
24
|
+
.name("dx")
|
|
25
|
+
.description("DX CLI")
|
|
26
|
+
.version(cliPackage.version)
|
|
27
|
+
.addOption(new Option("--json", "Print machine-readable JSON"))
|
|
28
|
+
.addOption(new Option("--agent <name>", "Agent name to send as an HTTP header"))
|
|
29
|
+
.addOption(new Option("--agent-session-id <id>", "Agent session ID to send as an HTTP header"));
|
|
30
|
+
program.addCommand(authCommand());
|
|
31
|
+
program.addCommand(catalogCommand());
|
|
32
|
+
program.addCommand(scorecardsCommand());
|
|
33
|
+
program.addCommand(studioCommand());
|
|
34
|
+
applyExitOverride(program);
|
|
35
|
+
return program;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Make Commander throw CommanderError instead of calling process.exit(), and
|
|
39
|
+
* suppress its own stderr writes so we can control all error output ourselves.
|
|
40
|
+
*
|
|
41
|
+
* exitOverride() and configureOutput() are not inherited by subcommands, so
|
|
42
|
+
* apply them to the full command tree after all subcommands are registered.
|
|
43
|
+
*/
|
|
44
|
+
function applyExitOverride(cmd) {
|
|
45
|
+
cmd.exitOverride();
|
|
46
|
+
cmd.configureOutput({ writeErr: () => { } });
|
|
47
|
+
for (const sub of cmd.commands) {
|
|
48
|
+
applyExitOverride(sub);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { CommanderError } from "commander";
|
|
2
|
+
import { CliError, EXIT_CODES, HttpError } from "./errors.js";
|
|
3
|
+
export function getContext(command) {
|
|
4
|
+
const root = command.optsWithGlobals();
|
|
5
|
+
return {
|
|
6
|
+
json: Boolean(root.json),
|
|
7
|
+
agent: root.agent || process.env.DX_AGENT_NAME,
|
|
8
|
+
agentSessionId: root.agentSessionId || process.env.DX_AGENT_SESSION_ID,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function wrapAction(action) {
|
|
12
|
+
return async (...args) => {
|
|
13
|
+
try {
|
|
14
|
+
await action(...args);
|
|
15
|
+
}
|
|
16
|
+
catch (error) {
|
|
17
|
+
const command = args[args.length - 1];
|
|
18
|
+
handleError(error, command, undefined);
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function handleError(error, command, argv) {
|
|
23
|
+
if (error instanceof CommanderError && error.exitCode === 0) {
|
|
24
|
+
// help or version was displayed.
|
|
25
|
+
// Commander already printed the output, so just exit cleanly.
|
|
26
|
+
process.exit(0);
|
|
27
|
+
}
|
|
28
|
+
const context = inferContext(command, argv);
|
|
29
|
+
if (context.json) {
|
|
30
|
+
if (error instanceof HttpError) {
|
|
31
|
+
printJson({
|
|
32
|
+
ok: false,
|
|
33
|
+
error: error.message,
|
|
34
|
+
http_status: error.status,
|
|
35
|
+
body: error.body,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
else if (error instanceof CliError) {
|
|
39
|
+
printJson({
|
|
40
|
+
ok: false,
|
|
41
|
+
error: error.message,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else if (error instanceof CommanderError) {
|
|
45
|
+
// Strip the "error: " prefix Commander adds — it's terminal formatting,
|
|
46
|
+
// not appropriate in a JSON payload.
|
|
47
|
+
const message = error.message.replace(/^error:\s+/i, "");
|
|
48
|
+
printJson({ ok: false, error: message });
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
printJson({
|
|
52
|
+
ok: false,
|
|
53
|
+
error: error instanceof Error ? error.message : String(error),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
else {
|
|
58
|
+
if (error instanceof HttpError) {
|
|
59
|
+
process.stderr.write(`The API returned an error with status code ${error.status}:\n\n${JSON.stringify(error.body, null, 2)}\n`);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
const exitCode = (() => {
|
|
66
|
+
if (error instanceof CliError) {
|
|
67
|
+
return error.exitCode;
|
|
68
|
+
}
|
|
69
|
+
else if (error instanceof CommanderError) {
|
|
70
|
+
return EXIT_CODES.ARGUMENT_ERROR;
|
|
71
|
+
}
|
|
72
|
+
else {
|
|
73
|
+
// Should be unreachable
|
|
74
|
+
return 1;
|
|
75
|
+
}
|
|
76
|
+
})();
|
|
77
|
+
process.exit(exitCode);
|
|
78
|
+
}
|
|
79
|
+
function printJson(value) {
|
|
80
|
+
process.stdout.write(JSON.stringify(value, null, 2) + "\n");
|
|
81
|
+
}
|
|
82
|
+
export function parsePositiveIntOption(value, flag) {
|
|
83
|
+
const n = Number(value);
|
|
84
|
+
if (!Number.isInteger(n) || n < 1) {
|
|
85
|
+
throw new CliError(`${flag} must be a positive integer`, EXIT_CODES.ARGUMENT_ERROR);
|
|
86
|
+
}
|
|
87
|
+
return n;
|
|
88
|
+
}
|
|
89
|
+
export function createExampleText(examples) {
|
|
90
|
+
const lines = [];
|
|
91
|
+
lines.push(""); // separate from the rest of the help text
|
|
92
|
+
lines.push("Examples:");
|
|
93
|
+
for (const example of examples) {
|
|
94
|
+
lines.push(` ${example.label}:`);
|
|
95
|
+
lines.push(` ${example.command}`);
|
|
96
|
+
}
|
|
97
|
+
return lines.join("\n");
|
|
98
|
+
}
|
|
99
|
+
function inferContext(command, argv) {
|
|
100
|
+
if (command) {
|
|
101
|
+
return getContext(command);
|
|
102
|
+
}
|
|
103
|
+
else {
|
|
104
|
+
const rawArgv = argv && argv.length > 0 ? argv : process.argv;
|
|
105
|
+
const hasJsonFlag = Boolean(rawArgv.find((arg) => arg === "--json"));
|
|
106
|
+
return {
|
|
107
|
+
json: hasJsonFlag,
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { deleteToken, setToken } from "../secrets.js";
|
|
3
|
+
import { renderJson } from "../renderers.js";
|
|
4
|
+
import { renderAuthInfo, renderLoggedOut } from "./authRendering.js";
|
|
5
|
+
import { wrapAction } from "../commandHelpers.js";
|
|
6
|
+
import { getContext } from "../commandHelpers.js";
|
|
7
|
+
import { persistBaseUrl, resolveBaseUrl } from "../config.js";
|
|
8
|
+
import { request } from "../http.js";
|
|
9
|
+
import { buildLogger, buildRuntime } from "../runtime.js";
|
|
10
|
+
import cliPackage from "../../package.json" with { type: "json" };
|
|
11
|
+
import { maskToken } from "../ui.js";
|
|
12
|
+
export function authCommand() {
|
|
13
|
+
const auth = new Command()
|
|
14
|
+
.name("auth")
|
|
15
|
+
.description("Manage DX authentication");
|
|
16
|
+
auth
|
|
17
|
+
.command("login")
|
|
18
|
+
.requiredOption("--token <token>", "Account web API token")
|
|
19
|
+
.action(wrapAction(async (commandOptions, command) => {
|
|
20
|
+
const context = getContext(command);
|
|
21
|
+
const baseUrl = resolveBaseUrl();
|
|
22
|
+
const runtime = {
|
|
23
|
+
baseUrl,
|
|
24
|
+
token: commandOptions.token,
|
|
25
|
+
context,
|
|
26
|
+
version: cliPackage.version,
|
|
27
|
+
logger: buildLogger(context),
|
|
28
|
+
};
|
|
29
|
+
const response = await getAuthInfo(runtime);
|
|
30
|
+
persistBaseUrl(baseUrl);
|
|
31
|
+
setToken(baseUrl, commandOptions.token);
|
|
32
|
+
if (context.json) {
|
|
33
|
+
renderJson({ ...response, base_url: baseUrl });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
renderAuthInfo(response, commandOptions.token, baseUrl);
|
|
37
|
+
}));
|
|
38
|
+
auth.command("logout").action(wrapAction(async (_options, command) => {
|
|
39
|
+
const context = getContext(command);
|
|
40
|
+
const baseUrl = resolveBaseUrl();
|
|
41
|
+
deleteToken(baseUrl);
|
|
42
|
+
if (context.json) {
|
|
43
|
+
renderJson({ ok: true, base_url: baseUrl, logged_out: true });
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
renderLoggedOut(baseUrl);
|
|
47
|
+
}
|
|
48
|
+
}));
|
|
49
|
+
auth.command("status").action(wrapAction(async (_options, command) => {
|
|
50
|
+
const runtime = buildRuntime(getContext(command));
|
|
51
|
+
const response = await getAuthInfo(runtime);
|
|
52
|
+
if (runtime.context.json) {
|
|
53
|
+
renderJson({
|
|
54
|
+
...response,
|
|
55
|
+
token: maskToken(runtime.token),
|
|
56
|
+
base_url: runtime.baseUrl,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
else {
|
|
60
|
+
renderAuthInfo(response, runtime.token, runtime.baseUrl);
|
|
61
|
+
}
|
|
62
|
+
}));
|
|
63
|
+
return auth;
|
|
64
|
+
}
|
|
65
|
+
async function getAuthInfo(runtime) {
|
|
66
|
+
const response = await request(runtime, "/auth.info", {
|
|
67
|
+
method: "GET",
|
|
68
|
+
});
|
|
69
|
+
return response.body;
|
|
70
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { renderRichText } from "../renderers.js";
|
|
2
|
+
import * as ui from "../ui.js";
|
|
3
|
+
export function renderAuthInfo(authInfo, token, baseUrl) {
|
|
4
|
+
const maskedToken = ui.maskToken(token) ?? "not configured";
|
|
5
|
+
renderRichText([
|
|
6
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Logged in to ${ui.link(baseUrl)} account ${ui.bold(authInfo.account.name)}`),
|
|
7
|
+
ui.dl([
|
|
8
|
+
ui.dli("Token", ui.code(maskedToken)),
|
|
9
|
+
ui.dli("Token type", tokenTypeName(authInfo.auth.token_type)),
|
|
10
|
+
ui.dli("Token name", authInfo.auth.token_name),
|
|
11
|
+
ui.dli("Token created at", ui.timestampSummary(authInfo.auth.created_at)),
|
|
12
|
+
], { termWidth: 18 }),
|
|
13
|
+
ui.p(ui.bold("Token scopes:"), false),
|
|
14
|
+
...scopesContent(authInfo.auth.scopes),
|
|
15
|
+
]);
|
|
16
|
+
}
|
|
17
|
+
export function renderLoggedOut(baseUrl) {
|
|
18
|
+
renderRichText([
|
|
19
|
+
ui.p(`${ui.success(ui.GLYPHS.CHECK)} Logged out of ${ui.link(baseUrl)} successfully`),
|
|
20
|
+
]);
|
|
21
|
+
}
|
|
22
|
+
function scopesContent(scopes) {
|
|
23
|
+
if (scopes.length === 0) {
|
|
24
|
+
return [ui.p("(none)", false)];
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
return [
|
|
28
|
+
ui.p("", false),
|
|
29
|
+
ui.ul(scopes.map((scope) => ui.li(ui.code(scope)))),
|
|
30
|
+
];
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function tokenTypeName(tokenType) {
|
|
34
|
+
switch (tokenType) {
|
|
35
|
+
case "account_web_api_token":
|
|
36
|
+
return "Account-level web API token";
|
|
37
|
+
default:
|
|
38
|
+
throw new Error(`Unknown token type: ${tokenType}`);
|
|
39
|
+
}
|
|
40
|
+
}
|