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.
- package/dist/bin.mjs +860 -155
- 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((
|
|
501
|
+
return new Promise((resolve8) => {
|
|
502
502
|
rl.question(question, (answer) => {
|
|
503
503
|
rl.close();
|
|
504
|
-
|
|
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((
|
|
611
|
+
return new Promise((resolve8) => {
|
|
612
612
|
rl.question(`${question}${suffix}: `, (answer) => {
|
|
613
613
|
rl.close();
|
|
614
|
-
|
|
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.
|
|
807
|
-
console.log(" 2.
|
|
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
|
|
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
|
|
6031
|
-
const
|
|
6032
|
-
|
|
6033
|
-
|
|
6034
|
-
|
|
6035
|
-
|
|
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
|
|
6038
|
-
const
|
|
6039
|
-
|
|
6040
|
-
|
|
6041
|
-
|
|
6042
|
-
return
|
|
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
|
-
|
|
6046
|
-
const
|
|
6047
|
-
|
|
6048
|
-
|
|
6049
|
-
|
|
6050
|
-
|
|
6051
|
-
|
|
6052
|
-
|
|
6053
|
-
|
|
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 =
|
|
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((
|
|
6425
|
+
return new Promise((resolve8, reject) => {
|
|
6379
6426
|
zlib.gzip(data, (err, result) => {
|
|
6380
6427
|
if (err) reject(err);
|
|
6381
|
-
else
|
|
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
|
-
|
|
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
|
-
|
|
6628
|
+
getBaseUrl() {
|
|
6629
|
+
return this.baseUrl;
|
|
6630
|
+
}
|
|
6631
|
+
headers(contentType = "application/json") {
|
|
6395
6632
|
return {
|
|
6396
6633
|
Authorization: `Bearer ${this.token}`,
|
|
6397
|
-
"Content-Type":
|
|
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,
|
|
6401
|
-
const url =
|
|
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
|
-
|
|
6412
|
-
|
|
6413
|
-
|
|
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
|
-
|
|
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:
|
|
6419
|
-
|
|
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 =
|
|
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
|
-
|
|
6485
|
-
if (
|
|
6486
|
-
|
|
6796
|
+
clearTimeout(timer);
|
|
6797
|
+
if (verbose && res.status) {
|
|
6798
|
+
console.log(` HTTP ${res.status}`);
|
|
6487
6799
|
}
|
|
6488
|
-
const
|
|
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:
|
|
6492
|
-
|
|
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 (!
|
|
6935
|
+
if (!fs7.existsSync(configPath)) {
|
|
6514
6936
|
return { valid: false, error: `Config file not found: ${configPath}` };
|
|
6515
6937
|
}
|
|
6516
|
-
const content =
|
|
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
|
-
|
|
6559
|
-
|
|
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
|
|
6590
|
-
if (!
|
|
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
|
-
|
|
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
|
-
|
|
6629
|
-
|
|
6630
|
-
|
|
6631
|
-
|
|
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
|
|
7118
|
+
import * as fs8 from "node:fs";
|
|
6637
7119
|
import * as path8 from "node:path";
|
|
6638
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
|
6661
|
-
let
|
|
6662
|
-
|
|
6663
|
-
const
|
|
6664
|
-
|
|
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
|
-
|
|
6668
|
-
|
|
6669
|
-
|
|
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
|
-
}
|
|
7187
|
+
}
|
|
7188
|
+
if (hasWorkflow) {
|
|
7189
|
+
const wfContent = fs8.readFileSync(workflowPath, "utf-8");
|
|
7190
|
+
workflowApiUrl = parseApiUrlFromWorkflow(wfContent);
|
|
6672
7191
|
checks.push({
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
|
|
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
|
-
|
|
6679
|
-
|
|
6680
|
-
|
|
6681
|
-
|
|
6682
|
-
|
|
6683
|
-
|
|
6684
|
-
|
|
6685
|
-
|
|
6686
|
-
|
|
6687
|
-
|
|
6688
|
-
|
|
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
|
-
|
|
7284
|
+
section: "Operator auth",
|
|
7285
|
+
name: "Operator vs publish URL",
|
|
6691
7286
|
status: "warn",
|
|
6692
|
-
message: `
|
|
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
|
|
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
|
-
|
|
6708
|
-
|
|
6709
|
-
|
|
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 (
|
|
6713
|
-
console.log("
|
|
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
|
|
6724
|
-
import * as
|
|
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((
|
|
7436
|
+
return new Promise((resolve8) => {
|
|
6749
7437
|
rl.question(`${question}${suffix}: `, (answer) => {
|
|
6750
7438
|
rl.close();
|
|
6751
|
-
|
|
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
|
-
|
|
6794
|
-
|
|
7481
|
+
path10.resolve("packages/worker"),
|
|
7482
|
+
path10.resolve("../worker")
|
|
6795
7483
|
];
|
|
6796
|
-
const cliDir = process.argv[1] ?
|
|
6797
|
-
candidates.push(
|
|
6798
|
-
candidates.push(
|
|
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 (
|
|
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 =
|
|
6944
|
-
|
|
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 =
|
|
6948
|
-
if (
|
|
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((
|
|
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", () =>
|
|
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((
|
|
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
|
-
|
|
7278
|
-
|
|
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
|
|
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
|
|
7528
|
-
|
|
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.
|
|
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
|
}
|