# reflection/nulib/modes.nu — Mode listing, detail, rendering, and execution. use ./shared.nu [all-mode-files, project-root] use ./help.nu [help-group] use ../modules/store.nu [daemon-export, daemon-export-safe] # ── List / Show ────────────────────────────────────────────────────────────── export def list-modes []: nothing -> list { let mode_files = (all-mode-files) $mode_files | each { |mf| let m = (daemon-export-safe $mf) if $m != null { { id: ($m.id? | default ""), trigger: ($m.trigger? | default ""), steps: ($m.steps? | default [] | length), preconditions: ($m.preconditions? | default [] | length), } } else { null } } | compact } export def show-mode [id: string, fmt_resolved: string] { let mode_files = (all-mode-files) let candidates = ($mode_files | where { |p| ($p | path basename | str replace ".ncl" "") == $id }) if ($candidates | is-empty) { let available = ($mode_files | each { |p| $p | path basename | str replace ".ncl" "" }) error make { msg: $"Mode '($id)' not found. Available: ($available | str join ', ')" } } let m = (daemon-export ($candidates | first)) match $fmt_resolved { "json" => { $m | to json }, "yaml" => { $m | to yaml }, "toml" => { $m | to toml }, "table" => { $m | table --expand }, _ => { mode-to-md $m }, } } def mode-to-md [m: record]: nothing -> string { mut lines = [] $lines = ($lines | append $"# ($m.id)") $lines = ($lines | append "") $lines = ($lines | append $"**Trigger**: ($m.trigger)") $lines = ($lines | append "") if ($m.preconditions? | is-not-empty) and (($m.preconditions | length) > 0) { $lines = ($lines | append "## Preconditions") for p in $m.preconditions { $lines = ($lines | append $"- ($p)") } $lines = ($lines | append "") } $lines = ($lines | append "## Steps") for s in $m.steps { $lines = ($lines | append $"### ($s.id) [($s.actor)]") $lines = ($lines | append $s.action) if ($s.cmd? | is-not-empty) { $lines = ($lines | append $"```\n($s.cmd)\n```") } if ($s.depends_on? | is-not-empty) and (($s.depends_on | length) > 0) { $lines = ($lines | append $"Depends on: ($s.depends_on | each { |d| $d.step } | str join ', ')") } $lines = ($lines | append $"On error: ($s.on_error.strategy)") $lines = ($lines | append "") } if ($m.postconditions? | is-not-empty) and (($m.postconditions | length) > 0) { $lines = ($lines | append "## Postconditions") for p in $m.postconditions { $lines = ($lines | append $"- ($p)") } } $lines | str join "\n" } export def run-modes-interactive [modes: list] { let ids = ($modes | get id) let picked = ($ids | input list $"(ansi cyan_bold)Mode:(ansi reset) ") if ($picked | is-not-empty) { let actor = ($env.ONTOREF_ACTOR? | default "developer") let f = if $actor == "agent" { "json" } else { "md" } show-mode $picked $f } } # ── Authorization ──────────────────────────────────────────────────────────── # Load mode_run config from .ontoref/config.ncl. def load-mode-run-config [root: string]: nothing -> record { let defaults = { rules: [], default_allow: false, confirm_each_step: true, } let config_file = $"($root)/.ontoref/config.ncl" if not ($config_file | path exists) { return $defaults } let cfg = (daemon-export-safe $config_file) if $cfg == null { return $defaults } let mr = ($cfg.mode_run? | default {}) { rules: ($mr.rules? | default $defaults.rules), default_allow: ($mr.default_allow? | default $defaults.default_allow), confirm_each_step: ($mr.confirm_each_step? | default $defaults.confirm_each_step), } } # Load profile name from .ontoref/config.ncl. def load-profile [root: string]: nothing -> string { let config_file = $"($root)/.ontoref/config.ncl" if not ($config_file | path exists) { return "unknown" } let cfg = (daemon-export-safe $config_file) if $cfg == null { return "unknown" } $cfg.profile? | default "unknown" } # Check if a single rule's `when` clause matches the context. # All present fields must match (AND). Missing fields = don't care. def rule-matches [when_clause: record, context: record]: nothing -> bool { let profile_ok = if ($when_clause.profile? | is-not-empty) { ($when_clause.profile == $context.profile) } else { true } let actor_ok = if ($when_clause.actor? | is-not-empty) { ($when_clause.actor == $context.actor) } else { true } let mode_ok = if ($when_clause.mode_id? | is-not-empty) { ($when_clause.mode_id == $context.mode_id) } else { true } $profile_ok and $actor_ok and $mode_ok } # Evaluate authorization rules. Returns { allowed: bool, reason: string }. def authorize-mode [mode_id: string, root: string]: nothing -> record { let mr_cfg = (load-mode-run-config $root) let profile = (load-profile $root) let actor = ($env.ONTOREF_ACTOR? | default "developer") let context = { profile: $profile, actor: $actor, mode_id: $mode_id } # First matching rule wins. mut result = { allowed: $mr_cfg.default_allow, reason: "no matching rule — default policy" } for rule in $mr_cfg.rules { if (rule-matches $rule.when $context) { let reason = if ($rule.reason? | is-not-empty) { $rule.reason } else { "matched rule" } $result = { allowed: $rule.allow, reason: $reason } break } } $result } # ── Runner ─────────────────────────────────────────────────────────────────── # Load a mode by id, returning the full NCL-exported record. def load-mode [id: string]: nothing -> record { let mode_files = (all-mode-files) let candidates = ($mode_files | where { |p| ($p | path basename | str replace ".ncl" "") == $id }) if ($candidates | is-empty) { let available = ($mode_files | each { |p| $p | path basename | str replace ".ncl" "" }) error make { msg: $"Mode '($id)' not found. Available: ($available | str join ', ')" } } daemon-export ($candidates | first) } # Check if the current actor can execute a step based on the step's actor field. def actor-can-run-step [step_actor: string]: nothing -> bool { let current = ($env.ONTOREF_ACTOR? | default "developer") match $step_actor { "Both" => true, "Human" => ($current == "developer" or $current == "admin"), "Agent" => ($current == "agent" or $current == "ci"), _ => true, } } # Execute a single step's command. Returns { success: bool, output: string }. def exec-step-cmd [cmd: string]: nothing -> record { let nu_patterns = ["| from json", "| get ", "| where ", "| each ", "| select ", "| sort-by"] let is_nu = ($nu_patterns | any { |p| $cmd | str contains $p }) let result = if $is_nu { do { ^nu -c $cmd } | complete } else { do { ^bash -c $cmd } | complete } { success: ($result.exit_code == 0), output: (if $result.exit_code == 0 { $result.stdout } else { $result.stderr }), } } # Print step summary before execution. def print-step-header [step: record, index: int, total: int] { let actor_color = match ($step.actor? | default "Both") { "Human" => (ansi yellow), "Agent" => (ansi magenta), _ => (ansi cyan), } print $" (ansi white_bold)\(($index + 1)/($total)\)(ansi reset) (ansi cyan_bold)($step.id)(ansi reset) (ansi dark_gray)[($actor_color)($step.actor? | default 'Both')(ansi reset)(ansi dark_gray)](ansi reset)" print $" ($step.action? | default '')" if ($step.cmd? | is-not-empty) { print $" (ansi dark_gray)$ ($step.cmd)(ansi reset)" } } # Run a mode: authorize → preconditions → steps → postconditions. export def run-mode [id: string, --dry-run, --yes] { let root = (project-root) # ── Load mode ── let m = (load-mode $id) # ── Authorization ── let auth = (authorize-mode $id $root) if not $auth.allowed { let profile = (load-profile $root) let actor = ($env.ONTOREF_ACTOR? | default "developer") let steps = ($m.steps? | default []) print "" print $" (ansi white_bold)($m.id)(ansi reset) (ansi dark_gray)($steps | length) steps(ansi reset)" print $" ($m.trigger? | default '')" print "" # Show steps with their commands so the user knows what to run manually. if ($steps | is-not-empty) { print $" (ansi white_bold)Steps \(manual execution\):(ansi reset)" for step in ($steps | enumerate) { let s = $step.item let cmd_display = if ($s.cmd? | is-not-empty) { $"(ansi dark_gray)$ ($s.cmd)(ansi reset)" } else { $"(ansi dark_gray)— informational(ansi reset)" } print $" (ansi dark_gray)($step.index + 1).(ansi reset) (ansi cyan)($s.id)(ansi reset) [($s.actor? | default 'Both')] ($cmd_display)" print $" ($s.action? | default '')" } print "" } print $" (ansi red_bold)DENIED(ansi reset) Automated execution blocked." print $" (ansi dark_gray)Reason: ($auth.reason)(ansi reset)" print $" (ansi dark_gray)Context: profile=($profile), actor=($actor)(ansi reset)" print $" (ansi dark_gray)Configure in .ontoref/config.ncl → mode_run.rules(ansi reset)" print "" return } let steps = ($m.steps? | default []) let preconditions = ($m.preconditions? | default []) let postconditions = ($m.postconditions? | default []) let mr_cfg = (load-mode-run-config $root) print "" print $" (ansi green_bold)AUTHORIZED(ansi reset) ($auth.reason)" print $" (ansi white_bold)Mode:(ansi reset) ($m.id) (ansi dark_gray)($steps | length) steps(ansi reset)" print $" ($m.trigger? | default '')" print "" # ── Preconditions ── if ($preconditions | is-not-empty) { print $" (ansi white_bold)Preconditions(ansi reset)" for p in $preconditions { print $" (ansi dark_gray)●(ansi reset) ($p)" } print "" } if $dry_run { print $" (ansi yellow_bold)DRY RUN(ansi reset) — showing steps without executing" print "" for step in ($steps | enumerate) { print-step-header $step.item $step.index ($steps | length) print "" } if ($postconditions | is-not-empty) { print $" (ansi white_bold)Postconditions(ansi reset)" for p in $postconditions { print $" (ansi dark_gray)●(ansi reset) ($p)" } } return } # ── Execute steps ── mut failed_steps = [] for step in ($steps | enumerate) { let s = $step.item let idx = $step.index # Skip steps whose actor doesn't match current actor. if not (actor-can-run-step ($s.actor? | default "Both")) { print $" (ansi dark_gray)SKIP(ansi reset) ($s.id) (ansi dark_gray)[requires ($s.actor)](ansi reset)" continue } # Check depends_on: if a dependency failed and its kind is OnSuccess, skip. let deps = ($s.depends_on? | default []) mut dep_blocked = false for dep in $deps { let dep_kind = ($dep.kind? | default "OnSuccess") if $dep_kind == "OnSuccess" and ($failed_steps | any { |f| $f == $dep.step }) { $dep_blocked = true break } } if $dep_blocked { print $" (ansi yellow)BLOCKED(ansi reset) ($s.id) (ansi dark_gray)— dependency failed(ansi reset)" continue } print-step-header $s $idx ($steps | length) # No command = informational step, just display. if ($s.cmd? | is-empty) or ($s.cmd == "") { print $" (ansi dark_gray)no command — informational step(ansi reset)" print "" continue } # Confirm if required and not --yes. if $mr_cfg.confirm_each_step and (not $yes) { let answer = (input $" (ansi cyan)Execute? [y/N/q](ansi reset) " | str trim | str downcase) if $answer == "q" { print $" (ansi yellow)Aborted by user.(ansi reset)" return } if $answer != "y" { print $" (ansi dark_gray)skipped(ansi reset)" print "" continue } } # Execute. let result = (exec-step-cmd $s.cmd) if $result.success { print $" (ansi green)OK(ansi reset)" if ($result.output | is-not-empty) { let trimmed = ($result.output | str trim) if ($trimmed | is-not-empty) { $trimmed | lines | each { |l| print $" (ansi dark_gray)│(ansi reset) ($l)" } | ignore } } } else { let strategy = ($s.on_error? | default {} | get -o strategy | default "Stop") $failed_steps = ($failed_steps | append $s.id) if $strategy == "Stop" { print $" (ansi red_bold)FAILED(ansi reset) (ansi dark_gray)— strategy: Stop(ansi reset)" if ($result.output | is-not-empty) { $result.output | str trim | lines | each { |l| print $" (ansi red)│(ansi reset) ($l)" } | ignore } print "" print $" (ansi red)Execution halted at step ($s.id).(ansi reset)" return } else { print $" (ansi yellow)FAILED(ansi reset) (ansi dark_gray)— strategy: Continue(ansi reset)" if ($result.output | is-not-empty) { $result.output | str trim | lines | each { |l| print $" (ansi yellow)│(ansi reset) ($l)" } | ignore } } } print "" } # ── Postconditions ── if ($postconditions | is-not-empty) { print $" (ansi white_bold)Postconditions(ansi reset)" for p in $postconditions { print $" (ansi dark_gray)●(ansi reset) ($p)" } print "" } let fail_count = ($failed_steps | length) if $fail_count == 0 { print $" (ansi green_bold)COMPLETE(ansi reset) All steps executed successfully." } else { let step_word = if $fail_count == 1 { "step" } else { "steps" } print $" (ansi yellow_bold)PARTIAL(ansi reset) ($fail_count) ($step_word) failed: ($failed_steps | str join ', ')" } print "" }