zz_

app/cli/lib/config.ts

zzmq.dev@gmail.com/zz / app / cli / lib / config.ts @ 568239fb Download
Ladder Summary
  • file The module manages a tiny local configuration file (`~/.zz/config.json` by default) that stores the current “agent” identifier and the list of repository slugs the user cares about. It provides helpers to read the configuration with optional overrides (`readConfig`), to retrieve just the configured repository list without requiring a full config (`configuredRepos`), and to add a new repository slug to the file in‑place (`addRepoToConfig`). **Exported symbols** - `interface ZzConfig` – shape of the configuration object (`agent: string; repos: string[]`). - `function readConfig(overrides?: { agent?: string; repos?: string[]; allowEmpty?: boolean }): ZzConfig` - `function configuredRepos(overrideRepos?: string[]): string[] | null` - `function addRepoToConfig(slug: string): void` (the internal helper `configPath` is not exported).
 // "Which repos do I care about" -- a small local config, deliberately not
 // a negotiation protocol: an agent (human or automated) just maintains a
 // list. How RBAC grants are actually issued/revoked is a later problem;
 // this is only the "which repos should I even ask about" input to that.
 import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
 import { homedir } from "node:os";
 import { dirname, join } from "node:path";
 import { resolveUserPath } from "./paths.js";
 
 export interface ZzConfig {
  agent: string;
  repos: string[];
 }
 
 function configPath(): string {
  return process.env.ZZ_CONFIG ? resolveUserPath(process.env.ZZ_CONFIG) : join(homedir(), ".zz", "config.json");
 }
 
 /** `allowEmpty`: for a read-only viewer (`zz top`) that has something sensible to show with zero repos (an empty dashboard) -- `zz work`/`zz review` genuinely need at least one repo to do anything, so they never pass this. */
 export function readConfig(overrides: { agent?: string; repos?: string[]; allowEmpty?: boolean } = {}): ZzConfig {
  const path = configPath();
  let base: Partial<ZzConfig> = {};
  if (existsSync(path)) {
  base = JSON.parse(readFileSync(path, "utf8")) as Partial<ZzConfig>;
  }
  const agent = overrides.agent ?? base.agent;
  const repos = overrides.repos ?? base.repos ?? (overrides.allowEmpty ? [] : undefined);
  if (!agent || !Array.isArray(repos) || (repos.length === 0 && !overrides.allowEmpty)) {
  throw new Error(
  `no usable zz config (looked at ${path}${existsSync(path) ? "" : ", which doesn't exist"}). Provide --agent and --repos, or create a config: {"agent": "your-agent-id", "repos": ["owner/repo", ...]}`,
  );
  }
  return { agent, repos };
 }
 
 /** `overrideRepos` if given, else the config file's own `repos` if the file exists and has a non-empty list, else null -- meaning "nothing explicit," distinct from readConfig's allowEmpty (which conflates a missing file with a genuinely empty list). `zz top`'s repo-resolution chain needs that distinction: it only falls through to a remote/local auto-discovery fallback when there's truly nothing explicit configured. */
 export function configuredRepos(overrideRepos?: string[]): string[] | null {
  if (overrideRepos && overrideRepos.length > 0) return overrideRepos;
  const path = configPath();
  if (!existsSync(path)) return null;
  const base = JSON.parse(readFileSync(path, "utf8")) as Partial<ZzConfig>;
  return base.repos && base.repos.length > 0 ? base.repos : null;
 }
 
 /** Explore's "track" action, for the two cases with no server-side "repos I care about" list to update instead (remote-not-logged-in, local): grows ~/.zz/config.json in place rather than requiring the user to edit it by hand. No-op if already present. Defaults a never-existing config to the same "zz-top-viewer" agent id `top.ts` already uses for its own read-only readConfig calls. */
 export function addRepoToConfig(slug: string): void {
  const path = configPath();
  let current: ZzConfig = { agent: "zz-top-viewer", repos: [] };
  if (existsSync(path)) {
  const base = JSON.parse(readFileSync(path, "utf8")) as Partial<ZzConfig>;
  current = { agent: base.agent ?? current.agent, repos: base.repos ?? [] };
  }
  if (current.repos.includes(slug)) return;
  mkdirSync(dirname(path), { recursive: true });
  writeFileSync(path, JSON.stringify({ ...current, repos: [...current.repos, slug] }, null, 2) + "\n", "utf8");
 }