@farming-labs/docs 0.2.62 → 0.2.64

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 (58) hide show
  1. package/dist/agent-CQTH7NFu.mjs +624 -0
  2. package/dist/agent-DKKptIgy.mjs +4365 -0
  3. package/dist/agent-evals-B7MIxuEX.mjs +2144 -0
  4. package/dist/agent-export-CBgWgPvH.mjs +910 -0
  5. package/dist/agent-scope-C_U--OZ7.mjs +283 -0
  6. package/dist/agent-skills-bundle.d.mts +13 -0
  7. package/dist/agent-skills-bundle.mjs +12 -0
  8. package/dist/agent-skills-server-CPja6Syt.d.mts +14 -0
  9. package/dist/agent-skills-server-DraIb6FV.mjs +415 -0
  10. package/dist/agent-skills-vite.d.mts +31 -0
  11. package/dist/agent-skills-vite.mjs +70 -0
  12. package/dist/agents-XWZBub6f.mjs +221 -0
  13. package/dist/analytics-Bx44lg6d.mjs +177 -0
  14. package/dist/cli/index.d.mts +15 -0
  15. package/dist/cli/index.mjs +452 -0
  16. package/dist/client/react.d.mts +45 -0
  17. package/dist/client/react.mjs +223 -0
  18. package/dist/cloud-analytics-CSyFE6SS.mjs +132 -0
  19. package/dist/cloud-ask-ai-sbpjOR2K.mjs +382 -0
  20. package/dist/cloud-ask-ai-zpwkdwnF.d.mts +23 -0
  21. package/dist/cloud-pdNC-tyj.mjs +1615 -0
  22. package/dist/code-blocks-DnNVNK2M.mjs +871 -0
  23. package/dist/codeblocks-CFuurVIH.mjs +250 -0
  24. package/dist/config-Wcdj-D0a.mjs +369 -0
  25. package/dist/dev-Cmy6DtdF.mjs +1333 -0
  26. package/dist/docs-cloud-server.d.mts +70 -0
  27. package/dist/docs-cloud-server.mjs +310 -0
  28. package/dist/doctor-DtGYZ41i.mjs +2036 -0
  29. package/dist/downgrade-w7e6Se0L.mjs +184 -0
  30. package/dist/errors-DbOhkE1h.mjs +20 -0
  31. package/dist/golden-evaluations-Dj-9Eo3v.mjs +1785 -0
  32. package/dist/i18n-CCaFUnAN.mjs +40 -0
  33. package/dist/index.d.mts +1150 -0
  34. package/dist/index.mjs +10 -0
  35. package/dist/init-CQY0Woe3.mjs +1264 -0
  36. package/dist/mcp-B9dcsivk.mjs +156 -0
  37. package/dist/mcp.d.mts +298 -0
  38. package/dist/mcp.mjs +4430 -0
  39. package/dist/metadata-DWExHQnx.mjs +237 -0
  40. package/dist/package-version-n5AFur8a.mjs +128 -0
  41. package/dist/reading-time-CYZ5VvKU.mjs +742 -0
  42. package/dist/review-CLoHTywU.mjs +673 -0
  43. package/dist/robots-BIpC4j4P.mjs +201 -0
  44. package/dist/robots-CUTahhoY.mjs +179 -0
  45. package/dist/search-B6V6qtiI.mjs +1826 -0
  46. package/dist/search-CaSyi6H6.d.mts +279 -0
  47. package/dist/search-DSjCeOk7.mjs +104 -0
  48. package/dist/server.d.mts +343 -0
  49. package/dist/server.mjs +14 -0
  50. package/dist/sitemap-Cykpe3Tz.mjs +249 -0
  51. package/dist/sitemap-server-C_6Wes83.mjs +1137 -0
  52. package/dist/standards-discovery-C4HUqMd2.d.mts +227 -0
  53. package/dist/standards-discovery-jkykaXq1.mjs +519 -0
  54. package/dist/templates-Bq_P7ctv.mjs +2465 -0
  55. package/dist/types-lMBIdZg0.d.mts +3315 -0
  56. package/dist/upgrade-oz-GChgt.mjs +56 -0
  57. package/dist/utils-DpiIioYb.mjs +225 -0
  58. package/package.json +1 -1
@@ -0,0 +1,1615 @@
1
+ import { n as markCliErrorReported } from "./errors-DbOhkE1h.mjs";
2
+ import { _ as resolveDocsContentDir, a as loadDocsConfigModule, c as loadProjectEnv, d as readNavTitle, g as resolveDocsConfigPath, h as readTopLevelStringProperty, m as readTopLevelBooleanProperty, p as readStringProperty, t as extractNestedObjectLiteral } from "./config-Wcdj-D0a.mjs";
3
+ import { i as detectPackageManagerFromProject, t as detectFramework } from "./utils-DpiIioYb.mjs";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { execFileSync } from "node:child_process";
7
+ import pc from "picocolors";
8
+
9
+ //#region src/cli/cloud.ts
10
+ const DOCS_JSON_FILE = "docs.json";
11
+ const DOCS_CLOUD_SCHEMA_URL = "https://docs.farming-labs.dev/schema/docs.json";
12
+ const DOCS_CLOUD_DEFAULT_API_KEY_ENV = "DOCS_CLOUD_API_KEY";
13
+ const DOCS_CLOUD_DEFAULT_ANALYTICS_PROJECT_ID_ENV = "PUBLIC_DOCS_CLOUD_PROJECT_ID";
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://api.farming-labs.dev";
16
+ const DEFAULT_PREVIEW_TIMEOUT_MS = 300 * 1e3;
17
+ const DEFAULT_PREVIEW_POLL_INTERVAL_MS = 2e3;
18
+ const REQUIRED_PREVIEW_API_KEY_SCOPES = [
19
+ "project:read",
20
+ "preview:write",
21
+ "jobs:read"
22
+ ];
23
+ const DOCS_CLOUD_PROJECT_ID_ENVS = [
24
+ "PUBLIC_DOCS_CLOUD_PROJECT_ID",
25
+ "NEXT_PUBLIC_DOCS_CLOUD_PROJECT_ID",
26
+ "DOCS_CLOUD_PROJECT_ID"
27
+ ];
28
+ const DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV = "PUBLIC_DOCS_CLOUD_API_KEY";
29
+ const CLOUD_CHECK_TARGETS = [
30
+ "deploy",
31
+ "analytics",
32
+ "ask-ai"
33
+ ];
34
+ const FUMADOCS_CONNECT_MARKER = "@farming-labs/docs cloud connect: fumadocs";
35
+ function isRecord(value) {
36
+ return Boolean(value && typeof value === "object" && !Array.isArray(value));
37
+ }
38
+ function isJsonRecord(value) {
39
+ return isRecord(value);
40
+ }
41
+ function toJsonRecord(value) {
42
+ if (!isRecord(value)) return void 0;
43
+ return JSON.parse(JSON.stringify(value));
44
+ }
45
+ function readPackageName(rootDir) {
46
+ const packagePath = path.join(rootDir, "package.json");
47
+ if (!fs.existsSync(packagePath)) return void 0;
48
+ try {
49
+ const parsed = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
50
+ return typeof parsed.name === "string" && parsed.name.trim() ? parsed.name.trim() : void 0;
51
+ } catch {
52
+ return;
53
+ }
54
+ }
55
+ function readPackageJsonRecord(rootDir) {
56
+ const packagePath = path.join(rootDir, "package.json");
57
+ if (!fs.existsSync(packagePath)) return void 0;
58
+ try {
59
+ const parsed = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
60
+ return isRecord(parsed) ? parsed : void 0;
61
+ } catch {
62
+ return;
63
+ }
64
+ }
65
+ function readPackageDependencies(rootDir) {
66
+ const packageJson = readPackageJsonRecord(rootDir);
67
+ const names = /* @__PURE__ */ new Set();
68
+ for (const key of [
69
+ "dependencies",
70
+ "devDependencies",
71
+ "peerDependencies",
72
+ "optionalDependencies"
73
+ ]) {
74
+ const dependencies = packageJson?.[key];
75
+ if (!isRecord(dependencies)) continue;
76
+ for (const name of Object.keys(dependencies)) names.add(name);
77
+ }
78
+ return names;
79
+ }
80
+ function hasFumadocsDependency(dependencies) {
81
+ return [...dependencies].some((name) => name === "fumadocs-core" || name === "fumadocs-ui" || name === "fumadocs-mdx" || name === "fumadocs-openapi" || name === "fumadocs-docgen");
82
+ }
83
+ function firstExistingFile(rootDir, candidates) {
84
+ return candidates.find((candidate) => fs.existsSync(path.join(rootDir, candidate)));
85
+ }
86
+ function hasMarkdownDescendant(rootDir, relativeDir) {
87
+ const absoluteDir = path.join(rootDir, relativeDir);
88
+ if (!fs.existsSync(absoluteDir)) return false;
89
+ const queue = [absoluteDir];
90
+ while (queue.length > 0) {
91
+ const current = queue.shift();
92
+ let entries;
93
+ try {
94
+ entries = fs.readdirSync(current, { withFileTypes: true });
95
+ } catch {
96
+ continue;
97
+ }
98
+ for (const entry of entries) {
99
+ const fullPath = path.join(current, entry.name);
100
+ if (entry.isDirectory()) {
101
+ queue.push(fullPath);
102
+ continue;
103
+ }
104
+ if (entry.isFile() && /\.(?:md|mdx)$/i.test(entry.name)) return true;
105
+ }
106
+ }
107
+ return false;
108
+ }
109
+ function detectFumadocsContentRoots(rootDir) {
110
+ const roots = [
111
+ "content/docs",
112
+ "src/content/docs",
113
+ "docs",
114
+ "content"
115
+ ].filter((candidate) => hasMarkdownDescendant(rootDir, candidate));
116
+ return roots.filter((root) => !roots.some((other) => other !== root && other.startsWith(`${root}/`)));
117
+ }
118
+ function detectConnectedFumadocsProfile(rootDir) {
119
+ const dependencies = readPackageDependencies(rootDir);
120
+ const sourceConfig = firstExistingFile(rootDir, [
121
+ "source.config.ts",
122
+ "source.config.tsx",
123
+ "source.config.js",
124
+ "source.config.mjs",
125
+ "source.config.cjs"
126
+ ]);
127
+ const sourceFile = firstExistingFile(rootDir, [
128
+ "lib/source.ts",
129
+ "lib/source.tsx",
130
+ "src/lib/source.ts",
131
+ "src/lib/source.tsx"
132
+ ]);
133
+ const contentRoots = detectFumadocsContentRoots(rootDir);
134
+ if (!(hasFumadocsDependency(dependencies) || Boolean(sourceConfig) || Boolean(sourceFile)) || contentRoots.length === 0) return;
135
+ const framework = detectFramework(rootDir) ?? "nextjs";
136
+ const configFiles = [sourceConfig, sourceFile].filter((file) => Boolean(file));
137
+ const packageManager = detectPackageManagerFromProject(rootDir)?.packageManager;
138
+ return {
139
+ engine: "fumadocs",
140
+ runtime: framework,
141
+ appRoot: ".",
142
+ contentRoots,
143
+ configFiles,
144
+ ...packageManager ? { packageManager } : {},
145
+ confidence: sourceConfig || sourceFile ? "high" : "medium",
146
+ reason: sourceConfig ? `Detected Fumadocs source config at ${sourceConfig}.` : "Detected Fumadocs dependencies and markdown content."
147
+ };
148
+ }
149
+ function runGit(rootDir, args) {
150
+ try {
151
+ return execFileSync("git", [
152
+ "-C",
153
+ rootDir,
154
+ ...args
155
+ ], {
156
+ encoding: "utf-8",
157
+ stdio: [
158
+ "ignore",
159
+ "pipe",
160
+ "ignore"
161
+ ]
162
+ }).trim();
163
+ } catch {
164
+ return;
165
+ }
166
+ }
167
+ function parseGitHubRemote(remoteUrl) {
168
+ const trimmed = remoteUrl.trim();
169
+ const match = trimmed.match(/^git@github\.com:([^/]+)\/(.+?)(?:\.git)?$/) ?? trimmed.match(/^https:\/\/github\.com\/([^/]+)\/(.+?)(?:\.git)?(?:\/)?$/);
170
+ if (!match?.[1] || !match[2]) return null;
171
+ return {
172
+ owner: match[1],
173
+ name: match[2].replace(/\.git$/i, "")
174
+ };
175
+ }
176
+ function resolveGitRepositoryMetadata(rootDir) {
177
+ const gitRoot = runGit(rootDir, ["rev-parse", "--show-toplevel"]);
178
+ const remoteUrl = runGit(rootDir, [
179
+ "remote",
180
+ "get-url",
181
+ "origin"
182
+ ]);
183
+ if (!gitRoot || !remoteUrl) return;
184
+ const parsedRemote = parseGitHubRemote(remoteUrl);
185
+ if (!parsedRemote) return;
186
+ const branch = runGit(rootDir, ["branch", "--show-current"]) ?? runGit(rootDir, [
187
+ "rev-parse",
188
+ "--abbrev-ref",
189
+ "HEAD"
190
+ ]);
191
+ const resolvedGitRoot = fs.realpathSync(gitRoot);
192
+ const resolvedRootDir = fs.realpathSync(rootDir);
193
+ const rootDirectory = path.relative(resolvedGitRoot, resolvedRootDir).split(path.sep).filter(Boolean).join("/");
194
+ return {
195
+ ...parsedRemote,
196
+ branch: branch && branch !== "HEAD" ? branch : void 0,
197
+ rootDirectory: rootDirectory || ".",
198
+ remoteUrl
199
+ };
200
+ }
201
+ function titleFromPackageName(name) {
202
+ if (!name) return void 0;
203
+ const normalized = name.replace(/^@[^/]+\//, "").replace(/[-_]+/g, " ").trim();
204
+ if (!normalized) return void 0;
205
+ return normalized.replace(/\b\w/g, (match) => match.toUpperCase());
206
+ }
207
+ function tryResolveDocsConfigPath(rootDir, explicitPath) {
208
+ if (explicitPath) return resolveDocsConfigPath(rootDir, explicitPath);
209
+ try {
210
+ return resolveDocsConfigPath(rootDir);
211
+ } catch {
212
+ return;
213
+ }
214
+ }
215
+ async function loadDocsConfigSnapshot(rootDir, explicitPath) {
216
+ const configPath = tryResolveDocsConfigPath(rootDir, explicitPath);
217
+ if (!configPath) return {};
218
+ return {
219
+ path: configPath,
220
+ content: fs.readFileSync(configPath, "utf-8"),
221
+ config: (await loadDocsConfigModule(rootDir, configPath, { silent: true }))?.config
222
+ };
223
+ }
224
+ function escapeRegExp(value) {
225
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
226
+ }
227
+ function findBalancedBraceEnd(content, braceStart) {
228
+ let depth = 0;
229
+ let stringQuote = null;
230
+ let escaped = false;
231
+ let lineComment = false;
232
+ let blockComment = false;
233
+ for (let index = braceStart; index < content.length; index += 1) {
234
+ const char = content[index];
235
+ const next = content[index + 1];
236
+ if (lineComment) {
237
+ if (char === "\n") lineComment = false;
238
+ continue;
239
+ }
240
+ if (blockComment) {
241
+ if (char === "*" && next === "/") {
242
+ blockComment = false;
243
+ index += 1;
244
+ }
245
+ continue;
246
+ }
247
+ if (stringQuote) {
248
+ if (escaped) {
249
+ escaped = false;
250
+ continue;
251
+ }
252
+ if (char === "\\") {
253
+ escaped = true;
254
+ continue;
255
+ }
256
+ if (char === stringQuote) stringQuote = null;
257
+ continue;
258
+ }
259
+ if (char === "/" && next === "/") {
260
+ lineComment = true;
261
+ index += 1;
262
+ continue;
263
+ }
264
+ if (char === "/" && next === "*") {
265
+ blockComment = true;
266
+ index += 1;
267
+ continue;
268
+ }
269
+ if (char === "\"" || char === "'" || char === "`") {
270
+ stringQuote = char;
271
+ continue;
272
+ }
273
+ if (char === "{") {
274
+ depth += 1;
275
+ continue;
276
+ }
277
+ if (char !== "}") continue;
278
+ depth -= 1;
279
+ if (depth === 0) return index;
280
+ }
281
+ return -1;
282
+ }
283
+ function findConfigObjectRange(content) {
284
+ for (const marker of ["defineDocs(", "export default"]) {
285
+ const markerIndex = content.indexOf(marker);
286
+ if (markerIndex === -1) continue;
287
+ const braceStart = content.indexOf("{", markerIndex);
288
+ if (braceStart === -1) continue;
289
+ const braceEnd = findBalancedBraceEnd(content, braceStart);
290
+ if (braceEnd === -1) continue;
291
+ return {
292
+ braceStart,
293
+ braceEnd,
294
+ bodyStart: braceStart + 1,
295
+ bodyEnd: braceEnd
296
+ };
297
+ }
298
+ }
299
+ function stripLeadingPropertyTrivia(content) {
300
+ let current = content;
301
+ while (true) {
302
+ const trimmed = current.replace(/^\s+/, "");
303
+ if (trimmed.startsWith("//")) {
304
+ const lineEnd = trimmed.indexOf("\n");
305
+ current = lineEnd === -1 ? "" : trimmed.slice(lineEnd + 1);
306
+ continue;
307
+ }
308
+ if (trimmed.startsWith("/*")) {
309
+ const blockEnd = trimmed.indexOf("*/");
310
+ current = blockEnd === -1 ? "" : trimmed.slice(blockEnd + 2);
311
+ continue;
312
+ }
313
+ return trimmed;
314
+ }
315
+ }
316
+ function propertyStartsWithKey(property, key) {
317
+ return new RegExp(`^${escapeRegExp(key)}\\s*:`).test(stripLeadingPropertyTrivia(property));
318
+ }
319
+ function findTopLevelPropertyRange(content, bodyStart, bodyEnd, key) {
320
+ let start = bodyStart;
321
+ let stringQuote = null;
322
+ let escaped = false;
323
+ let lineComment = false;
324
+ let blockComment = false;
325
+ let braceDepth = 0;
326
+ let bracketDepth = 0;
327
+ let parenDepth = 0;
328
+ const maybeMatch = (end) => {
329
+ if (!propertyStartsWithKey(content.slice(start, end), key)) return void 0;
330
+ return {
331
+ start,
332
+ end
333
+ };
334
+ };
335
+ for (let index = bodyStart; index <= bodyEnd; index += 1) {
336
+ const char = content[index];
337
+ const next = content[index + 1];
338
+ if (index === bodyEnd) return maybeMatch(index);
339
+ if (lineComment) {
340
+ if (char === "\n") lineComment = false;
341
+ continue;
342
+ }
343
+ if (blockComment) {
344
+ if (char === "*" && next === "/") {
345
+ blockComment = false;
346
+ index += 1;
347
+ }
348
+ continue;
349
+ }
350
+ if (stringQuote) {
351
+ if (escaped) {
352
+ escaped = false;
353
+ continue;
354
+ }
355
+ if (char === "\\") {
356
+ escaped = true;
357
+ continue;
358
+ }
359
+ if (char === stringQuote) stringQuote = null;
360
+ continue;
361
+ }
362
+ if (char === "/" && next === "/") {
363
+ lineComment = true;
364
+ index += 1;
365
+ continue;
366
+ }
367
+ if (char === "/" && next === "*") {
368
+ blockComment = true;
369
+ index += 1;
370
+ continue;
371
+ }
372
+ if (char === "\"" || char === "'" || char === "`") {
373
+ stringQuote = char;
374
+ continue;
375
+ }
376
+ if (char === "{") {
377
+ braceDepth += 1;
378
+ continue;
379
+ }
380
+ if (char === "}") {
381
+ braceDepth = Math.max(0, braceDepth - 1);
382
+ continue;
383
+ }
384
+ if (char === "[") {
385
+ bracketDepth += 1;
386
+ continue;
387
+ }
388
+ if (char === "]") {
389
+ bracketDepth = Math.max(0, bracketDepth - 1);
390
+ continue;
391
+ }
392
+ if (char === "(") {
393
+ parenDepth += 1;
394
+ continue;
395
+ }
396
+ if (char === ")") {
397
+ parenDepth = Math.max(0, parenDepth - 1);
398
+ continue;
399
+ }
400
+ if (char !== "," || braceDepth !== 0 || bracketDepth !== 0 || parenDepth !== 0) continue;
401
+ const match = maybeMatch(index);
402
+ if (match) return match;
403
+ start = index + 1;
404
+ }
405
+ }
406
+ function findObjectPropertyRange(content, object, key) {
407
+ const property = findTopLevelPropertyRange(content, object.bodyStart, object.bodyEnd, key);
408
+ if (!property) return void 0;
409
+ const colon = content.indexOf(":", property.start);
410
+ const braceStart = content.indexOf("{", colon);
411
+ if (colon === -1 || braceStart === -1 || braceStart > property.end) return void 0;
412
+ const braceEnd = findBalancedBraceEnd(content, braceStart);
413
+ if (braceEnd === -1 || braceEnd > property.end) return void 0;
414
+ return {
415
+ braceStart,
416
+ braceEnd,
417
+ bodyStart: braceStart + 1,
418
+ bodyEnd: braceEnd
419
+ };
420
+ }
421
+ function lineIndentAt(content, index) {
422
+ const lineStart = content.lastIndexOf("\n", index - 1) + 1;
423
+ return content.slice(lineStart, index).match(/^\s*/)?.[0] ?? "";
424
+ }
425
+ function insertObjectProperties(content, object, properties) {
426
+ if (properties.length === 0) return content;
427
+ const body = content.slice(object.bodyStart, object.bodyEnd);
428
+ const closingIndent = lineIndentAt(content, object.braceEnd);
429
+ const beforeObjectClose = content.slice(0, object.bodyEnd).replace(/\s*$/, "");
430
+ const suffix = content.slice(object.bodyEnd);
431
+ return `${beforeObjectClose}${body.trim().length > 0 && !beforeObjectClose.endsWith(",") ? "," : ""}\n${properties.join("\n")}\n${closingIndent}${suffix}`;
432
+ }
433
+ function renderAnalyticsConfigProperty(indent) {
434
+ return `${indent}analytics: {
435
+ ${indent} enabled: true,
436
+ ${indent} console: false,
437
+ ${indent} includeInputs: false,
438
+ ${indent}},`;
439
+ }
440
+ function renderCloudConfigProperty(indent, apiKeyEnv) {
441
+ return `${indent}cloud: {
442
+ ${indent} apiKey: { env: ${JSON.stringify(apiKeyEnv)} },
443
+ ${indent} deploy: { enabled: true },
444
+ ${indent} analytics: {
445
+ ${indent} enabled: true,
446
+ ${indent} console: false,
447
+ ${indent} includeInputs: false,
448
+ ${indent} },
449
+ ${indent} publish: { mode: "draft-pr", baseBranch: "main" },
450
+ ${indent}},`;
451
+ }
452
+ function renderCloudInitDocsConfig(apiKeyEnv) {
453
+ return `import { defineDocs } from "@farming-labs/docs";
454
+
455
+ export default defineDocs({
456
+ ${renderAnalyticsConfigProperty(" ")}
457
+ ${renderCloudConfigProperty(" ", apiKeyEnv)}
458
+ });
459
+ `;
460
+ }
461
+ function renderFumadocsConnectDocsConfig(apiKeyEnv, profile) {
462
+ const contentDir = profile.contentRoots[0] ?? "content/docs";
463
+ return `// ${FUMADOCS_CONNECT_MARKER}
464
+ import { defineDocs } from "@farming-labs/docs";
465
+
466
+ export default defineDocs({
467
+ entry: "docs",
468
+ contentDir: ${JSON.stringify(contentDir)},
469
+ ${renderAnalyticsConfigProperty(" ")}
470
+ ${renderCloudConfigProperty(" ", apiKeyEnv)}
471
+ });
472
+ `;
473
+ }
474
+ function normalizeEnvName(value, fallback) {
475
+ const normalized = value?.trim();
476
+ if (!normalized) return fallback;
477
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(normalized)) throw new Error(`Invalid environment variable name: ${normalized}`);
478
+ return normalized;
479
+ }
480
+ function ensureDocsConfigCloudInit(options) {
481
+ const resolvedConfigPath = tryResolveDocsConfigPath(options.rootDir, options.configPath);
482
+ const configPath = resolvedConfigPath ?? path.join(options.rootDir, "docs.config.ts");
483
+ if (!resolvedConfigPath) {
484
+ fs.writeFileSync(configPath, options.docsInfraProfile ? renderFumadocsConnectDocsConfig(options.apiKeyEnv, options.docsInfraProfile) : renderCloudInitDocsConfig(options.apiKeyEnv), "utf-8");
485
+ return {
486
+ configPath,
487
+ created: true,
488
+ updated: true
489
+ };
490
+ }
491
+ const original = fs.readFileSync(configPath, "utf-8");
492
+ let content = original;
493
+ let configObject = findConfigObjectRange(content);
494
+ if (!configObject) throw new Error(`Could not find an object export in ${path.relative(options.rootDir, configPath)}. Use defineDocs({ ... }) or export default { ... } before running cloud init.`);
495
+ const topLevelIndent = `${lineIndentAt(content, configObject.braceEnd)} `;
496
+ const cloudProperty = findTopLevelPropertyRange(content, configObject.bodyStart, configObject.bodyEnd, "cloud");
497
+ let cloudObject = findObjectPropertyRange(content, configObject, "cloud");
498
+ if (!cloudProperty) content = insertObjectProperties(content, configObject, [renderCloudConfigProperty(topLevelIndent, options.apiKeyEnv)]);
499
+ else if (!cloudObject) throw new Error(`Could not update cloud config in ${path.relative(options.rootDir, configPath)} because cloud is not an object literal.`);
500
+ configObject = findConfigObjectRange(content);
501
+ if (!configObject) throw new Error(`Could not re-read ${path.relative(options.rootDir, configPath)}.`);
502
+ cloudObject = findObjectPropertyRange(content, configObject, "cloud");
503
+ if (cloudObject) {
504
+ const cloudIndent = `${lineIndentAt(content, cloudObject.braceEnd)} `;
505
+ const missingCloudProperties = [];
506
+ if (!findTopLevelPropertyRange(content, cloudObject.bodyStart, cloudObject.bodyEnd, "apiKey")) missingCloudProperties.push(`${cloudIndent}apiKey: { env: ${JSON.stringify(options.apiKeyEnv)} },`);
507
+ else {
508
+ const apiKeyObject = findObjectPropertyRange(content, cloudObject, "apiKey");
509
+ if (apiKeyObject && !findTopLevelPropertyRange(content, apiKeyObject.bodyStart, apiKeyObject.bodyEnd, "env")) {
510
+ content = insertObjectProperties(content, apiKeyObject, [`${lineIndentAt(content, apiKeyObject.braceEnd)} env: ${JSON.stringify(options.apiKeyEnv)},`]);
511
+ configObject = findConfigObjectRange(content);
512
+ cloudObject = findObjectPropertyRange(content, configObject, "cloud");
513
+ }
514
+ }
515
+ if (!findTopLevelPropertyRange(content, cloudObject.bodyStart, cloudObject.bodyEnd, "deploy")) missingCloudProperties.push(`${cloudIndent}deploy: { enabled: true },`);
516
+ if (!findTopLevelPropertyRange(content, cloudObject.bodyStart, cloudObject.bodyEnd, "analytics")) missingCloudProperties.push(renderAnalyticsConfigProperty(cloudIndent));
517
+ if (!findTopLevelPropertyRange(content, cloudObject.bodyStart, cloudObject.bodyEnd, "publish")) missingCloudProperties.push(`${cloudIndent}publish: { mode: "draft-pr", baseBranch: "main" },`);
518
+ content = insertObjectProperties(content, cloudObject, missingCloudProperties);
519
+ }
520
+ configObject = findConfigObjectRange(content);
521
+ if (!configObject) throw new Error(`Could not re-read ${path.relative(options.rootDir, configPath)}.`);
522
+ if (!findTopLevelPropertyRange(content, configObject.bodyStart, configObject.bodyEnd, "analytics")) content = insertObjectProperties(content, configObject, [renderAnalyticsConfigProperty(`${lineIndentAt(content, configObject.braceEnd)} `)]);
523
+ if (content !== original) fs.writeFileSync(configPath, content, "utf-8");
524
+ return {
525
+ configPath,
526
+ created: false,
527
+ updated: content !== original
528
+ };
529
+ }
530
+ function readExistingDocsJson(docsJsonPath) {
531
+ if (!fs.existsSync(docsJsonPath)) return void 0;
532
+ try {
533
+ const parsed = JSON.parse(fs.readFileSync(docsJsonPath, "utf-8"));
534
+ if (!isJsonRecord(parsed)) throw new Error(`${DOCS_JSON_FILE} must contain a JSON object.`);
535
+ return parsed;
536
+ } catch (error) {
537
+ const message = error instanceof Error ? error.message : String(error);
538
+ throw new Error(`Could not parse ${DOCS_JSON_FILE}: ${message}`);
539
+ }
540
+ }
541
+ function normalizeApiKeyConfig(apiKey) {
542
+ return { env: apiKey?.env?.trim() || DOCS_CLOUD_DEFAULT_API_KEY_ENV };
543
+ }
544
+ function normalizePreviewConfig(preview) {
545
+ return { enabled: preview?.enabled ?? true };
546
+ }
547
+ function normalizePublishConfig(publish) {
548
+ return {
549
+ mode: publish?.mode === "direct-commit" ? "direct-commit" : "draft-pr",
550
+ baseBranch: publish?.baseBranch?.trim() || "main"
551
+ };
552
+ }
553
+ function normalizeFeatureConfig(feature) {
554
+ if (!feature) return void 0;
555
+ return { enabled: feature.enabled ?? true };
556
+ }
557
+ function normalizeAnalyticsConfig(analytics) {
558
+ if (typeof analytics === "undefined") return void 0;
559
+ if (typeof analytics === "boolean") return analytics;
560
+ return {
561
+ enabled: analytics.enabled ?? true,
562
+ ...typeof analytics.console !== "undefined" ? { console: analytics.console } : {},
563
+ includeInputs: analytics.includeInputs ?? false
564
+ };
565
+ }
566
+ function normalizeCloudConfig(cloud) {
567
+ const normalized = {
568
+ apiKey: normalizeApiKeyConfig(cloud?.apiKey),
569
+ publish: normalizePublishConfig(cloud?.publish)
570
+ };
571
+ if (cloud?.enabled === false) normalized.enabled = false;
572
+ const analytics = normalizeAnalyticsConfig(cloud?.analytics);
573
+ if (typeof analytics !== "undefined") normalized.analytics = analytics;
574
+ const ai = normalizeFeatureConfig(cloud?.ai);
575
+ if (ai) normalized.ai = ai;
576
+ const deploy = normalizeFeatureConfig(cloud?.deploy);
577
+ if (deploy) normalized.deploy = deploy;
578
+ if (cloud?.preview) normalized.preview = normalizePreviewConfig(cloud.preview);
579
+ return normalized;
580
+ }
581
+ function readStaticCloudConfig(content) {
582
+ if (!content || !extractNestedObjectLiteral(content, ["cloud"])) return void 0;
583
+ const cloudBlock = extractNestedObjectLiteral(content, ["cloud"]);
584
+ const apiKeyBlock = extractNestedObjectLiteral(content, ["cloud", "apiKey"]);
585
+ const previewBlock = extractNestedObjectLiteral(content, ["cloud", "preview"]);
586
+ const publishBlock = extractNestedObjectLiteral(content, ["cloud", "publish"]);
587
+ const analyticsBlock = extractNestedObjectLiteral(content, ["cloud", "analytics"]);
588
+ const aiBlock = extractNestedObjectLiteral(content, ["cloud", "ai"]);
589
+ const deployBlock = extractNestedObjectLiteral(content, ["cloud", "deploy"]);
590
+ const cloud = {};
591
+ const enabled = cloudBlock ? readTopLevelBooleanProperty(cloudBlock, "enabled") : void 0;
592
+ if (typeof enabled === "boolean") cloud.enabled = enabled;
593
+ const apiKeyEnv = apiKeyBlock ? readStringProperty(apiKeyBlock, "env") : void 0;
594
+ if (apiKeyEnv) cloud.apiKey = { env: apiKeyEnv };
595
+ const previewEnabled = previewBlock ? readTopLevelBooleanProperty(previewBlock, "enabled") : void 0;
596
+ if (typeof previewEnabled === "boolean") cloud.preview = { enabled: previewEnabled };
597
+ const publishMode = publishBlock ? readStringProperty(publishBlock, "mode") : void 0;
598
+ const baseBranch = publishBlock ? readStringProperty(publishBlock, "baseBranch") : void 0;
599
+ if (publishMode || baseBranch) cloud.publish = {
600
+ ...publishMode === "direct-commit" || publishMode === "draft-pr" ? { mode: publishMode } : {},
601
+ ...baseBranch ? { baseBranch } : {}
602
+ };
603
+ const analyticsEnabled = cloudBlock ? readTopLevelBooleanProperty(cloudBlock, "analytics") : void 0;
604
+ if (typeof analyticsEnabled === "boolean") cloud.analytics = analyticsEnabled;
605
+ else if (analyticsBlock) {
606
+ const analytics = {};
607
+ const enabledValue = readTopLevelBooleanProperty(analyticsBlock, "enabled");
608
+ const consoleBoolean = readTopLevelBooleanProperty(analyticsBlock, "console");
609
+ const consoleMode = readStringProperty(analyticsBlock, "console");
610
+ const includeInputs = readTopLevelBooleanProperty(analyticsBlock, "includeInputs");
611
+ if (typeof enabledValue === "boolean") analytics.enabled = enabledValue;
612
+ if (typeof consoleBoolean === "boolean") analytics.console = consoleBoolean;
613
+ else if (consoleMode === "log" || consoleMode === "info" || consoleMode === "debug") analytics.console = consoleMode;
614
+ if (typeof includeInputs === "boolean") analytics.includeInputs = includeInputs;
615
+ cloud.analytics = analytics;
616
+ }
617
+ const aiEnabled = aiBlock ? readTopLevelBooleanProperty(aiBlock, "enabled") : void 0;
618
+ if (typeof aiEnabled === "boolean") cloud.ai = { enabled: aiEnabled };
619
+ const deployEnabled = deployBlock ? readTopLevelBooleanProperty(deployBlock, "enabled") : void 0;
620
+ if (typeof deployEnabled === "boolean") cloud.deploy = { enabled: deployEnabled };
621
+ return cloud;
622
+ }
623
+ function resolveCloudConfig(snapshot, existing) {
624
+ const moduleCloud = snapshot.config?.cloud;
625
+ const staticCloud = readStaticCloudConfig(snapshot.content);
626
+ const existingCloud = existing?.cloud;
627
+ return normalizeCloudConfig(moduleCloud ?? staticCloud ?? existingCloud);
628
+ }
629
+ function resolveApiReferenceRoot(snapshot) {
630
+ const apiReference = snapshot.config?.apiReference;
631
+ if (isRecord(apiReference) && typeof apiReference.path === "string" && apiReference.path.trim()) return apiReference.path.trim();
632
+ const apiReferenceBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["apiReference"]);
633
+ if (!apiReferenceBlock) return void 0;
634
+ return readStringProperty(apiReferenceBlock, "path");
635
+ }
636
+ function resolveConnectedDocsProfile(params) {
637
+ if (!(params.snapshot.content?.includes(FUMADOCS_CONNECT_MARKER) || !params.snapshot.path)) return void 0;
638
+ const detectedProfile = detectConnectedFumadocsProfile(params.rootDir);
639
+ if (detectedProfile) return detectedProfile;
640
+ const existingProfile = params.existing?.extensions?.docsInfraProfile;
641
+ if (isRecord(existingProfile) && existingProfile.engine === "fumadocs" && Array.isArray(existingProfile.contentRoots)) return existingProfile;
642
+ }
643
+ function resolveSiteConfig(rootDir, snapshot, existing) {
644
+ const existingSite = toJsonRecord(existing?.site);
645
+ const navTitle = isRecord(snapshot.config?.nav) && typeof snapshot.config.nav.title === "string" ? snapshot.config.nav.title : readNavTitle(snapshot.content ?? "");
646
+ const metadata = isRecord(snapshot.config?.metadata) ? snapshot.config.metadata : void 0;
647
+ const metadataDescription = typeof metadata?.description === "string" && metadata.description.trim() ? metadata.description.trim() : void 0;
648
+ const packageTitle = titleFromPackageName(readPackageName(rootDir));
649
+ const name = navTitle?.trim() || existingSite?.name || packageTitle;
650
+ const description = metadataDescription || existingSite?.description;
651
+ if (!name && !description && !existingSite) return void 0;
652
+ return {
653
+ ...existingSite,
654
+ ...typeof name === "string" ? { name } : {},
655
+ ...typeof description === "string" ? { description } : {}
656
+ };
657
+ }
658
+ function resolveDocsRoot(rootDir, snapshot, existing, docsInfraProfile) {
659
+ if (docsInfraProfile?.contentRoots[0]) return docsInfraProfile.contentRoots[0];
660
+ const entry = snapshot.config?.entry ?? readTopLevelStringProperty(snapshot.content ?? "", "entry") ?? "docs";
661
+ if (snapshot.config?.contentDir) return snapshot.config.contentDir;
662
+ if (snapshot.content) return resolveDocsContentDir(rootDir, snapshot.content, entry);
663
+ if (typeof existing?.content?.docsRoot === "string") return existing.content.docsRoot;
664
+ return fs.existsSync(path.join(rootDir, "app", entry)) ? path.join("app", entry) : entry;
665
+ }
666
+ function resolveDocsBlock(rootDir, snapshot, existing, docsInfraProfile) {
667
+ const existingDocs = existing?.docs;
668
+ if (!snapshot.path && existingDocs) return existingDocs;
669
+ const detectedFramework = detectFramework(rootDir);
670
+ const runtime = detectedFramework ?? docsInfraProfile?.runtime ?? existingDocs?.runtime ?? "nextjs";
671
+ if (!Boolean(snapshot.path || detectedFramework) && existingDocs) return existingDocs;
672
+ return {
673
+ mode: "framework",
674
+ runtime,
675
+ root: existingDocs?.root || "."
676
+ };
677
+ }
678
+ function resolveExtensions(existing, docsInfraProfile, options = {}) {
679
+ const existingExtensions = toJsonRecord(existing?.extensions);
680
+ if (!docsInfraProfile) {
681
+ if (!existingExtensions?.docsInfraProfile || options.dropStaleDocsInfraProfile === false) return existingExtensions;
682
+ const { docsInfraProfile: _staleDocsInfraProfile, ...rest } = existingExtensions;
683
+ return Object.keys(rest).length > 0 ? rest : void 0;
684
+ }
685
+ return {
686
+ ...existingExtensions,
687
+ docsInfraProfile: toJsonRecord(docsInfraProfile)
688
+ };
689
+ }
690
+ function materializeDocsJsonObject(params) {
691
+ const cloud = resolveCloudConfig(params.snapshot, params.existing);
692
+ const docsInfraProfile = params.docsInfraProfile ?? (params.detectConnectedDocsProfile === false ? void 0 : resolveConnectedDocsProfile({
693
+ rootDir: params.rootDir,
694
+ snapshot: params.snapshot,
695
+ existing: params.existing
696
+ }));
697
+ const docsRoot = resolveDocsRoot(params.rootDir, params.snapshot, params.existing, docsInfraProfile);
698
+ const apiReferenceRoot = resolveApiReferenceRoot(params.snapshot);
699
+ const existingContent = toJsonRecord(params.existing?.content);
700
+ const site = resolveSiteConfig(params.rootDir, params.snapshot, params.existing);
701
+ const extensions = resolveExtensions(params.existing, docsInfraProfile, { dropStaleDocsInfraProfile: params.dropStaleDocsInfraProfile });
702
+ const content = {
703
+ ...existingContent,
704
+ docsRoot,
705
+ ...apiReferenceRoot ? { apiReferenceRoot } : {}
706
+ };
707
+ return {
708
+ ...params.existing,
709
+ $schema: params.existing?.$schema ?? DOCS_CLOUD_SCHEMA_URL,
710
+ version: 1,
711
+ docs: resolveDocsBlock(params.rootDir, params.snapshot, params.existing, docsInfraProfile),
712
+ content,
713
+ ...site ? { site } : {},
714
+ cloud,
715
+ ...extensions ? { extensions } : {}
716
+ };
717
+ }
718
+ function serializeMaterializedDocsJson(config) {
719
+ return `${JSON.stringify(config, null, 2)}\n`;
720
+ }
721
+ function withCloudInitDefaults(cloud, existingCloud, apiKeyEnv) {
722
+ const normalized = normalizeCloudConfig(cloud);
723
+ if (!existingCloud?.apiKey?.env) normalized.apiKey = { env: apiKeyEnv };
724
+ if (!normalized.deploy) normalized.deploy = { enabled: true };
725
+ if (typeof normalized.analytics === "undefined") normalized.analytics = {
726
+ enabled: true,
727
+ console: false,
728
+ includeInputs: false
729
+ };
730
+ return normalizeCloudConfig(normalized);
731
+ }
732
+ function writeMaterializedCloudConfig(params) {
733
+ const config = materializeDocsJsonObject({
734
+ rootDir: params.rootDir,
735
+ snapshot: params.snapshot,
736
+ existing: params.existing,
737
+ docsInfraProfile: params.docsInfraProfile,
738
+ detectConnectedDocsProfile: params.detectConnectedDocsProfile,
739
+ dropStaleDocsInfraProfile: params.dropStaleDocsInfraProfile
740
+ });
741
+ if (params.cloudInitApiKeyEnv) config.cloud = withCloudInitDefaults(config.cloud, params.existing?.cloud, params.cloudInitApiKeyEnv);
742
+ const serialized = serializeMaterializedDocsJson(config);
743
+ const updated = (params.existing ? fs.readFileSync(params.docsJsonPath, "utf-8") : void 0) !== serialized;
744
+ if (updated) fs.writeFileSync(params.docsJsonPath, serialized, "utf-8");
745
+ return {
746
+ configPath: params.snapshot.path ?? params.docsJsonPath,
747
+ docsJsonPath: params.docsJsonPath,
748
+ config,
749
+ apiKeyEnv: config.cloud?.apiKey?.env ?? DOCS_CLOUD_DEFAULT_API_KEY_ENV,
750
+ created: !params.existing,
751
+ updated
752
+ };
753
+ }
754
+ async function materializeCloudConfig(options = {}) {
755
+ const rootDir = options.rootDir ?? process.cwd();
756
+ const docsJsonPath = path.join(rootDir, DOCS_JSON_FILE);
757
+ const existing = readExistingDocsJson(docsJsonPath);
758
+ const snapshot = await loadDocsConfigSnapshot(rootDir, options.configPath);
759
+ const useDocsJsonAsSource = Boolean(existing && !snapshot.path);
760
+ return writeMaterializedCloudConfig({
761
+ rootDir,
762
+ docsJsonPath,
763
+ existing,
764
+ snapshot,
765
+ docsInfraProfile: options.docsInfraProfile,
766
+ detectConnectedDocsProfile: !useDocsJsonAsSource,
767
+ dropStaleDocsInfraProfile: !useDocsJsonAsSource
768
+ });
769
+ }
770
+ function readCombinedEnv(rootDir) {
771
+ const env = { ...loadProjectEnv(rootDir) };
772
+ for (const [key, value] of Object.entries(process.env)) if (typeof value === "string") env[key] = value;
773
+ return env;
774
+ }
775
+ function readEnvValue(env, name) {
776
+ if (!name) return void 0;
777
+ const value = env[name]?.trim();
778
+ return value ? value : void 0;
779
+ }
780
+ function readFirstEnv(env, names) {
781
+ for (const name of names) {
782
+ const value = readEnvValue(env, name);
783
+ if (value) return {
784
+ name,
785
+ value
786
+ };
787
+ }
788
+ }
789
+ function readConfiguredCloudApiKeyEnv(snapshot) {
790
+ const moduleEnv = snapshot.config?.cloud?.apiKey?.env?.trim();
791
+ if (moduleEnv) return moduleEnv;
792
+ const apiKeyBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["cloud", "apiKey"]);
793
+ return (apiKeyBlock ? readStringProperty(apiKeyBlock, "env") : void 0)?.trim() || void 0;
794
+ }
795
+ function readAiProvider(snapshot) {
796
+ const moduleProvider = (snapshot.config?.ai)?.provider;
797
+ if (typeof moduleProvider === "string" && moduleProvider.trim()) return moduleProvider.trim();
798
+ const aiBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["ai"]);
799
+ return (aiBlock ? readStringProperty(aiBlock, "provider") : void 0)?.trim() || void 0;
800
+ }
801
+ function readRuntimeAnalyticsDisabled(snapshot) {
802
+ const moduleAnalytics = snapshot.config?.analytics;
803
+ if (moduleAnalytics === false) return true;
804
+ if (isRecord(moduleAnalytics) && moduleAnalytics.enabled === false) return true;
805
+ if (readTopLevelBooleanProperty(snapshot.content ?? "", "analytics") === false) return true;
806
+ const analyticsBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["analytics"]);
807
+ return (analyticsBlock ? readTopLevelBooleanProperty(analyticsBlock, "enabled") : void 0) === false;
808
+ }
809
+ function isCloudAnalyticsEnabled(analytics) {
810
+ if (analytics === false) return false;
811
+ if (isRecord(analytics) && analytics.enabled === false) return false;
812
+ return typeof analytics !== "undefined";
813
+ }
814
+ function createCheck(name, status, message, details) {
815
+ return {
816
+ name,
817
+ status,
818
+ message,
819
+ ...details ? { details } : {}
820
+ };
821
+ }
822
+ function isBrowserSafeEnvName(name) {
823
+ return name.startsWith("PUBLIC_") || name.startsWith("NEXT_PUBLIC_");
824
+ }
825
+ function summarizeIdentity(identity) {
826
+ if (!isRecord(identity)) return void 0;
827
+ const workspace = isRecord(identity.workspace) ? identity.workspace : void 0;
828
+ const apiKey = isRecord(identity.apiKey) ? identity.apiKey : void 0;
829
+ const scopes = readApiKeyScopes(identity);
830
+ return {
831
+ ...workspace ? { workspace: {
832
+ ...typeof workspace.id === "string" ? { id: workspace.id } : {},
833
+ ...typeof workspace.name === "string" ? { name: workspace.name } : {}
834
+ } } : {},
835
+ ...apiKey ? { apiKey: {
836
+ ...typeof apiKey.id === "string" ? { id: apiKey.id } : {},
837
+ ...scopes.length > 0 ? { scopes } : {}
838
+ } } : {}
839
+ };
840
+ }
841
+ function formatCheckStatus(status) {
842
+ if (status === "pass") return pc.green("ok");
843
+ if (status === "warn") return pc.yellow("warn");
844
+ return pc.red("fail");
845
+ }
846
+ function countChecks(checks, status) {
847
+ return checks.filter((check) => check.status === status).length;
848
+ }
849
+ function resolveCloudCheckTargets(options) {
850
+ const targets = new Set(options.checkTargets);
851
+ if (targets.size > 0) return targets;
852
+ return new Set(CLOUD_CHECK_TARGETS);
853
+ }
854
+ function formatCloudCheckTargets(targets) {
855
+ return targets.join(", ");
856
+ }
857
+ function resolveApiBaseUrl(options, rootDir = process.cwd()) {
858
+ if (options.apiBaseUrl?.trim()) return {
859
+ url: options.apiBaseUrl.trim().replace(/\/+$/, ""),
860
+ source: "flag"
861
+ };
862
+ const projectEnv = loadProjectEnv(rootDir);
863
+ for (const envName of [
864
+ "DOCS_CLOUD_API_URL",
865
+ "PUBLIC_DOCS_CLOUD_URL",
866
+ "NEXT_PUBLIC_DOCS_CLOUD_URL"
867
+ ]) {
868
+ const value = process.env[envName]?.trim() ?? projectEnv[envName]?.trim();
869
+ if (value) return {
870
+ url: value.replace(/\/+$/, ""),
871
+ source: "env",
872
+ env: envName
873
+ };
874
+ }
875
+ return {
876
+ url: DEFAULT_DOCS_CLOUD_API_BASE_URL,
877
+ source: "default"
878
+ };
879
+ }
880
+ function resolveApiKey(options, rootDir, envName) {
881
+ if (options.apiKey?.trim()) return options.apiKey.trim();
882
+ const token = {
883
+ ...loadProjectEnv(rootDir),
884
+ ...process.env
885
+ }[envName]?.trim();
886
+ if (token) return token;
887
+ 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}.`);
888
+ }
889
+ function isLocalhostUrl(value) {
890
+ try {
891
+ const url = new URL(value);
892
+ return [
893
+ "localhost",
894
+ "127.0.0.1",
895
+ "::1"
896
+ ].includes(url.hostname);
897
+ } catch {
898
+ return false;
899
+ }
900
+ }
901
+ function readNestedString(value, pathSegments) {
902
+ let current = value;
903
+ for (const segment of pathSegments) {
904
+ if (!isRecord(current)) return void 0;
905
+ current = current[segment];
906
+ }
907
+ return typeof current === "string" && current.trim() ? current.trim() : void 0;
908
+ }
909
+ function readDocsSiteOrigin(snapshot, env) {
910
+ const envSite = readFirstEnv(env, [
911
+ "PUBLIC_BASE_URL",
912
+ "PUBLIC_SITE_URL",
913
+ "NEXT_PUBLIC_BASE_URL",
914
+ "NEXT_PUBLIC_SITE_URL",
915
+ "SITE_URL"
916
+ ]);
917
+ const sitemapBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["sitemap"]);
918
+ const llmsTxtBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["llmsTxt"]);
919
+ const robotsBlock = extractNestedObjectLiteral(snapshot.content ?? "", ["robots"]);
920
+ const candidates = [
921
+ {
922
+ value: envSite?.value,
923
+ source: envSite ? envSite.name : "env"
924
+ },
925
+ {
926
+ value: readNestedString(snapshot.config, ["site", "url"]),
927
+ source: "site.url"
928
+ },
929
+ {
930
+ value: readNestedString(snapshot.config, ["sitemap", "baseUrl"]),
931
+ source: "sitemap.baseUrl"
932
+ },
933
+ {
934
+ value: readNestedString(snapshot.config, ["llmsTxt", "baseUrl"]),
935
+ source: "llmsTxt.baseUrl"
936
+ },
937
+ {
938
+ value: readNestedString(snapshot.config, ["robots", "baseUrl"]),
939
+ source: "robots.baseUrl"
940
+ },
941
+ {
942
+ value: sitemapBlock ? readStringProperty(sitemapBlock, "baseUrl") : void 0,
943
+ source: "sitemap.baseUrl"
944
+ },
945
+ {
946
+ value: llmsTxtBlock ? readStringProperty(llmsTxtBlock, "baseUrl") : void 0,
947
+ source: "llmsTxt.baseUrl"
948
+ },
949
+ {
950
+ value: robotsBlock ? readStringProperty(robotsBlock, "baseUrl") : void 0,
951
+ source: "robots.baseUrl"
952
+ }
953
+ ];
954
+ for (const candidate of candidates) {
955
+ if (!candidate.value) continue;
956
+ try {
957
+ return {
958
+ origin: new URL(candidate.value).origin,
959
+ source: candidate.source
960
+ };
961
+ } catch {}
962
+ }
963
+ }
964
+ async function checkCorsPreflight(params) {
965
+ const response = await fetch(params.url, {
966
+ method: "OPTIONS",
967
+ headers: {
968
+ Origin: params.origin,
969
+ "Access-Control-Request-Method": "POST",
970
+ "Access-Control-Request-Headers": params.requestHeaders
971
+ }
972
+ });
973
+ const allowOrigin = response.headers.get("access-control-allow-origin");
974
+ const allowMethods = response.headers.get("access-control-allow-methods");
975
+ const allowHeaders = response.headers.get("access-control-allow-headers");
976
+ const normalizedAllowOrigin = allowOrigin?.toLowerCase();
977
+ const normalizedOrigin = params.origin.toLowerCase();
978
+ return {
979
+ ok: response.ok && (normalizedAllowOrigin === "*" || normalizedAllowOrigin === normalizedOrigin) && Boolean(allowMethods?.toUpperCase().includes("POST")) && areCorsRequestHeadersAllowed(params.requestHeaders, allowHeaders),
980
+ status: response.status,
981
+ allowOrigin,
982
+ allowMethods,
983
+ allowHeaders
984
+ };
985
+ }
986
+ function areCorsRequestHeadersAllowed(requestHeaders, allowHeaders) {
987
+ const requested = parseCorsHeaderList(requestHeaders);
988
+ if (requested.length === 0) return true;
989
+ if (!allowHeaders) return false;
990
+ const allowed = parseCorsHeaderList(allowHeaders);
991
+ if (allowed.includes("*")) return true;
992
+ return requested.every((header) => allowed.includes(header));
993
+ }
994
+ function parseCorsHeaderList(value) {
995
+ return value.split(",").map((header) => header.trim().toLowerCase()).filter(Boolean);
996
+ }
997
+ async function readJsonResponse(response) {
998
+ const text = await response.text();
999
+ if (!text.trim()) return {};
1000
+ try {
1001
+ return JSON.parse(text);
1002
+ } catch {
1003
+ return { message: text };
1004
+ }
1005
+ }
1006
+ function readResponseMessage(body, fallback) {
1007
+ if (isRecord(body)) for (const key of [
1008
+ "error",
1009
+ "message",
1010
+ "detail"
1011
+ ]) {
1012
+ const value = body[key];
1013
+ if (typeof value === "string" && value.trim()) return value.trim();
1014
+ }
1015
+ return fallback;
1016
+ }
1017
+ const FAILURE_DETAIL_OBJECT_KEYS = [
1018
+ "job",
1019
+ "run",
1020
+ "preview",
1021
+ "deployment",
1022
+ "build"
1023
+ ];
1024
+ const FAILURE_DETAIL_ARRAY_KEYS = [
1025
+ "jobs",
1026
+ "runs",
1027
+ "steps",
1028
+ "tasks",
1029
+ "stages",
1030
+ "checks"
1031
+ ];
1032
+ const FAILURE_DETAIL_MESSAGE_KEYS = [
1033
+ "error",
1034
+ "message",
1035
+ "detail",
1036
+ "reason",
1037
+ "summary"
1038
+ ];
1039
+ const FAILURE_DETAIL_LABEL_KEYS = [
1040
+ "name",
1041
+ "title",
1042
+ "label",
1043
+ "step",
1044
+ "phase",
1045
+ "stage",
1046
+ "id"
1047
+ ];
1048
+ const FAILURE_DETAIL_STATUS_KEYS = [
1049
+ "status",
1050
+ "state",
1051
+ "conclusion",
1052
+ "result",
1053
+ "outcome"
1054
+ ];
1055
+ function readTrimmedString(value) {
1056
+ return typeof value === "string" && value.trim() ? value.trim() : void 0;
1057
+ }
1058
+ function readFirstString(record, keys) {
1059
+ for (const key of keys) {
1060
+ const value = readTrimmedString(record[key]);
1061
+ if (value) return value;
1062
+ }
1063
+ }
1064
+ function singularizeKey(key) {
1065
+ return key.endsWith("s") ? key.slice(0, -1) : key;
1066
+ }
1067
+ function findPreviewFailureDetail(value, parentPath = [], fallbackLabel) {
1068
+ if (!isRecord(value)) return void 0;
1069
+ const label = readFirstString(value, FAILURE_DETAIL_LABEL_KEYS) ?? fallbackLabel;
1070
+ const path = label ? [...parentPath, label] : parentPath;
1071
+ for (const key of FAILURE_DETAIL_OBJECT_KEYS) {
1072
+ const detail = findPreviewFailureDetail(value[key], path, key);
1073
+ if (detail) return detail;
1074
+ }
1075
+ for (const key of FAILURE_DETAIL_ARRAY_KEYS) {
1076
+ const children = value[key];
1077
+ if (!Array.isArray(children)) continue;
1078
+ for (const child of children) {
1079
+ const detail = findPreviewFailureDetail(child, path, singularizeKey(key));
1080
+ if (detail) return detail;
1081
+ }
1082
+ }
1083
+ const status = readFirstString(value, FAILURE_DETAIL_STATUS_KEYS);
1084
+ if (!isTerminalFailureStatus(status)) return void 0;
1085
+ return {
1086
+ message: readFirstString(value, FAILURE_DETAIL_MESSAGE_KEYS),
1087
+ path,
1088
+ status
1089
+ };
1090
+ }
1091
+ function readPreviewFailureMessage(body, fallback) {
1092
+ const detail = findPreviewFailureDetail(body);
1093
+ if (!detail) return readResponseMessage(body, fallback);
1094
+ const prefix = fallback.replace(/\.+$/g, "");
1095
+ const location = detail.path.length > 0 ? ` at ${detail.path.join(" > ")}` : "";
1096
+ const status = detail.status ? ` (${detail.status})` : "";
1097
+ const message = detail.message ?? readResponseMessage(body, "");
1098
+ return message ? `${prefix}${location}${status}: ${message}` : `${prefix}${location}${status}.`;
1099
+ }
1100
+ async function fetchCloudJson(params) {
1101
+ const headers = new Headers(params.init?.headers);
1102
+ headers.set("authorization", `Bearer ${params.apiKey}`);
1103
+ headers.set("accept", "application/json");
1104
+ if (params.init?.body && !headers.has("content-type")) headers.set("content-type", "application/json");
1105
+ const response = await fetch(params.url, {
1106
+ ...params.init,
1107
+ headers
1108
+ });
1109
+ const body = await readJsonResponse(response);
1110
+ if (!response.ok) {
1111
+ const requestPath = new URL(params.url).pathname;
1112
+ 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.");
1113
+ const message = readResponseMessage(body, `Docs Cloud request failed with HTTP ${response.status}.`);
1114
+ throw new Error(message);
1115
+ }
1116
+ return body;
1117
+ }
1118
+ function readPreviewUrl(body) {
1119
+ if (!isRecord(body)) return void 0;
1120
+ const direct = body.url ?? body.previewUrl;
1121
+ if (typeof direct === "string" && direct.trim()) return direct.trim();
1122
+ const preview = body.preview;
1123
+ if (isRecord(preview) && typeof preview.url === "string" && preview.url.trim()) return preview.url.trim();
1124
+ }
1125
+ function readStatusUrl(body, apiBaseUrl) {
1126
+ if (!isRecord(body)) return void 0;
1127
+ const statusUrl = body.statusUrl ?? body.pollUrl;
1128
+ if (typeof statusUrl !== "string" || !statusUrl.trim()) return void 0;
1129
+ return new URL(statusUrl, apiBaseUrl).toString();
1130
+ }
1131
+ function readPreviewStatus(body) {
1132
+ if (!isRecord(body)) return void 0;
1133
+ const job = body.job;
1134
+ if (isRecord(job) && typeof job.status === "string") return job.status;
1135
+ const preview = body.preview;
1136
+ if (isRecord(preview) && typeof preview.status === "string") return preview.status;
1137
+ const status = body.status;
1138
+ if (typeof status === "string") return status;
1139
+ }
1140
+ function readApiKeyScopes(body) {
1141
+ if (!isRecord(body)) return [];
1142
+ const apiKey = body.apiKey;
1143
+ if (!isRecord(apiKey) || !Array.isArray(apiKey.scopes)) return [];
1144
+ return apiKey.scopes.filter((scope) => typeof scope === "string");
1145
+ }
1146
+ function assertPreviewApiKeyScopes(identity) {
1147
+ const scopes = readApiKeyScopes(identity);
1148
+ const missing = REQUIRED_PREVIEW_API_KEY_SCOPES.filter((scope) => !scopes.includes(scope));
1149
+ if (missing.length > 0) throw new Error(`Docs Cloud API key is missing required preview scope${missing.length === 1 ? "" : "s"}: ${missing.join(", ")}.`);
1150
+ return scopes;
1151
+ }
1152
+ function isTerminalFailureStatus(status) {
1153
+ const normalized = status?.toLowerCase();
1154
+ return normalized === "failed" || normalized === "failure" || normalized === "error" || normalized === "canceled" || normalized === "cancelled" || normalized === "timed_out" || normalized === "timeout";
1155
+ }
1156
+ function readStatusLabel(body) {
1157
+ const status = readPreviewStatus(body);
1158
+ return typeof status === "string" ? status : void 0;
1159
+ }
1160
+ async function requestPreview(params) {
1161
+ const initial = await fetchCloudJson({
1162
+ url: `${params.apiBaseUrl}/v1/cloud/preview`,
1163
+ apiKey: params.apiKey,
1164
+ init: {
1165
+ method: "POST",
1166
+ body: JSON.stringify({
1167
+ config: params.config,
1168
+ configPath: path.relative(params.rootDir, params.configPath) || DOCS_JSON_FILE,
1169
+ repository: resolveGitRepositoryMetadata(params.rootDir)
1170
+ })
1171
+ }
1172
+ });
1173
+ const initialUrl = readPreviewUrl(initial);
1174
+ if (initialUrl) return {
1175
+ url: initialUrl,
1176
+ response: initial
1177
+ };
1178
+ const statusUrl = readStatusUrl(initial, params.apiBaseUrl);
1179
+ if (!statusUrl) throw new Error("Docs Cloud accepted the preview request but did not return a preview URL or status URL.");
1180
+ const startedAt = Date.now();
1181
+ while (Date.now() - startedAt < params.timeoutMs) {
1182
+ await new Promise((resolve) => setTimeout(resolve, params.pollIntervalMs));
1183
+ params.spinner.update("Waiting for Docs Cloud preview deployment");
1184
+ const statusBody = await fetchCloudJson({
1185
+ url: statusUrl,
1186
+ apiKey: params.apiKey
1187
+ });
1188
+ const url = readPreviewUrl(statusBody);
1189
+ if (url) return {
1190
+ url,
1191
+ response: statusBody
1192
+ };
1193
+ if (isTerminalFailureStatus(readStatusLabel(statusBody))) throw new Error(readPreviewFailureMessage(statusBody, "Docs Cloud preview failed."));
1194
+ }
1195
+ throw new Error("Docs Cloud preview timed out before a URL was ready.");
1196
+ }
1197
+ function createSpinner(initialMessage, options = {}) {
1198
+ const interactive = Boolean(process.stdout.isTTY && !options.json && process.env.NODE_ENV !== "test");
1199
+ const frames = [
1200
+ "-",
1201
+ "\\",
1202
+ "|",
1203
+ "/"
1204
+ ];
1205
+ let frame = 0;
1206
+ let message = initialMessage;
1207
+ let timer;
1208
+ const render = () => {
1209
+ process.stdout.write(`\r${pc.cyan(frames[frame % frames.length])} ${message}`);
1210
+ frame += 1;
1211
+ };
1212
+ if (interactive) {
1213
+ render();
1214
+ timer = setInterval(render, 90);
1215
+ } else if (!options.json) console.log(`${pc.dim("-")} ${initialMessage}`);
1216
+ const clear = () => {
1217
+ if (timer) {
1218
+ clearInterval(timer);
1219
+ timer = void 0;
1220
+ }
1221
+ if (interactive) process.stdout.write("\r\x1B[K");
1222
+ };
1223
+ return {
1224
+ update(nextMessage) {
1225
+ message = nextMessage;
1226
+ if (!interactive && !options.json) console.log(`${pc.dim("-")} ${nextMessage}`);
1227
+ },
1228
+ succeed(doneMessage) {
1229
+ clear();
1230
+ if (!options.json) console.log(`${pc.green("ok")} ${doneMessage}`);
1231
+ },
1232
+ fail(doneMessage) {
1233
+ clear();
1234
+ if (!options.json) console.log(`${pc.red("error")} ${doneMessage}`);
1235
+ },
1236
+ stop() {
1237
+ clear();
1238
+ }
1239
+ };
1240
+ }
1241
+ async function checkCloudConfig(options = {}) {
1242
+ const rootDir = options.rootDir ?? process.cwd();
1243
+ const docsJsonPath = path.join(rootDir, DOCS_JSON_FILE);
1244
+ const existing = readExistingDocsJson(docsJsonPath);
1245
+ const snapshot = await loadDocsConfigSnapshot(rootDir, options.configPath);
1246
+ const config = materializeDocsJsonObject({
1247
+ rootDir,
1248
+ snapshot,
1249
+ existing
1250
+ });
1251
+ const serialized = serializeMaterializedDocsJson(config);
1252
+ const previous = existing ? fs.readFileSync(docsJsonPath, "utf-8") : void 0;
1253
+ const apiBaseUrlResolution = resolveApiBaseUrl(options, rootDir);
1254
+ const apiBaseUrl = apiBaseUrlResolution.url;
1255
+ const apiKeyEnv = config.cloud?.apiKey?.env ?? DOCS_CLOUD_DEFAULT_API_KEY_ENV;
1256
+ const env = readCombinedEnv(rootDir);
1257
+ const siteOrigin = readDocsSiteOrigin(snapshot, env);
1258
+ const checks = [];
1259
+ const configPath = snapshot.path ?? docsJsonPath;
1260
+ const network = options.network !== false;
1261
+ const explicitApiKey = options.apiKey?.trim();
1262
+ const targetSet = resolveCloudCheckTargets(options);
1263
+ const targets = CLOUD_CHECK_TARGETS.filter((target) => targetSet.has(target));
1264
+ const checkDeploy = targetSet.has("deploy");
1265
+ const checkAnalytics = targetSet.has("analytics");
1266
+ const checkAskAi = targetSet.has("ask-ai");
1267
+ const checkProjectEnv = checkAnalytics || checkAskAi;
1268
+ let identity;
1269
+ 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.`));
1270
+ 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.`));
1271
+ 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}.`, {
1272
+ source: apiBaseUrlResolution.source,
1273
+ ...apiBaseUrlResolution.env ? { env: apiBaseUrlResolution.env } : {}
1274
+ }));
1275
+ 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 PUBLIC_BASE_URL, PUBLIC_SITE_URL, NEXT_PUBLIC_BASE_URL, NEXT_PUBLIC_SITE_URL, SITE_URL, or a docs config baseUrl.", siteOrigin ? {
1276
+ origin: siteOrigin.origin,
1277
+ source: siteOrigin.source
1278
+ } : void 0));
1279
+ const apiKey = explicitApiKey || readEnvValue(env, apiKeyEnv);
1280
+ if (checkDeploy) {
1281
+ try {
1282
+ normalizeEnvName(apiKeyEnv, DOCS_CLOUD_DEFAULT_API_KEY_ENV);
1283
+ checks.push(createCheck("apiKey.config", "pass", `Using cloud.apiKey.env ${apiKeyEnv}.`));
1284
+ } catch (error) {
1285
+ checks.push(createCheck("apiKey.config", "fail", error instanceof Error ? error.message : `Invalid API key env ${apiKeyEnv}.`));
1286
+ }
1287
+ 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.`, {
1288
+ env: apiKeyEnv,
1289
+ source: explicitApiKey ? "flag" : apiKey ? "env" : "missing"
1290
+ }));
1291
+ }
1292
+ 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."));
1293
+ if (checkDeploy) {
1294
+ if (config.cloud?.deploy?.enabled === false) checks.push(createCheck("deploy.enabled", "fail", "Docs Cloud deployment is disabled by cloud.deploy.enabled: false."));
1295
+ else checks.push(createCheck("deploy.enabled", "pass", "Docs Cloud deployment is enabled."));
1296
+ if (config.cloud?.preview?.enabled === false) checks.push(createCheck("preview.enabled", "fail", "Docs Cloud preview deployment is disabled by cloud.preview.enabled: false."));
1297
+ }
1298
+ const runtimeAnalyticsDisabled = readRuntimeAnalyticsDisabled(snapshot);
1299
+ const cloudAnalyticsEnabled = isCloudAnalyticsEnabled(config.cloud?.analytics);
1300
+ if (checkAnalytics) {
1301
+ if (runtimeAnalyticsDisabled) checks.push(createCheck("analytics.runtime", "fail", "Runtime analytics is disabled by analytics: false or analytics.enabled: false."));
1302
+ else checks.push(createCheck("analytics.runtime", "pass", "Runtime analytics is not disabled."));
1303
+ 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."));
1304
+ }
1305
+ const projectEnv = readFirstEnv(env, DOCS_CLOUD_PROJECT_ID_ENVS);
1306
+ const analyticsNeedsProjectId = cloudAnalyticsEnabled && !runtimeAnalyticsDisabled;
1307
+ 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));
1308
+ const aiProvider = readAiProvider(snapshot);
1309
+ let askAiCorsMode = "none";
1310
+ if (checkAskAi) if (aiProvider === "docs-cloud") {
1311
+ checks.push(createCheck("askAi.provider", "pass", "Ask AI is configured with provider: \"docs-cloud\"."));
1312
+ const configuredApiKeyEnv = readConfiguredCloudApiKeyEnv(snapshot);
1313
+ const publicApiKeyEnv = configuredApiKeyEnv && isBrowserSafeEnvName(configuredApiKeyEnv) ? configuredApiKeyEnv : readFirstEnv(env, [DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV])?.name ?? DEFAULT_PUBLIC_DOCS_CLOUD_API_KEY_ENV;
1314
+ const publicApiKey = readEnvValue(env, publicApiKeyEnv);
1315
+ const publicProjectEnv = readFirstEnv(env, [DOCS_CLOUD_DEFAULT_ANALYTICS_PROJECT_ID_ENV]);
1316
+ const serverApiKeyEnv = configuredApiKeyEnv ?? DOCS_CLOUD_DEFAULT_API_KEY_ENV;
1317
+ const serverApiKey = readEnvValue(env, serverApiKeyEnv);
1318
+ if (publicApiKey && publicProjectEnv) {
1319
+ checks.push(createCheck("askAi.direct", "pass", `Ask AI can call the Docs Cloud knowledge endpoint directly with ${publicApiKeyEnv}.`, {
1320
+ apiKeyEnv: publicApiKeyEnv,
1321
+ projectIdEnv: publicProjectEnv.name
1322
+ }));
1323
+ askAiCorsMode = "direct";
1324
+ } else if (serverApiKey && projectEnv) {
1325
+ 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}.`, {
1326
+ apiKeyEnv: serverApiKeyEnv,
1327
+ projectIdEnv: projectEnv.name,
1328
+ proxy: true
1329
+ }));
1330
+ askAiCorsMode = "proxy";
1331
+ } 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 }));
1332
+ } else if (aiProvider) checks.push(createCheck("askAi.provider", "pass", `Ask AI provider is ${aiProvider}.`));
1333
+ else checks.push(createCheck("askAi.provider", "warn", "Ask AI is not configured with provider: \"docs-cloud\"."));
1334
+ if (checkAnalytics || checkAskAi) if (!network) checks.push(createCheck("cloud.cors", "warn", "Skipped Docs Cloud CORS validation because --no-network was passed."));
1335
+ else if (!siteOrigin) checks.push(createCheck("cloud.cors", "warn", "Could not infer the docs site origin for CORS checks. Set PUBLIC_BASE_URL, NEXT_PUBLIC_BASE_URL, or a docs config baseUrl."));
1336
+ else {
1337
+ if (checkAnalytics) try {
1338
+ const cors = await checkCorsPreflight({
1339
+ url: `${apiBaseUrl}/v1/analytics/events`,
1340
+ origin: siteOrigin.origin,
1341
+ requestHeaders: "content-type"
1342
+ });
1343
+ checks.push(createCheck("cors.analytics", cors.ok ? "pass" : "fail", cors.ok ? `Analytics CORS allows ${siteOrigin.origin}.` : `Analytics CORS blocked ${siteOrigin.origin}.`, {
1344
+ origin: siteOrigin.origin,
1345
+ originSource: siteOrigin.source,
1346
+ status: cors.status,
1347
+ allowOrigin: cors.allowOrigin,
1348
+ allowMethods: cors.allowMethods,
1349
+ allowHeaders: cors.allowHeaders
1350
+ }));
1351
+ } catch (error) {
1352
+ checks.push(createCheck("cors.analytics", "fail", error instanceof Error ? `Analytics CORS check failed: ${error.message}` : "Analytics CORS check failed."));
1353
+ }
1354
+ if (checkAskAi && projectEnv && askAiCorsMode === "direct") try {
1355
+ const cors = await checkCorsPreflight({
1356
+ url: `${apiBaseUrl}/v1/projects/${encodeURIComponent(projectEnv.value)}/knowledge/ask`,
1357
+ origin: siteOrigin.origin,
1358
+ requestHeaders: "authorization, content-type"
1359
+ });
1360
+ checks.push(createCheck("cors.askAi", cors.ok ? "pass" : "fail", cors.ok ? `Ask AI CORS allows ${siteOrigin.origin}.` : `Ask AI CORS blocked ${siteOrigin.origin}.`, {
1361
+ origin: siteOrigin.origin,
1362
+ originSource: siteOrigin.source,
1363
+ status: cors.status,
1364
+ allowOrigin: cors.allowOrigin,
1365
+ allowMethods: cors.allowMethods,
1366
+ allowHeaders: cors.allowHeaders
1367
+ }));
1368
+ } catch (error) {
1369
+ checks.push(createCheck("cors.askAi", "fail", error instanceof Error ? `Ask AI CORS check failed: ${error.message}` : "Ask AI CORS check failed."));
1370
+ }
1371
+ 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 }));
1372
+ }
1373
+ if (checkDeploy) if (!network) checks.push(createCheck("apiKey.network", "warn", "Skipped Docs Cloud API validation because --no-network was passed."));
1374
+ else if (!apiKey) checks.push(createCheck("apiKey.network", "warn", "Skipped Docs Cloud API validation because no API key value was available."));
1375
+ else try {
1376
+ const response = await fetchCloudJson({
1377
+ url: `${apiBaseUrl}/v1/cloud/me`,
1378
+ apiKey
1379
+ });
1380
+ identity = summarizeIdentity(response);
1381
+ checks.push(createCheck("apiKey.network", "pass", `Validated API key with ${apiBaseUrl}.`, identity));
1382
+ const scopes = readApiKeyScopes(response);
1383
+ if (scopes.length === 0) checks.push(createCheck("apiKey.scopes", "warn", "Docs Cloud validated the API key but did not return scope metadata."));
1384
+ else {
1385
+ const missing = REQUIRED_PREVIEW_API_KEY_SCOPES.filter((scope) => !scopes.includes(scope));
1386
+ 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 }));
1387
+ }
1388
+ } catch (error) {
1389
+ checks.push(createCheck("apiKey.network", "fail", error instanceof Error ? error.message : "Could not validate Docs Cloud API key."));
1390
+ }
1391
+ return {
1392
+ ok: countChecks(checks, "fail") === 0,
1393
+ apiBaseUrl,
1394
+ configPath,
1395
+ docsJsonPath,
1396
+ apiKeyEnv,
1397
+ analyticsProjectIdEnv: projectEnv?.name,
1398
+ network,
1399
+ targets,
1400
+ checks,
1401
+ ...identity ? { identity } : {}
1402
+ };
1403
+ }
1404
+ async function runCloudCheck(options = {}) {
1405
+ const result = await checkCloudConfig(options);
1406
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1407
+ else {
1408
+ console.log(pc.bold("Docs Cloud check"));
1409
+ console.log(`${pc.dim("api")} ${result.apiBaseUrl}`);
1410
+ console.log(`${pc.dim("scope")} ${formatCloudCheckTargets(result.targets)}`);
1411
+ console.log();
1412
+ for (const check of result.checks) console.log(`${formatCheckStatus(check.status)} ${pc.bold(check.name)} ${check.message}`);
1413
+ console.log();
1414
+ if (result.ok) {
1415
+ const warnings = countChecks(result.checks, "warn");
1416
+ const suffix = warnings > 0 ? ` with ${warnings} warning${warnings === 1 ? "" : "s"}` : "";
1417
+ console.log(`${pc.green("ok")} Docs Cloud check passed${suffix}.`);
1418
+ } else {
1419
+ const failures = countChecks(result.checks, "fail");
1420
+ console.log(`${pc.red("fail")} Docs Cloud check failed with ${failures} failed check${failures === 1 ? "" : "s"}.`);
1421
+ }
1422
+ }
1423
+ if (!result.ok) {
1424
+ const error = /* @__PURE__ */ new Error("Docs Cloud check failed.");
1425
+ markCliErrorReported(error);
1426
+ throw error;
1427
+ }
1428
+ return result;
1429
+ }
1430
+ async function syncCloudConfig(options = {}) {
1431
+ const result = await materializeCloudConfig(options);
1432
+ if (options.json) {
1433
+ console.log(JSON.stringify(result, null, 2));
1434
+ return result;
1435
+ }
1436
+ const action = result.created ? "Created" : result.updated ? "Synced" : "Checked";
1437
+ console.log(`${pc.green("ok")} ${action} ${pc.cyan(path.relative(process.cwd(), result.docsJsonPath) || DOCS_JSON_FILE)}`);
1438
+ console.log(`${pc.dim("api key env")} ${result.apiKeyEnv}`);
1439
+ return result;
1440
+ }
1441
+ async function initCloudConfig(options = {}) {
1442
+ const rootDir = options.rootDir ?? process.cwd();
1443
+ const docsJsonPath = path.join(rootDir, DOCS_JSON_FILE);
1444
+ const existingDocsJson = readExistingDocsJson(docsJsonPath);
1445
+ const apiKeyEnv = normalizeEnvName(options.apiKeyEnv, DOCS_CLOUD_DEFAULT_API_KEY_ENV);
1446
+ const existingConfigPath = tryResolveDocsConfigPath(rootDir, options.configPath);
1447
+ const useDocsJsonAsSource = Boolean(existingDocsJson && !existingConfigPath);
1448
+ const docsInfraProfile = options.docsInfraProfile ?? (existingConfigPath || existingDocsJson ? void 0 : detectConnectedFumadocsProfile(rootDir));
1449
+ if (useDocsJsonAsSource) {
1450
+ const materialized = writeMaterializedCloudConfig({
1451
+ rootDir,
1452
+ docsJsonPath,
1453
+ existing: existingDocsJson,
1454
+ snapshot: {},
1455
+ docsInfraProfile,
1456
+ cloudInitApiKeyEnv: apiKeyEnv,
1457
+ detectConnectedDocsProfile: false,
1458
+ dropStaleDocsInfraProfile: false
1459
+ });
1460
+ return {
1461
+ configPath: materialized.configPath,
1462
+ docsJsonPath: materialized.docsJsonPath,
1463
+ apiKeyEnv: materialized.apiKeyEnv,
1464
+ analyticsProjectIdEnv: DOCS_CLOUD_DEFAULT_ANALYTICS_PROJECT_ID_ENV,
1465
+ configCreated: false,
1466
+ configUpdated: false,
1467
+ docsJsonCreated: false,
1468
+ docsJsonUpdated: materialized.updated,
1469
+ ...docsInfraProfile ? { docsInfraProfile } : {}
1470
+ };
1471
+ }
1472
+ const configUpdate = ensureDocsConfigCloudInit({
1473
+ rootDir,
1474
+ configPath: options.configPath,
1475
+ apiKeyEnv,
1476
+ docsInfraProfile
1477
+ });
1478
+ const materialized = await materializeCloudConfig({
1479
+ ...options,
1480
+ rootDir,
1481
+ configPath: path.relative(rootDir, configUpdate.configPath),
1482
+ docsInfraProfile
1483
+ });
1484
+ return {
1485
+ configPath: configUpdate.configPath,
1486
+ docsJsonPath: materialized.docsJsonPath,
1487
+ apiKeyEnv: materialized.apiKeyEnv,
1488
+ analyticsProjectIdEnv: DOCS_CLOUD_DEFAULT_ANALYTICS_PROJECT_ID_ENV,
1489
+ configCreated: configUpdate.created,
1490
+ configUpdated: configUpdate.updated,
1491
+ docsJsonCreated: materialized.created,
1492
+ docsJsonUpdated: materialized.updated,
1493
+ ...docsInfraProfile ? { docsInfraProfile } : {}
1494
+ };
1495
+ }
1496
+ async function runCloudInit(options = {}) {
1497
+ const result = await initCloudConfig(options);
1498
+ if (options.json) {
1499
+ console.log(JSON.stringify(result, null, 2));
1500
+ return result;
1501
+ }
1502
+ const relativeConfigPath = path.relative(process.cwd(), result.configPath) || "docs.config.ts";
1503
+ const relativeDocsJsonPath = path.relative(process.cwd(), result.docsJsonPath) || DOCS_JSON_FILE;
1504
+ const configAction = result.configCreated ? "Created" : result.configUpdated ? "Updated" : "Checked";
1505
+ const docsJsonAction = result.docsJsonCreated ? "created" : result.docsJsonUpdated ? "updated" : "checked";
1506
+ console.log(`${pc.green("ok")} ${configAction} ${pc.cyan(relativeConfigPath)}`);
1507
+ console.log(`${pc.green("ok")} ${docsJsonAction} ${pc.cyan(relativeDocsJsonPath)}`);
1508
+ if (result.docsInfraProfile?.engine === "fumadocs") console.log(`${pc.green("ok")} Connected existing ${pc.cyan("Fumadocs")} content at ${pc.cyan(result.docsInfraProfile.contentRoots.join(", "))}`);
1509
+ console.log();
1510
+ console.log(pc.bold("Add these env vars"));
1511
+ console.log(`${pc.cyan(result.apiKeyEnv)}=${pc.dim("paste_your_docs_cloud_api_key")}`);
1512
+ console.log(`${pc.cyan(result.analyticsProjectIdEnv)}=${pc.dim("paste_your_docs_cloud_project_id")}`);
1513
+ console.log();
1514
+ console.log(pc.dim("Use the same env vars in production. The API key value is never written to config."));
1515
+ console.log(pc.dim(`Then run ${pc.cyan("pnpm dlx @farming-labs/docs deploy")}.`));
1516
+ return result;
1517
+ }
1518
+ async function runCloudDeployment(options = {}) {
1519
+ const rootDir = options.rootDir ?? process.cwd();
1520
+ const spinner = createSpinner("Preparing Docs Cloud deployment", options);
1521
+ try {
1522
+ const materialized = await materializeCloudConfig({
1523
+ ...options,
1524
+ rootDir
1525
+ });
1526
+ spinner.update(`${materialized.created ? "Created" : "Synced"} ${DOCS_JSON_FILE}`);
1527
+ if (materialized.config.cloud?.enabled === false) throw new Error("Docs Cloud is disabled in cloud.enabled. Remove that override or set cloud.enabled: true before deploying hosted preview docs.");
1528
+ 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.");
1529
+ 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.");
1530
+ const apiKey = resolveApiKey(options, rootDir, materialized.apiKeyEnv);
1531
+ const apiBaseUrl = resolveApiBaseUrl(options, rootDir).url;
1532
+ spinner.update("Validating Docs Cloud API key");
1533
+ const identity = await fetchCloudJson({
1534
+ url: `${apiBaseUrl}/v1/cloud/me`,
1535
+ apiKey
1536
+ });
1537
+ assertPreviewApiKeyScopes(identity);
1538
+ spinner.update("Requesting Docs Cloud deployment");
1539
+ const preview = await requestPreview({
1540
+ apiBaseUrl,
1541
+ apiKey,
1542
+ config: materialized.config,
1543
+ configPath: materialized.docsJsonPath,
1544
+ rootDir,
1545
+ spinner,
1546
+ timeoutMs: options.timeoutMs ?? DEFAULT_PREVIEW_TIMEOUT_MS,
1547
+ pollIntervalMs: options.pollIntervalMs ?? DEFAULT_PREVIEW_POLL_INTERVAL_MS
1548
+ });
1549
+ spinner.succeed("Docs Cloud deployment is ready");
1550
+ const result = {
1551
+ url: preview.url,
1552
+ docsJsonPath: materialized.docsJsonPath,
1553
+ apiBaseUrl,
1554
+ identity,
1555
+ response: preview.response
1556
+ };
1557
+ if (options.json) console.log(JSON.stringify(result, null, 2));
1558
+ else console.log(`${pc.bold("Deployment")} ${pc.cyan(preview.url)}`);
1559
+ return result;
1560
+ } catch (error) {
1561
+ const message = error instanceof Error ? error.message : String(error);
1562
+ if (!options.json) {
1563
+ spinner.fail(message);
1564
+ markCliErrorReported(error);
1565
+ }
1566
+ throw error;
1567
+ } finally {
1568
+ spinner.stop();
1569
+ }
1570
+ }
1571
+ async function runCloudDeploy(options = {}) {
1572
+ return runCloudDeployment(options);
1573
+ }
1574
+ async function runCloudPreview(options = {}) {
1575
+ return runCloudDeployment(options);
1576
+ }
1577
+ function printCloudHelp() {
1578
+ console.log(`
1579
+ ${pc.bold("@farming-labs/docs cloud")}
1580
+
1581
+ ${pc.dim("Usage:")}
1582
+ ${pc.cyan("docs cloud init")} Add Docs Cloud config to ${pc.dim("docs.config.ts")} and ${pc.dim("docs.json")}
1583
+ ${pc.cyan("docs cloud check")} Validate Docs Cloud config, analytics envs, API key, and Ask AI wiring
1584
+ ${pc.cyan("docs deploy")} Sync ${pc.dim("docs.config.ts")} to ${pc.dim("docs.json")} and deploy hosted preview docs
1585
+ ${pc.cyan("docs cloud deploy")} Same as ${pc.cyan("docs deploy")}
1586
+ ${pc.cyan("docs preview")} Compatibility alias for ${pc.cyan("docs deploy")}
1587
+ ${pc.cyan("docs cloud preview")} Compatibility alias for ${pc.cyan("docs cloud deploy")}
1588
+ ${pc.cyan("docs cloud sync")} Only materialize cloud settings into ${pc.dim("docs.json")}
1589
+
1590
+ ${pc.dim("Options:")}
1591
+ ${pc.cyan("--config <path>")} Use a custom docs config path
1592
+ ${pc.cyan("--api-key-env <name>")} Env var that stores the Docs Cloud API key
1593
+ ${pc.cyan("--api-base-url <url>")} Override Docs Cloud API base URL
1594
+ ${pc.cyan("--api-key <key>")} Use an API key directly; prefer ${pc.dim("cloud.apiKey.env")}
1595
+ ${pc.cyan("--analytics")} Only check Docs Cloud analytics integration
1596
+ ${pc.cyan("--ask-ai")} Only check Docs Cloud Ask AI integration
1597
+ ${pc.cyan("--deploy")} Only check Docs Cloud deploy integration
1598
+ ${pc.cyan("--no-network")} Skip live Docs Cloud API validation for ${pc.cyan("cloud check")}
1599
+ ${pc.cyan("--json")} Print machine-readable output
1600
+
1601
+ ${pc.dim("API key scopes:")}
1602
+ ${REQUIRED_PREVIEW_API_KEY_SCOPES.join(", ")}
1603
+
1604
+ ${pc.dim("Config example:")}
1605
+ cloud: {
1606
+ apiKey: { env: "DOCS_CLOUD_API_KEY" },
1607
+ deploy: { enabled: true },
1608
+ analytics: { enabled: true, console: false, includeInputs: false },
1609
+ publish: { mode: "draft-pr", baseBranch: "main" },
1610
+ }
1611
+ `);
1612
+ }
1613
+
1614
+ //#endregion
1615
+ export { initCloudConfig, printCloudHelp, runCloudCheck, runCloudDeploy, runCloudInit, runCloudPreview, syncCloudConfig };