Module: RepoTender::CLI

Defined in:
lib/repo_tender/cli.rb,
lib/repo_tender/cli/org.rb,
lib/repo_tender/cli/repo.rb,
lib/repo_tender/cli/sync.rb,
lib/repo_tender/cli/config.rb,
lib/repo_tender/cli/daemon.rb,
lib/repo_tender/cli/status.rb,
lib/repo_tender/cli/options.rb

Overview

CLI surface — thin translation layer between argv and the existing Config::Store / State::Store / Sync::Engine boundaries.

The CLI never mutates a git repo directly; repo mutation happens inside the engine, which already upholds the no-data-loss invariant (PRD §1). The CLI’s job is:

1. parse argv (via Dry::CLI's nested `register`)
2. load/mutate validated config (Config::Store) OR delegate to
   the engine / state store
3. translate Result to: out/err message + exit code

Exit-code seam: each command records an ‘Outcome(exit_code:, message:)` (the thread-local stash) and writes the user-facing message to `out`/`err` via the injected IOs. The `bin/repo-tender` entrypoint reads the recorded Outcome and calls Kernel.exit with the code — see CLI.run below. Tests can inspect last_outcome in-process (no subprocess needed for unit tests); a subprocess Open3.capture3 covers the G3 “real exit” proof.

Defined Under Namespace

Modules: ConfigCmd, Daemon, GlobalOptions, Org, Registry, Repo, Status, Sync Classes: Outcome

Constant Summary collapse

TOP_LEVEL_HELP =

Program-name-level invocations that must succeed (exit 0) with output on stdout. Dry::CLI has no root command registered, so it would otherwise route these through its “command not found” path (Usage → stderr → exit 1). We intercept ONLY the exact top-level forms here; a leaf help like ‘sync –help` (argv [“sync”, “–help”]) and a group like `repo` / `repo –help` are NOT matched, so Dry::CLI keeps handling them as before (leaf help →stdout/exit 0; group → usage/stderr/exit 1, accepted per G7).

[[], ["--help"], ["-h"], ["help"]].freeze
VERSION_REQUEST =
[["version"], ["--version"]].freeze

Class Method Summary collapse

Class Method Details

.envObject

Thread-local env hash. Defaults to ENV. Tests inject a temp HOME / XDG_* hash via Thread.current = env_hash. The CLI’s ‘make_paths` reads this to resolve the config/state file locations under the test’s temp home.



40
41
42
# File 'lib/repo_tender/cli.rb', line 40

def self.env
  Thread.current[:repo_tender_cli_env] || ENV
end

.last_outcomeObject



50
51
52
# File 'lib/repo_tender/cli.rb', line 50

def self.last_outcome
  Thread.current[:repo_tender_cli_outcome]
end

.make_pathsObject

Internal: build a Paths instance scoped to the active env (Thread.current || ENV). Every command uses this so tests can inject a temp home without mutating the real ENV.



116
117
118
# File 'lib/repo_tender/cli.rb', line 116

def self.make_paths
  Paths.new(environment: env)
end

Render the top-level command-group listing (reusing Dry::CLI’s own Usage formatter so it stays in sync with the registry) to stdout and exit 0.



101
102
103
104
# File 'lib/repo_tender/cli.rb', line 101

def self.print_usage(stdout)
  stdout.puts Dry::CLI::Usage.call(Registry.get([]))
  Kernel.exit(0)
end

Print the gem version to stdout and exit 0.



107
108
109
110
# File 'lib/repo_tender/cli.rb', line 107

def self.print_version(stdout)
  stdout.puts RepoTender::VERSION
  Kernel.exit(0)
end

.record_outcome(outcome) ⇒ Object

Thread-local Outcome stash. The most recent command’s Outcome is read by CLI.run to set the process exit code.



46
47
48
# File 'lib/repo_tender/cli.rb', line 46

def self.record_outcome(outcome)
  Thread.current[:repo_tender_cli_outcome] = outcome
end

.run(argv, stdout, stderr) ⇒ Object

Entrypoint. Called by bin/repo-tender. Intercepts the top-level help/version forms (stdout, exit 0), otherwise hands argv to Dry::CLI for command dispatch and translates the last Outcome to a process exit code. A ‘Interrupt` raised from inside command dispatch (most commonly: a SIGINT during a long-running `Shell.run`, e.g. at a `git` username prompt or mid-clone) is caught here and mapped to a clean exit code 130 (128 + SIGINT) with a single human line on stderr — the G2 ^C-hygiene fix (Slice 6). The reader-thread `IOError` noise that Open3 emits in the same scenario is suppressed at the `Shell.run` seam (see `lib/repo_tender/shell.rb`).



76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
# File 'lib/repo_tender/cli.rb', line 76

def self.run(argv, stdout, stderr)
  return print_usage(stdout) if TOP_LEVEL_HELP.include?(argv)
  return print_version(stdout) if VERSION_REQUEST.include?(argv)

  begin
    Dry::CLI.new(Registry).call(arguments: argv, out: stdout, err: stderr)
    outcome = last_outcome
    Kernel.exit(outcome&.exit_code || 0)
  rescue Interrupt
    # Map a user ^C to a clean exit-130 with a single human line.
    # `Kernel.exit` raises `SystemExit` (callers/tests can rescue
    # it to inspect the status). The `at_exit` handlers run,
    # stdio is flushed, the process exits with code 130. We do
    # NOT blanket-rescue `StandardError` and we do NOT make
    # non-interrupt failures exit 0 (the outcome-translation path
    # above is unchanged for the happy / non-Interrupt failure
    # paths).
    stderr.puts "interrupted"
    Kernel.exit(130)
  end
end