create-thally-docs 0.10.78 → 0.10.80

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -29,6 +29,30 @@ Dependency installation is deliberately opt-in so the scaffold finishes in
29
29
  seconds. Pass `--install` to run it immediately, or `--no-install` to skip the
30
30
  interactive question explicitly.
31
31
 
32
+ ## Terminal output
33
+
34
+ Interactive terminals show the THALLY wordmark, task headings, and live progress.
35
+ The wordmark adapts to narrow terminals. Output automatically
36
+ uses plain text in CI, when output is not a TTY, with `TERM=dumb`, or when
37
+ `NO_COLOR` is set.
38
+
39
+ Setup and migration end with a highlighted next-action panel. The preview
40
+ command is one copyable line, including `npm install` when needed.
41
+
42
+ Scaffolding and migration keep subprocess logs quiet in interactive terminals.
43
+ Add `--verbose` to see installation and build logs as they happen:
44
+
45
+ ```bash
46
+ npx create-thally-docs my-docs --yes --install --verbose
47
+ npx create-thally-docs migrate https://github.com/your-org/your-docs --platform auto --verbose
48
+ ```
49
+
50
+ Long-running steps show their current activity and elapsed time. Plain logs report an elapsed heartbeat every 15 seconds. Conversion, rendering, and content validation run off the terminal thread so progress keeps updating during larger imports.
51
+
52
+ Failures always show diagnostics, including without `--verbose`; quiet output retains the last 64 KiB. Non-interactive
53
+ runs retain subprocess output for scripts and CI. These options also work with
54
+ `thally init` and `thally migrate`.
55
+
32
56
  ## What you get
33
57
 
34
58
  - **MDX content** in `src/content/`, navigation in `docs.json`
@@ -1,120 +1,21 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- initGit,
4
- installDeps,
3
+ findPublicSpecs,
4
+ shadowNote,
5
+ specHasHiddenOperations
6
+ } from "./chunk-KDLP27DS.js";
7
+ import {
5
8
  readDocsJson,
6
- scaffold,
7
9
  writeDocsJson
8
- } from "./chunk-QLMRRNLL.js";
9
- import {
10
- pruneMissingSiteLinks
11
- } from "./chunk-4VKRBJV2.js";
12
-
13
- // src/migrate/index.ts
14
- import { existsSync as existsSync3, mkdirSync, mkdtempSync, readFileSync as readFileSync4, rmSync, writeFileSync } from "fs";
15
- import { tmpdir } from "os";
16
- import { basename, dirname, isAbsolute, join as join4, relative as relative3, resolve, sep } from "path";
17
- import {
18
- cloneGitHubRepository,
19
- hydrateRemoteApiSpecs,
20
- migrateRepository,
21
- migrateUrl,
22
- parseGitHubRepositoryUrl,
23
- renderMigrationFiles
24
- } from "@thallylabs/migrate";
25
-
26
- // src/public-specs.ts
27
- import { lstatSync, readFileSync, readdirSync, statSync } from "fs";
28
- import { join, relative } from "path";
29
- import { parse as parseYaml } from "yaml";
30
- var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
31
- var SPEC_EXTENSIONS = /\.(?:json|ya?ml)$/i;
32
- var MAX_SPEC_BYTES = 1e7;
33
- var MAX_DEPTH = 8;
34
- var MAX_ENTRIES = 5e3;
35
- function shadowNote(urlPath) {
36
- return urlPath === "openapi.json" || urlPath === "openapi.yaml" ? ` It is also served at /${urlPath} in place of the filtered specification, so visitors get this raw file there.` : "";
37
- }
38
- var isFlagged = (value) => value === true || value === "true";
39
- function specHasHiddenOperations(spec, overrides) {
40
- if (overrides && typeof overrides === "object" && Object.values(overrides).some((o) => o?.hidden === true)) return true;
41
- const components = spec.components;
42
- for (const group of [spec.paths, spec.webhooks, spec["x-webhooks"], components?.pathItems]) {
43
- if (!group || typeof group !== "object") continue;
44
- for (const item of Object.values(group)) {
45
- if (!item || typeof item !== "object") continue;
46
- const record = item;
47
- if (isFlagged(record["x-excluded"]) || isFlagged(record["x-hidden"])) return true;
48
- for (const method of OPENAPI_METHODS) {
49
- const op = record[method];
50
- if (op && typeof op === "object" && (isFlagged(op["x-excluded"]) || isFlagged(op["x-hidden"]))) return true;
51
- }
52
- }
53
- }
54
- return false;
55
- }
56
- function findPublicSpecs(projectDir) {
57
- const publicDir = join(projectDir, "public");
58
- const scan = { specs: [], skipped: [] };
59
- let seen = 0;
60
- const walk = (dir, depth) => {
61
- let entries;
62
- try {
63
- entries = readdirSync(dir).sort();
64
- } catch {
65
- return;
66
- }
67
- for (const entry of entries) {
68
- if (++seen > MAX_ENTRIES) return;
69
- const full = join(dir, entry);
70
- let stat;
71
- try {
72
- stat = lstatSync(full);
73
- if (stat.isSymbolicLink()) {
74
- const target = statSync(full);
75
- if (!target.isFile()) continue;
76
- stat = target;
77
- }
78
- } catch {
79
- continue;
80
- }
81
- if (stat.isDirectory()) {
82
- if (depth < MAX_DEPTH && entry !== "node_modules") walk(full, depth + 1);
83
- continue;
84
- }
85
- if (!stat.isFile() || !SPEC_EXTENSIONS.test(entry)) continue;
86
- const urlPath = relative(publicDir, full).split(/[\\/]/).join("/");
87
- if (stat.size > MAX_SPEC_BYTES) {
88
- scan.skipped.push(`public/${urlPath}`);
89
- continue;
90
- }
91
- try {
92
- const raw = readFileSync(full, "utf8");
93
- const parsed = /\.json$/i.test(entry) ? JSON.parse(raw) : parseYaml(raw);
94
- if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
95
- const spec = parsed;
96
- if (typeof spec.openapi !== "string" && typeof spec.swagger !== "string") continue;
97
- scan.specs.push({ path: `public/${urlPath}`, urlPath, hasHiddenOperations: specHasHiddenOperations(spec) });
98
- } catch {
99
- }
100
- }
101
- };
102
- walk(publicDir, 0);
103
- return scan;
104
- }
105
-
106
- // src/migrate/validate.ts
107
- import { spawnSync } from "child_process";
108
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "fs";
109
- import { join as join3 } from "path";
10
+ } from "./chunk-J7255UQA.js";
110
11
 
111
12
  // src/check.ts
112
- import { existsSync, readFileSync as readFileSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
113
- import { join as join2, extname, relative as relative2 } from "path";
13
+ import { existsSync, readFileSync, readdirSync, statSync } from "fs";
14
+ import { join, extname, relative } from "path";
114
15
  import { execFileSync } from "child_process";
115
16
 
116
17
  // src/frontmatter.ts
117
- import { parse as parseYaml2 } from "yaml";
18
+ import { parse as parseYaml } from "yaml";
118
19
  function parseFrontmatter(raw) {
119
20
  const source = raw.charCodeAt(0) === 65279 ? raw.slice(1) : raw;
120
21
  const opening = /^---([^\r\n]*)\r?\n/.exec(source);
@@ -129,7 +30,7 @@ function parseFrontmatter(raw) {
129
30
  if (matter.trim() === "" || !["", "yaml", "yml"].includes(language)) {
130
31
  return { content, data: {} };
131
32
  }
132
- const parsed = parseYaml2(matter);
33
+ const parsed = parseYaml(matter);
133
34
  return {
134
35
  content,
135
36
  data: parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {}
@@ -137,7 +38,7 @@ function parseFrontmatter(raw) {
137
38
  }
138
39
 
139
40
  // src/check.ts
140
- import { parse as parseYaml3 } from "yaml";
41
+ import { parse as parseYaml2 } from "yaml";
141
42
 
142
43
  // src/openapi-publication.ts
143
44
  var METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
@@ -398,14 +299,14 @@ function checkDrift(projectDir, file, data, issues) {
398
299
  function scanMdx(dir, results) {
399
300
  let entries;
400
301
  try {
401
- entries = readdirSync2(dir);
302
+ entries = readdirSync(dir);
402
303
  } catch {
403
304
  return;
404
305
  }
405
306
  for (const entry of entries) {
406
- const fullPath = join2(dir, entry);
307
+ const fullPath = join(dir, entry);
407
308
  try {
408
- const stat = statSync2(fullPath);
309
+ const stat = statSync(fullPath);
409
310
  if (stat.isDirectory()) scanMdx(fullPath, results);
410
311
  else if (extname(entry).toLowerCase() === ".mdx") results.push(fullPath);
411
312
  } catch {
@@ -621,10 +522,10 @@ function checkUnpublishedOperationPages(projectDir, tabs, pages, issues) {
621
522
  const source = tab.api.source;
622
523
  const overrides = tab.api.overrides;
623
524
  if (/^https?:/i.test(source)) return { source, overrides };
624
- const specPath = source.startsWith("/") ? join2(projectDir, "public", source.slice(1)) : join2(projectDir, source);
525
+ const specPath = source.startsWith("/") ? join(projectDir, "public", source.slice(1)) : join(projectDir, source);
625
526
  try {
626
- const raw = readFileSync2(specPath, "utf8");
627
- return { source, overrides, document: /\.json$/i.test(source) ? JSON.parse(raw) : parseYaml3(raw) };
527
+ const raw = readFileSync(specPath, "utf8");
528
+ return { source, overrides, document: /\.json$/i.test(source) ? JSON.parse(raw) : parseYaml2(raw) };
628
529
  } catch {
629
530
  return { source, overrides };
630
531
  }
@@ -643,15 +544,15 @@ function checkUnpublishedOperationPages(projectDir, tabs, pages, issues) {
643
544
  }
644
545
  }
645
546
  function validateOpenApi(projectDir, source, issues, overrides) {
646
- const specPath = source.startsWith("/") ? join2(projectDir, "public", source.slice(1)) : join2(projectDir, source);
547
+ const specPath = source.startsWith("/") ? join(projectDir, "public", source.slice(1)) : join(projectDir, source);
647
548
  if (!existsSync(specPath)) {
648
549
  issues.push({ severity: "error", message: `API reference points at "${source}" but the file does not exist`, file: source });
649
550
  return;
650
551
  }
651
552
  let spec;
652
553
  try {
653
- const raw = readFileSync2(specPath, "utf8");
654
- spec = source.endsWith(".json") ? JSON.parse(raw) : parseYaml3(raw);
554
+ const raw = readFileSync(specPath, "utf8");
555
+ spec = source.endsWith(".json") ? JSON.parse(raw) : parseYaml2(raw);
655
556
  } catch (err) {
656
557
  issues.push({ severity: "error", message: `OpenAPI spec is not valid ${source.endsWith(".json") ? "JSON" : "YAML"}: ${err.message}`, file: source });
657
558
  return;
@@ -693,13 +594,14 @@ function validateOpenApi(projectDir, source, issues, overrides) {
693
594
  }
694
595
  async function runCheck(projectDir, options) {
695
596
  const { fix, ci } = options;
696
- if (!existsSync(join2(projectDir, "docs.json"))) {
697
- console.error(`
597
+ if (!existsSync(join(projectDir, "docs.json"))) {
598
+ if (!options.silent) console.error(`
698
599
  \u274C Not a Thally project: docs.json not found in ${projectDir}
699
600
  `);
601
+ else options.onIssues?.([{ severity: "error", message: `docs.json not found in ${projectDir}` }]);
700
602
  return 1;
701
603
  }
702
- const contentDir = join2(projectDir, "src", "content");
604
+ const contentDir = join(projectDir, "src", "content");
703
605
  const issues = [];
704
606
  const config = readDocsJson(projectDir);
705
607
  const secondaryLocales = new Set(
@@ -731,7 +633,7 @@ async function runCheck(projectDir, options) {
731
633
  issues.push({ severity: "warning", message: `[duplicate] "${dup}" appears more than once in docs.json` });
732
634
  }
733
635
  for (const pageId of navPageIds) {
734
- const candidates = [join2(contentDir, `${pageId}.mdx`), join2(contentDir, `${pageId}/index.mdx`)];
636
+ const candidates = [join(contentDir, `${pageId}.mdx`), join(contentDir, `${pageId}/index.mdx`)];
735
637
  if (!candidates.some((c) => existsSync(c))) {
736
638
  issues.push({ severity: "error", message: `"${pageId}" is in docs.json but has no MDX file`, file: `src/content/${pageId}.mdx` });
737
639
  }
@@ -750,13 +652,13 @@ async function runCheck(projectDir, options) {
750
652
  let content = "";
751
653
  let lineOffset = 0;
752
654
  try {
753
- const raw = readFileSync2(filePath, "utf8");
655
+ const raw = readFileSync(filePath, "utf8");
754
656
  const parsed = parseFrontmatter(raw);
755
657
  data = parsed.data;
756
658
  content = parsed.content;
757
659
  lineOffset = raw.slice(0, raw.indexOf(content)).split("\n").length - 1;
758
660
  } catch {
759
- issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative2(projectDir, filePath) });
661
+ issues.push({ severity: "error", message: `Could not parse frontmatter`, file: relative(projectDir, filePath) });
760
662
  continue;
761
663
  }
762
664
  const { navPageId } = localizedPage(pageId, secondaryLocales);
@@ -765,10 +667,10 @@ async function runCheck(projectDir, options) {
765
667
  addOrphanToNav(projectDir, pageId);
766
668
  fixedOrphans.push(pageId);
767
669
  } else {
768
- issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file: relative2(projectDir, filePath) });
670
+ issues.push({ severity: "warning", message: `"${pageId}" is not in docs.json nav (orphan)`, file: relative(projectDir, filePath) });
769
671
  }
770
672
  }
771
- const rel2 = relative2(projectDir, filePath);
673
+ const rel2 = relative(projectDir, filePath);
772
674
  if (!data.title) issues.push({ severity: "warning", message: `Missing "title" in frontmatter`, file: rel2 });
773
675
  if (!data.description) issues.push({ severity: "warning", message: `Missing "description" in frontmatter`, file: rel2 });
774
676
  if (typeof data.openapi !== "string" && content.trim().length < 50) {
@@ -822,7 +724,7 @@ async function runCheck(projectDir, options) {
822
724
  }
823
725
  }
824
726
  }
825
- const publicDir = join2(projectDir, "public");
727
+ const publicDir = join(projectDir, "public");
826
728
  for (const { file, images, offset } of linksByFile) {
827
729
  for (const { target, line: contentLine } of images) {
828
730
  const line = contentLine + offset;
@@ -835,7 +737,7 @@ async function runCheck(projectDir, options) {
835
737
  } catch {
836
738
  continue;
837
739
  }
838
- if (!existsSync(join2(publicDir, decoded))) {
740
+ if (!existsSync(join(publicDir, decoded))) {
839
741
  issues.push({ severity: "warning", message: `Image not found: "${target}" has no file under public/`, file, line });
840
742
  }
841
743
  }
@@ -848,6 +750,7 @@ async function runCheck(projectDir, options) {
848
750
  const errors = issues.filter((i) => i.severity === "error");
849
751
  options.onIssues?.(issues);
850
752
  const warnings = issues.filter((i) => i.severity === "warning");
753
+ if (options.silent) return errors.length > 0 ? 1 : 0;
851
754
  if (ci) {
852
755
  for (const issue of issues) {
853
756
  const loc = issue.file ? `file=${issue.file}${issue.line ? `,line=${issue.line}` : ""}` : "";
@@ -893,231 +796,7 @@ thally check: ${errors.length} error(s), ${warnings.length} warning(s)`);
893
796
  return errors.length > 0 ? 1 : 0;
894
797
  }
895
798
 
896
- // src/migrate/validate.ts
897
- async function validateMigration(projectDir, skip = false, installationFailed = false) {
898
- const result = { content: "skipped", build: "skipped", messages: [], diagnostics: [] };
899
- if (skip) {
900
- result.messages.push("Validation was explicitly skipped; this import is not verified.");
901
- return result;
902
- }
903
- try {
904
- result.content = await runCheck(projectDir, { fix: false, ci: true, onIssues: (issues) => {
905
- result.diagnostics = issues;
906
- } }) === 0 ? "passed" : "failed";
907
- if (result.content === "failed") result.messages.push("Content validation failed. Review the check output; source links are not silently rewritten.");
908
- } catch (error) {
909
- result.content = "failed";
910
- result.messages.push(`Content validation failed: ${error instanceof Error ? error.message : String(error)}`);
911
- }
912
- if (installationFailed) {
913
- result.build = "failed";
914
- result.messages.push("Dependency installation failed; production build was not attempted. Review the installation output before retrying.");
915
- return result;
916
- }
917
- const packagePath = join3(projectDir, "package.json");
918
- try {
919
- const manifest = existsSync2(packagePath) ? JSON.parse(readFileSync3(packagePath, "utf8")) : void 0;
920
- if (!manifest?.scripts?.build) {
921
- result.messages.push("No build script is available; production rendering is not verified.");
922
- return result;
923
- }
924
- const build = spawnSync(process.platform === "win32" ? "npm.cmd" : "npm", ["run", "build"], {
925
- cwd: projectDir,
926
- stdio: "inherit",
927
- timeout: 10 * 60 * 1e3
928
- });
929
- result.build = build.status === 0 && !build.error ? "passed" : "failed";
930
- if (result.build === "failed") result.messages.push("Production build failed. Review the build output before publishing.");
931
- } catch (error) {
932
- result.build = "failed";
933
- result.messages.push(`Production build failed: ${error instanceof Error ? error.message : String(error)}`);
934
- }
935
- return result;
936
- }
937
-
938
- // src/migrate/index.ts
939
- function projectPath(projectDir, candidate) {
940
- const target = resolve(projectDir, candidate);
941
- const fromRoot = relative3(resolve(projectDir), target);
942
- if (isAbsolute(fromRoot) || fromRoot === ".." || fromRoot.startsWith(`..${sep}`)) {
943
- throw new Error(`Generated migration path escapes the project: ${candidate}`);
944
- }
945
- return target;
946
- }
947
- function ignoreQuarantineDirectory(projectDir) {
948
- const ignorePath = projectPath(projectDir, ".gitignore");
949
- const existing = existsSync3(ignorePath) ? readFileSync4(ignorePath, "utf8") : "";
950
- const rules = existing.split(/\r?\n/).map((line) => line.trim()).filter((line) => /^!?\/?migration-quarantine(?:\/(?:\*\*?)?)?$/.test(line));
951
- if (rules.length > 0 && !rules[rules.length - 1].startsWith("!")) return;
952
- const separator = existing === "" || existing.endsWith("\n") ? "" : "\n";
953
- writeFileSync(ignorePath, `${existing}${separator}
954
- # Access-restricted pages withheld by migration: local only, never commit or deploy
955
- /migration-quarantine/
956
- `);
957
- }
958
- function readExistingConfig(projectDir) {
959
- const configPath = projectPath(projectDir, "docs.json");
960
- if (!existsSync3(configPath)) return void 0;
961
- return JSON.parse(readFileSync4(configPath, "utf8"));
962
- }
963
- function resetFreshMigrationContent(projectDir) {
964
- const contentDirectory = projectPath(projectDir, "src/content");
965
- rmSync(contentDirectory, { recursive: true, force: true });
966
- mkdirSync(contentDirectory, { recursive: true });
967
- for (const sampleSpec of ["openapi.yaml", "openapi.json"]) {
968
- rmSync(projectPath(projectDir, sampleSpec), { force: true });
969
- }
970
- }
971
- async function discoverMigration(options) {
972
- const url = new URL(options.sourceUrl);
973
- if (url.hostname.toLowerCase() !== "github.com") {
974
- console.log(` \u{1F310} Discovering public docs at ${url.origin}${url.pathname}...`);
975
- return migrateUrl({
976
- sourceUrl: options.sourceUrl,
977
- platform: options.platform,
978
- maxPages: options.maxPages,
979
- fetcher: options.fetcher
980
- });
981
- }
982
- const source = parseGitHubRepositoryUrl(options.sourceUrl);
983
- if (options.branch) source.branch = options.branch;
984
- const temporaryRoot = mkdtempSync(join4(tmpdir(), "thally-migrate-"));
985
- const cloneDir = join4(temporaryRoot, "repository");
986
- console.log(` \u{1F4E6} Cloning ${source.owner}/${source.repo}...`);
987
- try {
988
- const cloneWarnings = [];
989
- await cloneGitHubRepository(source, cloneDir, cloneWarnings);
990
- const bundle = migrateRepository({
991
- repositoryDir: cloneDir,
992
- sourceUrl: options.sourceUrl,
993
- docsDir: options.docsDir ?? (source.docsDir || void 0),
994
- platform: options.platform
995
- });
996
- const hydrated = await hydrateRemoteApiSpecs(bundle, options.fetcher);
997
- return cloneWarnings.length > 0 ? { ...hydrated, warnings: [...cloneWarnings, ...hydrated.warnings] } : hydrated;
998
- } finally {
999
- rmSync(temporaryRoot, { recursive: true, force: true });
1000
- }
1001
- }
1002
- function publicSpecWarning(projectDir, bundle) {
1003
- const { specs, skipped } = findPublicSpecs(projectDir);
1004
- if (specs.length === 0 && skipped.length === 0) return null;
1005
- const migrated = new Set(bundle.assets.filter((asset) => asset.projectRelative).map((asset) => basename(asset.path)));
1006
- const lines = [
1007
- ...specs.map((spec) => `${spec.path}${migrated.has(basename(spec.path)) ? " (same file name as a migrated spec)" : ""}${spec.hasHiddenOperations ? " (contains x-excluded/x-hidden operations)" : ""}${shadowNote(spec.urlPath) ? ` (answers /${spec.urlPath} in place of the filtered specification)` : ""}`),
1008
- ...skipped.map((path) => `${path} (too large to inspect)`)
1009
- ];
1010
- return {
1011
- code: "unsupported-config",
1012
- message: `Existing OpenAPI files under public/ are served publicly as-is: ${lines.join("; ")}. Migrated specs are written to openapi/, and re-running migration does not remove old copies from public/. Delete any old copy manually (nothing was deleted), or its hidden operations stay downloadable.`
1013
- };
1014
- }
1015
- async function migrateDocs(options) {
1016
- const projectDir = resolve(options.projectDir);
1017
- const bundle = await discoverMigration(options);
1018
- if (!options.into) {
1019
- console.log(`
1020
- \u{1F3D7} Scaffolding new project at ${projectDir}...`);
1021
- await scaffold({
1022
- projectDir,
1023
- projectName: options.projectName ?? bundle.site?.name ?? "My Docs",
1024
- description: bundle.site?.description ?? `Documentation migrated from ${new URL(options.sourceUrl).hostname}`,
1025
- brandPreset: "primary",
1026
- // Source provenance is not the destination repository. Migrated pages
1027
- // live at new paths, so source URLs cannot power edit/issue actions.
1028
- repoUrl: "",
1029
- doInstall: false,
1030
- colors: bundle.site?.colors
1031
- });
1032
- resetFreshMigrationContent(projectDir);
1033
- pruneMissingSiteLinks(projectDir, new Set(bundle.pages.map((page) => page.id)));
1034
- } else if (!existsSync3(projectDir)) {
1035
- throw new Error(`Project directory "${projectDir}" does not exist. Use without --into to scaffold a new one.`);
1036
- }
1037
- if (!options.into) {
1038
- const starterConfig = readExistingConfig(projectDir);
1039
- if (starterConfig?.markdown) bundle.docsConfig.markdown ??= starterConfig.markdown;
1040
- if (!bundle.docsConfig.navbar?.primary) {
1041
- bundle.docsConfig.navbar = { ...bundle.docsConfig.navbar, primary: null };
1042
- }
1043
- bundle.docsConfig.i18n ??= { defaultLocale: "en", locales: [{ code: "en", label: "English" }] };
1044
- }
1045
- if (options.into) {
1046
- const stale = publicSpecWarning(projectDir, bundle);
1047
- if (stale) bundle.warnings.unshift(stale);
1048
- }
1049
- const rendered = renderMigrationFiles(bundle, {
1050
- existingConfig: options.into ? readExistingConfig(projectDir) : void 0,
1051
- existingComponentRegistry: existsSync3(projectPath(projectDir, "src/mdx/custom-components.tsx")) ? readFileSync4(projectPath(projectDir, "src/mdx/custom-components.tsx"), "utf8") : void 0
1052
- });
1053
- for (const file of rendered) {
1054
- const destination = projectPath(projectDir, file.path);
1055
- mkdirSync(dirname(destination), { recursive: true });
1056
- writeFileSync(destination, file.content);
1057
- }
1058
- if (bundle.quarantinedFiles?.length) ignoreQuarantineDirectory(projectDir);
1059
- const format = (warning) => `${warning.message}${warning.source ? ` (${warning.source})` : ""}`;
1060
- for (const warning of bundle.warnings.filter((item) => item.code !== "gated-page")) console.warn(` \u26A0 ${format(warning)}`);
1061
- const gatedWarnings = bundle.warnings.filter((item) => item.code === "gated-page");
1062
- if (gatedWarnings.length > 0) {
1063
- console.warn("\n \u{1F512} ACCESS-RESTRICTED CONTENT \u2014 review before publishing");
1064
- for (const warning of gatedWarnings) console.warn(` \u{1F512} ${format(warning)}`);
1065
- if (bundle.droppedGatedPages) {
1066
- console.warn(` \u{1F512} ${bundle.droppedGatedPages} access-restricted page(s) were dropped by the file limit: not published and not saved under migration-quarantine/; recover them from the source repository.`);
1067
- }
1068
- if (bundle.quarantinedFiles?.length || gatedWarnings.some((warning) => !/dashboard access settings/i.test(warning.message))) {
1069
- console.warn(" \u{1F512} Before publishing, review migration-quarantine/assets/ and the dashboard access settings of the source site.");
1070
- }
1071
- }
1072
- console.log(` \u2713 Imported ${bundle.pages.length} pages and ${bundle.assets.length} assets from ${bundle.platform}.`);
1073
- let installationFailed = false;
1074
- if (!options.skipValidation) console.log(" Migration validation runs project code locally, including imported MDX and components.");
1075
- if (!options.into && !options.skipValidation) {
1076
- try {
1077
- installDeps(projectDir);
1078
- } catch {
1079
- installationFailed = true;
1080
- }
1081
- }
1082
- console.log("\n Validating imported documentation...");
1083
- const validation = await validateMigration(projectDir, options.skipValidation, installationFailed);
1084
- const quarantinedPages = (bundle.quarantinedFiles ?? []).filter((file) => /\.mdx?$/i.test(file.path)).length;
1085
- const reportPath = projectPath(projectDir, "migration-report.json");
1086
- writeFileSync(reportPath, `${JSON.stringify({
1087
- version: 1,
1088
- sourceUrl: `${new URL(options.sourceUrl).origin}${new URL(options.sourceUrl).pathname}`,
1089
- platform: bundle.platform,
1090
- pages: bundle.pages.length,
1091
- assets: bundle.assets.length,
1092
- components: bundle.componentFiles?.length ?? 0,
1093
- quarantined: quarantinedPages,
1094
- quarantinedAssets: (bundle.quarantinedFiles?.length ?? 0) - quarantinedPages,
1095
- droppedGatedPages: bundle.droppedGatedPages ?? 0,
1096
- warnings: bundle.warnings,
1097
- validation
1098
- }, null, 2)}
1099
- `);
1100
- for (const message of validation.messages) console.warn(` \u26A0 ${message}`);
1101
- console.log(` Migration report: ${reportPath}`);
1102
- if (validation.content === "passed" && validation.build === "passed") {
1103
- console.log(` \u2713 Content and production build passed.${bundle.warnings.length ? " Review the migration warnings for compatibility limitations." : ""}`);
1104
- } else {
1105
- console.warn(" Import retained, but validation is incomplete. Do not publish without reviewing the report.");
1106
- }
1107
- if (!options.into) initGit(projectDir);
1108
- return {
1109
- pagesWritten: bundle.pages.length,
1110
- assetsWritten: bundle.assets.length,
1111
- projectDir,
1112
- platform: bundle.platform,
1113
- warnings: bundle.warnings,
1114
- validation,
1115
- reportPath
1116
- };
1117
- }
1118
-
1119
799
  export {
1120
800
  parseFrontmatter,
1121
- runCheck,
1122
- migrateDocs
801
+ runCheck
1123
802
  };
@@ -0,0 +1,34 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/docs-json.ts
4
+ import { readFileSync, writeFileSync } from "fs";
5
+ import { join } from "path";
6
+ function readDocsJson(projectDir) {
7
+ const docsPath = join(projectDir, "docs.json");
8
+ const raw = readFileSync(docsPath, "utf8");
9
+ return JSON.parse(raw);
10
+ }
11
+ function writeDocsJson(projectDir, config) {
12
+ const docsPath = join(projectDir, "docs.json");
13
+ writeFileSync(docsPath, JSON.stringify(config, null, 2) + "\n", "utf8");
14
+ }
15
+ function resetTrackingConfig(projectDir) {
16
+ const config = readDocsJson(projectDir);
17
+ if (config.tracking) {
18
+ delete config.tracking;
19
+ writeDocsJson(projectDir, config);
20
+ }
21
+ }
22
+ function writeTrackingConfig(projectDir, repos) {
23
+ if (repos.length === 0) return;
24
+ const config = readDocsJson(projectDir);
25
+ config.tracking = { repos: repos.map((r) => ({ owner: r.owner, repo: r.repo, branch: "main" })) };
26
+ writeDocsJson(projectDir, config);
27
+ }
28
+
29
+ export {
30
+ readDocsJson,
31
+ writeDocsJson,
32
+ resetTrackingConfig,
33
+ writeTrackingConfig
34
+ };
@@ -0,0 +1,87 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/public-specs.ts
4
+ import { lstatSync, readFileSync, readdirSync, statSync } from "fs";
5
+ import { join, relative } from "path";
6
+ import { parse as parseYaml } from "yaml";
7
+ var OPENAPI_METHODS = ["get", "post", "put", "patch", "delete", "options", "head", "trace"];
8
+ var SPEC_EXTENSIONS = /\.(?:json|ya?ml)$/i;
9
+ var MAX_SPEC_BYTES = 1e7;
10
+ var MAX_DEPTH = 8;
11
+ var MAX_ENTRIES = 5e3;
12
+ function shadowNote(urlPath) {
13
+ return urlPath === "openapi.json" || urlPath === "openapi.yaml" ? ` It is also served at /${urlPath} in place of the filtered specification, so visitors get this raw file there.` : "";
14
+ }
15
+ var isFlagged = (value) => value === true || value === "true";
16
+ function specHasHiddenOperations(spec, overrides) {
17
+ if (overrides && typeof overrides === "object" && Object.values(overrides).some((o) => o?.hidden === true)) return true;
18
+ const components = spec.components;
19
+ for (const group of [spec.paths, spec.webhooks, spec["x-webhooks"], components?.pathItems]) {
20
+ if (!group || typeof group !== "object") continue;
21
+ for (const item of Object.values(group)) {
22
+ if (!item || typeof item !== "object") continue;
23
+ const record = item;
24
+ if (isFlagged(record["x-excluded"]) || isFlagged(record["x-hidden"])) return true;
25
+ for (const method of OPENAPI_METHODS) {
26
+ const op = record[method];
27
+ if (op && typeof op === "object" && (isFlagged(op["x-excluded"]) || isFlagged(op["x-hidden"]))) return true;
28
+ }
29
+ }
30
+ }
31
+ return false;
32
+ }
33
+ function findPublicSpecs(projectDir) {
34
+ const publicDir = join(projectDir, "public");
35
+ const scan = { specs: [], skipped: [] };
36
+ let seen = 0;
37
+ const walk = (dir, depth) => {
38
+ let entries;
39
+ try {
40
+ entries = readdirSync(dir).sort();
41
+ } catch {
42
+ return;
43
+ }
44
+ for (const entry of entries) {
45
+ if (++seen > MAX_ENTRIES) return;
46
+ const full = join(dir, entry);
47
+ let stat;
48
+ try {
49
+ stat = lstatSync(full);
50
+ if (stat.isSymbolicLink()) {
51
+ const target = statSync(full);
52
+ if (!target.isFile()) continue;
53
+ stat = target;
54
+ }
55
+ } catch {
56
+ continue;
57
+ }
58
+ if (stat.isDirectory()) {
59
+ if (depth < MAX_DEPTH && entry !== "node_modules") walk(full, depth + 1);
60
+ continue;
61
+ }
62
+ if (!stat.isFile() || !SPEC_EXTENSIONS.test(entry)) continue;
63
+ const urlPath = relative(publicDir, full).split(/[\\/]/).join("/");
64
+ if (stat.size > MAX_SPEC_BYTES) {
65
+ scan.skipped.push(`public/${urlPath}`);
66
+ continue;
67
+ }
68
+ try {
69
+ const raw = readFileSync(full, "utf8");
70
+ const parsed = /\.json$/i.test(entry) ? JSON.parse(raw) : parseYaml(raw);
71
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) continue;
72
+ const spec = parsed;
73
+ if (typeof spec.openapi !== "string" && typeof spec.swagger !== "string") continue;
74
+ scan.specs.push({ path: `public/${urlPath}`, urlPath, hasHiddenOperations: specHasHiddenOperations(spec) });
75
+ } catch {
76
+ }
77
+ }
78
+ };
79
+ walk(publicDir, 0);
80
+ return scan;
81
+ }
82
+
83
+ export {
84
+ shadowNote,
85
+ specHasHiddenOperations,
86
+ findPublicSpecs
87
+ };