@get-dx/cli 0.3.0 → 0.3.2
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 +11 -4
- package/dist/authCodeCallbackServer.js +50 -16
- package/dist/cli.js +2 -0
- package/dist/commands/snapshots.js +379 -0
- package/dist/secrets.js +91 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -18,6 +18,13 @@ Interactively login and install the AI agent skill:
|
|
|
18
18
|
dx init
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
For CI, Docker/dev containers, or other headless environments, pass the token
|
|
22
|
+
through the environment instead of storing it in the OS credential store:
|
|
23
|
+
|
|
24
|
+
```shell
|
|
25
|
+
DX_API_TOKEN="$DX_TOKEN" dx auth status
|
|
26
|
+
```
|
|
27
|
+
|
|
21
28
|
Show help text:
|
|
22
29
|
|
|
23
30
|
```shell
|
|
@@ -42,8 +49,8 @@ Set the env vars once when initializing:
|
|
|
42
49
|
# Interactive login
|
|
43
50
|
DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" dx init
|
|
44
51
|
|
|
45
|
-
# Non-interactive
|
|
46
|
-
DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" dx auth
|
|
52
|
+
# Non-interactive use for CI, containers, or remote agents
|
|
53
|
+
DX_WEB_BASE_URL="https://mycompany.getdx.io" DX_API_BASE_URL="https://api.mycompany.getdx.io" DX_API_TOKEN="$DX_TOKEN" dx auth status
|
|
47
54
|
```
|
|
48
55
|
|
|
49
56
|
#### For managed deployments
|
|
@@ -54,8 +61,8 @@ Set the env vars once when initializing:
|
|
|
54
61
|
# Interactive login
|
|
55
62
|
DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" dx init
|
|
56
63
|
|
|
57
|
-
# Non-interactive
|
|
58
|
-
DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" dx auth
|
|
64
|
+
# Non-interactive use for CI, containers, or remote agents
|
|
65
|
+
DX_WEB_BASE_URL="https://dx.some-example-subdomain.example.com" DX_API_BASE_URL="https://api.dx.some-example-subdomain.example.com" DX_API_TOKEN="$DX_TOKEN" dx auth status
|
|
59
66
|
```
|
|
60
67
|
|
|
61
68
|
## Logging
|
|
@@ -3,6 +3,7 @@ import { CliError } from "./errors.js";
|
|
|
3
3
|
export class AuthCodeCallbackServer {
|
|
4
4
|
server = null;
|
|
5
5
|
address = null;
|
|
6
|
+
requestReceived = false;
|
|
6
7
|
async start() {
|
|
7
8
|
await new Promise((resolve, reject) => {
|
|
8
9
|
const srv = createServer();
|
|
@@ -23,19 +24,34 @@ export class AuthCodeCallbackServer {
|
|
|
23
24
|
}
|
|
24
25
|
async listenForCode(onCodeReceipt) {
|
|
25
26
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
+
const srv = this.server;
|
|
28
|
+
if (!srv) {
|
|
27
29
|
reject(new Error("Unreachable: server not started"));
|
|
28
30
|
return;
|
|
29
31
|
}
|
|
30
|
-
|
|
31
|
-
this.
|
|
32
|
+
srv.on("request", async (req, res) => {
|
|
33
|
+
if (this.requestReceived) {
|
|
34
|
+
res.writeHead(409, { Connection: "close" }).end();
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
this.requestReceived = true;
|
|
32
38
|
const url = new URL(req.url ?? "/", this.address);
|
|
33
39
|
const code = url.searchParams.get("code");
|
|
34
40
|
const state = url.searchParams.get("state");
|
|
41
|
+
const stopServerAndResolve = (result) => {
|
|
42
|
+
res.once("close", () => {
|
|
43
|
+
this.stop();
|
|
44
|
+
resolve(result);
|
|
45
|
+
});
|
|
46
|
+
};
|
|
35
47
|
if (!code) {
|
|
36
48
|
const message = "Authentication failed: no code received";
|
|
37
|
-
res.writeHead(500, {
|
|
38
|
-
|
|
49
|
+
res.writeHead(500, {
|
|
50
|
+
Connection: "close",
|
|
51
|
+
"Content-Type": "text/plain",
|
|
52
|
+
});
|
|
53
|
+
stopServerAndResolve({ type: "ERROR", error: new CliError(message) });
|
|
54
|
+
res.end(message);
|
|
39
55
|
return;
|
|
40
56
|
}
|
|
41
57
|
let result;
|
|
@@ -46,34 +62,52 @@ export class AuthCodeCallbackServer {
|
|
|
46
62
|
const message = err instanceof Error
|
|
47
63
|
? err.message
|
|
48
64
|
: "Authentication failed: unknown error";
|
|
49
|
-
res.writeHead(500, {
|
|
50
|
-
|
|
65
|
+
res.writeHead(500, {
|
|
66
|
+
Connection: "close",
|
|
67
|
+
"Content-Type": "text/plain",
|
|
68
|
+
});
|
|
69
|
+
stopServerAndResolve({
|
|
51
70
|
type: "ERROR",
|
|
52
71
|
error: err instanceof Error ? err : new CliError(message),
|
|
53
72
|
});
|
|
73
|
+
res.end(message);
|
|
54
74
|
return;
|
|
55
75
|
}
|
|
56
76
|
if (result.type === "SUCCESS") {
|
|
57
77
|
if (result.redirectUri) {
|
|
58
|
-
res.writeHead(302, {
|
|
78
|
+
res.writeHead(302, {
|
|
79
|
+
Connection: "close",
|
|
80
|
+
Location: result.redirectUri,
|
|
81
|
+
});
|
|
82
|
+
stopServerAndResolve(result);
|
|
83
|
+
res.end();
|
|
59
84
|
}
|
|
60
85
|
else {
|
|
61
|
-
res
|
|
62
|
-
|
|
63
|
-
|
|
86
|
+
res.writeHead(200, {
|
|
87
|
+
Connection: "close",
|
|
88
|
+
"Content-Type": "text/plain",
|
|
89
|
+
});
|
|
90
|
+
stopServerAndResolve(result);
|
|
91
|
+
res.end("Authentication successful");
|
|
64
92
|
}
|
|
65
93
|
}
|
|
66
94
|
else {
|
|
67
|
-
res
|
|
68
|
-
|
|
69
|
-
|
|
95
|
+
res.writeHead(500, {
|
|
96
|
+
Connection: "close",
|
|
97
|
+
"Content-Type": "text/plain",
|
|
98
|
+
});
|
|
99
|
+
stopServerAndResolve(result);
|
|
100
|
+
res.end(result.error.message);
|
|
70
101
|
}
|
|
71
|
-
resolve(result);
|
|
72
102
|
});
|
|
73
103
|
});
|
|
74
104
|
}
|
|
75
105
|
stop() {
|
|
76
|
-
this.server
|
|
106
|
+
const srv = this.server;
|
|
107
|
+
if (!srv)
|
|
108
|
+
return;
|
|
109
|
+
srv.closeAllConnections();
|
|
110
|
+
srv.close();
|
|
77
111
|
this.server = null;
|
|
78
112
|
}
|
|
79
113
|
}
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import { authCommand } from "./commands/auth.js";
|
|
|
3
3
|
import { catalogCommand } from "./commands/catalog.js";
|
|
4
4
|
import { initCommand } from "./commands/init.js";
|
|
5
5
|
import { scorecardsCommand } from "./commands/scorecards.js";
|
|
6
|
+
import { snapshotsCommand } from "./commands/snapshots.js";
|
|
6
7
|
import { studioCommand } from "./commands/studio.js";
|
|
7
8
|
import { teamsCommand } from "./commands/teams.js";
|
|
8
9
|
import { workflowsCommand } from "./commands/workflows.js";
|
|
@@ -36,6 +37,7 @@ function createProgram() {
|
|
|
36
37
|
program.addCommand(catalogCommand());
|
|
37
38
|
program.addCommand(initCommand());
|
|
38
39
|
program.addCommand(scorecardsCommand());
|
|
40
|
+
program.addCommand(snapshotsCommand());
|
|
39
41
|
program.addCommand(studioCommand());
|
|
40
42
|
program.addCommand(teamsCommand());
|
|
41
43
|
program.addCommand(workflowsCommand());
|
|
@@ -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
|
+
}
|
package/dist/secrets.js
CHANGED
|
@@ -40,16 +40,28 @@ function macosSecretStore() {
|
|
|
40
40
|
}
|
|
41
41
|
},
|
|
42
42
|
set(baseUrl, token) {
|
|
43
|
-
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
43
|
+
try {
|
|
44
|
+
execaSync("security", [
|
|
45
|
+
"add-generic-password",
|
|
46
|
+
"-U",
|
|
47
|
+
"-s",
|
|
48
|
+
SERVICE,
|
|
49
|
+
"-a",
|
|
50
|
+
baseUrl,
|
|
51
|
+
"-w",
|
|
52
|
+
token,
|
|
53
|
+
]);
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
throw new CliError([
|
|
57
|
+
"Unable to save the DX API token securely using macOS Keychain.",
|
|
58
|
+
"",
|
|
59
|
+
"Set DX_API_TOKEN in the environment, or try again from a macOS session with Keychain access.",
|
|
60
|
+
formatErrorDetails(error),
|
|
61
|
+
]
|
|
62
|
+
.filter(Boolean)
|
|
63
|
+
.join("\n"));
|
|
64
|
+
}
|
|
53
65
|
},
|
|
54
66
|
delete(baseUrl) {
|
|
55
67
|
try {
|
|
@@ -84,10 +96,78 @@ function linuxSecretStore() {
|
|
|
84
96
|
}
|
|
85
97
|
},
|
|
86
98
|
set(baseUrl, token) {
|
|
87
|
-
|
|
99
|
+
try {
|
|
100
|
+
execaSync("secret-tool", ["store", "--label=dx-cli", "service", SERVICE, "account", baseUrl], { input: token });
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
throw buildLinuxTokenStorageError(error);
|
|
104
|
+
}
|
|
88
105
|
},
|
|
89
106
|
delete(_baseUrl) {
|
|
90
107
|
throw new CliError("Linux token deletion is not supported by secret-tool in this implementation");
|
|
91
108
|
},
|
|
92
109
|
};
|
|
93
110
|
}
|
|
111
|
+
function buildLinuxTokenStorageError(error) {
|
|
112
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
113
|
+
return new CliError([
|
|
114
|
+
"Unable to save the DX API token securely because `secret-tool` is not installed.",
|
|
115
|
+
"",
|
|
116
|
+
"Install `libsecret-tools`, or set DX_API_TOKEN in the environment instead of using `dx auth login` or `dx init` to store credentials.",
|
|
117
|
+
].join("\n"));
|
|
118
|
+
}
|
|
119
|
+
if (isLinuxSecretServiceUnavailable(error)) {
|
|
120
|
+
return new CliError([
|
|
121
|
+
"Unable to save the DX API token securely because the Linux Secret Service/keyring is not available.",
|
|
122
|
+
"",
|
|
123
|
+
"This often happens in Docker or dev containers, even when `secret-tool` is installed. Set DX_API_TOKEN in the container environment instead of using `dx auth login` or `dx init` to store credentials, or run the CLI in an environment with a working Secret Service provider such as GNOME Keyring.",
|
|
124
|
+
formatErrorDetails(error),
|
|
125
|
+
]
|
|
126
|
+
.filter(Boolean)
|
|
127
|
+
.join("\n"));
|
|
128
|
+
}
|
|
129
|
+
return new CliError([
|
|
130
|
+
"Unable to save the DX API token securely using `secret-tool`.",
|
|
131
|
+
"",
|
|
132
|
+
"Set DX_API_TOKEN in the environment, or configure a working Linux Secret Service provider such as GNOME Keyring.",
|
|
133
|
+
formatErrorDetails(error),
|
|
134
|
+
]
|
|
135
|
+
.filter(Boolean)
|
|
136
|
+
.join("\n"));
|
|
137
|
+
}
|
|
138
|
+
function hasErrorCode(error, code) {
|
|
139
|
+
if (!isRecord(error)) {
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
return error.code === code;
|
|
143
|
+
}
|
|
144
|
+
function isLinuxSecretServiceUnavailable(error) {
|
|
145
|
+
const text = errorText(error);
|
|
146
|
+
return [
|
|
147
|
+
"org.freedesktop.secrets",
|
|
148
|
+
"Cannot autolaunch D-Bus",
|
|
149
|
+
"No such secret service",
|
|
150
|
+
].some((message) => text.includes(message));
|
|
151
|
+
}
|
|
152
|
+
function formatErrorDetails(error) {
|
|
153
|
+
if (!isRecord(error) || typeof error.stderr !== "string") {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
const stderr = error.stderr.trim();
|
|
157
|
+
return stderr ? `Details: ${stderr}` : null;
|
|
158
|
+
}
|
|
159
|
+
function errorText(error) {
|
|
160
|
+
if (!isRecord(error)) {
|
|
161
|
+
return String(error);
|
|
162
|
+
}
|
|
163
|
+
return [
|
|
164
|
+
typeof error.stderr === "string" ? error.stderr : null,
|
|
165
|
+
typeof error.shortMessage === "string" ? error.shortMessage : null,
|
|
166
|
+
typeof error.message === "string" ? error.message : null,
|
|
167
|
+
]
|
|
168
|
+
.filter(Boolean)
|
|
169
|
+
.join("\n");
|
|
170
|
+
}
|
|
171
|
+
function isRecord(value) {
|
|
172
|
+
return typeof value === "object" && value !== null;
|
|
173
|
+
}
|