Class: Mt::Wall::CLI

Inherits:
Object
  • Object
show all
Defined in:
lib/mt/wall/cli.rb

Overview

Command-line entry point driving the GitOps loop:

mt-wall validate <paths...>            # load + compile the DSL, no device
mt-wall plan     <paths...> [--device] # show the diff per device (read-only)
mt-wall apply    <paths...> [--device] # converge each device to desired

PATHS may be ‘*.rb` files and/or DIRECTORIES (a directory contributes every `*.rb` under it, recursively, in sorted order) — see Mt::Wall.load.

FLEET: plan/apply operate on EVERY device in the loaded Configuration by default. ‘–device NAME` may be REPEATED to target a subset. Output is a per-device section followed by a rollup summary line.

Typical CI wiring: ‘validate` + `plan` on every PR, `apply` on merge.

EXIT CODES (CI-friendly, Terraform-style; aggregated across the fleet):

0  success / no device has changes
1  error (invalid DSL, unknown device, transport/plan failure, bad args,
   apply aborted/refused, any device failed during apply)
2  `plan` ONLY: success, but at least one device has pending changes

SECRETS: credentials are NEVER passed on the command line. The per-device transport adapter (selected by the DSL ‘transport:`) reads them from ENV.

Constant Summary collapse

EXIT_OK =

rubocop:disable Metrics/ClassLength

0
EXIT_ERROR =
1
EXIT_CHANGES =

‘plan` returns this when the diff is non-empty so CI can branch on “there is work to apply” without parsing stdout.

2
ACTION_GLYPHS =

Glyphs prefixing each operation in a printed plan (Terraform-style).

{ create: "+", update: "~", delete: "-", move: "»" }.freeze
USAGE =
<<~USAGE
  Usage: mt-wall {validate|plan|apply} [options] <paths...>

    validate <paths...>             Load and compile the DSL; no device access.
    plan     <paths...> [--device]  Show the per-device diff (read-only).
    apply    <paths...> [--device]  Converge each device to the desired state.

  PATHS may be DSL files or directories (a directory loads every *.rb under
  it, recursively, in sorted order). plan/apply target ALL devices by default.

  Options:
    --device NAME   Limit plan/apply to device NAME (repeatable; default: all).
    --json          plan/validate: emit machine-readable JSON instead of text.
    --auto-approve  apply: skip the interactive confirmation (required for CI).
    -h, --help      Show this help.

  apply prompts for confirmation ('yes' to proceed) and REFUSES on a
  non-interactive stdin unless --auto-approve is given.

  Exit codes: 0 = success/no changes, 1 = error, 2 = plan has changes.
USAGE

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(out: $stdout, err: $stderr, input: $stdin, transport_factory: nil) ⇒ CLI

Returns a new instance of CLI.

Parameters:

  • out (IO) (defaults to: $stdout)

    stream for normal output (test seam)

  • err (IO) (defaults to: $stderr)

    stream for errors/usage (test seam)

  • input (IO) (defaults to: $stdin)

    stream the apply confirmation prompt reads from (test seam). Its ‘#tty?` decides whether stdin is interactive.

  • transport_factory (#call) (defaults to: nil)

    device -> Transport (test seam: inject a stub so plan/apply never touch a real device). Defaults to the DSL-driven adapter built from ‘device.transport`.



75
76
77
78
79
80
# File 'lib/mt/wall/cli.rb', line 75

def initialize(out: $stdout, err: $stderr, input: $stdin, transport_factory: nil)
  @out = out
  @err = err
  @input = input
  @transport_factory = transport_factory || method(:build_transport)
end

Class Method Details

.start(argv = ARGV) ⇒ Integer

Returns process exit status.

Returns:

  • (Integer)

    process exit status



64
65
66
# File 'lib/mt/wall/cli.rb', line 64

def self.start(argv = ARGV)
  new.run(argv)
end

Instance Method Details

#run(argv) ⇒ Integer

Returns process exit status.

Returns:

  • (Integer)

    process exit status



83
84
85
86
87
88
89
# File 'lib/mt/wall/cli.rb', line 83

def run(argv)
  dispatch(argv.dup)
rescue ConfigurationError, TransportError, PlanError, OptionParser::ParseError => e
  # Known, user-facing failures: a concise message, never a backtrace.
  @err.puts("Error: #{e.message}")
  EXIT_ERROR
end