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
-
.env ⇒ Object
Thread-local env hash.
- .last_outcome ⇒ Object
-
.make_paths ⇒ Object
Internal: build a Paths instance scoped to the active env (Thread.current || ENV).
-
.print_usage(stdout) ⇒ Object
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.
-
.print_version(stdout) ⇒ Object
Print the gem version to stdout and exit 0.
-
.record_outcome(outcome) ⇒ Object
Thread-local Outcome stash.
-
.run(argv, stdout, stderr) ⇒ Object
Entrypoint.
Class Method Details
.env ⇒ Object
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_outcome ⇒ Object
50 51 52 |
# File 'lib/repo_tender/cli.rb', line 50 def self.last_outcome Thread.current[:repo_tender_cli_outcome] end |
.make_paths ⇒ Object
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 |
.print_usage(stdout) ⇒ Object
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_version(stdout) ⇒ Object
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 |