@get-dx/cli 0.7.0 → 0.9.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/cli.js +4 -0
- package/dist/commands/attributeGroups.js +100 -0
- package/dist/commands/auth.js +2 -1
- package/dist/commands/catalog/entityTypes.js +6 -6
- package/dist/commands/doctor.js +85 -0
- package/dist/commands/scorecards.js +6 -6
- package/dist/commands/snapshots.js +116 -0
- package/dist/commands/studio/reports.js +6 -6
- package/dist/commands/teams.js +1 -1
- package/dist/commands/workflow-blank-template.yaml +78 -0
- package/dist/commands/workflows.js +112 -2
- package/dist/commands/workflowsRendering.js +21 -9
- package/dist/config.js +70 -9
- package/dist/doctor/auth.js +133 -0
- package/dist/doctor/cli.js +154 -0
- package/dist/doctor/config.js +119 -0
- package/dist/doctor/environment.js +60 -0
- package/dist/doctor/format.js +68 -0
- package/dist/doctor/index.js +81 -0
- package/dist/doctor/network.js +224 -0
- package/dist/doctor/probes.js +130 -0
- package/dist/doctor/skill.js +16 -0
- package/dist/doctor/types.js +11 -0
- package/dist/http.js +2 -0
- package/dist/secrets.js +87 -9
- package/dist/ui/blocks.js +46 -0
- package/dist/ui.js +8 -1
- package/dist/versionCheck.js +25 -3
- package/package.json +1 -1
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isSkillInstalled } from "../skill.js";
|
|
4
|
+
import { tildify } from "./format.js";
|
|
5
|
+
import { check } from "./types.js";
|
|
6
|
+
export async function checkSkill() {
|
|
7
|
+
const installed = await isSkillInstalled();
|
|
8
|
+
const skillPath = tildify(path.join(os.homedir(), ".agents", "skills", "dx-cli"));
|
|
9
|
+
return {
|
|
10
|
+
id: "skill",
|
|
11
|
+
title: "Agent skill",
|
|
12
|
+
checks: [
|
|
13
|
+
check("skill", "dx-cli skill", "ok", installed ? `installed (${skillPath})` : "not installed", { data: { installed, path: installed ? skillPath : null } }),
|
|
14
|
+
],
|
|
15
|
+
};
|
|
16
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export function check(id, label, status, message, options = {}) {
|
|
2
|
+
return {
|
|
3
|
+
id,
|
|
4
|
+
label,
|
|
5
|
+
status,
|
|
6
|
+
message,
|
|
7
|
+
hint: status === "warn" || status === "fail" ? (options.hint ?? null) : null,
|
|
8
|
+
source: options.source ?? null,
|
|
9
|
+
data: options.data ?? null,
|
|
10
|
+
};
|
|
11
|
+
}
|
package/dist/http.js
CHANGED
|
@@ -2,6 +2,7 @@ import { CliError, HttpError } from "./errors.js";
|
|
|
2
2
|
export async function request(runtime, route, options = {}) {
|
|
3
3
|
const method = options.method || "GET";
|
|
4
4
|
const headers = new Headers({
|
|
5
|
+
...options.headers,
|
|
5
6
|
Accept: "application/json",
|
|
6
7
|
"X-Client-Type": "dx-cli",
|
|
7
8
|
"X-Client-Version": runtime.version,
|
|
@@ -40,6 +41,7 @@ export async function request(runtime, route, options = {}) {
|
|
|
40
41
|
method,
|
|
41
42
|
headers,
|
|
42
43
|
body: requestBody,
|
|
44
|
+
signal: options.signal,
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
catch (error) {
|
package/dist/secrets.js
CHANGED
|
@@ -8,6 +8,35 @@ export async function getToken(baseUrl) {
|
|
|
8
8
|
}
|
|
9
9
|
return await getSecretStore().get(baseUrl);
|
|
10
10
|
}
|
|
11
|
+
/**
|
|
12
|
+
* Like `getToken`, but reports where the token came from, and surfaces errors
|
|
13
|
+
* reading the OS secret store instead of treating them as a missing token.
|
|
14
|
+
*/
|
|
15
|
+
export async function getTokenWithSource(baseUrl) {
|
|
16
|
+
if (process.env.DX_API_TOKEN) {
|
|
17
|
+
return { source: "env", token: process.env.DX_API_TOKEN };
|
|
18
|
+
}
|
|
19
|
+
let store;
|
|
20
|
+
try {
|
|
21
|
+
store = getSecretStore();
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
return {
|
|
25
|
+
source: "error",
|
|
26
|
+
storeName: null,
|
|
27
|
+
error: error instanceof Error ? error.message : String(error),
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const result = await store.probe(baseUrl);
|
|
31
|
+
switch (result.status) {
|
|
32
|
+
case "found":
|
|
33
|
+
return { source: "keychain", token: result.token, storeName: store.name };
|
|
34
|
+
case "missing":
|
|
35
|
+
return { source: "none", storeName: store.name };
|
|
36
|
+
case "error":
|
|
37
|
+
return { source: "error", storeName: store.name, error: result.error };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
11
40
|
export async function setToken(baseUrl, token) {
|
|
12
41
|
await getSecretStore().set(baseUrl, token);
|
|
13
42
|
}
|
|
@@ -26,9 +55,16 @@ function getSecretStore(platform = process.platform) {
|
|
|
26
55
|
}
|
|
27
56
|
throw new CliError(`Unsupported platform for secure token storage: ${platform}`);
|
|
28
57
|
}
|
|
58
|
+
// `security` exits with this code when no matching keychain item exists.
|
|
59
|
+
const MACOS_ITEM_NOT_FOUND_EXIT_CODE = 44;
|
|
29
60
|
function macosSecretStore() {
|
|
30
61
|
return {
|
|
62
|
+
name: "macOS Keychain",
|
|
31
63
|
async get(baseUrl) {
|
|
64
|
+
const result = await this.probe(baseUrl);
|
|
65
|
+
return result.status === "found" ? result.token : null;
|
|
66
|
+
},
|
|
67
|
+
async probe(baseUrl) {
|
|
32
68
|
try {
|
|
33
69
|
const result = await execa("security", [
|
|
34
70
|
"find-generic-password",
|
|
@@ -38,10 +74,15 @@ function macosSecretStore() {
|
|
|
38
74
|
baseUrl,
|
|
39
75
|
"-w",
|
|
40
76
|
]);
|
|
41
|
-
|
|
77
|
+
const token = result.stdout?.trim();
|
|
78
|
+
return token ? { status: "found", token } : { status: "missing" };
|
|
42
79
|
}
|
|
43
|
-
catch {
|
|
44
|
-
|
|
80
|
+
catch (error) {
|
|
81
|
+
if (isRecord(error) &&
|
|
82
|
+
error.exitCode === MACOS_ITEM_NOT_FOUND_EXIT_CODE) {
|
|
83
|
+
return { status: "missing" };
|
|
84
|
+
}
|
|
85
|
+
return { status: "error", error: describeStoreError(error) };
|
|
45
86
|
}
|
|
46
87
|
},
|
|
47
88
|
async set(baseUrl, token) {
|
|
@@ -86,7 +127,12 @@ function macosSecretStore() {
|
|
|
86
127
|
}
|
|
87
128
|
function linuxSecretStore() {
|
|
88
129
|
return {
|
|
130
|
+
name: "Linux Secret Service (secret-tool)",
|
|
89
131
|
async get(baseUrl) {
|
|
132
|
+
const result = await this.probe(baseUrl);
|
|
133
|
+
return result.status === "found" ? result.token : null;
|
|
134
|
+
},
|
|
135
|
+
async probe(baseUrl) {
|
|
90
136
|
try {
|
|
91
137
|
const result = await execa("secret-tool", [
|
|
92
138
|
"lookup",
|
|
@@ -95,10 +141,21 @@ function linuxSecretStore() {
|
|
|
95
141
|
"account",
|
|
96
142
|
baseUrl,
|
|
97
143
|
]);
|
|
98
|
-
|
|
144
|
+
const token = result.stdout?.trim();
|
|
145
|
+
return token ? { status: "found", token } : { status: "missing" };
|
|
99
146
|
}
|
|
100
|
-
catch {
|
|
101
|
-
|
|
147
|
+
catch (error) {
|
|
148
|
+
if (hasErrorCode(error, "ENOENT")) {
|
|
149
|
+
return { status: "error", error: "`secret-tool` is not installed" };
|
|
150
|
+
}
|
|
151
|
+
// `secret-tool lookup` exits 1 without output when nothing matches.
|
|
152
|
+
if (isRecord(error) &&
|
|
153
|
+
error.exitCode === 1 &&
|
|
154
|
+
!errorStderr(error) &&
|
|
155
|
+
!isLinuxSecretServiceUnavailable(error)) {
|
|
156
|
+
return { status: "missing" };
|
|
157
|
+
}
|
|
158
|
+
return { status: "error", error: describeStoreError(error) };
|
|
102
159
|
}
|
|
103
160
|
},
|
|
104
161
|
async set(baseUrl, token) {
|
|
@@ -116,12 +173,18 @@ function linuxSecretStore() {
|
|
|
116
173
|
}
|
|
117
174
|
function windowsSecretStore() {
|
|
118
175
|
return {
|
|
176
|
+
name: "Windows Credential Manager",
|
|
119
177
|
async get(baseUrl) {
|
|
178
|
+
const result = await this.probe(baseUrl);
|
|
179
|
+
return result.status === "found" ? result.token : null;
|
|
180
|
+
},
|
|
181
|
+
async probe(baseUrl) {
|
|
120
182
|
try {
|
|
121
|
-
|
|
183
|
+
const token = await crossKeychain.getPassword(SERVICE, encodeAccountName(baseUrl));
|
|
184
|
+
return token ? { status: "found", token } : { status: "missing" };
|
|
122
185
|
}
|
|
123
|
-
catch {
|
|
124
|
-
return
|
|
186
|
+
catch (error) {
|
|
187
|
+
return { status: "error", error: describeStoreError(error) };
|
|
125
188
|
}
|
|
126
189
|
},
|
|
127
190
|
async set(baseUrl, token) {
|
|
@@ -191,6 +254,21 @@ function isLinuxSecretServiceUnavailable(error) {
|
|
|
191
254
|
"No such secret service",
|
|
192
255
|
].some((message) => text.includes(message));
|
|
193
256
|
}
|
|
257
|
+
function errorStderr(error) {
|
|
258
|
+
return typeof error.stderr === "string" ? error.stderr.trim() : "";
|
|
259
|
+
}
|
|
260
|
+
function describeStoreError(error) {
|
|
261
|
+
if (isRecord(error)) {
|
|
262
|
+
const stderr = errorStderr(error);
|
|
263
|
+
if (stderr) {
|
|
264
|
+
return stderr;
|
|
265
|
+
}
|
|
266
|
+
if (typeof error.shortMessage === "string") {
|
|
267
|
+
return error.shortMessage;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return error instanceof Error ? error.message : String(error);
|
|
271
|
+
}
|
|
194
272
|
function formatErrorDetails(error) {
|
|
195
273
|
if (!isRecord(error) || typeof error.stderr !== "string") {
|
|
196
274
|
return null;
|
package/dist/ui/blocks.js
CHANGED
|
@@ -104,3 +104,49 @@ export class JsonBlock extends AbstractBlock {
|
|
|
104
104
|
return pc.cyan(JSON.stringify(this.value, null, 2)) + "\n";
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
function statusMarker(status) {
|
|
108
|
+
switch (status) {
|
|
109
|
+
case "ok":
|
|
110
|
+
return pc.green("[ OK ]");
|
|
111
|
+
case "warn":
|
|
112
|
+
return pc.yellow("[WARN]");
|
|
113
|
+
case "fail":
|
|
114
|
+
return pc.red("[FAIL]");
|
|
115
|
+
case "skip":
|
|
116
|
+
return pc.dim("[SKIP]");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* A titled list of `[ OK ]` / `[WARN]` / `[FAIL]` / `[SKIP]` rows, e.g.:
|
|
121
|
+
*
|
|
122
|
+
* ```
|
|
123
|
+
* Network
|
|
124
|
+
* [ OK ] DNS resolved to 104.18.20.33
|
|
125
|
+
* [FAIL] TLS handshake connection reset by peer
|
|
126
|
+
* → A firewall or proxy is likely interrupting HTTPS traffic.
|
|
127
|
+
* ```
|
|
128
|
+
*/
|
|
129
|
+
export class StatusListBlock extends AbstractBlock {
|
|
130
|
+
title;
|
|
131
|
+
rows;
|
|
132
|
+
options;
|
|
133
|
+
constructor(title, rows, options) {
|
|
134
|
+
super();
|
|
135
|
+
this.title = title;
|
|
136
|
+
this.rows = rows;
|
|
137
|
+
this.options = options;
|
|
138
|
+
}
|
|
139
|
+
render() {
|
|
140
|
+
const lines = [pc.bold(this.title)];
|
|
141
|
+
for (const row of this.rows) {
|
|
142
|
+
const label = padEnd(row.label, this.options.labelWidth);
|
|
143
|
+
lines.push(` ${statusMarker(row.status)} ${label}${row.detail}`);
|
|
144
|
+
if (row.hint) {
|
|
145
|
+
const [first, ...rest] = row.hint.split("\n");
|
|
146
|
+
lines.push(` → ${first}`);
|
|
147
|
+
lines.push(...rest.map((line) => ` ${line}`));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return lines.join("\n") + "\n";
|
|
151
|
+
}
|
|
152
|
+
}
|
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, CodeBlock, } from "./ui/blocks.js";
|
|
5
|
+
import { DescriptionListBlock, DescriptionListItemContainer, HeadingBlock, JsonBlock, ListItemContainer, ParagraphBlock, UnorderedListBlock, CodeBlock, StatusListBlock, } from "./ui/blocks.js";
|
|
6
6
|
dayjs.extend(relativeTimePlugin);
|
|
7
7
|
// Block-level elements
|
|
8
8
|
export function h1(text) {
|
|
@@ -49,6 +49,13 @@ export function dli(term, detailItems) {
|
|
|
49
49
|
return new DescriptionListItemContainer(term, detailItems);
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Create a titled list of `[ OK ]` / `[WARN]` / `[FAIL]` / `[SKIP]` rows, as
|
|
54
|
+
* used by `dx doctor`.
|
|
55
|
+
*/
|
|
56
|
+
export function statusList(title, rows, options) {
|
|
57
|
+
return new StatusListBlock(title, rows, options);
|
|
58
|
+
}
|
|
52
59
|
export function codeBlock(text) {
|
|
53
60
|
return new CodeBlock(text);
|
|
54
61
|
}
|
package/dist/versionCheck.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as ui from "./ui.js";
|
|
|
10
10
|
import cliPackage from "../package.json" with { type: "json" };
|
|
11
11
|
const VERSION_CHECK_INTERVAL_HOURS = 24;
|
|
12
12
|
const SNOOZE_DAYS = 7;
|
|
13
|
-
const SKIP_COMMANDS = new Set(["auth", "init"]);
|
|
13
|
+
const SKIP_COMMANDS = new Set(["auth", "doctor", "init"]);
|
|
14
14
|
const SKIP_OPTIONS = new Set(["-h", "--help", "-v", "--version"]);
|
|
15
15
|
/**
|
|
16
16
|
* Compares two semver strings (e.g. "1.2.3").
|
|
@@ -52,10 +52,32 @@ export function shouldShowVersionPrompt(latestVersion, versionPromptSelection) {
|
|
|
52
52
|
}
|
|
53
53
|
return true;
|
|
54
54
|
}
|
|
55
|
-
async function fetchLatestVersion(runtime) {
|
|
56
|
-
const response = await request(runtime, "/cli.currentVersion", { method: "GET" });
|
|
55
|
+
export async function fetchLatestVersion(runtime, options) {
|
|
56
|
+
const response = await request(runtime, "/cli.currentVersion", { ...options, method: "GET" });
|
|
57
57
|
return response.body.versions.cli;
|
|
58
58
|
}
|
|
59
|
+
const NPM_LATEST_URL = `https://registry.npmjs.org/${cliPackage.name}/latest`;
|
|
60
|
+
/**
|
|
61
|
+
* Fetch the latest published version from the npm registry. Unlike
|
|
62
|
+
* `fetchLatestVersion`, this does not need a DX API token.
|
|
63
|
+
*/
|
|
64
|
+
export async function fetchLatestVersionFromNpm(options) {
|
|
65
|
+
const response = await fetch(NPM_LATEST_URL, {
|
|
66
|
+
headers: {
|
|
67
|
+
Accept: "application/json",
|
|
68
|
+
"User-Agent": `dx-cli/${cliPackage.version}`,
|
|
69
|
+
},
|
|
70
|
+
signal: options?.signal,
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
throw new Error(`npm registry returned HTTP ${response.status}`);
|
|
74
|
+
}
|
|
75
|
+
const body = (await response.json());
|
|
76
|
+
if (typeof body.version !== "string") {
|
|
77
|
+
throw new Error("npm registry response did not include a version");
|
|
78
|
+
}
|
|
79
|
+
return body.version;
|
|
80
|
+
}
|
|
59
81
|
/**
|
|
60
82
|
* Checks whether a newer CLI version is available.
|
|
61
83
|
*
|