status
uses node:fs
tcm status: where things stand. The branch; what's in the codebase, by namespace; what needs fixing; scratch
files holding work that isn't saved, and what saving them would do; other branches holding work main doesn't have
(an earlier session's, to land or go on with), and what this branch has that main doesn't; session directories
whose branch is gone that hold unsaved work; and whether a mirror's files are in step.
build is which tcm this is: the hash of its code, which a new build changes.
import { sessionDirFor, sessionDirsGone, unsavedIn } from "#codebase/tcmts/cli/codebase"; import type { Codebase } from "#codebase/tcmts/cli/codebase"; import { classify, todo } from "#codebase/tcmts/cli/main"; import { hasWork } from "#codebase/tcmts/cli/merge"; import { loadMirror, onMirrorBranch } from "#codebase/tcmts/cli/mirror"; import { bold, dim, green, yellow } from "#codebase/tcmts/cli/style"; import { scratchShown, shellWord, shownPath } from "#codebase/tcmts/cli/ui"; import fs from "node:fs"; import path_1 from "node:path"; export function status(cb: Codebase, build?: string) { // another branch, only read (`--branch`): its own scratch files, not this process's const reading = cb.store.reading; const session = reading === undefined && !!cb.sessionDir; console.log( `${bold("branch")} ${cb.store.branch().name}${reading !== undefined ? dim(" (read with --branch: nothing here changes it)") : session ? dim(" (a session: it lands on main when the user says to)") : ""}`, ); if (build) console.log( `${bold("tcm")} ${dim(`build #${build.slice(0, 8)}, run from ${process.argv[1] ?? "?"}`)}`, ); const names = cb.store.names(undefined, true); // by namespace, as deep as fits in a screenful let counts = new Map<string, number>(); for (let depth = 3; depth > 0; depth--) { counts = new Map(); for (const n of names) { const segs = n.path.split("."); const ns = segs.slice(0, Math.min(segs.length - 1, depth)).join(".") || "(root)"; counts.set(ns, (counts.get(ns) ?? 0) + 1); } if (counts.size <= 20) break; } console.log( `\n${bold(`${names.length} named definition${names.length === 1 ? "" : "s"}`)}${counts.size > 1 ? dim(", by namespace") : ""}`, ); const width = Math.max(0, ...[...counts.keys()].map((k) => k.length)); for (const [ns, n] of [...counts].sort()) console.log(` ${ns.padEnd(width)} ${dim(String(n))}`); // scratch files with what isn't saved yet (another branch's: its session's, by name, as checking them is its to do) const theirs = reading !== undefined ? sessionDirFor(cb.lay.root, reading) : undefined; const dir = theirs ?? cb.sessionDir ?? cb.lay.root; const scratch = theirs ? [] : fs .readdirSync(dir) .filter((f) => /\.tsx?$/.test(f) && !f.endsWith(".d.ts")) .map((f) => path_1.join(dir, f)); const work: string[] = theirs ? unsavedIn(theirs).map((f) => ` ${shownPath(f)} ${dim("(its session's, unsaved)")}`) : []; for (const file of scratch) { if (!fs.readFileSync(file, "utf8").trim()) continue; const a = cb.analyze(file, cb.scratchSalts()); const rel = shownPath(file); if (a.errors.length || a.diagnostics.length) { work.push( ` ${rel} ${yellow(`doesn't check (${a.errors.length + a.diagnostics.length} problem${a.errors.length + a.diagnostics.length === 1 ? "" : "s"}): \`tcm check ${shellWord(rel)}\``)}`, ); continue; } const tally = new Map<string, number>(); for (const r of classify(cb, a.defs, a.namespace ?? "")) if (r.status !== "identical") tally.set(r.status, (tally.get(r.status) ?? 0) + 1); if (tally.size) work.push( ` ${rel} ${[...tally].map(([s, n]) => `${n} ${s}`).join(", ")}${dim(`: \`tcm ${tally.has("update") || tally.has("conflict") ? "update" : "add"} ${shellWord(rel)}\` saves ${[...tally.values()].reduce((x, y) => x + y, 0) > 1 ? "them" : "it"}`)}`, ); } // files kept as written, as `tcm edit` writes them into a session's directory (a page, a `Dockerfile`): saved with // `--into` their namespace, unless it's a page saying where it goes if (!theirs && dir !== cb.lay.root) for (const file of unsavedIn(dir).filter((f) => !scratch.includes(f))) { const rel = shownPath(file); const placed = /^<!--\s*tcm:\s*(path|namespace)\s/.test(fs.readFileSync(file, "utf8")); work.push( ` ${rel} ${dim(`kept as written: \`tcm update ${shellWord(scratchShown(file, cb.sessionDir))}${placed ? "" : " --into <namespace>"}\` saves it`)}`, ); } // (a session's are in its own directory: say which, as its name isn't the branch's) console.log( `\n${bold("scratch files")}${dir !== cb.lay.root ? dim(` in ${shownPath(dir)}/`) : ""}`, ); console.log(work.length ? work.join("\n") : dim(" nothing unsaved")); console.log(`\n${bold("todo")}`); todo(cb); // other branches holding work main doesn't have (an earlier session's, say): to land, or go on with const here = cb.store.branch().name; const mainHead = cb.store.hasBranch("main") ? cb.store.head("main") : undefined; const ahead = mainHead ? cb.store.branches().filter((b) => b !== "main" && b !== here && hasWork(cb, b, "main")) : []; if (ahead.length) { console.log(`\n${bold("branches with work main doesn't have")}`); for (const b of ahead) console.log(` ${b}`); console.log( dim( " (each lands when the user says to: `tcm merge <branch> --squash` on main, or an agent's tcm_merge with `branch`)", ), ); } // this branch's own: what landing it would bring, said plainly (a session's work is here, not among the others) if (mainHead && here !== "main") { const onMain = new Set(cb.store.ancestors(mainHead)); const entries = cb.store.ancestors(cb.store.head(here)).filter((c) => !onMain.has(c)).length; // the names it changed: since it and main parted (main's own changes since aren't its) const named = (causal: string) => new Map( [ ...Map.groupBy(cb.store.namespaceRows(cb.store.causal(causal)!.namespace), (n) => n.path), ].map(([k, ns]) => [k, ns.map((n) => n.hash).join(" ")]), ); const mine = named(cb.store.head(here)); const theirs = named(cb.store.mergeBase(cb.store.head(here), mainHead)); const changed = new Set( [...mine.keys(), ...theirs.keys()].filter((k) => mine.get(k) !== theirs.get(k)), ).size; console.log(`\n${bold(`${session ? "this session's branch" : "this branch"}, ${here}`)}`); console.log( entries && hasWork(cb, here, "main") ? ` ${entries} history entr${entries === 1 ? "y" : "ies"} main doesn't have${changed ? `, changing ${changed} name${changed === 1 ? "" : "s"}` : ""}\n${dim(session ? " (it lands on main with tcm_merge, when the user says to)" : ` (it lands when the user says to: \`tcm merge ${here} --squash\` on main)`)}` : dim(" nothing main doesn't have"), ); } // what sessions whose branch is gone left with unsaved work in it (the rest is cleared away when a session starts) const left = sessionDirsGone(cb.lay.root, cb.store.branches()).filter((d) => unsavedIn(d).length); if (left.length) { console.log(`\n${bold("session directories whose branch is gone, holding unsaved work")}`); for (const d of left) console.log( ` ${shownPath(d)} ${dim( unsavedIn(d) .map((f) => path_1.basename(f)) .join(", "), )}`, ); console.log( dim( " (save what's wanted from them, `tcm add <file>` with its path, then delete the directory)", ), ); } const m = loadMirror(cb); if (m) { const behind = onMirrorBranch(cb, m) && m.log !== cb.store.logHead(); console.log( `\n${bold("mirror")} ${shownPath(m.dir) || "."} ${dim(`(${Object.keys(m.files).length} files)`)} ${behind ? yellow("behind: the codebase changed since the files were written (the next command writes them, or says why it can't)") : green("in step")}`, ); } }
uses
- tcmts.cli.codebase.Codebase
- tcmts.cli.codebase.sessionDirFor
- tcmts.cli.codebase.sessionDirsGone
- tcmts.cli.codebase.unsavedIn
- tcmts.cli.main.classify
- tcmts.cli.main.todo
- tcmts.cli.merge.hasWork
- tcmts.cli.mirror.loadMirror
- tcmts.cli.mirror.onMirrorBranch
- tcmts.cli.style.bold
- tcmts.cli.style.dim
- tcmts.cli.style.green
- tcmts.cli.style.yellow
- tcmts.cli.ui.scratchShown
- tcmts.cli.ui.shellWord
- tcmts.cli.ui.shownPath