@sandboxsonny/ace-kit 0.6.0 → 0.7.1

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.
@@ -126,12 +126,6 @@ async function createSite(options) {
126
126
  text = replaceOnce(file, text, /site: "https?:\/\/[^"]+"/, `site: "https://${domain}"`);
127
127
  if (text !== original) await writeFile(path, text);
128
128
  }
129
- const settings = [
130
- "# Where CMS Ace runs. Leave these out for the hosted CMS Ace; they are for a CMS on your own machine.",
131
- "# ACE_API_URL=http://localhost:8080",
132
- "# ACE_PREVIEW_ADMIN_ORIGIN=http://localhost:5173",
133
- ""
134
- ];
135
129
  const extraSettings = Object.entries(options.environmentSettings ?? {}).map(([name2, value]) => `${name2}=${value}`);
136
130
  if (options.framework === "astro") {
137
131
  await writeNew(
@@ -140,8 +134,7 @@ async function createSite(options) {
140
134
  [
141
135
  "# The site's domain in CMS Ace is the `site` setting in astro.config.mjs. Set this only to override it.",
142
136
  "# ACE_SITE_DOMAIN=" + domain,
143
- "",
144
- ...settings
137
+ ""
145
138
  ].join("\n")
146
139
  );
147
140
  if (extraSettings.length > 0) await writeNew(folder, ".env", `${extraSettings.join("\n")}
@@ -149,7 +142,7 @@ async function createSite(options) {
149
142
  } else {
150
143
  const domainLine = `ACE_SITE_DOMAIN=${domain}
151
144
  `;
152
- await writeNew(folder, ".env.example", [`# The site's domain in CMS Ace.`, domainLine, ...settings].join("\n"));
145
+ await writeNew(folder, ".env.example", [`# The site's domain in CMS Ace.`, domainLine].join("\n"));
153
146
  await writeNew(folder, ".env", [domainLine.trim(), ...extraSettings].join("\n") + "\n");
154
147
  }
155
148
  await writeNew(folder, "README.md", readme({ name, domain, framework: options.framework }));
@@ -182,7 +175,7 @@ var richText = (text) => ({
182
175
  type: "doc",
183
176
  content: text.split("\n\n").map((paragraph) => ({ type: "paragraph", content: [{ type: "text", text: paragraph }] }))
184
177
  });
185
- var frameworksWithSections = ["astro"];
178
+ var frameworksWithSections = ["astro", "react-router"];
186
179
  function pageFields(framework) {
187
180
  const fields = [
188
181
  { componentKey: "text", label: "Headline", handle: "headline", config: { required: true } },
@@ -396,6 +389,9 @@ async function resolveApiUrl(fromOption, environment) {
396
389
  return null;
397
390
  }
398
391
  }
392
+ var cmsName = (apiUrl) => apiUrl === hostedApiUrl ? "CMS Ace" : `the CMS at ${apiUrl}`;
393
+ var cmsLabel = (apiUrl) => apiUrl === hostedApiUrl ? "CMS Ace" : apiUrl;
394
+ var loginCommandFor = (apiUrl) => apiUrl === hostedApiUrl ? "npx ace-kit login" : `npx ace-kit login --api-url ${apiUrl}`;
399
395
  function isLocalApiUrl(apiUrl) {
400
396
  return ["localhost", "127.0.0.1", "[::1]"].includes(new URL(apiUrl).hostname);
401
397
  }
@@ -525,13 +521,12 @@ async function waitForApproval(dependencies) {
525
521
  var loginUsage = `ace-kit login: sign in to CMS Ace from the command line.
526
522
 
527
523
  Usage:
528
- ace-kit login [--api-url <address>] Sign in. Opens the admin app, where you type the code shown here.
529
- ace-kit logout [--api-url <address>] Sign out, and forget the login kept on this machine.
530
- ace-kit whoami [--api-url <address>] Say who you are signed in as.
524
+ ace-kit login Sign in. Opens the admin app, where you type the code shown here.
525
+ ace-kit logout Sign out, and forget the login kept on this machine.
526
+ ace-kit whoami Say who you are signed in as.
531
527
 
532
528
  Options:
533
- --api-url Where the CMS Ace API lives (or set ACE_API_URL, in the environment or a .env file).
534
- --help Show this message.
529
+ --help Show this message.
535
530
 
536
531
  The login is kept in the macOS Keychain, or in a file only you can read on other systems. On a build server,
537
532
  where nobody can approve a login in a browser, set ACE_TOKEN instead.
@@ -565,7 +560,7 @@ async function loginCommand(commandArguments, environment) {
565
560
  const { apiUrl } = read;
566
561
  if (apiUrl.startsWith("http://") && !isLocalApiUrl(apiUrl)) {
567
562
  environment.error(
568
- `Warning: ${apiUrl} is not encrypted (http://), so the login could be read on the way. Use https:// if you can.`
563
+ `Warning: ${cmsName(apiUrl)} is not encrypted (http://), so the login could be read on the way. Use https:// if you can.`
569
564
  );
570
565
  }
571
566
  try {
@@ -589,7 +584,7 @@ async function signIn(apiUrl, environment) {
589
584
  "unsupported",
590
585
  "The CMS sent an approval address that isn't a web address, so it was not opened."
591
586
  );
592
- environment.out(`Signing in to ${apiUrl}
587
+ environment.out(`Signing in to ${cmsLabel(apiUrl)}
593
588
  `);
594
589
  environment.out(` 1. Go to ${page}`);
595
590
  environment.out(` 2. Type ${request.userCode}
@@ -619,7 +614,7 @@ async function logoutCommand(commandArguments, environment) {
619
614
  const { apiUrl } = read;
620
615
  const stored = await environment.store.read(apiUrl);
621
616
  if (!stored) {
622
- environment.out(`Not signed in to ${apiUrl}.`);
617
+ environment.out(`Not signed in to ${cmsLabel(apiUrl)}.`);
623
618
  return 0;
624
619
  }
625
620
  const ended = await environment.fetchImplementation(`${apiUrl}/auth/logout`, {
@@ -632,7 +627,7 @@ async function logoutCommand(commandArguments, environment) {
632
627
  );
633
628
  await environment.store.remove(apiUrl);
634
629
  environment.out(
635
- ended ? `Signed out of ${apiUrl}.` : `Forgot the login for ${apiUrl} on this machine, but could not reach the CMS to end it there. It ends by itself after 30 days unused; a super admin can also end it from the Sessions screen.`
630
+ ended ? `Signed out of ${cmsLabel(apiUrl)}.` : `Forgot the login for ${cmsLabel(apiUrl)} on this machine, but could not reach the CMS to end it there. It ends by itself after 30 days unused; a super admin can also end it from the Sessions screen.`
636
631
  );
637
632
  return 0;
638
633
  }
@@ -651,7 +646,7 @@ async function whoamiCommand(commandArguments, environment) {
651
646
  now: environment.now
652
647
  });
653
648
  if (!login) {
654
- environment.out(`Not signed in to ${apiUrl}. Run: ace-kit login`);
649
+ environment.out(`Not signed in to ${cmsLabel(apiUrl)}. Run: ${loginCommandFor(apiUrl).replace("npx ", "")}`);
655
650
  return 1;
656
651
  }
657
652
  try {
@@ -668,10 +663,10 @@ async function whoamiCommand(commandArguments, environment) {
668
663
  return 1;
669
664
  }
670
665
  const me = await response.json();
671
- environment.out(`Signed in to ${apiUrl} as ${String(me.email)}, using the login in ${login.source}.`);
666
+ environment.out(`Signed in to ${cmsLabel(apiUrl)} as ${String(me.email)}, using the login in ${login.source}.`);
672
667
  return 0;
673
668
  } catch {
674
- environment.error(`Could not reach the CMS at ${apiUrl}.`);
669
+ environment.error(`Could not reach ${cmsName(apiUrl)}.`);
675
670
  return 1;
676
671
  }
677
672
  }
@@ -680,7 +675,7 @@ async function whoamiCommand(commandArguments, environment) {
680
675
  var createUsage = `ace-kit create: make a new site that uses CMS Ace.
681
676
 
682
677
  Usage:
683
- ace-kit create <folder> <domain> [--framework astro|react-router] [--name <name>] [--api-url <address>]
678
+ ace-kit create <folder> <domain> [--framework astro|react-router] [--name <name>]
684
679
 
685
680
  It signs you in if you aren't already (your browser opens to approve it), makes the site in CMS Ace with starter
686
681
  content (a Pages type, Home, About and Contact, a main menu), and copies a starter site into the folder with your
@@ -691,7 +686,6 @@ Options:
691
686
  <domain> The domain the site has in CMS Ace, e.g. example.com. (--site <domain> means the same.)
692
687
  --framework astro (the default) or react-router.
693
688
  --name The name shown to visitors (default: made from the folder's name, so my-site is "My Site").
694
- --api-url Where the CMS Ace API lives (or set ACE_API_URL). Only for a CMS on your own machine.
695
689
  --help Show this message.
696
690
  `;
697
691
  async function createCommand(commandArguments, environment) {
@@ -742,7 +736,7 @@ ${createUsage}`);
742
736
  }
743
737
  if (apiUrl.startsWith("http://") && !isLocalApiUrl(apiUrl)) {
744
738
  environment.error(
745
- `Warning: ${apiUrl} is not encrypted (http://), so your login could be read on the way. Use https:// if you can.`
739
+ `Warning: ${cmsName(apiUrl)} is not encrypted (http://), so your login could be read on the way. Use https:// if you can.`
746
740
  );
747
741
  }
748
742
  let login = await resolveLogin({
@@ -797,7 +791,7 @@ Nothing was made.`);
797
791
  switch (seeded.kind) {
798
792
  case "seeded":
799
793
  environment.out(
800
- `Made ${created.domain} in CMS Ace (${apiUrl}) with a Pages type, ${seeded.entries} entries and a main menu.`
794
+ `Made ${created.domain} in ${apiUrl === hostedApiUrl ? "CMS Ace" : `CMS Ace (${apiUrl})`} with a Pages type, ${seeded.entries} entries and a main menu.`
801
795
  );
802
796
  break;
803
797
  case "exists":
@@ -822,10 +816,462 @@ Next:
822
816
  return seeded.kind === "failed" ? 1 : 0;
823
817
  }
824
818
 
819
+ // src/cli/doctor-command.ts
820
+ import { readFile as readFile3 } from "fs/promises";
821
+ import { join as join4 } from "path";
822
+ import { parseArgs as parseArgs3 } from "util";
823
+
824
+ // src/sections/find-schemas.ts
825
+ import { readdir as readdir2 } from "fs/promises";
826
+ import { join as join3, relative as relative2 } from "path";
827
+ var schemaFilePattern = /\.schema\.(?:ts|mts|js|mjs)$/;
828
+ var skippedFolders = /* @__PURE__ */ new Set([
829
+ "node_modules",
830
+ ".git",
831
+ "dist",
832
+ "build",
833
+ ".astro",
834
+ ".react-router",
835
+ ".output",
836
+ "coverage"
837
+ ]);
838
+ var defaultSchemaFolders = ["src", "app"];
839
+ async function findSchemaFiles(root, folder) {
840
+ const folders = folder === void 0 ? defaultSchemaFolders : [folder];
841
+ const found = [];
842
+ async function walk(directory) {
843
+ let entries;
844
+ try {
845
+ entries = await readdir2(directory, { withFileTypes: true });
846
+ } catch {
847
+ return;
848
+ }
849
+ for (const entry of entries) {
850
+ const path = join3(directory, entry.name);
851
+ if (entry.isDirectory()) {
852
+ if (!skippedFolders.has(entry.name)) await walk(path);
853
+ } else if (entry.isFile() && schemaFilePattern.test(entry.name) && !entry.name.endsWith(".d.ts")) {
854
+ found.push(path);
855
+ }
856
+ }
857
+ }
858
+ for (const name of folders) await walk(join3(root, name));
859
+ return found.sort((first, second) => {
860
+ const [firstName, secondName] = [relative2(root, first), relative2(root, second)];
861
+ return firstName < secondName ? -1 : firstName > secondName ? 1 : 0;
862
+ });
863
+ }
864
+
865
+ // src/sections/load-schemas.ts
866
+ import { relative as relative3 } from "path";
867
+
868
+ // src/sections/payload.ts
869
+ var handlePattern = /^[a-z0-9]+(-[a-z0-9]+)*$/;
870
+ var maximumHandleLength = 64;
871
+ var maximumNameLength = 200;
872
+ var fieldTypes = [
873
+ "text",
874
+ "textarea",
875
+ "number",
876
+ "checkbox",
877
+ "select",
878
+ "image",
879
+ "richtext",
880
+ "date",
881
+ "link"
882
+ ];
883
+ var isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
884
+ var isHandle = (value) => typeof value === "string" && value.length <= maximumHandleLength && handlePattern.test(value);
885
+ var handleRule = 'lowercase letters, numbers and hyphens (such as "call-to-action")';
886
+ function checkFields(fields, where, problems) {
887
+ if (fields === void 0) return;
888
+ if (!Array.isArray(fields)) {
889
+ problems.push(`${where}: fields must be a list.`);
890
+ return;
891
+ }
892
+ const seen = /* @__PURE__ */ new Set();
893
+ fields.forEach((field, index) => {
894
+ if (!isObject(field)) {
895
+ problems.push(`${where}: field ${index + 1} must be an object with a handle, a label and a type.`);
896
+ return;
897
+ }
898
+ const name = isHandle(field.handle) ? `field "${field.handle}"` : `field ${index + 1}`;
899
+ if (!isHandle(field.handle)) problems.push(`${where}: ${name} needs a handle of ${handleRule}.`);
900
+ else if (seen.has(field.handle)) problems.push(`${where}: ${name} is defined twice.`);
901
+ else seen.add(field.handle);
902
+ if (typeof field.label !== "string" || field.label.trim() === "" || field.label.length > maximumNameLength) {
903
+ problems.push(`${where}: ${name} needs a label.`);
904
+ }
905
+ if (typeof field.type !== "string" || !fieldTypes.includes(field.type)) {
906
+ const given = typeof field.type === "string" ? ` "${field.type}"` : "";
907
+ problems.push(`${where}: ${name} has a type${given} that isn't one of ${fieldTypes.join(", ")}.`);
908
+ } else if (field.type === "select") {
909
+ const { options } = field;
910
+ if (!Array.isArray(options) || options.length === 0 || options.some((option) => typeof option !== "string" || option === "")) {
911
+ problems.push(`${where}: ${name} is a select, so it needs options: a list of the choices, as text.`);
912
+ }
913
+ }
914
+ if (field.required !== void 0 && typeof field.required !== "boolean") {
915
+ problems.push(`${where}: ${name} has a required that isn't true or false.`);
916
+ }
917
+ });
918
+ }
919
+ function checkSectionDefinition(definition, where = "this section") {
920
+ const problems = [];
921
+ if (!isObject(definition)) {
922
+ return [`${where}: the default export must be the result of defineSection({ ... }).`];
923
+ }
924
+ if (!isHandle(definition.handle)) {
925
+ problems.push(`${where}: needs a handle of ${handleRule}.`);
926
+ }
927
+ if (typeof definition.name !== "string" || definition.name.trim() === "" || definition.name.length > maximumNameLength) {
928
+ problems.push(`${where}: needs a name.`);
929
+ }
930
+ checkFields(definition.fields, where, problems);
931
+ if (definition.blocks !== void 0) {
932
+ if (!isObject(definition.blocks)) {
933
+ problems.push(`${where}: blocks must be an object of the kinds of block, by handle.`);
934
+ } else {
935
+ for (const [handle, block] of Object.entries(definition.blocks)) {
936
+ const blockWhere = `${where}, block "${handle}"`;
937
+ if (!isHandle(handle)) problems.push(`${blockWhere}: the handle must be ${handleRule}.`);
938
+ if (!isObject(block)) {
939
+ problems.push(`${blockWhere}: must be an object with a name and fields.`);
940
+ continue;
941
+ }
942
+ if (typeof block.name !== "string" || block.name.trim() === "") problems.push(`${blockWhere}: needs a name.`);
943
+ checkFields(block.fields, blockWhere, problems);
944
+ }
945
+ }
946
+ }
947
+ return problems;
948
+ }
949
+ function apiFields(fields) {
950
+ return (fields ?? []).map((field) => ({
951
+ handle: field.handle,
952
+ label: field.label,
953
+ component_key: field.type,
954
+ config: {
955
+ ...field.required ? { required: true } : {},
956
+ ...field.type === "select" ? { options: [...field.options] } : {}
957
+ }
958
+ }));
959
+ }
960
+ function sectionTypesPayload(definitions) {
961
+ return {
962
+ sections: definitions.map((definition) => ({
963
+ handle: definition.handle,
964
+ name: definition.name,
965
+ fields: apiFields(definition.fields),
966
+ blocks: Object.entries(definition.blocks ?? {}).map(([handle, block]) => ({
967
+ handle,
968
+ name: block.name,
969
+ fields: apiFields(block.fields)
970
+ }))
971
+ }))
972
+ };
973
+ }
974
+
975
+ // src/sections/load-schemas.ts
976
+ async function loadSchemas(files, root, importModule) {
977
+ const definitions = [];
978
+ const problems = [];
979
+ const handleToFile = /* @__PURE__ */ new Map();
980
+ for (const file of files) {
981
+ const name = relative3(root, file);
982
+ let exported;
983
+ try {
984
+ const module = await importModule(file);
985
+ exported = module.default;
986
+ } catch (error) {
987
+ problems.push(`${name}: could not be loaded: ${error instanceof Error ? error.message : String(error)}`);
988
+ continue;
989
+ }
990
+ const fileProblems = checkSectionDefinition(exported, name);
991
+ if (fileProblems.length > 0) {
992
+ problems.push(...fileProblems);
993
+ continue;
994
+ }
995
+ const definition = exported;
996
+ const earlier = handleToFile.get(definition.handle);
997
+ if (earlier) {
998
+ problems.push(`${name}: the handle "${definition.handle}" is already used by ${earlier}.`);
999
+ continue;
1000
+ }
1001
+ handleToFile.set(definition.handle, name);
1002
+ definitions.push({ file, definition });
1003
+ }
1004
+ return { definitions, problems };
1005
+ }
1006
+
1007
+ // src/cli/doctor-command.ts
1008
+ var doctorUsage = `ace-kit doctor: check that this site is set up to work with CMS Ace.
1009
+
1010
+ Usage:
1011
+ ace-kit doctor [--site <domain>] [--dir <folder>]
1012
+
1013
+ Run it in your site's folder. It changes nothing and never prints your login. It checks your Node version, that Ace Kit
1014
+ is installed, which CMS and site the settings point at, that the CMS can be reached and knows the site, that you are
1015
+ signed in, that the site has its Pages content, and that the section types in your code match the CMS. Each problem
1016
+ says what to do. It exits with 1 if anything is wrong.
1017
+
1018
+ Options:
1019
+ --site The domain your site has in the CMS (default: ACE_SITE_DOMAIN, else Astro's \`site\` setting).
1020
+ --dir The folder holding your *.schema.ts files (default: src and app).
1021
+ --help Show this message.
1022
+ `;
1023
+ var symbols = { ok: "\u2713", warn: "!", fail: "\u2717", info: "-" };
1024
+ var requestTimeoutMilliseconds4 = 15e3;
1025
+ var plain = (text) => String(text).replace(/\p{Cc}/gu, "").slice(0, 200);
1026
+ var isOlderThan = (version, major, minor) => {
1027
+ const [actualMajor = 0, actualMinor = 0] = version.split(".").map(Number);
1028
+ return actualMajor < major || actualMajor === major && actualMinor < minor;
1029
+ };
1030
+ async function readText(path) {
1031
+ try {
1032
+ return await readFile3(path, "utf8");
1033
+ } catch {
1034
+ return null;
1035
+ }
1036
+ }
1037
+ async function domainFromAstroConfig(root) {
1038
+ for (const name of ["astro.config.mjs", "astro.config.ts", "astro.config.js", "astro.config.mts"]) {
1039
+ const text = await readText(join4(root, name));
1040
+ const found = text?.match(/\bsite\s*:\s*["']([^"']+)["']/);
1041
+ if (found?.[1]) return domainFromSite(found[1]);
1042
+ }
1043
+ return null;
1044
+ }
1045
+ async function doctorCommand(commandArguments, environment) {
1046
+ const { values } = parseArgs3({
1047
+ // eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
1048
+ args: commandArguments,
1049
+ options: {
1050
+ "api-url": { type: "string" },
1051
+ site: { type: "string" },
1052
+ dir: { type: "string" },
1053
+ help: { type: "boolean" }
1054
+ }
1055
+ });
1056
+ if (values.help) {
1057
+ environment.out(doctorUsage);
1058
+ return 0;
1059
+ }
1060
+ const checks = [];
1061
+ const add = (status, label, hint) => checks.push({ status, label, hint });
1062
+ const root = environment.workingDirectory;
1063
+ if (isOlderThan(environment.nodeVersion, 22, 12))
1064
+ add(
1065
+ "fail",
1066
+ `Node ${environment.nodeVersion} is too old`,
1067
+ "Ace Kit needs Node 22.12 or newer. If you use mise, nvm or fnm, check which version it picks in this folder."
1068
+ );
1069
+ else add("ok", `Node ${environment.nodeVersion}`);
1070
+ const manifestText = await readText(join4(root, "package.json"));
1071
+ let framework = null;
1072
+ if (manifestText === null) {
1073
+ add("fail", "No package.json in this folder", "Run this in your site's folder.");
1074
+ } else {
1075
+ let manifest = {};
1076
+ try {
1077
+ manifest = JSON.parse(manifestText);
1078
+ } catch {
1079
+ add("fail", "package.json isn't valid JSON");
1080
+ }
1081
+ const dependencies = { ...manifest.devDependencies, ...manifest.dependencies };
1082
+ if ("astro" in dependencies) framework = "astro";
1083
+ else if ("react-router" in dependencies) framework = "react-router";
1084
+ add(
1085
+ framework ? "ok" : "info",
1086
+ framework ? framework === "astro" ? "An Astro site" : "A React Router site" : "Not an Astro or React Router site"
1087
+ );
1088
+ const installed = await readText(join4(root, "node_modules", "@sandboxsonny", "ace-kit", "package.json"));
1089
+ if (!("@sandboxsonny/ace-kit" in dependencies)) {
1090
+ add("fail", "Ace Kit isn't a dependency of this site", "Run: npm install @sandboxsonny/ace-kit");
1091
+ } else if (installed === null) {
1092
+ add("fail", "Ace Kit isn't installed", "Run: npm install");
1093
+ } else {
1094
+ let version = "unknown";
1095
+ try {
1096
+ version = String(JSON.parse(installed).version ?? "unknown");
1097
+ } catch {
1098
+ }
1099
+ add("ok", `Ace Kit ${version} is installed`);
1100
+ if (version !== environment.version && environment.version !== "development")
1101
+ add("info", `This command is Ace Kit ${environment.version}; the site uses ${version}`);
1102
+ }
1103
+ }
1104
+ let apiUrl = null;
1105
+ try {
1106
+ apiUrl = normaliseApiUrl(values["api-url"] ?? environment.variables.ACE_API_URL ?? hostedApiUrl);
1107
+ add("ok", apiUrl === hostedApiUrl ? "Connected to CMS Ace" : `CMS: ${apiUrl}`);
1108
+ } catch (error) {
1109
+ add("fail", `The CMS address is wrong: ${error instanceof Error ? error.message : String(error)}`);
1110
+ }
1111
+ const given = values.site ?? environment.variables.ACE_SITE_DOMAIN;
1112
+ const domain = given ? domainFromSite(given) : framework === "astro" ? await domainFromAstroConfig(root) : null;
1113
+ if (domain) add("ok", `Site: ${domain}`);
1114
+ else
1115
+ add(
1116
+ "fail",
1117
+ "Can't tell which site this is",
1118
+ 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."
1119
+ );
1120
+ if (apiUrl) await checkTheCms(apiUrl, domain);
1121
+ return finish();
1122
+ async function request(path, headers = {}) {
1123
+ try {
1124
+ const response = await environment.fetchImplementation(`${apiUrl}${path}`, {
1125
+ headers: { Accept: "application/json", ...headers },
1126
+ signal: AbortSignal.timeout(requestTimeoutMilliseconds4)
1127
+ });
1128
+ return { status: response.status, body: await response.json().catch(() => null) };
1129
+ } catch {
1130
+ return null;
1131
+ }
1132
+ }
1133
+ async function checkTheCms(address, siteDomain) {
1134
+ const health = await request("/health");
1135
+ if (!health || health.status >= 500 || health.status === 404) {
1136
+ add(
1137
+ "fail",
1138
+ `Can't reach the CMS at ${address}`,
1139
+ isLocalApiUrl(address) ? "Is your local CMS running? (docker compose -p cms-backend up -d)" : "Check the address and your connection."
1140
+ );
1141
+ return;
1142
+ }
1143
+ add("ok", "CMS Ace answers");
1144
+ if (!siteDomain) return;
1145
+ const asSite = { "X-Ace-Site-Domain": siteDomain };
1146
+ const site = await request("/public/site", asSite);
1147
+ if (site?.status === 404) {
1148
+ add(
1149
+ "fail",
1150
+ `The CMS has no site with the domain ${siteDomain}`,
1151
+ "Make it with `npx @sandboxsonny/ace-kit create`, or in the admin app, or fix the domain."
1152
+ );
1153
+ } else if (site?.status === 200) {
1154
+ add("ok", `The CMS knows the site: "${plain(site.body?.name)}"`);
1155
+ await checkContent(asSite);
1156
+ } else {
1157
+ add("fail", `The CMS answered with an error (${site?.status ?? "no answer"}) when asked about the site`);
1158
+ }
1159
+ await checkLogin(address, siteDomain);
1160
+ }
1161
+ async function checkContent(asSite) {
1162
+ const types = await request("/public/content-types", asSite);
1163
+ const typeList = Array.isArray(types?.body) ? types.body : [];
1164
+ if (!typeList.some((type) => type.handle === "pages")) {
1165
+ add(
1166
+ "warn",
1167
+ "The site has no Pages content type",
1168
+ "The starter's pages read one. Add it in the admin app (Content types)."
1169
+ );
1170
+ return;
1171
+ }
1172
+ const entries = await request("/public/entries?content_type=pages&limit=100", asSite);
1173
+ const list = Array.isArray(entries?.body) ? entries.body : [];
1174
+ if (list.some((entry) => entry.handle === "home"))
1175
+ add("ok", `Pages has ${list.length} published ${list.length === 1 ? "entry" : "entries"}, including home`);
1176
+ else
1177
+ add(
1178
+ "warn",
1179
+ "Pages has no published entry called home",
1180
+ "The site's front page (/) reads it. Add one in the admin app and publish it."
1181
+ );
1182
+ }
1183
+ async function checkLogin(address, siteDomain) {
1184
+ const login = await resolveLogin({
1185
+ apiUrl: address,
1186
+ variables: environment.variables,
1187
+ store: environment.store,
1188
+ now: environment.now
1189
+ });
1190
+ if (!login) {
1191
+ add("warn", "Not signed in", `Run: ${loginCommandFor(address)}. Section sync and dev sites need it.`);
1192
+ return;
1193
+ }
1194
+ const authorised = { Authorization: `Bearer ${login.token}` };
1195
+ const me = await request("/auth/me", authorised);
1196
+ if (me?.status === 401) {
1197
+ add("warn", "Your login has ended", "Run: npx ace-kit login");
1198
+ return;
1199
+ }
1200
+ if (me?.status !== 200) {
1201
+ add("warn", `Couldn't check your login (${me?.status ?? "no answer"})`);
1202
+ return;
1203
+ }
1204
+ add("ok", `Signed in as ${plain(me.body?.email)}`);
1205
+ const sites = await request("/sites", authorised);
1206
+ const visible = Array.isArray(sites?.body) ? sites.body : [];
1207
+ const mine = visible.find((candidate) => candidate.kind !== "dev" && candidate.domain.toLowerCase() === siteDomain);
1208
+ if (!mine) {
1209
+ add("info", "This login can't see that site, so its section types and preview address weren't checked");
1210
+ return;
1211
+ }
1212
+ if (mine.preview_url) add("ok", `Preview address: ${plain(mine.preview_url)}`);
1213
+ else if (isLocalApiUrl(address))
1214
+ add(
1215
+ "warn",
1216
+ "The site has no Preview address",
1217
+ "Set it in the site's Settings (for example http://localhost:4321) so the customiser can show your site."
1218
+ );
1219
+ await checkSections(address, mine.id, authorised);
1220
+ }
1221
+ async function checkSections(address, siteId, authorised) {
1222
+ const files = await findSchemaFiles(root, values.dir);
1223
+ if (files.length === 0) {
1224
+ add("info", "No section schemas (*.schema.ts) in your code");
1225
+ return;
1226
+ }
1227
+ const loaded = await loadSchemas(files, root, environment.importModule);
1228
+ if (loaded.problems.length > 0) {
1229
+ for (const problem of loaded.problems) add("fail", `Section schema problem: ${plain(problem)}`);
1230
+ return;
1231
+ }
1232
+ const inCode = loaded.definitions.map(({ definition }) => definition.handle);
1233
+ add("ok", `${inCode.length} section type${inCode.length === 1 ? "" : "s"} in your code (${inCode.join(", ")})`);
1234
+ const cms = await request(`/sites/${siteId}/section-types`, authorised);
1235
+ if (!Array.isArray(cms?.body)) return;
1236
+ const active = new Set(
1237
+ cms.body.filter((type) => !type.retired_at).map((type) => type.handle)
1238
+ );
1239
+ const missing = inCode.filter((handle) => !active.has(handle));
1240
+ const unused = [...active].filter((handle) => !inCode.includes(handle));
1241
+ if (missing.length > 0)
1242
+ add(
1243
+ "warn",
1244
+ `Not in the CMS yet: ${missing.join(", ")}`,
1245
+ `Run: npx ace-kit sync${isLocalApiUrl(address) ? ` --api-url ${address}` : ""} (the dev server does this on save)`
1246
+ );
1247
+ else add("ok", "The CMS has all of them");
1248
+ if (unused.length > 0)
1249
+ add(
1250
+ "info",
1251
+ `In the CMS but not in your code: ${unused.join(", ")} (a sync would retire ${unused.length === 1 ? "it" : "them"})`
1252
+ );
1253
+ }
1254
+ function finish() {
1255
+ environment.out("ace-kit doctor\n");
1256
+ for (const check of checks) {
1257
+ environment.out(` ${symbols[check.status]} ${check.label}`);
1258
+ if (check.hint) environment.out(` ${check.hint}`);
1259
+ }
1260
+ const failed = checks.filter((check) => check.status === "fail").length;
1261
+ const warned = checks.filter((check) => check.status === "warn").length;
1262
+ environment.out(
1263
+ failed > 0 ? `
1264
+ ${failed} problem${failed === 1 ? "" : "s"} to fix${warned > 0 ? `, and ${warned} warning${warned === 1 ? "" : "s"}` : ""}.` : warned > 0 ? `
1265
+ Nothing is broken; ${warned} warning${warned === 1 ? "" : "s"}.` : "\nAll good."
1266
+ );
1267
+ return failed > 0 ? 1 : 0;
1268
+ }
1269
+ }
1270
+
825
1271
  // src/cli/generate-command.ts
826
1272
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
827
1273
  import { dirname as dirname2, resolve as resolve3 } from "path";
828
- import { parseArgs as parseArgs3 } from "util";
1274
+ import { parseArgs as parseArgs4 } from "util";
829
1275
 
830
1276
  // src/client/errors.ts
831
1277
  var AcePreviewExpiredError = class extends Error {
@@ -842,7 +1288,7 @@ var AceUnavailableError = class extends Error {
842
1288
  };
843
1289
 
844
1290
  // src/client/create-client.ts
845
- var requestTimeoutMilliseconds4 = 5e3;
1291
+ var requestTimeoutMilliseconds5 = 5e3;
846
1292
  var maximumCachedAnswers = 500;
847
1293
  function createAceClient({
848
1294
  apiUrl,
@@ -866,7 +1312,7 @@ function createAceClient({
866
1312
  try {
867
1313
  response = await fetchImplementation(`${apiUrl.replace(/\/+$/, "")}${path}`, {
868
1314
  headers: { Accept: "application/json", "X-Ace-Site-Domain": siteDomain, "X-Forwarded-Host": siteDomain },
869
- signal: AbortSignal.timeout(requestTimeoutMilliseconds4)
1315
+ signal: AbortSignal.timeout(requestTimeoutMilliseconds5)
870
1316
  });
871
1317
  } catch {
872
1318
  throw new AceUnavailableError("Could not reach the CMS.");
@@ -985,16 +1431,15 @@ export {};
985
1431
  var generateUsage = `ace-kit generate: write TypeScript types for your CMS Ace content types.
986
1432
 
987
1433
  Usage:
988
- ace-kit generate [--site <domain>] [--api-url <address>] [--out <file>]
1434
+ ace-kit generate [--site <domain>] [--out <file>]
989
1435
 
990
1436
  Options:
991
1437
  --site The domain your site has in the CMS, e.g. example.com (or set ACE_SITE_DOMAIN). --domain means the same.
992
- --api-url Where the CMS Ace API lives (or set ACE_API_URL). Only for a CMS on your own machine.
993
1438
  --out Where to write the types (default: src/ace-kit.generated.d.ts).
994
1439
  --help Show this message.
995
1440
  `;
996
1441
  async function generateCommand(commandArguments, environment) {
997
- const { values } = parseArgs3({
1442
+ const { values } = parseArgs4({
998
1443
  // eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
999
1444
  args: commandArguments,
1000
1445
  options: {
@@ -1033,192 +1478,10 @@ async function generateCommand(commandArguments, environment) {
1033
1478
  }
1034
1479
 
1035
1480
  // src/cli/sync-command.ts
1036
- import { parseArgs as parseArgs4 } from "util";
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
- }
1481
+ import { parseArgs as parseArgs5 } from "util";
1219
1482
 
1220
1483
  // src/sections/sync.ts
1221
- var requestTimeoutMilliseconds5 = 2e4;
1484
+ var requestTimeoutMilliseconds6 = 2e4;
1222
1485
  async function readMessage2(response, fallback) {
1223
1486
  try {
1224
1487
  const body = await response.json();
@@ -1234,7 +1497,7 @@ async function syncSectionTypes(options) {
1234
1497
  try {
1235
1498
  sitesResponse = await fetchImplementation(`${apiUrl}/sites`, {
1236
1499
  headers,
1237
- signal: AbortSignal.timeout(requestTimeoutMilliseconds5)
1500
+ signal: AbortSignal.timeout(requestTimeoutMilliseconds6)
1238
1501
  });
1239
1502
  } catch {
1240
1503
  return { kind: "unreachable" };
@@ -1256,7 +1519,7 @@ async function syncSectionTypes(options) {
1256
1519
  method: "PUT",
1257
1520
  headers: { ...headers, "Content-Type": "application/json" },
1258
1521
  body: JSON.stringify(payload),
1259
- signal: AbortSignal.timeout(requestTimeoutMilliseconds5)
1522
+ signal: AbortSignal.timeout(requestTimeoutMilliseconds6)
1260
1523
  });
1261
1524
  } catch {
1262
1525
  return { kind: "unreachable" };
@@ -1290,9 +1553,9 @@ async function syncSectionTypes(options) {
1290
1553
  var syncUsage = `ace-kit sync: send the section types your code defines to CMS Ace.
1291
1554
 
1292
1555
  Usage:
1293
- ace-kit sync [--site <domain>] [--api-url <address>] [--dir <folder>]
1556
+ ace-kit sync [--site <domain>] [--dir <folder>]
1294
1557
 
1295
- It finds every *.schema.ts file in the folder (src by default), checks them, and makes the CMS's section types for your
1558
+ 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
1559
  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
1560
  build server (with ACE_TOKEN set).
1298
1561
 
@@ -1300,15 +1563,14 @@ Nothing is deleted: a section type, block or field you take out of your code is
1300
1563
  written in it survives. If any schema file can't be loaded, nothing is synced.
1301
1564
 
1302
1565
  Options:
1303
- --site The domain your site has in the CMS, e.g. example.com (default: ACE_SITE_DOMAIN, else localhost).
1566
+ --site The domain your site has in the CMS, e.g. example.com (or set ACE_SITE_DOMAIN).
1304
1567
  (--domain means the same.)
1305
- --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).
1568
+ --dir The folder to look in (default: src and app).
1307
1569
  --help Show this message.
1308
1570
  `;
1309
1571
  var isSwitchedOff = (value) => ["false", "off", "no", "0"].includes((value ?? "").trim().toLowerCase());
1310
1572
  async function syncCommand(commandArguments, environment) {
1311
- const { values } = parseArgs4({
1573
+ const { values } = parseArgs5({
1312
1574
  // eslint-disable-next-line id-denylist -- "args" is the name node's parseArgs gives this option.
1313
1575
  args: commandArguments,
1314
1576
  options: {
@@ -1344,11 +1606,11 @@ async function syncCommand(commandArguments, environment) {
1344
1606
  );
1345
1607
  return 0;
1346
1608
  }
1347
- const files = await findSchemaFiles(environment.workingDirectory, values.dir ?? "src");
1609
+ const files = await findSchemaFiles(environment.workingDirectory, values.dir);
1348
1610
  if (files.length === 0) {
1349
1611
  if (!automatic) {
1350
1612
  environment.out(
1351
- `No *.schema.ts files found in ${values.dir ?? "src"}, so nothing was synced (syncing none would retire every section).`
1613
+ `No *.schema.ts files found in ${values.dir ?? "src or app"}, so nothing was synced (syncing none would retire every section).`
1352
1614
  );
1353
1615
  }
1354
1616
  return 0;
@@ -1360,7 +1622,7 @@ async function syncCommand(commandArguments, environment) {
1360
1622
  now: environment.now
1361
1623
  });
1362
1624
  if (!login) {
1363
- environment.out(`Section sync is waiting for you to sign in. Run: npx ace-kit login --api-url ${apiUrl}`);
1625
+ environment.out(`Section sync is waiting for you to sign in. Run: ${loginCommandFor(apiUrl)}`);
1364
1626
  return automatic ? 0 : 1;
1365
1627
  }
1366
1628
  const loaded = await loadSchemas(files, environment.workingDirectory, environment.importModule);
@@ -1381,17 +1643,17 @@ async function syncCommand(commandArguments, environment) {
1381
1643
  const defined = outcome.sections.filter((section) => !section.retired).map((section) => section.handle);
1382
1644
  const retired = outcome.sections.filter((section) => section.retired).map((section) => section.handle);
1383
1645
  environment.out(
1384
- `Synced ${defined.length} section type${defined.length === 1 ? "" : "s"} (${defined.join(", ")}) to ${outcome.site.domain} (${apiUrl}).` + (retired.length > 0 ? ` No longer in your code, kept but hidden: ${retired.join(", ")}.` : "")
1646
+ `Synced ${defined.length} section type${defined.length === 1 ? "" : "s"} (${defined.join(", ")}) to ${outcome.site.domain}${apiUrl === hostedApiUrl ? "" : ` (${apiUrl})`}.` + (retired.length > 0 ? ` No longer in your code, kept but hidden: ${retired.join(", ")}.` : "")
1385
1647
  );
1386
1648
  for (const warning of outcome.warnings) environment.error(`Warning: ${warning}`);
1387
1649
  return 0;
1388
1650
  }
1389
1651
  case "login-ended":
1390
- environment.out(`Your login has ended. Run: npx ace-kit login --api-url ${apiUrl}`);
1652
+ environment.out(`Your login has ended. Run: ${loginCommandFor(apiUrl)}`);
1391
1653
  return automatic ? 0 : 1;
1392
1654
  case "site-not-found":
1393
1655
  environment.error(
1394
- `The CMS at ${apiUrl} has no site with the domain "${outcome.domain}" that you can see` + (outcome.available.length > 0 ? ` (your sites: ${outcome.available.join(", ")})` : "") + ". Set ACE_SITE_DOMAIN or pass --site."
1656
+ `${cmsName(apiUrl)} has no site with the domain "${outcome.domain}" that you can see` + (outcome.available.length > 0 ? ` (your sites: ${outcome.available.join(", ")})` : "") + ". Set ACE_SITE_DOMAIN or pass --site."
1395
1657
  );
1396
1658
  return 1;
1397
1659
  case "forbidden":
@@ -1399,7 +1661,7 @@ async function syncCommand(commandArguments, environment) {
1399
1661
  environment.error(outcome.message);
1400
1662
  return 1;
1401
1663
  case "unreachable":
1402
- environment.error(`Could not reach the CMS at ${apiUrl}.`);
1664
+ environment.error(`Could not reach ${cmsName(apiUrl)}.`);
1403
1665
  return 1;
1404
1666
  }
1405
1667
  }
@@ -1409,6 +1671,7 @@ var usage = `ace-kit: tools for sites built on CMS Ace.
1409
1671
 
1410
1672
  Commands:
1411
1673
  ace-kit create Make a new site from a starter.
1674
+ ace-kit doctor Check this site is set up to work with CMS Ace.
1412
1675
  ace-kit generate Write TypeScript types for your content types.
1413
1676
  ace-kit sync Send the section types your code defines to the CMS.
1414
1677
  ace-kit login Sign in from the command line (opens the admin app).
@@ -1422,6 +1685,8 @@ async function runCli(commandArguments, environment) {
1422
1685
  switch (command) {
1423
1686
  case "create":
1424
1687
  return createCommand(rest, environment);
1688
+ case "doctor":
1689
+ return doctorCommand(rest, environment);
1425
1690
  case "generate":
1426
1691
  return generateCommand(rest, environment);
1427
1692
  case "sync":
@@ -1447,6 +1712,7 @@ ${usage}`);
1447
1712
  }
1448
1713
  export {
1449
1714
  createUsage,
1715
+ doctorUsage,
1450
1716
  generateUsage,
1451
1717
  loginUsage,
1452
1718
  runCli,