@sandboxsonny/ace-kit 0.6.0 → 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/CHANGELOG.md +7 -0
- package/dist/astro-integration.js +1 -1
- package/dist/{chunk-G5FGBOFT.js → chunk-T7DKI2U3.js} +4 -3
- package/dist/cli.js +2 -2
- package/dist/{environment-T5K5ZJ2M.js → environment-J4VYOHDK.js} +2 -1
- package/dist/{main-YRQQWLHX.js → main-AGLPUGNN.js} +475 -196
- package/dist/react-router-server.d.ts +1 -1
- package/dist/vite.js +1 -1
- package/docs/command-line.md +32 -7
- package/docs/react-router.md +32 -3
- package/docs/react.md +5 -2
- package/package.json +1 -1
- package/templates/react-router/app/ace-kit.generated.d.ts +7 -1
- package/templates/react-router/app/app.css +85 -0
- package/templates/react-router/app/components/page-content.tsx +47 -24
- package/templates/react-router/app/components/sections/faq.schema.ts +17 -0
- package/templates/react-router/app/components/sections/faq.tsx +26 -0
- package/templates/react-router/app/components/sections/gallery.schema.ts +20 -0
- package/templates/react-router/app/components/sections/gallery.tsx +39 -0
- package/templates/react-router/app/components/sections/hero.schema.ts +14 -0
- package/templates/react-router/app/components/sections/hero.tsx +25 -0
- package/templates/react-router/app/routes/page.test.tsx +54 -0
- package/templates/react-router/app/routes.ts +7 -3
- package/templates/react-router.section-types.json +109 -0
|
@@ -182,7 +182,7 @@ var richText = (text) => ({
|
|
|
182
182
|
type: "doc",
|
|
183
183
|
content: text.split("\n\n").map((paragraph) => ({ type: "paragraph", content: [{ type: "text", text: paragraph }] }))
|
|
184
184
|
});
|
|
185
|
-
var frameworksWithSections = ["astro"];
|
|
185
|
+
var frameworksWithSections = ["astro", "react-router"];
|
|
186
186
|
function pageFields(framework) {
|
|
187
187
|
const fields = [
|
|
188
188
|
{ componentKey: "text", label: "Headline", handle: "headline", config: { required: true } },
|
|
@@ -822,10 +822,467 @@ Next:
|
|
|
822
822
|
return seeded.kind === "failed" ? 1 : 0;
|
|
823
823
|
}
|
|
824
824
|
|
|
825
|
+
// src/cli/doctor-command.ts
|
|
826
|
+
import { readFile as readFile3 } from "fs/promises";
|
|
827
|
+
import { join as join4 } from "path";
|
|
828
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
829
|
+
|
|
830
|
+
// src/sections/find-schemas.ts
|
|
831
|
+
import { readdir as readdir2 } from "fs/promises";
|
|
832
|
+
import { join as join3, relative as relative2 } from "path";
|
|
833
|
+
var schemaFilePattern = /\.schema\.(?:ts|mts|js|mjs)$/;
|
|
834
|
+
var skippedFolders = /* @__PURE__ */ new Set([
|
|
835
|
+
"node_modules",
|
|
836
|
+
".git",
|
|
837
|
+
"dist",
|
|
838
|
+
"build",
|
|
839
|
+
".astro",
|
|
840
|
+
".react-router",
|
|
841
|
+
".output",
|
|
842
|
+
"coverage"
|
|
843
|
+
]);
|
|
844
|
+
var defaultSchemaFolders = ["src", "app"];
|
|
845
|
+
async function findSchemaFiles(root, folder) {
|
|
846
|
+
const folders = folder === void 0 ? defaultSchemaFolders : [folder];
|
|
847
|
+
const found = [];
|
|
848
|
+
async function walk(directory) {
|
|
849
|
+
let entries;
|
|
850
|
+
try {
|
|
851
|
+
entries = await readdir2(directory, { withFileTypes: true });
|
|
852
|
+
} catch {
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
for (const entry of entries) {
|
|
856
|
+
const path = join3(directory, entry.name);
|
|
857
|
+
if (entry.isDirectory()) {
|
|
858
|
+
if (!skippedFolders.has(entry.name)) await walk(path);
|
|
859
|
+
} else if (entry.isFile() && schemaFilePattern.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
860
|
+
found.push(path);
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
for (const name of folders) await walk(join3(root, name));
|
|
865
|
+
return found.sort((first, second) => {
|
|
866
|
+
const [firstName, secondName] = [relative2(root, first), relative2(root, second)];
|
|
867
|
+
return firstName < secondName ? -1 : firstName > secondName ? 1 : 0;
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/sections/load-schemas.ts
|
|
872
|
+
import { relative as relative3 } from "path";
|
|
873
|
+
|
|
874
|
+
// src/sections/payload.ts
|
|
875
|
+
var handlePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
876
|
+
var maximumHandleLength = 64;
|
|
877
|
+
var maximumNameLength = 200;
|
|
878
|
+
var fieldTypes = [
|
|
879
|
+
"text",
|
|
880
|
+
"textarea",
|
|
881
|
+
"number",
|
|
882
|
+
"checkbox",
|
|
883
|
+
"select",
|
|
884
|
+
"image",
|
|
885
|
+
"richtext",
|
|
886
|
+
"date",
|
|
887
|
+
"link"
|
|
888
|
+
];
|
|
889
|
+
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
890
|
+
var isHandle = (value) => typeof value === "string" && value.length <= maximumHandleLength && handlePattern.test(value);
|
|
891
|
+
var handleRule = 'lowercase letters, numbers and hyphens (such as "call-to-action")';
|
|
892
|
+
function checkFields(fields, where, problems) {
|
|
893
|
+
if (fields === void 0) return;
|
|
894
|
+
if (!Array.isArray(fields)) {
|
|
895
|
+
problems.push(`${where}: fields must be a list.`);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const seen = /* @__PURE__ */ new Set();
|
|
899
|
+
fields.forEach((field, index) => {
|
|
900
|
+
if (!isObject(field)) {
|
|
901
|
+
problems.push(`${where}: field ${index + 1} must be an object with a handle, a label and a type.`);
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
904
|
+
const name = isHandle(field.handle) ? `field "${field.handle}"` : `field ${index + 1}`;
|
|
905
|
+
if (!isHandle(field.handle)) problems.push(`${where}: ${name} needs a handle of ${handleRule}.`);
|
|
906
|
+
else if (seen.has(field.handle)) problems.push(`${where}: ${name} is defined twice.`);
|
|
907
|
+
else seen.add(field.handle);
|
|
908
|
+
if (typeof field.label !== "string" || field.label.trim() === "" || field.label.length > maximumNameLength) {
|
|
909
|
+
problems.push(`${where}: ${name} needs a label.`);
|
|
910
|
+
}
|
|
911
|
+
if (typeof field.type !== "string" || !fieldTypes.includes(field.type)) {
|
|
912
|
+
const given = typeof field.type === "string" ? ` "${field.type}"` : "";
|
|
913
|
+
problems.push(`${where}: ${name} has a type${given} that isn't one of ${fieldTypes.join(", ")}.`);
|
|
914
|
+
} else if (field.type === "select") {
|
|
915
|
+
const { options } = field;
|
|
916
|
+
if (!Array.isArray(options) || options.length === 0 || options.some((option) => typeof option !== "string" || option === "")) {
|
|
917
|
+
problems.push(`${where}: ${name} is a select, so it needs options: a list of the choices, as text.`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (field.required !== void 0 && typeof field.required !== "boolean") {
|
|
921
|
+
problems.push(`${where}: ${name} has a required that isn't true or false.`);
|
|
922
|
+
}
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
function checkSectionDefinition(definition, where = "this section") {
|
|
926
|
+
const problems = [];
|
|
927
|
+
if (!isObject(definition)) {
|
|
928
|
+
return [`${where}: the default export must be the result of defineSection({ ... }).`];
|
|
929
|
+
}
|
|
930
|
+
if (!isHandle(definition.handle)) {
|
|
931
|
+
problems.push(`${where}: needs a handle of ${handleRule}.`);
|
|
932
|
+
}
|
|
933
|
+
if (typeof definition.name !== "string" || definition.name.trim() === "" || definition.name.length > maximumNameLength) {
|
|
934
|
+
problems.push(`${where}: needs a name.`);
|
|
935
|
+
}
|
|
936
|
+
checkFields(definition.fields, where, problems);
|
|
937
|
+
if (definition.blocks !== void 0) {
|
|
938
|
+
if (!isObject(definition.blocks)) {
|
|
939
|
+
problems.push(`${where}: blocks must be an object of the kinds of block, by handle.`);
|
|
940
|
+
} else {
|
|
941
|
+
for (const [handle, block] of Object.entries(definition.blocks)) {
|
|
942
|
+
const blockWhere = `${where}, block "${handle}"`;
|
|
943
|
+
if (!isHandle(handle)) problems.push(`${blockWhere}: the handle must be ${handleRule}.`);
|
|
944
|
+
if (!isObject(block)) {
|
|
945
|
+
problems.push(`${blockWhere}: must be an object with a name and fields.`);
|
|
946
|
+
continue;
|
|
947
|
+
}
|
|
948
|
+
if (typeof block.name !== "string" || block.name.trim() === "") problems.push(`${blockWhere}: needs a name.`);
|
|
949
|
+
checkFields(block.fields, blockWhere, problems);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
return problems;
|
|
954
|
+
}
|
|
955
|
+
function apiFields(fields) {
|
|
956
|
+
return (fields ?? []).map((field) => ({
|
|
957
|
+
handle: field.handle,
|
|
958
|
+
label: field.label,
|
|
959
|
+
component_key: field.type,
|
|
960
|
+
config: {
|
|
961
|
+
...field.required ? { required: true } : {},
|
|
962
|
+
...field.type === "select" ? { options: [...field.options] } : {}
|
|
963
|
+
}
|
|
964
|
+
}));
|
|
965
|
+
}
|
|
966
|
+
function sectionTypesPayload(definitions) {
|
|
967
|
+
return {
|
|
968
|
+
sections: definitions.map((definition) => ({
|
|
969
|
+
handle: definition.handle,
|
|
970
|
+
name: definition.name,
|
|
971
|
+
fields: apiFields(definition.fields),
|
|
972
|
+
blocks: Object.entries(definition.blocks ?? {}).map(([handle, block]) => ({
|
|
973
|
+
handle,
|
|
974
|
+
name: block.name,
|
|
975
|
+
fields: apiFields(block.fields)
|
|
976
|
+
}))
|
|
977
|
+
}))
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
// src/sections/load-schemas.ts
|
|
982
|
+
async function loadSchemas(files, root, importModule) {
|
|
983
|
+
const definitions = [];
|
|
984
|
+
const problems = [];
|
|
985
|
+
const handleToFile = /* @__PURE__ */ new Map();
|
|
986
|
+
for (const file of files) {
|
|
987
|
+
const name = relative3(root, file);
|
|
988
|
+
let exported;
|
|
989
|
+
try {
|
|
990
|
+
const module = await importModule(file);
|
|
991
|
+
exported = module.default;
|
|
992
|
+
} catch (error) {
|
|
993
|
+
problems.push(`${name}: could not be loaded: ${error instanceof Error ? error.message : String(error)}`);
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
const fileProblems = checkSectionDefinition(exported, name);
|
|
997
|
+
if (fileProblems.length > 0) {
|
|
998
|
+
problems.push(...fileProblems);
|
|
999
|
+
continue;
|
|
1000
|
+
}
|
|
1001
|
+
const definition = exported;
|
|
1002
|
+
const earlier = handleToFile.get(definition.handle);
|
|
1003
|
+
if (earlier) {
|
|
1004
|
+
problems.push(`${name}: the handle "${definition.handle}" is already used by ${earlier}.`);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
handleToFile.set(definition.handle, name);
|
|
1008
|
+
definitions.push({ file, definition });
|
|
1009
|
+
}
|
|
1010
|
+
return { definitions, problems };
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
// src/cli/doctor-command.ts
|
|
1014
|
+
var doctorUsage = `ace-kit doctor: check that this site is set up to work with CMS Ace.
|
|
1015
|
+
|
|
1016
|
+
Usage:
|
|
1017
|
+
ace-kit doctor [--site <domain>] [--api-url <address>] [--dir <folder>]
|
|
1018
|
+
|
|
1019
|
+
Run it in your site's folder. It changes nothing and never prints your login. It checks your Node version, that Ace Kit
|
|
1020
|
+
is installed, which CMS and site the settings point at, that the CMS can be reached and knows the site, that you are
|
|
1021
|
+
signed in, that the site has its Pages content, and that the section types in your code match the CMS. Each problem
|
|
1022
|
+
says what to do. It exits with 1 if anything is wrong.
|
|
1023
|
+
|
|
1024
|
+
Options:
|
|
1025
|
+
--site The domain your site has in the CMS (default: ACE_SITE_DOMAIN, else Astro's \`site\` setting).
|
|
1026
|
+
--api-url Where the CMS Ace API lives (default: ACE_API_URL, else the hosted CMS Ace).
|
|
1027
|
+
--dir The folder holding your *.schema.ts files (default: src and app).
|
|
1028
|
+
--help Show this message.
|
|
1029
|
+
`;
|
|
1030
|
+
var symbols = { ok: "\u2713", warn: "!", fail: "\u2717", info: "-" };
|
|
1031
|
+
var requestTimeoutMilliseconds4 = 15e3;
|
|
1032
|
+
var plain = (text) => String(text).replace(/\p{Cc}/gu, "").slice(0, 200);
|
|
1033
|
+
var isOlderThan = (version, major, minor) => {
|
|
1034
|
+
const [actualMajor = 0, actualMinor = 0] = version.split(".").map(Number);
|
|
1035
|
+
return actualMajor < major || actualMajor === major && actualMinor < minor;
|
|
1036
|
+
};
|
|
1037
|
+
async function readText(path) {
|
|
1038
|
+
try {
|
|
1039
|
+
return await readFile3(path, "utf8");
|
|
1040
|
+
} catch {
|
|
1041
|
+
return null;
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
async function domainFromAstroConfig(root) {
|
|
1045
|
+
for (const name of ["astro.config.mjs", "astro.config.ts", "astro.config.js", "astro.config.mts"]) {
|
|
1046
|
+
const text = await readText(join4(root, name));
|
|
1047
|
+
const found = text?.match(/\bsite\s*:\s*["']([^"']+)["']/);
|
|
1048
|
+
if (found?.[1]) return domainFromSite(found[1]);
|
|
1049
|
+
}
|
|
1050
|
+
return null;
|
|
1051
|
+
}
|
|
1052
|
+
async function doctorCommand(commandArguments, environment) {
|
|
1053
|
+
const { values } = parseArgs3({
|
|
1054
|
+
// eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
|
|
1055
|
+
args: commandArguments,
|
|
1056
|
+
options: {
|
|
1057
|
+
"api-url": { type: "string" },
|
|
1058
|
+
site: { type: "string" },
|
|
1059
|
+
dir: { type: "string" },
|
|
1060
|
+
help: { type: "boolean" }
|
|
1061
|
+
}
|
|
1062
|
+
});
|
|
1063
|
+
if (values.help) {
|
|
1064
|
+
environment.out(doctorUsage);
|
|
1065
|
+
return 0;
|
|
1066
|
+
}
|
|
1067
|
+
const checks = [];
|
|
1068
|
+
const add = (status, label, hint) => checks.push({ status, label, hint });
|
|
1069
|
+
const root = environment.workingDirectory;
|
|
1070
|
+
if (isOlderThan(environment.nodeVersion, 22, 12))
|
|
1071
|
+
add(
|
|
1072
|
+
"fail",
|
|
1073
|
+
`Node ${environment.nodeVersion} is too old`,
|
|
1074
|
+
"Ace Kit needs Node 22.12 or newer. If you use mise, nvm or fnm, check which version it picks in this folder."
|
|
1075
|
+
);
|
|
1076
|
+
else add("ok", `Node ${environment.nodeVersion}`);
|
|
1077
|
+
const manifestText = await readText(join4(root, "package.json"));
|
|
1078
|
+
let framework = null;
|
|
1079
|
+
if (manifestText === null) {
|
|
1080
|
+
add("fail", "No package.json in this folder", "Run this in your site's folder.");
|
|
1081
|
+
} else {
|
|
1082
|
+
let manifest = {};
|
|
1083
|
+
try {
|
|
1084
|
+
manifest = JSON.parse(manifestText);
|
|
1085
|
+
} catch {
|
|
1086
|
+
add("fail", "package.json isn't valid JSON");
|
|
1087
|
+
}
|
|
1088
|
+
const dependencies = { ...manifest.devDependencies, ...manifest.dependencies };
|
|
1089
|
+
if ("astro" in dependencies) framework = "astro";
|
|
1090
|
+
else if ("react-router" in dependencies) framework = "react-router";
|
|
1091
|
+
add(
|
|
1092
|
+
framework ? "ok" : "info",
|
|
1093
|
+
framework ? framework === "astro" ? "An Astro site" : "A React Router site" : "Not an Astro or React Router site"
|
|
1094
|
+
);
|
|
1095
|
+
const installed = await readText(join4(root, "node_modules", "@sandboxsonny", "ace-kit", "package.json"));
|
|
1096
|
+
if (!("@sandboxsonny/ace-kit" in dependencies)) {
|
|
1097
|
+
add("fail", "Ace Kit isn't a dependency of this site", "Run: npm install @sandboxsonny/ace-kit");
|
|
1098
|
+
} else if (installed === null) {
|
|
1099
|
+
add("fail", "Ace Kit isn't installed", "Run: npm install");
|
|
1100
|
+
} else {
|
|
1101
|
+
let version = "unknown";
|
|
1102
|
+
try {
|
|
1103
|
+
version = String(JSON.parse(installed).version ?? "unknown");
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
add("ok", `Ace Kit ${version} is installed`);
|
|
1107
|
+
if (version !== environment.version && environment.version !== "development")
|
|
1108
|
+
add("info", `This command is Ace Kit ${environment.version}; the site uses ${version}`);
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
let apiUrl = null;
|
|
1112
|
+
try {
|
|
1113
|
+
apiUrl = normaliseApiUrl(values["api-url"] ?? environment.variables.ACE_API_URL ?? hostedApiUrl);
|
|
1114
|
+
add("ok", `CMS: ${apiUrl}${apiUrl === hostedApiUrl ? " (the hosted CMS Ace)" : ""}`);
|
|
1115
|
+
} catch (error) {
|
|
1116
|
+
add("fail", `The CMS address is wrong: ${error instanceof Error ? error.message : String(error)}`);
|
|
1117
|
+
}
|
|
1118
|
+
const given = values.site ?? environment.variables.ACE_SITE_DOMAIN;
|
|
1119
|
+
const domain = given ? domainFromSite(given) : framework === "astro" ? await domainFromAstroConfig(root) : null;
|
|
1120
|
+
if (domain) add("ok", `Site: ${domain}`);
|
|
1121
|
+
else
|
|
1122
|
+
add(
|
|
1123
|
+
"fail",
|
|
1124
|
+
"Can't tell which site this is",
|
|
1125
|
+
framework === "astro" ? 'Set `site: "https://your-domain.com"` in astro.config.mjs, or ACE_SITE_DOMAIN in .env.' : "Set ACE_SITE_DOMAIN=your-domain.com in .env."
|
|
1126
|
+
);
|
|
1127
|
+
if (apiUrl) await checkTheCms(apiUrl, domain);
|
|
1128
|
+
return finish();
|
|
1129
|
+
async function request(path, headers = {}) {
|
|
1130
|
+
try {
|
|
1131
|
+
const response = await environment.fetchImplementation(`${apiUrl}${path}`, {
|
|
1132
|
+
headers: { Accept: "application/json", ...headers },
|
|
1133
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds4)
|
|
1134
|
+
});
|
|
1135
|
+
return { status: response.status, body: await response.json().catch(() => null) };
|
|
1136
|
+
} catch {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
async function checkTheCms(address, siteDomain) {
|
|
1141
|
+
const health = await request("/health");
|
|
1142
|
+
if (!health || health.status >= 500 || health.status === 404) {
|
|
1143
|
+
add(
|
|
1144
|
+
"fail",
|
|
1145
|
+
`Can't reach the CMS at ${address}`,
|
|
1146
|
+
isLocalApiUrl(address) ? "Is your local CMS running? (docker compose -p cms-backend up -d)" : "Check the address and your connection."
|
|
1147
|
+
);
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
add("ok", "The CMS answers");
|
|
1151
|
+
if (!siteDomain) return;
|
|
1152
|
+
const asSite = { "X-Ace-Site-Domain": siteDomain };
|
|
1153
|
+
const site = await request("/public/site", asSite);
|
|
1154
|
+
if (site?.status === 404) {
|
|
1155
|
+
add(
|
|
1156
|
+
"fail",
|
|
1157
|
+
`The CMS has no site with the domain ${siteDomain}`,
|
|
1158
|
+
"Make it with `npx @sandboxsonny/ace-kit create`, or in the admin app, or fix the domain."
|
|
1159
|
+
);
|
|
1160
|
+
} else if (site?.status === 200) {
|
|
1161
|
+
add("ok", `The CMS knows the site: "${plain(site.body?.name)}"`);
|
|
1162
|
+
await checkContent(asSite);
|
|
1163
|
+
} else {
|
|
1164
|
+
add("fail", `The CMS answered with an error (${site?.status ?? "no answer"}) when asked about the site`);
|
|
1165
|
+
}
|
|
1166
|
+
await checkLogin(address, siteDomain);
|
|
1167
|
+
}
|
|
1168
|
+
async function checkContent(asSite) {
|
|
1169
|
+
const types = await request("/public/content-types", asSite);
|
|
1170
|
+
const typeList = Array.isArray(types?.body) ? types.body : [];
|
|
1171
|
+
if (!typeList.some((type) => type.handle === "pages")) {
|
|
1172
|
+
add(
|
|
1173
|
+
"warn",
|
|
1174
|
+
"The site has no Pages content type",
|
|
1175
|
+
"The starter's pages read one. Add it in the admin app (Content types)."
|
|
1176
|
+
);
|
|
1177
|
+
return;
|
|
1178
|
+
}
|
|
1179
|
+
const entries = await request("/public/entries?content_type=pages&limit=100", asSite);
|
|
1180
|
+
const list = Array.isArray(entries?.body) ? entries.body : [];
|
|
1181
|
+
if (list.some((entry) => entry.handle === "home"))
|
|
1182
|
+
add("ok", `Pages has ${list.length} published ${list.length === 1 ? "entry" : "entries"}, including home`);
|
|
1183
|
+
else
|
|
1184
|
+
add(
|
|
1185
|
+
"warn",
|
|
1186
|
+
"Pages has no published entry called home",
|
|
1187
|
+
"The site's front page (/) reads it. Add one in the admin app and publish it."
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
async function checkLogin(address, siteDomain) {
|
|
1191
|
+
const login = await resolveLogin({
|
|
1192
|
+
apiUrl: address,
|
|
1193
|
+
variables: environment.variables,
|
|
1194
|
+
store: environment.store,
|
|
1195
|
+
now: environment.now
|
|
1196
|
+
});
|
|
1197
|
+
if (!login) {
|
|
1198
|
+
add(
|
|
1199
|
+
"warn",
|
|
1200
|
+
"Not signed in",
|
|
1201
|
+
`Run: npx ace-kit login${isLocalApiUrl(address) ? ` --api-url ${address}` : ""}. Section sync and dev sites need it.`
|
|
1202
|
+
);
|
|
1203
|
+
return;
|
|
1204
|
+
}
|
|
1205
|
+
const authorised = { Authorization: `Bearer ${login.token}` };
|
|
1206
|
+
const me = await request("/auth/me", authorised);
|
|
1207
|
+
if (me?.status === 401) {
|
|
1208
|
+
add("warn", "Your login has ended", "Run: npx ace-kit login");
|
|
1209
|
+
return;
|
|
1210
|
+
}
|
|
1211
|
+
if (me?.status !== 200) {
|
|
1212
|
+
add("warn", `Couldn't check your login (${me?.status ?? "no answer"})`);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
add("ok", `Signed in as ${plain(me.body?.email)}`);
|
|
1216
|
+
const sites = await request("/sites", authorised);
|
|
1217
|
+
const visible = Array.isArray(sites?.body) ? sites.body : [];
|
|
1218
|
+
const mine = visible.find((candidate) => candidate.kind !== "dev" && candidate.domain.toLowerCase() === siteDomain);
|
|
1219
|
+
if (!mine) {
|
|
1220
|
+
add("info", "This login can't see that site, so its section types and preview address weren't checked");
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (mine.preview_url) add("ok", `Preview address: ${plain(mine.preview_url)}`);
|
|
1224
|
+
else if (isLocalApiUrl(address))
|
|
1225
|
+
add(
|
|
1226
|
+
"warn",
|
|
1227
|
+
"The site has no Preview address",
|
|
1228
|
+
"Set it in the site's Settings (for example http://localhost:4321) so the customiser can show your site."
|
|
1229
|
+
);
|
|
1230
|
+
await checkSections(address, mine.id, authorised);
|
|
1231
|
+
}
|
|
1232
|
+
async function checkSections(address, siteId, authorised) {
|
|
1233
|
+
const files = await findSchemaFiles(root, values.dir);
|
|
1234
|
+
if (files.length === 0) {
|
|
1235
|
+
add("info", "No section schemas (*.schema.ts) in your code");
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
const loaded = await loadSchemas(files, root, environment.importModule);
|
|
1239
|
+
if (loaded.problems.length > 0) {
|
|
1240
|
+
for (const problem of loaded.problems) add("fail", `Section schema problem: ${plain(problem)}`);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
const inCode = loaded.definitions.map(({ definition }) => definition.handle);
|
|
1244
|
+
add("ok", `${inCode.length} section type${inCode.length === 1 ? "" : "s"} in your code (${inCode.join(", ")})`);
|
|
1245
|
+
const cms = await request(`/sites/${siteId}/section-types`, authorised);
|
|
1246
|
+
if (!Array.isArray(cms?.body)) return;
|
|
1247
|
+
const active = new Set(
|
|
1248
|
+
cms.body.filter((type) => !type.retired_at).map((type) => type.handle)
|
|
1249
|
+
);
|
|
1250
|
+
const missing = inCode.filter((handle) => !active.has(handle));
|
|
1251
|
+
const unused = [...active].filter((handle) => !inCode.includes(handle));
|
|
1252
|
+
if (missing.length > 0)
|
|
1253
|
+
add(
|
|
1254
|
+
"warn",
|
|
1255
|
+
`Not in the CMS yet: ${missing.join(", ")}`,
|
|
1256
|
+
`Run: npx ace-kit sync${isLocalApiUrl(address) ? ` --api-url ${address}` : ""} (the dev server does this on save)`
|
|
1257
|
+
);
|
|
1258
|
+
else add("ok", "The CMS has all of them");
|
|
1259
|
+
if (unused.length > 0)
|
|
1260
|
+
add(
|
|
1261
|
+
"info",
|
|
1262
|
+
`In the CMS but not in your code: ${unused.join(", ")} (a sync would retire ${unused.length === 1 ? "it" : "them"})`
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
function finish() {
|
|
1266
|
+
environment.out("ace-kit doctor\n");
|
|
1267
|
+
for (const check of checks) {
|
|
1268
|
+
environment.out(` ${symbols[check.status]} ${check.label}`);
|
|
1269
|
+
if (check.hint) environment.out(` ${check.hint}`);
|
|
1270
|
+
}
|
|
1271
|
+
const failed = checks.filter((check) => check.status === "fail").length;
|
|
1272
|
+
const warned = checks.filter((check) => check.status === "warn").length;
|
|
1273
|
+
environment.out(
|
|
1274
|
+
failed > 0 ? `
|
|
1275
|
+
${failed} problem${failed === 1 ? "" : "s"} to fix${warned > 0 ? `, and ${warned} warning${warned === 1 ? "" : "s"}` : ""}.` : warned > 0 ? `
|
|
1276
|
+
Nothing is broken; ${warned} warning${warned === 1 ? "" : "s"}.` : "\nAll good."
|
|
1277
|
+
);
|
|
1278
|
+
return failed > 0 ? 1 : 0;
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
|
|
825
1282
|
// src/cli/generate-command.ts
|
|
826
1283
|
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
827
1284
|
import { dirname as dirname2, resolve as resolve3 } from "path";
|
|
828
|
-
import { parseArgs as
|
|
1285
|
+
import { parseArgs as parseArgs4 } from "util";
|
|
829
1286
|
|
|
830
1287
|
// src/client/errors.ts
|
|
831
1288
|
var AcePreviewExpiredError = class extends Error {
|
|
@@ -842,7 +1299,7 @@ var AceUnavailableError = class extends Error {
|
|
|
842
1299
|
};
|
|
843
1300
|
|
|
844
1301
|
// src/client/create-client.ts
|
|
845
|
-
var
|
|
1302
|
+
var requestTimeoutMilliseconds5 = 5e3;
|
|
846
1303
|
var maximumCachedAnswers = 500;
|
|
847
1304
|
function createAceClient({
|
|
848
1305
|
apiUrl,
|
|
@@ -866,7 +1323,7 @@ function createAceClient({
|
|
|
866
1323
|
try {
|
|
867
1324
|
response = await fetchImplementation(`${apiUrl.replace(/\/+$/, "")}${path}`, {
|
|
868
1325
|
headers: { Accept: "application/json", "X-Ace-Site-Domain": siteDomain, "X-Forwarded-Host": siteDomain },
|
|
869
|
-
signal: AbortSignal.timeout(
|
|
1326
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds5)
|
|
870
1327
|
});
|
|
871
1328
|
} catch {
|
|
872
1329
|
throw new AceUnavailableError("Could not reach the CMS.");
|
|
@@ -994,7 +1451,7 @@ Options:
|
|
|
994
1451
|
--help Show this message.
|
|
995
1452
|
`;
|
|
996
1453
|
async function generateCommand(commandArguments, environment) {
|
|
997
|
-
const { values } =
|
|
1454
|
+
const { values } = parseArgs4({
|
|
998
1455
|
// eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
|
|
999
1456
|
args: commandArguments,
|
|
1000
1457
|
options: {
|
|
@@ -1033,192 +1490,10 @@ async function generateCommand(commandArguments, environment) {
|
|
|
1033
1490
|
}
|
|
1034
1491
|
|
|
1035
1492
|
// src/cli/sync-command.ts
|
|
1036
|
-
import { parseArgs as
|
|
1037
|
-
|
|
1038
|
-
// src/sections/find-schemas.ts
|
|
1039
|
-
import { readdir as readdir2 } from "fs/promises";
|
|
1040
|
-
import { join as join3, relative as relative2 } from "path";
|
|
1041
|
-
var schemaFilePattern = /\.schema\.(?:ts|mts|js|mjs)$/;
|
|
1042
|
-
var skippedFolders = /* @__PURE__ */ new Set([
|
|
1043
|
-
"node_modules",
|
|
1044
|
-
".git",
|
|
1045
|
-
"dist",
|
|
1046
|
-
"build",
|
|
1047
|
-
".astro",
|
|
1048
|
-
".react-router",
|
|
1049
|
-
".output",
|
|
1050
|
-
"coverage"
|
|
1051
|
-
]);
|
|
1052
|
-
async function findSchemaFiles(root, folder = "src") {
|
|
1053
|
-
const start = join3(root, folder);
|
|
1054
|
-
const found = [];
|
|
1055
|
-
async function walk(directory) {
|
|
1056
|
-
let entries;
|
|
1057
|
-
try {
|
|
1058
|
-
entries = await readdir2(directory, { withFileTypes: true });
|
|
1059
|
-
} catch {
|
|
1060
|
-
return;
|
|
1061
|
-
}
|
|
1062
|
-
for (const entry of entries) {
|
|
1063
|
-
const path = join3(directory, entry.name);
|
|
1064
|
-
if (entry.isDirectory()) {
|
|
1065
|
-
if (!skippedFolders.has(entry.name)) await walk(path);
|
|
1066
|
-
} else if (entry.isFile() && schemaFilePattern.test(entry.name) && !entry.name.endsWith(".d.ts")) {
|
|
1067
|
-
found.push(path);
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
}
|
|
1071
|
-
await walk(start);
|
|
1072
|
-
return found.sort((first, second) => {
|
|
1073
|
-
const [firstName, secondName] = [relative2(root, first), relative2(root, second)];
|
|
1074
|
-
return firstName < secondName ? -1 : firstName > secondName ? 1 : 0;
|
|
1075
|
-
});
|
|
1076
|
-
}
|
|
1077
|
-
|
|
1078
|
-
// src/sections/load-schemas.ts
|
|
1079
|
-
import { relative as relative3 } from "path";
|
|
1080
|
-
|
|
1081
|
-
// src/sections/payload.ts
|
|
1082
|
-
var handlePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
1083
|
-
var maximumHandleLength = 64;
|
|
1084
|
-
var maximumNameLength = 200;
|
|
1085
|
-
var fieldTypes = [
|
|
1086
|
-
"text",
|
|
1087
|
-
"textarea",
|
|
1088
|
-
"number",
|
|
1089
|
-
"checkbox",
|
|
1090
|
-
"select",
|
|
1091
|
-
"image",
|
|
1092
|
-
"richtext",
|
|
1093
|
-
"date",
|
|
1094
|
-
"link"
|
|
1095
|
-
];
|
|
1096
|
-
var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1097
|
-
var isHandle = (value) => typeof value === "string" && value.length <= maximumHandleLength && handlePattern.test(value);
|
|
1098
|
-
var handleRule = 'lowercase letters, numbers and hyphens (such as "call-to-action")';
|
|
1099
|
-
function checkFields(fields, where, problems) {
|
|
1100
|
-
if (fields === void 0) return;
|
|
1101
|
-
if (!Array.isArray(fields)) {
|
|
1102
|
-
problems.push(`${where}: fields must be a list.`);
|
|
1103
|
-
return;
|
|
1104
|
-
}
|
|
1105
|
-
const seen = /* @__PURE__ */ new Set();
|
|
1106
|
-
fields.forEach((field, index) => {
|
|
1107
|
-
if (!isObject(field)) {
|
|
1108
|
-
problems.push(`${where}: field ${index + 1} must be an object with a handle, a label and a type.`);
|
|
1109
|
-
return;
|
|
1110
|
-
}
|
|
1111
|
-
const name = isHandle(field.handle) ? `field "${field.handle}"` : `field ${index + 1}`;
|
|
1112
|
-
if (!isHandle(field.handle)) problems.push(`${where}: ${name} needs a handle of ${handleRule}.`);
|
|
1113
|
-
else if (seen.has(field.handle)) problems.push(`${where}: ${name} is defined twice.`);
|
|
1114
|
-
else seen.add(field.handle);
|
|
1115
|
-
if (typeof field.label !== "string" || field.label.trim() === "" || field.label.length > maximumNameLength) {
|
|
1116
|
-
problems.push(`${where}: ${name} needs a label.`);
|
|
1117
|
-
}
|
|
1118
|
-
if (typeof field.type !== "string" || !fieldTypes.includes(field.type)) {
|
|
1119
|
-
const given = typeof field.type === "string" ? ` "${field.type}"` : "";
|
|
1120
|
-
problems.push(`${where}: ${name} has a type${given} that isn't one of ${fieldTypes.join(", ")}.`);
|
|
1121
|
-
} else if (field.type === "select") {
|
|
1122
|
-
const { options } = field;
|
|
1123
|
-
if (!Array.isArray(options) || options.length === 0 || options.some((option) => typeof option !== "string" || option === "")) {
|
|
1124
|
-
problems.push(`${where}: ${name} is a select, so it needs options: a list of the choices, as text.`);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
if (field.required !== void 0 && typeof field.required !== "boolean") {
|
|
1128
|
-
problems.push(`${where}: ${name} has a required that isn't true or false.`);
|
|
1129
|
-
}
|
|
1130
|
-
});
|
|
1131
|
-
}
|
|
1132
|
-
function checkSectionDefinition(definition, where = "this section") {
|
|
1133
|
-
const problems = [];
|
|
1134
|
-
if (!isObject(definition)) {
|
|
1135
|
-
return [`${where}: the default export must be the result of defineSection({ ... }).`];
|
|
1136
|
-
}
|
|
1137
|
-
if (!isHandle(definition.handle)) {
|
|
1138
|
-
problems.push(`${where}: needs a handle of ${handleRule}.`);
|
|
1139
|
-
}
|
|
1140
|
-
if (typeof definition.name !== "string" || definition.name.trim() === "" || definition.name.length > maximumNameLength) {
|
|
1141
|
-
problems.push(`${where}: needs a name.`);
|
|
1142
|
-
}
|
|
1143
|
-
checkFields(definition.fields, where, problems);
|
|
1144
|
-
if (definition.blocks !== void 0) {
|
|
1145
|
-
if (!isObject(definition.blocks)) {
|
|
1146
|
-
problems.push(`${where}: blocks must be an object of the kinds of block, by handle.`);
|
|
1147
|
-
} else {
|
|
1148
|
-
for (const [handle, block] of Object.entries(definition.blocks)) {
|
|
1149
|
-
const blockWhere = `${where}, block "${handle}"`;
|
|
1150
|
-
if (!isHandle(handle)) problems.push(`${blockWhere}: the handle must be ${handleRule}.`);
|
|
1151
|
-
if (!isObject(block)) {
|
|
1152
|
-
problems.push(`${blockWhere}: must be an object with a name and fields.`);
|
|
1153
|
-
continue;
|
|
1154
|
-
}
|
|
1155
|
-
if (typeof block.name !== "string" || block.name.trim() === "") problems.push(`${blockWhere}: needs a name.`);
|
|
1156
|
-
checkFields(block.fields, blockWhere, problems);
|
|
1157
|
-
}
|
|
1158
|
-
}
|
|
1159
|
-
}
|
|
1160
|
-
return problems;
|
|
1161
|
-
}
|
|
1162
|
-
function apiFields(fields) {
|
|
1163
|
-
return (fields ?? []).map((field) => ({
|
|
1164
|
-
handle: field.handle,
|
|
1165
|
-
label: field.label,
|
|
1166
|
-
component_key: field.type,
|
|
1167
|
-
config: {
|
|
1168
|
-
...field.required ? { required: true } : {},
|
|
1169
|
-
...field.type === "select" ? { options: [...field.options] } : {}
|
|
1170
|
-
}
|
|
1171
|
-
}));
|
|
1172
|
-
}
|
|
1173
|
-
function sectionTypesPayload(definitions) {
|
|
1174
|
-
return {
|
|
1175
|
-
sections: definitions.map((definition) => ({
|
|
1176
|
-
handle: definition.handle,
|
|
1177
|
-
name: definition.name,
|
|
1178
|
-
fields: apiFields(definition.fields),
|
|
1179
|
-
blocks: Object.entries(definition.blocks ?? {}).map(([handle, block]) => ({
|
|
1180
|
-
handle,
|
|
1181
|
-
name: block.name,
|
|
1182
|
-
fields: apiFields(block.fields)
|
|
1183
|
-
}))
|
|
1184
|
-
}))
|
|
1185
|
-
};
|
|
1186
|
-
}
|
|
1187
|
-
|
|
1188
|
-
// src/sections/load-schemas.ts
|
|
1189
|
-
async function loadSchemas(files, root, importModule) {
|
|
1190
|
-
const definitions = [];
|
|
1191
|
-
const problems = [];
|
|
1192
|
-
const handleToFile = /* @__PURE__ */ new Map();
|
|
1193
|
-
for (const file of files) {
|
|
1194
|
-
const name = relative3(root, file);
|
|
1195
|
-
let exported;
|
|
1196
|
-
try {
|
|
1197
|
-
const module = await importModule(file);
|
|
1198
|
-
exported = module.default;
|
|
1199
|
-
} catch (error) {
|
|
1200
|
-
problems.push(`${name}: could not be loaded: ${error instanceof Error ? error.message : String(error)}`);
|
|
1201
|
-
continue;
|
|
1202
|
-
}
|
|
1203
|
-
const fileProblems = checkSectionDefinition(exported, name);
|
|
1204
|
-
if (fileProblems.length > 0) {
|
|
1205
|
-
problems.push(...fileProblems);
|
|
1206
|
-
continue;
|
|
1207
|
-
}
|
|
1208
|
-
const definition = exported;
|
|
1209
|
-
const earlier = handleToFile.get(definition.handle);
|
|
1210
|
-
if (earlier) {
|
|
1211
|
-
problems.push(`${name}: the handle "${definition.handle}" is already used by ${earlier}.`);
|
|
1212
|
-
continue;
|
|
1213
|
-
}
|
|
1214
|
-
handleToFile.set(definition.handle, name);
|
|
1215
|
-
definitions.push({ file, definition });
|
|
1216
|
-
}
|
|
1217
|
-
return { definitions, problems };
|
|
1218
|
-
}
|
|
1493
|
+
import { parseArgs as parseArgs5 } from "util";
|
|
1219
1494
|
|
|
1220
1495
|
// src/sections/sync.ts
|
|
1221
|
-
var
|
|
1496
|
+
var requestTimeoutMilliseconds6 = 2e4;
|
|
1222
1497
|
async function readMessage2(response, fallback) {
|
|
1223
1498
|
try {
|
|
1224
1499
|
const body = await response.json();
|
|
@@ -1234,7 +1509,7 @@ async function syncSectionTypes(options) {
|
|
|
1234
1509
|
try {
|
|
1235
1510
|
sitesResponse = await fetchImplementation(`${apiUrl}/sites`, {
|
|
1236
1511
|
headers,
|
|
1237
|
-
signal: AbortSignal.timeout(
|
|
1512
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds6)
|
|
1238
1513
|
});
|
|
1239
1514
|
} catch {
|
|
1240
1515
|
return { kind: "unreachable" };
|
|
@@ -1256,7 +1531,7 @@ async function syncSectionTypes(options) {
|
|
|
1256
1531
|
method: "PUT",
|
|
1257
1532
|
headers: { ...headers, "Content-Type": "application/json" },
|
|
1258
1533
|
body: JSON.stringify(payload),
|
|
1259
|
-
signal: AbortSignal.timeout(
|
|
1534
|
+
signal: AbortSignal.timeout(requestTimeoutMilliseconds6)
|
|
1260
1535
|
});
|
|
1261
1536
|
} catch {
|
|
1262
1537
|
return { kind: "unreachable" };
|
|
@@ -1292,7 +1567,7 @@ var syncUsage = `ace-kit sync: send the section types your code defines to CMS A
|
|
|
1292
1567
|
Usage:
|
|
1293
1568
|
ace-kit sync [--site <domain>] [--api-url <address>] [--dir <folder>]
|
|
1294
1569
|
|
|
1295
|
-
It finds every *.schema.ts file in the folder (src by default), checks them, and makes the CMS's section types for your
|
|
1570
|
+
It finds every *.schema.ts file in the folder (src and app by default), checks them, and makes the CMS's section types for your
|
|
1296
1571
|
site match. The dev server does this for you whenever a schema file is saved; run it yourself for a one-off sync, or on a
|
|
1297
1572
|
build server (with ACE_TOKEN set).
|
|
1298
1573
|
|
|
@@ -1303,12 +1578,12 @@ Options:
|
|
|
1303
1578
|
--site The domain your site has in the CMS, e.g. example.com (default: ACE_SITE_DOMAIN, else localhost).
|
|
1304
1579
|
(--domain means the same.)
|
|
1305
1580
|
--api-url Where the CMS Ace API lives (default: ACE_API_URL, else the hosted CMS Ace). Only for a CMS on your own machine.
|
|
1306
|
-
--dir The folder to look in (default: src).
|
|
1581
|
+
--dir The folder to look in (default: src and app).
|
|
1307
1582
|
--help Show this message.
|
|
1308
1583
|
`;
|
|
1309
1584
|
var isSwitchedOff = (value) => ["false", "off", "no", "0"].includes((value ?? "").trim().toLowerCase());
|
|
1310
1585
|
async function syncCommand(commandArguments, environment) {
|
|
1311
|
-
const { values } =
|
|
1586
|
+
const { values } = parseArgs5({
|
|
1312
1587
|
// eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
|
|
1313
1588
|
args: commandArguments,
|
|
1314
1589
|
options: {
|
|
@@ -1344,11 +1619,11 @@ async function syncCommand(commandArguments, environment) {
|
|
|
1344
1619
|
);
|
|
1345
1620
|
return 0;
|
|
1346
1621
|
}
|
|
1347
|
-
const files = await findSchemaFiles(environment.workingDirectory, values.dir
|
|
1622
|
+
const files = await findSchemaFiles(environment.workingDirectory, values.dir);
|
|
1348
1623
|
if (files.length === 0) {
|
|
1349
1624
|
if (!automatic) {
|
|
1350
1625
|
environment.out(
|
|
1351
|
-
`No *.schema.ts files found in ${values.dir ?? "src"}, so nothing was synced (syncing none would retire every section).`
|
|
1626
|
+
`No *.schema.ts files found in ${values.dir ?? "src or app"}, so nothing was synced (syncing none would retire every section).`
|
|
1352
1627
|
);
|
|
1353
1628
|
}
|
|
1354
1629
|
return 0;
|
|
@@ -1409,6 +1684,7 @@ var usage = `ace-kit: tools for sites built on CMS Ace.
|
|
|
1409
1684
|
|
|
1410
1685
|
Commands:
|
|
1411
1686
|
ace-kit create Make a new site from a starter.
|
|
1687
|
+
ace-kit doctor Check this site is set up to work with CMS Ace.
|
|
1412
1688
|
ace-kit generate Write TypeScript types for your content types.
|
|
1413
1689
|
ace-kit sync Send the section types your code defines to the CMS.
|
|
1414
1690
|
ace-kit login Sign in from the command line (opens the admin app).
|
|
@@ -1422,6 +1698,8 @@ async function runCli(commandArguments, environment) {
|
|
|
1422
1698
|
switch (command) {
|
|
1423
1699
|
case "create":
|
|
1424
1700
|
return createCommand(rest, environment);
|
|
1701
|
+
case "doctor":
|
|
1702
|
+
return doctorCommand(rest, environment);
|
|
1425
1703
|
case "generate":
|
|
1426
1704
|
return generateCommand(rest, environment);
|
|
1427
1705
|
case "sync":
|
|
@@ -1447,6 +1725,7 @@ ${usage}`);
|
|
|
1447
1725
|
}
|
|
1448
1726
|
export {
|
|
1449
1727
|
createUsage,
|
|
1728
|
+
doctorUsage,
|
|
1450
1729
|
generateUsage,
|
|
1451
1730
|
loginUsage,
|
|
1452
1731
|
runCli,
|