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");
}
}