|
| 1 | +#!/usr/bin/env node |
| 2 | +import fs from "node:fs"; |
| 3 | +import path from "node:path"; |
| 4 | +import { fileURLToPath } from "node:url"; |
| 5 | + |
| 6 | +const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); |
| 7 | +const docsDir = path.join(repoRoot, "docs"); |
| 8 | +const cname = fs.readFileSync(path.join(docsDir, "CNAME"), "utf8").trim(); |
| 9 | +const origin = "https://" + cname; |
| 10 | +const productName = "CodexBar"; |
| 11 | +const productDescription = "CodexBar shows OpenAI Codex and Claude Code usage limits in the macOS menu bar."; |
| 12 | +const source = "https://github.com/steipete/CodexBar"; |
| 13 | + |
| 14 | +const pages = allHtml(docsDir) |
| 15 | + .map((file) => { |
| 16 | + const rel = path.relative(docsDir, file).replaceAll(path.sep, "/"); |
| 17 | + if (rel === "404.html" || rel === "social.html") return null; |
| 18 | + const html = fs.readFileSync(file, "utf8"); |
| 19 | + return { |
| 20 | + rel, |
| 21 | + title: textContent(html.match(/<title[^>]*>([\s\S]*?)<\/title>/i)?.[1]) || titleize(path.basename(rel, ".html")), |
| 22 | + description: attr(html.match(/<meta\s+name=["']description["']\s+content=["']([^"']*)["'][^>]*>/i)?.[1] || ""), |
| 23 | + }; |
| 24 | + }) |
| 25 | + .filter(Boolean) |
| 26 | + .sort((a, b) => (a.rel === "index.html" ? -1 : b.rel === "index.html" ? 1 : a.rel.localeCompare(b.rel))); |
| 27 | + |
| 28 | +const lines = [ |
| 29 | + "# " + productName, |
| 30 | + "", |
| 31 | + productDescription, |
| 32 | + "", |
| 33 | + "Canonical documentation:", |
| 34 | + ...pages.map((page) => "- " + page.title + ": " + pageUrl(page.rel) + (page.description ? " - " + page.description : "")), |
| 35 | + "", |
| 36 | + "Source: " + source, |
| 37 | + "", |
| 38 | + "Guidance for agents:", |
| 39 | + "- Prefer the canonical documentation URLs above over README excerpts or package metadata.", |
| 40 | + "- Fetch only the pages needed for the current task; this is an index, not a full-site corpus.", |
| 41 | + "", |
| 42 | +]; |
| 43 | + |
| 44 | +fs.writeFileSync(path.join(docsDir, "llms.txt"), lines.join("\n"), "utf8"); |
| 45 | +console.log("wrote " + path.relative(repoRoot, path.join(docsDir, "llms.txt"))); |
| 46 | + |
| 47 | +function allHtml(dir) { |
| 48 | + return fs.readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { |
| 49 | + const full = path.join(dir, entry.name); |
| 50 | + if (entry.name === "node_modules" || entry.name.startsWith(".")) return []; |
| 51 | + if (entry.isDirectory()) return allHtml(full); |
| 52 | + return entry.name.endsWith(".html") ? [full] : []; |
| 53 | + }); |
| 54 | +} |
| 55 | + |
| 56 | +function pageUrl(rel) { |
| 57 | + return rel === "index.html" ? origin + "/" : origin + "/" + rel; |
| 58 | +} |
| 59 | + |
| 60 | +function textContent(value) { |
| 61 | + return attr(value || "").replace(/<[^>]+>/g, "").replace(/\s+/g, " ").trim(); |
| 62 | +} |
| 63 | + |
| 64 | +function attr(value) { |
| 65 | + return String(value || "") |
| 66 | + .replace(/—/g, "-") |
| 67 | + .replace(/&/g, "&") |
| 68 | + .replace(/ /g, " ") |
| 69 | + .replace(/'/g, "'") |
| 70 | + .replace(/"/g, '"') |
| 71 | + .trim(); |
| 72 | +} |
| 73 | + |
| 74 | +function titleize(input) { |
| 75 | + return input.replaceAll("-", " ").replace(/\b\w/g, (m) => m.toUpperCase()); |
| 76 | +} |
0 commit comments