Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 11 additions & 19 deletions src/commands/branch.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { basename } from "node:path";
import type { Command } from "commander";
import { type Command, Option } from "commander";
import { ArbError, arbAction, readWorkspaceConfig, writeWorkspaceConfig } from "../lib/core";
import type { ArbContext } from "../lib/core";
import { GitCache, branchNameError } from "../lib/git";
Expand Down Expand Up @@ -173,22 +173,24 @@ export function registerBranchCommand(program: Command): void {

branch
.command("show", { isDefault: true })
.option("-q, --quiet", "Output just the branch name")
.option("-v, --verbose", "Show per-repo branch and remote tracking detail")
.addOption(new Option("-q, --quiet", "Output just the branch name").conflicts(["json", "verbose"]))
.addOption(new Option("-v, --verbose", "Show per-repo branch and remote tracking detail").conflicts("quiet"))
.option("--fetch", "Fetch remotes before displaying (default in verbose mode)")
.option("-N, --no-fetch", "Skip fetching")
.option("--json", "Output structured JSON")
.option("--schema", "Print JSON Schema for this command's --json output and exit")
.addOption(new Option("--json", "Output structured JSON").conflicts("quiet"))
.addOption(
new Option("--schema", "Print JSON Schema for this command's --json output and exit").conflicts([
"json",
"quiet",
"verbose",
]),
)
.summary("Show the workspace branch (default)")
.description(
"Examples:\n\n arb branch show Show branch, base, and share\n arb branch show -v Per-repo tracking detail\n arb branch show -q Just the branch name\n\nShow the workspace branch, base branch, share (remote tracking) branch, and any per-repo deviations. Use --verbose to show a per-repo table with branch and remote tracking info (fetches by default; use -N to skip). Press Ctrl+C during the fetch to cancel and use stale data. Use --quiet to output just the branch name (useful for scripting). Use --json for machine-readable output.\n\nSee 'arb help scripting' for output modes and piping.",
)
.action(async (options, command) => {
if (options.schema) {
if (options.json || options.quiet || options.verbose) {
error("Cannot combine --schema with --json, --quiet, or --verbose.");
throw new ArbError("Cannot combine --schema with --json, --quiet, or --verbose.");
}
printSchema(BranchJsonOutputSchema);
return;
}
Expand All @@ -209,16 +211,6 @@ async function runBranch(
const wsDir = `${ctx.arbRootDir}/${ctx.currentWorkspace}`;
const configFile = `${wsDir}/.arbws/config.json`;

if (options.quiet && options.json) {
error("Cannot combine --quiet with --json.");
throw new ArbError("Cannot combine --quiet with --json.");
}

if (options.quiet && options.verbose) {
error("Cannot combine --quiet with --verbose.");
throw new ArbError("Cannot combine --quiet with --verbose.");
}

const wb = await workspaceBranch(wsDir);
const branch = wb?.branch ?? (ctx.currentWorkspace as string);
const base = readWorkspaceConfig(configFile)?.base ?? null;
Expand Down
23 changes: 9 additions & 14 deletions src/commands/list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync } from "node:fs";
import { basename } from "node:path";
import type { Command } from "commander";
import { type Command, Option } from "commander";
import { z } from "zod";
import { ArbError, type RelativeTimeParts, arbAction, formatRelativeTimeParts, readWorkspaceConfig } from "../lib/core";
import type { ArbContext } from "../lib/core";
Expand Down Expand Up @@ -69,28 +69,23 @@ export function registerListCommand(program: Command): void {
.option("-w, --where <filter>", "Filter workspaces by repo status flags (comma = OR, + = AND, ^ = negate)")
.option("--older-than <duration>", "Only list workspaces not touched in the given duration (e.g. 30d, 2w, 3m, 1y)")
.option("--newer-than <duration>", "Only list workspaces touched within the given duration (e.g. 7d, 2w)")
.option("-q, --quiet", "Output one workspace name per line")
.option("--json", "Output structured JSON")
.option("--schema", "Print JSON Schema for this command's --json output and exit")
.addOption(new Option("-q, --quiet", "Output one workspace name per line").conflicts("json"))
.addOption(new Option("--json", "Output structured JSON").conflicts("quiet"))
.addOption(
new Option("--schema", "Print JSON Schema for this command's --json output and exit").conflicts([
"json",
"quiet",
]),
)
.action(async (options, command) => {
if (options.schema) {
if (options.json || options.quiet) {
error("Cannot combine --schema with --json or --quiet.");
throw new ArbError("Cannot combine --schema with --json or --quiet.");
}
printSchema(z.array(ListJsonEntrySchema));
return;
}
await arbAction(async (ctx, options) => {
const cache = ctx.cache;
const aCache = ctx.analysisCache;
{
// Conflict checks
if (options.quiet && options.json) {
error("Cannot combine --quiet with --json.");
throw new ArbError("Cannot combine --quiet with --json.");
}

const whereFilter = resolveWhereFilter(options);
const ageFilter = resolveAgeFilter(options);
if ((whereFilter || ageFilter) && options.status === false) {
Expand Down
10 changes: 3 additions & 7 deletions src/commands/log.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { basename } from "node:path";
import type { Command } from "commander";
import { type Command, Option } from "commander";
import { ArbError, arbAction } from "../lib/core";
import { getCommitsBetweenFull, gitLocal } from "../lib/git";
import { printSchema } from "../lib/json";
Expand Down Expand Up @@ -56,18 +56,14 @@ export function registerLogCommand(program: Command): void {
.option("-d, --dirty", "Only log dirty repos (shorthand for --where dirty)")
.option("-w, --where <filter>", "Only log repos matching status filter (comma = OR, + = AND, ^ = negate)")
.option("-v, --verbose", "Show commit bodies and changed files")
.option("--json", "Output structured JSON to stdout")
.option("--schema", "Print JSON Schema for this command's --json output and exit")
.addOption(new Option("--json", "Output structured JSON to stdout").conflicts("schema"))
.addOption(new Option("--schema", "Print JSON Schema for this command's --json output and exit").conflicts("json"))
.summary("Show feature branch commits across repos")
.description(
"Examples:\n\n arb log Show feature commits across repos\n arb log api --verbose Include commit bodies and files\n arb log -n 5 --where dirty Limit commits, filter repos\n\nShow commits on the feature branch since diverging from the base branch across all repos in the workspace. Answers 'what have I done in this workspace?' by showing only the commits that belong to the current feature.\n\nShows commits in the range base..HEAD for each repo. Use --fetch to fetch before showing log (default is no fetch). Use -n to limit how many commits are shown per repo. Use -v/--verbose to also show commit bodies and changed files. Use --json for machine-readable output.\n\nRepos are positional arguments — name specific repos to filter, or omit to show all. Reads repo names from stdin when piped (one per line). Use --where to filter by status flags. See 'arb help filtering' for filter syntax. Skipped repos (detached HEAD, wrong branch) are explained in the output, never silently omitted.\n\nSee 'arb help scripting' for output modes and piping.",
)
.action(async (repoArgs: string[], options, command) => {
if (options.schema) {
if (options.json) {
error("Cannot combine --schema with --json.");
throw new ArbError("Cannot combine --schema with --json.");
}
printSchema(LogJsonOutputSchema);
return;
}
Expand Down
8 changes: 2 additions & 6 deletions src/commands/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ export function registerPullCommand(program: Command): void {
.option("--reset", "Reset to remote tip instead of pulling (overrides rebased-locally skip)")
.option("-y, --yes", "Skip confirmation prompt")
.option("--dry-run", "Show what would happen without executing")
.option("--rebase", "Pull with rebase")
.option("--merge", "Pull with merge")
.addOption(new Option("--rebase", "Pull with rebase").conflicts("merge"))
.addOption(new Option("--merge", "Pull with merge").conflicts("rebase"))
.option("--autostash", "Stash uncommitted changes before pull, re-apply after")
.option("--include-wrong-branch", "Include repos on a different branch than the workspace")
.option("-v, --verbose", "Show incoming commits in the plan")
Expand All @@ -75,10 +75,6 @@ export function registerPullCommand(program: Command): void {
error(`${flag} does not accept repo arguments`);
throw new ArbError(`${flag} does not accept repo arguments`);
}
if (options.rebase && options.merge) {
error("Cannot use both --rebase and --merge");
throw new ArbError("Cannot use both --rebase and --merge");
}
const { wsDir } = requireWorkspace(ctx);
const repoNames = await resolveReposFromArgsOrStdin(wsDir, repoArgs);
await runPull(ctx, repoNames, options);
Expand Down
33 changes: 11 additions & 22 deletions src/commands/repo.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { existsSync, rmSync } from "node:fs";
import { basename, join } from "node:path";
import type { Command } from "commander";
import { type Command, Option } from "commander";
import { z } from "zod";
import { ArbError, arbAction, readProjectConfig, writeProjectConfig } from "../lib/core";
import { gitLocal, gitNetwork, networkTimeout } from "../lib/git";
Expand Down Expand Up @@ -157,37 +157,26 @@ export function registerRepoCommand(program: Command): void {

repo
.command("list", { isDefault: true })
.option("-q, --quiet", "Output one repo name per line")
.option("-v, --verbose", "Show remote URLs alongside names")
.option("--json", "Output structured JSON")
.option("--schema", "Print JSON Schema for this command's --json output and exit")
.addOption(new Option("-q, --quiet", "Output one repo name per line").conflicts(["json", "verbose"]))
.addOption(new Option("-v, --verbose", "Show remote URLs alongside names").conflicts(["quiet", "json"]))
.addOption(new Option("--json", "Output structured JSON").conflicts(["quiet", "verbose"]))
.addOption(
new Option("--schema", "Print JSON Schema for this command's --json output and exit").conflicts([
"json",
"quiet",
"verbose",
]),
)
.summary("List cloned repos (default)")
.description(
"Examples:\n\n arb repo list List repos with remote roles\n arb repo list -v Include remote URLs\n arb repo list -q One name per line\n\nList all repositories that have been cloned into .arb/repos/. Shows resolved SHARE and BASE remote names for each repo. Use --verbose to include remote URLs alongside names. Use --quiet for plain enumeration (one name per line). Use --json for machine-readable output.",
)
.action(async (options, command) => {
if (options.schema) {
if (options.json || options.quiet || options.verbose) {
error("Cannot combine --schema with --json, --quiet, or --verbose.");
throw new ArbError("Cannot combine --schema with --json, --quiet, or --verbose.");
}
printSchema(z.array(RepoListJsonEntrySchema));
return;
}
await arbAction(async (ctx, options) => {
if (options.quiet && options.json) {
error("Cannot combine --quiet with --json.");
throw new ArbError("Cannot combine --quiet with --json.");
}
if (options.quiet && options.verbose) {
error("Cannot combine --quiet with --verbose.");
throw new ArbError("Cannot combine --quiet with --verbose.");
}
if (options.verbose && options.json) {
error("Cannot combine --verbose with --json.");
throw new ArbError("Cannot combine --verbose with --json.");
}

const repos = listRepos(ctx.reposDir);
if (repos.length === 0) return;

Expand Down
33 changes: 12 additions & 21 deletions src/commands/reset.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { basename } from "node:path";
import type { Command } from "commander";
import { type Command, Option } from "commander";
import {
ArbError,
type OperationRecord,
type RepoOperationState,
arbAction,
Expand Down Expand Up @@ -33,17 +32,7 @@ import {
runPlanFlow,
} from "../lib/sync";
import type { CommitDisplayEntry } from "../lib/sync/types";
import {
dryRunNotice,
error,
info,
inlineResult,
inlineStart,
plural,
shouldColor,
warn,
yellow,
} from "../lib/terminal";
import { dryRunNotice, info, inlineResult, inlineStart, plural, shouldColor, warn, yellow } from "../lib/terminal";
import { requireBranch, requireWorkspace, resolveReposFromArgsOrStdin, workspaceRepoDirs } from "../lib/workspace";

export type ResetMode = "soft" | "mixed" | "hard";
Expand Down Expand Up @@ -375,9 +364,16 @@ export function registerResetCommand(program: Command): void {
.option("--fetch", "Fetch from all remotes before reset (default)")
.option("-N, --no-fetch", "Skip fetching before reset")
.option("--base", "Always reset to the base branch, even when a remote share branch exists")
.option("--soft", "Move HEAD only; commits become staged changes")
.option("--mixed", "Move HEAD and reset index; changes become unstaged (default)")
.option("--hard", "Move HEAD, reset index and working tree; discards all local changes")
.addOption(new Option("--soft", "Move HEAD only; commits become staged changes").conflicts(["mixed", "hard"]))
.addOption(
new Option("--mixed", "Move HEAD and reset index; changes become unstaged (default)").conflicts(["soft", "hard"]),
)
.addOption(
new Option("--hard", "Move HEAD, reset index and working tree; discards all local changes").conflicts([
"soft",
"mixed",
]),
)
.option("-y, --yes", "Skip confirmation prompt")
.option("--dry-run", "Show what would happen without executing")
.option("-v, --verbose", "Show commits to be reset in the plan")
Expand All @@ -389,11 +385,6 @@ export function registerResetCommand(program: Command): void {
)
.action(
arbAction(async (ctx, repoArgs: string[], options) => {
const modeFlags = [options.soft, options.mixed, options.hard].filter(Boolean);
if (modeFlags.length > 1) {
error("Cannot combine --soft, --mixed, and --hard");
throw new ArbError("Cannot combine --soft, --mixed, and --hard");
}
const resetMode: ResetMode = options.soft ? "soft" : options.hard ? "hard" : "mixed";

const where = resolveWhereFilter(options);
Expand Down
37 changes: 16 additions & 21 deletions src/commands/status.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { basename, resolve } from "node:path";
import type { Command } from "commander";
import { type Command, Option } from "commander";
import { predictMergeConflict } from "../lib/analysis";
import { ArbError, type CommandContext, arbAction, readOperationRecord, readWorkspaceConfig } from "../lib/core";
import { type CommandContext, arbAction, readOperationRecord, readWorkspaceConfig } from "../lib/core";
import { localTimeout } from "../lib/git/git";
import { printSchema } from "../lib/json";
import { type StatusJsonOutput, StatusJsonOutputSchema } from "../lib/json";
Expand Down Expand Up @@ -38,7 +38,6 @@ import {
} from "../lib/sync";
import {
clearScanProgress,
error,
hintsEnabled,
isTTY,
listenForAbortSignal,
Expand All @@ -55,20 +54,26 @@ export function registerStatusCommand(program: Command): void {
.option("-w, --where <filter>", "Filter repos by status flags (comma = OR, + = AND, ^ = negate)")
.option("--fetch", "Fetch from all remotes before showing status (default)")
.option("-N, --no-fetch", "Skip fetching")
.option("-v, --verbose", "Show file-level detail for each repo")
.option("-q, --quiet", "Output one repo name per line")
.option("--json", "Output structured JSON (combine with --verbose for commit and file detail)")
.option("--schema", "Print JSON Schema for this command's --json output and exit")
.addOption(new Option("-v, --verbose", "Show file-level detail for each repo").conflicts("quiet"))
.addOption(new Option("-q, --quiet", "Output one repo name per line").conflicts(["json", "verbose"]))
.addOption(
new Option("--json", "Output structured JSON (combine with --verbose for commit and file detail)").conflicts(
"quiet",
),
)
.addOption(
new Option("--schema", "Print JSON Schema for this command's --json output and exit").conflicts([
"json",
"quiet",
"verbose",
]),
)
.summary("Show repo branches, sync status, and local changes")
.description(
"Examples:\n\n arb status Show all repos\n arb status --dirty Only repos with local changes\n arb status api web --verbose File-level detail for specific repos\n\nShow each repo's position relative to the base branch, push status against the share remote, and local changes (staged, modified, untracked). The summary includes the workspace's last commit date (most recent author date across all repos).\n\nRepos are positional arguments — name specific repos to filter, or omit to show all. Reads repo names from stdin when piped (one per line), enabling composition like: arb status -q --where dirty | arb log.\n\nUse --dirty to only show repos with uncommitted changes. Use --where <filter> to filter by status flags. See 'arb help filtering' for filter syntax. Fetches from all remotes by default for fresh data (skip with -N/--no-fetch). Press Ctrl+C during the fetch to cancel and use stale data. Quiet mode (-q) skips fetching by default for scripting speed. Use --verbose for file-level detail. Use --json for machine-readable output. Combine --json --verbose to include commit lists and file-level detail in JSON output.\n\nFor a live dashboard with sync commands, use 'arb watch'.\n\nMerged branches show the detected PR number when available (e.g. 'merged (#123), gone'), extracted from merge or squash commit subjects. JSON output includes detectedPr fields.\n\nSee 'arb help stacked' for stacked workspace status flags. See 'arb help scripting' for output modes and piping.",
)
.action(async (repoArgs: string[], options, command) => {
if (options.schema) {
if (options.json || options.quiet || options.verbose) {
error("Cannot combine --schema with --json, --quiet, or --verbose.");
throw new ArbError("Cannot combine --schema with --json, --quiet, or --verbose.");
}
printSchema(StatusJsonOutputSchema);
return;
}
Expand Down Expand Up @@ -96,16 +101,6 @@ async function runStatus(
const aCache = ctx.analysisCache;
const where = resolveWhereFilter(options);

// Conflict checks
if (options.quiet && options.json) {
error("Cannot combine --quiet with --json.");
throw new ArbError("Cannot combine --quiet with --json.");
}
if (options.quiet && options.verbose) {
error("Cannot combine --quiet with --verbose.");
throw new ArbError("Cannot combine --quiet with --verbose.");
}

// Resolve repo selection: positional args > stdin > all
const selectedRepos = await resolveReposFromArgsOrStdin(wsDir, repoArgs);
const selectedSet = new Set(selectedRepos);
Expand Down
Loading
Loading