@get-dx/cli 0.2.0 → 0.3.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/config.js CHANGED
@@ -1,14 +1,10 @@
1
1
  import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
- const DEFAULT_BASE_URL = "https://api.getdx.com";
5
- export function getConfigDir() {
6
- const xdg = process.env.XDG_CONFIG_HOME;
7
- if (xdg) {
8
- return path.join(xdg, "dx");
9
- }
10
- return path.join(os.homedir(), ".config", "dx");
11
- }
4
+ import { CliError, EXIT_CODES } from "./errors.js";
5
+ const DEFAULT_API_BASE_URL = "https://api.getdx.com";
6
+ const DEFAULT_WEB_BASE_URL = "https://app.getdx.com";
7
+ // --- Config file I/O ---------------------------------------------------------
12
8
  export function getConfigPath() {
13
9
  return path.join(getConfigDir(), "config.json");
14
10
  }
@@ -18,38 +14,184 @@ export function readConfig() {
18
14
  return {};
19
15
  }
20
16
  const content = fs.readFileSync(configPath, "utf8");
21
- return JSON.parse(content);
17
+ const raw = JSON.parse(content);
18
+ const api = raw.apiBaseUrl;
19
+ const web = raw.webBaseUrl;
20
+ const stored = {};
21
+ if (web) {
22
+ stored.webBaseUrl = web;
23
+ }
24
+ if (api) {
25
+ stored.apiBaseUrl = api;
26
+ }
27
+ return stored;
28
+ }
29
+ /**
30
+ * Responsible for handling the migration from `baseUrl` to `apiBaseUrl` and `webBaseUrl`,
31
+ * so the upgrade to 0.3.x will be non-breaking for `cloud` and `dedicated` deployments.
32
+ *
33
+ * `managed` deployments will encounter an error and will have to reauthenticate.
34
+ *
35
+ * TODO: delete after 2026-07-01.
36
+ */
37
+ export function handleTemporaryBaseUrlMigration(argv) {
38
+ if (!process.env.VITEST) {
39
+ // Running tests - we don't want our permanent tests to exercise this logic
40
+ return;
41
+ }
42
+ const COMMANDS_TO_SKIP_MIGRATION = ["login", "logout", "init"];
43
+ if (COMMANDS_TO_SKIP_MIGRATION.some((command) => argv.includes(command))) {
44
+ // Logging in or logging out: we need to ignore the current config file's contents
45
+ return;
46
+ }
47
+ const configPath = getConfigPath();
48
+ if (!fs.existsSync(configPath)) {
49
+ // No config file, no need to migrate
50
+ return;
51
+ }
52
+ const content = fs.readFileSync(configPath, "utf8");
53
+ const raw = JSON.parse(content);
54
+ const api = raw.apiBaseUrl;
55
+ const web = raw.webBaseUrl;
56
+ if (api !== undefined && web !== undefined) {
57
+ // Already migrated, no need to do anything
58
+ return;
59
+ }
60
+ if (raw.baseUrl) {
61
+ // Legacy baseUrl is present, migrate to apiBaseUrl and webBaseUrl
62
+ const apiBaseUrl = raw.baseUrl;
63
+ let webBaseUrl;
64
+ if (apiBaseUrl.endsWith(".getdx.com")) {
65
+ // Migrate `cloud`
66
+ webBaseUrl = "https://app.getdx.com";
67
+ writeConfig({
68
+ apiBaseUrl: normalizeUrl(apiBaseUrl),
69
+ webBaseUrl: normalizeUrl(webBaseUrl),
70
+ });
71
+ return;
72
+ }
73
+ const dedicatedMatch = apiBaseUrl.match(/^api\.(.+)\.getdx\.io$/);
74
+ if (dedicatedMatch) {
75
+ // Migrate `dedicated`
76
+ webBaseUrl = `https://${dedicatedMatch[1]}.getdx.io`;
77
+ writeConfig({
78
+ apiBaseUrl: normalizeUrl(apiBaseUrl),
79
+ webBaseUrl: normalizeUrl(webBaseUrl),
80
+ });
81
+ return;
82
+ }
83
+ // Unable to automatically migrate `managed`
84
+ throw new CliError("Your on-disk DX CLI config is missing apiBaseUrl and webBaseUrl. Run `dx auth logout`, then login again via `dx init` or `dx auth login`.");
85
+ }
22
86
  }
23
87
  export function writeConfig(config) {
24
88
  fs.mkdirSync(getConfigDir(), { recursive: true });
25
89
  fs.writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf8");
26
90
  }
27
- export function resolveBaseUrl() {
28
- if (process.env.DX_BASE_URL) {
29
- return normalizeBaseUrl(process.env.DX_BASE_URL);
91
+ export function persistBaseUrls(apiBaseUrl, webBaseUrl) {
92
+ writeConfig({
93
+ ...readConfig(),
94
+ apiBaseUrl: normalizeUrl(apiBaseUrl),
95
+ webBaseUrl: normalizeUrl(webBaseUrl),
96
+ });
97
+ }
98
+ /** Strips trailing slashes from an HTTP(S) base URL string. */
99
+ export function normalizeUrl(url) {
100
+ return url.replace(/\/+$/, "");
101
+ }
102
+ function getConfigDir() {
103
+ const xdg = process.env.XDG_CONFIG_HOME;
104
+ if (xdg) {
105
+ return path.join(xdg, "dx");
30
106
  }
31
- return normalizeBaseUrl(readConfig().baseUrl || DEFAULT_BASE_URL);
107
+ return path.join(os.homedir(), ".config", "dx");
32
108
  }
109
+ // --- Resolving URLs at command runtime ------------------------------------------------------
33
110
  /**
34
- * Web UI origin for browser OAuth (`/cli/auth`), inferred from the API base URL.
35
- * Dedicated and cloud hostnames map to their app origins; otherwise the API URL origin is used.
111
+ * Resolved API base URL for HTTP requests.
112
+ * Precedence: `DX_API_BASE_URL`, then persisted `apiBaseUrl`, then DX Cloud default.
36
113
  */
37
- export function resolveUiUrl(apiBaseUrl) {
38
- const normalized = normalizeBaseUrl(apiBaseUrl);
39
- const url = new URL(normalized);
40
- const host = url.hostname;
41
- if (host === "api.getdx.com") {
42
- return "https://app.getdx.com";
114
+ export function resolveApiBaseUrl() {
115
+ if (process.env.DX_API_BASE_URL) {
116
+ return normalizeUrl(process.env.DX_API_BASE_URL);
43
117
  }
44
- const dedicated = host.match(/^api\.(.+)\.getdx\.io$/);
45
- if (dedicated) {
46
- return `https://${dedicated[1]}.getdx.io`;
118
+ return normalizeUrl(readConfig().apiBaseUrl || DEFAULT_API_BASE_URL);
119
+ }
120
+ /**
121
+ * Resolved web-app base URL for CLI deep links and browser OAuth.
122
+ * Precedence: `DX_WEB_BASE_URL`, persisted `webBaseUrl`, then DX Cloud default.
123
+ */
124
+ export function resolveWebBaseUrl() {
125
+ if (process.env.DX_WEB_BASE_URL) {
126
+ return normalizeUrl(process.env.DX_WEB_BASE_URL);
47
127
  }
48
- return url.origin;
128
+ return normalizeUrl(readConfig().webBaseUrl || DEFAULT_WEB_BASE_URL);
49
129
  }
50
- export function persistBaseUrl(baseUrl) {
51
- writeConfig({ ...readConfig(), baseUrl: normalizeBaseUrl(baseUrl) });
130
+ // --- Deriving base URLs at login time -------------------------------------
131
+ /**
132
+ * Derive the appropriate `apiBaseUrl` and `webBaseUrl` to persist into config,
133
+ * considering the `DX_API_BASE_URL` and `DX_WEB_BASE_URL` env vars if they are present.
134
+ *
135
+ * If the env vars are not set, we assume the user is configuring for a `cloud` deployment.
136
+ *
137
+ * If they are set, we parse them to determine whether the deployment is `cloud`, `dedicated`, or `managed`.
138
+ *
139
+ * If we fail to derive the base URLs, a `CliError` is thrown.
140
+ */
141
+ export function deriveBaseUrlsFromEnv() {
142
+ const apiBaseUrlFromEnv = process.env.DX_API_BASE_URL;
143
+ const webBaseUrlFromEnv = process.env.DX_WEB_BASE_URL;
144
+ if (apiBaseUrlFromEnv && !webBaseUrlFromEnv) {
145
+ throw new CliError("DX_WEB_BASE_URL must be set when DX_API_BASE_URL is set", EXIT_CODES.ARGUMENT_ERROR);
146
+ }
147
+ const parsed = webBaseUrlFromEnv
148
+ ? parseWebBaseUrl(webBaseUrlFromEnv)
149
+ : { type: "cloud" };
150
+ switch (parsed.type) {
151
+ case "cloud":
152
+ return {
153
+ apiBaseUrl: "https://api.getdx.com",
154
+ webBaseUrl: "https://app.getdx.com",
155
+ };
156
+ case "dedicated": {
157
+ const { accountName } = parsed;
158
+ return {
159
+ apiBaseUrl: `https://api.${accountName}.getdx.io`,
160
+ webBaseUrl: `https://${accountName}.getdx.io`,
161
+ };
162
+ }
163
+ case "managed": {
164
+ const apiBaseUrl = process.env.DX_API_BASE_URL;
165
+ if (!apiBaseUrl) {
166
+ throw new CliError("DX_API_BASE_URL must be specified when authenticating with a managed deployment");
167
+ }
168
+ return {
169
+ apiBaseUrl,
170
+ webBaseUrl: parsed.webAppUrl,
171
+ };
172
+ }
173
+ case "invalid":
174
+ throw new CliError(`Could not recognize web base URL "${parsed.input}". Expected https://app.getdx.com, https://<account>.getdx.io, or a custom domain.`);
175
+ }
52
176
  }
53
- function normalizeBaseUrl(baseUrl) {
54
- return baseUrl.replace(/\/+$/, "");
177
+ function parseWebBaseUrl(raw) {
178
+ const normalized = raw.trim().replace(/\/$/, "");
179
+ if (!normalized || normalized === "app.getdx.com") {
180
+ return { type: "cloud" };
181
+ }
182
+ try {
183
+ const url = new URL(normalized.startsWith("http") ? normalized : `https://${normalized}`);
184
+ const host = url.hostname;
185
+ const dedicatedMatch = host.match(/^(.+)\.getdx\.io$/);
186
+ if (dedicatedMatch) {
187
+ return { type: "dedicated", accountName: dedicatedMatch[1] };
188
+ }
189
+ if (host) {
190
+ return { type: "managed", webAppUrl: url.origin };
191
+ }
192
+ }
193
+ catch {
194
+ // fall through to invalid
195
+ }
196
+ return { type: "invalid", input: raw };
55
197
  }
package/dist/http.js CHANGED
@@ -23,7 +23,7 @@ export async function request(runtime, route, options = {}) {
23
23
  query.set(key, String(value));
24
24
  }
25
25
  });
26
- const url = `${runtime.baseUrl}${route}${query.size > 0 ? `?${query.toString()}` : ""}`;
26
+ const url = `${runtime.apiBaseUrl}${route}${query.size > 0 ? `?${query.toString()}` : ""}`;
27
27
  const requestBody = options.body !== undefined ? JSON.stringify(options.body) : undefined;
28
28
  runtime.logger.debug("Sending HTTP request", {
29
29
  body: options.body ?? null,
@@ -4,7 +4,7 @@ import { AuthCodeCallbackServer, } from "./authCodeCallbackServer.js";
4
4
  import { CliError } from "./errors.js";
5
5
  import { renderRichText } from "./renderers.js";
6
6
  import * as ui from "./ui.js";
7
- export async function loginViaBrowser(uiBaseUrl) {
7
+ export async function loginViaBrowser(webBaseUrl) {
8
8
  const server = new AuthCodeCallbackServer();
9
9
  await server.start();
10
10
  const callbackUri = server.getAddress();
@@ -17,7 +17,7 @@ export async function loginViaBrowser(uiBaseUrl) {
17
17
  code_challenge: codeChallenge,
18
18
  code_challenge_method: "S256",
19
19
  });
20
- const authUrl = `${uiBaseUrl}/cli/auth?${params.toString()}`;
20
+ const authUrl = `${webBaseUrl}/cli/auth?${params.toString()}`;
21
21
  renderRichText([
22
22
  ui.p(`Opening your browser to complete authentication...`),
23
23
  ui.blankLine(),
@@ -42,7 +42,7 @@ export async function loginViaBrowser(uiBaseUrl) {
42
42
  error: new CliError("Authentication failed: state mismatch"),
43
43
  };
44
44
  }
45
- const tokenExchangeResponse = await exchangeCodeForToken(uiBaseUrl, code, codeVerifier);
45
+ const tokenExchangeResponse = await exchangeCodeForToken(webBaseUrl, code, codeVerifier);
46
46
  if (!tokenExchangeResponse.ok) {
47
47
  return {
48
48
  type: "ERROR",
@@ -64,8 +64,8 @@ export async function loginViaBrowser(uiBaseUrl) {
64
64
  function validateState(stateFromResponse, originalState) {
65
65
  return stateFromResponse === originalState;
66
66
  }
67
- async function exchangeCodeForToken(baseUrl, code, codeVerifier) {
68
- return fetch(`${baseUrl}/cli/token`, {
67
+ async function exchangeCodeForToken(webBaseUrl, code, codeVerifier) {
68
+ return fetch(`${webBaseUrl}/cli/token`, {
69
69
  method: "POST",
70
70
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
71
71
  body: new URLSearchParams({ code, code_verifier: codeVerifier }),
package/dist/runtime.js CHANGED
@@ -1,4 +1,4 @@
1
- import { resolveBaseUrl } from "./config.js";
1
+ import { resolveApiBaseUrl, resolveWebBaseUrl } from "./config.js";
2
2
  import { CliError } from "./errors.js";
3
3
  import { getToken } from "./secrets.js";
4
4
  import { createLogger } from "./logger.js";
@@ -7,13 +7,15 @@ export function buildLogger(context) {
7
7
  return createLogger({ json: context.json || !process.stderr.isTTY });
8
8
  }
9
9
  export function buildRuntime(context, overrides) {
10
- const baseUrl = overrides?.baseUrl ?? resolveBaseUrl();
11
- const token = overrides?.token ?? getToken(baseUrl);
10
+ const apiBaseUrl = overrides?.apiBaseUrl ?? resolveApiBaseUrl();
11
+ const webBaseUrl = overrides?.webBaseUrl ?? resolveWebBaseUrl();
12
+ const token = overrides?.token ?? getToken(apiBaseUrl);
12
13
  if (!token) {
13
14
  throw new CliError("No API token configured. Run `dx auth login --token <token>` or set DX_API_TOKEN.");
14
15
  }
15
16
  return {
16
- baseUrl,
17
+ apiBaseUrl,
18
+ webBaseUrl,
17
19
  token,
18
20
  context,
19
21
  version: cliPackage.version,
@@ -21,13 +23,15 @@ export function buildRuntime(context, overrides) {
21
23
  };
22
24
  }
23
25
  export function buildRuntimeSafe(context, overrides) {
24
- const baseUrl = overrides?.baseUrl ?? resolveBaseUrl();
25
- const token = overrides?.token ?? getToken(baseUrl);
26
+ const apiBaseUrl = overrides?.apiBaseUrl ?? resolveApiBaseUrl();
27
+ const webBaseUrl = overrides?.webBaseUrl ?? resolveWebBaseUrl();
28
+ const token = overrides?.token ?? getToken(apiBaseUrl);
26
29
  if (!token) {
27
30
  return null;
28
31
  }
29
32
  return {
30
- baseUrl,
33
+ apiBaseUrl,
34
+ webBaseUrl,
31
35
  token,
32
36
  context,
33
37
  version: cliPackage.version,
package/dist/ui.js CHANGED
@@ -58,6 +58,14 @@ export function json(value) {
58
58
  export function blankLine() {
59
59
  return new ParagraphBlock("", false);
60
60
  }
61
+ export function truncatedSqlBlock(sql) {
62
+ const lines = sql.split("\n");
63
+ const firstLine = lines[0];
64
+ const isLong = firstLine.length > 80 || lines.length > 1;
65
+ const truncated = firstLine.length > 80 ? firstLine.slice(0, 80) : firstLine;
66
+ const display = isLong ? truncated + dim(" …") : truncated;
67
+ return p(code(display));
68
+ }
61
69
  // Inline elements
62
70
  export function bold(text) {
63
71
  return pc.bold(text);
@@ -113,3 +121,6 @@ export function maskToken(token) {
113
121
  }
114
122
  return `${token.slice(0, 4)}${"*".repeat(token.length - 8)}${token.slice(-4)}`;
115
123
  }
124
+ export function webLink(path, runtime) {
125
+ return `${runtime.webBaseUrl}${path}`;
126
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@get-dx/cli",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "DX CLI",
5
5
  "author": "DX <developers@getdx.com>",
6
6
  "bugs": {