From c3dd798fddcb6a11ae31231698b3905ef35b9e5f Mon Sep 17 00:00:00 2001 From: "Joakim L. Engeset" Date: Mon, 2 Feb 2026 16:13:08 +0100 Subject: [PATCH] feat: add status messages for network operations Add stderr status messages before GitHub API calls to provide user feedback during long-running operations. Also refactor groups command to read from local definition file instead of fetching from GitHub. --- src/cli/commands/clone.ts | 7 +++- src/cli/commands/groups.ts | 69 ++++++++++++++++++++++++++------------ src/cli/commands/repos.ts | 1 + src/cli/commands/sync.ts | 1 + src/cli/commands/topics.ts | 1 + src/cli/reporter.ts | 8 +++++ 6 files changed, 65 insertions(+), 22 deletions(-) diff --git a/src/cli/commands/clone.ts b/src/cli/commands/clone.ts index 5ef1e6ad..c70777aa 100644 --- a/src/cli/commands/clone.ts +++ b/src/cli/commands/clone.ts @@ -6,16 +6,19 @@ import { hideBin } from "yargs/helpers" import type { Config } from "../../config" import { createGitHubService, type GitHubService } from "../../github" import { getGroupedRepos, includesTopic } from "../../github/util" -import { createCacheProvider, createConfig } from "../util" +import type { Reporter } from "../reporter" +import { createCacheProvider, createConfig, createReporter } from "../util" async function generateCloneCommands({ config, github, + reporter, org, ...opt }: { config: Config github: GitHubService + reporter: Reporter all: boolean skipCloned: boolean group: string | undefined @@ -29,6 +32,7 @@ async function generateCloneCommands({ return } + reporter.status(`Fetching repositories from ${org}...`) const repos = await github.getOrgRepoList({ org }) const groups = getGroupedRepos(repos) @@ -103,6 +107,7 @@ const command: CommandModule = { github: await createGitHubService({ cache: createCacheProvider(config, argv), }), + reporter: createReporter(), all: !!argv.all, includeArchived: !!argv["include-archived"], name: argv.name as string | undefined, diff --git a/src/cli/commands/groups.ts b/src/cli/commands/groups.ts index d047f0ed..096ee4a2 100644 --- a/src/cli/commands/groups.ts +++ b/src/cli/commands/groups.ts @@ -1,31 +1,58 @@ +import fs from "node:fs" +import path from "node:path" +import process from "node:process" +import { findUp } from "find-up" +import yaml from "js-yaml" import type { CommandModule } from "yargs" -import { createGitHubService } from "../../github" -import { getGroupedRepos } from "../../github/util" -import { createCacheProvider, createConfig, createReporter } from "../util" +import { DefinitionFile } from "../../definition" +import { createReporter } from "../util" + +const CALS_YAML = ".cals.yaml" + +interface CalsManifest { + version: 2 + githubOrganization: string + resourcesDefinition: { + path: string + } +} const command: CommandModule = { command: "groups", - describe: "List available repository groups in a GitHub organization", - builder: (yargs) => - yargs.options("org", { - alias: "o", - default: "capralifecycle", - requiresArg: true, - describe: "GitHub organization", - type: "string", - }), - handler: async (argv) => { - const config = createConfig() + describe: "List available project groups from the definition file", + builder: (yargs) => yargs, + handler: async () => { const reporter = createReporter() - const github = await createGitHubService({ - cache: createCacheProvider(config, argv), - }) - const repos = await github.getOrgRepoList({ org: argv.org as string }) - const groups = getGroupedRepos(repos) + const manifestPath = await findUp(CALS_YAML) + if (manifestPath === undefined) { + reporter.error(`File ${CALS_YAML} not found`) + process.exitCode = 1 + return + } + + const manifest: CalsManifest = yaml.load( + fs.readFileSync(manifestPath, "utf-8"), + ) as CalsManifest + + const definitionPath = path.resolve( + path.dirname(manifestPath), + manifest.resourcesDefinition.path, + ) + + if (!fs.existsSync(definitionPath)) { + reporter.error(`Definition file not found: ${definitionPath}`) + process.exitCode = 1 + return + } + + const definition = await new DefinitionFile(definitionPath).getDefinition() + const projectNames = definition.projects + .map((p) => p.name) + .sort((a, b) => a.localeCompare(b)) - for (const group of groups) { - reporter.log(group.name) + for (const name of projectNames) { + reporter.log(name) } }, } diff --git a/src/cli/commands/repos.ts b/src/cli/commands/repos.ts index 7f37feb0..f414ccd3 100644 --- a/src/cli/commands/repos.ts +++ b/src/cli/commands/repos.ts @@ -41,6 +41,7 @@ async function listRepos({ csv: boolean org: string }) { + reporter.status(`Fetching repositories from ${org}...`) let repos = await github.getOrgRepoList({ org }) if (!includeArchived) { diff --git a/src/cli/commands/sync.ts b/src/cli/commands/sync.ts index 416cf56d..f19ae889 100644 --- a/src/cli/commands/sync.ts +++ b/src/cli/commands/sync.ts @@ -300,6 +300,7 @@ async function getExpectedRepos( expectedRepos: ExpectedRepo[] definitionRepo: ExpectedRepo | null }> { + reporter.status(`Fetching repositories from ${cals.githubOrganization}...`) const githubRepos = await github.getOrgRepoList({ org: cals.githubOrganization, }) diff --git a/src/cli/commands/topics.ts b/src/cli/commands/topics.ts index dc7dfeb5..2ae6e23b 100644 --- a/src/cli/commands/topics.ts +++ b/src/cli/commands/topics.ts @@ -20,6 +20,7 @@ const command: CommandModule = { cache: createCacheProvider(config, argv), }) + reporter.status(`Fetching repositories from ${argv.org}...`) const repos = await github.getOrgRepoList({ org: argv.org as string }) const topics = new Set() diff --git a/src/cli/reporter.ts b/src/cli/reporter.ts index 991ccfd8..b430a8d3 100644 --- a/src/cli/reporter.ts +++ b/src/cli/reporter.ts @@ -105,4 +105,12 @@ export class Reporter { clearLine(this.stdout) this.stdout.write(`${this.format.blue("info")} ${msg}\n`) } + + /** + * Write a status message to stderr for feedback during long-running operations. + * Writing to stderr ensures it doesn't interfere with piped stdout. + */ + public status(msg: string): void { + this.stderr.write(`${this.format.dim(msg)}\n`) + } }