@get-dx/cli 0.5.5 → 0.6.1

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
@@ -42,9 +42,10 @@ function createProgram() {
42
42
  program
43
43
  .name("dx")
44
44
  .description("DX CLI")
45
- .version(cliPackage.version)
45
+ .version(cliPackage.version, "-v, --version")
46
46
  .addOption(new Option("--json", "Print machine-readable JSON"))
47
47
  .addOption(new Option("--agent <name>", "Agent name to send as an HTTP header"))
48
+ .addOption(new Option("--agent-model <model>", "Agent model name to send as an HTTP header"))
48
49
  .addOption(new Option("--agent-session-id <id>", "Agent session ID to send as an HTTP header"));
49
50
  program.addCommand(authCommand());
50
51
  program.addCommand(catalogCommand());
@@ -6,6 +6,7 @@ export function getContext(command) {
6
6
  return {
7
7
  json: Boolean(root.json),
8
8
  agent: root.agent || process.env.DX_AGENT_NAME,
9
+ agentModel: root.agentModel || process.env.DX_AGENT_MODEL,
9
10
  agentSessionId: root.agentSessionId || process.env.DX_AGENT_SESSION_ID,
10
11
  };
11
12
  }
@@ -101,6 +102,44 @@ export function createExampleText(examples) {
101
102
  }
102
103
  return lines.join("\n");
103
104
  }
105
+ export function createNoteText(paragraphs) {
106
+ const width = getHelpWidth();
107
+ const indent = " ";
108
+ const output = [];
109
+ output.push(""); // separate from the rest of the help text
110
+ output.push("Notes:");
111
+ paragraphs.forEach((paragraph, index) => {
112
+ if (index > 0) {
113
+ output.push(""); // blank line between paragraphs
114
+ }
115
+ output.push(...wrapParagraph(paragraph, width, indent));
116
+ });
117
+ return output.join("\n");
118
+ }
119
+ function getHelpWidth() {
120
+ return process.stdout.isTTY && process.stdout.columns
121
+ ? process.stdout.columns
122
+ : 80;
123
+ }
124
+ function wrapParagraph(text, width, indent) {
125
+ const words = text.split(/\s+/).filter(Boolean);
126
+ const lines = [];
127
+ let current = "";
128
+ for (const word of words) {
129
+ const candidate = current ? `${current} ${word}` : word;
130
+ if (current && indent.length + candidate.length > width) {
131
+ lines.push(`${indent}${current}`);
132
+ current = word;
133
+ }
134
+ else {
135
+ current = candidate;
136
+ }
137
+ }
138
+ if (current) {
139
+ lines.push(`${indent}${current}`);
140
+ }
141
+ return lines;
142
+ }
104
143
  function inferContext(command, argv) {
105
144
  if (command) {
106
145
  return getContext(command);
@@ -170,6 +170,9 @@ function buildHeaders(runtime, accept) {
170
170
  if (runtime.context.agent) {
171
171
  headers.set("X-DX-Agent-Name", runtime.context.agent);
172
172
  }
173
+ if (runtime.context.agentModel) {
174
+ headers.set("X-DX-Agent-Model", runtime.context.agentModel);
175
+ }
173
176
  if (runtime.context.agentSessionId) {
174
177
  headers.set("X-DX-Agent-Session-Id", runtime.context.agentSessionId);
175
178
  }
@@ -22,6 +22,11 @@ edit_access_type: read_only
22
22
  editor_emails: []
23
23
 
24
24
  # Supported chart_type values: line, pie, stacked_bar, scatter, table
25
+ #
26
+ # Tile sql can reference report variables (e.g. $team_ids, $start_date, or a
27
+ # custom variable) as $variable_name. Variables themselves can't be managed
28
+ # from the CLI/API — enable built-in variables or add/edit custom ones for
29
+ # this report in the Data Studio UI, then reference them here.
25
30
  tiles:
26
31
  - title: ""
27
32
  sql: |-
@@ -1,12 +1,16 @@
1
1
  import fs from "fs";
2
2
  import { Command } from "commander";
3
3
  import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
4
- import { createExampleText, getContext, parsePositiveIntOption, wrapAction, } from "../../commandHelpers.js";
4
+ import { createExampleText, createNoteText, getContext, parsePositiveIntOption, wrapAction, } from "../../commandHelpers.js";
5
5
  import { CliError, EXIT_CODES, HttpError } from "../../errors.js";
6
6
  import { request } from "../../http.js";
7
7
  import { renderJson, renderRichText } from "../../renderers.js";
8
8
  import { buildRuntime } from "../../runtime.js";
9
9
  import * as ui from "../../ui.js";
10
+ const VARIABLES_NOTE_TEXT = [
11
+ "Tile SQL can reference report variables using `$variable_name` syntax. Built-in variables — $service_ids, $team_ids, $tag_ids, $user_ids, $repo_ids, $start_date, $end_date — filter by service, team, attribute, user, repo, and date range, respectively. Custom variables are account-defined dropdown filters backed by a SQL query.",
12
+ "Variables cannot be created, updated, enabled, or disabled via this CLI or the API. Built-in variables must be toggled, and custom variables must be added, edited, or removed, from the report's settings in the Data Studio UI. Once a variable is enabled on a report, its `$variable_name` can be used in any tile's SQL via --from-file/--from-stdin.",
13
+ ];
10
14
  export function reportsCommand() {
11
15
  const reports = new Command()
12
16
  .name("reports")
@@ -16,6 +20,7 @@ export function reportsCommand() {
16
20
  .description("Create a Data Studio report from a YAML file or stdin")
17
21
  .option("--from-file <path>", "Read a YAML file and create a report from its contents")
18
22
  .option("--from-stdin", "Read YAML from stdin and create a report from its contents")
23
+ .addHelpText("afterAll", createNoteText(VARIABLES_NOTE_TEXT))
19
24
  .addHelpText("afterAll", createExampleText([
20
25
  {
21
26
  label: "Create a studio report from a YAML file",
@@ -174,6 +179,7 @@ export function reportsCommand() {
174
179
  .argument("<id>", "Studio report ID")
175
180
  .option("--from-file <path>", "Read a YAML file and update the report with its contents")
176
181
  .option("--from-stdin", "Read YAML from stdin and update the report with its contents")
182
+ .addHelpText("afterAll", createNoteText(VARIABLES_NOTE_TEXT))
177
183
  .addHelpText("afterAll", createExampleText([
178
184
  {
179
185
  label: "Initialize a file first",
@@ -361,9 +367,9 @@ function studioReportToYaml(report) {
361
367
  description: report.description ?? "",
362
368
  markdown_notes: report.markdown_notes ?? "",
363
369
  view_access_type: report.view_access_type,
364
- viewer_emails: [],
370
+ viewer_emails: report.viewer_emails ?? [],
365
371
  edit_access_type: report.edit_access_type,
366
- editor_emails: [],
372
+ editor_emails: report.editor_emails ?? [],
367
373
  tiles: report.tiles.map((tile) => ({
368
374
  id: tile.id,
369
375
  title: tile.title,
package/dist/config.js CHANGED
@@ -2,7 +2,7 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { CliError, EXIT_CODES } from "./errors.js";
5
- const DEFAULT_API_BASE_URL = "https://api.getdx.com";
5
+ export const DEFAULT_API_BASE_URL = "https://api.getdx.com";
6
6
  const DEFAULT_WEB_BASE_URL = "https://app.getdx.com";
7
7
  // --- Config file I/O ---------------------------------------------------------
8
8
  export function getConfigPath() {
@@ -14,7 +14,16 @@ export function readConfig() {
14
14
  return {};
15
15
  }
16
16
  const content = fs.readFileSync(configPath, "utf8");
17
- const raw = JSON.parse(content);
17
+ let raw;
18
+ try {
19
+ raw = JSON.parse(content);
20
+ }
21
+ catch {
22
+ // The config file can be left empty or truncated if a previous `dx`
23
+ // process was killed mid-write, or two processes wrote concurrently.
24
+ // Treat it like a missing config rather than crashing every command.
25
+ return {};
26
+ }
18
27
  const stored = {};
19
28
  if (typeof raw.webBaseUrl === "string") {
20
29
  stored.webBaseUrl = raw.webBaseUrl;
@@ -46,8 +55,15 @@ export function persistVersionPromptSelection(prompt) {
46
55
  writeConfig(config);
47
56
  }
48
57
  export function writeConfig(config) {
49
- fs.mkdirSync(getConfigDir(), { recursive: true });
50
- fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf8");
58
+ const configDir = getConfigDir();
59
+ fs.mkdirSync(configDir, { recursive: true });
60
+ const configPath = getConfigPath();
61
+ // Write to a temp file and rename into place so a crash or a race between
62
+ // concurrent `dx` processes can't leave config.json empty or truncated —
63
+ // renames are atomic, so readers always see either the old or new content.
64
+ const tempPath = path.join(configDir, `.config.json.${process.pid}.${Date.now()}.tmp`);
65
+ fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + "\n", "utf8");
66
+ fs.renameSync(tempPath, configPath);
51
67
  }
52
68
  export function persistBaseUrls(apiBaseUrl, webBaseUrl) {
53
69
  writeConfig({
package/dist/http.js CHANGED
@@ -14,6 +14,9 @@ export async function request(runtime, route, options = {}) {
14
14
  if (runtime.context.agent) {
15
15
  headers.set("X-DX-Agent-Name", runtime.context.agent);
16
16
  }
17
+ if (runtime.context.agentModel) {
18
+ headers.set("X-DX-Agent-Model", runtime.context.agentModel);
19
+ }
17
20
  if (runtime.context.agentSessionId) {
18
21
  headers.set("X-DX-Agent-Session-Id", runtime.context.agentSessionId);
19
22
  }
@@ -49,7 +49,16 @@ export async function loginViaBrowser(webBaseUrl) {
49
49
  error: new CliError(`Authentication failed: token exchange failed with status ${tokenExchangeResponse.status}`),
50
50
  };
51
51
  }
52
- const body = (await tokenExchangeResponse.json());
52
+ let body;
53
+ try {
54
+ body = (await tokenExchangeResponse.json());
55
+ }
56
+ catch {
57
+ return {
58
+ type: "ERROR",
59
+ error: new CliError("Authentication failed: token exchange returned an invalid response"),
60
+ };
61
+ }
53
62
  return {
54
63
  type: "SUCCESS",
55
64
  token: body.access_token,
@@ -11,6 +11,7 @@ import cliPackage from "../package.json" with { type: "json" };
11
11
  const VERSION_CHECK_INTERVAL_HOURS = 24;
12
12
  const SNOOZE_DAYS = 7;
13
13
  const SKIP_COMMANDS = new Set(["auth", "init"]);
14
+ const SKIP_OPTIONS = new Set(["-h", "--help", "-v", "--version"]);
14
15
  /**
15
16
  * Compares two semver strings (e.g. "1.2.3").
16
17
  * Returns a negative number if a < b, 0 if equal, positive if a > b.
@@ -67,13 +68,21 @@ export async function checkForNewVersion(argv) {
67
68
  if (process.env.DX_DISABLE_VERSION_CHECK) {
68
69
  return { status: "disabled" };
69
70
  }
70
- const topLevelCommand = argv.slice(2).find((arg) => !arg.startsWith("-"));
71
+ const args = argv.slice(2);
72
+ if (args.some((arg) => SKIP_OPTIONS.has(arg))) {
73
+ return { status: "disabled" };
74
+ }
75
+ const topLevelCommand = args.find((arg) => !arg.startsWith("-"));
76
+ if (!topLevelCommand) {
77
+ return { status: "disabled" };
78
+ }
71
79
  if (topLevelCommand && SKIP_COMMANDS.has(topLevelCommand)) {
72
80
  return { status: "disabled" };
73
81
  }
74
82
  const context = {
75
83
  json: argv.includes("--json"),
76
84
  agent: undefined,
85
+ agentModel: undefined,
77
86
  agentSessionId: undefined,
78
87
  };
79
88
  const runtime = await buildRuntimeSafe(context);
@@ -137,6 +146,8 @@ export async function promptVersionUpdate(latestVersion) {
137
146
  { name: `Remind me later (in ${SNOOZE_DAYS} days)`, value: "snooze" },
138
147
  { name: "Skip this version", value: "skip" },
139
148
  ],
149
+ }, {
150
+ output: process.stderr,
140
151
  });
141
152
  if (choice === "update") {
142
153
  persistVersionPromptSelection(undefined);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@get-dx/cli",
3
- "version": "0.5.5",
3
+ "version": "0.6.1",
4
4
  "description": "DX CLI",
5
5
  "author": "DX <developers@getdx.com>",
6
6
  "bugs": {