zz_

app/cli/commands/top.ts

zzmq.dev@gmail.com/zz / app / cli / commands / top.ts @ 3599b4eb Download
Ladder Summary
  • file **What the file does** The `top` command implements the `zz top` CLI entry point: it determines whether to run in remote or local mode (without requiring a login), resolves the set of watched repositories, creates a live watch store, wires up helper callbacks for tracking new repos, logging in/out, and posting “ask” items, then launches the full‑screen Ink‑based TUI (`TopApp`) that shows a real‑time dashboard of all lanes. **Exported symbols** - `interface TopOptions` – the options object accepted by the command (`repos?`, `local?`). - `async function top(options: TopOptions): Promise<void>` – the main entry point that sets up the mode, stores, callbacks and renders the dashboard.
 // `zz top`: real-time dashboard over every Lane in the repos you care
-// about (same local config as `zz work`) -- watched live, not read back
-// from a dump after the fact. Remote by default, like work/review;
-// --local stays fully supported for the demo/local-backend path.
+// about -- watched live, not read back from a dump after the fact.
+// Remote by default, like work/review; --local stays fully supported
+// for the demo/local-backend path. Unlike work/review, `top` is
+// read-only and has to work with ZERO config: no ~/.zz/config.json,
+// no --repos, no login (remote) or other running zz process (local)
+// all mean "show an empty/starter dashboard, not a crash" -- the
+// repo-resolution chain below (configuredRepos -> /api/me/repos ->
+// local discovery -> empty) is what makes that true, and Explore
+// (reachable as its own tab, see TopApp.tsx) is how you find repos to
+// add from inside the TUI instead of hand-editing config.json.
 import React from "react";
 import { render } from "ink";
-import { resolveRepo } from "../lib/repo.js";
-import { readConfig } from "../lib/config.js";
+import { execFileSync } from "node:child_process";
+import { resolveRepo, isLocallyOnboarded, inferGitHubSlugFromCwd } from "../lib/repo.js";
+import { configuredRepos, addRepoToConfig } from "../lib/config.js";
 import { resolveCliBackendMode } from "../lib/backendMode.js";
+import { apiFetch } from "../lib/api.js";
+import { createRepoWatchStore } from "../lib/watchedRepos.js";
+import { listRunningRepos } from "../lib/processRegistry.js";
 import { TopApp } from "../../tui/TopApp.js";
 import { enterFullscreen } from "../../tui/fullscreen.js";
 
 export interface TopOptions {
  repos?: string[];
  local?: boolean;
 }
 
+/** No explicit config/--repos, local mode: union whatever other local `zz work`/`zz review` processes are watching with the current directory, if it's an onboarded GitHub-backed repo. Prints onboarding guidance and returns null (meaning "nothing to show, caller should exit") rather than ever rendering an empty-but-otherwise-normal TUI with no obvious next step. */
+async function discoverLocalRepos(): Promise<string[] | null> {
+ const running = listRunningRepos();
+ const cwdSlug = inferGitHubSlugFromCwd();
+ const cwdRepo = cwdSlug ? resolveRepo(cwdSlug) : null;
+ const cwdOnboarded = cwdRepo ? await isLocallyOnboarded(cwdRepo) : false;
+ const repos = cwdOnboarded && cwdSlug ? [...new Set([...running, cwdSlug])] : running;
+ if (repos.length > 0) return repos;
+
+ if (cwdSlug && !cwdOnboarded) {
+ console.log(`Nothing to show yet. This directory looks like it's "${cwdSlug}" on GitHub, but it isn't onboarded into zz locally. Run:\n\n zz ladder build ${cwdSlug}\n\nthen run \`zz top --local\` again.`);
+ } else if (cwdRepo === null && cwdSlugLooksLikeGitRepo()) {
+ console.log(`Nothing to show yet. This directory is a git repo, but its origin isn't a GitHub remote -- \`zz ladder build\` (zz's only onboarding path right now) needs one. Run \`zz work --local <owner/repo>\`/\`zz review --local <owner/repo>\` directly on a repo you've already onboarded, or use Explore (local mode browses the filesystem) from a terminal where one is nearby.`);
+ } else {
+ console.log("Nothing to show yet -- no other local zz process is running, and this directory isn't a git repo. Run `zz work`/`zz review` on a repo, or `zz ladder build owner/repo` to onboard one from GitHub, then run `zz top --local` again.");
+ }
+ return null;
+}
+
+// A plain git-repo check (no GitHub remote needed) just to pick the
+// right wording above -- inferGitHubSlugFromCwd() returning null is
+// ambiguous between "not a git repo" and "git repo, no GitHub
+// remote," and those two deserve different guidance.
+function cwdSlugLooksLikeGitRepo(): boolean {
+ try {
+ execFileSync("git", ["rev-parse", "--show-toplevel"], { stdio: "ignore" });
+ return true;
+ } catch {
+ return false;
+ }
+}
+
 export async function top(options: TopOptions): Promise<void> {
  // A read-only viewer, never an actor -- "zz-top-viewer" never needs
  // to be a real identity (nothing is ever posted/claimed as it).
- // resolveCliBackendMode still resolves real credentials in remote
- // mode so the TUI's own reads (useLaneSnapshots/useLadderTrees's
- // callRemoteTool calls) are authed.
- const mode = await resolveCliBackendMode(options);
+ // requireIdentity: false is the whole point here -- remote mode
+ // with no `zz login` session is a real, supported state (an
+ // anonymous Explore search, an empty dashboard), not a crash.
+ const mode = await resolveCliBackendMode({ ...options, requireIdentity: false });
  process.on("SIGINT", () => void mode.cleanup().then(() => process.exit(0)));
  process.on("SIGTERM", () => void mode.cleanup().then(() => process.exit(0)));
 
- // Zero repos resolved (no config.json, no --repos) is a real, valid
- // state here -- an empty dashboard, not a crash -- unlike
- // work/review, which always need at least one repo to do anything.
- const config = readConfig({ agent: "zz-top-viewer", repos: options.repos, allowEmpty: true });
- const resolvedRepos = config.repos.map((slug) => resolveRepo(slug));
+ const explicit = configuredRepos(options.repos);
+ let repoSlugs: string[];
+ if (explicit) {
+ repoSlugs = explicit;
+ } else if (mode.useRemote) {
+ repoSlugs = mode.identity ? (await apiFetch<{ repos: { slug: string }[] }>("GET", "/api/me/repos")).repos.map((r) => r.slug) : [];
+ } else {
+ const discovered = await discoverLocalRepos();
+ if (discovered === null) {
+ await mode.cleanup();
+ return; // discoverLocalRepos already printed guidance -- nothing to render
+ }
+ repoSlugs = discovered;
+ }
+
+ const resolvedRepos = repoSlugs.map((slug) => resolveRepo(slug));
+ const watch = createRepoWatchStore(resolvedRepos);
+
+ // The one "track" action Explore needs, in both its remote (search)
+ // and local (directory browser) modes -- which underlying store it
+ // writes to depends on the exact same three-way split the original
+ // spec draws: a real server-side list when you're logged in to
+ // remote, a local config file otherwise (not logged in remotely, or
+ // local mode entirely, where there's no server concept at all).
+ const onTrack = async (slug: string): Promise<void> => {
+ if (mode.useRemote && mode.identity) {
+ await apiFetch("POST", `/api/repos/${slug}/track`);
+ } else {
+ addRepoToConfig(slug);
+ }
+ watch.add(resolveRepo(slug));
+ };
+
  enterFullscreen();
- render(React.createElement(TopApp, { repos: resolvedRepos.map((r) => ({ repo: r.slug, repoDir: r.repoDir, stateDir: r.stateDir })), remote: mode.useRemote }));
+ render(React.createElement(TopApp, { watch, remote: mode.useRemote, local: !mode.useRemote, onTrack }));
 }