nrdocs 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/bin.mjs CHANGED
@@ -599,9 +599,292 @@ function authLogout(opts = {}) {
599
599
  }
600
600
 
601
601
  // src/commands/init.ts
602
+ import * as fs4 from "node:fs";
603
+ import * as path5 from "node:path";
604
+ import * as readline2 from "node:readline";
605
+
606
+ // src/config/docs-config.ts
607
+ import * as fs3 from "node:fs";
608
+ import * as path4 from "node:path";
609
+ import YAML from "yaml";
610
+
611
+ // src/renderer/navigation.ts
602
612
  import * as fs2 from "node:fs";
603
613
  import * as path3 from "node:path";
604
- import * as readline2 from "node:readline";
614
+ function extractTitle(markdownContent, filePath) {
615
+ const match2 = markdownContent.match(/^#\s+(.+)$/m);
616
+ if (match2) {
617
+ return match2[1].trim();
618
+ }
619
+ const basename3 = path3.basename(filePath, ".md");
620
+ if (basename3 === "index") {
621
+ return "Home";
622
+ }
623
+ return basename3.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
624
+ }
625
+ function findMarkdownFiles(dir, relativeTo) {
626
+ const results = [];
627
+ if (!fs2.existsSync(dir)) return results;
628
+ const entries = fs2.readdirSync(dir, { withFileTypes: true });
629
+ for (const entry of entries) {
630
+ const fullPath = path3.join(dir, entry.name);
631
+ if (entry.isDirectory()) {
632
+ results.push(...findMarkdownFiles(fullPath, relativeTo));
633
+ } else if (entry.isFile() && entry.name.endsWith(".md")) {
634
+ results.push(path3.relative(relativeTo, fullPath).replace(/\\/g, "/"));
635
+ }
636
+ }
637
+ return results;
638
+ }
639
+ function sortNavPaths(files, indexPath = "index.md") {
640
+ const normalizedIndex = indexPath.replace(/\\/g, "/");
641
+ const sorted = [...files].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
642
+ const indexIdx = sorted.indexOf(normalizedIndex);
643
+ if (indexIdx <= 0) return sorted;
644
+ const without = sorted.filter((f) => f !== normalizedIndex);
645
+ return [normalizedIndex, ...without];
646
+ }
647
+ function mdPathToHref(filePath) {
648
+ const normalized = filePath.replace(/\\/g, "/");
649
+ const withoutExt = normalized.replace(/\.md$/, "");
650
+ if (withoutExt === "index" || withoutExt.endsWith("/index")) {
651
+ const dir = withoutExt === "index" ? "" : withoutExt.slice(0, -"/index".length);
652
+ return dir ? `${dir}/` : "";
653
+ }
654
+ return `${withoutExt}/`;
655
+ }
656
+ function navEntryFromFile(contentDir, file) {
657
+ const fullPath = path3.join(contentDir, file);
658
+ const content = fs2.readFileSync(fullPath, "utf-8");
659
+ return {
660
+ title: extractTitle(content, file),
661
+ path: file
662
+ };
663
+ }
664
+ function folderSegmentToTitle(segment) {
665
+ if (segment === "index") return "Home";
666
+ return segment.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
667
+ }
668
+ function groupNavEntriesByFolders(files, contentDir, indexPath = "index.md") {
669
+ const sorted = sortNavPaths(files, indexPath);
670
+ const rootLinks = [];
671
+ const byFolder = /* @__PURE__ */ new Map();
672
+ for (const file of sorted) {
673
+ const dir = path3.dirname(file).replace(/\\/g, "/");
674
+ if (dir === ".") {
675
+ rootLinks.push(navEntryFromFile(contentDir, file));
676
+ continue;
677
+ }
678
+ const top = dir.split("/")[0];
679
+ const list2 = byFolder.get(top) ?? [];
680
+ list2.push(file);
681
+ byFolder.set(top, list2);
682
+ }
683
+ const sections = [];
684
+ for (const folder of [...byFolder.keys()].sort(
685
+ (a, b) => a.localeCompare(b, void 0, { numeric: true })
686
+ )) {
687
+ const paths = sortNavPaths(byFolder.get(folder), indexPath);
688
+ sections.push({
689
+ title: folderSegmentToTitle(folder),
690
+ children: paths.map((f) => navEntryFromFile(contentDir, f))
691
+ });
692
+ }
693
+ return [...rootLinks, ...sections];
694
+ }
695
+ function discoverNavEntries(contentDir, options) {
696
+ const indexPath = (options?.indexPath ?? "index.md").replace(/\\/g, "/");
697
+ const files = findMarkdownFiles(contentDir, contentDir);
698
+ return groupNavEntriesByFolders(files, contentDir, indexPath);
699
+ }
700
+ function navConfigToNavItems(entries, contentDir) {
701
+ const items = [];
702
+ const walk = (list2) => {
703
+ for (const entry of list2) {
704
+ if (entry.path) {
705
+ const normalizedPath = entry.path.replace(/\\/g, "/");
706
+ items.push({
707
+ title: entry.title,
708
+ path: normalizedPath,
709
+ href: mdPathToHref(normalizedPath)
710
+ });
711
+ }
712
+ if (entry.children?.length) {
713
+ walk(entry.children);
714
+ }
715
+ }
716
+ };
717
+ walk(entries);
718
+ if (items.length === 0) {
719
+ throw new Error("Nav has no pages (entries need path or nested children with paths)");
720
+ }
721
+ for (const item of items) {
722
+ const full = path3.join(contentDir, item.path);
723
+ if (!fs2.existsSync(full)) {
724
+ throw new Error(`Nav path not found: ${item.path}`);
725
+ }
726
+ }
727
+ return items;
728
+ }
729
+ function navConfigToSidebar(entries, activePath) {
730
+ const normalizedActive = activePath?.replace(/\\/g, "/");
731
+ const mapEntry = (entry) => {
732
+ const children = entry.children?.length ? entry.children.map(mapEntry).filter((n) => n !== null) : [];
733
+ if (entry.path) {
734
+ const normalizedPath = entry.path.replace(/\\/g, "/");
735
+ return {
736
+ kind: "link",
737
+ title: entry.title,
738
+ path: normalizedPath,
739
+ href: mdPathToHref(normalizedPath),
740
+ active: normalizedActive === normalizedPath
741
+ };
742
+ }
743
+ if (children.length === 0) return null;
744
+ const open = normalizedActive ? children.some((c) => sidebarContainsActive(c, normalizedActive)) : true;
745
+ return { kind: "section", title: entry.title, children, open };
746
+ };
747
+ return entries.map(mapEntry).filter((n) => n !== null);
748
+ }
749
+ function sidebarContainsActive(entry, activePath) {
750
+ if (entry.kind === "link") return entry.path === activePath;
751
+ return entry.children.some((c) => sidebarContainsActive(c, activePath));
752
+ }
753
+ function flattenNavPaths(entries) {
754
+ const paths = [];
755
+ const walk = (list2) => {
756
+ for (const e of list2) {
757
+ if (e.path) paths.push(e.path.replace(/\\/g, "/"));
758
+ if (e.children?.length) walk(e.children);
759
+ }
760
+ };
761
+ walk(entries);
762
+ return paths;
763
+ }
764
+
765
+ // src/config/docs-config.ts
766
+ function loadDocsConfig(docsDir) {
767
+ const configPath = path4.resolve(docsDir, "nrdocs.yml");
768
+ if (!fs3.existsSync(configPath)) {
769
+ throw new Error(`Config file not found: ${configPath}`);
770
+ }
771
+ const raw = fs3.readFileSync(configPath, "utf-8");
772
+ const config2 = YAML.parse(raw);
773
+ if (!config2 || typeof config2 !== "object") {
774
+ throw new Error(`Invalid config: ${configPath}`);
775
+ }
776
+ const sourceDir = config2.content?.source_dir ?? ".";
777
+ const contentDir = path4.resolve(docsDir, sourceDir);
778
+ return { config: config2, configPath, contentDir };
779
+ }
780
+ function hasExplicitNav(config2) {
781
+ return Array.isArray(config2.content?.nav);
782
+ }
783
+ function parseNavEntries(nav) {
784
+ if (!Array.isArray(nav)) {
785
+ throw new Error('content.nav must be a list or "auto"');
786
+ }
787
+ const entries = [];
788
+ for (const item of nav) {
789
+ if (!item || typeof item !== "object") {
790
+ throw new Error("Each nav entry must be an object with title");
791
+ }
792
+ const rec = item;
793
+ if (typeof rec["title"] !== "string") {
794
+ throw new Error("Each nav entry must have a title string");
795
+ }
796
+ const hasPath = typeof rec["path"] === "string";
797
+ const hasChildren = Array.isArray(rec["children"]) && rec["children"].length > 0;
798
+ if (!hasPath && !hasChildren) {
799
+ throw new Error("Each nav entry needs path and/or children");
800
+ }
801
+ const entry = { title: rec["title"] };
802
+ if (hasPath) {
803
+ entry.path = rec["path"].replace(/\\/g, "/");
804
+ }
805
+ if (Array.isArray(rec["children"])) {
806
+ entry.children = parseNavEntries(rec["children"]);
807
+ }
808
+ entries.push(entry);
809
+ }
810
+ return entries;
811
+ }
812
+ function getExplicitNav(config2) {
813
+ const nav = config2.content?.nav;
814
+ if (nav === void 0 || nav === "auto") return null;
815
+ if (Array.isArray(nav)) return parseNavEntries(nav);
816
+ throw new Error('content.nav must be "auto" or a list of entries');
817
+ }
818
+ function validateNavPaths(entries, contentDir) {
819
+ const errors = [];
820
+ const seen = /* @__PURE__ */ new Set();
821
+ const walk = (list2) => {
822
+ for (const e of list2) {
823
+ if (!e.path && !e.children?.length) {
824
+ errors.push(`Nav entry "${e.title}" has no path or children`);
825
+ continue;
826
+ }
827
+ if (e.path) {
828
+ const p = e.path.replace(/\\/g, "/");
829
+ if (seen.has(p)) {
830
+ errors.push(`Duplicate nav path: ${p}`);
831
+ }
832
+ seen.add(p);
833
+ const full = path4.join(contentDir, p);
834
+ if (!fs3.existsSync(full)) {
835
+ errors.push(`Nav path not found: ${p}`);
836
+ }
837
+ }
838
+ if (e.children?.length) walk(e.children);
839
+ }
840
+ };
841
+ walk(entries);
842
+ return { valid: errors.length === 0, errors };
843
+ }
844
+ function resolveContentIndex(navEntries, indexPath = "index.md") {
845
+ const paths = flattenNavPaths(navEntries);
846
+ const preferred = indexPath.replace(/\\/g, "/");
847
+ if (paths.includes(preferred)) return preferred;
848
+ return paths[0];
849
+ }
850
+ function writeNavToConfig(configPath, navEntries, options) {
851
+ const generatedBy = options?.generatedBy ?? "nrdocs nav generate";
852
+ let raw = fs3.readFileSync(configPath, "utf-8");
853
+ raw = raw.replace(/^# content\.nav generated by: .+\n/gm, "");
854
+ const config2 = YAML.parse(raw);
855
+ if (!config2 || typeof config2 !== "object") {
856
+ throw new Error(`Invalid config: ${configPath}`);
857
+ }
858
+ if (!config2.content) {
859
+ config2.content = {};
860
+ }
861
+ config2.content.nav = navEntries;
862
+ config2.content.index = resolveContentIndex(navEntries, options?.indexPath);
863
+ const doc = new YAML.Document(config2);
864
+ const header = `# content.nav generated by: ${generatedBy}
865
+ `;
866
+ const body = doc.toString();
867
+ fs3.writeFileSync(configPath, header + body, "utf-8");
868
+ }
869
+ function generateNavInConfig(docsDir, options) {
870
+ const loaded = loadDocsConfig(docsDir);
871
+ const indexPath = options?.indexPath ?? loaded.config.content?.index ?? "index.md";
872
+ const entries = discoverNavEntries(loaded.contentDir, { indexPath });
873
+ const pageCount = flattenNavPaths(entries).length;
874
+ if (pageCount === 0) return 0;
875
+ writeNavToConfig(loaded.configPath, entries, { ...options, indexPath });
876
+ return pageCount;
877
+ }
878
+ function formatNavYaml(navEntries) {
879
+ const partial = {
880
+ content: {
881
+ nav: navEntries
882
+ }
883
+ };
884
+ return YAML.stringify(partial).trimEnd();
885
+ }
886
+
887
+ // src/commands/init.ts
605
888
  async function prompt2(question, defaultValue) {
606
889
  const rl = readline2.createInterface({
607
890
  input: process.stdin,
@@ -622,7 +905,8 @@ site:
622
905
  api_url: ${apiUrl}
623
906
 
624
907
  content:
625
- index: index.md
908
+ source_dir: .
909
+ nav: auto
626
910
  `;
627
911
  if (requestedAccess) {
628
912
  yml += `
@@ -632,16 +916,6 @@ request:
632
916
  }
633
917
  return yml;
634
918
  }
635
- function generateIndexMd(title) {
636
- return `# ${title}
637
-
638
- Welcome to your documentation site powered by nrdocs.
639
-
640
- ## Getting Started
641
-
642
- Edit this file to add your documentation content.
643
- `;
644
- }
645
919
  function generateWorkflowYml(docsDir, apiUrl) {
646
920
  return `name: Publish Docs (nrdocs)
647
921
 
@@ -717,9 +991,9 @@ function normalizeUrl(url) {
717
991
  return normalized;
718
992
  }
719
993
  function readExistingConfig(configPath) {
720
- if (!fs2.existsSync(configPath)) return {};
994
+ if (!fs4.existsSync(configPath)) return {};
721
995
  try {
722
- const content = fs2.readFileSync(configPath, "utf-8");
996
+ const content = fs4.readFileSync(configPath, "utf-8");
723
997
  const titleMatch = content.match(/(?:title|name):\s*["']?([^"'\n]+)["']?/);
724
998
  const apiMatch = content.match(/api_url:\s*["']?([^"'\n]+)["']?/);
725
999
  return {
@@ -737,11 +1011,11 @@ async function handleInit(args2) {
737
1011
  process.exit(2);
738
1012
  }
739
1013
  const docsDir = opts.docsDir || "docs";
740
- const docsPath = path3.resolve(docsDir);
741
- const configFile = path3.join(docsPath, "nrdocs.yml");
1014
+ const docsPath = path5.resolve(docsDir);
1015
+ const configFile = path5.join(docsPath, "nrdocs.yml");
742
1016
  const existing = readExistingConfig(configFile);
743
- const configExists = fs2.existsSync(configFile);
744
- const dirName = path3.basename(process.cwd());
1017
+ const configExists = fs4.existsSync(configFile);
1018
+ const dirName = path5.basename(process.cwd());
745
1019
  let title = opts.title || existing.title;
746
1020
  if (!title) {
747
1021
  title = await prompt2("Site title", `${dirName} Docs`);
@@ -774,43 +1048,46 @@ async function handleInit(args2) {
774
1048
  process.exit(2);
775
1049
  }
776
1050
  apiUrl = normalizeUrl(apiUrl);
777
- const indexFile = path3.join(docsPath, "index.md");
778
- const workflowDir = path3.resolve(".github", "workflows");
779
- const workflowFile = path3.join(workflowDir, "nrdocs.yml");
780
- if (!opts.force && fs2.existsSync(workflowFile)) {
1051
+ const workflowDir = path5.resolve(".github", "workflows");
1052
+ const workflowFile = path5.join(workflowDir, "nrdocs.yml");
1053
+ if (!opts.force && fs4.existsSync(workflowFile)) {
781
1054
  console.error("Error: Workflow already exists:");
782
1055
  console.error(` ${workflowFile}`);
783
1056
  console.error("Use --force to overwrite.");
784
1057
  process.exit(3);
785
1058
  }
786
- fs2.mkdirSync(docsPath, { recursive: true });
787
- fs2.mkdirSync(workflowDir, { recursive: true });
1059
+ fs4.mkdirSync(docsPath, { recursive: true });
1060
+ fs4.mkdirSync(workflowDir, { recursive: true });
788
1061
  const createdConfig = !configExists || opts.force;
789
1062
  if (createdConfig) {
790
- fs2.writeFileSync(configFile, generateNrdocsYml(title, apiUrl, opts.requestedAccess));
1063
+ fs4.writeFileSync(configFile, generateNrdocsYml(title, apiUrl, opts.requestedAccess));
791
1064
  }
792
- const createdIndex = !fs2.existsSync(indexFile);
793
- if (createdIndex) {
794
- fs2.writeFileSync(indexFile, generateIndexMd(title));
1065
+ fs4.writeFileSync(workflowFile, generateWorkflowYml(docsDir, apiUrl));
1066
+ let navPageCount = 0;
1067
+ if (createdConfig || opts.force) {
1068
+ navPageCount = generateNavInConfig(docsDir, { generatedBy: "nrdocs init" });
795
1069
  }
796
- fs2.writeFileSync(workflowFile, generateWorkflowYml(docsDir, apiUrl));
797
1070
  console.log("nrdocs initialized successfully!");
798
1071
  console.log("");
799
1072
  console.log("Created/updated:");
800
1073
  if (createdConfig) {
801
- console.log(` ${path3.relative(process.cwd(), configFile)}`);
802
- }
803
- if (createdIndex) {
804
- console.log(` ${path3.relative(process.cwd(), indexFile)}`);
1074
+ console.log(` ${path5.relative(process.cwd(), configFile)}`);
805
1075
  }
806
- console.log(` ${path3.relative(process.cwd(), workflowFile)}`);
1076
+ console.log(` ${path5.relative(process.cwd(), workflowFile)}`);
807
1077
  if (!createdConfig) {
808
1078
  console.log("");
809
- console.log(`Using existing: ${path3.relative(process.cwd(), configFile)}`);
1079
+ console.log(`Using existing: ${path5.relative(process.cwd(), configFile)}`);
1080
+ }
1081
+ if (navPageCount > 0) {
1082
+ console.log(` content.nav: ${navPageCount} page(s) from markdown under ${docsDir}/`);
810
1083
  }
811
1084
  console.log("");
812
1085
  console.log("Next steps:");
813
- console.log(" 1. Add markdown files under docs/, then run: nrdocs nav generate");
1086
+ if (navPageCount === 0) {
1087
+ console.log(` 1. Add markdown files under ${docsDir}/, then run: nrdocs nav generate`);
1088
+ } else {
1089
+ console.log(` 1. Edit content.nav in ${path5.relative(process.cwd(), configFile)} to reorder pages`);
1090
+ }
814
1091
  console.log(" 2. Commit and push to trigger the workflow");
815
1092
  console.log(" 3. Ask your operator to approve the repo");
816
1093
  }
@@ -819,8 +1096,8 @@ async function handleInit(args2) {
819
1096
  import * as fs8 from "node:fs";
820
1097
 
821
1098
  // src/renderer/index.ts
822
- import * as fs6 from "node:fs";
823
- import * as path7 from "node:path";
1099
+ import * as fs7 from "node:fs";
1100
+ import * as path8 from "node:path";
824
1101
 
825
1102
  // ../../node_modules/.pnpm/markdown-it@14.1.1/node_modules/markdown-it/lib/common/utils.mjs
826
1103
  var utils_exports = {};
@@ -6022,97 +6299,6 @@ function renderMarkdown(content) {
6022
6299
  return md.render(content);
6023
6300
  }
6024
6301
 
6025
- // src/renderer/navigation.ts
6026
- import * as fs3 from "node:fs";
6027
- import * as path4 from "node:path";
6028
- function extractTitle(markdownContent, filePath) {
6029
- const match2 = markdownContent.match(/^#\s+(.+)$/m);
6030
- if (match2) {
6031
- return match2[1].trim();
6032
- }
6033
- const basename3 = path4.basename(filePath, ".md");
6034
- if (basename3 === "index") {
6035
- return "Home";
6036
- }
6037
- return basename3.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
6038
- }
6039
- function findMarkdownFiles(dir, relativeTo) {
6040
- const results = [];
6041
- if (!fs3.existsSync(dir)) return results;
6042
- const entries = fs3.readdirSync(dir, { withFileTypes: true });
6043
- for (const entry of entries) {
6044
- const fullPath = path4.join(dir, entry.name);
6045
- if (entry.isDirectory()) {
6046
- results.push(...findMarkdownFiles(fullPath, relativeTo));
6047
- } else if (entry.isFile() && entry.name.endsWith(".md")) {
6048
- results.push(path4.relative(relativeTo, fullPath).replace(/\\/g, "/"));
6049
- }
6050
- }
6051
- return results;
6052
- }
6053
- function sortNavPaths(files, indexPath = "index.md") {
6054
- const normalizedIndex = indexPath.replace(/\\/g, "/");
6055
- const sorted = [...files].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
6056
- const indexIdx = sorted.indexOf(normalizedIndex);
6057
- if (indexIdx <= 0) return sorted;
6058
- const without = sorted.filter((f) => f !== normalizedIndex);
6059
- return [normalizedIndex, ...without];
6060
- }
6061
- function mdPathToHref(filePath) {
6062
- const normalized = filePath.replace(/\\/g, "/");
6063
- const withoutExt = normalized.replace(/\.md$/, "");
6064
- if (withoutExt === "index" || withoutExt.endsWith("/index")) {
6065
- const dir = withoutExt === "index" ? "" : withoutExt.slice(0, -"/index".length);
6066
- return dir ? `${dir}/` : "";
6067
- }
6068
- return `${withoutExt}/`;
6069
- }
6070
- function discoverNavEntries(contentDir, options) {
6071
- const indexPath = (options?.indexPath ?? "index.md").replace(/\\/g, "/");
6072
- const files = findMarkdownFiles(contentDir, contentDir);
6073
- const sorted = sortNavPaths(files, indexPath);
6074
- return sorted.map((file) => {
6075
- const fullPath = path4.join(contentDir, file);
6076
- const content = fs3.readFileSync(fullPath, "utf-8");
6077
- return {
6078
- title: extractTitle(content, file),
6079
- path: file
6080
- };
6081
- });
6082
- }
6083
- function navConfigToNavItems(entries, contentDir) {
6084
- const items = [];
6085
- const walk = (list2) => {
6086
- for (const entry of list2) {
6087
- const normalizedPath = entry.path.replace(/\\/g, "/");
6088
- items.push({
6089
- title: entry.title,
6090
- path: normalizedPath,
6091
- href: mdPathToHref(normalizedPath)
6092
- });
6093
- if (entry.children?.length) {
6094
- walk(entry.children);
6095
- }
6096
- }
6097
- };
6098
- walk(entries);
6099
- for (const item of items) {
6100
- const full = path4.join(contentDir, item.path);
6101
- if (!fs3.existsSync(full)) {
6102
- throw new Error(`Nav path not found: ${item.path}`);
6103
- }
6104
- }
6105
- return items;
6106
- }
6107
- function generateAutoNav(docsDir, indexPath = "index.md") {
6108
- const entries = discoverNavEntries(docsDir, { indexPath });
6109
- return entries.map((e) => ({
6110
- title: e.title,
6111
- path: e.path,
6112
- href: mdPathToHref(e.path)
6113
- }));
6114
- }
6115
-
6116
6302
  // src/renderer/links.ts
6117
6303
  function rewriteLinks(html, basePath, owner, repo) {
6118
6304
  return html.replace(/<a\s+([^>]*?)href="([^"]*)"([^>]*?)>/g, (_match, before, href, after) => {
@@ -6282,6 +6468,13 @@ nav.sidebar .site-title{font-weight:700;font-size:1.1rem;color:var(--text);flex:
6282
6468
  #theme-toggle:hover{background:var(--bg-active)}
6283
6469
  nav.sidebar ul{list-style:none}
6284
6470
  nav.sidebar li{margin-bottom:0.25rem}
6471
+ nav.sidebar .nav-section{margin-top:0.5rem}
6472
+ nav.sidebar .nav-section>summary{cursor:pointer;font-size:0.8rem;font-weight:600;text-transform:uppercase;letter-spacing:0.04em;color:var(--toc-title);padding:0.35rem 0.6rem;list-style:none;border-radius:4px;user-select:none}
6473
+ nav.sidebar .nav-section>summary::-webkit-details-marker{display:none}
6474
+ nav.sidebar .nav-section>summary::before{content:"\u25B8";display:inline-block;margin-right:0.35rem;transition:transform 0.15s ease}
6475
+ nav.sidebar .nav-section[open]>summary::before{transform:rotate(90deg)}
6476
+ nav.sidebar .nav-section>summary:hover{background:var(--bg-hover);color:var(--text)}
6477
+ nav.sidebar .nav-section ul{padding-left:0.25rem;margin-top:0.15rem;margin-bottom:0.35rem}
6285
6478
  nav.sidebar a{color:var(--text-muted);text-decoration:none;padding:0.3rem 0.6rem;display:block;border-radius:4px;font-size:0.95rem}
6286
6479
  nav.sidebar a:hover{background:var(--bg-hover);color:var(--text)}
6287
6480
  nav.sidebar a.active{background:var(--bg-active);color:var(--link-active);font-weight:500}
@@ -6323,7 +6516,7 @@ footer a{color:var(--link)}
6323
6516
  <button type="button" id="theme-toggle" aria-label="Toggle color theme" title="Toggle light/dark mode">&#9789;</button>
6324
6517
  </div>
6325
6518
  <ul>
6326
- ${renderNavItems(nav, baseUrl)}
6519
+ ${renderNavTree(nav, baseUrl)}
6327
6520
  </ul>
6328
6521
  </nav>
6329
6522
  <div class="content-wrapper">
@@ -6374,22 +6567,30 @@ ${items}
6374
6567
  </ul>
6375
6568
  </aside>`;
6376
6569
  }
6377
- function renderNavItems(items, baseUrl) {
6378
- return items.map((item) => {
6379
- const href = baseUrl + item.href;
6380
- const activeClass = item.active ? ' class="active"' : "";
6381
- return `<li><a href="${escapeHtml2(href)}"${activeClass}>${escapeHtml2(item.title)}</a></li>`;
6382
- }).join("\n");
6570
+ function renderNavTree(entries, baseUrl) {
6571
+ return entries.map((entry) => renderNavNode(entry, baseUrl)).join("\n");
6572
+ }
6573
+ function renderNavNode(entry, baseUrl) {
6574
+ if (entry.kind === "link") {
6575
+ const href = baseUrl + entry.href;
6576
+ const activeClass = entry.active ? ' class="active"' : "";
6577
+ return `<li><a href="${escapeHtml2(href)}"${activeClass}>${escapeHtml2(entry.title)}</a></li>`;
6578
+ }
6579
+ const openAttr = entry.open !== false ? " open" : "";
6580
+ const childItems = entry.children.map((c) => renderNavNode(c, baseUrl)).join("\n");
6581
+ return `<li class="nav-section"><details class="nav-details"${openAttr}><summary>${escapeHtml2(entry.title)}</summary><ul>
6582
+ ${childItems}
6583
+ </ul></details></li>`;
6383
6584
  }
6384
6585
  function escapeHtml2(str) {
6385
6586
  return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
6386
6587
  }
6387
6588
 
6388
6589
  // src/renderer/assets.ts
6389
- import * as fs4 from "node:fs";
6390
- import * as path5 from "node:path";
6590
+ import * as fs5 from "node:fs";
6591
+ import * as path6 from "node:path";
6391
6592
  function collectAssets(docsDir) {
6392
- const resolvedDocsDir = path5.resolve(docsDir);
6593
+ const resolvedDocsDir = path6.resolve(docsDir);
6393
6594
  const files = [];
6394
6595
  collectFromDir(resolvedDocsDir, resolvedDocsDir, files);
6395
6596
  return files;
@@ -6397,13 +6598,13 @@ function collectAssets(docsDir) {
6397
6598
  function collectFromDir(dir, rootDir, results) {
6398
6599
  let entries;
6399
6600
  try {
6400
- entries = fs4.readdirSync(dir, { withFileTypes: true });
6601
+ entries = fs5.readdirSync(dir, { withFileTypes: true });
6401
6602
  } catch {
6402
6603
  return;
6403
6604
  }
6404
6605
  for (const entry of entries) {
6405
- const fullPath = path5.join(dir, entry.name);
6406
- const resolved = path5.resolve(fullPath);
6606
+ const fullPath = path6.join(dir, entry.name);
6607
+ const resolved = path6.resolve(fullPath);
6407
6608
  if (!resolved.startsWith(rootDir)) {
6408
6609
  continue;
6409
6610
  }
@@ -6411,37 +6612,37 @@ function collectFromDir(dir, rootDir, results) {
6411
6612
  if (entry.name.startsWith(".")) continue;
6412
6613
  collectFromDir(fullPath, rootDir, results);
6413
6614
  } else if (entry.isFile()) {
6414
- const ext = path5.extname(entry.name).toLowerCase();
6615
+ const ext = path6.extname(entry.name).toLowerCase();
6415
6616
  if (ext === ".md") continue;
6416
6617
  if (REJECTED_EXTENSIONS.has(ext)) continue;
6417
6618
  if (!ALLOWED_ASSET_EXTENSIONS.has(ext)) continue;
6418
- const relativePath = path5.relative(rootDir, fullPath);
6619
+ const relativePath = path6.relative(rootDir, fullPath);
6419
6620
  if (relativePath.includes("..")) continue;
6420
6621
  results.push({
6421
6622
  path: relativePath.replace(/\\/g, "/"),
6422
- content: fs4.readFileSync(fullPath)
6623
+ content: fs5.readFileSync(fullPath)
6423
6624
  });
6424
6625
  }
6425
6626
  }
6426
6627
  }
6427
6628
 
6428
6629
  // src/renderer/mermaid-runtime.ts
6429
- import * as fs5 from "node:fs";
6430
- import * as path6 from "node:path";
6630
+ import * as fs6 from "node:fs";
6631
+ import * as path7 from "node:path";
6431
6632
  import { fileURLToPath } from "node:url";
6432
- var __dirname = path6.dirname(fileURLToPath(import.meta.url));
6633
+ var __dirname = path7.dirname(fileURLToPath(import.meta.url));
6433
6634
  var MERMAID_RUNTIME_REL = "runtime/mermaid.min.js";
6434
6635
  var MERMAID_ARTIFACT_PATH = "_nrdocs/mermaid.min.js";
6435
6636
  function mermaidRuntimeCandidates() {
6436
6637
  return [
6437
- path6.join(__dirname, MERMAID_RUNTIME_REL),
6438
- path6.join(__dirname, "../../dist/runtime/mermaid.min.js")
6638
+ path7.join(__dirname, MERMAID_RUNTIME_REL),
6639
+ path7.join(__dirname, "../../dist/runtime/mermaid.min.js")
6439
6640
  ];
6440
6641
  }
6441
6642
  function loadMermaidRuntime() {
6442
6643
  for (const candidate of mermaidRuntimeCandidates()) {
6443
- if (fs5.existsSync(candidate)) {
6444
- return fs5.readFileSync(candidate);
6644
+ if (fs6.existsSync(candidate)) {
6645
+ return fs6.readFileSync(candidate);
6445
6646
  }
6446
6647
  }
6447
6648
  throw new Error(
@@ -6455,40 +6656,44 @@ function mermaidScriptSrcForOutput(outputPath) {
6455
6656
  }
6456
6657
 
6457
6658
  // src/renderer/index.ts
6458
- function resolveNavItems(resolvedDocsDir, nav, indexPath) {
6659
+ function resolveNav(resolvedDocsDir, nav, indexPath) {
6459
6660
  if (nav && nav !== "auto" && Array.isArray(nav)) {
6460
- return navConfigToNavItems(nav, resolvedDocsDir);
6661
+ return {
6662
+ items: navConfigToNavItems(nav, resolvedDocsDir),
6663
+ sidebarConfig: nav
6664
+ };
6461
6665
  }
6462
- return generateAutoNav(resolvedDocsDir, indexPath);
6666
+ const sidebarConfig = discoverNavEntries(resolvedDocsDir, { indexPath });
6667
+ return {
6668
+ items: navConfigToNavItems(sidebarConfig, resolvedDocsDir),
6669
+ sidebarConfig
6670
+ };
6463
6671
  }
6464
6672
  async function renderSite(options) {
6465
6673
  const { docsDir, siteTitle, baseUrl, owner, repo, nav, indexPath = "index.md" } = options;
6466
- const resolvedDocsDir = path7.resolve(docsDir);
6467
- const navItems = resolveNavItems(resolvedDocsDir, nav, indexPath);
6674
+ const resolvedDocsDir = path8.resolve(docsDir);
6675
+ const { items: navItems, sidebarConfig } = resolveNav(resolvedDocsDir, nav, indexPath);
6468
6676
  const siteBase = `/${owner}/${repo}/`;
6469
6677
  const renderedFiles = [];
6470
6678
  let siteHasMermaid = false;
6471
6679
  for (const navItem of navItems) {
6472
- const filePath = path7.join(resolvedDocsDir, navItem.path);
6473
- const markdownContent = fs6.readFileSync(filePath, "utf-8");
6680
+ const filePath = path8.join(resolvedDocsDir, navItem.path);
6681
+ const markdownContent = fs7.readFileSync(filePath, "utf-8");
6474
6682
  const pageHasMermaid = contentHasMermaid(markdownContent);
6475
6683
  if (pageHasMermaid) siteHasMermaid = true;
6476
6684
  let html = renderMarkdown(markdownContent);
6477
- const fileDir = path7.dirname(navItem.path);
6685
+ const fileDir = path8.dirname(navItem.path);
6478
6686
  const baseLinkPath = fileDir === "." ? "" : fileDir;
6479
6687
  html = rewriteLinks(html, baseLinkPath, owner, repo);
6480
6688
  const pageTitle = extractTitle(markdownContent, navItem.path);
6481
6689
  const canonicalUrl = `${baseUrl}${siteBase}${navItem.href}`;
6482
- const navWithActive = navItems.map((item) => ({
6483
- ...item,
6484
- active: item.path === navItem.path
6485
- }));
6690
+ const sidebar = navConfigToSidebar(sidebarConfig, navItem.path);
6486
6691
  const outputPath = navItem.href === "" ? "index.html" : navItem.href.replace(/\/$/, "") + "/index.html";
6487
6692
  const fullHtml = wrapInTemplate({
6488
6693
  title: pageTitle,
6489
6694
  siteTitle,
6490
6695
  content: html,
6491
- nav: navWithActive,
6696
+ nav: sidebar,
6492
6697
  canonicalUrl,
6493
6698
  baseUrl: siteBase,
6494
6699
  includeMermaid: pageHasMermaid,
@@ -6989,102 +7194,6 @@ var ApiClient = class {
6989
7194
  }
6990
7195
  };
6991
7196
 
6992
- // src/config/docs-config.ts
6993
- import * as fs7 from "node:fs";
6994
- import * as path8 from "node:path";
6995
- import YAML from "yaml";
6996
- function loadDocsConfig(docsDir) {
6997
- const configPath = path8.resolve(docsDir, "nrdocs.yml");
6998
- if (!fs7.existsSync(configPath)) {
6999
- throw new Error(`Config file not found: ${configPath}`);
7000
- }
7001
- const raw = fs7.readFileSync(configPath, "utf-8");
7002
- const config2 = YAML.parse(raw);
7003
- if (!config2 || typeof config2 !== "object") {
7004
- throw new Error(`Invalid config: ${configPath}`);
7005
- }
7006
- const sourceDir = config2.content?.source_dir ?? ".";
7007
- const contentDir = path8.resolve(docsDir, sourceDir);
7008
- return { config: config2, configPath, contentDir };
7009
- }
7010
- function hasExplicitNav(config2) {
7011
- return Array.isArray(config2.content?.nav);
7012
- }
7013
- function parseNavEntries(nav) {
7014
- if (!Array.isArray(nav)) {
7015
- throw new Error('content.nav must be a list or "auto"');
7016
- }
7017
- const entries = [];
7018
- for (const item of nav) {
7019
- if (!item || typeof item !== "object") {
7020
- throw new Error("Each nav entry must have title and path");
7021
- }
7022
- const rec = item;
7023
- if (typeof rec["title"] !== "string" || typeof rec["path"] !== "string") {
7024
- throw new Error("Each nav entry must have title and path strings");
7025
- }
7026
- const entry = {
7027
- title: rec["title"],
7028
- path: rec["path"].replace(/\\/g, "/")
7029
- };
7030
- if (Array.isArray(rec["children"])) {
7031
- entry.children = parseNavEntries(rec["children"]);
7032
- }
7033
- entries.push(entry);
7034
- }
7035
- return entries;
7036
- }
7037
- function getExplicitNav(config2) {
7038
- const nav = config2.content?.nav;
7039
- if (nav === void 0 || nav === "auto") return null;
7040
- if (Array.isArray(nav)) return parseNavEntries(nav);
7041
- throw new Error('content.nav must be "auto" or a list of entries');
7042
- }
7043
- function validateNavPaths(entries, contentDir) {
7044
- const errors = [];
7045
- const seen = /* @__PURE__ */ new Set();
7046
- const walk = (list2) => {
7047
- for (const e of list2) {
7048
- const p = e.path.replace(/\\/g, "/");
7049
- if (seen.has(p)) {
7050
- errors.push(`Duplicate nav path: ${p}`);
7051
- }
7052
- seen.add(p);
7053
- const full = path8.join(contentDir, p);
7054
- if (!fs7.existsSync(full)) {
7055
- errors.push(`Nav path not found: ${p}`);
7056
- }
7057
- if (e.children?.length) walk(e.children);
7058
- }
7059
- };
7060
- walk(entries);
7061
- return { valid: errors.length === 0, errors };
7062
- }
7063
- function writeNavToConfig(configPath, navEntries) {
7064
- let raw = fs7.readFileSync(configPath, "utf-8");
7065
- raw = raw.replace(/^# content\.nav generated by: nrdocs nav generate\n/gm, "");
7066
- const config2 = YAML.parse(raw);
7067
- if (!config2 || typeof config2 !== "object") {
7068
- throw new Error(`Invalid config: ${configPath}`);
7069
- }
7070
- if (!config2.content) {
7071
- config2.content = {};
7072
- }
7073
- config2.content.nav = navEntries;
7074
- const doc = new YAML.Document(config2);
7075
- const header = "# content.nav generated by: nrdocs nav generate\n";
7076
- const body = doc.toString();
7077
- fs7.writeFileSync(configPath, header + body, "utf-8");
7078
- }
7079
- function formatNavYaml(navEntries) {
7080
- const partial = {
7081
- content: {
7082
- nav: navEntries
7083
- }
7084
- };
7085
- return YAML.stringify(partial).trimEnd();
7086
- }
7087
-
7088
7197
  // src/commands/publish.ts
7089
7198
  function parsePublishArgs(args2) {
7090
7199
  const opts = {};
@@ -7569,7 +7678,7 @@ async function handleNavGenerate(args2) {
7569
7678
  console.log(formatNavYaml(entries));
7570
7679
  return;
7571
7680
  }
7572
- writeNavToConfig(loaded.configPath, entries);
7681
+ generateNavInConfig(docsDir, { generatedBy: "nrdocs nav generate", indexPath });
7573
7682
  if (!opts.json) {
7574
7683
  console.log(`Generated navigation for ${entries.length} page(s) in ${path10.relative(process.cwd(), configPath)}`);
7575
7684
  console.log("Edit the file to reorder or rename entries, then run publish.");
@@ -7647,23 +7756,52 @@ function normalizeUrl2(url) {
7647
7756
  }
7648
7757
  return normalized;
7649
7758
  }
7759
+ function packagedWorkerDir() {
7760
+ const cliEntry = process.argv[1];
7761
+ if (!cliEntry) return null;
7762
+ const distDir = path11.dirname(path11.resolve(cliEntry));
7763
+ const dir = path11.join(distDir, "deploy-worker");
7764
+ if (fs11.existsSync(path11.join(dir, "index.js"))) return dir;
7765
+ return null;
7766
+ }
7650
7767
  function findWorkerDir() {
7651
- const candidates = [
7768
+ const packaged = packagedWorkerDir();
7769
+ if (packaged) return packaged;
7770
+ const monorepoCandidates = [
7652
7771
  path11.resolve("packages/worker"),
7653
7772
  path11.resolve("../worker")
7654
7773
  ];
7655
- const cliDir = process.argv[1] ? path11.dirname(process.argv[1]) : process.cwd();
7656
- candidates.push(path11.resolve(cliDir, "../../../worker"));
7657
- candidates.push(path11.resolve(cliDir, "../../../../packages/worker"));
7658
- for (const candidate of candidates) {
7774
+ if (process.argv[1]) {
7775
+ const cliDir = path11.dirname(path11.resolve(process.argv[1]));
7776
+ monorepoCandidates.push(path11.resolve(cliDir, "../../../worker"));
7777
+ monorepoCandidates.push(path11.resolve(cliDir, "../../../../packages/worker"));
7778
+ }
7779
+ for (const candidate of monorepoCandidates) {
7659
7780
  if (fs11.existsSync(path11.join(candidate, "src", "index.ts"))) {
7660
7781
  return candidate;
7661
7782
  }
7662
7783
  }
7663
7784
  return null;
7664
7785
  }
7786
+ function isDocsContentRepo(cwd) {
7787
+ return fs11.existsSync(path11.join(cwd, "docs", "nrdocs.yml")) && !fs11.existsSync(path11.join(cwd, "packages", "worker", "src", "index.ts"));
7788
+ }
7789
+ function workerUsesBundledEntry(workerDir) {
7790
+ return fs11.existsSync(path11.join(workerDir, "index.js"));
7791
+ }
7665
7792
  async function handleDeploy(args2) {
7666
7793
  const opts = parseDeployArgs(args2);
7794
+ console.log("nrdocs deploy \u2014 Cloudflare infrastructure (Worker, D1, R2)");
7795
+ console.log("This does not publish markdown from the current repo.");
7796
+ console.log("Repo owners publish docs via GitHub Actions: nrdocs publish");
7797
+ console.log("");
7798
+ if (isDocsContentRepo(process.cwd())) {
7799
+ console.warn(
7800
+ "Note: This directory looks like a documentation repository (docs/nrdocs.yml)."
7801
+ );
7802
+ console.warn("You are deploying the nrdocs hosting stack, not site content here.");
7803
+ console.log("");
7804
+ }
7667
7805
  if (!checkWrangler()) {
7668
7806
  console.error("Error: Wrangler is not available.");
7669
7807
  console.error("Install it: npm install -g wrangler");
@@ -7707,7 +7845,10 @@ async function handleDeploy(args2) {
7707
7845
  }
7708
7846
  } else {
7709
7847
  instance = instance || await prompt3("Instance name", "default");
7710
- baseUrl = baseUrl || await prompt3("Docs base URL", "https://docs.example.com");
7848
+ baseUrl = baseUrl || await prompt3(
7849
+ "Public site URL (readers visit this host)",
7850
+ "https://docs.example.com"
7851
+ );
7711
7852
  }
7712
7853
  baseUrl = normalizeUrl2(baseUrl);
7713
7854
  const validation = validateInstanceName(instance);
@@ -7747,10 +7888,11 @@ async function handleDeploy(args2) {
7747
7888
  }
7748
7889
  const workerDir = findWorkerDir();
7749
7890
  if (!workerDir) {
7750
- console.error("Error: Cannot find packages/worker directory.");
7751
- console.error("Run nrdocs deploy from the nrdocs project root.");
7891
+ console.error("Error: Cannot find the nrdocs Worker bundle.");
7892
+ console.error("Reinstall the CLI (npm install -g nrdocs) or run from the nrdocs monorepo.");
7752
7893
  process.exit(4);
7753
7894
  }
7895
+ const workerMain = workerUsesBundledEntry(workerDir) ? "index.js" : "src/index.ts";
7754
7896
  console.log(`Creating R2 bucket ${names.r2}...`);
7755
7897
  const r2Check = runSilent("npx wrangler r2 bucket list");
7756
7898
  if (r2Check.ok && r2Check.stdout.includes(names.r2)) {
@@ -7784,7 +7926,7 @@ async function handleDeploy(args2) {
7784
7926
  }
7785
7927
  }
7786
7928
  const wranglerToml = `name = "${names.worker}"
7787
- main = "src/index.ts"
7929
+ main = "${workerMain}"
7788
7930
  compatibility_date = "2026-05-07"
7789
7931
 
7790
7932
  [[d1_databases]]
@@ -7871,13 +8013,15 @@ BASE_URL = "${baseUrl}"
7871
8013
  console.log(`Operator profile saved: ${profileName}`);
7872
8014
  }
7873
8015
  console.log("");
7874
- console.log("Deployment complete.");
7875
- console.log(` API: ${baseUrl}/api`);
7876
- console.log(` Docs: ${baseUrl}/`);
8016
+ console.log("Infrastructure deployment complete.");
8017
+ console.log(` API: ${baseUrl}/api`);
8018
+ console.log(` Public site: ${baseUrl}/`);
7877
8019
  console.log("");
7878
- console.log("Next:");
8020
+ console.log("Next (operator):");
7879
8021
  console.log(` nrdocs rules add 'OWNER/*' --access password`);
7880
8022
  console.log(" nrdocs repos");
8023
+ console.log("");
8024
+ console.log("Repo owners publish content with GitHub Actions (nrdocs publish), not deploy.");
7881
8025
  }
7882
8026
 
7883
8027
  // src/commands/repos.ts