zinkee 0.1.17 → 0.1.18
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/package.json +1 -1
- package/src/client.test.ts +61 -0
- package/src/client.ts +27 -1
- package/src/commands/profiles.test.ts +109 -0
- package/src/commands/profiles.ts +24 -2
- package/src/config.test.ts +22 -0
- package/src/config.ts +4 -8
- package/src/runtime-context.ts +7 -1
- package/src/utils/errors.test.ts +38 -1
- package/src/utils/errors.ts +10 -2
package/package.json
CHANGED
package/src/client.test.ts
CHANGED
|
@@ -113,6 +113,67 @@ describe("ApiClient", () => {
|
|
|
113
113
|
}
|
|
114
114
|
});
|
|
115
115
|
|
|
116
|
+
it("surfaces the underlying network cause in the error message", async () => {
|
|
117
|
+
const dnsCause = Object.assign(new Error("getaddrinfo ENOTFOUND api-z2.zinkee.invalid"), {
|
|
118
|
+
code: "ENOTFOUND",
|
|
119
|
+
});
|
|
120
|
+
const fetchError = Object.assign(new TypeError("fetch failed"), { cause: dnsCause });
|
|
121
|
+
const fetchMock = vi.fn(async () => {
|
|
122
|
+
throw fetchError;
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const client = new ApiClient({
|
|
126
|
+
baseUrl: "https://api-z2.zinkee.invalid",
|
|
127
|
+
fetch: fetchMock,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
await expect(client.getJson("/api/v2/schemas")).rejects.toMatchObject({
|
|
131
|
+
name: "ApiClientError",
|
|
132
|
+
code: "network_error",
|
|
133
|
+
message:
|
|
134
|
+
"Request failed for GET https://api-z2.zinkee.invalid/api/v2/schemas: getaddrinfo ENOTFOUND api-z2.zinkee.invalid.",
|
|
135
|
+
cause: fetchError,
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it("appends the cause code when it is not already part of the message", async () => {
|
|
140
|
+
const tlsCause = Object.assign(new Error("self signed certificate"), {
|
|
141
|
+
code: "DEPTH_ZERO_SELF_SIGNED_CERT",
|
|
142
|
+
});
|
|
143
|
+
const fetchError = Object.assign(new TypeError("fetch failed"), { cause: tlsCause });
|
|
144
|
+
const fetchMock = vi.fn(async () => {
|
|
145
|
+
throw fetchError;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
const client = new ApiClient({
|
|
149
|
+
baseUrl: "https://self-signed.example.com",
|
|
150
|
+
fetch: fetchMock,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await expect(client.getJson("/api/v2/schemas")).rejects.toMatchObject({
|
|
154
|
+
code: "network_error",
|
|
155
|
+
message:
|
|
156
|
+
"Request failed for GET https://self-signed.example.com/api/v2/schemas: self signed certificate (DEPTH_ZERO_SELF_SIGNED_CERT).",
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("falls back to the bare message when the network cause has no useful detail", async () => {
|
|
161
|
+
const fetchMock = vi.fn(async () => {
|
|
162
|
+
throw new TypeError("fetch failed");
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
const client = new ApiClient({
|
|
166
|
+
baseUrl: "https://api.example.com",
|
|
167
|
+
fetch: fetchMock,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
await expect(client.getJson("/api/v2/schemas")).rejects.toMatchObject({
|
|
171
|
+
name: "ApiClientError",
|
|
172
|
+
code: "network_error",
|
|
173
|
+
message: "Request failed for GET https://api.example.com/api/v2/schemas.",
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
116
177
|
it("returns binary responses as ArrayBuffers", async () => {
|
|
117
178
|
const fetchMock = vi.fn(async () => binaryResponse([1, 2, 3]));
|
|
118
179
|
|
package/src/client.ts
CHANGED
|
@@ -110,6 +110,27 @@ function parseErrorBody(text: string, contentType: string | null): unknown {
|
|
|
110
110
|
}
|
|
111
111
|
}
|
|
112
112
|
|
|
113
|
+
function summarizeNetworkCause(cause: unknown): string | undefined {
|
|
114
|
+
const seen = new Set<unknown>();
|
|
115
|
+
let current: unknown = cause;
|
|
116
|
+
|
|
117
|
+
while (current && typeof current === "object" && !seen.has(current)) {
|
|
118
|
+
seen.add(current);
|
|
119
|
+
|
|
120
|
+
const candidate = current as { message?: unknown; code?: unknown; cause?: unknown };
|
|
121
|
+
const message = typeof candidate.message === "string" ? candidate.message.trim() : "";
|
|
122
|
+
const code = typeof candidate.code === "string" ? candidate.code : undefined;
|
|
123
|
+
|
|
124
|
+
if (message && message !== "fetch failed") {
|
|
125
|
+
return code && !message.includes(code) ? `${message} (${code})` : message;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
current = candidate.cause;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
|
|
113
134
|
export class ApiClient {
|
|
114
135
|
private readonly fetchImpl: typeof fetch;
|
|
115
136
|
|
|
@@ -206,7 +227,12 @@ export class ApiClient {
|
|
|
206
227
|
});
|
|
207
228
|
}
|
|
208
229
|
|
|
209
|
-
|
|
230
|
+
const causeSummary = summarizeNetworkCause(cause);
|
|
231
|
+
const message = causeSummary
|
|
232
|
+
? `Request failed for ${method} ${url}: ${causeSummary}.`
|
|
233
|
+
: `Request failed for ${method} ${url}.`;
|
|
234
|
+
|
|
235
|
+
throw new ApiClientError("network_error", message, {
|
|
210
236
|
url,
|
|
211
237
|
method,
|
|
212
238
|
cause,
|
|
@@ -79,4 +79,113 @@ base_url = "https://api.zinkee.test"
|
|
|
79
79
|
expect(saved).toContain('base_url = "http://localhost:8088"');
|
|
80
80
|
expect(saved).toContain('default_profile = "local"');
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
it("rejects a malformed base URL when adding a profile", async () => {
|
|
84
|
+
const configPath = join(
|
|
85
|
+
mkdtempSync(join(tmpdir(), "zinkee-profiles-add-bad-url-")),
|
|
86
|
+
"config.toml",
|
|
87
|
+
);
|
|
88
|
+
const output = createStdoutBuffer();
|
|
89
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
90
|
+
|
|
91
|
+
await expect(
|
|
92
|
+
program.parseAsync([
|
|
93
|
+
"node",
|
|
94
|
+
"zinkee",
|
|
95
|
+
"profiles",
|
|
96
|
+
"add",
|
|
97
|
+
"broken",
|
|
98
|
+
"--api-key",
|
|
99
|
+
"any",
|
|
100
|
+
"--base-url",
|
|
101
|
+
"api-z2.zinkee.com",
|
|
102
|
+
]),
|
|
103
|
+
).rejects.toMatchObject({
|
|
104
|
+
name: "CliError",
|
|
105
|
+
code: "configuration_error",
|
|
106
|
+
message: '"--base-url" must be an absolute http(s) URL.',
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("rejects a non-http(s) base URL when adding a profile", async () => {
|
|
111
|
+
const configPath = join(
|
|
112
|
+
mkdtempSync(join(tmpdir(), "zinkee-profiles-add-ftp-")),
|
|
113
|
+
"config.toml",
|
|
114
|
+
);
|
|
115
|
+
const output = createStdoutBuffer();
|
|
116
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
117
|
+
|
|
118
|
+
await expect(
|
|
119
|
+
program.parseAsync([
|
|
120
|
+
"node",
|
|
121
|
+
"zinkee",
|
|
122
|
+
"profiles",
|
|
123
|
+
"add",
|
|
124
|
+
"broken",
|
|
125
|
+
"--api-key",
|
|
126
|
+
"any",
|
|
127
|
+
"--base-url",
|
|
128
|
+
"ftp://api-z2.zinkee.com",
|
|
129
|
+
]),
|
|
130
|
+
).rejects.toMatchObject({
|
|
131
|
+
name: "CliError",
|
|
132
|
+
code: "configuration_error",
|
|
133
|
+
message: '"--base-url" must be an absolute http(s) URL.',
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("normalizes trailing slashes when adding a profile", async () => {
|
|
138
|
+
const configPath = join(
|
|
139
|
+
mkdtempSync(join(tmpdir(), "zinkee-profiles-add-slash-")),
|
|
140
|
+
"config.toml",
|
|
141
|
+
);
|
|
142
|
+
const output = createStdoutBuffer();
|
|
143
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
144
|
+
|
|
145
|
+
await program.parseAsync([
|
|
146
|
+
"node",
|
|
147
|
+
"zinkee",
|
|
148
|
+
"profiles",
|
|
149
|
+
"add",
|
|
150
|
+
"local",
|
|
151
|
+
"--api-key",
|
|
152
|
+
"any",
|
|
153
|
+
"--base-url",
|
|
154
|
+
"https://api-z2.zinkee.com///",
|
|
155
|
+
]);
|
|
156
|
+
|
|
157
|
+
const saved = readFileSync(configPath, "utf8");
|
|
158
|
+
|
|
159
|
+
expect(saved).toContain('base_url = "https://api-z2.zinkee.com"');
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("rejects a malformed base URL when updating a profile", async () => {
|
|
163
|
+
const configPath = createTempConfig(`
|
|
164
|
+
[profiles.prod]
|
|
165
|
+
api_key = "secret"
|
|
166
|
+
base_url = "https://api-z2.zinkee.com"
|
|
167
|
+
`);
|
|
168
|
+
const output = createStdoutBuffer();
|
|
169
|
+
const program = buildProgram({ configPath, stdout: output.stdout });
|
|
170
|
+
|
|
171
|
+
await expect(
|
|
172
|
+
program.parseAsync([
|
|
173
|
+
"node",
|
|
174
|
+
"zinkee",
|
|
175
|
+
"profiles",
|
|
176
|
+
"update",
|
|
177
|
+
"prod",
|
|
178
|
+
"--base-url",
|
|
179
|
+
"not a url",
|
|
180
|
+
]),
|
|
181
|
+
).rejects.toMatchObject({
|
|
182
|
+
name: "CliError",
|
|
183
|
+
code: "configuration_error",
|
|
184
|
+
message: '"--base-url" must be an absolute http(s) URL.',
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const saved = readFileSync(configPath, "utf8");
|
|
188
|
+
|
|
189
|
+
expect(saved).toContain('base_url = "https://api-z2.zinkee.com"');
|
|
190
|
+
});
|
|
82
191
|
});
|
package/src/commands/profiles.ts
CHANGED
|
@@ -4,11 +4,13 @@ import { Command } from "commander";
|
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
6
|
defaultConfigPath,
|
|
7
|
+
ProfileResolutionError,
|
|
7
8
|
readConfigIfExists,
|
|
8
9
|
redactProfile,
|
|
9
10
|
removeProfileConfig,
|
|
10
11
|
setDefaultProfile,
|
|
11
12
|
setProfileConfig,
|
|
13
|
+
validateBaseUrl,
|
|
12
14
|
writeConfig,
|
|
13
15
|
} from "../config.js";
|
|
14
16
|
import type { AppConfig, ProfileConfig } from "../types.js";
|
|
@@ -93,6 +95,22 @@ function loadConfigForAdd(filePath: string): AppConfig {
|
|
|
93
95
|
return readConfigIfExists(filePath) ?? { profiles: {} };
|
|
94
96
|
}
|
|
95
97
|
|
|
98
|
+
function normalizeProfileBaseUrl(baseUrl: string | undefined): string | undefined {
|
|
99
|
+
if (baseUrl === undefined) {
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
return validateBaseUrl(baseUrl, '"--base-url"');
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error instanceof ProfileResolutionError) {
|
|
107
|
+
throw new CliError("configuration_error", error.message);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
function emitProfilesTable(stdout: CommandIo, data: readonly DisplayProfile[]): void {
|
|
97
115
|
printTable(
|
|
98
116
|
data.map((profile) => ({
|
|
@@ -191,9 +209,11 @@ export function registerProfilesCommands(
|
|
|
191
209
|
);
|
|
192
210
|
}
|
|
193
211
|
|
|
212
|
+
const baseUrl = normalizeProfileBaseUrl(commandOptions.baseUrl);
|
|
213
|
+
|
|
194
214
|
let nextConfig = setProfileConfig(config, profileName, {
|
|
195
215
|
apiKey: commandOptions.apiKey,
|
|
196
|
-
baseUrl
|
|
216
|
+
baseUrl,
|
|
197
217
|
});
|
|
198
218
|
|
|
199
219
|
if (!nextConfig.defaultProfile) {
|
|
@@ -236,9 +256,11 @@ export function registerProfilesCommands(
|
|
|
236
256
|
const config = readRequiredConfig(filePath);
|
|
237
257
|
getRequiredProfile(config, profileName);
|
|
238
258
|
|
|
259
|
+
const baseUrl = normalizeProfileBaseUrl(commandOptions.baseUrl);
|
|
260
|
+
|
|
239
261
|
const nextConfig = setProfileConfig(config, profileName, {
|
|
240
262
|
apiKey: commandOptions.apiKey,
|
|
241
|
-
baseUrl
|
|
263
|
+
baseUrl,
|
|
242
264
|
});
|
|
243
265
|
|
|
244
266
|
writeConfig(nextConfig, filePath);
|
package/src/config.test.ts
CHANGED
|
@@ -256,6 +256,28 @@ describe("resolveRuntimeContext", () => {
|
|
|
256
256
|
else process.env[ENV_BASE_URL] = previousBaseUrl;
|
|
257
257
|
}
|
|
258
258
|
});
|
|
259
|
+
|
|
260
|
+
it("rejects a --base-url override that is not an absolute http(s) URL", () => {
|
|
261
|
+
expect(() =>
|
|
262
|
+
resolveRuntimeContext({ profiles: {} }, { baseUrl: "api-z2.zinkee.com", apiKey: "k" }),
|
|
263
|
+
).toThrowError('"--base-url" must be an absolute http(s) URL.');
|
|
264
|
+
|
|
265
|
+
expect(() =>
|
|
266
|
+
resolveRuntimeContext(
|
|
267
|
+
{ profiles: {} },
|
|
268
|
+
{ baseUrl: "ftp://api-z2.zinkee.com", apiKey: "k" },
|
|
269
|
+
),
|
|
270
|
+
).toThrowError('"--base-url" must be an absolute http(s) URL.');
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("normalizes trailing slashes on a --base-url override", () => {
|
|
274
|
+
expect(
|
|
275
|
+
resolveRuntimeContext(
|
|
276
|
+
{ profiles: {} },
|
|
277
|
+
{ baseUrl: "https://api-z2.zinkee.com///", apiKey: "k" },
|
|
278
|
+
).baseUrl,
|
|
279
|
+
).toBe("https://api-z2.zinkee.com");
|
|
280
|
+
});
|
|
259
281
|
});
|
|
260
282
|
|
|
261
283
|
describe("env readers", () => {
|
package/src/config.ts
CHANGED
|
@@ -260,21 +260,17 @@ export function resolveProfile(
|
|
|
260
260
|
throw new ProfileResolutionError("No profile could be resolved from config.");
|
|
261
261
|
}
|
|
262
262
|
|
|
263
|
-
export function validateBaseUrl(value: string): string {
|
|
263
|
+
export function validateBaseUrl(value: string, source: string = "Base URL"): string {
|
|
264
264
|
let parsed: URL;
|
|
265
265
|
|
|
266
266
|
try {
|
|
267
267
|
parsed = new URL(value);
|
|
268
268
|
} catch {
|
|
269
|
-
throw new ProfileResolutionError(
|
|
270
|
-
`Environment variable ${ENV_BASE_URL} must be an absolute http(s) URL.`,
|
|
271
|
-
);
|
|
269
|
+
throw new ProfileResolutionError(`${source} must be an absolute http(s) URL.`);
|
|
272
270
|
}
|
|
273
271
|
|
|
274
272
|
if (!["http:", "https:"].includes(parsed.protocol) || !parsed.host) {
|
|
275
|
-
throw new ProfileResolutionError(
|
|
276
|
-
`Environment variable ${ENV_BASE_URL} must be an absolute http(s) URL.`,
|
|
277
|
-
);
|
|
273
|
+
throw new ProfileResolutionError(`${source} must be an absolute http(s) URL.`);
|
|
278
274
|
}
|
|
279
275
|
|
|
280
276
|
return value.replace(/\/+$/, "");
|
|
@@ -290,7 +286,7 @@ export function readEnvBaseUrl(env: NodeJS.ProcessEnv = process.env): string | u
|
|
|
290
286
|
return undefined;
|
|
291
287
|
}
|
|
292
288
|
|
|
293
|
-
return validateBaseUrl(baseUrl);
|
|
289
|
+
return validateBaseUrl(baseUrl, `Environment variable ${ENV_BASE_URL}`);
|
|
294
290
|
}
|
|
295
291
|
|
|
296
292
|
export function readEnvProfile(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
package/src/runtime-context.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
readEnvProfile,
|
|
10
10
|
resolveProfile,
|
|
11
11
|
ProfileResolutionError,
|
|
12
|
+
validateBaseUrl,
|
|
12
13
|
} from "./config.js";
|
|
13
14
|
|
|
14
15
|
export function resolveRuntimeContext(
|
|
@@ -19,9 +20,14 @@ export function resolveRuntimeContext(
|
|
|
19
20
|
const envApiKey = readEnvApiKey();
|
|
20
21
|
const envBaseUrl = readEnvBaseUrl();
|
|
21
22
|
|
|
23
|
+
const flagBaseUrl =
|
|
24
|
+
overrides.baseUrl !== undefined
|
|
25
|
+
? validateBaseUrl(overrides.baseUrl, '"--base-url"')
|
|
26
|
+
: undefined;
|
|
27
|
+
|
|
22
28
|
const profileOverride = overrides.profile ?? envProfile;
|
|
23
29
|
const apiKeyOverride = overrides.apiKey ?? envApiKey;
|
|
24
|
-
const baseUrlOverride =
|
|
30
|
+
const baseUrlOverride = flagBaseUrl ?? envBaseUrl;
|
|
25
31
|
|
|
26
32
|
if (apiKeyOverride && !baseUrlOverride) {
|
|
27
33
|
throw new ProfileResolutionError(
|
package/src/utils/errors.test.ts
CHANGED
|
@@ -28,7 +28,7 @@ describe("formatCliErrorJson", () => {
|
|
|
28
28
|
|
|
29
29
|
expect(JSON.parse(formatCliErrorJson(error, { exitCode: 1 }))).toEqual({
|
|
30
30
|
error: {
|
|
31
|
-
code: "
|
|
31
|
+
code: "http_error",
|
|
32
32
|
message: "Schema is locked.",
|
|
33
33
|
exitCode: 1,
|
|
34
34
|
details: {
|
|
@@ -45,4 +45,41 @@ describe("formatCliErrorJson", () => {
|
|
|
45
45
|
},
|
|
46
46
|
});
|
|
47
47
|
});
|
|
48
|
+
|
|
49
|
+
it("preserves network_error code for transport-layer failures", () => {
|
|
50
|
+
const error = new ApiClientError(
|
|
51
|
+
"network_error",
|
|
52
|
+
"Request failed for GET https://api.example.com/api/v2/schemas: getaddrinfo ENOTFOUND api.example.com.",
|
|
53
|
+
{
|
|
54
|
+
url: "https://api.example.com/api/v2/schemas",
|
|
55
|
+
method: "GET",
|
|
56
|
+
cause: new Error("getaddrinfo ENOTFOUND api.example.com"),
|
|
57
|
+
},
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
expect(JSON.parse(formatCliErrorJson(error)).error.code).toBe("network_error");
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("preserves timeout code for aborted requests", () => {
|
|
64
|
+
const error = new ApiClientError("timeout", "Request timed out after 10ms.", {
|
|
65
|
+
url: "https://api.example.com/api/v2/schemas",
|
|
66
|
+
method: "GET",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(JSON.parse(formatCliErrorJson(error)).error.code).toBe("timeout");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("preserves invalid_json code for unparseable responses", () => {
|
|
73
|
+
const error = new ApiClientError(
|
|
74
|
+
"invalid_json",
|
|
75
|
+
"Failed to parse JSON response from GET https://api.example.com/api/v2/schemas.",
|
|
76
|
+
{
|
|
77
|
+
url: "https://api.example.com/api/v2/schemas",
|
|
78
|
+
method: "GET",
|
|
79
|
+
responseText: "<html>oops</html>",
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
expect(JSON.parse(formatCliErrorJson(error)).error.code).toBe("invalid_json");
|
|
84
|
+
});
|
|
48
85
|
});
|
package/src/utils/errors.ts
CHANGED
|
@@ -7,13 +7,21 @@ export type CliErrorCode =
|
|
|
7
7
|
| "unknown_error"
|
|
8
8
|
| "invalid_cli_usage"
|
|
9
9
|
| "configuration_error"
|
|
10
|
-
| "command_not_allowed_in_read_only_mode"
|
|
10
|
+
| "command_not_allowed_in_read_only_mode"
|
|
11
|
+
| "network_error"
|
|
12
|
+
| "http_error"
|
|
13
|
+
| "timeout"
|
|
14
|
+
| "invalid_json";
|
|
11
15
|
|
|
12
16
|
export const cliExitCodeByErrorCode = {
|
|
13
17
|
unknown_error: 1,
|
|
14
18
|
invalid_cli_usage: 2,
|
|
15
19
|
configuration_error: 2,
|
|
16
20
|
command_not_allowed_in_read_only_mode: 7,
|
|
21
|
+
network_error: 1,
|
|
22
|
+
http_error: 1,
|
|
23
|
+
timeout: 1,
|
|
24
|
+
invalid_json: 1,
|
|
17
25
|
} as const satisfies Record<CliErrorCode, number>;
|
|
18
26
|
|
|
19
27
|
export interface CliErrorDetails {
|
|
@@ -144,7 +152,7 @@ export function toCliError(error: unknown): CliError {
|
|
|
144
152
|
if (error instanceof ApiClientError) {
|
|
145
153
|
const apiError = extractApiErrorDetails(error);
|
|
146
154
|
|
|
147
|
-
return new CliError(
|
|
155
|
+
return new CliError(error.code, apiError.message, apiError.details);
|
|
148
156
|
}
|
|
149
157
|
|
|
150
158
|
if (error instanceof Error) {
|