#!/usr/bin/env bun // sama — verify any TypeScript/JavaScript repo against the four SAMA // disciplines (Sorted, Architecture, Modeled, Atomic). Mirrors the // /sama/verify web tool but runs locally with no network for `check`, // and uses the GitHub public API (no token) for `verify-repo`. // // Subcommands: // sama check [--json] [--src=] verify the current repo's src/ // sama verify-repo [--json] verify a public GitHub repo // sama --help usage // sama --version version // // Exit codes: 0 = all checks pass, 1 = at least one check failed, // 2 = error (bad args, network failure, etc.) import { readdirSync, readFileSync, existsSync, statSync } from "node:fs"; import { resolve, relative } from "node:path"; import { verifySama, type SamaReport } from "../src/b32_sama_verify.ts"; const VERSION = "0.1.0"; const RED = "\x1b[31m"; const GREEN = "\x1b[32m"; const YELLOW = "\x1b[33m"; const DIM = "\x1b[2m"; const BOLD = "\x1b[1m"; const RESET = "\x1b[0m"; const isTty = process.stdout.isTTY; const c = (color: string, s: string): string => (isTty ? `${color}${s}${RESET}` : s); const HELP = `sama ${VERSION} — verify any repo against the four SAMA disciplines. Usage: sama check [--json] [--src=] Verify the current repo's src/ directory. sama verify-repo [--json] Verify a public GitHub repo over HTTPS. sama --help, --version Disciplines checked: S — Sorted UI must not be a dependency of foundation/data/logic. A — Architecture Every cXX_ prefix must be in the known set. M — Modeled c32_*.ts logic files must have a sibling cXX_*.test.ts. A — Atomic ~700-line split rule, plus zero-expect() placeholder detection on test files. Exit codes: 0 pass · 1 fail · 2 error. See https://tdd.md/sama for the full discipline pages. `; interface ParsedArgs { command: "check" | "verify-repo" | "help" | "version" | null; json: boolean; src: string; repoArg?: string; } const parseArgs = (argv: string[]): ParsedArgs => { const out: ParsedArgs = { command: null, json: false, src: "src" }; let positional = 0; for (const a of argv) { if (a === "--help" || a === "-h") return { ...out, command: "help" }; if (a === "--version" || a === "-V") return { ...out, command: "version" }; if (a === "--json") out.json = true; else if (a.startsWith("--src=")) out.src = a.slice("--src=".length); else if (a.startsWith("--")) { console.error(`unknown flag: ${a}`); out.command = null; return out; } else if (positional === 0) { if (a === "check" || a === "verify-repo") out.command = a; else { console.error(`unknown command: ${a}`); return out; } positional++; } else if (positional === 1 && out.command === "verify-repo") { out.repoArg = a; positional++; } else { console.error(`unexpected argument: ${a}`); return out; } } return out; }; const walkSrc = (root: string, dir: string, out: string[]): void => { const entries = readdirSync(dir, { withFileTypes: true }); for (const e of entries) { const full = `${dir}/${e.name}`; if (e.isDirectory()) { // Skip nested build/output dirs that conventionally aren't source. if (["node_modules", "dist", "build", ".bun", ".next"].includes(e.name)) continue; walkSrc(root, full, out); } else if (e.isFile() && e.name.endsWith(".ts")) { out.push(relative(root, full)); } } }; const runCheck = async (args: ParsedArgs): Promise => { const srcDir = resolve(process.cwd(), args.src); if (!existsSync(srcDir) || !statSync(srcDir).isDirectory()) { console.error(c(RED, `✗ ${args.src}/ not found in ${process.cwd()}`)); return 2; } const tsFiles: string[] = []; walkSrc(srcDir, srcDir, tsFiles); const contents = new Map(); for (const f of tsFiles) { if (/(?:^|\/)c\d{2}_/.test(f)) { contents.set(f, readFileSync(`${srcDir}/${f}`, "utf8")); } } const report = verifySama({ repoOwner: "(local)", repoName: args.src, defaultBranch: "(working tree)", srcPaths: tsFiles, contents, }); return printReport(report, args.json); }; const runVerifyRepo = async (args: ParsedArgs): Promise => { if (!args.repoArg) { console.error(c(RED, "✗ verify-repo requires an owner/name argument")); return 2; } const m = /^([^\/\s]+)\/([^\/\s]+)$/.exec(args.repoArg); if (!m) { console.error(c(RED, `✗ couldn't parse '${args.repoArg}' — expected owner/name`)); return 2; } const [, owner, name] = m; const repoRes = await fetch(`https://api.github.com/repos/${encodeURIComponent(owner!)}/${encodeURIComponent(name!)}`, { headers: { Accept: "application/vnd.github+json", "User-Agent": "sama-cli" }, }); if (!repoRes.ok) { console.error(c(RED, `✗ GitHub repo lookup failed: HTTP ${repoRes.status}`)); if (repoRes.status === 404) console.error(c(DIM, " the repo is private, the name is wrong, or it doesn't exist")); if (repoRes.status === 403) console.error(c(DIM, " GitHub rate limit hit (60/h anonymous) — wait or set up a token")); return 2; } const repoMeta = (await repoRes.json()) as { default_branch?: string }; const defaultBranch = repoMeta.default_branch ?? "main"; const treeRes = await fetch( `https://api.github.com/repos/${encodeURIComponent(owner!)}/${encodeURIComponent(name!)}/git/trees/${encodeURIComponent(defaultBranch)}?recursive=1`, { headers: { Accept: "application/vnd.github+json", "User-Agent": "sama-cli" } }, ); if (!treeRes.ok) { console.error(c(RED, `✗ GitHub tree fetch failed: HTTP ${treeRes.status}`)); return 2; } const treeData = (await treeRes.json()) as { tree?: Array<{ path: string; type: string }>; }; const srcEntries = (treeData.tree ?? []) .filter((e) => e.type === "blob" && e.path.startsWith("src/") && e.path.endsWith(".ts")) .slice(0, 200); const srcPaths = srcEntries.map((e) => e.path.slice("src/".length)); const samaPaths = srcPaths.filter((p) => /^c\d{2}_/.test(p)); const contents = new Map(); await Promise.all( samaPaths.map(async (p) => { const url = `https://raw.githubusercontent.com/${encodeURIComponent(owner!)}/${encodeURIComponent(name!)}/${encodeURIComponent(defaultBranch)}/src/${p.split("/").map(encodeURIComponent).join("/")}`; const r = await fetch(url, { headers: { "User-Agent": "sama-cli" } }); if (r.ok) contents.set(p, await r.text()); }), ); const report = verifySama({ repoOwner: owner!, repoName: name!, defaultBranch, srcPaths, contents, }); return printReport(report, args.json); }; const printReport = (report: SamaReport, json: boolean): number => { if (json) { process.stdout.write(JSON.stringify(report, null, 2) + "\n"); return report.overallPassed ? 0 : 1; } const head = `${c(BOLD, "SAMA verify")} · ${report.repoSlug} · ${c(DIM, report.defaultBranch)}`; console.log(head); console.log(c(DIM, ` examined ${report.samaFiles} SAMA files / ${report.testFiles} tests / ${report.totalSrcFiles} src files`)); console.log(""); for (const ch of report.checks) { const status = ch.passed ? c(GREEN, "✓ pass") : c(RED, `✗ ${ch.violations.length} violation${ch.violations.length === 1 ? "" : "s"}`); console.log(` ${c(BOLD, ch.letter)} — ${ch.property}: ${status} ${c(DIM, `(${ch.examined} files)`)}`); for (const v of ch.violations.slice(0, 10)) { console.log(` ${c(DIM, "·")} ${v.file} — ${v.detail}`); } if (ch.violations.length > 10) { console.log(c(DIM, ` · ...and ${ch.violations.length - 10} more`)); } if (ch.note) console.log(c(YELLOW, ` ${ch.note}`)); } console.log(""); if (report.overallPassed) { console.log(c(GREEN, "✓ all four checks passed")); return 0; } else { const failed = report.checks.filter((ch) => !ch.passed).length; console.log(c(RED, `✗ ${failed} of 4 checks failed`)); return 1; } }; const args = parseArgs(process.argv.slice(2)); let exitCode = 0; try { if (args.command === "help" || args.command === null) { console.log(HELP); exitCode = args.command === "help" ? 0 : 2; } else if (args.command === "version") { console.log(`sama ${VERSION}`); } else if (args.command === "check") { exitCode = await runCheck(args); } else if (args.command === "verify-repo") { exitCode = await runVerifyRepo(args); } } catch (e) { console.error(c(RED, `✗ ${e instanceof Error ? e.message : String(e)}`)); exitCode = 2; } process.exit(exitCode);