#!/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);