Module: Space::Src::CLI
- Defined in:
- lib/space_src/cli.rb,
lib/space_src/cli/repo.rb,
lib/space_src/cli/sync.rb,
lib/space_src/cli/clone.rb,
lib/space_src/cli/shell.rb,
lib/space_src/cli/config.rb,
lib/space_src/cli/daemon.rb,
lib/space_src/cli/status.rb,
lib/space_src/cli/options.rb,
lib/space_src/cli/org.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 exe/src
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, Shell, Status, Sync Classes: Clone, 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 likerepo/repo --helpare 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
-
.bare_query?(argv) ⇒ Boolean
True when argv is a single token that is not a registered top-level command or group, and not already handled by the help/version intercepts.
-
.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
.bare_query?(argv) ⇒ Boolean
True when argv is a single token that is not a registered top-level command or group, and not already handled by the help/version intercepts. Both CLI.run and dispatch_src delegate to this shared predicate so the routing logic is defined exactly once.
69 70 71 72 73 |
# File 'lib/space_src/cli.rb', line 69 def self.(argv) return false if TOP_LEVEL_HELP.include?(argv) return false if VERSION_REQUEST.include?(argv) argv.length == 1 && !Registry.get([]).children.key?(argv[0]) end |
.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/space_src/cli.rb', line 40 def self.env Thread.current[:space_src_cli_env] || ENV end |
.last_outcome ⇒ Object
50 51 52 |
# File 'lib/space_src/cli.rb', line 50 def self.last_outcome Thread.current[:space_src_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.
135 136 137 |
# File 'lib/space_src/cli.rb', line 135 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.
120 121 122 123 |
# File 'lib/space_src/cli.rb', line 120 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.
126 127 128 129 |
# File 'lib/space_src/cli.rb', line 126 def self.print_version(stdout) stdout.puts Space::Src::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/space_src/cli.rb', line 46 def self.record_outcome(outcome) Thread.current[:space_src_cli_outcome] = outcome end |
.run(argv, stdout, stderr) ⇒ Object
Entrypoint. Called by exe/src. 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/space_src/shell.rb).
86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/space_src/cli.rb', line 86 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) Migration.run(paths: make_paths, err: stderr) if (argv) paths = make_paths config = Config::Store.load(paths.config_file).success exit_code = Nav.dispatch(argv[0], stdout, stderr, config.base_dir) Kernel.exit(exit_code) end 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 |