status

tcmts.cli.main.status · #lwetek56 · term · runs on the server

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

used by