@farming-labs/docs 0.2.0 → 0.2.2

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.
@@ -27,7 +27,11 @@ function parseFlags(argv) {
27
27
  "algolia",
28
28
  "verbose",
29
29
  "host",
30
- "json"
30
+ "json",
31
+ "network",
32
+ "analytics",
33
+ "ask-ai",
34
+ "deploy"
31
35
  ]);
32
36
  for (let i = 0; i < argv.length; i++) {
33
37
  const arg = argv[i];
@@ -85,36 +89,45 @@ async function main() {
85
89
  apiBaseUrl: typeof flags["api-base-url"] === "string" ? flags["api-base-url"] : typeof flags.url === "string" ? flags.url : void 0,
86
90
  apiKey: typeof flags["api-key"] === "string" ? flags["api-key"] : void 0,
87
91
  apiKeyEnv: typeof flags["api-key-env"] === "string" ? flags["api-key-env"] : void 0,
88
- json: typeof flags.json === "boolean" ? flags.json : void 0
92
+ json: typeof flags.json === "boolean" ? flags.json : void 0,
93
+ network: typeof flags.network === "boolean" ? flags.network : void 0,
94
+ checkTargets: [
95
+ ...flags.deploy === true ? ["deploy"] : [],
96
+ ...flags.analytics === true ? ["analytics"] : [],
97
+ ...flags["ask-ai"] === true ? ["ask-ai"] : []
98
+ ]
89
99
  };
90
100
  if (!parsedCommand.command || parsedCommand.command === "init") {
91
- const { init } = await import("../init-CZx_vasZ.mjs");
101
+ const { init } = await import("../init-DeeHyP-N.mjs");
92
102
  await init(initOptions);
93
103
  } else if (parsedCommand.command === "dev") {
94
104
  const { dev } = await import("../dev-D58nBPBv.mjs");
95
105
  await dev(devOptions);
96
106
  } else if (parsedCommand.command === "deploy") {
97
- const { runCloudDeploy } = await import("../cloud-BBg-41RG.mjs");
107
+ const { runCloudDeploy } = await import("../cloud-DLc03Z41.mjs");
98
108
  await runCloudDeploy(cloudOptions);
99
109
  } else if (parsedCommand.command === "preview") {
100
- const { runCloudPreview } = await import("../cloud-BBg-41RG.mjs");
110
+ const { runCloudPreview } = await import("../cloud-DLc03Z41.mjs");
101
111
  await runCloudPreview(cloudOptions);
102
112
  } else if (parsedCommand.command === "cloud" && subcommand === "deploy") {
103
- const { runCloudDeploy } = await import("../cloud-BBg-41RG.mjs");
113
+ const { runCloudDeploy } = await import("../cloud-DLc03Z41.mjs");
104
114
  await runCloudDeploy(cloudOptions);
105
115
  } else if (parsedCommand.command === "cloud" && subcommand === "preview") {
106
- const { runCloudPreview } = await import("../cloud-BBg-41RG.mjs");
116
+ const { runCloudPreview } = await import("../cloud-DLc03Z41.mjs");
107
117
  await runCloudPreview(cloudOptions);
108
118
  } else if (parsedCommand.command === "cloud" && subcommand === "init") {
109
- const { runCloudInit } = await import("../cloud-BBg-41RG.mjs");
119
+ const { runCloudInit } = await import("../cloud-DLc03Z41.mjs");
110
120
  await runCloudInit(cloudOptions);
111
121
  } else if (parsedCommand.command === "cloud" && subcommand === "sync") {
112
- const { syncCloudConfig } = await import("../cloud-BBg-41RG.mjs");
122
+ const { syncCloudConfig } = await import("../cloud-DLc03Z41.mjs");
113
123
  await syncCloudConfig(cloudOptions);
124
+ } else if (parsedCommand.command === "cloud" && subcommand === "check") {
125
+ const { runCloudCheck } = await import("../cloud-DLc03Z41.mjs");
126
+ await runCloudCheck(cloudOptions);
114
127
  } else if (parsedCommand.command === "cloud") {
115
128
  console.error(pc.red(`Unknown cloud subcommand: ${subcommand ?? "(missing)"}`));
116
129
  console.error();
117
- const { printCloudHelp } = await import("../cloud-BBg-41RG.mjs");
130
+ const { printCloudHelp } = await import("../cloud-DLc03Z41.mjs");
118
131
  printCloudHelp();
119
132
  process.exit(1);
120
133
  } else if (parsedCommand.command === "mcp") {
@@ -253,7 +266,7 @@ ${pc.dim("Commands:")}
253
266
  ${pc.cyan("dev")} Run frameworkless docs locally from ${pc.dim("docs.json")}
254
267
  ${pc.cyan("deploy")} Sync cloud config and deploy hosted preview docs
255
268
  ${pc.cyan("preview")} Alias for ${pc.cyan("deploy")}
256
- ${pc.cyan("cloud")} Docs Cloud utilities (${pc.dim("init")}, ${pc.dim("deploy")}, ${pc.dim("preview")}, ${pc.dim("sync")})
269
+ ${pc.cyan("cloud")} Docs Cloud utilities (${pc.dim("init")}, ${pc.dim("check")}, ${pc.dim("deploy")}, ${pc.dim("preview")}, ${pc.dim("sync")})
257
270
  ${pc.cyan("agent")} Agent utilities (${pc.dim("compact")} to generate sibling agent.md files)
258
271
  ${pc.cyan("agents")} AGENTS.md utilities (${pc.dim("generate")} for static agent instructions)
259
272
  ${pc.cyan("doctor")} Inspect and score agent or reader-facing docs quality
@@ -289,17 +302,22 @@ ${pc.dim("Options for dev:")}
289
302
  ${pc.cyan("--host [host]")} Expose the preview on your network; optionally pass a host value
290
303
  ${pc.cyan("--verbose")} Show raw runtime logs in addition to branded CLI output
291
304
 
292
- ${pc.dim("Options for cloud deploy:")}
305
+ ${pc.dim("Options for cloud:")}
293
306
  ${pc.cyan("cloud init")} Add Docs Cloud config to ${pc.dim("docs.config.ts")} and ${pc.dim("docs.json")}
294
307
  ${pc.cyan("deploy")} Sync ${pc.dim("docs.config.ts")} into ${pc.dim("docs.json")} and deploy hosted preview docs
295
308
  ${pc.cyan("cloud deploy")} Same as ${pc.cyan("deploy")}
296
309
  ${pc.cyan("preview")} Alias for ${pc.cyan("deploy")}
297
310
  ${pc.cyan("cloud preview")} Compatibility alias for ${pc.cyan("cloud deploy")}
298
311
  ${pc.cyan("cloud sync")} Only materialize cloud settings into ${pc.dim("docs.json")}
312
+ ${pc.cyan("cloud check")} Validate Docs Cloud config, analytics envs, API key, and Ask AI wiring
299
313
  ${pc.cyan("--config <path>")} Use a custom docs config path
300
314
  ${pc.cyan("--api-key-env <name>")} Env var that stores the Docs Cloud API key
301
315
  ${pc.cyan("--api-base-url <url>")} Override the Docs Cloud API base URL
302
316
  ${pc.cyan("--api-key <key>")} Use an API key directly; prefer ${pc.dim("cloud.apiKey.env")}
317
+ ${pc.cyan("--analytics")} Only check Docs Cloud analytics integration
318
+ ${pc.cyan("--ask-ai")} Only check Docs Cloud Ask AI integration
319
+ ${pc.cyan("--deploy")} Only check Docs Cloud deploy integration
320
+ ${pc.cyan("--no-network")} Skip live Docs Cloud API validation for ${pc.cyan("cloud check")}
303
321
  ${pc.cyan("--json")} Print machine-readable output
304
322
  ${pc.dim("required scopes")} project:read, preview:write, jobs:read
305
323
 
@@ -12,7 +12,7 @@ const DOCS_CLOUD_SCHEMA_URL = "https://docs.farming-labs.dev/schema/docs.json";
12
12
  const DOCS_CLOUD_DEFAULT_API_KEY_ENV = "DOCS_CLOUD_API_KEY";
13
13
  const DOCS_CLOUD_DEFAULT_ANALYTICS_PROJECT_ID_ENV = "NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID";
14
14
  const DOCS_CLOUD_MISSING_API_KEY_DOCS_URL = "https://docs.farming-labs.dev/docs/cloud/deploy#missing-api-key";
15
- const DEFAULT_DOCS_CLOUD_API_BASE_URL = "https://docs-app.farming-labs.dev";
15
+ const DEFAULT_DOCS_CLOUD_API_BASE_URL = "https://api.farming-labs.dev";
16
16
  const DEFAULT_PREVIEW_TIMEOUT_MS = 300 * 1e3;
17
17
  const DEFAULT_PREVIEW_POLL_INTERVAL_MS = 2e3;
18
18
  const REQUIRED_PREVIEW_API_KEY_SCOPES = [
@@ -20,6 +20,13 @@ const REQUIRED_PREVIEW_API_KEY_SCOPES = [
20
20
  "preview:write",
21
21
  "jobs:read"
22
22
  ];
23
+ const DOCS_CLOUD_PROJECT_ID_ENVS = ["NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID", "DOCS_CLOUD_PROJECT_ID"];
24
+ const DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV = "NEXT_PUBLIC_DOCS_CLOUD_API_KEY";
25
+ const CLOUD_CHECK_TARGETS = [
26
+ "deploy",
27
+ "analytics",
28
+ "ask-ai"
29
+ ];
23
30
  function isRecord(value) {
24
31
  return Boolean(value && typeof value === "object" && !Array.isArray(value));
25
32
  }
@@ -593,8 +600,111 @@ async function materializeCloudConfig(options = {}) {
593
600
  updated
594
601
  };
595
602
  }
596
- function resolveApiBaseUrl(options) {
597
- return (options.apiBaseUrl ?? process.env.DOCS_CLOUD_API_URL ?? process.env.NEXT_PUBLIC_DOCS_CLOUD_URL ?? DEFAULT_DOCS_CLOUD_API_BASE_URL).replace(/\/+$/, "");
603
+ function readCombinedEnv(rootDir) {
604
+ const env = { ...loadProjectEnv(rootDir) };
605
+ for (const [key, value] of Object.entries(process.env)) if (typeof value === "string") env[key] = value;
606
+ return env;
607
+ }
608
+ function readEnvValue(env, name) {
609
+ if (!name) return void 0;
610
+ const value = env[name]?.trim();
611
+ return value ? value : void 0;
612
+ }
613
+ function readFirstEnv(env, names) {
614
+ for (const name of names) {
615
+ const value = readEnvValue(env, name);
616
+ if (value) return {
617
+ name,
618
+ value
619
+ };
620
+ }
621
+ }
622
+ function readConfiguredCloudApiKeyEnv(snapshot) {
623
+ const moduleEnv = snapshot.config?.cloud?.apiKey?.env?.trim();
624
+ if (moduleEnv) return moduleEnv;
625
+ const apiKeyBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["cloud", "apiKey"]);
626
+ return (apiKeyBlock ? readStringProperty(apiKeyBlock, "env") : void 0)?.trim() || void 0;
627
+ }
628
+ function readAiProvider(snapshot) {
629
+ const moduleProvider = (snapshot.config?.ai)?.provider;
630
+ if (typeof moduleProvider === "string" && moduleProvider.trim()) return moduleProvider.trim();
631
+ const aiBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["ai"]);
632
+ return (aiBlock ? readStringProperty(aiBlock, "provider") : void 0)?.trim() || void 0;
633
+ }
634
+ function readRuntimeAnalyticsDisabled(snapshot) {
635
+ const moduleAnalytics = snapshot.config?.analytics;
636
+ if (moduleAnalytics === false) return true;
637
+ if (isRecord(moduleAnalytics) && moduleAnalytics.enabled === false) return true;
638
+ if (readTopLevelBooleanProperty(snapshot.content ?? "", "analytics") === false) return true;
639
+ const analyticsBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["analytics"]);
640
+ return (analyticsBlock ? readTopLevelBooleanProperty(analyticsBlock, "enabled") : void 0) === false;
641
+ }
642
+ function isCloudAnalyticsEnabled(analytics) {
643
+ if (analytics === false) return false;
644
+ if (isRecord(analytics) && analytics.enabled === false) return false;
645
+ return typeof analytics !== "undefined";
646
+ }
647
+ function createCheck(name, status, message, details) {
648
+ return {
649
+ name,
650
+ status,
651
+ message,
652
+ ...details ? { details } : {}
653
+ };
654
+ }
655
+ function isBrowserSafeEnvName(name) {
656
+ return name.startsWith("NEXT_PUBLIC_");
657
+ }
658
+ function summarizeIdentity(identity) {
659
+ if (!isRecord(identity)) return void 0;
660
+ const workspace = isRecord(identity.workspace) ? identity.workspace : void 0;
661
+ const apiKey = isRecord(identity.apiKey) ? identity.apiKey : void 0;
662
+ const scopes = readApiKeyScopes(identity);
663
+ return {
664
+ ...workspace ? { workspace: {
665
+ ...typeof workspace.id === "string" ? { id: workspace.id } : {},
666
+ ...typeof workspace.name === "string" ? { name: workspace.name } : {}
667
+ } } : {},
668
+ ...apiKey ? { apiKey: {
669
+ ...typeof apiKey.id === "string" ? { id: apiKey.id } : {},
670
+ ...scopes.length > 0 ? { scopes } : {}
671
+ } } : {}
672
+ };
673
+ }
674
+ function formatCheckStatus(status) {
675
+ if (status === "pass") return pc.green("ok");
676
+ if (status === "warn") return pc.yellow("warn");
677
+ return pc.red("fail");
678
+ }
679
+ function countChecks(checks, status) {
680
+ return checks.filter((check) => check.status === status).length;
681
+ }
682
+ function resolveCloudCheckTargets(options) {
683
+ const targets = new Set(options.checkTargets);
684
+ if (targets.size > 0) return targets;
685
+ return new Set(CLOUD_CHECK_TARGETS);
686
+ }
687
+ function formatCloudCheckTargets(targets) {
688
+ return targets.join(", ");
689
+ }
690
+ function resolveApiBaseUrl(options, rootDir = process.cwd()) {
691
+ if (options.apiBaseUrl?.trim()) return {
692
+ url: options.apiBaseUrl.trim().replace(/\/+$/, ""),
693
+ source: "flag"
694
+ };
695
+ const projectEnv = loadProjectEnv(rootDir);
696
+ for (const envName of ["DOCS_CLOUD_API_URL", "NEXT_PUBLIC_DOCS_CLOUD_URL"]) {
697
+ const value = process.env[envName]?.trim() ?? projectEnv[envName]?.trim();
698
+ if (value) return {
699
+ url: value.replace(/\/+$/, ""),
700
+ source: "env",
701
+ env: envName
702
+ };
703
+ }
704
+ return {
705
+ url: DEFAULT_DOCS_CLOUD_API_BASE_URL,
706
+ source: "default"
707
+ };
598
708
  }
599
709
  function resolveApiKey(options, rootDir, envName) {
600
710
  if (options.apiKey?.trim()) return options.apiKey.trim();
@@ -605,6 +715,112 @@ function resolveApiKey(options, rootDir, envName) {
605
715
  if (token) return token;
606
716
  throw new Error(`Missing Docs Cloud API key. Set ${envName} in your shell or .env.local, or configure cloud.apiKey.env in docs.config.ts. See ${DOCS_CLOUD_MISSING_API_KEY_DOCS_URL}.`);
607
717
  }
718
+ function isLocalhostUrl(value) {
719
+ try {
720
+ const url = new URL(value);
721
+ return [
722
+ "localhost",
723
+ "127.0.0.1",
724
+ "::1"
725
+ ].includes(url.hostname);
726
+ } catch {
727
+ return false;
728
+ }
729
+ }
730
+ function readNestedString(value, pathSegments) {
731
+ let current = value;
732
+ for (const segment of pathSegments) {
733
+ if (!isRecord(current)) return void 0;
734
+ current = current[segment];
735
+ }
736
+ return typeof current === "string" && current.trim() ? current.trim() : void 0;
737
+ }
738
+ function readDocsSiteOrigin(snapshot, env) {
739
+ const envSite = readFirstEnv(env, [
740
+ "NEXT_PUBLIC_BASE_URL",
741
+ "NEXT_PUBLIC_SITE_URL",
742
+ "SITE_URL"
743
+ ]);
744
+ const sitemapBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["sitemap"]);
745
+ const llmsTxtBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["llmsTxt"]);
746
+ const robotsBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["robots"]);
747
+ const candidates = [
748
+ {
749
+ value: envSite?.value,
750
+ source: envSite ? envSite.name : "env"
751
+ },
752
+ {
753
+ value: readNestedString(snapshot.config, ["site", "url"]),
754
+ source: "site.url"
755
+ },
756
+ {
757
+ value: readNestedString(snapshot.config, ["sitemap", "baseUrl"]),
758
+ source: "sitemap.baseUrl"
759
+ },
760
+ {
761
+ value: readNestedString(snapshot.config, ["llmsTxt", "baseUrl"]),
762
+ source: "llmsTxt.baseUrl"
763
+ },
764
+ {
765
+ value: readNestedString(snapshot.config, ["robots", "baseUrl"]),
766
+ source: "robots.baseUrl"
767
+ },
768
+ {
769
+ value: sitemapBlock ? readStringProperty(sitemapBlock, "baseUrl") : void 0,
770
+ source: "sitemap.baseUrl"
771
+ },
772
+ {
773
+ value: llmsTxtBlock ? readStringProperty(llmsTxtBlock, "baseUrl") : void 0,
774
+ source: "llmsTxt.baseUrl"
775
+ },
776
+ {
777
+ value: robotsBlock ? readStringProperty(robotsBlock, "baseUrl") : void 0,
778
+ source: "robots.baseUrl"
779
+ }
780
+ ];
781
+ for (const candidate of candidates) {
782
+ if (!candidate.value) continue;
783
+ try {
784
+ return {
785
+ origin: new URL(candidate.value).origin,
786
+ source: candidate.source
787
+ };
788
+ } catch {}
789
+ }
790
+ }
791
+ async function checkCorsPreflight(params) {
792
+ const response = await fetch(params.url, {
793
+ method: "OPTIONS",
794
+ headers: {
795
+ Origin: params.origin,
796
+ "Access-Control-Request-Method": "POST",
797
+ "Access-Control-Request-Headers": params.requestHeaders
798
+ }
799
+ });
800
+ const allowOrigin = response.headers.get("access-control-allow-origin");
801
+ const allowMethods = response.headers.get("access-control-allow-methods");
802
+ const allowHeaders = response.headers.get("access-control-allow-headers");
803
+ const normalizedAllowOrigin = allowOrigin?.toLowerCase();
804
+ const normalizedOrigin = params.origin.toLowerCase();
805
+ return {
806
+ ok: response.ok && (normalizedAllowOrigin === "*" || normalizedAllowOrigin === normalizedOrigin) && Boolean(allowMethods?.toUpperCase().includes("POST")) && areCorsRequestHeadersAllowed(params.requestHeaders, allowHeaders),
807
+ status: response.status,
808
+ allowOrigin,
809
+ allowMethods,
810
+ allowHeaders
811
+ };
812
+ }
813
+ function areCorsRequestHeadersAllowed(requestHeaders, allowHeaders) {
814
+ const requested = parseCorsHeaderList(requestHeaders);
815
+ if (requested.length === 0) return true;
816
+ if (!allowHeaders) return false;
817
+ const allowed = parseCorsHeaderList(allowHeaders);
818
+ if (allowed.includes("*")) return true;
819
+ return requested.every((header) => allowed.includes(header));
820
+ }
821
+ function parseCorsHeaderList(value) {
822
+ return value.split(",").map((header) => header.trim().toLowerCase()).filter(Boolean);
823
+ }
608
824
  async function readJsonResponse(response) {
609
825
  const text = await response.text();
610
826
  if (!text.trim()) return {};
@@ -720,7 +936,7 @@ async function fetchCloudJson(params) {
720
936
  const body = await readJsonResponse(response);
721
937
  if (!response.ok) {
722
938
  const requestPath = new URL(params.url).pathname;
723
- if (response.status === 404 && requestPath === "/api/cloud/preview") throw new Error("Docs Cloud preview API is not available on this cloud host yet. The API key was validated, but the host did not expose /api/cloud/preview.");
939
+ if (response.status === 404 && requestPath === "/v1/cloud/preview") throw new Error("Docs Cloud preview API is not available on this cloud host yet. The API key was validated, but the host did not expose /v1/cloud/preview.");
724
940
  const message = readResponseMessage(body, `Docs Cloud request failed with HTTP ${response.status}.`);
725
941
  throw new Error(message);
726
942
  }
@@ -770,7 +986,7 @@ function readStatusLabel(body) {
770
986
  }
771
987
  async function requestPreview(params) {
772
988
  const initial = await fetchCloudJson({
773
- url: `${params.apiBaseUrl}/api/cloud/preview`,
989
+ url: `${params.apiBaseUrl}/v1/cloud/preview`,
774
990
  apiKey: params.apiKey,
775
991
  init: {
776
992
  method: "POST",
@@ -849,6 +1065,195 @@ function createSpinner(initialMessage, options = {}) {
849
1065
  }
850
1066
  };
851
1067
  }
1068
+ async function checkCloudConfig(options = {}) {
1069
+ const rootDir = options.rootDir ?? process.cwd();
1070
+ const docsJsonPath = path.join(rootDir, DOCS_JSON_FILE);
1071
+ const existing = readExistingDocsJson(docsJsonPath);
1072
+ const snapshot = await loadDocsConfigSnapshot(rootDir, options.configPath);
1073
+ const config = materializeDocsJsonObject({
1074
+ rootDir,
1075
+ snapshot,
1076
+ existing
1077
+ });
1078
+ const serialized = serializeMaterializedDocsJson(config);
1079
+ const previous = existing ? fs.readFileSync(docsJsonPath, "utf-8") : void 0;
1080
+ const apiBaseUrlResolution = resolveApiBaseUrl(options, rootDir);
1081
+ const apiBaseUrl = apiBaseUrlResolution.url;
1082
+ const apiKeyEnv = config.cloud?.apiKey?.env ?? DOCS_CLOUD_DEFAULT_API_KEY_ENV;
1083
+ const env = readCombinedEnv(rootDir);
1084
+ const siteOrigin = readDocsSiteOrigin(snapshot, env);
1085
+ const checks = [];
1086
+ const configPath = snapshot.path ?? docsJsonPath;
1087
+ const network = options.network !== false;
1088
+ const explicitApiKey = options.apiKey?.trim();
1089
+ const targetSet = resolveCloudCheckTargets(options);
1090
+ const targets = CLOUD_CHECK_TARGETS.filter((target) => targetSet.has(target));
1091
+ const checkDeploy = targetSet.has("deploy");
1092
+ const checkAnalytics = targetSet.has("analytics");
1093
+ const checkAskAi = targetSet.has("ask-ai");
1094
+ const checkProjectEnv = checkAnalytics || checkAskAi;
1095
+ let identity;
1096
+ checks.push(createCheck("config", snapshot.path ? "pass" : "warn", snapshot.path ? `Loaded ${path.relative(rootDir, snapshot.path) || "docs.config.ts"}` : `No docs.config.* found; checking ${DOCS_JSON_FILE} defaults instead.`));
1097
+ checks.push(createCheck("docs.json", !existing ? "warn" : previous === serialized ? "pass" : "warn", !existing ? `${DOCS_JSON_FILE} is missing. Run docs cloud sync to materialize cloud config.` : previous === serialized ? `${DOCS_JSON_FILE} is in sync with docs.config.` : `${DOCS_JSON_FILE} is stale. Run docs cloud sync before deploying.`));
1098
+ checks.push(createCheck("cloud.apiBaseUrl", isLocalhostUrl(apiBaseUrl) ? "warn" : "pass", apiBaseUrlResolution.source === "default" ? `Using the hosted Docs Cloud API at ${apiBaseUrl}.` : isLocalhostUrl(apiBaseUrl) ? `Docs Cloud API base URL is ${apiBaseUrl}; production docs should use the hosted API base URL.` : `Docs Cloud API base URL is ${apiBaseUrl}.`, {
1099
+ source: apiBaseUrlResolution.source,
1100
+ ...apiBaseUrlResolution.env ? { env: apiBaseUrlResolution.env } : {}
1101
+ }));
1102
+ if (checkAnalytics || checkAskAi) checks.push(createCheck("docs.siteOrigin", siteOrigin ? "pass" : "warn", siteOrigin ? `Public docs origin is ${siteOrigin.origin}.` : "Could not infer the public docs origin for CORS checks. Set NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_SITE_URL, SITE_URL, or a docs config baseUrl.", siteOrigin ? {
1103
+ origin: siteOrigin.origin,
1104
+ source: siteOrigin.source
1105
+ } : void 0));
1106
+ const apiKey = explicitApiKey || readEnvValue(env, apiKeyEnv);
1107
+ if (checkDeploy) {
1108
+ try {
1109
+ normalizeEnvName(apiKeyEnv, DOCS_CLOUD_DEFAULT_API_KEY_ENV);
1110
+ checks.push(createCheck("apiKey.config", "pass", `Using cloud.apiKey.env ${apiKeyEnv}.`));
1111
+ } catch (error) {
1112
+ checks.push(createCheck("apiKey.config", "fail", error instanceof Error ? error.message : `Invalid API key env ${apiKeyEnv}.`));
1113
+ }
1114
+ checks.push(createCheck("apiKey.value", apiKey ? "pass" : "fail", apiKey ? explicitApiKey ? "Docs Cloud API key was provided with --api-key." : `Docs Cloud API key is present in ${apiKeyEnv}.` : `Missing Docs Cloud API key. Set ${apiKeyEnv} or pass --api-key.`, {
1115
+ env: apiKeyEnv,
1116
+ source: explicitApiKey ? "flag" : apiKey ? "env" : "missing"
1117
+ }));
1118
+ }
1119
+ checks.push(createCheck("cloud.enabled", config.cloud?.enabled === false ? "fail" : "pass", config.cloud?.enabled === false ? "Docs Cloud is disabled by cloud.enabled: false." : "Docs Cloud is enabled."));
1120
+ if (checkDeploy) {
1121
+ if (config.cloud?.deploy?.enabled === false) checks.push(createCheck("deploy.enabled", "fail", "Docs Cloud deployment is disabled by cloud.deploy.enabled: false."));
1122
+ else checks.push(createCheck("deploy.enabled", "pass", "Docs Cloud deployment is enabled."));
1123
+ if (config.cloud?.preview?.enabled === false) checks.push(createCheck("preview.enabled", "fail", "Docs Cloud preview deployment is disabled by cloud.preview.enabled: false."));
1124
+ }
1125
+ const runtimeAnalyticsDisabled = readRuntimeAnalyticsDisabled(snapshot);
1126
+ const cloudAnalyticsEnabled = isCloudAnalyticsEnabled(config.cloud?.analytics);
1127
+ if (checkAnalytics) {
1128
+ if (runtimeAnalyticsDisabled) checks.push(createCheck("analytics.runtime", "fail", "Runtime analytics is disabled by analytics: false or analytics.enabled: false."));
1129
+ else checks.push(createCheck("analytics.runtime", "pass", "Runtime analytics is not disabled."));
1130
+ checks.push(createCheck("analytics.cloud", cloudAnalyticsEnabled ? "pass" : "warn", cloudAnalyticsEnabled ? "Docs Cloud analytics is enabled in cloud.analytics." : "cloud.analytics is not enabled; run docs cloud init to add the recommended analytics config."));
1131
+ }
1132
+ const projectEnv = readFirstEnv(env, DOCS_CLOUD_PROJECT_ID_ENVS);
1133
+ const analyticsNeedsProjectId = cloudAnalyticsEnabled && !runtimeAnalyticsDisabled;
1134
+ if (checkProjectEnv) checks.push(createCheck("project.env", projectEnv ? "pass" : analyticsNeedsProjectId || checkAskAi ? "fail" : "warn", projectEnv ? `Docs Cloud project id is present in ${projectEnv.name}.` : `Missing Docs Cloud project id. Set ${DOCS_CLOUD_PROJECT_ID_ENVS.join(" or ")} for analytics and docs-cloud Ask AI.`, projectEnv ? { env: projectEnv.name } : void 0));
1135
+ const aiProvider = readAiProvider(snapshot);
1136
+ let askAiCorsMode = "none";
1137
+ if (checkAskAi) if (aiProvider === "docs-cloud") {
1138
+ checks.push(createCheck("askAi.provider", "pass", "Ask AI is configured with provider: \"docs-cloud\"."));
1139
+ const configuredApiKeyEnv = readConfiguredCloudApiKeyEnv(snapshot);
1140
+ const publicApiKeyEnv = configuredApiKeyEnv && isBrowserSafeEnvName(configuredApiKeyEnv) ? configuredApiKeyEnv : readFirstEnv(env, [DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV])?.name ?? DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV;
1141
+ const publicApiKey = readEnvValue(env, publicApiKeyEnv);
1142
+ const publicProjectEnv = readFirstEnv(env, [DOCS_CLOUD_DEFAULT_ANALYTICS_PROJECT_ID_ENV]);
1143
+ const serverApiKeyEnv = configuredApiKeyEnv ?? DOCS_CLOUD_DEFAULT_API_KEY_ENV;
1144
+ const serverApiKey = readEnvValue(env, serverApiKeyEnv);
1145
+ if (publicApiKey && publicProjectEnv) {
1146
+ checks.push(createCheck("askAi.direct", "pass", `Ask AI can call the Docs Cloud knowledge endpoint directly with ${publicApiKeyEnv}.`, {
1147
+ apiKeyEnv: publicApiKeyEnv,
1148
+ projectIdEnv: publicProjectEnv.name
1149
+ }));
1150
+ askAiCorsMode = "direct";
1151
+ } else if (serverApiKey && projectEnv) {
1152
+ checks.push(createCheck("askAi.direct", "warn", `Direct browser Ask AI needs ${DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV} and ${DOCS_CLOUD_DEFAULT_ANALYTICS_PROJECT_ID_ENV}; this app can use the local docs API route with ${serverApiKeyEnv}.`, {
1153
+ apiKeyEnv: serverApiKeyEnv,
1154
+ projectIdEnv: projectEnv.name,
1155
+ proxy: true
1156
+ }));
1157
+ askAiCorsMode = "proxy";
1158
+ } else checks.push(createCheck("askAi.direct", "fail", `Ask AI docs-cloud direct mode needs ${DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV} and ${DOCS_CLOUD_DEFAULT_ANALYTICS_PROJECT_ID_ENV}.`, { apiKeyEnv: publicApiKeyEnv }));
1159
+ } else if (aiProvider) checks.push(createCheck("askAi.provider", "pass", `Ask AI provider is ${aiProvider}.`));
1160
+ else checks.push(createCheck("askAi.provider", "warn", "Ask AI is not configured with provider: \"docs-cloud\"."));
1161
+ if (checkAnalytics || checkAskAi) if (!network) checks.push(createCheck("cloud.cors", "warn", "Skipped Docs Cloud CORS validation because --no-network was passed."));
1162
+ else if (!siteOrigin) checks.push(createCheck("cloud.cors", "warn", "Could not infer the docs site origin for CORS checks. Set NEXT_PUBLIC_BASE_URL or a docs config baseUrl."));
1163
+ else {
1164
+ if (checkAnalytics) try {
1165
+ const cors = await checkCorsPreflight({
1166
+ url: `${apiBaseUrl}/v1/analytics/events`,
1167
+ origin: siteOrigin.origin,
1168
+ requestHeaders: "content-type"
1169
+ });
1170
+ checks.push(createCheck("cors.analytics", cors.ok ? "pass" : "fail", cors.ok ? `Analytics CORS allows ${siteOrigin.origin}.` : `Analytics CORS blocked ${siteOrigin.origin}.`, {
1171
+ origin: siteOrigin.origin,
1172
+ originSource: siteOrigin.source,
1173
+ status: cors.status,
1174
+ allowOrigin: cors.allowOrigin,
1175
+ allowMethods: cors.allowMethods,
1176
+ allowHeaders: cors.allowHeaders
1177
+ }));
1178
+ } catch (error) {
1179
+ checks.push(createCheck("cors.analytics", "fail", error instanceof Error ? `Analytics CORS check failed: ${error.message}` : "Analytics CORS check failed."));
1180
+ }
1181
+ if (checkAskAi && projectEnv && askAiCorsMode === "direct") try {
1182
+ const cors = await checkCorsPreflight({
1183
+ url: `${apiBaseUrl}/v1/projects/${encodeURIComponent(projectEnv.value)}/knowledge/ask`,
1184
+ origin: siteOrigin.origin,
1185
+ requestHeaders: "authorization, content-type"
1186
+ });
1187
+ checks.push(createCheck("cors.askAi", cors.ok ? "pass" : "fail", cors.ok ? `Ask AI CORS allows ${siteOrigin.origin}.` : `Ask AI CORS blocked ${siteOrigin.origin}.`, {
1188
+ origin: siteOrigin.origin,
1189
+ originSource: siteOrigin.source,
1190
+ status: cors.status,
1191
+ allowOrigin: cors.allowOrigin,
1192
+ allowMethods: cors.allowMethods,
1193
+ allowHeaders: cors.allowHeaders
1194
+ }));
1195
+ } catch (error) {
1196
+ checks.push(createCheck("cors.askAi", "fail", error instanceof Error ? `Ask AI CORS check failed: ${error.message}` : "Ask AI CORS check failed."));
1197
+ }
1198
+ else if (checkAskAi && askAiCorsMode === "proxy") checks.push(createCheck("cors.askAi", "pass", "Ask AI uses the local docs API route, so Docs Cloud browser CORS is not required.", { proxy: true }));
1199
+ }
1200
+ if (checkDeploy) if (!network) checks.push(createCheck("apiKey.network", "warn", "Skipped Docs Cloud API validation because --no-network was passed."));
1201
+ else if (!apiKey) checks.push(createCheck("apiKey.network", "warn", "Skipped Docs Cloud API validation because no API key value was available."));
1202
+ else try {
1203
+ const response = await fetchCloudJson({
1204
+ url: `${apiBaseUrl}/v1/cloud/me`,
1205
+ apiKey
1206
+ });
1207
+ identity = summarizeIdentity(response);
1208
+ checks.push(createCheck("apiKey.network", "pass", `Validated API key with ${apiBaseUrl}.`, identity));
1209
+ const scopes = readApiKeyScopes(response);
1210
+ if (scopes.length === 0) checks.push(createCheck("apiKey.scopes", "warn", "Docs Cloud validated the API key but did not return scope metadata."));
1211
+ else {
1212
+ const missing = REQUIRED_PREVIEW_API_KEY_SCOPES.filter((scope) => !scopes.includes(scope));
1213
+ checks.push(createCheck("apiKey.scopes", missing.length === 0 ? "pass" : "fail", missing.length === 0 ? `API key has required deploy scopes: ${REQUIRED_PREVIEW_API_KEY_SCOPES.join(", ")}.` : `API key is missing required deploy scope${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}.`, { scopes }));
1214
+ }
1215
+ } catch (error) {
1216
+ checks.push(createCheck("apiKey.network", "fail", error instanceof Error ? error.message : "Could not validate Docs Cloud API key."));
1217
+ }
1218
+ return {
1219
+ ok: countChecks(checks, "fail") === 0,
1220
+ apiBaseUrl,
1221
+ configPath,
1222
+ docsJsonPath,
1223
+ apiKeyEnv,
1224
+ analyticsProjectIdEnv: projectEnv?.name,
1225
+ network,
1226
+ targets,
1227
+ checks,
1228
+ ...identity ? { identity } : {}
1229
+ };
1230
+ }
1231
+ async function runCloudCheck(options = {}) {
1232
+ const result = await checkCloudConfig(options);
1233
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1234
+ else {
1235
+ console.log(pc.bold("Docs Cloud check"));
1236
+ console.log(`${pc.dim("api")} ${result.apiBaseUrl}`);
1237
+ console.log(`${pc.dim("scope")} ${formatCloudCheckTargets(result.targets)}`);
1238
+ console.log();
1239
+ for (const check of result.checks) console.log(`${formatCheckStatus(check.status)} ${pc.bold(check.name)} ${check.message}`);
1240
+ console.log();
1241
+ if (result.ok) {
1242
+ const warnings = countChecks(result.checks, "warn");
1243
+ const suffix = warnings > 0 ? ` with ${warnings} warning${warnings === 1 ? "" : "s"}` : "";
1244
+ console.log(`${pc.green("ok")} Docs Cloud check passed${suffix}.`);
1245
+ } else {
1246
+ const failures = countChecks(result.checks, "fail");
1247
+ console.log(`${pc.red("fail")} Docs Cloud check failed with ${failures} failed check${failures === 1 ? "" : "s"}.`);
1248
+ }
1249
+ }
1250
+ if (!result.ok) {
1251
+ const error = /* @__PURE__ */ new Error("Docs Cloud check failed.");
1252
+ markCliErrorReported(error);
1253
+ throw error;
1254
+ }
1255
+ return result;
1256
+ }
852
1257
  async function syncCloudConfig(options = {}) {
853
1258
  const result = await materializeCloudConfig(options);
854
1259
  if (options.json) {
@@ -918,10 +1323,10 @@ async function runCloudDeployment(options = {}) {
918
1323
  if (materialized.config.cloud?.preview?.enabled === false) throw new Error("Docs Cloud preview deployments are disabled in cloud.preview.enabled. Remove that legacy override before deploying hosted preview docs.");
919
1324
  if (materialized.config.cloud?.deploy?.enabled === false) throw new Error("Docs Cloud deployment is disabled in cloud.deploy.enabled. Set it to true before deploying hosted preview docs.");
920
1325
  const apiKey = resolveApiKey(options, rootDir, materialized.apiKeyEnv);
921
- const apiBaseUrl = resolveApiBaseUrl(options);
1326
+ const apiBaseUrl = resolveApiBaseUrl(options, rootDir).url;
922
1327
  spinner.update("Validating Docs Cloud API key");
923
1328
  const identity = await fetchCloudJson({
924
- url: `${apiBaseUrl}/api/cloud/me`,
1329
+ url: `${apiBaseUrl}/v1/cloud/me`,
925
1330
  apiKey
926
1331
  });
927
1332
  assertPreviewApiKeyScopes(identity);
@@ -970,6 +1375,7 @@ ${pc.bold("@farming-labs/docs cloud")}
970
1375
 
971
1376
  ${pc.dim("Usage:")}
972
1377
  ${pc.cyan("docs cloud init")} Add Docs Cloud config to ${pc.dim("docs.config.ts")} and ${pc.dim("docs.json")}
1378
+ ${pc.cyan("docs cloud check")} Validate Docs Cloud config, analytics envs, API key, and Ask AI wiring
973
1379
  ${pc.cyan("docs deploy")} Sync ${pc.dim("docs.config.ts")} to ${pc.dim("docs.json")} and deploy hosted preview docs
974
1380
  ${pc.cyan("docs cloud deploy")} Same as ${pc.cyan("docs deploy")}
975
1381
  ${pc.cyan("docs preview")} Compatibility alias for ${pc.cyan("docs deploy")}
@@ -981,6 +1387,10 @@ ${pc.dim("Options:")}
981
1387
  ${pc.cyan("--api-key-env <name>")} Env var that stores the Docs Cloud API key
982
1388
  ${pc.cyan("--api-base-url <url>")} Override Docs Cloud API base URL
983
1389
  ${pc.cyan("--api-key <key>")} Use an API key directly; prefer ${pc.dim("cloud.apiKey.env")}
1390
+ ${pc.cyan("--analytics")} Only check Docs Cloud analytics integration
1391
+ ${pc.cyan("--ask-ai")} Only check Docs Cloud Ask AI integration
1392
+ ${pc.cyan("--deploy")} Only check Docs Cloud deploy integration
1393
+ ${pc.cyan("--no-network")} Skip live Docs Cloud API validation for ${pc.cyan("cloud check")}
984
1394
  ${pc.cyan("--json")} Print machine-readable output
985
1395
 
986
1396
  ${pc.dim("API key scopes:")}
@@ -997,4 +1407,4 @@ ${pc.dim("Config example:")}
997
1407
  }
998
1408
 
999
1409
  //#endregion
1000
- export { initCloudConfig, printCloudHelp, runCloudDeploy, runCloudInit, runCloudPreview, syncCloudConfig };
1410
+ export { initCloudConfig, printCloudHelp, runCloudCheck, runCloudDeploy, runCloudInit, runCloudPreview, syncCloudConfig };
@@ -188,7 +188,7 @@ async function configureDocsCloudOnboarding(options) {
188
188
  enabled = cloudAnswer;
189
189
  }
190
190
  if (!enabled) return;
191
- const { initCloudConfig } = await import("./cloud-BBg-41RG.mjs");
191
+ const { initCloudConfig } = await import("./cloud-DLc03Z41.mjs");
192
192
  printDocsCloudOnboardingInstructions(await initCloudConfig({
193
193
  rootDir: options.rootDir,
194
194
  configPath: getDocsCloudConfigPathForFramework(options.framework)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farming-labs/docs",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "Modern, flexible MDX-based docs framework — core types, config, and CLI",
5
5
  "keywords": [
6
6
  "docs",