@get-dx/cli 0.2.1 → 0.3.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/README.md +28 -15
- package/dist/cli.js +4 -0
- package/dist/commands/auth.js +27 -22
- package/dist/commands/catalog/entities.js +6 -6
- package/dist/commands/catalog/entitiesRendering.js +13 -9
- package/dist/commands/catalog/entityTypes.js +4 -4
- package/dist/commands/catalog/entityTypesRendering.js +15 -21
- package/dist/commands/init.js +15 -60
- package/dist/commands/scorecards.js +4 -4
- package/dist/commands/scorecardsRendering.js +8 -5
- package/dist/commands/snapshots.js +379 -0
- package/dist/commands/studio/query.js +2 -2
- package/dist/config.js +171 -29
- package/dist/http.js +1 -1
- package/dist/loginViaBrowser.js +5 -5
- package/dist/runtime.js +11 -7
- package/dist/ui.js +3 -0
- package/package.json +1 -1
|
@@ -0,0 +1,379 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createExampleText, getContext, parsePositiveIntOption, wrapAction, } from "../commandHelpers.js";
|
|
3
|
+
import { CliError, EXIT_CODES } from "../errors.js";
|
|
4
|
+
import { request } from "../http.js";
|
|
5
|
+
import { renderJson, renderRichText } from "../renderers.js";
|
|
6
|
+
import { buildRuntime } from "../runtime.js";
|
|
7
|
+
import * as ui from "../ui.js";
|
|
8
|
+
export function snapshotsCommand() {
|
|
9
|
+
const snapshots = new Command()
|
|
10
|
+
.name("snapshots")
|
|
11
|
+
.description("Work with DX snapshots");
|
|
12
|
+
const csatComments = new Command()
|
|
13
|
+
.name("csatComments")
|
|
14
|
+
.description("Work with snapshot CSAT comments");
|
|
15
|
+
csatComments
|
|
16
|
+
.command("list")
|
|
17
|
+
.description("List CSAT comments for a snapshot")
|
|
18
|
+
.option("--id <id>", "The unique ID of the snapshot")
|
|
19
|
+
.option("--cursor <cursor>", "Cursor for the next page of results")
|
|
20
|
+
.option("--limit <n>", "Max comments per page (default is 50, max is 100)", (value) => parseLimitOption(value, "--limit"))
|
|
21
|
+
.addHelpText("afterAll", createExampleText([
|
|
22
|
+
{
|
|
23
|
+
label: "List CSAT comments for a snapshot",
|
|
24
|
+
command: "dx snapshots csatComments list --id MjUyNbaY",
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
label: "List CSAT comments as JSON",
|
|
28
|
+
command: "dx --json snapshots csatComments list --id MjUyNbaY",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
label: "Fetch the next page using a cursor from the prior response",
|
|
32
|
+
command: "dx snapshots csatComments list --id MjUyNbaY --cursor xuvkgfq9t0ty --limit 100",
|
|
33
|
+
},
|
|
34
|
+
]))
|
|
35
|
+
.action(wrapAction(async (options, command) => {
|
|
36
|
+
const id = parseRequiredTextOption(options.id, "--id");
|
|
37
|
+
const runtime = buildRuntime(getContext(command));
|
|
38
|
+
const response = await listSnapshotCsatComments(runtime, {
|
|
39
|
+
id,
|
|
40
|
+
cursor: options.cursor,
|
|
41
|
+
limit: options.limit,
|
|
42
|
+
});
|
|
43
|
+
if (runtime.context.json) {
|
|
44
|
+
renderJson(response);
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
renderSnapshotCsatComments(extractCsatComments(response), response.response_metadata?.next_cursor ?? null, id, runtime);
|
|
48
|
+
}
|
|
49
|
+
}));
|
|
50
|
+
snapshots.addCommand(csatComments);
|
|
51
|
+
const driverComments = new Command()
|
|
52
|
+
.name("driverComments")
|
|
53
|
+
.description("Work with snapshot driver comments");
|
|
54
|
+
driverComments
|
|
55
|
+
.command("list")
|
|
56
|
+
.description("List driver comments for a snapshot")
|
|
57
|
+
.option("--id <id>", "The unique ID of the snapshot")
|
|
58
|
+
.option("--cursor <cursor>", "Cursor for the next page of results")
|
|
59
|
+
.option("--limit <n>", "Max comments per page (default is 50, max is 100)", (value) => parseLimitOption(value, "--limit"))
|
|
60
|
+
.addHelpText("afterAll", createExampleText([
|
|
61
|
+
{
|
|
62
|
+
label: "List driver comments for a snapshot",
|
|
63
|
+
command: "dx snapshots driverComments list --id MjUyNbaY",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
label: "List driver comments as JSON",
|
|
67
|
+
command: "dx --json snapshots driverComments list --id MjUyNbaY",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
label: "Fetch the next page using a cursor from the prior response",
|
|
71
|
+
command: "dx snapshots driverComments list --id MjUyNbaY --cursor xuvkgfq9t0ty --limit 100",
|
|
72
|
+
},
|
|
73
|
+
]))
|
|
74
|
+
.action(wrapAction(async (options, command) => {
|
|
75
|
+
const id = parseRequiredTextOption(options.id, "--id");
|
|
76
|
+
const runtime = buildRuntime(getContext(command));
|
|
77
|
+
const response = await listSnapshotDriverComments(runtime, {
|
|
78
|
+
id,
|
|
79
|
+
cursor: options.cursor,
|
|
80
|
+
limit: options.limit,
|
|
81
|
+
});
|
|
82
|
+
if (runtime.context.json) {
|
|
83
|
+
renderJson(response);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
renderSnapshotDriverComments(extractDriverComments(response), response.response_metadata?.next_cursor ?? null, id, runtime);
|
|
87
|
+
}
|
|
88
|
+
}));
|
|
89
|
+
snapshots.addCommand(driverComments);
|
|
90
|
+
snapshots
|
|
91
|
+
.command("info")
|
|
92
|
+
.description("Retrieve results for a single snapshot")
|
|
93
|
+
.option("--id <snapshot_id>", "The unique ID of the snapshot")
|
|
94
|
+
.addHelpText("afterAll", createExampleText([
|
|
95
|
+
{
|
|
96
|
+
label: "Fetch info for a snapshot",
|
|
97
|
+
command: "dx snapshots info --id MjUyNbaY",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
label: "Fetch snapshot info as JSON",
|
|
101
|
+
command: "dx --json snapshots info --id MjUyNbaY",
|
|
102
|
+
},
|
|
103
|
+
]))
|
|
104
|
+
.action(wrapAction(async (options, command) => {
|
|
105
|
+
const snapshotId = parseRequiredTextOption(options.id, "--id");
|
|
106
|
+
const runtime = buildRuntime(getContext(command));
|
|
107
|
+
const response = await getSnapshotInfo(runtime, snapshotId);
|
|
108
|
+
if (runtime.context.json) {
|
|
109
|
+
renderJson(response);
|
|
110
|
+
}
|
|
111
|
+
else {
|
|
112
|
+
renderSnapshotInfo(response.snapshot, snapshotId, runtime);
|
|
113
|
+
}
|
|
114
|
+
}));
|
|
115
|
+
snapshots
|
|
116
|
+
.command("list")
|
|
117
|
+
.description("List DX snapshots")
|
|
118
|
+
.addHelpText("afterAll", createExampleText([
|
|
119
|
+
{
|
|
120
|
+
label: "List snapshots",
|
|
121
|
+
command: "dx snapshots list",
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
label: "List snapshots as JSON",
|
|
125
|
+
command: "dx --json snapshots list",
|
|
126
|
+
},
|
|
127
|
+
]))
|
|
128
|
+
.action(wrapAction(async (_options, command) => {
|
|
129
|
+
const runtime = buildRuntime(getContext(command));
|
|
130
|
+
const response = await listSnapshots(runtime);
|
|
131
|
+
if (runtime.context.json) {
|
|
132
|
+
renderJson(response);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
renderSnapshots(response.snapshots, runtime);
|
|
136
|
+
}
|
|
137
|
+
}));
|
|
138
|
+
return snapshots;
|
|
139
|
+
}
|
|
140
|
+
async function listSnapshotDriverComments(runtime, options) {
|
|
141
|
+
const response = await request(runtime, "/snapshots.driverComments.list", {
|
|
142
|
+
method: "GET",
|
|
143
|
+
query: {
|
|
144
|
+
id: options.id,
|
|
145
|
+
cursor: options.cursor,
|
|
146
|
+
limit: options.limit,
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
return response.body;
|
|
150
|
+
}
|
|
151
|
+
async function listSnapshotCsatComments(runtime, options) {
|
|
152
|
+
const response = await request(runtime, "/snapshots.csatComments.list", {
|
|
153
|
+
method: "GET",
|
|
154
|
+
query: {
|
|
155
|
+
id: options.id,
|
|
156
|
+
cursor: options.cursor,
|
|
157
|
+
limit: options.limit,
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
return response.body;
|
|
161
|
+
}
|
|
162
|
+
async function getSnapshotInfo(runtime, snapshotId) {
|
|
163
|
+
const response = await request(runtime, "/snapshots.info", {
|
|
164
|
+
method: "GET",
|
|
165
|
+
query: { snapshot_id: snapshotId },
|
|
166
|
+
});
|
|
167
|
+
return response.body;
|
|
168
|
+
}
|
|
169
|
+
async function listSnapshots(runtime) {
|
|
170
|
+
const response = await request(runtime, "/snapshots.list", {
|
|
171
|
+
method: "GET",
|
|
172
|
+
query: {
|
|
173
|
+
ordering: "completed_at",
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
return response.body;
|
|
177
|
+
}
|
|
178
|
+
function renderSnapshotDriverComments(comments, nextCursor, snapshotId, runtime) {
|
|
179
|
+
const blocks = [ui.h1("Snapshot Driver Comments")];
|
|
180
|
+
blocks.push(ui.p(`Displaying ${ui.bold(comments.length.toString())} comments.`));
|
|
181
|
+
blocks.push(ui.p(ui.link(snapshotWebLink(snapshotId, "drivers/comments", runtime))));
|
|
182
|
+
if (nextCursor) {
|
|
183
|
+
blocks.push(ui.p(`Next cursor: ${ui.code(nextCursor)}`));
|
|
184
|
+
}
|
|
185
|
+
if (comments.length === 0) {
|
|
186
|
+
blocks.push(ui.p(ui.dim("(None)")));
|
|
187
|
+
renderRichText(blocks);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
for (const comment of comments) {
|
|
191
|
+
blocks.push(ui.h2(formatCommentHeading(comment)));
|
|
192
|
+
const details = [
|
|
193
|
+
optionalDetail("Email", formatText(comment.email), comment.email),
|
|
194
|
+
optionalDetail("Driver", formatText(comment.driver_name ?? comment.driver), comment.driver_name ?? comment.driver),
|
|
195
|
+
optionalDetail("Driver ID", formatText(comment.driver_id), comment.driver_id),
|
|
196
|
+
optionalDetail("Team", formatText(comment.team_name), comment.team_name),
|
|
197
|
+
optionalDetail("Team ID", formatText(comment.team_id), comment.team_id),
|
|
198
|
+
optionalDetail("Item", formatText(comment.item_name), comment.item_name),
|
|
199
|
+
optionalDetail("Item ID", formatText(comment.item_id), comment.item_id),
|
|
200
|
+
optionalDetail("Snapshot ID", formatText(comment.snapshot_id), comment.snapshot_id),
|
|
201
|
+
optionalDetail("Timestamp", formatTimestamp(comment.timestamp), comment.timestamp),
|
|
202
|
+
].filter((detail) => detail !== null);
|
|
203
|
+
if (details.length > 0) {
|
|
204
|
+
blocks.push(ui.dl(details, { termWidth: 11 }));
|
|
205
|
+
}
|
|
206
|
+
blocks.push(ui.p(formatText(comment.text ?? comment.comment)));
|
|
207
|
+
}
|
|
208
|
+
renderRichText(blocks);
|
|
209
|
+
}
|
|
210
|
+
function renderSnapshotCsatComments(comments, nextCursor, snapshotId, runtime) {
|
|
211
|
+
const blocks = [ui.h1("Snapshot CSAT Comments")];
|
|
212
|
+
blocks.push(ui.p(`Displaying ${ui.bold(comments.length.toString())} comments.`));
|
|
213
|
+
blocks.push(ui.p(ui.link(snapshotWebLink(snapshotId, "csat/comments", runtime))));
|
|
214
|
+
if (nextCursor) {
|
|
215
|
+
blocks.push(ui.p(`Next cursor: ${ui.code(nextCursor)}`));
|
|
216
|
+
}
|
|
217
|
+
if (comments.length === 0) {
|
|
218
|
+
blocks.push(ui.p(ui.dim("(None)")));
|
|
219
|
+
renderRichText(blocks);
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
for (const comment of comments) {
|
|
223
|
+
blocks.push(ui.h2(formatCommentHeading(comment)));
|
|
224
|
+
const details = [
|
|
225
|
+
optionalDetail("Email", formatText(comment.email), comment.email),
|
|
226
|
+
optionalDetail("Team", formatText(comment.team_name), comment.team_name),
|
|
227
|
+
optionalDetail("Team ID", formatText(comment.team_id), comment.team_id),
|
|
228
|
+
optionalDetail("Item", formatText(comment.item_name), comment.item_name),
|
|
229
|
+
optionalDetail("Item ID", formatText(comment.item_id), comment.item_id),
|
|
230
|
+
optionalDetail("Snapshot ID", formatText(comment.snapshot_id), comment.snapshot_id),
|
|
231
|
+
optionalDetail("Timestamp", formatTimestamp(comment.timestamp), comment.timestamp),
|
|
232
|
+
].filter((detail) => detail !== null);
|
|
233
|
+
if (details.length > 0) {
|
|
234
|
+
blocks.push(ui.dl(details, { termWidth: 11 }));
|
|
235
|
+
}
|
|
236
|
+
blocks.push(ui.p(formatText(comment.text ?? comment.comment)));
|
|
237
|
+
}
|
|
238
|
+
renderRichText(blocks);
|
|
239
|
+
}
|
|
240
|
+
function renderSnapshotInfo(snapshot, snapshotId, runtime) {
|
|
241
|
+
const teamScores = snapshot.team_scores ?? [];
|
|
242
|
+
const blocks = [ui.h1("Snapshot Information")];
|
|
243
|
+
blocks.push(ui.p(`Displaying ${ui.bold(teamScores.length.toString())} team scores.`));
|
|
244
|
+
blocks.push(ui.p(ui.link(snapshotWebLink(snapshotId, "drivers", runtime))));
|
|
245
|
+
if (teamScores.length === 0) {
|
|
246
|
+
blocks.push(ui.p(ui.dim("(None)")));
|
|
247
|
+
renderRichText(blocks);
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
for (const teamScore of teamScores) {
|
|
251
|
+
blocks.push(ui.h2("Team Score"));
|
|
252
|
+
blocks.push(ui.dl([
|
|
253
|
+
optionalDetail("Team name", formatText(teamScore.snapshot_team?.name), teamScore.snapshot_team?.name),
|
|
254
|
+
optionalDetail("Team ID", formatText(teamScore.snapshot_team?.team_id), teamScore.snapshot_team?.team_id),
|
|
255
|
+
optionalDetail("item_name", formatText(teamScore.item_name), teamScore.item_name),
|
|
256
|
+
optionalDetail("Response count", formatNumber(teamScore.response_count), teamScore.response_count),
|
|
257
|
+
optionalDetail("Score", formatNumber(teamScore.score), teamScore.score),
|
|
258
|
+
].filter((detail) => detail !== null), { termWidth: 16 }));
|
|
259
|
+
blocks.push(ui.h3("Benchmarks"));
|
|
260
|
+
blocks.push(ui.p(formatBenchmarkTable(teamScore)));
|
|
261
|
+
}
|
|
262
|
+
renderRichText(blocks);
|
|
263
|
+
}
|
|
264
|
+
function renderSnapshots(snapshots, runtime) {
|
|
265
|
+
const blocks = [ui.h1("Snapshots")];
|
|
266
|
+
blocks.push(ui.p(`Displaying ${ui.bold(snapshots.length.toString())} snapshots.`));
|
|
267
|
+
if (snapshots.length === 0) {
|
|
268
|
+
blocks.push(ui.p(ui.dim("(None)")));
|
|
269
|
+
renderRichText(blocks);
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
for (const snapshot of snapshots) {
|
|
273
|
+
blocks.push(ui.h2(ui.code(snapshot.id)));
|
|
274
|
+
blocks.push(ui.dl([
|
|
275
|
+
optionalDetail("Account ID", formatText(snapshot.account_id), snapshot.account_id),
|
|
276
|
+
optionalDetail("Scheduled for", formatText(snapshot.scheduled_for), snapshot.scheduled_for),
|
|
277
|
+
optionalDetail("Completed", formatTimestamp(snapshot.completed_at), snapshot.completed_at),
|
|
278
|
+
optionalDetail("Completed count", formatNumber(snapshot.completed_count), snapshot.completed_count),
|
|
279
|
+
optionalDetail("Total count", formatNumber(snapshot.total_count), snapshot.total_count),
|
|
280
|
+
optionalDetail("Last result change", formatTimestamp(snapshot.last_result_change_at), snapshot.last_result_change_at),
|
|
281
|
+
optionalDetail("Deleted", formatTimestamp(snapshot.deleted_at), snapshot.deleted_at),
|
|
282
|
+
ui.dli("Web link", ui.link(snapshotWebLink(snapshot.id, "drivers", runtime))),
|
|
283
|
+
].filter((detail) => detail !== null), { termWidth: 18 }));
|
|
284
|
+
}
|
|
285
|
+
renderRichText(blocks);
|
|
286
|
+
}
|
|
287
|
+
function extractDriverComments(response) {
|
|
288
|
+
return (response.driver_comments ??
|
|
289
|
+
response.driverComments ??
|
|
290
|
+
response.drivercomments ??
|
|
291
|
+
response.comments ??
|
|
292
|
+
[]);
|
|
293
|
+
}
|
|
294
|
+
function extractCsatComments(response) {
|
|
295
|
+
return (response.csat_comments ??
|
|
296
|
+
response.csatComments ??
|
|
297
|
+
response.csatcomments ??
|
|
298
|
+
response.comments ??
|
|
299
|
+
[]);
|
|
300
|
+
}
|
|
301
|
+
function formatCommentHeading(comment) {
|
|
302
|
+
const heading = comment.item_name ??
|
|
303
|
+
comment.driver_name ??
|
|
304
|
+
comment.driver ??
|
|
305
|
+
comment.email ??
|
|
306
|
+
comment.item_id ??
|
|
307
|
+
comment.driver_id;
|
|
308
|
+
return typeof heading === "string" && heading.trim().length > 0
|
|
309
|
+
? heading
|
|
310
|
+
: "Comment";
|
|
311
|
+
}
|
|
312
|
+
function formatText(value) {
|
|
313
|
+
return typeof value === "string" && value.trim().length > 0
|
|
314
|
+
? value
|
|
315
|
+
: ui.dim("(None)");
|
|
316
|
+
}
|
|
317
|
+
function formatNumber(value) {
|
|
318
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
319
|
+
? value.toString()
|
|
320
|
+
: ui.dim("(None)");
|
|
321
|
+
}
|
|
322
|
+
function formatTimestamp(value) {
|
|
323
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
324
|
+
return "";
|
|
325
|
+
}
|
|
326
|
+
const trimmed = value.trim();
|
|
327
|
+
const seconds = Number(trimmed);
|
|
328
|
+
if (Number.isFinite(seconds) && seconds > 0) {
|
|
329
|
+
return ui.timestampSummary(new Date(seconds * 1000).toISOString());
|
|
330
|
+
}
|
|
331
|
+
return Number.isNaN(Date.parse(trimmed))
|
|
332
|
+
? trimmed
|
|
333
|
+
: ui.timestampSummary(trimmed);
|
|
334
|
+
}
|
|
335
|
+
function optionalDetail(term, value, rawValue) {
|
|
336
|
+
if (rawValue === undefined || rawValue === null) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
if (typeof rawValue === "string" && rawValue.trim().length === 0) {
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
return ui.dli(term, value);
|
|
343
|
+
}
|
|
344
|
+
function formatBenchmarkTable(teamScore) {
|
|
345
|
+
const rows = [
|
|
346
|
+
["vs_prev", formatBenchmarkNumber(teamScore.vs_prev)],
|
|
347
|
+
["vs_org", formatBenchmarkNumber(teamScore.vs_org)],
|
|
348
|
+
["vs_50th", formatBenchmarkNumber(teamScore.vs_50th)],
|
|
349
|
+
["vs_75th", formatBenchmarkNumber(teamScore.vs_75th)],
|
|
350
|
+
["vs_90th", formatBenchmarkNumber(teamScore.vs_90th)],
|
|
351
|
+
];
|
|
352
|
+
const columns = ["Benchmark", "Value"];
|
|
353
|
+
const widths = columns.map((column, index) => Math.max(column.length, ...rows.map((row) => row[index].length)));
|
|
354
|
+
const border = `+${widths.map((width) => "-".repeat(width + 2)).join("+")}+`;
|
|
355
|
+
const header = `| ${columns.map((column, index) => column.padEnd(widths[index])).join(" | ")} |`;
|
|
356
|
+
const body = rows.map((row) => `| ${row.map((cell, index) => cell.padEnd(widths[index])).join(" | ")} |`);
|
|
357
|
+
return [border, header, border, ...body, border].join("\n");
|
|
358
|
+
}
|
|
359
|
+
function formatBenchmarkNumber(value) {
|
|
360
|
+
return typeof value === "number" && Number.isFinite(value)
|
|
361
|
+
? value.toString()
|
|
362
|
+
: "(None)";
|
|
363
|
+
}
|
|
364
|
+
function snapshotWebLink(snapshotId, section, runtime) {
|
|
365
|
+
return ui.webLink(`/snapshots/${snapshotId}/${section}`, runtime);
|
|
366
|
+
}
|
|
367
|
+
function parseRequiredTextOption(value, flag) {
|
|
368
|
+
if (typeof value !== "string" || value.trim().length === 0) {
|
|
369
|
+
throw new CliError(`${flag} is required`, EXIT_CODES.ARGUMENT_ERROR);
|
|
370
|
+
}
|
|
371
|
+
return value.trim();
|
|
372
|
+
}
|
|
373
|
+
function parseLimitOption(value, flag) {
|
|
374
|
+
const limit = parsePositiveIntOption(value, flag);
|
|
375
|
+
if (limit > 100) {
|
|
376
|
+
throw new CliError(`${flag} must be at most 100`, EXIT_CODES.ARGUMENT_ERROR);
|
|
377
|
+
}
|
|
378
|
+
return limit;
|
|
379
|
+
}
|
|
@@ -144,13 +144,13 @@ async function getStudioQueryResults(runtime, id) {
|
|
|
144
144
|
return response.body;
|
|
145
145
|
}
|
|
146
146
|
async function downloadStudioQueryResultsCsv(runtime, id, filename) {
|
|
147
|
-
const response = await fetchWithApiHeaders(runtime, `${runtime.
|
|
147
|
+
const response = await fetchWithApiHeaders(runtime, `${runtime.apiBaseUrl}/studio.queryRuns.results?id=${encodeURIComponent(id)}&format=csv`, { redirect: "manual" });
|
|
148
148
|
if (response.status >= 300 && response.status < 400) {
|
|
149
149
|
const location = response.headers.get("location");
|
|
150
150
|
if (!location) {
|
|
151
151
|
throw new HttpError("CSV download redirect did not include a Location header.", response.status);
|
|
152
152
|
}
|
|
153
|
-
const downloadResponse = await fetchResponse(new URL(location, runtime.
|
|
153
|
+
const downloadResponse = await fetchResponse(new URL(location, runtime.apiBaseUrl).toString(), {
|
|
154
154
|
method: "GET",
|
|
155
155
|
headers: buildDownloadHeaders(runtime, "text/csv"),
|
|
156
156
|
});
|
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
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
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
|
-
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
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
|
|
107
|
+
return path.join(os.homedir(), ".config", "dx");
|
|
32
108
|
}
|
|
109
|
+
// --- Resolving URLs at command runtime ------------------------------------------------------
|
|
33
110
|
/**
|
|
34
|
-
*
|
|
35
|
-
*
|
|
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
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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
|
|
128
|
+
return normalizeUrl(readConfig().webBaseUrl || DEFAULT_WEB_BASE_URL);
|
|
49
129
|
}
|
|
50
|
-
|
|
51
|
-
|
|
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
|
|
54
|
-
|
|
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.
|
|
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,
|
package/dist/loginViaBrowser.js
CHANGED
|
@@ -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(
|
|
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 = `${
|
|
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(
|
|
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(
|
|
68
|
-
return fetch(`${
|
|
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 }),
|