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

370
scripts/mark-migrated.ts Normal file
View File

@@ -0,0 +1,370 @@
#!/usr/bin/env bun
/**
* mark-migrated — flip STATUS.md rows from `–` / `❌` to `✅` for the given
* package names. Optionally adds new ✅ rows for genesis-original packages
* that have no existing STATUS.md entry.
*
* Usage:
* bun scripts/mark-migrated.ts cron job-scheduler
* bun scripts/mark-migrated.ts @suaveplan/retry @suaveplan/types
* bun scripts/mark-migrated.ts --add cron
* bun scripts/mark-migrated.ts --add --section primitives cron
*
* Args may include or omit the `@suaveplan/` prefix.
*
* Flags:
* --add Create ✅ rows for packages not already in STATUS.md.
* The description is read from each package's
* package.json; the section defaults to the on-disk
* plugin folder (`packages/<category>/<plugin>/<pkg>/`)
* unless overridden by `--section`.
* --section <name> Explicit section under which `--add` inserts new
* rows. Must match an existing `### \`<name>\`` heading
* in STATUS.md.
*
* Recomputes the count table at the top of STATUS.md after substitutions.
*
* Exit codes:
* 0 — every supplied package was already ✅, flipped to ✅, or added.
* 1 — at least one supplied package could not be flipped or added.
* 2 — STATUS.md is malformed (missing counts table or required section).
*/
import { readFileSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { CONFIG } from "./lib/config.js";
import { discoverPackageDirs } from "./lib/discover-packages.js";
const STATUS_PATH = join(CONFIG.paths.root, "STATUS.md");
const PKG_PREFIX = "@suaveplan/";
interface MarkResult {
flipped: string[];
alreadyMigrated: string[];
added: string[];
notFound: string[];
}
interface PackageMeta {
name: string; // short name without @suaveplan/
description: string;
plugin: string; // e.g. "primitives", "control-flow"
}
function normalize(name: string): string {
const trimmed = name.trim();
return trimmed.startsWith(PKG_PREFIX)
? trimmed.slice(PKG_PREFIX.length)
: trimmed;
}
/**
* Parse a STATUS.md row, returning `{ name, status }` if it's a package row,
* `null` otherwise. Rows look like:
* | `@suaveplan/<name>` | <status> | <description> |
*/
function parseRow(line: string): { name: string; status: string } | null {
const match = line.match(
/^\|\s*`@suaveplan\/([^`]+)`\s*\|\s*(✅|–|❌)\s*\|/u
);
if (!match) return null;
return { name: match[1] ?? "", status: match[2] ?? "" };
}
function flipRow(line: string): string {
return line.replace(/^(\|\s*`@suaveplan\/[^`]+`\s*\|\s*)(–|❌)/u, "$1✅");
}
function recomputeCounts(lines: string[]): string[] {
let migrated = 0;
let pending = 0;
let unwritten = 0;
for (const line of lines) {
const parsed = parseRow(line);
if (!parsed) continue;
if (parsed.status === "✅") migrated++;
else if (parsed.status === "–") pending++;
else if (parsed.status === "❌") unwritten++;
}
const total = migrated + pending + unwritten;
const out = [...lines];
let countsTableSeen = false;
for (let i = 0; i < out.length; i++) {
const line = out[i] ?? "";
if (line.startsWith("## Counts")) {
countsTableSeen = true;
continue;
}
if (!countsTableSeen) continue;
if (line.startsWith("| ✅ Migrated |")) {
out[i] = `| ✅ Migrated | ${migrated} |`;
} else if (line.startsWith("| – Yet to be migrated |")) {
out[i] = `| – Yet to be migrated | ${pending} |`;
} else if (line.startsWith("| ❌ Hasn't been written yet |")) {
out[i] = `| ❌ Hasn't been written yet | ${unwritten} |`;
} else if (line.startsWith("| **Total tracked** |")) {
out[i] = `| **Total tracked** | **${total}** |`;
break;
}
}
if (!countsTableSeen) {
throw new Error("STATUS.md is missing the '## Counts' section");
}
return out;
}
async function loadPackageMetaIndex(): Promise<Map<string, PackageMeta>> {
const pkgDirs = await discoverPackageDirs();
const index = new Map<string, PackageMeta>();
for (const dir of pkgDirs) {
const pkgJsonPath = join(dir, "package.json");
let raw: string;
try {
raw = readFileSync(pkgJsonPath, "utf-8");
} catch {
continue;
}
let pkg: { name?: string; description?: string };
try {
pkg = JSON.parse(raw) as { name?: string; description?: string };
} catch {
continue;
}
if (!pkg.name?.startsWith(PKG_PREFIX)) continue;
const shortName = pkg.name.slice(PKG_PREFIX.length);
// Plugin folder is the parent of <pkg> in the canonical 3-level
// layout: `packages/<category>/<plugin>/<pkg>/`.
const segments = dir.split(/[\\/]/);
const plugin = segments[segments.length - 2] ?? "";
index.set(shortName, {
name: shortName,
description: pkg.description ?? "",
plugin,
});
}
return index;
}
function findSectionInsertIndex(
lines: readonly string[],
sectionName: string
): number {
const sectionHeader = `### \`${sectionName}\``;
let start = -1;
for (let i = 0; i < lines.length; i++) {
if (lines[i] === sectionHeader) {
start = i;
break;
}
}
if (start === -1) {
throw new Error(
`STATUS.md has no section "### \`${sectionName}\`" — pass --section <name> to target an existing section`
);
}
// Insert at the last contiguous table row in this section (before the
// next heading or blank gap leading to a heading).
let insertAt = start;
for (let i = start + 1; i < lines.length; i++) {
const line = lines[i] ?? "";
if (line.startsWith("### ") || line.startsWith("## ")) break;
if (line.startsWith("| `@suaveplan/")) insertAt = i;
}
return insertAt + 1;
}
function buildRow(meta: PackageMeta): string {
const desc = meta.description.replace(/\s+/g, " ").trim();
return `| \`@suaveplan/${meta.name}\` | ✅ | ${desc} |`;
}
interface MarkOptions {
add: boolean;
section: string | undefined;
}
function markMigrated(
statusMd: string,
targets: ReadonlySet<string>,
options: MarkOptions,
metaIndex: ReadonlyMap<string, PackageMeta>
): { content: string; result: MarkResult } {
const lines = statusMd.split("\n");
const flipped: string[] = [];
const alreadyMigrated: string[] = [];
const added: string[] = [];
const seen = new Set<string>();
for (let i = 0; i < lines.length; i++) {
const parsed = parseRow(lines[i] ?? "");
if (!parsed) continue;
if (!targets.has(parsed.name)) continue;
seen.add(parsed.name);
if (parsed.status === "✅") {
alreadyMigrated.push(parsed.name);
} else {
lines[i] = flipRow(lines[i] ?? "");
flipped.push(parsed.name);
}
}
const stillMissing = [...targets].filter((name) => !seen.has(name));
const notFound: string[] = [];
if (options.add) {
// Process from last-to-first so insertion indexes stay valid as we mutate.
// Group by target section to insert all rows of one section together.
const grouped = new Map<string, PackageMeta[]>();
for (const name of stillMissing) {
const meta = metaIndex.get(name);
if (!meta) {
notFound.push(name);
continue;
}
const section = options.section ?? meta.plugin;
if (!section) {
notFound.push(name);
continue;
}
const bucket = grouped.get(section) ?? [];
bucket.push(meta);
grouped.set(section, bucket);
}
for (const [section, metas] of grouped) {
let insertAt: number;
try {
insertAt = findSectionInsertIndex(lines, section);
} catch (err) {
for (const meta of metas) {
notFound.push(meta.name);
console.error(
err instanceof Error ? err.message : String(err)
);
}
continue;
}
// Insert all rows for this section at the same point in order.
const rows = metas.map(buildRow);
lines.splice(insertAt, 0, ...rows);
for (const meta of metas) added.push(meta.name);
}
} else {
notFound.push(...stillMissing);
}
const recomputed = recomputeCounts(lines);
return {
content: recomputed.join("\n"),
result: { flipped, alreadyMigrated, added, notFound },
};
}
function printSummary(result: MarkResult): void {
if (result.flipped.length > 0) {
console.log(`✅ Flipped to migrated (${result.flipped.length}):`);
for (const name of result.flipped)
console.log(` - @suaveplan/${name}`);
}
if (result.added.length > 0) {
console.log(`➕ Added new ✅ rows (${result.added.length}):`);
for (const name of result.added) console.log(` - @suaveplan/${name}`);
}
if (result.alreadyMigrated.length > 0) {
console.log(`⏭️ Already migrated (${result.alreadyMigrated.length}):`);
for (const name of result.alreadyMigrated)
console.log(` - @suaveplan/${name}`);
}
if (result.notFound.length > 0) {
console.log(`❌ Not found in STATUS.md (${result.notFound.length}):`);
for (const name of result.notFound)
console.log(` - @suaveplan/${name}`);
if (result.added.length === 0) {
console.log(
" (pass --add to create new ✅ rows for genesis-original packages)"
);
}
}
}
interface ParsedArgs {
targets: Set<string>;
add: boolean;
section: string | undefined;
}
function parseArgs(argv: readonly string[]): ParsedArgs {
const targets = new Set<string>();
let add = false;
let section: string | undefined;
for (let i = 0; i < argv.length; i++) {
const arg = argv[i] ?? "";
if (arg === "--add") {
add = true;
} else if (arg === "--section") {
const next = argv[i + 1];
if (!next || next.startsWith("--")) {
throw new Error("--section requires a value");
}
section = next;
i++;
} else if (arg.startsWith("--section=")) {
section = arg.slice("--section=".length);
} else if (arg.startsWith("--")) {
throw new Error(`Unknown flag: ${arg}`);
} else {
targets.add(normalize(arg));
}
}
return { targets, add, section };
}
async function main(): Promise<void> {
let parsed: ParsedArgs;
try {
parsed = parseArgs(process.argv.slice(2));
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(1);
}
if (parsed.targets.size === 0) {
console.error(
"Usage: bun scripts/mark-migrated.ts [--add] [--section <name>] <pkg> [<pkg> ...]"
);
console.error("Names may include or omit the `@suaveplan/` prefix.");
process.exit(1);
}
const statusMd = readFileSync(STATUS_PATH, "utf-8");
const metaIndex = parsed.add
? await loadPackageMetaIndex()
: new Map<string, PackageMeta>();
let next: { content: string; result: MarkResult };
try {
next = markMigrated(
statusMd,
parsed.targets,
{ add: parsed.add, section: parsed.section },
metaIndex
);
} catch (err) {
console.error(err instanceof Error ? err.message : String(err));
process.exit(2);
}
if (next.content !== statusMd) {
writeFileSync(STATUS_PATH, next.content);
console.log(`✏️ Wrote ${STATUS_PATH}`);
} else {
console.log("No changes to STATUS.md.");
}
printSummary(next.result);
process.exit(next.result.notFound.length === 0 ? 0 : 1);
}
void main();