nrdocs 0.1.4 → 0.1.6

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 (2) hide show
  1. package/dist/bin.mjs +860 -155
  2. package/package.json +4 -1
package/dist/bin.mjs CHANGED
@@ -498,10 +498,10 @@ async function prompt(question) {
498
498
  input: process.stdin,
499
499
  output: process.stdout
500
500
  });
501
- return new Promise((resolve7) => {
501
+ return new Promise((resolve8) => {
502
502
  rl.question(question, (answer) => {
503
503
  rl.close();
504
- resolve7(answer.trim());
504
+ resolve8(answer.trim());
505
505
  });
506
506
  });
507
507
  }
@@ -608,17 +608,18 @@ async function prompt2(question, defaultValue) {
608
608
  output: process.stdout
609
609
  });
610
610
  const suffix = defaultValue ? ` (${defaultValue})` : "";
611
- return new Promise((resolve7) => {
611
+ return new Promise((resolve8) => {
612
612
  rl.question(`${question}${suffix}: `, (answer) => {
613
613
  rl.close();
614
- resolve7(answer.trim() || defaultValue || "");
614
+ resolve8(answer.trim() || defaultValue || "");
615
615
  });
616
616
  });
617
617
  }
618
- function generateNrdocsYml(title, requestedAccess) {
618
+ function generateNrdocsYml(title, apiUrl, requestedAccess) {
619
619
  let yml = `# nrdocs site configuration
620
620
  site:
621
621
  title: "${title}"
622
+ api_url: ${apiUrl}
622
623
 
623
624
  content:
624
625
  index: index.md
@@ -670,6 +671,11 @@ jobs:
670
671
  - name: Install nrdocs CLI
671
672
  run: npm install -g nrdocs
672
673
 
674
+ - name: Diagnose setup
675
+ run: nrdocs doctor --ci
676
+ env:
677
+ NRDOCS_API_URL: ${apiUrl}
678
+
673
679
  - name: Publish docs
674
680
  run: nrdocs publish --docs-dir ${docsDir}
675
681
  env:
@@ -780,7 +786,7 @@ async function handleInit(args2) {
780
786
  fs2.mkdirSync(workflowDir, { recursive: true });
781
787
  const createdConfig = !configExists || opts.force;
782
788
  if (createdConfig) {
783
- fs2.writeFileSync(configFile, generateNrdocsYml(title, opts.requestedAccess));
789
+ fs2.writeFileSync(configFile, generateNrdocsYml(title, apiUrl, opts.requestedAccess));
784
790
  }
785
791
  const createdIndex = !fs2.existsSync(indexFile);
786
792
  if (createdIndex) {
@@ -803,13 +809,13 @@ async function handleInit(args2) {
803
809
  }
804
810
  console.log("");
805
811
  console.log("Next steps:");
806
- console.log(" 1. Commit and push to trigger the workflow");
807
- console.log(" 2. Ask your operator to approve the repo");
812
+ console.log(" 1. Add markdown files under docs/, then run: nrdocs nav generate");
813
+ console.log(" 2. Commit and push to trigger the workflow");
814
+ console.log(" 3. Ask your operator to approve the repo");
808
815
  }
809
816
 
810
817
  // src/commands/publish.ts
811
- import * as fs6 from "node:fs";
812
- import * as path7 from "node:path";
818
+ import * as fs7 from "node:fs";
813
819
 
814
820
  // src/renderer/index.ts
815
821
  import * as fs5 from "node:fs";
@@ -6016,45 +6022,80 @@ function extractTitle(markdownContent, filePath) {
6016
6022
  }
6017
6023
  function findMarkdownFiles(dir, relativeTo) {
6018
6024
  const results = [];
6025
+ if (!fs3.existsSync(dir)) return results;
6019
6026
  const entries = fs3.readdirSync(dir, { withFileTypes: true });
6020
6027
  for (const entry of entries) {
6021
6028
  const fullPath = path4.join(dir, entry.name);
6022
6029
  if (entry.isDirectory()) {
6023
6030
  results.push(...findMarkdownFiles(fullPath, relativeTo));
6024
6031
  } else if (entry.isFile() && entry.name.endsWith(".md")) {
6025
- results.push(path4.relative(relativeTo, fullPath));
6032
+ results.push(path4.relative(relativeTo, fullPath).replace(/\\/g, "/"));
6026
6033
  }
6027
6034
  }
6028
6035
  return results;
6029
6036
  }
6030
- function mdPathToHref(filePath) {
6031
- const withoutExt = filePath.replace(/\.md$/, "");
6032
- if (withoutExt === "index") {
6033
- return "";
6034
- }
6035
- return withoutExt.replace(/\\/g, "/") + "/";
6037
+ function sortNavPaths(files, indexPath = "index.md") {
6038
+ const normalizedIndex = indexPath.replace(/\\/g, "/");
6039
+ const sorted = [...files].sort((a, b) => a.localeCompare(b, void 0, { numeric: true }));
6040
+ const indexIdx = sorted.indexOf(normalizedIndex);
6041
+ if (indexIdx <= 0) return sorted;
6042
+ const without = sorted.filter((f) => f !== normalizedIndex);
6043
+ return [normalizedIndex, ...without];
6036
6044
  }
6037
- function generateAutoNav(docsDir) {
6038
- const files = findMarkdownFiles(docsDir, docsDir);
6039
- files.sort((a, b) => {
6040
- if (a === "index.md") return -1;
6041
- if (b === "index.md") return 1;
6042
- return a.localeCompare(b);
6045
+ function mdPathToHref(filePath) {
6046
+ const normalized = filePath.replace(/\\/g, "/");
6047
+ const withoutExt = normalized.replace(/\.md$/, "");
6048
+ if (withoutExt === "index" || withoutExt.endsWith("/index")) {
6049
+ const dir = withoutExt === "index" ? "" : withoutExt.slice(0, -"/index".length);
6050
+ return dir ? `${dir}/` : "";
6051
+ }
6052
+ return `${withoutExt}/`;
6053
+ }
6054
+ function discoverNavEntries(contentDir, options) {
6055
+ const indexPath = (options?.indexPath ?? "index.md").replace(/\\/g, "/");
6056
+ const files = findMarkdownFiles(contentDir, contentDir);
6057
+ const sorted = sortNavPaths(files, indexPath);
6058
+ return sorted.map((file) => {
6059
+ const fullPath = path4.join(contentDir, file);
6060
+ const content = fs3.readFileSync(fullPath, "utf-8");
6061
+ return {
6062
+ title: extractTitle(content, file),
6063
+ path: file
6064
+ };
6043
6065
  });
6066
+ }
6067
+ function navConfigToNavItems(entries, contentDir) {
6044
6068
  const items = [];
6045
- for (const file of files) {
6046
- const fullPath = path4.join(docsDir, file);
6047
- const content = fs3.readFileSync(fullPath, "utf-8");
6048
- const title = extractTitle(content, file);
6049
- const href = mdPathToHref(file);
6050
- items.push({
6051
- title,
6052
- path: file,
6053
- href
6054
- });
6069
+ const walk = (list2) => {
6070
+ for (const entry of list2) {
6071
+ const normalizedPath = entry.path.replace(/\\/g, "/");
6072
+ items.push({
6073
+ title: entry.title,
6074
+ path: normalizedPath,
6075
+ href: mdPathToHref(normalizedPath)
6076
+ });
6077
+ if (entry.children?.length) {
6078
+ walk(entry.children);
6079
+ }
6080
+ }
6081
+ };
6082
+ walk(entries);
6083
+ for (const item of items) {
6084
+ const full = path4.join(contentDir, item.path);
6085
+ if (!fs3.existsSync(full)) {
6086
+ throw new Error(`Nav path not found: ${item.path}`);
6087
+ }
6055
6088
  }
6056
6089
  return items;
6057
6090
  }
6091
+ function generateAutoNav(docsDir, indexPath = "index.md") {
6092
+ const entries = discoverNavEntries(docsDir, { indexPath });
6093
+ return entries.map((e) => ({
6094
+ title: e.title,
6095
+ path: e.path,
6096
+ href: mdPathToHref(e.path)
6097
+ }));
6098
+ }
6058
6099
 
6059
6100
  // src/renderer/links.ts
6060
6101
  function rewriteLinks(html, basePath, owner, repo) {
@@ -6266,10 +6307,16 @@ function collectFromDir(dir, rootDir, results) {
6266
6307
  }
6267
6308
 
6268
6309
  // src/renderer/index.ts
6310
+ function resolveNavItems(resolvedDocsDir, nav, indexPath) {
6311
+ if (nav && nav !== "auto" && Array.isArray(nav)) {
6312
+ return navConfigToNavItems(nav, resolvedDocsDir);
6313
+ }
6314
+ return generateAutoNav(resolvedDocsDir, indexPath);
6315
+ }
6269
6316
  async function renderSite(options) {
6270
- const { docsDir, siteTitle, baseUrl, owner, repo } = options;
6317
+ const { docsDir, siteTitle, baseUrl, owner, repo, nav, indexPath = "index.md" } = options;
6271
6318
  const resolvedDocsDir = path6.resolve(docsDir);
6272
- const navItems = generateAutoNav(resolvedDocsDir);
6319
+ const navItems = resolveNavItems(resolvedDocsDir, nav, indexPath);
6273
6320
  const siteBase = `/${owner}/${repo}/`;
6274
6321
  const renderedFiles = [];
6275
6322
  for (const navItem of navItems) {
@@ -6375,52 +6422,311 @@ function writeOctal(buf, value, offset, length) {
6375
6422
  writeString(buf, str, offset, length);
6376
6423
  }
6377
6424
  function gzipCompress(data) {
6378
- return new Promise((resolve7, reject) => {
6425
+ return new Promise((resolve8, reject) => {
6379
6426
  zlib.gzip(data, (err, result) => {
6380
6427
  if (err) reject(err);
6381
- else resolve7(result);
6428
+ else resolve8(result);
6382
6429
  });
6383
6430
  });
6384
6431
  }
6385
6432
 
6433
+ // src/errors.ts
6434
+ function normalizeApiBaseUrl(url) {
6435
+ let normalized = url.trim().replace(/\/+$/, "");
6436
+ let strippedApiSuffix = false;
6437
+ if (normalized.endsWith("/api")) {
6438
+ normalized = normalized.slice(0, -4);
6439
+ strippedApiSuffix = true;
6440
+ }
6441
+ return { url: normalized, strippedApiSuffix };
6442
+ }
6443
+ function buildApiUrl(baseUrl, apiPath) {
6444
+ const { url } = normalizeApiBaseUrl(baseUrl);
6445
+ const path11 = apiPath.startsWith("/") ? apiPath : `/${apiPath}`;
6446
+ return `${url}${path11}`;
6447
+ }
6448
+ function extractFetchError(err) {
6449
+ if (!(err instanceof Error)) {
6450
+ return { message: String(err), kind: "unknown" };
6451
+ }
6452
+ const parts = [];
6453
+ let kind = "network_error";
6454
+ let current = err;
6455
+ for (let depth = 0; depth < 5 && current; depth++) {
6456
+ if (current instanceof Error) {
6457
+ if (current.message && !parts.includes(current.message)) {
6458
+ parts.push(current.message);
6459
+ }
6460
+ const c = current;
6461
+ if (c.code) {
6462
+ const code2 = String(c.code);
6463
+ parts.push(code2);
6464
+ if (code2 === "ENOTFOUND" || code2 === "EAI_AGAIN") kind = "dns_failure";
6465
+ else if (code2 === "ECONNREFUSED" || code2 === "ECONNRESET") kind = "connection_refused";
6466
+ else if (code2 === "CERT_HAS_EXPIRED" || code2 === "UNABLE_TO_VERIFY_LEAF_SIGNATURE") kind = "tls_error";
6467
+ else if (code2 === "UND_ERR_CONNECT_TIMEOUT" || code2 === "ETIMEDOUT") kind = "timeout";
6468
+ }
6469
+ current = c.cause;
6470
+ } else if (typeof current === "object" && current !== null && "code" in current) {
6471
+ parts.push(String(current.code));
6472
+ break;
6473
+ } else {
6474
+ break;
6475
+ }
6476
+ }
6477
+ const cause = parts.length > 1 ? parts.slice(1).join(" \u2192 ") : parts[0];
6478
+ return {
6479
+ message: err.message,
6480
+ cause: cause !== err.message ? cause : void 0,
6481
+ kind
6482
+ };
6483
+ }
6484
+ function mapPublishExitCode(error) {
6485
+ const code2 = error.code.toUpperCase();
6486
+ if (code2 === "UNAUTHORIZED" || code2 === "OIDC_VERIFICATION_FAILED") return 13;
6487
+ if (code2 === "REPO_DISABLED") return 16;
6488
+ if (code2 === "INVALID_REQUEST" || code2 === "EXTRACTION_FAILED" || code2 === "INVALID_EXTENSION" || code2 === "PATH_TRAVERSAL" || code2 === "REJECTED_EXTENSION") {
6489
+ return 15;
6490
+ }
6491
+ if (code2 === "network_error" || code2 === "timeout" || error.status === 0) return 14;
6492
+ return 14;
6493
+ }
6494
+ function formatPublishFailure(error, context) {
6495
+ const exitCode = mapPublishExitCode(error);
6496
+ const publishUrl = context.apiBaseUrl ? buildApiUrl(context.apiBaseUrl, "/api/publish") : void 0;
6497
+ const details = [];
6498
+ if (publishUrl) details.push(`URL: ${publishUrl}`);
6499
+ if (error.status && error.status > 0) details.push(`HTTP: ${error.status}`);
6500
+ details.push(`Code: ${error.code}`);
6501
+ if (error.message) details.push(`Message: ${error.message}`);
6502
+ if (error.cause) details.push(`Cause: ${error.cause}`);
6503
+ if (context.fullName) details.push(`Repo: ${context.fullName}`);
6504
+ if (context.archiveSizeBytes !== void 0) {
6505
+ details.push(`Size: ${(context.archiveSizeBytes / 1024).toFixed(1)} KB`);
6506
+ }
6507
+ if (error.responseBody) {
6508
+ const snippet = error.responseBody.slice(0, 120).replace(/\s+/g, " ");
6509
+ details.push(`Body: ${snippet}${error.responseBody.length > 120 ? "\u2026" : ""}`);
6510
+ }
6511
+ let headline;
6512
+ const fixes = [];
6513
+ if (error.code === "network_error" || error.status === 0) {
6514
+ headline = "Could not reach nrdocs API";
6515
+ fixes.push(
6516
+ "Check api_url in docs/nrdocs.yml and NRDOCS_API_URL in .github/workflows/nrdocs.yml match your deployment."
6517
+ );
6518
+ if (context.apiBaseUrl) {
6519
+ fixes.push(`From a terminal: curl -fsS ${buildApiUrl(context.apiBaseUrl, "/api/status")}`);
6520
+ }
6521
+ fixes.push("Ask your operator to confirm the Worker is deployed (nrdocs deploy).");
6522
+ fixes.push(
6523
+ "If the API host is only reachable on a VPN or private network, GitHub Actions cannot publish to it."
6524
+ );
6525
+ } else if (error.code === "UNAUTHORIZED" || error.code === "OIDC_VERIFICATION_FAILED") {
6526
+ headline = "Publish authentication rejected";
6527
+ fixes.push("Ensure the workflow has permissions.id-token: write.");
6528
+ fixes.push("Re-run the workflow \u2014 OIDC tokens are short-lived.");
6529
+ fixes.push("Confirm this repo is publishing to the correct nrdocs instance URL.");
6530
+ } else if (error.code === "REPO_NOT_ALLOWED") {
6531
+ headline = "Publish rejected \u2014 repo not on allowlist";
6532
+ const owner = context.fullName?.split("/")[0];
6533
+ if (owner) {
6534
+ fixes.push(`Ask the operator: nrdocs rules add '${owner}/*' --access password`);
6535
+ } else {
6536
+ fixes.push("Ask the operator to add an auto-approval rule for this repo.");
6537
+ }
6538
+ fixes.push("Re-run the GitHub Action after the rule exists.");
6539
+ } else if (error.code === "REPO_DISABLED") {
6540
+ headline = "Publish rejected \u2014 repo disabled";
6541
+ fixes.push("Ask the operator to re-enable the repo or approve it again.");
6542
+ } else if (error.code === "timeout") {
6543
+ headline = "Upload timed out";
6544
+ fixes.push("Retry the workflow; if it persists, check Worker limits and network stability.");
6545
+ } else {
6546
+ headline = "Publish failed";
6547
+ fixes.push("Run nrdocs doctor (or nrdocs doctor --ci in GitHub Actions) to diagnose connectivity.");
6548
+ if (context.fullName) {
6549
+ fixes.push(`Check repo status: nrdocs status ${context.fullName}`);
6550
+ }
6551
+ }
6552
+ return { headline, details, fixes, exitCode };
6553
+ }
6554
+ function printFailure(formatted) {
6555
+ console.error(`
6556
+ Error: ${formatted.headline}
6557
+ `);
6558
+ for (const line of formatted.details) {
6559
+ console.error(` ${line}`);
6560
+ }
6561
+ if (formatted.fixes.length > 0) {
6562
+ console.error("\nWhat to try:");
6563
+ formatted.fixes.forEach((fix, i) => {
6564
+ console.error(` ${i + 1}. ${fix}`);
6565
+ });
6566
+ }
6567
+ console.error("");
6568
+ }
6569
+ async function probeApiStatus(baseUrl, timeoutMs = 15e3) {
6570
+ const url = buildApiUrl(baseUrl, "/api/status");
6571
+ const controller = new AbortController();
6572
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
6573
+ try {
6574
+ const res = await fetch(url, { signal: controller.signal });
6575
+ clearTimeout(timer);
6576
+ if (!res.ok) {
6577
+ return { ok: false, status: res.status, message: `HTTP ${res.status}` };
6578
+ }
6579
+ const contentType = res.headers.get("Content-Type") ?? "";
6580
+ if (!contentType.includes("json")) {
6581
+ const text2 = await res.text();
6582
+ return {
6583
+ ok: false,
6584
+ status: res.status,
6585
+ message: `Non-JSON response (${contentType || "unknown"}): ${text2.slice(0, 80)}`
6586
+ };
6587
+ }
6588
+ const json = await res.json();
6589
+ const version = json.data?.version;
6590
+ return {
6591
+ ok: true,
6592
+ status: res.status,
6593
+ message: version ? `OK (nrdocs ${version})` : "OK",
6594
+ version
6595
+ };
6596
+ } catch (e) {
6597
+ clearTimeout(timer);
6598
+ const extracted = extractFetchError(e);
6599
+ const label = extracted.kind === "timeout" ? "timed out" : extracted.cause ?? extracted.message;
6600
+ return { ok: false, status: 0, message: label };
6601
+ }
6602
+ }
6603
+ function parseApiUrlFromConfig(content) {
6604
+ const match2 = content.match(/api_url:\s*["']?([^"'\n]+)["']?/);
6605
+ return match2 ? match2[1].trim() : void 0;
6606
+ }
6607
+ function parseApiUrlFromWorkflow(content) {
6608
+ const match2 = content.match(/NRDOCS_API_URL:\s*(\S+)/);
6609
+ return match2 ? match2[1].trim() : void 0;
6610
+ }
6611
+
6386
6612
  // src/api-client.ts
6613
+ var DEFAULT_TIMEOUT_MS = 3e4;
6614
+ var PUBLISH_TIMEOUT_MS = 12e4;
6387
6615
  var ApiClient = class {
6388
6616
  baseUrl;
6389
6617
  token;
6390
6618
  constructor(baseUrl, token) {
6391
- this.baseUrl = baseUrl.replace(/\/+$/, "");
6619
+ const { url, strippedApiSuffix } = normalizeApiBaseUrl(baseUrl);
6620
+ this.baseUrl = url;
6621
+ if (strippedApiSuffix) {
6622
+ console.warn(
6623
+ "Warning: api_url should be the site base (e.g. https://docs.example.com), not \u2026/api \u2014 stripped trailing /api"
6624
+ );
6625
+ }
6392
6626
  this.token = token;
6393
6627
  }
6394
- headers() {
6628
+ getBaseUrl() {
6629
+ return this.baseUrl;
6630
+ }
6631
+ headers(contentType = "application/json") {
6395
6632
  return {
6396
6633
  Authorization: `Bearer ${this.token}`,
6397
- "Content-Type": "application/json"
6634
+ "Content-Type": contentType
6635
+ };
6636
+ }
6637
+ async parseJsonResponse(res, url) {
6638
+ const contentType = res.headers?.get?.("Content-Type") ?? "";
6639
+ let text2;
6640
+ try {
6641
+ text2 = await res.text();
6642
+ } catch {
6643
+ return {
6644
+ ok: false,
6645
+ error: {
6646
+ code: "invalid_response",
6647
+ message: "Could not read response body",
6648
+ status: res.status,
6649
+ url
6650
+ }
6651
+ };
6652
+ }
6653
+ if (contentType && !contentType.includes("json")) {
6654
+ return {
6655
+ ok: false,
6656
+ error: {
6657
+ code: "invalid_response",
6658
+ message: `Expected JSON, got ${contentType}`,
6659
+ status: res.status,
6660
+ url,
6661
+ responseBody: text2
6662
+ }
6663
+ };
6664
+ }
6665
+ let json;
6666
+ try {
6667
+ json = JSON.parse(text2);
6668
+ } catch {
6669
+ return {
6670
+ ok: false,
6671
+ error: {
6672
+ code: "invalid_response",
6673
+ message: "Response body is not valid JSON",
6674
+ status: res.status,
6675
+ url,
6676
+ responseBody: text2
6677
+ }
6678
+ };
6679
+ }
6680
+ if (json["ok"] === true) {
6681
+ return { ok: true, data: json["data"] };
6682
+ }
6683
+ const err = json["error"];
6684
+ return {
6685
+ ok: false,
6686
+ error: {
6687
+ code: err?.code ?? "unknown",
6688
+ message: err?.message ?? `HTTP ${res.status}`,
6689
+ status: res.status,
6690
+ url
6691
+ }
6398
6692
  };
6399
6693
  }
6400
- async request(method, path10, body) {
6401
- const url = `${this.baseUrl}${path10}`;
6694
+ async request(method, path11, body, timeoutMs = DEFAULT_TIMEOUT_MS) {
6695
+ const url = buildApiUrl(this.baseUrl, path11);
6696
+ const controller = new AbortController();
6697
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
6402
6698
  const init = {
6403
6699
  method,
6404
- headers: this.headers()
6700
+ headers: this.headers(),
6701
+ signal: controller.signal
6405
6702
  };
6406
6703
  if (body !== void 0) {
6407
6704
  init.body = JSON.stringify(body);
6408
6705
  }
6409
6706
  try {
6410
6707
  const res = await fetch(url, init);
6411
- const json = await res.json();
6412
- if (json["ok"] === true) {
6413
- return { ok: true, status: res.status, data: json["data"] };
6708
+ clearTimeout(timer);
6709
+ const parsed = await this.parseJsonResponse(res, url);
6710
+ if (parsed.ok) {
6711
+ return { ok: true, status: res.status, data: parsed.data, url };
6414
6712
  }
6415
- const err = json["error"];
6713
+ return { ok: false, status: res.status, error: parsed.error, url };
6714
+ } catch (e) {
6715
+ clearTimeout(timer);
6716
+ const extracted = extractFetchError(e);
6717
+ const code2 = extracted.kind === "timeout" ? "timeout" : "network_error";
6416
6718
  return {
6417
6719
  ok: false,
6418
- status: res.status,
6419
- error: err ?? { code: "unknown", message: `HTTP ${res.status}` }
6720
+ status: 0,
6721
+ url,
6722
+ error: {
6723
+ code: code2,
6724
+ message: extracted.message,
6725
+ cause: extracted.cause,
6726
+ status: 0,
6727
+ url
6728
+ }
6420
6729
  };
6421
- } catch (e) {
6422
- const msg = e instanceof Error ? e.message : String(e);
6423
- return { ok: false, status: 0, error: { code: "network_error", message: msg } };
6424
6730
  }
6425
6731
  }
6426
6732
  async listRepos(filters) {
@@ -6473,31 +6779,145 @@ var ApiClient = class {
6473
6779
  async getOperatorMe() {
6474
6780
  return this.request("GET", "/api/operator/me");
6475
6781
  }
6476
- async publish(formData) {
6477
- const url = `${this.baseUrl}/api/publish`;
6782
+ async publish(formData, verbose = false) {
6783
+ const url = buildApiUrl(this.baseUrl, "/api/publish");
6784
+ const controller = new AbortController();
6785
+ const timer = setTimeout(() => controller.abort(), PUBLISH_TIMEOUT_MS);
6786
+ if (verbose) {
6787
+ console.log(` POST ${url}`);
6788
+ }
6478
6789
  try {
6479
6790
  const res = await fetch(url, {
6480
6791
  method: "POST",
6481
6792
  headers: { Authorization: `Bearer ${this.token}` },
6482
- body: formData
6793
+ body: formData,
6794
+ signal: controller.signal
6483
6795
  });
6484
- const json = await res.json();
6485
- if (json["ok"] === true) {
6486
- return { ok: true, status: res.status, data: json["data"] };
6796
+ clearTimeout(timer);
6797
+ if (verbose && res.status) {
6798
+ console.log(` HTTP ${res.status}`);
6487
6799
  }
6488
- const err = json["error"];
6800
+ const parsed = await this.parseJsonResponse(res, url);
6801
+ if (parsed.ok) {
6802
+ return { ok: true, status: res.status, data: parsed.data, url };
6803
+ }
6804
+ return { ok: false, status: res.status, error: parsed.error, url };
6805
+ } catch (e) {
6806
+ clearTimeout(timer);
6807
+ const extracted = extractFetchError(e);
6808
+ const code2 = extracted.kind === "timeout" ? "timeout" : "network_error";
6489
6809
  return {
6490
6810
  ok: false,
6491
- status: res.status,
6492
- error: err ?? { code: "unknown", message: `HTTP ${res.status}` }
6811
+ status: 0,
6812
+ url,
6813
+ error: {
6814
+ code: code2,
6815
+ message: extracted.message,
6816
+ cause: extracted.cause,
6817
+ status: 0,
6818
+ url
6819
+ }
6493
6820
  };
6494
- } catch (e) {
6495
- const msg = e instanceof Error ? e.message : String(e);
6496
- return { ok: false, status: 0, error: { code: "network_error", message: msg } };
6497
6821
  }
6498
6822
  }
6499
6823
  };
6500
6824
 
6825
+ // src/config/docs-config.ts
6826
+ import * as fs6 from "node:fs";
6827
+ import * as path7 from "node:path";
6828
+ import YAML from "yaml";
6829
+ function loadDocsConfig(docsDir) {
6830
+ const configPath = path7.resolve(docsDir, "nrdocs.yml");
6831
+ if (!fs6.existsSync(configPath)) {
6832
+ throw new Error(`Config file not found: ${configPath}`);
6833
+ }
6834
+ const raw = fs6.readFileSync(configPath, "utf-8");
6835
+ const config2 = YAML.parse(raw);
6836
+ if (!config2 || typeof config2 !== "object") {
6837
+ throw new Error(`Invalid config: ${configPath}`);
6838
+ }
6839
+ const sourceDir = config2.content?.source_dir ?? ".";
6840
+ const contentDir = path7.resolve(docsDir, sourceDir);
6841
+ return { config: config2, configPath, contentDir };
6842
+ }
6843
+ function hasExplicitNav(config2) {
6844
+ return Array.isArray(config2.content?.nav);
6845
+ }
6846
+ function parseNavEntries(nav) {
6847
+ if (!Array.isArray(nav)) {
6848
+ throw new Error('content.nav must be a list or "auto"');
6849
+ }
6850
+ const entries = [];
6851
+ for (const item of nav) {
6852
+ if (!item || typeof item !== "object") {
6853
+ throw new Error("Each nav entry must have title and path");
6854
+ }
6855
+ const rec = item;
6856
+ if (typeof rec["title"] !== "string" || typeof rec["path"] !== "string") {
6857
+ throw new Error("Each nav entry must have title and path strings");
6858
+ }
6859
+ const entry = {
6860
+ title: rec["title"],
6861
+ path: rec["path"].replace(/\\/g, "/")
6862
+ };
6863
+ if (Array.isArray(rec["children"])) {
6864
+ entry.children = parseNavEntries(rec["children"]);
6865
+ }
6866
+ entries.push(entry);
6867
+ }
6868
+ return entries;
6869
+ }
6870
+ function getExplicitNav(config2) {
6871
+ const nav = config2.content?.nav;
6872
+ if (nav === void 0 || nav === "auto") return null;
6873
+ if (Array.isArray(nav)) return parseNavEntries(nav);
6874
+ throw new Error('content.nav must be "auto" or a list of entries');
6875
+ }
6876
+ function validateNavPaths(entries, contentDir) {
6877
+ const errors = [];
6878
+ const seen = /* @__PURE__ */ new Set();
6879
+ const walk = (list2) => {
6880
+ for (const e of list2) {
6881
+ const p = e.path.replace(/\\/g, "/");
6882
+ if (seen.has(p)) {
6883
+ errors.push(`Duplicate nav path: ${p}`);
6884
+ }
6885
+ seen.add(p);
6886
+ const full = path7.join(contentDir, p);
6887
+ if (!fs6.existsSync(full)) {
6888
+ errors.push(`Nav path not found: ${p}`);
6889
+ }
6890
+ if (e.children?.length) walk(e.children);
6891
+ }
6892
+ };
6893
+ walk(entries);
6894
+ return { valid: errors.length === 0, errors };
6895
+ }
6896
+ function writeNavToConfig(configPath, navEntries) {
6897
+ let raw = fs6.readFileSync(configPath, "utf-8");
6898
+ raw = raw.replace(/^# content\.nav generated by: nrdocs nav generate\n/gm, "");
6899
+ const config2 = YAML.parse(raw);
6900
+ if (!config2 || typeof config2 !== "object") {
6901
+ throw new Error(`Invalid config: ${configPath}`);
6902
+ }
6903
+ if (!config2.content) {
6904
+ config2.content = {};
6905
+ }
6906
+ config2.content.nav = navEntries;
6907
+ const doc = new YAML.Document(config2);
6908
+ const header = "# content.nav generated by: nrdocs nav generate\n";
6909
+ const body = doc.toString();
6910
+ fs6.writeFileSync(configPath, header + body, "utf-8");
6911
+ }
6912
+ function formatNavYaml(navEntries) {
6913
+ const partial = {
6914
+ content: {
6915
+ nav: navEntries
6916
+ }
6917
+ };
6918
+ return YAML.stringify(partial).trimEnd();
6919
+ }
6920
+
6501
6921
  // src/commands/publish.ts
6502
6922
  function parsePublishArgs(args2) {
6503
6923
  const opts = {};
@@ -6505,15 +6925,17 @@ function parsePublishArgs(args2) {
6505
6925
  const arg = args2[i];
6506
6926
  if (arg === "--docs-dir" && i + 1 < args2.length) {
6507
6927
  opts.docsDir = args2[++i];
6928
+ } else if (arg === "--verbose" || arg === "-v") {
6929
+ opts.verbose = true;
6508
6930
  }
6509
6931
  }
6510
6932
  return opts;
6511
6933
  }
6512
6934
  function validateConfig2(configPath) {
6513
- if (!fs6.existsSync(configPath)) {
6935
+ if (!fs7.existsSync(configPath)) {
6514
6936
  return { valid: false, error: `Config file not found: ${configPath}` };
6515
6937
  }
6516
- const content = fs6.readFileSync(configPath, "utf-8");
6938
+ const content = fs7.readFileSync(configPath, "utf-8");
6517
6939
  if (!content.includes("site:")) {
6518
6940
  return { valid: false, error: 'Config file missing "site:" section' };
6519
6941
  }
@@ -6555,12 +6977,29 @@ async function getOIDCToken() {
6555
6977
  async function handlePublish(args2) {
6556
6978
  const opts = parsePublishArgs(args2);
6557
6979
  const docsDir = opts.docsDir || "docs";
6558
- const configPath = path7.resolve(docsDir, "nrdocs.yml");
6559
- const validation = validateConfig2(configPath);
6980
+ let docsConfig;
6981
+ try {
6982
+ docsConfig = loadDocsConfig(docsDir);
6983
+ } catch (e) {
6984
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
6985
+ process.exit(10);
6986
+ }
6987
+ const validation = validateConfig2(docsConfig.configPath);
6560
6988
  if (!validation.valid) {
6561
6989
  console.error(`Error: ${validation.error}`);
6562
6990
  process.exit(10);
6563
6991
  }
6992
+ const explicitNav = getExplicitNav(docsConfig.config);
6993
+ if (explicitNav) {
6994
+ const navCheck = validateNavPaths(explicitNav, docsConfig.contentDir);
6995
+ if (!navCheck.valid) {
6996
+ console.error("Error: Invalid content.nav in nrdocs.yml:");
6997
+ for (const err of navCheck.errors) {
6998
+ console.error(` - ${err}`);
6999
+ }
7000
+ process.exit(10);
7001
+ }
7002
+ }
6564
7003
  const ci = detectCI();
6565
7004
  if (!ci.inCI) {
6566
7005
  console.error("Error: nrdocs publish must run inside GitHub Actions.");
@@ -6585,22 +7024,31 @@ async function handlePublish(args2) {
6585
7024
  const { owner, repo } = repoInfo;
6586
7025
  const ownerLower = owner.toLowerCase();
6587
7026
  const repoLower = repo.toLowerCase();
7027
+ const fullName = `${ownerLower}/${repoLower}`;
6588
7028
  const siteTitle = validation.title || "Documentation";
6589
- const apiUrl = validation.apiUrl || process.env["NRDOCS_API_URL"] || "";
6590
- if (!apiUrl) {
7029
+ const rawApiUrl = validation.apiUrl || process.env["NRDOCS_API_URL"] || "";
7030
+ if (!rawApiUrl) {
6591
7031
  console.error("Error: No API URL configured. Set api_url in nrdocs.yml or NRDOCS_API_URL env var.");
6592
7032
  process.exit(10);
6593
7033
  }
6594
- console.log(`Publishing docs for ${ownerLower}/${repoLower}...`);
7034
+ const { url: apiUrl } = normalizeApiBaseUrl(rawApiUrl);
7035
+ console.log(`Publishing docs for ${fullName}...`);
6595
7036
  console.log(`Docs directory: ${docsDir}`);
6596
7037
  console.log(`Site title: ${siteTitle}`);
7038
+ if (opts.verbose) {
7039
+ console.log(`API base: ${apiUrl}`);
7040
+ }
6597
7041
  console.log("Rendering Markdown...");
7042
+ const indexPath = docsConfig.config.content?.index ?? "index.md";
7043
+ const navOption = explicitNav ?? "auto";
6598
7044
  const site = await renderSite({
6599
- docsDir,
7045
+ docsDir: docsConfig.contentDir,
6600
7046
  siteTitle,
6601
7047
  baseUrl: apiUrl,
6602
7048
  owner: ownerLower,
6603
- repo: repoLower
7049
+ repo: repoLower,
7050
+ nav: navOption,
7051
+ indexPath
6604
7052
  });
6605
7053
  console.log(`Rendered ${site.files.length} files.`);
6606
7054
  console.log("Creating archive...");
@@ -6622,106 +7070,346 @@ async function handlePublish(args2) {
6622
7070
  artifact: { format: "tar.gz", size_bytes: archive.length },
6623
7071
  nrdocs: { cli_version: "0.1.1" }
6624
7072
  }));
6625
- const result = await client.publish(formData);
7073
+ const result = await client.publish(formData, opts.verbose);
6626
7074
  if (result.ok) {
7075
+ const data = result.data;
7076
+ const approval = data?.["approval"];
7077
+ const serving = data?.["serving"];
7078
+ const access = data?.["access"];
6627
7079
  console.log("Published successfully!");
6628
- console.log(`View at: ${apiUrl}/${ownerLower}/${repoLower}/`);
6629
- } else {
6630
- console.error(`Error: Upload failed \u2014 ${result.error?.message || "unknown error"}`);
6631
- process.exit(14);
7080
+ if (approval?.state) {
7081
+ console.log(`Approval: ${approval.state}`);
7082
+ }
7083
+ if (access?.mode) {
7084
+ console.log(`Access: ${access.mode}`);
7085
+ }
7086
+ if (serving) {
7087
+ if (serving.visible) {
7088
+ console.log(`Serving: live (${serving.reason ?? "serving"})`);
7089
+ } else {
7090
+ console.log(`Serving: not visible (${serving.reason ?? "unknown"})`);
7091
+ if (serving.reason === "awaiting_operator_approval") {
7092
+ console.log("");
7093
+ console.log("An operator must approve this repo before docs are visible.");
7094
+ console.log(` nrdocs approve ${fullName} --access public`);
7095
+ } else if (serving.reason === "needs_password") {
7096
+ console.log("");
7097
+ console.log("Operator must set a password before password-protected docs are served.");
7098
+ console.log(` nrdocs password set ${fullName}`);
7099
+ }
7100
+ }
7101
+ }
7102
+ const viewUrl = serving?.url ?? `${apiUrl}/${fullName}/`;
7103
+ console.log(`View at: ${viewUrl}`);
7104
+ return;
6632
7105
  }
7106
+ const apiError = result.error ?? { code: "unknown", message: "unknown error" };
7107
+ const formatted = formatPublishFailure(apiError, {
7108
+ command: "publish",
7109
+ apiBaseUrl: apiUrl,
7110
+ fullName,
7111
+ archiveSizeBytes: archive.length
7112
+ });
7113
+ printFailure(formatted);
7114
+ process.exit(formatted.exitCode);
6633
7115
  }
6634
7116
 
6635
7117
  // src/commands/doctor.ts
6636
- import * as fs7 from "node:fs";
7118
+ import * as fs8 from "node:fs";
6637
7119
  import * as path8 from "node:path";
6638
- async function handleDoctor(_args) {
7120
+ function parseDoctorArgs(args2) {
7121
+ const opts = {};
7122
+ for (const arg of args2) {
7123
+ if (arg === "--json") opts.json = true;
7124
+ if (arg === "--ci") opts.ci = true;
7125
+ }
7126
+ return opts;
7127
+ }
7128
+ function countMarkdownFiles(docsDir) {
7129
+ if (!fs8.existsSync(docsDir)) return 0;
7130
+ let count = 0;
7131
+ const walk = (dir) => {
7132
+ for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
7133
+ const full = path8.join(dir, entry.name);
7134
+ if (entry.isDirectory()) walk(full);
7135
+ else if (entry.name.endsWith(".md")) count++;
7136
+ }
7137
+ };
7138
+ walk(docsDir);
7139
+ return count;
7140
+ }
7141
+ async function handleDoctor(args2) {
7142
+ const opts = parseDoctorArgs(args2);
7143
+ const inCI = opts.ci || process.env["GITHUB_ACTIONS"] === "true";
6639
7144
  const checks = [];
6640
- const isGitRepo = fs7.existsSync(path8.resolve(".git"));
7145
+ const isGitRepo = fs8.existsSync(path8.resolve(".git"));
6641
7146
  checks.push({
7147
+ section: "Repo setup",
6642
7148
  name: "Git repository",
6643
7149
  status: isGitRepo ? "ok" : "fail",
6644
7150
  message: isGitRepo ? "Found .git directory" : "Not a git repository"
6645
7151
  });
6646
7152
  const configPath = path8.resolve("docs", "nrdocs.yml");
6647
- const hasConfig = fs7.existsSync(configPath);
7153
+ const hasConfig = fs8.existsSync(configPath);
6648
7154
  checks.push({
7155
+ section: "Repo setup",
6649
7156
  name: "Docs config",
6650
7157
  status: hasConfig ? "ok" : "fail",
6651
7158
  message: hasConfig ? "Found docs/nrdocs.yml" : "Missing docs/nrdocs.yml \u2014 run: nrdocs init"
6652
7159
  });
7160
+ const docsDir = path8.resolve("docs");
7161
+ const mdCount = countMarkdownFiles(docsDir);
7162
+ checks.push({
7163
+ section: "Repo setup",
7164
+ name: "Docs sources",
7165
+ status: mdCount > 0 ? "ok" : "warn",
7166
+ message: mdCount > 0 ? `${mdCount} markdown file(s) in docs/` : "No .md files in docs/ \u2014 publish will produce an empty site"
7167
+ });
6653
7168
  const workflowPath = path8.resolve(".github", "workflows", "nrdocs.yml");
6654
- const hasWorkflow = fs7.existsSync(workflowPath);
7169
+ const hasWorkflow = fs8.existsSync(workflowPath);
6655
7170
  checks.push({
7171
+ section: "Repo setup",
6656
7172
  name: "GitHub workflow",
6657
7173
  status: hasWorkflow ? "ok" : "warn",
6658
7174
  message: hasWorkflow ? "Found .github/workflows/nrdocs.yml" : "Missing .github/workflows/nrdocs.yml \u2014 run: nrdocs init"
6659
7175
  });
6660
- let apiUrl;
6661
- let token;
6662
- try {
6663
- const creds = resolveCredentials();
6664
- apiUrl = creds.api_url;
6665
- token = creds.operator_token;
7176
+ let configApiUrl;
7177
+ let workflowApiUrl;
7178
+ if (hasConfig) {
7179
+ const content = fs8.readFileSync(configPath, "utf-8");
7180
+ configApiUrl = parseApiUrlFromConfig(content);
6666
7181
  checks.push({
6667
- name: "API URL",
6668
- status: "ok",
6669
- message: `Configured: ${apiUrl}`
7182
+ section: "Publish URL",
7183
+ name: "docs/nrdocs.yml api_url",
7184
+ status: configApiUrl ? "ok" : "warn",
7185
+ message: configApiUrl ?? "Not set \u2014 CI uses NRDOCS_API_URL from workflow env only"
6670
7186
  });
6671
- } catch {
7187
+ }
7188
+ if (hasWorkflow) {
7189
+ const wfContent = fs8.readFileSync(workflowPath, "utf-8");
7190
+ workflowApiUrl = parseApiUrlFromWorkflow(wfContent);
6672
7191
  checks.push({
6673
- name: "API URL",
6674
- status: "warn",
6675
- message: "Not configured \u2014 run: nrdocs auth login"
7192
+ section: "Publish URL",
7193
+ name: "Workflow NRDOCS_API_URL",
7194
+ status: workflowApiUrl ? "ok" : "fail",
7195
+ message: workflowApiUrl ?? "Missing NRDOCS_API_URL in workflow",
7196
+ fixes: workflowApiUrl ? void 0 : ["Re-run nrdocs init --api-url https://your-docs-url.com"]
6676
7197
  });
6677
7198
  }
6678
- if (apiUrl && token) {
6679
- try {
6680
- const client = new ApiClient(apiUrl, token);
6681
- const res = await client.getOperatorMe();
6682
- if (res.ok) {
6683
- checks.push({
6684
- name: "API connectivity",
6685
- status: "ok",
6686
- message: "Successfully connected to API"
6687
- });
6688
- } else {
7199
+ const publishBaseUrl = workflowApiUrl ?? configApiUrl ?? process.env["NRDOCS_API_URL"];
7200
+ if (publishBaseUrl && configApiUrl && workflowApiUrl) {
7201
+ const a = normalizeApiBaseUrl(configApiUrl).url;
7202
+ const b = normalizeApiBaseUrl(workflowApiUrl).url;
7203
+ checks.push({
7204
+ section: "Publish URL",
7205
+ name: "URL consistency",
7206
+ status: a === b ? "ok" : "warn",
7207
+ message: a === b ? "docs/nrdocs.yml and workflow env match" : `Mismatch: yml=${a}, workflow=${b}`,
7208
+ fixes: a === b ? void 0 : ["Use the same base URL in docs/nrdocs.yml and NRDOCS_API_URL in the workflow"]
7209
+ });
7210
+ }
7211
+ if (publishBaseUrl) {
7212
+ const { url } = normalizeApiBaseUrl(publishBaseUrl);
7213
+ const probe = await probeApiStatus(url);
7214
+ checks.push({
7215
+ section: "API reachability",
7216
+ name: "GET /api/status",
7217
+ status: probe.ok ? "ok" : "fail",
7218
+ message: probe.ok ? probe.message : probe.message,
7219
+ fixes: probe.ok ? void 0 : [
7220
+ `Verify the host is reachable: curl -fsS ${url}/api/status`,
7221
+ "Confirm the operator has deployed the Worker (nrdocs deploy).",
7222
+ "GitHub Actions uses this URL for publish \u2014 it must be public on the internet."
7223
+ ]
7224
+ });
7225
+ } else {
7226
+ checks.push({
7227
+ section: "Publish URL",
7228
+ name: "Publish API URL",
7229
+ status: "fail",
7230
+ message: "No publish URL configured",
7231
+ fixes: [
7232
+ "Run nrdocs init --api-url https://your-docs-url.com",
7233
+ "Or set NRDOCS_API_URL in .github/workflows/nrdocs.yml"
7234
+ ]
7235
+ });
7236
+ }
7237
+ if (inCI) {
7238
+ const hasOidcUrl = !!process.env["ACTIONS_ID_TOKEN_REQUEST_URL"];
7239
+ const hasOidcToken = !!process.env["ACTIONS_ID_TOKEN_REQUEST_TOKEN"];
7240
+ checks.push({
7241
+ section: "GitHub Actions",
7242
+ name: "OIDC request URL",
7243
+ status: hasOidcUrl ? "ok" : "fail",
7244
+ message: hasOidcUrl ? "Present" : "Missing ACTIONS_ID_TOKEN_REQUEST_URL",
7245
+ fixes: hasOidcUrl ? void 0 : ["Add permissions.id-token: write to the workflow"]
7246
+ });
7247
+ checks.push({
7248
+ section: "GitHub Actions",
7249
+ name: "OIDC request token",
7250
+ status: hasOidcToken ? "ok" : "fail",
7251
+ message: hasOidcToken ? "Present" : "Missing ACTIONS_ID_TOKEN_REQUEST_TOKEN",
7252
+ fixes: hasOidcToken ? void 0 : ["Add permissions.id-token: write to the workflow"]
7253
+ });
7254
+ const ghRepo = process.env["GITHUB_REPOSITORY"];
7255
+ checks.push({
7256
+ section: "GitHub Actions",
7257
+ name: "GITHUB_REPOSITORY",
7258
+ status: ghRepo ? "ok" : "fail",
7259
+ message: ghRepo ?? "Not set"
7260
+ });
7261
+ }
7262
+ let operatorApiUrl;
7263
+ let operatorToken;
7264
+ try {
7265
+ const creds = resolveCredentials();
7266
+ operatorApiUrl = creds.api_url;
7267
+ operatorToken = creds.operator_token;
7268
+ } catch {
7269
+ }
7270
+ if (operatorApiUrl && operatorToken) {
7271
+ const client = new ApiClient(operatorApiUrl, operatorToken);
7272
+ const res = await client.getOperatorMe();
7273
+ checks.push({
7274
+ section: "Operator auth",
7275
+ name: "Operator token",
7276
+ status: res.ok ? "ok" : "warn",
7277
+ message: res.ok ? "Accepted (GET /api/operator/me)" : `Rejected: ${res.error?.message ?? "unknown"}`
7278
+ });
7279
+ if (publishBaseUrl) {
7280
+ const opBase = normalizeApiBaseUrl(operatorApiUrl).url;
7281
+ const pubBase = normalizeApiBaseUrl(publishBaseUrl).url;
7282
+ if (opBase !== pubBase) {
6689
7283
  checks.push({
6690
- name: "API connectivity",
7284
+ section: "Operator auth",
7285
+ name: "Operator vs publish URL",
6691
7286
  status: "warn",
6692
- message: `API returned error: ${res.error?.message ?? "unknown"}`
7287
+ message: `Operator profile uses ${opBase}, publish uses ${pubBase}`,
7288
+ fixes: ["These can differ if intentional; publish uses workflow/config URL, not operator profile"]
6693
7289
  });
6694
7290
  }
6695
- } catch {
6696
- checks.push({
6697
- name: "API connectivity",
6698
- status: "fail",
6699
- message: "Could not connect to API"
6700
- });
6701
7291
  }
7292
+ } else if (!inCI) {
7293
+ checks.push({
7294
+ section: "Operator auth",
7295
+ name: "Operator token",
7296
+ status: "warn",
7297
+ message: "Not configured (optional for repo owners) \u2014 run: nrdocs auth login"
7298
+ });
7299
+ }
7300
+ if (opts.json) {
7301
+ console.log(JSON.stringify({ checks }, null, 2));
7302
+ const publishFailed = checks.some(
7303
+ (c) => (c.section === "API reachability" || c.section === "Publish URL") && c.status === "fail"
7304
+ );
7305
+ if (publishFailed) process.exitCode = 1;
7306
+ return;
6702
7307
  }
6703
7308
  console.log("nrdocs doctor\n");
6704
- let hasFailure = false;
7309
+ let currentSection = "";
7310
+ let publishPathFailed = false;
7311
+ let repoSetupFailed = false;
6705
7312
  for (const check of checks) {
7313
+ if (check.section !== currentSection) {
7314
+ currentSection = check.section;
7315
+ console.log(`${currentSection}`);
7316
+ }
6706
7317
  const icon = check.status === "ok" ? "\u2713" : check.status === "warn" ? "!" : "\u2717";
6707
- const prefix = check.status === "ok" ? " " : check.status === "warn" ? " " : " ";
6708
- console.log(`${prefix}${icon} ${check.name}: ${check.message}`);
6709
- if (check.status === "fail") hasFailure = true;
7318
+ console.log(` ${icon} ${check.name}: ${check.message}`);
7319
+ if (check.fixes?.length) {
7320
+ for (const fix of check.fixes) {
7321
+ console.log(` \u2192 ${fix}`);
7322
+ }
7323
+ }
7324
+ if (check.status === "fail") {
7325
+ if (check.section === "Repo setup") repoSetupFailed = true;
7326
+ if (check.section === "API reachability" || check.section === "Publish URL") {
7327
+ publishPathFailed = true;
7328
+ }
7329
+ if (check.section === "GitHub Actions") publishPathFailed = true;
7330
+ }
6710
7331
  }
6711
7332
  console.log("");
6712
- if (hasFailure) {
6713
- console.log("Some checks failed. Fix the issues above and run doctor again.");
7333
+ if (publishPathFailed) {
7334
+ console.log("Summary: Publish path checks FAILED \u2014 fix API reachability/URL before pushing.");
7335
+ process.exitCode = 1;
7336
+ } else if (repoSetupFailed) {
7337
+ console.log("Summary: Repo setup checks failed.");
6714
7338
  process.exitCode = 1;
6715
7339
  } else {
6716
- console.log("All checks passed!");
7340
+ console.log("Summary: All checks passed for this environment.");
7341
+ if (!operatorToken && !inCI) {
7342
+ console.log("(Operator auth not tested \u2014 optional for repo owners.)");
7343
+ }
7344
+ }
7345
+ }
7346
+
7347
+ // src/commands/nav.ts
7348
+ import * as fs9 from "node:fs";
7349
+ import * as path9 from "node:path";
7350
+ function parseNavGenerateArgs(args2) {
7351
+ const opts = {};
7352
+ for (let i = 0; i < args2.length; i++) {
7353
+ const arg = args2[i];
7354
+ if (arg === "--docs-dir" && i + 1 < args2.length) {
7355
+ opts.docsDir = args2[++i];
7356
+ } else if (arg === "--force") {
7357
+ opts.force = true;
7358
+ } else if (arg === "--dry-run") {
7359
+ opts.dryRun = true;
7360
+ } else if (arg === "--json") {
7361
+ opts.json = true;
7362
+ }
7363
+ }
7364
+ return opts;
7365
+ }
7366
+ async function handleNavGenerate(args2) {
7367
+ const opts = parseNavGenerateArgs(args2);
7368
+ const docsDir = path9.resolve(opts.docsDir ?? "docs");
7369
+ const configPath = path9.join(docsDir, "nrdocs.yml");
7370
+ if (!fs9.existsSync(configPath)) {
7371
+ console.error(`Error: Config file not found: ${configPath}`);
7372
+ console.error("Run: nrdocs init");
7373
+ process.exit(10);
7374
+ }
7375
+ let loaded;
7376
+ try {
7377
+ loaded = loadDocsConfig(docsDir);
7378
+ } catch (e) {
7379
+ console.error(`Error: ${e instanceof Error ? e.message : String(e)}`);
7380
+ process.exit(10);
7381
+ }
7382
+ if (hasExplicitNav(loaded.config) && !opts.force) {
7383
+ console.error("Error: content.nav is already an explicit list in nrdocs.yml.");
7384
+ console.error("Use --force to overwrite, or edit the file manually.");
7385
+ process.exit(2);
7386
+ }
7387
+ const indexPath = loaded.config.content?.index ?? "index.md";
7388
+ const entries = discoverNavEntries(loaded.contentDir, { indexPath });
7389
+ if (entries.length === 0) {
7390
+ console.error(`Error: No markdown files found in ${loaded.contentDir}`);
7391
+ process.exit(10);
7392
+ }
7393
+ if (opts.json) {
7394
+ console.log(JSON.stringify({ nav: entries, files: entries.length }, null, 2));
7395
+ if (opts.dryRun) return;
7396
+ } else if (opts.dryRun) {
7397
+ console.log("# Dry run \u2014 content.nav that would be written:\n");
7398
+ console.log(formatNavYaml(entries));
7399
+ return;
7400
+ }
7401
+ writeNavToConfig(loaded.configPath, entries);
7402
+ if (!opts.json) {
7403
+ console.log(`Generated navigation for ${entries.length} page(s) in ${path9.relative(process.cwd(), configPath)}`);
7404
+ console.log("Edit the file to reorder or rename entries, then run publish.");
6717
7405
  }
6718
7406
  }
6719
7407
 
6720
7408
  // src/commands/deploy.ts
6721
7409
  import * as readline3 from "node:readline";
6722
7410
  import * as crypto from "node:crypto";
6723
- import * as fs8 from "node:fs";
6724
- import * as path9 from "node:path";
7411
+ import * as fs10 from "node:fs";
7412
+ import * as path10 from "node:path";
6725
7413
  import { execSync } from "node:child_process";
6726
7414
  function parseDeployArgs(args2) {
6727
7415
  const opts = {};
@@ -6745,10 +7433,10 @@ function parseDeployArgs(args2) {
6745
7433
  async function prompt3(question, defaultValue) {
6746
7434
  const rl = readline3.createInterface({ input: process.stdin, output: process.stdout });
6747
7435
  const suffix = defaultValue ? ` [${defaultValue}]` : "";
6748
- return new Promise((resolve7) => {
7436
+ return new Promise((resolve8) => {
6749
7437
  rl.question(`${question}${suffix}: `, (answer) => {
6750
7438
  rl.close();
6751
- resolve7(answer.trim() || defaultValue || "");
7439
+ resolve8(answer.trim() || defaultValue || "");
6752
7440
  });
6753
7441
  });
6754
7442
  }
@@ -6790,14 +7478,14 @@ function normalizeUrl2(url) {
6790
7478
  }
6791
7479
  function findWorkerDir() {
6792
7480
  const candidates = [
6793
- path9.resolve("packages/worker"),
6794
- path9.resolve("../worker")
7481
+ path10.resolve("packages/worker"),
7482
+ path10.resolve("../worker")
6795
7483
  ];
6796
- const cliDir = process.argv[1] ? path9.dirname(process.argv[1]) : process.cwd();
6797
- candidates.push(path9.resolve(cliDir, "../../../worker"));
6798
- candidates.push(path9.resolve(cliDir, "../../../../packages/worker"));
7484
+ const cliDir = process.argv[1] ? path10.dirname(process.argv[1]) : process.cwd();
7485
+ candidates.push(path10.resolve(cliDir, "../../../worker"));
7486
+ candidates.push(path10.resolve(cliDir, "../../../../packages/worker"));
6799
7487
  for (const candidate of candidates) {
6800
- if (fs8.existsSync(path9.join(candidate, "src", "index.ts"))) {
7488
+ if (fs10.existsSync(path10.join(candidate, "src", "index.ts"))) {
6801
7489
  return candidate;
6802
7490
  }
6803
7491
  }
@@ -6940,12 +7628,12 @@ bucket_name = "${names.r2}"
6940
7628
  [vars]
6941
7629
  BASE_URL = "${baseUrl}"
6942
7630
  `;
6943
- const wranglerPath = path9.join(workerDir, "wrangler.toml");
6944
- fs8.writeFileSync(wranglerPath, wranglerToml);
7631
+ const wranglerPath = path10.join(workerDir, "wrangler.toml");
7632
+ fs10.writeFileSync(wranglerPath, wranglerToml);
6945
7633
  console.log("\u2705 wrangler.toml generated");
6946
7634
  console.log("Applying D1 migrations...");
6947
- const migrationsDir = path9.join(workerDir, "migrations");
6948
- if (fs8.existsSync(migrationsDir)) {
7635
+ const migrationsDir = path10.join(workerDir, "migrations");
7636
+ if (fs10.existsSync(migrationsDir)) {
6949
7637
  const migResult = runSilent(`npx wrangler d1 migrations apply ${names.d1} --remote --config "${wranglerPath}"`);
6950
7638
  if (migResult.ok) {
6951
7639
  console.log("\u2705 Migrations applied");
@@ -7249,36 +7937,42 @@ function parsePasswordSetArgs(args2) {
7249
7937
  return opts;
7250
7938
  }
7251
7939
  async function readFromStdin() {
7252
- return new Promise((resolve7, reject) => {
7940
+ return new Promise((resolve8, reject) => {
7253
7941
  let data = "";
7254
7942
  process.stdin.setEncoding("utf-8");
7255
7943
  process.stdin.on("data", (chunk) => {
7256
7944
  data += chunk;
7257
7945
  });
7258
- process.stdin.on("end", () => resolve7(data.trim()));
7946
+ process.stdin.on("end", () => resolve8(data.trim()));
7259
7947
  process.stdin.on("error", reject);
7260
7948
  });
7261
7949
  }
7262
7950
  async function promptPassword(question) {
7263
- return new Promise((resolve7) => {
7951
+ return new Promise((resolve8) => {
7264
7952
  const rl = readline4.createInterface({
7265
7953
  input: process.stdin,
7266
- output: process.stdout
7267
- });
7268
- if (process.stdin.isTTY) {
7269
- process.stdin.setRawMode?.(false);
7270
- }
7271
- rl.question(question, (answer) => {
7272
- rl.close();
7273
- console.log("");
7274
- resolve7(answer.trim());
7954
+ output: process.stdout,
7955
+ terminal: true
7275
7956
  });
7276
7957
  const origWrite = process.stdout.write.bind(process.stdout);
7277
- process.stdout.write = ((chunk) => {
7278
- if (typeof chunk === "string" && !chunk.includes(question) && chunk !== "\n") {
7958
+ let maskInput = false;
7959
+ process.stdout.write = ((chunk, encodingOrCb, cb) => {
7960
+ if (maskInput && typeof chunk === "string" && chunk !== "\n" && chunk !== "\r\n") {
7961
+ if (typeof encodingOrCb === "function") encodingOrCb();
7962
+ else if (typeof cb === "function") cb();
7279
7963
  return true;
7280
7964
  }
7281
- return origWrite(chunk);
7965
+ return origWrite(chunk, encodingOrCb, cb);
7966
+ });
7967
+ rl.question(question, (answer) => {
7968
+ maskInput = false;
7969
+ process.stdout.write = origWrite;
7970
+ rl.close();
7971
+ process.stdout.write("\n");
7972
+ resolve8(answer.trim());
7973
+ });
7974
+ setImmediate(() => {
7975
+ maskInput = true;
7282
7976
  });
7283
7977
  });
7284
7978
  }
@@ -7506,7 +8200,8 @@ async function handleStatus(args2) {
7506
8200
  console.error('Error: Repository must be in "owner/repo" format.');
7507
8201
  process.exit(2);
7508
8202
  }
7509
- const [owner, repo] = parts;
8203
+ const owner = parts[0].toLowerCase();
8204
+ const repo = parts[1].toLowerCase();
7510
8205
  let creds;
7511
8206
  try {
7512
8207
  creds = resolveCredentials();
@@ -7524,8 +8219,9 @@ async function handleStatus(args2) {
7524
8219
  console.log(JSON.stringify(res.data, null, 2));
7525
8220
  return;
7526
8221
  }
7527
- const data = res.data;
7528
- console.log(`Repository: ${owner}/${repo}`);
8222
+ const payload = res.data;
8223
+ const data = payload["repo"] ?? payload;
8224
+ console.log(`Repository: ${String(data["full_name"] ?? `${owner}/${repo}`)}`);
7529
8225
  console.log(`State: ${String(data["approval_state"] ?? "-")}`);
7530
8226
  console.log(`Access: ${String(data["access_mode"] ?? "-")}`);
7531
8227
  console.log(`Created: ${String(data["created_at"] ?? "-")}`);
@@ -7663,6 +8359,14 @@ async function runCommand(args2) {
7663
8359
  case "doctor":
7664
8360
  await handleDoctor(rest);
7665
8361
  break;
8362
+ case "nav":
8363
+ if (subCmd === "generate") {
8364
+ await handleNavGenerate(args2.slice(2));
8365
+ } else {
8366
+ console.error("Usage: nrdocs nav generate [--docs-dir docs] [--force] [--dry-run]");
8367
+ process.exitCode = 1;
8368
+ }
8369
+ break;
7666
8370
  case "deploy":
7667
8371
  await handleDeploy(rest);
7668
8372
  break;
@@ -7810,7 +8514,8 @@ Usage:
7810
8514
  Repo-owner commands:
7811
8515
  init Initialize nrdocs in a GitHub repository
7812
8516
  publish Build and upload docs artifacts
7813
- doctor Diagnose setup and connectivity
8517
+ doctor Diagnose setup and connectivity (--ci for Actions)
8518
+ nav Navigation helpers (generate)
7814
8519
 
7815
8520
  Operator commands:
7816
8521
  deploy Deploy or update nrdocs infrastructure
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nrdocs",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "CLI for nrdocs - serverless docs publishing for private GitHub repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -25,5 +25,8 @@
25
25
  "markdown-it": "^14.1.1",
26
26
  "typescript": "^5.5.0",
27
27
  "vitest": "^2.0.0"
28
+ },
29
+ "dependencies": {
30
+ "yaml": "^2.9.0"
28
31
  }
29
32
  }