@get-dx/cli 0.8.0 → 0.10.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/commandHelpers.js +10 -0
- package/dist/commands/attributeGroups.js +100 -0
- package/dist/commands/auth.js +2 -1
- package/dist/commands/catalog/entities.js +1 -11
- package/dist/commands/catalog/relation-edges-blank-template.yaml +19 -0
- package/dist/commands/catalog/relationEdges.js +285 -0
- package/dist/commands/catalog/relationEdgesRendering.js +45 -0
- package/dist/commands/catalog/relations.js +231 -0
- package/dist/commands/catalog/relationsRendering.js +37 -0
- package/dist/commands/catalog.js +4 -0
- package/dist/commands/doctor.js +85 -0
- package/dist/commands/snapshots.js +318 -8
- package/dist/commands/studio/reports.js +8 -0
- package/dist/commands/teams.js +1 -1
- package/dist/commands/workflow-blank-template.yaml +6 -0
- package/dist/commands/workflowsRendering.js +3 -0
- 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,154 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import dayjs from "dayjs";
|
|
4
|
+
import { compareVersions, fetchLatestVersion, fetchLatestVersionFromNpm, } from "../versionCheck.js";
|
|
5
|
+
import { describeError, tildify } from "./format.js";
|
|
6
|
+
import { CHECK_TIMEOUT_MS, DOCTOR_HEADER } from "./probes.js";
|
|
7
|
+
import { check } from "./types.js";
|
|
8
|
+
import cliPackage from "../../package.json" with { type: "json" };
|
|
9
|
+
export const defaultLatestVersionLookup = {
|
|
10
|
+
fromApi: (runtime) => fetchLatestVersion(runtime, {
|
|
11
|
+
headers: DOCTOR_HEADER,
|
|
12
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
|
|
13
|
+
}),
|
|
14
|
+
fromNpm: () => fetchLatestVersionFromNpm({
|
|
15
|
+
signal: AbortSignal.timeout(CHECK_TIMEOUT_MS),
|
|
16
|
+
}),
|
|
17
|
+
};
|
|
18
|
+
export async function checkCli(options) {
|
|
19
|
+
const { runtime, apiReachable, install, latestVersion, now } = options;
|
|
20
|
+
const checks = [
|
|
21
|
+
await versionCheck(apiReachable ? runtime : null, install.method, latestVersion),
|
|
22
|
+
nodeCheck(),
|
|
23
|
+
check("platform", "Platform", "ok", `${process.platform}/${process.arch}`, {
|
|
24
|
+
data: { platform: process.platform, arch: process.arch },
|
|
25
|
+
}),
|
|
26
|
+
check("install", "Install", "ok", `${installMethodName(install.method)} (${tildify(install.path)})`, { data: { method: install.method, path: tildify(install.path) } }),
|
|
27
|
+
check("time", "Time", "ok", dayjs(now).format(), {
|
|
28
|
+
data: { time: dayjs(now).format() },
|
|
29
|
+
}),
|
|
30
|
+
];
|
|
31
|
+
return { id: "cli", title: "CLI", checks };
|
|
32
|
+
}
|
|
33
|
+
async function versionCheck(runtime, method, lookup) {
|
|
34
|
+
const current = cliPackage.version;
|
|
35
|
+
const failures = [];
|
|
36
|
+
let latest = null;
|
|
37
|
+
let latestSource = null;
|
|
38
|
+
if (runtime) {
|
|
39
|
+
try {
|
|
40
|
+
latest = await lookup.fromApi(runtime);
|
|
41
|
+
latestSource = "api";
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
failures.push(`DX API: ${describeError(error, "request")}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
failures.push("DX API: skipped (no token or API unreachable)");
|
|
49
|
+
}
|
|
50
|
+
if (!latest) {
|
|
51
|
+
try {
|
|
52
|
+
latest = await lookup.fromNpm();
|
|
53
|
+
latestSource = "npm";
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
failures.push(`npm: ${describeError(error, "request")}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (!latest) {
|
|
60
|
+
return check("version", "Version", "fail", `${current} (could not determine the latest version)`, {
|
|
61
|
+
hint: `Tried the DX API and the npm registry:\n${failures.join("\n")}`,
|
|
62
|
+
data: { current, latest: null, latest_source: null },
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
const data = { current, latest, latest_source: latestSource };
|
|
66
|
+
const comparison = compareVersions(current, latest);
|
|
67
|
+
if (comparison < 0) {
|
|
68
|
+
return check("version", "Version", "warn", `${current} (latest is ${latest})`, {
|
|
69
|
+
hint: `Upgrade with: ${upgradeCommand(method)}`,
|
|
70
|
+
data,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
const message = comparison === 0
|
|
74
|
+
? `${current} (up to date)`
|
|
75
|
+
: `${current} (newer than the latest release, ${latest})`;
|
|
76
|
+
return check("version", "Version", "ok", message, { data });
|
|
77
|
+
}
|
|
78
|
+
function nodeCheck() {
|
|
79
|
+
const version = process.version;
|
|
80
|
+
const supportedMajors = [
|
|
81
|
+
...cliPackage.engines.node.matchAll(/\^(\d+)\./g),
|
|
82
|
+
].map((match) => Number(match[1]));
|
|
83
|
+
const major = Number(process.versions.node.split(".")[0]);
|
|
84
|
+
const data = { version, supported: cliPackage.engines.node };
|
|
85
|
+
if (supportedMajors.length > 0 && !supportedMajors.includes(major)) {
|
|
86
|
+
return check("node", "Node.js", "warn", `${version} (unsupported)`, {
|
|
87
|
+
hint: `dx-cli supports Node.js ${cliPackage.engines.node}. Other versions may not work correctly.`,
|
|
88
|
+
data,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
return check("node", "Node.js", "ok", version, { data });
|
|
92
|
+
}
|
|
93
|
+
export function upgradeCommand(method) {
|
|
94
|
+
switch (method) {
|
|
95
|
+
case "npx":
|
|
96
|
+
return "npx @get-dx/cli@latest";
|
|
97
|
+
case "dev":
|
|
98
|
+
return "pull the latest changes and run `make`";
|
|
99
|
+
case "pnpm":
|
|
100
|
+
return "pnpm add -g @get-dx/cli@latest";
|
|
101
|
+
case "npm":
|
|
102
|
+
case "unknown":
|
|
103
|
+
return "npm install -g @get-dx/cli@latest";
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function installMethodName(method) {
|
|
107
|
+
switch (method) {
|
|
108
|
+
case "npx":
|
|
109
|
+
return "npx";
|
|
110
|
+
case "dev":
|
|
111
|
+
return "pnpm link (dev)";
|
|
112
|
+
case "pnpm":
|
|
113
|
+
return "pnpm global";
|
|
114
|
+
case "npm":
|
|
115
|
+
return "npm global";
|
|
116
|
+
case "unknown":
|
|
117
|
+
return "unknown";
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Work out how the CLI was installed from where its entry script lives.
|
|
122
|
+
*/
|
|
123
|
+
export function detectInstall(entryScript = process.argv[1], execPath = process.execPath) {
|
|
124
|
+
let resolved = entryScript;
|
|
125
|
+
try {
|
|
126
|
+
resolved = fs.realpathSync(entryScript);
|
|
127
|
+
}
|
|
128
|
+
catch {
|
|
129
|
+
// Fall back to the unresolved path.
|
|
130
|
+
}
|
|
131
|
+
// `dist/index.js` lives one level below the package root.
|
|
132
|
+
const packageRoot = path.dirname(path.dirname(resolved));
|
|
133
|
+
const normalized = resolved.split(path.sep).join("/");
|
|
134
|
+
if (normalized.includes("/_npx/")) {
|
|
135
|
+
return { method: "npx", path: packageRoot };
|
|
136
|
+
}
|
|
137
|
+
if (!normalized.includes("/node_modules/")) {
|
|
138
|
+
if (fs.existsSync(path.join(packageRoot, ".git"))) {
|
|
139
|
+
return { method: "dev", path: packageRoot };
|
|
140
|
+
}
|
|
141
|
+
return { method: "unknown", path: resolved };
|
|
142
|
+
}
|
|
143
|
+
if (normalized.includes("/pnpm/global/") || normalized.includes("/.pnpm/")) {
|
|
144
|
+
return { method: "pnpm", path: packageRoot };
|
|
145
|
+
}
|
|
146
|
+
const npmGlobalRoot = process.platform === "win32"
|
|
147
|
+
? path.join(path.dirname(execPath), "node_modules")
|
|
148
|
+
: path.join(path.dirname(path.dirname(execPath)), "lib", "node_modules");
|
|
149
|
+
if (resolved.startsWith(npmGlobalRoot + path.sep) ||
|
|
150
|
+
normalized.includes("/lib/node_modules/@get-dx/cli/")) {
|
|
151
|
+
return { method: "npm", path: packageRoot };
|
|
152
|
+
}
|
|
153
|
+
return { method: "unknown", path: packageRoot };
|
|
154
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import dayjs from "dayjs";
|
|
2
|
+
import { inspectConfigFile, parseWebBaseUrl, resolveApiBaseUrlWithSource, resolveWebBaseUrlWithSource, } from "../config.js";
|
|
3
|
+
import { getTokenWithSource } from "../secrets.js";
|
|
4
|
+
import { maskToken } from "../ui.js";
|
|
5
|
+
import { formatBytes, tildify } from "./format.js";
|
|
6
|
+
import { check } from "./types.js";
|
|
7
|
+
export async function checkConfig() {
|
|
8
|
+
const checks = [];
|
|
9
|
+
checks.push(configFileCheck());
|
|
10
|
+
const api = resolveApiBaseUrlWithSource();
|
|
11
|
+
const web = resolveWebBaseUrlWithSource();
|
|
12
|
+
checks.push(check("api_base_url", "API base URL", "ok", api.url, {
|
|
13
|
+
source: api.source,
|
|
14
|
+
data: { url: api.url },
|
|
15
|
+
}), check("web_base_url", "Web base URL", "ok", web.url, {
|
|
16
|
+
source: web.source,
|
|
17
|
+
data: { url: web.url },
|
|
18
|
+
}));
|
|
19
|
+
if (process.env.DX_API_BASE_URL && !process.env.DX_WEB_BASE_URL) {
|
|
20
|
+
checks.push(check("base_url_env", "Base URL env vars", "fail", "DX_API_BASE_URL is set but DX_WEB_BASE_URL is not", {
|
|
21
|
+
hint: "Set DX_WEB_BASE_URL to your DX web app URL (e.g. https://acme.getdx.io) or unset DX_API_BASE_URL.",
|
|
22
|
+
}));
|
|
23
|
+
}
|
|
24
|
+
checks.push(deploymentCheck(web.url));
|
|
25
|
+
const { check: tokenCheck, token } = await tokenCheckFor(api.url);
|
|
26
|
+
checks.push(tokenCheck);
|
|
27
|
+
return {
|
|
28
|
+
section: { id: "config", title: "Config", checks },
|
|
29
|
+
facts: { apiBaseUrl: api.url, webBaseUrl: web.url, token },
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function configFileCheck() {
|
|
33
|
+
const inspection = inspectConfigFile();
|
|
34
|
+
const displayPath = tildify(inspection.path);
|
|
35
|
+
switch (inspection.status) {
|
|
36
|
+
case "missing":
|
|
37
|
+
if (process.env.DX_API_TOKEN) {
|
|
38
|
+
return check("config_file", "Config file", "ok", `${displayPath} not present (using env/defaults)`, { data: { path: displayPath, exists: false } });
|
|
39
|
+
}
|
|
40
|
+
return check("config_file", "Config file", "warn", `${displayPath} not present`, {
|
|
41
|
+
hint: "This usually means `dx auth login` has never completed. Run `dx auth login`.",
|
|
42
|
+
data: { path: displayPath, exists: false },
|
|
43
|
+
});
|
|
44
|
+
case "invalid":
|
|
45
|
+
return check("config_file", "Config file", "fail", `${displayPath} could not be read (${inspection.error})`, {
|
|
46
|
+
hint: "The file is ignored while it is invalid. Fix or delete it, then run `dx auth login`.",
|
|
47
|
+
data: { path: displayPath, exists: true, error: inspection.error },
|
|
48
|
+
});
|
|
49
|
+
case "ok": {
|
|
50
|
+
const modifiedAt = dayjs(inspection.modifiedAt).format();
|
|
51
|
+
return check("config_file", "Config file", "ok", `${displayPath} (${formatBytes(inspection.sizeBytes)}, modified ${modifiedAt})`, {
|
|
52
|
+
data: {
|
|
53
|
+
path: displayPath,
|
|
54
|
+
exists: true,
|
|
55
|
+
size_bytes: inspection.sizeBytes,
|
|
56
|
+
modified_at: modifiedAt,
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function deploymentCheck(webBaseUrl) {
|
|
63
|
+
const parsed = parseWebBaseUrl(webBaseUrl);
|
|
64
|
+
switch (parsed.type) {
|
|
65
|
+
case "cloud":
|
|
66
|
+
return check("deployment", "Deployment", "ok", "cloud", {
|
|
67
|
+
data: { type: "cloud" },
|
|
68
|
+
});
|
|
69
|
+
case "dedicated":
|
|
70
|
+
return check("deployment", "Deployment", "ok", `dedicated (${parsed.accountName})`, { data: { type: "dedicated", account: parsed.accountName } });
|
|
71
|
+
case "managed":
|
|
72
|
+
return check("deployment", "Deployment", "ok", "managed (custom domain)", {
|
|
73
|
+
data: { type: "managed" },
|
|
74
|
+
});
|
|
75
|
+
case "invalid":
|
|
76
|
+
return check("deployment", "Deployment", "fail", `could not recognize web base URL "${parsed.input}"`, {
|
|
77
|
+
hint: "Expected https://app.getdx.com, https://<account>.getdx.io, or a custom domain.",
|
|
78
|
+
data: { type: "invalid" },
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
async function tokenCheckFor(apiBaseUrl) {
|
|
83
|
+
const lookup = await getTokenWithSource(apiBaseUrl);
|
|
84
|
+
switch (lookup.source) {
|
|
85
|
+
case "env":
|
|
86
|
+
case "keychain": {
|
|
87
|
+
const masked = maskToken(lookup.token);
|
|
88
|
+
return {
|
|
89
|
+
token: lookup.token,
|
|
90
|
+
check: check("token", "Token", "ok", masked ?? "", {
|
|
91
|
+
source: lookup.source,
|
|
92
|
+
data: { source: lookup.source, masked_token: masked },
|
|
93
|
+
}),
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
case "none":
|
|
97
|
+
return {
|
|
98
|
+
token: null,
|
|
99
|
+
check: check("token", "Token", "fail", `no token found (checked DX_API_TOKEN and ${lookup.storeName})`, {
|
|
100
|
+
hint: "Run `dx auth login`, or set DX_API_TOKEN.",
|
|
101
|
+
data: { source: null },
|
|
102
|
+
}),
|
|
103
|
+
};
|
|
104
|
+
case "error":
|
|
105
|
+
return {
|
|
106
|
+
token: null,
|
|
107
|
+
check: check("token", "Token", "fail", `could not read ${lookup.storeName ?? "the OS secret store"}: ${lookup.error}`, {
|
|
108
|
+
hint: tokenStoreHint(),
|
|
109
|
+
data: { source: null, error: lookup.error },
|
|
110
|
+
}),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function tokenStoreHint() {
|
|
115
|
+
if (process.platform === "linux") {
|
|
116
|
+
return "Install `libsecret-tools` and make sure a Secret Service provider (e.g. GNOME Keyring) is running, or set DX_API_TOKEN instead.";
|
|
117
|
+
}
|
|
118
|
+
return "Set DX_API_TOKEN in the environment instead of storing the token in the OS secret store.";
|
|
119
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { maskToken } from "../ui.js";
|
|
3
|
+
import { redactUrlCredentials, tildify } from "./format.js";
|
|
4
|
+
import { check } from "./types.js";
|
|
5
|
+
const PROXY_VARS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY"];
|
|
6
|
+
/** Variable names that likely hold credentials, so their values are masked. */
|
|
7
|
+
const SENSITIVE_NAME_PATTERN = /TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|API_?KEY|PRIVATE_?KEY|AUTH|COOKIE|SESSION_?KEY/i;
|
|
8
|
+
export function isSensitiveEnvName(name) {
|
|
9
|
+
return SENSITIVE_NAME_PATTERN.test(name);
|
|
10
|
+
}
|
|
11
|
+
export function checkEnvironment(env = process.env) {
|
|
12
|
+
const checks = [];
|
|
13
|
+
const names = Object.keys(env).sort();
|
|
14
|
+
for (const name of names.filter((name) => name.startsWith("DX_"))) {
|
|
15
|
+
const raw = env[name] ?? "";
|
|
16
|
+
const sensitive = isSensitiveEnvName(name);
|
|
17
|
+
const value = sensitive ? (maskToken(raw) ?? "") : raw;
|
|
18
|
+
checks.push(check(name.toLowerCase(), name, "ok", value, {
|
|
19
|
+
data: { masked: sensitive },
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
for (const name of names.filter((name) => PROXY_VARS.includes(name.toUpperCase()))) {
|
|
23
|
+
const value = redactUrlCredentials(env[name] ?? "");
|
|
24
|
+
checks.push(check(name.toLowerCase(), name, "warn", value, {
|
|
25
|
+
hint: "dx-cli does not use proxy environment variables. Requests go directly to the DX API.",
|
|
26
|
+
data: { value },
|
|
27
|
+
}));
|
|
28
|
+
}
|
|
29
|
+
const extraCaCerts = env.NODE_EXTRA_CA_CERTS;
|
|
30
|
+
if (extraCaCerts) {
|
|
31
|
+
const displayPath = tildify(extraCaCerts);
|
|
32
|
+
const exists = fs.existsSync(extraCaCerts);
|
|
33
|
+
checks.push(exists
|
|
34
|
+
? check("node_extra_ca_certs", "NODE_EXTRA_CA_CERTS", "ok", `${displayPath} (file exists)`, { data: { path: displayPath, exists } })
|
|
35
|
+
: check("node_extra_ca_certs", "NODE_EXTRA_CA_CERTS", "warn", `${displayPath} (file not found)`, {
|
|
36
|
+
hint: "Node ignores this setting when the file does not exist. Point it at your CA bundle in PEM format.",
|
|
37
|
+
data: { path: displayPath, exists },
|
|
38
|
+
}));
|
|
39
|
+
}
|
|
40
|
+
const useSystemCa = env.NODE_USE_SYSTEM_CA;
|
|
41
|
+
if (useSystemCa) {
|
|
42
|
+
checks.push(check("node_use_system_ca", "NODE_USE_SYSTEM_CA", "ok", useSystemCa));
|
|
43
|
+
}
|
|
44
|
+
const rejectUnauthorized = env.NODE_TLS_REJECT_UNAUTHORIZED;
|
|
45
|
+
if (rejectUnauthorized !== undefined) {
|
|
46
|
+
checks.push(rejectUnauthorized === "0"
|
|
47
|
+
? check("node_tls_reject_unauthorized", "NODE_TLS_REJECT_UNAUTHORIZED", "warn", "0 (certificate verification disabled)", {
|
|
48
|
+
hint: "This disables TLS certificate checks and is insecure. Use NODE_EXTRA_CA_CERTS to trust your corporate CA instead.",
|
|
49
|
+
})
|
|
50
|
+
: check("node_tls_reject_unauthorized", "NODE_TLS_REJECT_UNAUTHORIZED", "ok", rejectUnauthorized));
|
|
51
|
+
}
|
|
52
|
+
const nodeOptions = env.NODE_OPTIONS;
|
|
53
|
+
if (nodeOptions) {
|
|
54
|
+
checks.push(check("node_options", "NODE_OPTIONS", "ok", nodeOptions));
|
|
55
|
+
}
|
|
56
|
+
if (checks.length === 0) {
|
|
57
|
+
checks.push(check("none", "Variables", "ok", "(none set)"));
|
|
58
|
+
}
|
|
59
|
+
return { id: "environment", title: "Environment", checks };
|
|
60
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import { describeFetchFailure } from "../http.js";
|
|
3
|
+
import { CHECK_TIMEOUT_MS, ProbeTimeoutError } from "./probes.js";
|
|
4
|
+
/** Replace the home directory with `~` so paths don't reveal usernames. */
|
|
5
|
+
export function tildify(filePath) {
|
|
6
|
+
const home = os.homedir();
|
|
7
|
+
if (home &&
|
|
8
|
+
(filePath === home ||
|
|
9
|
+
filePath.startsWith(home + "/") ||
|
|
10
|
+
filePath.startsWith(home + "\\"))) {
|
|
11
|
+
return "~" + filePath.slice(home.length);
|
|
12
|
+
}
|
|
13
|
+
return filePath;
|
|
14
|
+
}
|
|
15
|
+
export function formatBytes(bytes) {
|
|
16
|
+
if (bytes < 1024) {
|
|
17
|
+
return `${bytes} B`;
|
|
18
|
+
}
|
|
19
|
+
const units = ["KB", "MB", "GB"];
|
|
20
|
+
let value = bytes / 1024;
|
|
21
|
+
let unit = 0;
|
|
22
|
+
while (value >= 1024 && unit < units.length - 1) {
|
|
23
|
+
value /= 1024;
|
|
24
|
+
unit += 1;
|
|
25
|
+
}
|
|
26
|
+
return `${value.toFixed(1)} ${units[unit]}`;
|
|
27
|
+
}
|
|
28
|
+
/** Remove credentials from a URL-like string, e.g. a proxy URL. */
|
|
29
|
+
export function redactUrlCredentials(value) {
|
|
30
|
+
return value.replace(/\/\/[^/@\s]+@/g, "//***@");
|
|
31
|
+
}
|
|
32
|
+
export function isTimeout(error) {
|
|
33
|
+
if (error instanceof ProbeTimeoutError) {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
let current = error;
|
|
37
|
+
while (current instanceof Error) {
|
|
38
|
+
if (current.name === "TimeoutError") {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
current = current.cause;
|
|
42
|
+
}
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
/** The error code (e.g. `ECONNRESET`) of an error or anything in its cause chain. */
|
|
46
|
+
export function errorCode(error) {
|
|
47
|
+
let current = error;
|
|
48
|
+
while (current instanceof Error) {
|
|
49
|
+
const { code } = current;
|
|
50
|
+
if (typeof code === "string") {
|
|
51
|
+
return code;
|
|
52
|
+
}
|
|
53
|
+
current = current.cause;
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
export function describeError(error, action) {
|
|
58
|
+
if (isTimeout(error)) {
|
|
59
|
+
const timeoutMs = error instanceof ProbeTimeoutError ? error.timeoutMs : CHECK_TIMEOUT_MS;
|
|
60
|
+
return `${action} timed out after ${timeoutMs / 1000}s`;
|
|
61
|
+
}
|
|
62
|
+
return describeFetchFailure(error);
|
|
63
|
+
}
|
|
64
|
+
export async function timed(fn) {
|
|
65
|
+
const start = performance.now();
|
|
66
|
+
const value = await fn();
|
|
67
|
+
return { value, ms: Math.round(performance.now() - start) };
|
|
68
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { buildLogger } from "../runtime.js";
|
|
2
|
+
import { checkAuth } from "./auth.js";
|
|
3
|
+
import { checkCli, defaultLatestVersionLookup, detectInstall, } from "./cli.js";
|
|
4
|
+
import { checkConfig } from "./config.js";
|
|
5
|
+
import { checkEnvironment } from "./environment.js";
|
|
6
|
+
import { checkNetwork } from "./network.js";
|
|
7
|
+
import { defaultProbes } from "./probes.js";
|
|
8
|
+
import { checkSkill } from "./skill.js";
|
|
9
|
+
import cliPackage from "../../package.json" with { type: "json" };
|
|
10
|
+
const defaultDependencies = {
|
|
11
|
+
probes: defaultProbes,
|
|
12
|
+
latestVersion: defaultLatestVersionLookup,
|
|
13
|
+
install: () => detectInstall(),
|
|
14
|
+
now: () => new Date(),
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Run every doctor check. Never throws: problems are reported as checks.
|
|
18
|
+
*
|
|
19
|
+
* Checks run in dependency order (config before network before auth), but the
|
|
20
|
+
* report lists sections in reading order.
|
|
21
|
+
*/
|
|
22
|
+
export async function runDoctor(context, options = {}) {
|
|
23
|
+
const deps = { ...defaultDependencies, ...options.dependencies };
|
|
24
|
+
const onPhase = options.onPhase ?? (() => { });
|
|
25
|
+
const now = deps.now();
|
|
26
|
+
onPhase("config");
|
|
27
|
+
const environment = checkEnvironment();
|
|
28
|
+
const config = await checkConfig();
|
|
29
|
+
const { apiBaseUrl, webBaseUrl, token } = config.facts;
|
|
30
|
+
const runtime = token
|
|
31
|
+
? {
|
|
32
|
+
apiBaseUrl,
|
|
33
|
+
webBaseUrl,
|
|
34
|
+
token,
|
|
35
|
+
context,
|
|
36
|
+
version: cliPackage.version,
|
|
37
|
+
logger: buildLogger(context),
|
|
38
|
+
}
|
|
39
|
+
: null;
|
|
40
|
+
onPhase("network");
|
|
41
|
+
const network = await checkNetwork({
|
|
42
|
+
apiBaseUrl,
|
|
43
|
+
webBaseUrl,
|
|
44
|
+
version: cliPackage.version,
|
|
45
|
+
probes: deps.probes,
|
|
46
|
+
});
|
|
47
|
+
const { apiReachable } = network.facts;
|
|
48
|
+
onPhase("auth");
|
|
49
|
+
const auth = await checkAuth({ runtime, apiReachable, now });
|
|
50
|
+
onPhase("version");
|
|
51
|
+
const cli = await checkCli({
|
|
52
|
+
runtime,
|
|
53
|
+
apiReachable,
|
|
54
|
+
install: deps.install(),
|
|
55
|
+
latestVersion: deps.latestVersion,
|
|
56
|
+
now,
|
|
57
|
+
});
|
|
58
|
+
const skill = await checkSkill();
|
|
59
|
+
return buildReport([
|
|
60
|
+
cli,
|
|
61
|
+
environment,
|
|
62
|
+
config.section,
|
|
63
|
+
network.section,
|
|
64
|
+
auth,
|
|
65
|
+
skill,
|
|
66
|
+
]);
|
|
67
|
+
}
|
|
68
|
+
function buildReport(sections) {
|
|
69
|
+
const summary = {
|
|
70
|
+
ok: 0,
|
|
71
|
+
warn: 0,
|
|
72
|
+
fail: 0,
|
|
73
|
+
skip: 0,
|
|
74
|
+
};
|
|
75
|
+
for (const section of sections) {
|
|
76
|
+
for (const check of section.checks) {
|
|
77
|
+
summary[check.status] += 1;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { ok: summary.fail === 0, summary, sections };
|
|
81
|
+
}
|