blodemd 0.0.6 → 0.0.8

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.
Files changed (34) hide show
  1. package/README.md +2 -2
  2. package/dev-server/app/favicon.ico +0 -0
  3. package/dev-server/next-env.d.ts +5 -0
  4. package/dev-server/package.json +1 -1
  5. package/dev-server/tsconfig.json +3 -2
  6. package/dist/cli.mjs +209 -53
  7. package/dist/cli.mjs.map +1 -1
  8. package/docs/app/globals.css +4 -2
  9. package/docs/components/docs/doc-header.tsx +35 -17
  10. package/docs/components/docs/doc-shell.tsx +46 -22
  11. package/docs/components/docs/doc-sidebar.tsx +13 -8
  12. package/docs/components/docs/mobile-nav.tsx +150 -152
  13. package/docs/components/icons/doc-icon.tsx +96 -0
  14. package/docs/components/mdx/card.tsx +60 -54
  15. package/docs/components/mdx/icon.tsx +2 -46
  16. package/docs/components/mdx/index.tsx +12 -1
  17. package/docs/components/mdx/tree.tsx +7 -7
  18. package/docs/components/ui/search.tsx +11 -7
  19. package/docs/lib/mdx.ts +2 -5
  20. package/docs/lib/navigation.ts +2 -2
  21. package/docs/lib/routes.ts +34 -0
  22. package/docs/lib/shiki.ts +6 -1
  23. package/package.json +13 -5
  24. package/packages/@repo/contracts/dist/tenant.d.ts +12 -0
  25. package/packages/@repo/contracts/dist/tenant.d.ts.map +1 -1
  26. package/packages/@repo/contracts/dist/tenant.js +20 -0
  27. package/packages/@repo/contracts/src/tenant.ts +38 -0
  28. package/packages/@repo/previewing/dist/fs-source.d.ts.map +1 -1
  29. package/packages/@repo/previewing/dist/fs-source.js +1 -8
  30. package/packages/@repo/previewing/src/fs-source.ts +1 -8
  31. package/packages/@repo/validation/src/mintlify-docs-schema.json +1 -1
  32. package/scripts/prepare-package.mjs +39 -0
  33. package/packages/@repo/common/src/common.unit.test.ts +0 -55
  34. package/packages/@repo/previewing/src/index.unit.test.ts +0 -290
package/README.md CHANGED
@@ -23,7 +23,7 @@ Or run without installing:
23
23
  npx blodemd
24
24
  ```
25
25
 
26
- Requires Node.js 18+.
26
+ Requires Node.js 20.17+ and <25.
27
27
 
28
28
  ## Quick Start
29
29
 
@@ -90,7 +90,7 @@ The CLI looks for a `docs.json` file in the docs directory. Minimal example:
90
90
 
91
91
  ```json
92
92
  {
93
- "$schema": "https://mintlify.com/docs.json",
93
+ "$schema": "https://docs.blode.md/docs.json",
94
94
  "name": "my-project",
95
95
  "theme": "mint",
96
96
  "colors": { "primary": "#0D9373" },
Binary file
@@ -0,0 +1,5 @@
1
+ /// <reference types="next" />
2
+ /// <reference types="next/image-types/global" />
3
+
4
+ // NOTE: This file should not be edited
5
+ // see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
@@ -4,7 +4,7 @@
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "scripts": {
7
- "dev": "next dev",
7
+ "dev": "next dev --webpack",
8
8
  "build": "next build",
9
9
  "start": "next start",
10
10
  "lint": "oxlint .",
@@ -6,7 +6,7 @@
6
6
  "esModuleInterop": true,
7
7
  "incremental": false,
8
8
  "isolatedModules": true,
9
- "jsx": "preserve",
9
+ "jsx": "react-jsx",
10
10
  "lib": [
11
11
  "es2022",
12
12
  "DOM",
@@ -44,6 +44,7 @@
44
44
  "**/*.tsx",
45
45
  "next-env.d.ts",
46
46
  "next.config.js",
47
- ".next/types/**/*.ts"
47
+ ".next/types/**/*.ts",
48
+ ".next/dev/types/**/*.ts"
48
49
  ]
49
50
  }
package/dist/cli.mjs CHANGED
@@ -9,12 +9,14 @@ import { Command } from "commander";
9
9
  import open from "open";
10
10
  import { homedir } from "node:os";
11
11
  import { once } from "node:events";
12
+ import { createServer } from "node:net";
12
13
  import { setTimeout as setTimeout$1 } from "node:timers/promises";
13
14
  import { fileURLToPath } from "node:url";
14
15
  import { createFsSource, loadSiteConfig } from "@repo/previewing";
15
16
  import { watch } from "chokidar";
16
- import { createServer } from "node:http";
17
+ import { createServer as createServer$1 } from "node:http";
17
18
  import { createHash, randomBytes } from "node:crypto";
19
+ import { readFileSync } from "node:fs";
18
20
  //#region src/constants.ts
19
21
  const CLI_NAME = "blodemd";
20
22
  const OAUTH_CLIENT_ID = "6b5f9860-fe96-4a83-b1ad-266260523c91";
@@ -289,9 +291,20 @@ const resolveTokenStatus = (token) => {
289
291
  };
290
292
  };
291
293
  //#endregion
294
+ //#region src/site-config.ts
295
+ const CONFIG_FILE$2 = "docs.json";
296
+ const loadValidatedSiteConfig = async (root) => {
297
+ const result = await loadSiteConfig(createFsSource(root));
298
+ if (!result.ok) throw new CliError(result.errors.join("\n"), EXIT_CODES.VALIDATION, `Make sure ${CONFIG_FILE$2} exists and is valid JSON.`);
299
+ return {
300
+ config: result.config,
301
+ warnings: result.warnings
302
+ };
303
+ };
304
+ //#endregion
292
305
  //#region src/dev/resolve-root.ts
293
306
  const CONFIG_FILE$1 = "docs.json";
294
- const fileExists$2 = async (filePath) => {
307
+ const fileExists$1 = async (filePath) => {
295
308
  try {
296
309
  await fs.access(filePath);
297
310
  return true;
@@ -299,21 +312,17 @@ const fileExists$2 = async (filePath) => {
299
312
  return false;
300
313
  }
301
314
  };
302
- const resolveDocsRoot$1 = async (dir) => {
315
+ const resolveDocsRoot = async (dir) => {
303
316
  if (dir) return path.resolve(process.cwd(), dir);
304
317
  const candidates = [
305
318
  process.cwd(),
306
319
  path.join(process.cwd(), "docs"),
307
320
  path.join(process.cwd(), "apps/docs")
308
321
  ];
309
- for (const candidate of candidates) if (await fileExists$2(path.join(candidate, CONFIG_FILE$1))) return candidate;
322
+ for (const candidate of candidates) if (await fileExists$1(path.join(candidate, CONFIG_FILE$1))) return candidate;
310
323
  return process.cwd();
311
324
  };
312
- const validateDocsRoot = async (root) => {
313
- const result = await loadSiteConfig(createFsSource(root));
314
- if (!result.ok) throw new CliError(result.errors.join("\n"), EXIT_CODES.VALIDATION, `Make sure ${CONFIG_FILE$1} exists and is valid JSON.`);
315
- return result;
316
- };
325
+ const validateDocsRoot = async (root) => await loadValidatedSiteConfig(root);
317
326
  //#endregion
318
327
  //#region src/dev/watcher.ts
319
328
  const INVALIDATE_ENDPOINT = "/blodemd-dev/invalidate";
@@ -376,12 +385,20 @@ const createDevWatcher = ({ port, root }) => {
376
385
  //#region src/dev/command.ts
377
386
  const DEV_READY_ENDPOINT = "/blodemd-dev/version";
378
387
  const DEV_READY_TIMEOUT_MS = 45e3;
388
+ const DEV_PORT_SCAN_LIMIT = 10;
389
+ const DEV_SHUTDOWN_TIMEOUT_MS = 5e3;
390
+ const LOCALHOST = "127.0.0.1";
391
+ const RUNTIME_EXCLUDE_DIRS = new Set([
392
+ ".next",
393
+ ".turbo",
394
+ "node_modules"
395
+ ]);
379
396
  const parsePositiveInteger$1 = (value, label) => {
380
397
  const parsed = Number.parseInt(value, 10);
381
398
  if (!Number.isInteger(parsed) || parsed <= 0) throw new CliError(`${label} must be a positive integer.`, EXIT_CODES.VALIDATION);
382
399
  return parsed;
383
400
  };
384
- const fileExists$1 = async (filePath) => {
401
+ const fileExists = async (filePath) => {
385
402
  try {
386
403
  await fs.access(filePath);
387
404
  return true;
@@ -389,11 +406,115 @@ const fileExists$1 = async (filePath) => {
389
406
  return false;
390
407
  }
391
408
  };
409
+ const probePortAvailability = async (port) => {
410
+ const server = createServer();
411
+ const listening = (async () => {
412
+ await once(server, "listening");
413
+ return { kind: "listening" };
414
+ })();
415
+ const errored = (async () => {
416
+ const [error] = await once(server, "error");
417
+ return {
418
+ error,
419
+ kind: "error"
420
+ };
421
+ })();
422
+ server.listen({
423
+ exclusive: true,
424
+ host: LOCALHOST,
425
+ port
426
+ });
427
+ const outcome = await Promise.race([listening, errored]);
428
+ if (outcome.kind === "error") {
429
+ if (outcome.error.code === "EADDRINUSE" || outcome.error.code === "EACCES") return false;
430
+ throw outcome.error;
431
+ }
432
+ server.close();
433
+ await once(server, "close");
434
+ return true;
435
+ };
436
+ const resolveDevPort = async (requestedPort, probePort = probePortAvailability) => {
437
+ for (let offset = 0; offset < DEV_PORT_SCAN_LIMIT; offset += 1) {
438
+ const candidate = requestedPort + offset;
439
+ if (candidate > 65535) break;
440
+ if (await probePort(candidate)) return candidate;
441
+ }
442
+ throw new CliError(`No available port found within ${DEV_PORT_SCAN_LIMIT} attempts starting at ${requestedPort}.`, EXIT_CODES.ERROR, "Close the process using the port or pass a different --port value.");
443
+ };
444
+ const shutdownChildProcess = async (child, timeoutMs = DEV_SHUTDOWN_TIMEOUT_MS) => {
445
+ if (child.exitCode !== null) return;
446
+ const timer = setTimeout(() => {
447
+ if (child.exitCode === null) child.kill("SIGKILL");
448
+ }, timeoutMs);
449
+ const exitPromise = once(child, "exit");
450
+ try {
451
+ child.kill("SIGTERM");
452
+ } catch (error) {
453
+ clearTimeout(timer);
454
+ if (error.code === "ESRCH") return;
455
+ throw error;
456
+ }
457
+ await exitPromise.finally(() => {
458
+ clearTimeout(timer);
459
+ });
460
+ };
392
461
  /**
393
462
  * Derive the CLI npm package root from the running script path.
394
463
  * The CLI entry point is at `<pkg-root>/dist/cli.mjs`.
395
464
  */
396
465
  const resolveCliPackageRoot = (cliFilePath) => path.dirname(path.dirname(cliFilePath));
466
+ const copyStandaloneTree = async (sourceDir, targetDir) => {
467
+ await fs.cp(sourceDir, targetDir, {
468
+ filter: (source) => {
469
+ const relative = path.relative(sourceDir, source);
470
+ if (!relative) return true;
471
+ const topSegment = relative.split(path.sep)[0] ?? "";
472
+ return !RUNTIME_EXCLUDE_DIRS.has(topSegment);
473
+ },
474
+ recursive: true
475
+ });
476
+ };
477
+ const isStandaloneCliInstall = async (cliPackageRoot) => {
478
+ try {
479
+ return (await fs.realpath(cliPackageRoot)).split(path.sep).includes("node_modules");
480
+ } catch {
481
+ return cliPackageRoot.split(path.sep).includes("node_modules");
482
+ }
483
+ };
484
+ const materializeStandaloneRuntime = async (cliPackageRoot) => {
485
+ const runtimeRoot = path.join(CONFIG_DIR, "standalone-runtime");
486
+ await fs.rm(runtimeRoot, {
487
+ force: true,
488
+ recursive: true
489
+ });
490
+ await fs.mkdir(runtimeRoot, { recursive: true });
491
+ for (const dir of [
492
+ "dev-server",
493
+ "docs",
494
+ "packages"
495
+ ]) await copyStandaloneTree(path.join(cliPackageRoot, dir), path.join(runtimeRoot, dir));
496
+ await fs.symlink(path.join(cliPackageRoot, "node_modules"), path.join(runtimeRoot, "node_modules"), process.platform === "win32" ? "junction" : "dir");
497
+ await fs.writeFile(path.join(runtimeRoot, "dev-server", "package.json"), `${JSON.stringify({
498
+ dependencies: {
499
+ next: "16.2.1",
500
+ react: "^19.2.0",
501
+ "react-dom": "^19.2.0"
502
+ },
503
+ devDependencies: {
504
+ "@types/node": "^22.19.15",
505
+ "@types/react": "19.2.14",
506
+ "@types/react-dom": "19.2.3",
507
+ typescript: "6.0.2"
508
+ },
509
+ name: "blodemd-dev-server",
510
+ private: true,
511
+ type: "module"
512
+ }, null, 2)}\n`);
513
+ return {
514
+ devServerDir: path.join(runtimeRoot, "dev-server"),
515
+ packagesDir: path.join(runtimeRoot, "packages")
516
+ };
517
+ };
397
518
  /**
398
519
  * Check if a shipped dev-server exists alongside the CLI (npm-installed mode).
399
520
  * Verifies both the dev-server directory AND that `next` is resolvable
@@ -401,16 +522,19 @@ const resolveCliPackageRoot = (cliFilePath) => path.dirname(path.dirname(cliFile
401
522
  */
402
523
  const findStandaloneDevServer = async (cliPackageRoot) => {
403
524
  const devServerDir = path.join(cliPackageRoot, "dev-server");
404
- if (!await fileExists$1(path.join(devServerDir, "next.config.js"))) return null;
525
+ if (!await fileExists(path.join(devServerDir, "next.config.js"))) return null;
526
+ if (!await isStandaloneCliInstall(cliPackageRoot)) return null;
405
527
  try {
406
528
  createRequire(path.join(cliPackageRoot, "package.json")).resolve("next/package.json");
407
529
  } catch {
408
530
  return null;
409
531
  }
532
+ const runtime = await materializeStandaloneRuntime(cliPackageRoot);
410
533
  return {
411
- devServerDir,
534
+ devServerDir: runtime.devServerDir,
412
535
  mode: "standalone",
413
- packagesDir: path.join(cliPackageRoot, "packages")
536
+ nextPackageRoot: cliPackageRoot,
537
+ packagesDir: runtime.packagesDir
414
538
  };
415
539
  };
416
540
  /**
@@ -424,7 +548,7 @@ const findMonorepoRoot = async (start) => {
424
548
  let current = start;
425
549
  while (true) {
426
550
  const packageJsonPath = path.join(current, "package.json");
427
- if (await fileExists$1(packageJsonPath)) {
551
+ if (await fileExists(packageJsonPath)) {
428
552
  const raw = await fs.readFile(packageJsonPath, "utf8");
429
553
  const workspaces = JSON.parse(raw).workspaces ?? [];
430
554
  if (workspaces.includes("apps/*") && workspaces.includes("packages/*")) return current;
@@ -445,8 +569,12 @@ const resolveDevServer = async (cliFilePath) => {
445
569
  };
446
570
  const spawnDevServer = (server, { root, port }) => {
447
571
  if (server.mode === "standalone") {
448
- const nextBin = resolveNextBin(path.dirname(server.devServerDir));
449
- return spawn(process.execPath, [nextBin, "dev"], {
572
+ const nextBin = resolveNextBin(server.nextPackageRoot);
573
+ return spawn(process.execPath, [
574
+ nextBin,
575
+ "dev",
576
+ "--webpack"
577
+ ], {
450
578
  cwd: server.devServerDir,
451
579
  env: {
452
580
  ...process.env,
@@ -490,14 +618,14 @@ const waitForServer = async ({ child, port }) => {
490
618
  const devCommand = async ({ dir, openBrowser, port: portValue }) => {
491
619
  intro(chalk.bold("blodemd dev"));
492
620
  try {
493
- const port = parsePositiveInteger$1(portValue, "Port");
494
- const root = await resolveDocsRoot$1(dir);
621
+ const resolvedPort = await resolveDevPort(parsePositiveInteger$1(portValue, "Port"));
622
+ const root = await resolveDocsRoot(dir);
495
623
  await validateDocsRoot(root);
496
624
  const server = await resolveDevServer(fileURLToPath(import.meta.url));
497
- const localUrl = `http://localhost:${port}`;
625
+ const localUrl = `http://localhost:${resolvedPort}`;
498
626
  log.info(`Docs root: ${chalk.cyan(root)}`);
499
627
  const child = spawnDevServer(server, {
500
- port,
628
+ port: resolvedPort,
501
629
  root
502
630
  });
503
631
  let watcher = null;
@@ -509,17 +637,17 @@ const devCommand = async ({ dir, openBrowser, port: portValue }) => {
509
637
  await watcher.close();
510
638
  watcher = null;
511
639
  }
512
- if (child.exitCode === null && !child.killed) child.kill("SIGTERM");
640
+ await shutdownChildProcess(child);
513
641
  };
514
642
  process.once("SIGINT", closeAll);
515
643
  process.once("SIGTERM", closeAll);
516
644
  try {
517
645
  await waitForServer({
518
646
  child,
519
- port
647
+ port: resolvedPort
520
648
  });
521
649
  watcher = await createDevWatcher({
522
- port,
650
+ port: resolvedPort,
523
651
  root
524
652
  });
525
653
  log.success(`Dev server running at ${chalk.cyan(localUrl)}`);
@@ -562,7 +690,7 @@ const waitForOAuthCode = (options) => {
562
690
  });
563
691
  for (const socket of sockets) socket.destroy();
564
692
  };
565
- const httpServer = createServer((request, response) => {
693
+ const httpServer = createServer$1((request, response) => {
566
694
  if (!request.url) {
567
695
  response.writeHead(400, { "content-type": "text/html; charset=utf-8" });
568
696
  response.end(errorHtml("Missing request URL"));
@@ -619,6 +747,53 @@ const createOAuthState = () => randomBytes(24).toString("hex");
619
747
  const createCodeVerifier = () => randomBytes(64).toString("base64url");
620
748
  const createCodeChallenge = (verifier) => createHash("sha256").update(verifier).digest().toString("base64url");
621
749
  //#endregion
750
+ //#region src/runtime.ts
751
+ const MIN_SUPPORTED_NODE_VERSION = [
752
+ 20,
753
+ 17,
754
+ 0
755
+ ];
756
+ const MAX_SUPPORTED_NODE_MAJOR = 25;
757
+ const SUPPORTED_NODE_RANGE = ">=20.17.0 <25";
758
+ const parseVersion = (input) => {
759
+ const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(input.trim());
760
+ if (!match) return null;
761
+ const [, majorText = "", minorText = "", patchText = ""] = match;
762
+ if (!majorText || !minorText || !patchText) return null;
763
+ const major = Number.parseInt(majorText, 10);
764
+ const minor = Number.parseInt(minorText, 10);
765
+ const patch = Number.parseInt(patchText, 10);
766
+ if ([
767
+ major,
768
+ minor,
769
+ patch
770
+ ].some((value) => Number.isNaN(value))) return null;
771
+ return [
772
+ major,
773
+ minor,
774
+ patch
775
+ ];
776
+ };
777
+ const isSupportedNodeVersion = (version) => {
778
+ const parsed = parseVersion(version);
779
+ if (!parsed) return false;
780
+ const [major, minor, patch] = parsed;
781
+ const [minMajor, minMinor, minPatch] = MIN_SUPPORTED_NODE_VERSION;
782
+ if (major >= MAX_SUPPORTED_NODE_MAJOR) return false;
783
+ if (major !== minMajor) return major > minMajor;
784
+ if (minor !== minMinor) return minor > minMinor;
785
+ return patch >= minPatch;
786
+ };
787
+ const assertSupportedNodeVersion = (version = process.versions.node) => {
788
+ if (isSupportedNodeVersion(version)) return;
789
+ throw new CliError(`blodemd requires Node.js ${SUPPORTED_NODE_RANGE}. Current version: ${version}.`, EXIT_CODES.VALIDATION, "Install a supported Node.js version and try again.");
790
+ };
791
+ const readCliVersion = (moduleUrl) => {
792
+ const moduleDir = path.dirname(fileURLToPath(moduleUrl));
793
+ const raw = readFileSync(path.resolve(moduleDir, "..", "package.json"), "utf8");
794
+ return JSON.parse(raw).version ?? "0.0.0";
795
+ };
796
+ //#endregion
622
797
  //#region src/cli.ts
623
798
  const CONFIG_FILE = "docs.json";
624
799
  const TEXT_CONTENT_TYPES = {
@@ -638,31 +813,6 @@ const ensureFile = async (filePath, content) => {
638
813
  await fs.writeFile(filePath, content, { flag: "wx" });
639
814
  } catch {}
640
815
  };
641
- const fileExists = async (filePath) => {
642
- try {
643
- await fs.access(filePath);
644
- return true;
645
- } catch {
646
- return false;
647
- }
648
- };
649
- const readConfig = async (root) => {
650
- const raw = await fs.readFile(path.join(root, CONFIG_FILE), "utf8");
651
- return {
652
- name: JSON.parse(raw).name,
653
- raw
654
- };
655
- };
656
- const resolveDocsRoot = async (dir) => {
657
- if (dir) return path.resolve(process.cwd(), dir);
658
- const candidates = [
659
- process.cwd(),
660
- path.join(process.cwd(), "docs"),
661
- path.join(process.cwd(), "apps/docs")
662
- ];
663
- for (const candidate of candidates) if (await fileExists(path.join(candidate, CONFIG_FILE))) return candidate;
664
- return process.cwd();
665
- };
666
816
  const readGitValue = (gitArgs) => {
667
817
  const result = spawnSync("git", gitArgs, {
668
818
  encoding: "utf8",
@@ -806,7 +956,11 @@ const uploadFiles = async (files, root, apiPath, deploymentId, headers, s) => {
806
956
  s.stop(`Uploaded ${chalk.cyan(String(files.length))} files`);
807
957
  };
808
958
  const program = new Command();
809
- program.name("blodemd").description("Blode.md CLI").version("0.0.3");
959
+ const cliVersion = readCliVersion(import.meta.url);
960
+ program.name("blodemd").description("Blode.md CLI").version(cliVersion);
961
+ program.hook("preAction", () => {
962
+ assertSupportedNodeVersion();
963
+ });
810
964
  program.command("login").description("Authenticate with Blode.md").option("--token", "Paste an API key instead of using browser login").option("--port <port>", "Loopback callback port", String(DEFAULT_OAUTH_CALLBACK_PORT)).option("--timeout <seconds>", "OAuth timeout in seconds", String(180)).option("--no-open", "Print URL instead of opening the browser").action(async (options) => {
811
965
  intro(chalk.bold("blodemd login"));
812
966
  try {
@@ -916,7 +1070,7 @@ program.command("init").description("Scaffold a docs folder").argument("[dir]",
916
1070
  const root = path.resolve(process.cwd(), dir);
917
1071
  await fs.mkdir(root, { recursive: true });
918
1072
  await ensureFile(path.join(root, CONFIG_FILE), `${JSON.stringify({
919
- $schema: "https://mintlify.com/docs.json",
1073
+ $schema: "https://docs.blode.md/docs.json",
920
1074
  colors: { primary: "#0D9373" },
921
1075
  name: "my-project",
922
1076
  navigation: { groups: [{
@@ -936,7 +1090,8 @@ program.command("init").description("Scaffold a docs folder").argument("[dir]",
936
1090
  program.command("validate").description("Validate docs.json").argument("[dir]", "docs directory").action(async (dir) => {
937
1091
  intro(chalk.bold("blodemd validate"));
938
1092
  try {
939
- await readConfig(await resolveDocsRoot(dir));
1093
+ const { warnings } = await loadValidatedSiteConfig(await resolveDocsRoot(dir));
1094
+ for (const warning of warnings) log.warn(warning);
940
1095
  log.success(`${chalk.cyan(CONFIG_FILE)} is valid.`);
941
1096
  log.info("Done");
942
1097
  } catch (error) {
@@ -949,8 +1104,9 @@ program.command("push").description("Deploy docs").argument("[dir]", "docs direc
949
1104
  try {
950
1105
  const root = await resolveDocsRoot(dir);
951
1106
  s.start("Validating configuration");
952
- const config = await readConfig(root);
1107
+ const { config, warnings } = await loadValidatedSiteConfig(root);
953
1108
  s.stop("Configuration valid");
1109
+ for (const warning of warnings) log.warn(warning);
954
1110
  const { project, apiUrl, authToken, branch, commitMessage } = await resolvePushConfig(config, options);
955
1111
  s.start("Collecting files");
956
1112
  const files = await collectFiles(root);