@get-dx/cli 0.1.2 → 0.1.4

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
@@ -2,27 +2,28 @@
2
2
 
3
3
  AI-Native CLI for interacting with DX.
4
4
 
5
+ <img width="800" height="423" alt="cli-demo" src="https://github.com/user-attachments/assets/a89f7d53-2617-413e-84df-07293149e9c8" />
6
+
5
7
  ## Install
6
8
 
7
- ```bash
8
- npm install --global @get-dx/cli
9
+ ```shell
10
+ npm install -g @get-dx/cli
9
11
  ```
10
12
 
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>`
13
+ ## Getting started
17
14
 
18
- ## Agent Provenance
15
+ Interactively login and install the AI agent skill:
19
16
 
20
- The CLI can automatically attach agent metadata from:
17
+ ```shell
18
+ dx init
19
+ ```
21
20
 
22
- - `DX_AGENT_NAME`
23
- - `DX_AGENT_SESSION_ID`
21
+ Show help text:
24
22
 
25
- Explicit `--agent` and `--agent-session-id` flags take precedence over the environment.
23
+ ```shell
24
+ dx --help
25
+ dx <subcommand> --help
26
+ ```
26
27
 
27
28
  ## Logging
28
29
 
@@ -30,15 +31,6 @@ Set `DX_LOG_LEVEL` to one of `debug`, `info`, `warn`, or `error` to enable CLI l
30
31
 
31
32
  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
 
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
34
  ## Developing the CLI
43
35
 
44
36
  See [CONTRIBUTING.md](./CONTRIBUTING.md).
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { Command, Option } from "commander";
2
2
  import { authCommand } from "./commands/auth.js";
3
3
  import { catalogCommand } from "./commands/catalog.js";
4
+ import { initCommand } from "./commands/init.js";
4
5
  import { scorecardsCommand } from "./commands/scorecards.js";
5
6
  import { studioCommand } from "./commands/studio.js";
6
7
  import { handleError } from "./commandHelpers.js";
@@ -29,6 +30,7 @@ function createProgram() {
29
30
  .addOption(new Option("--agent-session-id <id>", "Agent session ID to send as an HTTP header"));
30
31
  program.addCommand(authCommand());
31
32
  program.addCommand(catalogCommand());
33
+ program.addCommand(initCommand());
32
34
  program.addCommand(scorecardsCommand());
33
35
  program.addCommand(studioCommand());
34
36
  applyExitOverride(program);
@@ -43,7 +45,7 @@ function createProgram() {
43
45
  */
44
46
  function applyExitOverride(cmd) {
45
47
  cmd.exitOverride();
46
- cmd.configureOutput({ writeErr: () => { } });
48
+ cmd.configureOutput({ outputError: () => { } });
47
49
  for (const sub of cmd.commands) {
48
50
  applyExitOverride(sub);
49
51
  }
@@ -58,6 +58,10 @@ export function handleError(error, command, argv) {
58
58
  if (error instanceof HttpError) {
59
59
  process.stderr.write(`The API returned an error with status code ${error.status}:\n\n${JSON.stringify(error.body, null, 2)}\n`);
60
60
  }
61
+ else if (error instanceof CommanderError &&
62
+ (error.code === "commander.help" || error.message === "(outputHelp)")) {
63
+ // Commander already printed the relevant help text.
64
+ }
61
65
  else {
62
66
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
63
67
  }
@@ -15,7 +15,7 @@ export function authCommand() {
15
15
  .description("Manage DX authentication");
16
16
  auth
17
17
  .command("login")
18
- .requiredOption("--token <token>", "Account web API token")
18
+ .requiredOption("--token <token>", "Account web API token or personal access token")
19
19
  .action(wrapAction(async (commandOptions, command) => {
20
20
  const context = getContext(command);
21
21
  const baseUrl = resolveBaseUrl();
@@ -62,7 +62,8 @@ export function authCommand() {
62
62
  }));
63
63
  return auth;
64
64
  }
65
- async function getAuthInfo(runtime) {
65
+ // TODO: should we move this somewhere more central, since `init` needs it?
66
+ export async function getAuthInfo(runtime) {
66
67
  const response = await request(runtime, "/auth.info", {
67
68
  method: "GET",
68
69
  });
@@ -34,6 +34,8 @@ function tokenTypeName(tokenType) {
34
34
  switch (tokenType) {
35
35
  case "account_web_api_token":
36
36
  return "Account-level web API token";
37
+ case "personal_access_token":
38
+ return "Personal access token";
37
39
  default:
38
40
  throw new Error(`Unknown token type: ${tokenType}`);
39
41
  }
@@ -0,0 +1,194 @@
1
+ import { access } from "node:fs/promises";
2
+ import { homedir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { input, password, confirm } from "@inquirer/prompts";
5
+ import { Command } from "commander";
6
+ import { execa } from "execa";
7
+ import { buildRuntime, buildRuntimeSafe } from "../runtime.js";
8
+ import { getAuthInfo } from "./auth.js";
9
+ import { renderAuthInfo } from "./authRendering.js";
10
+ import { getContext } from "../commandHelpers.js";
11
+ import { wrapAction } from "../commandHelpers.js";
12
+ import { renderRichText } from "../renderers.js";
13
+ import * as ui from "../ui.js";
14
+ import { CliError } from "../errors.js";
15
+ import { persistBaseUrl } from "../config.js";
16
+ import { setToken } from "../secrets.js";
17
+ function parseHostname(raw) {
18
+ const normalized = raw.trim().replace(/\/$/, "");
19
+ if (!normalized || normalized === "app.getdx.com") {
20
+ return { type: "cloud" };
21
+ }
22
+ try {
23
+ const url = new URL(normalized.startsWith("http") ? normalized : `https://${normalized}`);
24
+ const host = url.hostname;
25
+ const dedicatedMatch = host.match(/^(.+)\.getdx\.io$/);
26
+ if (dedicatedMatch) {
27
+ return { type: "dedicated", accountName: dedicatedMatch[1] };
28
+ }
29
+ if (host) {
30
+ return { type: "managed", uiBaseUrl: url.origin };
31
+ }
32
+ }
33
+ catch {
34
+ // fall through to invalid
35
+ }
36
+ return { type: "invalid" };
37
+ }
38
+ // FIXME: make this WAY more glam
39
+ // Choose a more interesting ASCII art font from https://patorjk.com/software/taag/
40
+ const WELCOME_BANNER = ui.indent(`\
41
+ ▄▄▄▄▄ ▄▄▄ ▄▄▄
42
+ ██▀▀▀██ ██▄▄██
43
+ ██ ██ ████
44
+ ██ ██ ██
45
+ ██ ██ ████
46
+ ██▄▄▄██ ██ ██
47
+ ▀▀▀▀▀ ▀▀▀ ▀▀▀
48
+ `, 2);
49
+ export function initCommand() {
50
+ const init = new Command()
51
+ .name("init")
52
+ .description("Initialize the DX CLI")
53
+ .action(wrapAction(async (_commandOptions, command) => {
54
+ const context = getContext(command);
55
+ let runtime = buildRuntimeSafe(context);
56
+ ensureInteractive();
57
+ showWelcomeBanner();
58
+ runtime = await ensureLoggedIn(runtime);
59
+ await optionallySetupSkill(runtime);
60
+ renderRichText([
61
+ ui.blankLine(),
62
+ ui.p(`${ui.success(ui.GLYPHS.CHECK)} Done! You are now ready to use the DX CLI.`),
63
+ ]);
64
+ }));
65
+ return init;
66
+ }
67
+ function ensureInteractive() {
68
+ if (!process.stdin.isTTY || !process.stderr.isTTY) {
69
+ throw new CliError("`dx init` must be run interactively");
70
+ }
71
+ }
72
+ function showWelcomeBanner() {
73
+ renderRichText([ui.p(ui.success(WELCOME_BANNER))]);
74
+ }
75
+ async function ensureLoggedIn(runtime) {
76
+ renderRichText([ui.h1(`Checking if you are logged in...`), ui.blankLine()]);
77
+ if (runtime) {
78
+ let authInfo = null;
79
+ try {
80
+ authInfo = await getAuthInfo(runtime);
81
+ }
82
+ catch (_error) {
83
+ // Do nothing, we don't mind if this fails
84
+ }
85
+ if (authInfo) {
86
+ renderAuthInfo(authInfo, runtime.token, runtime.baseUrl);
87
+ return runtime;
88
+ }
89
+ }
90
+ 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
+ }
102
+ }
103
+ switch (parsed.type) {
104
+ case "cloud":
105
+ return await attemptLogin("https://api.getdx.com", "https://app.getdx.com");
106
+ case "dedicated": {
107
+ const { accountName } = parsed;
108
+ return await attemptLogin(`https://api.${accountName}.getdx.io`, `https://${accountName}.getdx.io`);
109
+ }
110
+ case "managed": {
111
+ const apiBaseUrl = await input({
112
+ message: "What is your API base URL?",
113
+ });
114
+ if (!apiBaseUrl) {
115
+ throw new CliError("API base URL is required");
116
+ }
117
+ return await attemptLogin(apiBaseUrl, parsed.uiBaseUrl);
118
+ }
119
+ }
120
+ }
121
+ async function attemptLogin(apiBaseUrl, _uiBaseUrl) {
122
+ const token = await password({
123
+ message: "Paste your account web API token here:",
124
+ mask: true,
125
+ });
126
+ if (!token) {
127
+ throw new CliError("Account web API token is required");
128
+ }
129
+ const context = createEmptyContext();
130
+ const runtime = buildRuntime(context, {
131
+ baseUrl: apiBaseUrl,
132
+ token,
133
+ });
134
+ renderRichText([ui.blankLine(), ui.p(`Attempting login...`), ui.blankLine()]);
135
+ const response = await getAuthInfo(runtime);
136
+ if (!response.ok) {
137
+ throw new CliError(`Login failed`);
138
+ }
139
+ persistBaseUrl(apiBaseUrl);
140
+ setToken(apiBaseUrl, token);
141
+ renderAuthInfo(response, token, apiBaseUrl);
142
+ return runtime;
143
+ }
144
+ async function optionallySetupSkill(runtime) {
145
+ renderRichText([
146
+ ui.blankLine(),
147
+ ui.h1(`Checking for the DX skill...`),
148
+ ui.blankLine(),
149
+ ]);
150
+ const skillPath = join(homedir(), ".agents", "skills", "dx-cli");
151
+ const skillInstalled = await access(skillPath)
152
+ .then(() => true)
153
+ .catch(() => false);
154
+ if (skillInstalled) {
155
+ renderRichText([ui.p(`The DX skill is already installed.`)]);
156
+ runtime.logger.debug("To update: npx skills update dx-cli --global");
157
+ runtime.logger.debug("To uninstall: npx skills remove dx-cli --global");
158
+ return;
159
+ }
160
+ renderRichText([
161
+ ui.p(`The DX skill is not installed. Would you like to install it?`),
162
+ ui.p("This will run the following command:", false),
163
+ ui.codeBlock("npx --yes -- skills@latest add get-dx/cli --global"),
164
+ ui.blankLine(),
165
+ ]);
166
+ const setupSkill = await confirm({
167
+ message: "Continue?",
168
+ default: true,
169
+ });
170
+ if (!setupSkill) {
171
+ renderRichText([
172
+ ui.p(`Skipping the DX skill setup. You can always do this later.`),
173
+ ]);
174
+ return;
175
+ }
176
+ renderRichText([ui.p(`Setting up the DX skill...`)]);
177
+ // NOTE: if the user chooses "no" for the final confirmation prompt, `skills` still exits with a 0 status.
178
+ // We are also not able to pipe the output without also breaking interactivity.
179
+ const result = await execa({
180
+ stdout: "inherit",
181
+ stderr: "inherit",
182
+ stdin: "inherit",
183
+ }) `npx --yes -- skills@latest add get-dx/cli --global`;
184
+ runtime.logger.debug("To update: npx skills update dx-cli --global");
185
+ runtime.logger.debug("To uninstall: npx skills remove dx-cli --global");
186
+ if (result.exitCode !== 0) {
187
+ throw new CliError(`Failed to setup the DX skill: ${result.stderr}`);
188
+ }
189
+ }
190
+ function createEmptyContext() {
191
+ return {
192
+ json: false,
193
+ };
194
+ }
@@ -21,6 +21,7 @@ export function queryCommand() {
21
21
  .description("Execute a Data Studio SQL query and wait for the results")
22
22
  .argument("<sql>", "SQL query to execute")
23
23
  .option("--output <filename>", "Save the full result set as CSV to a file")
24
+ .option("--variable <key=value>", "Set a template variable referenced in the SQL (can be repeated; use comma-separated values for arrays)", (val, prev) => [...prev, val], [])
24
25
  .addHelpText("afterAll", createExampleText([
25
26
  {
26
27
  label: "Run a query and print the results as a table",
@@ -28,21 +29,30 @@ export function queryCommand() {
28
29
  },
29
30
  {
30
31
  label: "Run a query and print the JSON results payload",
31
- command: "dx studio query 'SELECT id, name FROM github_repos LIMIT 5' --json",
32
+ command: "dx studio query 'SELECT id, name FROM github_repositories LIMIT 5' --json",
32
33
  },
33
34
  {
34
35
  label: "Run a query and save the full result set as CSV",
35
36
  command: "dx studio query 'SELECT * FROM github_pulls' --output pulls.csv",
36
37
  },
38
+ {
39
+ label: "Run a parameterized query with a scalar variable",
40
+ command: "dx studio query 'SELECT * FROM github_repositories WHERE name = $name' --variable name=my-repo",
41
+ },
42
+ {
43
+ label: "Run a parameterized query with an array variable",
44
+ command: "dx studio query 'SELECT * FROM github_repositories WHERE id IN ($repo_ids)' --variable repo_ids=1,2,3",
45
+ },
37
46
  ]))
38
47
  .action(wrapAction(async (sql, options, command) => {
39
48
  const context = getContext(command);
40
49
  if (context.json && options.output) {
41
50
  throw new CliError("--output cannot be used with --json", EXIT_CODES.ARGUMENT_ERROR);
42
51
  }
52
+ const variables = parseVariables(options.variable);
43
53
  const runtime = buildRuntime(context);
44
54
  const progress = new QueryProgressReporter();
45
- const queryRun = await executeQuery(runtime, progress, sql);
55
+ const queryRun = await executeQuery(runtime, progress, sql, variables);
46
56
  await waitForQuery(runtime, progress, queryRun.id);
47
57
  if (options.output) {
48
58
  await downloadStudioQueryResultsCsv(runtime, queryRun.id, options.output);
@@ -58,11 +68,11 @@ export function queryCommand() {
58
68
  }
59
69
  }));
60
70
  }
61
- async function executeQuery(runtime, progress, sql) {
71
+ async function executeQuery(runtime, progress, sql, variables) {
62
72
  try {
63
73
  renderRunningQuery(sql);
64
74
  progress.start(ui.bold("Submitting query"));
65
- const executeResponse = await executeStudioQuery(runtime, sql);
75
+ const executeResponse = await executeStudioQuery(runtime, sql, variables);
66
76
  const queryRun = executeResponse.body.query_run;
67
77
  await waitForRetryAfter(executeResponse.retryAfterMs);
68
78
  return queryRun;
@@ -115,10 +125,10 @@ function renderRunningQuery(sql) {
115
125
  function renderPendingQueryStatus(queryRunId) {
116
126
  return `${ui.bold("Query running")} ${ui.dim(`(${queryRunId})`)}`;
117
127
  }
118
- async function executeStudioQuery(runtime, sql) {
128
+ async function executeStudioQuery(runtime, sql, variables) {
119
129
  return request(runtime, "/studio.queryRuns.execute", {
120
130
  method: "POST",
121
- body: { sql },
131
+ body: variables ? { sql, variables } : { sql },
122
132
  });
123
133
  }
124
134
  async function getStudioQueryRun(runtime, id) {
@@ -262,6 +272,24 @@ async function writeResponseBodyToFile(response, filename) {
262
272
  reader.releaseLock();
263
273
  }
264
274
  }
275
+ function parseVariables(vars) {
276
+ if (vars.length === 0)
277
+ return undefined;
278
+ const result = {};
279
+ for (const v of vars) {
280
+ const eqIdx = v.indexOf("=");
281
+ if (eqIdx === -1) {
282
+ throw new CliError(`Invalid variable format: "${v}". Expected key=value.`, EXIT_CODES.ARGUMENT_ERROR);
283
+ }
284
+ const key = v.slice(0, eqIdx);
285
+ const raw = v.slice(eqIdx + 1);
286
+ const parts = raw.includes(",") ? raw.split(",") : [raw];
287
+ const coerced = parts.map((p) => p !== "" && !isNaN(Number(p)) ? Number(p) : p);
288
+ result[key] =
289
+ coerced.length === 1 ? coerced[0] : coerced;
290
+ }
291
+ return result;
292
+ }
265
293
  function buildFailedQueryError(error) {
266
294
  if (!error) {
267
295
  return new CliError("Query failed.", 1);
package/dist/logger.js CHANGED
@@ -1,3 +1,4 @@
1
+ import pc from "picocolors";
1
2
  const LOG_LEVELS = ["debug", "info", "warn", "error"];
2
3
  const LOG_LEVEL_PRIORITY = {
3
4
  debug: 10,
@@ -66,5 +67,17 @@ function writeLog(configuredLevel, options, level, message, fields) {
66
67
  const renderedFields = entry.fields && Object.keys(entry.fields).length > 0
67
68
  ? ` ${JSON.stringify(entry.fields)}`
68
69
  : "";
69
- process.stderr.write(`${entry.time} ${entry.level.toUpperCase()} ${entry.message}${renderedFields}\n`);
70
+ process.stderr.write(`${pc.dim(entry.time)} ${colorizeLevel(entry.level)} ${entry.message}${pc.dim(renderedFields)}\n`);
71
+ }
72
+ function colorizeLevel(level) {
73
+ switch (level) {
74
+ case "debug":
75
+ return pc.blue("DEBUG");
76
+ case "info":
77
+ return pc.bold("INFO");
78
+ case "warn":
79
+ return pc.yellow("WARN");
80
+ case "error":
81
+ return pc.red("ERROR");
82
+ }
70
83
  }
package/dist/runtime.js CHANGED
@@ -6,9 +6,9 @@ import cliPackage from "../package.json" with { type: "json" };
6
6
  export function buildLogger(context) {
7
7
  return createLogger({ json: context.json || !process.stderr.isTTY });
8
8
  }
9
- export function buildRuntime(context) {
10
- const baseUrl = resolveBaseUrl();
11
- const token = getToken(baseUrl);
9
+ export function buildRuntime(context, overrides) {
10
+ const baseUrl = overrides?.baseUrl ?? resolveBaseUrl();
11
+ const token = overrides?.token ?? getToken(baseUrl);
12
12
  if (!token) {
13
13
  throw new CliError("No API token configured. Run `dx auth login --token <token>` or set DX_API_TOKEN.");
14
14
  }
@@ -20,3 +20,17 @@ export function buildRuntime(context) {
20
20
  logger: buildLogger(context),
21
21
  };
22
22
  }
23
+ export function buildRuntimeSafe(context, overrides) {
24
+ const baseUrl = overrides?.baseUrl ?? resolveBaseUrl();
25
+ const token = overrides?.token ?? getToken(baseUrl);
26
+ if (!token) {
27
+ return null;
28
+ }
29
+ return {
30
+ baseUrl,
31
+ token,
32
+ context,
33
+ version: cliPackage.version,
34
+ logger: buildLogger(context),
35
+ };
36
+ }
package/dist/secrets.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execFileSync } from "node:child_process";
1
+ import { execaSync } from "execa";
2
2
  import { CliError } from "./errors.js";
3
3
  const SERVICE = "dx-cli";
4
4
  export function getToken(baseUrl) {
@@ -26,14 +26,21 @@ function macosSecretStore() {
26
26
  return {
27
27
  get(baseUrl) {
28
28
  try {
29
- return execFileSync("security", ["find-generic-password", "-s", SERVICE, "-a", baseUrl, "-w"], { encoding: "utf8" }).trim();
29
+ return execaSync("security", [
30
+ "find-generic-password",
31
+ "-s",
32
+ SERVICE,
33
+ "-a",
34
+ baseUrl,
35
+ "-w",
36
+ ]).stdout.trim();
30
37
  }
31
38
  catch {
32
39
  return null;
33
40
  }
34
41
  },
35
42
  set(baseUrl, token) {
36
- execFileSync("security", [
43
+ execaSync("security", [
37
44
  "add-generic-password",
38
45
  "-U",
39
46
  "-s",
@@ -46,7 +53,7 @@ function macosSecretStore() {
46
53
  },
47
54
  delete(baseUrl) {
48
55
  try {
49
- execFileSync("security", [
56
+ execaSync("security", [
50
57
  "delete-generic-password",
51
58
  "-s",
52
59
  SERVICE,
@@ -64,14 +71,20 @@ function linuxSecretStore() {
64
71
  return {
65
72
  get(baseUrl) {
66
73
  try {
67
- return execFileSync("secret-tool", ["lookup", "service", SERVICE, "account", baseUrl], { encoding: "utf8" }).trim();
74
+ return execaSync("secret-tool", [
75
+ "lookup",
76
+ "service",
77
+ SERVICE,
78
+ "account",
79
+ baseUrl,
80
+ ]).stdout.trim();
68
81
  }
69
82
  catch {
70
83
  return null;
71
84
  }
72
85
  },
73
86
  set(baseUrl, token) {
74
- execFileSync("secret-tool", ["store", "--label=dx-cli", "service", SERVICE, "account", baseUrl], { input: token });
87
+ execaSync("secret-tool", ["store", "--label=dx-cli", "service", SERVICE, "account", baseUrl], { input: token });
75
88
  },
76
89
  delete(_baseUrl) {
77
90
  throw new CliError("Linux token deletion is not supported by secret-tool in this implementation");
package/dist/ui/blocks.js CHANGED
@@ -82,6 +82,16 @@ export class DescriptionListItemContainer extends AbstractBlock {
82
82
  this.options = options;
83
83
  }
84
84
  }
85
+ export class CodeBlock extends AbstractBlock {
86
+ text;
87
+ constructor(text) {
88
+ super();
89
+ this.text = text;
90
+ }
91
+ render() {
92
+ return pc.cyan(this.text) + "\n";
93
+ }
94
+ }
85
95
  export class JsonBlock extends AbstractBlock {
86
96
  value;
87
97
  constructor(value) {
package/dist/ui.js CHANGED
@@ -2,7 +2,7 @@ import dayjs from "dayjs";
2
2
  import relativeTimePlugin from "dayjs/plugin/relativeTime.js";
3
3
  import pc from "picocolors";
4
4
  export { GLYPHS } from "./ui/glyphs.js";
5
- import { DescriptionListBlock, DescriptionListItemContainer, HeadingBlock, JsonBlock, ListItemContainer, ParagraphBlock, UnorderedListBlock, } from "./ui/blocks.js";
5
+ import { DescriptionListBlock, DescriptionListItemContainer, HeadingBlock, JsonBlock, ListItemContainer, ParagraphBlock, UnorderedListBlock, CodeBlock, } from "./ui/blocks.js";
6
6
  dayjs.extend(relativeTimePlugin);
7
7
  // Block-level elements
8
8
  export function h1(text) {
@@ -49,9 +49,15 @@ export function dli(term, detailItems) {
49
49
  return new DescriptionListItemContainer(term, detailItems);
50
50
  }
51
51
  }
52
+ export function codeBlock(text) {
53
+ return new CodeBlock(text);
54
+ }
52
55
  export function json(value) {
53
56
  return new JsonBlock(value);
54
57
  }
58
+ export function blankLine() {
59
+ return new ParagraphBlock("", false);
60
+ }
55
61
  // Inline elements
56
62
  export function bold(text) {
57
63
  return pc.bold(text);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@get-dx/cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "DX CLI",
5
5
  "author": "DX <developers@getdx.com>",
6
6
  "bugs": {
@@ -16,7 +16,7 @@
16
16
  "homepage": "https://github.com/get-dx/cli#readme",
17
17
  "license": "Apache-2.0",
18
18
  "engines": {
19
- "node": "20.x"
19
+ "node": "^20.0.0 || ^22.0.0 || ^24.0.0"
20
20
  },
21
21
  "private": false,
22
22
  "publishConfig": {
@@ -38,8 +38,10 @@
38
38
  "typecheck": "tsc --noEmit"
39
39
  },
40
40
  "dependencies": {
41
+ "@inquirer/prompts": "^8.4.1",
41
42
  "commander": "^14.0.1",
42
43
  "dayjs": "^1.11.20",
44
+ "execa": "^9.6.1",
43
45
  "picocolors": "^1.1.1",
44
46
  "yaml": "^2.8.3"
45
47
  },