@evcraddock/slug-cli 0.6.4 → 0.7.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/README.md +24 -2
- package/dist/commands.js +221 -10
- package/dist/errors.d.ts +2 -1
- package/dist/errors.js +3 -1
- package/dist/http.js +2 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,7 +25,7 @@ Create a standalone site with:
|
|
|
25
25
|
slug init ./my-site --name my-site --site-title "My Site"
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Run `slug init --help` for template options. By default, `slug init` downloads the
|
|
28
|
+
Run `slug init --help` for template options. By default, `slug init` downloads the latest template zip artifact and falls back to the template artifact published with the installed CLI version if the latest artifact is missing. `--template-dir` remains available for local template development, `--template-url` supports alternate remote zip artifacts, and `--template` reserves the selection path for future named templates. Explicit `--template-url` and `SLUGKIT_TEMPLATE_URL` overrides do not fall back.
|
|
29
29
|
|
|
30
30
|
## Login and configuration
|
|
31
31
|
|
|
@@ -51,7 +51,29 @@ Use `slug --site <name> site config show` to read supported runtime site configu
|
|
|
51
51
|
|
|
52
52
|
Use `slug --site <name> doctor` to check API compatibility and package metadata. Use `slug --site <name> upgrade` for advisory, non-destructive package update recommendations; it reports suggested dependency changes without rewriting site files.
|
|
53
53
|
|
|
54
|
-
Use `slug --site <name> post list`, `slug --site <name> post show <slug>`, `slug --site <name> post create`, `slug --site <name> post edit
|
|
54
|
+
Use `slug --site <name> post list`, `slug --site <name> post show <slug>`, `slug --site <name> post create`, `slug --site <name> post edit`, `slug --site <name> post publish`, `slug --site <name> post unpublish`, and `slug --site <name> post delete` to manage posts through the Slugkit posts API. For migration imports, pass `--published-at <date-or-datetime>` to `post create` or `post publish` to preserve an original publish date without triggering follower delivery by default.
|
|
55
|
+
|
|
56
|
+
## Markdown imports
|
|
57
|
+
|
|
58
|
+
Import one Markdown file with `slug --site <name> post import <file.md>`. Imports require YAML frontmatter delimited by `---` with `slug` and `type`; articles also require `title`. Supported frontmatter fields are `title`, `slug`, `type`, `excerpt`, `date`, `publishedAt`, `tags`, `url`, and `banner`. `date` is used when `publishedAt` is absent. Dates must be ISO dates or datetimes. Tags support either a YAML list or a flow-style list such as `[writing, notes]`.
|
|
59
|
+
|
|
60
|
+
```markdown
|
|
61
|
+
---
|
|
62
|
+
title: Hello world
|
|
63
|
+
slug: hello-world
|
|
64
|
+
type: article
|
|
65
|
+
excerpt: A short introduction
|
|
66
|
+
date: 2020-01-02
|
|
67
|
+
tags:
|
|
68
|
+
- writing
|
|
69
|
+
- updates
|
|
70
|
+
banner: images/hero.png
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
The imported body.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
A relative `banner` path is resolved from the Markdown file, uploaded through the Slugkit media API under `posts/<slug>/banner.<extension>`, and inserted as the first Markdown image in the post body so standard Slugkit article previews render it. The command looks up the post by slug: it creates a missing post and updates an existing one, replacing the imported metadata, body, tags, and publication date. Re-importing a banner uploads the current file again to refresh the deterministic media object key.
|
|
55
77
|
|
|
56
78
|
Use `slug tag list` to list tags and post usage counts through the Slugkit tags API.
|
|
57
79
|
|
package/dist/commands.js
CHANGED
|
@@ -19,6 +19,7 @@ Usage:
|
|
|
19
19
|
slug [--config <file>] init <directory> --name <name> [--site-title <title>] [--template <name>] [--template-url <url>] [--template-dir <dir>] [--json]
|
|
20
20
|
slug [--config <file>] --site <name> post list [--type article|link|note] [--status draft|published|all] [--tag <slug>] [--json]
|
|
21
21
|
slug [--config <file>] post show <slug> [--json]
|
|
22
|
+
slug [--config <file>] post import <file.md> [--json]
|
|
22
23
|
slug [--config <file>] post create --type article|link|note --slug <slug> --content <text> [--title <text>] [--url <url>] [--excerpt <text>] [--published-at <datetime>] [--tag <slug>]... [--source-id <id>] [--credit-contact-id <id>]... [--json]
|
|
23
24
|
slug [--config <file>] post edit <slug> [--slug <new-slug>] [--title <text>] [--content <text>] [--url <url>] [--excerpt <text>] [--published-at <datetime>] [--tag <slug>]... [--source-id <id>] [--credit-contact-id <id>]... [--json]
|
|
24
25
|
slug [--config <file>] post delete <slug> [--json]
|
|
@@ -155,7 +156,7 @@ Options:
|
|
|
155
156
|
--json Print command output as JSON.
|
|
156
157
|
--help, -h Show this help.
|
|
157
158
|
|
|
158
|
-
By default, slug init downloads the
|
|
159
|
+
By default, slug init downloads the latest template zip artifact and falls back to the CLI-versioned artifact if latest is missing. Set SLUGKIT_TEMPLATE_URL or pass --template-url to use a different zip artifact without fallback. Generated sites do not depend on the template artifact at runtime.`;
|
|
159
160
|
const SITE_INIT_NEXT_STEPS = [
|
|
160
161
|
"Install dependencies",
|
|
161
162
|
"Copy and edit .env",
|
|
@@ -438,17 +439,37 @@ async function resolveInitTemplateSource(context, options) {
|
|
|
438
439
|
if (options.template !== undefined && options.template !== DEFAULT_TEMPLATE_NAME) {
|
|
439
440
|
throw createInvalidUsageError(`Unsupported template: ${options.template}. Use --template-url for external template zip artifacts.`);
|
|
440
441
|
}
|
|
441
|
-
return
|
|
442
|
+
return downloadDefaultTemplateSite(context, getDefaultTemplateResolution(context));
|
|
442
443
|
}
|
|
443
|
-
function
|
|
444
|
-
if (context.templateAssetBaseUrl !== undefined) {
|
|
445
|
-
return `${context.templateAssetBaseUrl.replace(/\/+$/u, "")}/slugkit-site-template.zip`;
|
|
446
|
-
}
|
|
444
|
+
function getDefaultTemplateResolution(context) {
|
|
447
445
|
const environmentUrl = process.env.SLUGKIT_TEMPLATE_URL?.trim();
|
|
448
446
|
if (environmentUrl !== undefined && environmentUrl !== "") {
|
|
449
|
-
return environmentUrl;
|
|
447
|
+
return { primaryUrl: environmentUrl };
|
|
448
|
+
}
|
|
449
|
+
const primaryUrl = context.templateAssetBaseUrl !== undefined
|
|
450
|
+
? `${context.templateAssetBaseUrl.replace(/\/+$/u, "")}/slugkit-site-template.zip`
|
|
451
|
+
: DEFAULT_TEMPLATE_URL;
|
|
452
|
+
return {
|
|
453
|
+
primaryUrl,
|
|
454
|
+
fallbackUrl: getVersionedTemplateUrl(context.packageVersion),
|
|
455
|
+
};
|
|
456
|
+
}
|
|
457
|
+
function getVersionedTemplateUrl(packageVersion) {
|
|
458
|
+
const version = packageVersion.trim().replace(/^v/u, "");
|
|
459
|
+
return `https://forge.caradoc.com/erik/slugkit/releases/download/v${version}/slugkit-site-template-v${version}.zip`;
|
|
460
|
+
}
|
|
461
|
+
async function downloadDefaultTemplateSite(context, resolution) {
|
|
462
|
+
try {
|
|
463
|
+
return await downloadTemplateSite(context, resolution.primaryUrl);
|
|
464
|
+
}
|
|
465
|
+
catch (error) {
|
|
466
|
+
if (resolution.fallbackUrl === undefined ||
|
|
467
|
+
!(error instanceof TemplateDownloadError) ||
|
|
468
|
+
error.status !== 404) {
|
|
469
|
+
throw error;
|
|
470
|
+
}
|
|
471
|
+
return downloadTemplateSite(context, resolution.fallbackUrl);
|
|
450
472
|
}
|
|
451
|
-
return DEFAULT_TEMPLATE_URL;
|
|
452
473
|
}
|
|
453
474
|
async function downloadTemplateSite(context, templateUrl) {
|
|
454
475
|
let parsedUrl;
|
|
@@ -464,7 +485,7 @@ async function downloadTemplateSite(context, templateUrl) {
|
|
|
464
485
|
const fetchImpl = context.fetchImpl ?? fetch;
|
|
465
486
|
const response = await fetchImpl(parsedUrl);
|
|
466
487
|
if (!response.ok) {
|
|
467
|
-
throw new
|
|
488
|
+
throw new TemplateDownloadError(parsedUrl.toString(), response.status);
|
|
468
489
|
}
|
|
469
490
|
const tempDirectory = await mkdtemp(join(tmpdir(), "slug-template-"));
|
|
470
491
|
const archivePath = join(tempDirectory, "template.zip");
|
|
@@ -479,6 +500,13 @@ async function downloadTemplateSite(context, templateUrl) {
|
|
|
479
500
|
cleanup: () => rm(tempDirectory, { force: true, recursive: true }),
|
|
480
501
|
};
|
|
481
502
|
}
|
|
503
|
+
class TemplateDownloadError extends CliError {
|
|
504
|
+
status;
|
|
505
|
+
constructor(templateUrl, status) {
|
|
506
|
+
super(`Failed to download Slugkit template from ${templateUrl}: HTTP ${status}`, ExitCode.NetworkError);
|
|
507
|
+
this.status = status;
|
|
508
|
+
}
|
|
509
|
+
}
|
|
482
510
|
async function findExtractedTemplateDirectory(extractDirectory) {
|
|
483
511
|
if (await isTemplateSiteDirectory(extractDirectory)) {
|
|
484
512
|
return extractDirectory;
|
|
@@ -877,6 +905,8 @@ async function runPostsCommand(context, args) {
|
|
|
877
905
|
return runPostsListCommand(context, args.slice(1));
|
|
878
906
|
case "show":
|
|
879
907
|
return runPostsShowCommand(context, args.slice(1));
|
|
908
|
+
case "import":
|
|
909
|
+
return runPostsImportCommand(context, args.slice(1));
|
|
880
910
|
case "create":
|
|
881
911
|
return runPostsCreateCommand(context, args.slice(1));
|
|
882
912
|
case "edit":
|
|
@@ -888,8 +918,189 @@ async function runPostsCommand(context, args) {
|
|
|
888
918
|
case "unpublish":
|
|
889
919
|
return runPostsLifecycleCommand(context, args.slice(1), "unpublish");
|
|
890
920
|
default:
|
|
891
|
-
throw createInvalidUsageError("Usage: slug post <list|show|create|edit|delete|publish|unpublish>");
|
|
921
|
+
throw createInvalidUsageError("Usage: slug post <list|show|import|create|edit|delete|publish|unpublish>");
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
async function runPostsImportCommand(context, args) {
|
|
925
|
+
const { filePath, json } = readPostImportArgs(args);
|
|
926
|
+
const post = await readMarkdownPostImport(filePath);
|
|
927
|
+
const api = await createConfiguredApiContext(context);
|
|
928
|
+
const bannerUrl = post.bannerPath === undefined
|
|
929
|
+
? undefined
|
|
930
|
+
: await uploadPostImportBanner(api, post, post.bannerPath);
|
|
931
|
+
const content = bannerUrl === undefined ? post.content : createBannerContent(post, bannerUrl);
|
|
932
|
+
const body = {
|
|
933
|
+
type: post.type,
|
|
934
|
+
slug: post.slug,
|
|
935
|
+
content,
|
|
936
|
+
...(post.title === undefined ? {} : { title: post.title }),
|
|
937
|
+
...(post.excerpt === undefined ? {} : { excerpt: post.excerpt }),
|
|
938
|
+
...(post.url === undefined ? {} : { url: post.url }),
|
|
939
|
+
...(post.tags === undefined ? {} : { tagSlugs: post.tags }),
|
|
940
|
+
...(post.publishedAt === undefined ? {} : { publishedAt: post.publishedAt }),
|
|
941
|
+
};
|
|
942
|
+
writeMutationTarget(context.writer, api.apiBaseUrl, json);
|
|
943
|
+
const existing = await findImportedPost(api.client, post.slug);
|
|
944
|
+
const response = existing === undefined
|
|
945
|
+
? await api.client.requestJson({ method: "POST", path: "/posts", body })
|
|
946
|
+
: await api.client.requestJson({
|
|
947
|
+
method: "PUT",
|
|
948
|
+
path: `/posts/${encodeURIComponent(post.slug)}`,
|
|
949
|
+
body: withoutPostType(body),
|
|
950
|
+
});
|
|
951
|
+
if (json) {
|
|
952
|
+
writeJson(context.writer, response);
|
|
953
|
+
}
|
|
954
|
+
else {
|
|
955
|
+
context.writer.stdout(`Imported post ${response.data.slug} (${existing === undefined ? "created" : "updated"}).`);
|
|
956
|
+
}
|
|
957
|
+
return { exitCode: ExitCode.Ok };
|
|
958
|
+
}
|
|
959
|
+
function readPostImportArgs(args) {
|
|
960
|
+
const json = args.includes("--json");
|
|
961
|
+
const positional = args.filter((arg) => arg !== "--json");
|
|
962
|
+
if (positional.length !== 1 || positional[0] === undefined || positional[0].startsWith("--")) {
|
|
963
|
+
throw createInvalidUsageError("Usage: slug post import <file.md> [--json]");
|
|
964
|
+
}
|
|
965
|
+
return { filePath: positional[0], json };
|
|
966
|
+
}
|
|
967
|
+
async function readMarkdownPostImport(filePath) {
|
|
968
|
+
let markdown;
|
|
969
|
+
try {
|
|
970
|
+
markdown = await readFile(filePath, "utf8");
|
|
971
|
+
}
|
|
972
|
+
catch {
|
|
973
|
+
throw createInvalidUsageError(`Cannot read Markdown file: ${filePath}`);
|
|
974
|
+
}
|
|
975
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(markdown);
|
|
976
|
+
if (match === null) {
|
|
977
|
+
throw createInvalidUsageError("Markdown import requires YAML frontmatter delimited by ---");
|
|
978
|
+
}
|
|
979
|
+
const frontmatter = parseMarkdownFrontmatter(match[1]);
|
|
980
|
+
const slug = readRequiredFrontmatterString(frontmatter, "slug");
|
|
981
|
+
const type = readRequiredFrontmatterString(frontmatter, "type");
|
|
982
|
+
if (type !== "article" && type !== "link" && type !== "note") {
|
|
983
|
+
throw createInvalidUsageError("Frontmatter type must be article, link, or note");
|
|
984
|
+
}
|
|
985
|
+
const title = readOptionalFrontmatterString(frontmatter, "title");
|
|
986
|
+
if (type === "article" && title === undefined) {
|
|
987
|
+
throw createInvalidUsageError("Article imports require frontmatter title");
|
|
988
|
+
}
|
|
989
|
+
const excerpt = readOptionalFrontmatterString(frontmatter, "excerpt");
|
|
990
|
+
const url = readOptionalFrontmatterString(frontmatter, "url");
|
|
991
|
+
const tags = readFrontmatterTags(frontmatter);
|
|
992
|
+
const publishedAt = readImportPublishedAt(frontmatter);
|
|
993
|
+
const banner = readOptionalFrontmatterString(frontmatter, "banner");
|
|
994
|
+
return {
|
|
995
|
+
slug,
|
|
996
|
+
type,
|
|
997
|
+
...(title === undefined ? {} : { title }),
|
|
998
|
+
...(excerpt === undefined ? {} : { excerpt }),
|
|
999
|
+
...(url === undefined ? {} : { url }),
|
|
1000
|
+
...(tags === undefined ? {} : { tags }),
|
|
1001
|
+
...(publishedAt === undefined ? {} : { publishedAt }),
|
|
1002
|
+
...(banner === undefined ? {} : { bannerPath: resolve(dirname(filePath), banner) }),
|
|
1003
|
+
content: markdown.slice(match[0].length),
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
function parseMarkdownFrontmatter(value) {
|
|
1007
|
+
const frontmatter = new Map();
|
|
1008
|
+
const lines = value.split(/\r?\n/u);
|
|
1009
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
1010
|
+
const line = lines[index];
|
|
1011
|
+
const match = /^([A-Za-z][A-Za-z0-9]*):(?:\s*(.*))?$/u.exec(line);
|
|
1012
|
+
if (match === null || match[1] === undefined)
|
|
1013
|
+
continue;
|
|
1014
|
+
const key = match[1];
|
|
1015
|
+
const rawValue = match[2]?.trim() ?? "";
|
|
1016
|
+
if (key === "tags" && rawValue === "") {
|
|
1017
|
+
const tags = [];
|
|
1018
|
+
while (/^\s+-\s+/u.test(lines[index + 1] ?? "")) {
|
|
1019
|
+
index += 1;
|
|
1020
|
+
tags.push(parseFrontmatterScalar((lines[index] ?? "").replace(/^\s+-\s+/u, "")));
|
|
1021
|
+
}
|
|
1022
|
+
frontmatter.set(key, tags);
|
|
1023
|
+
}
|
|
1024
|
+
else {
|
|
1025
|
+
frontmatter.set(key, key === "tags" ? parseFrontmatterTags(rawValue) : parseFrontmatterScalar(rawValue));
|
|
1026
|
+
}
|
|
892
1027
|
}
|
|
1028
|
+
return frontmatter;
|
|
1029
|
+
}
|
|
1030
|
+
function parseFrontmatterScalar(value) {
|
|
1031
|
+
if (value.startsWith('"') && value.endsWith('"')) {
|
|
1032
|
+
try {
|
|
1033
|
+
const parsed = JSON.parse(value);
|
|
1034
|
+
if (typeof parsed === "string")
|
|
1035
|
+
return parsed;
|
|
1036
|
+
}
|
|
1037
|
+
catch {
|
|
1038
|
+
// Report the unparsed value below so the API can validate it where applicable.
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
if (value.startsWith("'") && value.endsWith("'")) {
|
|
1042
|
+
return value.slice(1, -1).replace(/''/gu, "'");
|
|
1043
|
+
}
|
|
1044
|
+
return value;
|
|
1045
|
+
}
|
|
1046
|
+
function parseFrontmatterTags(value) {
|
|
1047
|
+
const trimmed = value.trim();
|
|
1048
|
+
const items = trimmed.startsWith("[") && trimmed.endsWith("]")
|
|
1049
|
+
? trimmed.slice(1, -1).split(",")
|
|
1050
|
+
: trimmed.split(",");
|
|
1051
|
+
return items.map((item) => parseFrontmatterScalar(item.trim())).filter((item) => item !== "");
|
|
1052
|
+
}
|
|
1053
|
+
function readRequiredFrontmatterString(frontmatter, key) {
|
|
1054
|
+
const value = readOptionalFrontmatterString(frontmatter, key);
|
|
1055
|
+
if (value === undefined)
|
|
1056
|
+
throw createInvalidUsageError(`Markdown frontmatter requires ${key}`);
|
|
1057
|
+
return value;
|
|
1058
|
+
}
|
|
1059
|
+
function readOptionalFrontmatterString(frontmatter, key) {
|
|
1060
|
+
const value = frontmatter.get(key);
|
|
1061
|
+
return typeof value === "string" && value.trim() !== "" ? value : undefined;
|
|
1062
|
+
}
|
|
1063
|
+
function readFrontmatterTags(frontmatter) {
|
|
1064
|
+
const value = frontmatter.get("tags");
|
|
1065
|
+
return Array.isArray(value) ? value : undefined;
|
|
1066
|
+
}
|
|
1067
|
+
function readImportPublishedAt(frontmatter) {
|
|
1068
|
+
const publishedAt = readOptionalFrontmatterString(frontmatter, "publishedAt") ??
|
|
1069
|
+
readOptionalFrontmatterString(frontmatter, "date");
|
|
1070
|
+
return publishedAt === undefined ? undefined : normalizeCliPublishedAt(publishedAt);
|
|
1071
|
+
}
|
|
1072
|
+
async function uploadPostImportBanner(api, post, bannerPath) {
|
|
1073
|
+
const file = await readMediaUploadFile(bannerPath);
|
|
1074
|
+
const fileBody = file.body.buffer.slice(file.body.byteOffset, file.body.byteOffset + file.body.byteLength);
|
|
1075
|
+
const body = new FormData();
|
|
1076
|
+
body.set("file", new File([fileBody], file.filename, { type: file.mimeType }));
|
|
1077
|
+
body.set("altText", `${post.title ?? post.slug} banner`);
|
|
1078
|
+
body.set("key", `posts/${post.slug}/banner${extname(file.filename).toLowerCase()}`);
|
|
1079
|
+
const response = await api.client.requestJson({
|
|
1080
|
+
method: "POST",
|
|
1081
|
+
path: "/media",
|
|
1082
|
+
body,
|
|
1083
|
+
});
|
|
1084
|
+
return response.data.url;
|
|
1085
|
+
}
|
|
1086
|
+
function createBannerContent(post, bannerUrl) {
|
|
1087
|
+
return `\n\n${post.content}`;
|
|
1088
|
+
}
|
|
1089
|
+
async function findImportedPost(client, slug) {
|
|
1090
|
+
try {
|
|
1091
|
+
return (await client.requestJson({ path: `/posts/${encodeURIComponent(slug)}` }))
|
|
1092
|
+
.data;
|
|
1093
|
+
}
|
|
1094
|
+
catch (error) {
|
|
1095
|
+
if (error instanceof CliError && error.status === 404)
|
|
1096
|
+
return undefined;
|
|
1097
|
+
throw error;
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
function withoutPostType(input) {
|
|
1101
|
+
const { type, ...update } = input;
|
|
1102
|
+
void type;
|
|
1103
|
+
return update;
|
|
893
1104
|
}
|
|
894
1105
|
async function runPostsListCommand(context, args) {
|
|
895
1106
|
const parsed = parsePostFlags(args, ["type", "status", "tag"]);
|
package/dist/errors.d.ts
CHANGED
|
@@ -7,7 +7,8 @@ export declare const ExitCode: {
|
|
|
7
7
|
};
|
|
8
8
|
export type ExitCode = (typeof ExitCode)[keyof typeof ExitCode];
|
|
9
9
|
export declare class CliError extends Error {
|
|
10
|
+
readonly status?: number | undefined;
|
|
10
11
|
readonly exitCode: ExitCode;
|
|
11
|
-
constructor(message: string, exitCode: ExitCode);
|
|
12
|
+
constructor(message: string, exitCode: ExitCode, status?: number | undefined);
|
|
12
13
|
}
|
|
13
14
|
export declare function createInvalidUsageError(message: string): CliError;
|
package/dist/errors.js
CHANGED
|
@@ -6,9 +6,11 @@ export const ExitCode = {
|
|
|
6
6
|
ApiError: 12,
|
|
7
7
|
};
|
|
8
8
|
export class CliError extends Error {
|
|
9
|
+
status;
|
|
9
10
|
exitCode;
|
|
10
|
-
constructor(message, exitCode) {
|
|
11
|
+
constructor(message, exitCode, status) {
|
|
11
12
|
super(message);
|
|
13
|
+
this.status = status;
|
|
12
14
|
this.name = "CliError";
|
|
13
15
|
this.exitCode = exitCode;
|
|
14
16
|
}
|
package/dist/http.js
CHANGED
|
@@ -25,10 +25,10 @@ export class SlugHttpClient {
|
|
|
25
25
|
body: createRequestBody(options.body),
|
|
26
26
|
});
|
|
27
27
|
if (response.status === 401 || response.status === 403) {
|
|
28
|
-
throw new CliError(await readApiErrorMessage(response, "Authentication failed"), ExitCode.AuthenticationError);
|
|
28
|
+
throw new CliError(await readApiErrorMessage(response, "Authentication failed"), ExitCode.AuthenticationError, response.status);
|
|
29
29
|
}
|
|
30
30
|
if (!response.ok) {
|
|
31
|
-
throw new CliError(await readApiErrorMessage(response, `API request failed with status ${response.status}`), ExitCode.ApiError);
|
|
31
|
+
throw new CliError(await readApiErrorMessage(response, `API request failed with status ${response.status}`), ExitCode.ApiError, response.status);
|
|
32
32
|
}
|
|
33
33
|
return response;
|
|
34
34
|
}
|