Initial Commit

This commit is contained in:
2026-05-27 23:47:53 +01:00
parent fbc70f96e6
commit 9b28db724f
18103 changed files with 1600554 additions and 0 deletions

View File

@@ -0,0 +1,206 @@
#!/usr/bin/env bun
/**
* generate-root-readme — rebuild `README.md` at the repo root.
*
* The output has two regions:
*
* 1. **Header.** If `_intro.md` exists at the repo root, its contents
* are dropped in verbatim. Otherwise a small default header is
* generated.
* 2. **Auto-generated category index.** A markdown table whose rows are
* `(category, # packages, link to packages/<category>/README.md)`.
* Wrapped in `<!-- begin:auto-docs -->`…`<!-- end:auto-docs -->`
* markers so future rewrites are surgical.
*
* The per-category READMEs are owned by `generate-category-readmes.ts`;
* the per-package documentation trees are owned by
* `generate-package-module-tree.ts`. This script intentionally stays at
* the top of the doc spine — it does not duplicate package-level detail.
*
* Usage:
* bun scripts/generate-root-readme.ts # rewrite README.md
* bun scripts/generate-root-readme.ts --check # exit 1 if drift
*/
import { type Dirent, readdirSync, readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { CONFIG } from "./lib/config.js";
import {
AUTO_MARKER_BEGIN,
AUTO_MARKER_END,
isDirectorySafe,
tryRead,
} from "./lib/doc-tree.js";
const ROOT = CONFIG.paths.root;
const README_PATH = join(ROOT, "README.md");
const INTRO_PATH = join(ROOT, "_intro.md");
const PACKAGES_ROOT = join(ROOT, CONFIG.paths.packagesRoot);
const PKG_PREFIX = "@suaveplan/";
interface CategoryEntry {
name: string;
packageCount: number;
}
function countSuaveplanPackagesAt(dir: string): number {
const pkgJson = join(dir, "package.json");
const raw = tryRead(pkgJson);
if (!raw) return 0;
try {
const pkg = JSON.parse(raw) as { name?: string };
return pkg.name?.startsWith(PKG_PREFIX) ? 1 : 0;
} catch {
return 0;
}
}
function countPackagesInCategory(categoryDir: string): number {
let entries: Dirent[];
try {
entries = readdirSync(categoryDir, { withFileTypes: true });
} catch {
return 0;
}
let n = 0;
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith(".")) continue;
const pluginDir = join(categoryDir, entry.name);
// Canonical: packages/<category>/<plugin>/<pkg>/package.json (depth 4)
// Fallback: packages/<category>/<pkg>/package.json (legacy 2-level)
const direct = countSuaveplanPackagesAt(pluginDir);
if (direct > 0) {
n += direct;
continue;
}
let pkgEntries: Dirent[];
try {
pkgEntries = readdirSync(pluginDir, { withFileTypes: true });
} catch {
continue;
}
for (const pkg of pkgEntries) {
if (!pkg.isDirectory()) continue;
if (pkg.name.startsWith(".")) continue;
n += countSuaveplanPackagesAt(join(pluginDir, pkg.name));
}
}
return n;
}
function collectCategories(): CategoryEntry[] {
if (!isDirectorySafe(PACKAGES_ROOT)) return [];
let entries: Dirent[];
try {
entries = readdirSync(PACKAGES_ROOT, { withFileTypes: true });
} catch {
return [];
}
const out: CategoryEntry[] = [];
for (const entry of entries) {
if (!entry.isDirectory()) continue;
if (entry.name.startsWith(".")) continue;
const count = countPackagesInCategory(join(PACKAGES_ROOT, entry.name));
if (count === 0) continue;
out.push({ name: entry.name, packageCount: count });
}
out.sort((a, b) => a.name.localeCompare(b.name));
return out;
}
function renderAutoBlock(categories: readonly CategoryEntry[]): string {
const totalPackages = categories.reduce(
(acc, c) => acc + c.packageCount,
0
);
const rows = categories
.map((c) => {
const link = `[\`${c.name}\`](./packages/${c.name}/README.md)`;
return `| ${link} | ${c.packageCount} |`;
})
.join("\n");
const lines = [
"## Packages by category",
"",
`${totalPackages} \`@suaveplan/*\` packages across ${categories.length} categories. Each row links to the category's own README, which in turn lists every package and its first-paragraph description.`,
"",
"| Category | Packages |",
"|---|---:|",
rows,
];
return lines.join("\n");
}
function defaultHeader(): string {
return [
"# SuavePlan",
"",
"Enterprise-grade monorepo, rebuilt bottom-up on the `genesis` branch. Drop an `_intro.md` at the repo root to replace this default header.",
"",
"## Companion docs",
"",
"- [`STATUS.md`](./STATUS.md) — migration dashboard (✅ migrated / – pending / ❌ unwritten).",
"- [`SUMMARY.md`](./SUMMARY.md) — long-form per-package summaries.",
"- [`MANIFEST.md`](./MANIFEST.md) — dependency graph + tier overlay (auto-generated).",
"- [`MISSING.yml`](./MISSING.yml) — machine-readable list of unbuilt packages.",
"- [`CLAUDE.md`](./CLAUDE.md) — repo rules and AI-agent contract.",
"- [`openspec/`](./openspec/) — capability specs + change proposals + archived changes.",
].join("\n");
}
interface CliOptions {
check: boolean;
}
function parseArgs(argv: readonly string[]): CliOptions {
let check = false;
for (const arg of argv) {
if (arg === "--check") check = true;
else throw new Error(`Unknown flag: ${arg}`);
}
return { check };
}
function main(): void {
let options: CliOptions;
try {
options = parseArgs(process.argv.slice(2));
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
const intro = tryRead(INTRO_PATH);
const header = intro?.trim() ? intro.trim() : defaultHeader();
const categories = collectCategories();
const auto = renderAutoBlock(categories);
const next = `${header}\n\n${AUTO_MARKER_BEGIN}\n${auto}\n${AUTO_MARKER_END}\n`;
let current = "";
try {
current = readFileSync(README_PATH, "utf-8");
} catch {
current = "";
}
if (current === next) {
console.log(
`✅ README.md already up to date (${categories.length} categories).`
);
process.exit(0);
}
if (options.check) {
console.error(
"❌ README.md is stale — run without --check to rewrite."
);
process.exit(1);
}
writeFileSync(README_PATH, next);
console.log(`✏️ Wrote ${README_PATH} (${categories.length} categories).`);
}
main();