///|
pub struct CommandResult {
exit_code : Int
output : String
} derive(Eq, @debug.Debug)
///|
pub fn CommandResult::exit_code(self : CommandResult) -> Int {
self.exit_code
}
///|
pub fn CommandResult::output(self : CommandResult) -> String {
self.output
}
///|
fn command_result(exit_code : Int, output : String) -> CommandResult {
{ exit_code, output, }
}
///|
fn usage() -> String {
"MoonChange - repository change-governance gates\n\n" +
"USAGE:\n" +
" moonchange audit --policy FILE --manifest FILE\n" +
" moonchange audit-diff --policy FILE --evidence FILE --diff FILE\n" +
" moonchange explain --policy FILE --path REPOSITORY_PATH\n" +
" moonchange lint --policy FILE\n" +
" moonchange compare --before FILE --after FILE --paths FILE\n" +
" moonchange demo [--format text|json]\n\n" +
"All report commands accept --format text|json (default: text).\n\n" +
"EXIT CODES:\n" +
" 0 passed, 1 needs review or has lint warnings, 2 rejected or invalid input"
}
///|
fn cli_option(
args : Array[String],
name : String,
) -> Result[String, Diagnostic] {
let mut found : String? = None
let mut index = 1
while index < args.length() {
if args[index] == name {
if found is Some(_) {
return Err(
Diagnostic::new(
"cli.option.duplicate", name, "option appears more than once", "one value",
"duplicate",
),
)
}
if index + 1 >= args.length() {
return Err(
Diagnostic::new(
"cli.option.value", name, "option is missing its value", "value after option",
"missing",
),
)
}
found = Some(args[index + 1])
index = index + 2
} else {
index = index + 1
}
}
match found {
Some(value) => Ok(value)
None =>
Err(
Diagnostic::new(
"cli.option.required",
name,
"required option is missing",
name + " VALUE",
"missing",
),
)
}
}
///|
fn cli_optional_option(
args : Array[String],
name : String,
) -> Result[String?, Diagnostic] {
match cli_option(args, name) {
Ok(value) => Ok(Some(value))
Err(error) if error.code() == "cli.option.required" => Ok(None)
Err(error) => Err(error)
}
}
///|
fn cli_output_format(args : Array[String]) -> Result[String, Diagnostic] {
match cli_optional_option(args, "--format") {
Ok(Some("text")) | Ok(None) => Ok("text")
Ok(Some("json")) => Ok("json")
Ok(Some(value)) =>
Err(
Diagnostic::new(
"cli.format.invalid", "--format", "output format is unsupported", "text or json",
value,
),
)
Err(error) => Err(error)
}
}
///|
fn cli_check_options(
args : Array[String],
allowed : Array[String],
) -> Result[Unit, Diagnostic] {
let mut index = 1
while index < args.length() {
let name = args[index]
if !name.has_prefix("--") {
return Err(
Diagnostic::new(
"cli.argument.unexpected",
"args[" + index.to_string() + "]",
"unexpected positional argument",
"named option",
name,
),
)
}
if !contains_string(allowed, name) {
return Err(
Diagnostic::new(
"cli.option.unknown",
"args[" + index.to_string() + "]",
"unknown option",
allowed.join(","),
name,
),
)
}
if index + 1 >= args.length() {
return Err(
Diagnostic::new(
"cli.option.value", name, "option is missing its value", "value after option",
"missing",
),
)
}
index = index + 2
}
Ok(())
}
///|
fn read_cli_file(
path : String,
option_name : String,
) -> Result[String, Diagnostic] {
let content = @fs.read_file_to_string(path) catch {
@fs.IOError(message) =>
return Err(
Diagnostic::new(
"cli.file.read",
option_name,
"cannot read UTF-8 input file",
"readable file",
path + ": " + message,
),
)
}
Ok(content)
}
///|
fn load_policy_option(
args : Array[String],
option_name : String,
) -> Result[Policy, Diagnostic] {
let path = match cli_option(args, option_name) {
Ok(value) => value
Err(error) => return Err(error)
}
let source = match read_cli_file(path, option_name) {
Ok(value) => value
Err(error) => return Err(error)
}
parse_policy(source)
}
///|
fn load_policy(args : Array[String]) -> Result[Policy, Diagnostic] {
load_policy_option(args, "--policy")
}
///|
fn gate_exit(status : GateStatus) -> Int {
match status {
GateStatus::Passed => 0
Review => 1
Rejected => 2
}
}
///|
fn audit_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
match cli_check_options(args, ["--policy", "--manifest", "--format"]) {
Ok(_) => ()
Err(error) => return Err(error)
}
let policy = match load_policy(args) {
Ok(value) => value
Err(error) => return Err(error)
}
let manifest_path = match cli_option(args, "--manifest") {
Ok(value) => value
Err(error) => return Err(error)
}
let source = match read_cli_file(manifest_path, "--manifest") {
Ok(value) => value
Err(error) => return Err(error)
}
let changes = match parse_manifest(source) {
Ok(value) => value
Err(error) => return Err(error)
}
let report = evaluate(policy, changes)
let format = match cli_output_format(args) {
Ok(value) => value
Err(error) => return Err(error)
}
let output = if format == "json" {
report.to_json()
} else {
report.to_text()
}
Ok(command_result(gate_exit(report.status), output))
}
///|
fn audit_diff_command(
args : Array[String],
) -> Result[CommandResult, Diagnostic] {
match
cli_check_options(args, ["--policy", "--evidence", "--diff", "--format"]) {
Ok(_) => ()
Err(error) => return Err(error)
}
let policy = match load_policy(args) {
Ok(value) => value
Err(error) => return Err(error)
}
let evidence_path = match cli_option(args, "--evidence") {
Ok(value) => value
Err(error) => return Err(error)
}
let evidence_source = match read_cli_file(evidence_path, "--evidence") {
Ok(value) => value
Err(error) => return Err(error)
}
let review = match parse_review_evidence(evidence_source) {
Ok(value) => value
Err(error) => return Err(error)
}
let diff_path = match cli_option(args, "--diff") {
Ok(value) => value
Err(error) => return Err(error)
}
let diff_source = match read_cli_file(diff_path, "--diff") {
Ok(value) => value
Err(error) => return Err(error)
}
let changes = match
change_set_from_unified_diff(review.id, review.evidence, diff_source) {
Ok(value) => value
Err(error) => return Err(error)
}
let report = evaluate(policy, changes)
let format = match cli_output_format(args) {
Ok(value) => value
Err(error) => return Err(error)
}
let output = if format == "json" {
report.to_json()
} else {
report.to_text()
}
Ok(command_result(gate_exit(report.status), output))
}
///|
fn explain_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
match cli_check_options(args, ["--policy", "--path", "--format"]) {
Ok(_) => ()
Err(error) => return Err(error)
}
let policy = match load_policy(args) {
Ok(value) => value
Err(error) => return Err(error)
}
let path = match cli_option(args, "--path") {
Ok(value) => value
Err(error) => return Err(error)
}
let format = match cli_output_format(args) {
Ok(value) => value
Err(error) => return Err(error)
}
match policy.explain(path) {
Ok(value) =>
Ok(
command_result(
0,
if format == "json" {
value.to_explain_json()
} else {
value.to_text()
},
),
)
Err(error) => Err(error)
}
}
///|
fn lint_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
match cli_check_options(args, ["--policy", "--format"]) {
Ok(_) => ()
Err(error) => return Err(error)
}
let policy = match load_policy(args) {
Ok(value) => value
Err(error) => return Err(error)
}
let report = lint_policy(policy)
let exit_code = if report.has_errors() {
2
} else if report.is_clean() {
0
} else {
1
}
let format = match cli_output_format(args) {
Ok(value) => value
Err(error) => return Err(error)
}
let output = if format == "json" {
report.to_json()
} else {
report.to_text()
}
Ok(command_result(exit_code, output))
}
///|
fn compare_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
match
cli_check_options(args, ["--before", "--after", "--paths", "--format"]) {
Ok(_) => ()
Err(error) => return Err(error)
}
let before = match load_policy_option(args, "--before") {
Ok(value) => value
Err(error) => return Err(error)
}
let after = match load_policy_option(args, "--after") {
Ok(value) => value
Err(error) => return Err(error)
}
let paths_file = match cli_option(args, "--paths") {
Ok(value) => value
Err(error) => return Err(error)
}
let paths_source = match read_cli_file(paths_file, "--paths") {
Ok(value) => value
Err(error) => return Err(error)
}
let paths : Array[String] = []
for line in split_owned_lines(paths_source) {
if !line.is_empty() && !line.has_prefix("#") {
paths.push(line)
}
}
let format = match cli_output_format(args) {
Ok(value) => value
Err(error) => return Err(error)
}
match compare_policies(before, after, paths) {
Ok(report) =>
Ok(
command_result(
0,
if format == "json" {
report.to_json()
} else {
report.to_text()
},
),
)
Err(error) => Err(error)
}
}
///|
fn demo_command(args : Array[String]) -> Result[CommandResult, Diagnostic] {
match cli_check_options(args, ["--format"]) {
Ok(_) => ()
Err(error) => return Err(error)
}
let policy = match
parse_policy(
"MOONCHANGE_POLICY 1\nDEFAULT approvals=1 max_total=300 require_owned=yes\nOWNER **/* @maintainers\nOWNER src/security/** @security,@maintainers\nRULE source src/** approvals=1 checks=unit labels=code forbid=delete max_lines=200 release_note=no binary=deny\nRULE security src/security/** approvals=2 checks=security labels=security-reviewed forbid=rename max_lines=80 release_note=yes binary=deny",
) {
Ok(value) => value
Err(error) => return Err(error)
}
let changes = match
parse_manifest(
"MOONCHANGE 1\nID DEMO-1\nACTOR @alice\nAPPROVAL @maintainers\nAPPROVAL @security\nCHECK unit passed\nCHECK security passed\nLABEL code\nLABEL security-reviewed\nRELEASE_NOTE yes\nCHANGE modify src/security/auth.mbt 20 4 text",
) {
Ok(value) => value
Err(error) => return Err(error)
}
let report = evaluate(policy, changes)
let format = match cli_output_format(args) {
Ok(value) => value
Err(error) => return Err(error)
}
let output = if format == "json" {
report.to_json()
} else {
report.to_text()
}
Ok(command_result(gate_exit(report.status), output))
}
///|
pub fn execute(args : Array[String]) -> CommandResult {
if args.is_empty() ||
args[0] == "help" ||
args[0] == "--help" ||
args[0] == "-h" {
return command_result(0, usage())
}
let result = match args[0] {
"audit" => audit_command(args)
"audit-diff" => audit_diff_command(args)
"explain" => explain_command(args)
"lint" => lint_command(args)
"compare" => compare_command(args)
"demo" => demo_command(args)
command =>
Err(
Diagnostic::new(
"cli.command.unknown", "args[0]", "unknown command", "audit, audit-diff, explain, lint, compare, demo, or help",
command,
),
)
}
match result {
Ok(value) => value
Err(error) => command_result(2, error.to_text())
}
}