zz_

app/cli/commands/lane-log.ts

zzmq.dev@gmail.com/zz / app / cli / commands / lane-log.ts @ 5e5720fb Download
Ladder Summary
  • file The **`lane-log`** command retrieves the history of all lanes for a given repository (either from the local store or via a remote tool), flattens and chronologically sorts the ASK/ANSWER/INSIGHT entries, and then renders the result as either a human‑readable text report or a JSON dump, optionally writing it to a file. Helper functions format payload summaries and render the output, while the exported `laneLog` function orchestrates the whole process. **Exported symbols** * `LaneLogFormat` – type alias for the output format (`"text"` | `"json"`). * `LaneLogOptions` – interface describing optional flags (`format`, `lane`, `out`, `local`). * `laneLog(slug: string, options: LaneLogOptions): Promise<void>` – the main CLI entry point.
// `zz lane log <owner/repo>`: a chronological feed of every ASK/ANSWER
// across every Lane for a repo -- the protocol trail `ladder dump` shows
// the resulting content for, but not the process that produced it.
// Remote by default, like work/review/top -- confirmed live this was
// the one command that never got this treatment (always read the
// local flat-file store regardless of where the repo actually lived),
// so this showed nothing at all for any repo only ever touched on the
// deployed Worker.
import { resolveRepo } from "../lib/repo.js";
import { listLanes } from "../../engine/store.js";
import { callRemoteTool } from "../lib/remoteTools.js";
import type { HistoryEntry, Lane } from "../../engine/types.js";

export type LaneLogFormat = "text" | "json";

export interface LaneLogOptions {
  format?: LaneLogFormat;
  lane?: string;
  out?: string;
  local?: boolean;
}

interface LogRow {
  at: string;
  lane: string;
  laneName: string;
  entry: HistoryEntry;
}

function flatten(lanes: Lane[]): LogRow[] {
  const rows: LogRow[] = [];
  for (const lane of lanes) {
    for (const entry of lane.history) {
      rows.push({ at: entry.at, lane: lane.id, laneName: lane.name, entry });
    }
  }
  return rows.sort((a, b) => a.at.localeCompare(b.at));
}

// A payload can carry a whole Rung (or array of Rungs), a commit, or
// just a few scalar fields -- print enough to identify what's being
// asked for/answered without dumping a full diff or Ladder subtree into
// every line of the log.
function summarizePayload(payload: unknown): string {
  if (payload === undefined || payload === null) return "";
  if (typeof payload !== "object") return String(payload);
  const p = payload as Record<string, unknown>;
  if (p.target) return `{target: ${p.target}}`;
  if (p.grant) return `{grant: ${p.grant}}`;
  if (p.commit && typeof p.commit === "object") {
    const c = p.commit as { sha?: string; message?: string };
    return `{commit: ${c.sha?.slice(0, 7)} "${c.message}"${p.change ? `, change: ${(p.change as { path?: string }).path}` : ""}}`;
  }
  if (p.path && p.changeSummary) return `{path: ${p.path}}`;
  if (p.ladder) {
    const ladder = p.ladder as { path?: string; kind?: string } | { path?: string; kind?: string }[];
    const rungs = Array.isArray(ladder) ? ladder : [ladder];
    return `{ladder: ${rungs.map((r) => `${r.kind}:${r.path}`).join(", ")}}`;
  }
  if ("path" in p && "kind" in p) return `{${p.kind}:${p.path}}`;
  const json = JSON.stringify(payload);
  return json.length > 80 ? json.slice(0, 77) + "..." : json;
}

function renderEntry(entry: HistoryEntry): string {
  if (entry.type === "ASK") {
    const deps = entry.depends_on?.length ? ` deps=[${entry.depends_on.join(",")}]` : "";
    return `ASK    ${entry.id.padEnd(14)} ${entry.kind.padEnd(18)} from ${entry.from}${deps} ${summarizePayload(entry.payload)}`;
  }
  if (entry.type === "ANSWER") {
    return `ANSWER ${entry.id.padEnd(14)} -> ${entry.ask.padEnd(14)} ${entry.action.padEnd(9)} from ${entry.from} ${summarizePayload(entry.payload)}`;
  }
  return `INSIGHT ${entry.kind.padEnd(17)} from ${entry.from}: ${entry.text}`;
}

function renderText(lanes: Lane[], rows: LogRow[]): string {
  const lines: string[] = [];
  lines.push(`${lanes.length} lanes, ${rows.length} protocol events.`);
  lines.push("");
  for (const row of rows) {
    lines.push(`${row.at}  [${row.lane}] ${renderEntry(row.entry)}`);
  }
  lines.push("");
  lines.push("lanes:");
  for (const lane of lanes) {
    lines.push(`  [${lane.status}] ${lane.id} -- ${lane.name} (origin: ${lane.origin ? lane.origin.lane : "none"})`);
  }
  return lines.join("\n");
}

export async function laneLog(slug: string, options: LaneLogOptions): Promise<void> {
  // allowAnonymous: lane_list is one of the server's own ANON_READ_TOOLS
  // for a public repo (webApp.ts) -- works the same with or without a
  // `zz login` session, same as `zz top`'s own reads.
  let lanes = options.local ? listLanes(resolveRepo(slug).stateDir) : ((await callRemoteTool(slug, "lane_list", {}, true)) as Lane[]);
  if (options.lane) lanes = lanes.filter((l) => l.id === options.lane);
  const rows = flatten(lanes);

  const format = options.format ?? "text";
  const output = format === "json" ? JSON.stringify({ repo: slug, lanes, events: rows }, null, 2) : renderText(lanes, rows);

  if (options.out) {
    await import("node:fs").then((fs) => fs.writeFileSync(options.out!, output));
    console.error(`wrote ${lanes.length} lanes, ${rows.length} events to ${options.out}`);
  } else {
    process.stdout.write(output + "\n");
  }
}