ontoref-derive: #[onto_mcp_tool] attribute macro registers MCP tool unit-structs in
the catalog at link time via inventory::submit!; annotated item is emitted unchanged,
ToolBase/AsyncTool impls stay on the struct. All 34 tools migrated from manual wiring
(net +5: ontoref_list_projects, ontoref_search, ontoref_describe,
ontoref_list_ontology_extensions, ontoref_get_ontology_extension).
validate modes (ADR-018): reads level_hierarchy from workflow.ncl and checks every
.ncl mode for level declared, strategy declared, delegate chain coherent, compose
extends valid. mode resolve <id> shows which hierarchy level handles a mode and why.
--self-test generates synthetic fixtures in a temp dir for CI smoke-testing.
validate run-cargo: two-step Cargo.toml resolution — workspace layout first
(crates/<check.crate>/Cargo.toml), single-crate fallback by package name or repo
basename. Lets the same ADR constraint shape apply to workspace and single-crate repos.
ontology/schemas/manifest.ncl: registry_topology_type contract — multi-registry
coordination, push targets, participant scopes, per-namespace capability.
reflection/requirements/base.ncl: oras ≥1.2.0, cosign ≥2.0.0, sops ≥3.9.0, age
≥1.1.0, restic declared as Hard/Soft requirements with version_min, check_cmd, and
install_hint (ADR-017 toolchain surface).
ADR-019: per-file recipient routing for tenant isolation without multi-vault. Schema
additions: sops.recipient_groups + sops.recipient_rules in ontoref-project.ncl.
secrets-bootstrap generates .sops.yaml from project.ncl in declarative mode. Three
new secrets-audit checks: recipient-routing-coherent, recipient-routing-coverage,
no-multi-vault. Adoption templates: single-team/, multi-tenant/, agent-first/.
Integration templates: domain-producer/, mode-producer/, mode-consumer/.
UI: project_picker surfaces registry badge (⟳ participant) and vault badge
(⛁ vault_id · N, green=declarative / amber=legacy) per project card. Expanded panel
adds collapsible Registry section with namespace, endpoint, and push/pull capability.
manage.html gains Runtime Services card — MCP and GraphQL toggleable without restart
via HTMX POST /ui/manage/services/{service}/toggle.
describe.nu: capabilities JSON includes registry_topology and vault_state per project.
sync.nu: drift check extended to detect //! absence on newly registered crates.
qa.ncl: six entries — credential-vault-best-practice (layered data-flow diagram),
credential-vault-templates (paths A/B/C), credential-vault-troubleshooting (15 named
errors), integration-what-and-why (ADR-042 OCI federation), integration-how-to-implement,
integration-troubleshooting.
on+re: core.ncl + manifest.ncl updated to reflect OCI, MCP, and mode-hierarchy nodes.
Deleted stale presentation assets (2026-02 slides + voice notes).
514 lines
18 KiB
Text
514 lines
18 KiB
Text
#!/usr/bin/env nu
|
|
# reflection/modules/workflow.nu — workflow layer model generator.
|
|
#
|
|
# Reads .ontology/workflow.ncl (WorkflowDeclaration) and generates implementation
|
|
# artifacts for each declared provider. Layers are independent — generating one
|
|
# layer never affects another.
|
|
#
|
|
# Commands:
|
|
# workflow validate — typecheck workflow.ncl against schema
|
|
# workflow generate — generate all providers for all layers
|
|
# workflow generate --layer <id> — generate one layer
|
|
# workflow generate --provider <tag> — generate one provider type across all layers
|
|
# workflow diff — show what would change without writing
|
|
# workflow list — list declared layers + providers
|
|
|
|
use env.nu *
|
|
use store.nu [daemon-export-safe]
|
|
|
|
# ── Helpers ──────────────────────────────────────────────────────────────────
|
|
|
|
# Join args with space, trimming trailing whitespace.
|
|
# Prevents trailing-space commands like `markdownlint-cli2 `.
|
|
def args-str [args: list<string>]: nothing -> string {
|
|
$args | str join ' '
|
|
}
|
|
|
|
# Build a tool invocation string from tool name + args, trimmed.
|
|
def tool-cmd [tool: string, args: list<string>]: nothing -> string {
|
|
let base = if ($tool == "cargo-nextest") {
|
|
$"cargo nextest (args-str $args)"
|
|
} else if ($tool == "cargo-deny") {
|
|
$"cargo deny (args-str $args)"
|
|
} else if ($tool == "cargo-geiger") {
|
|
$"cargo geiger (args-str $args)"
|
|
} else if ($tool == "cargo-sbom") {
|
|
$"cargo sbom (args-str $args)"
|
|
} else if ($tool =~ "^cargo-") {
|
|
let subcmd = ($tool | str replace "cargo-" "")
|
|
$"cargo ($subcmd) (args-str $args)"
|
|
} else if ($tool == "cargo") {
|
|
$"cargo (args-str $args)"
|
|
} else {
|
|
$"($tool) (args-str $args)"
|
|
}
|
|
$base | str trim --right
|
|
}
|
|
|
|
def project-root []: nothing -> string {
|
|
$env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT
|
|
}
|
|
|
|
def workflow-ncl-path [root: string]: nothing -> string {
|
|
[$root, ".ontology", "workflow.ncl"] | path join
|
|
}
|
|
|
|
def nickel-import-path [root: string]: nothing -> string {
|
|
let entries = [
|
|
$"($root)/.ontology"
|
|
$"($root)/reflection/schemas"
|
|
$"($root)/reflection/defaults"
|
|
$"($root)/adrs"
|
|
$root
|
|
$"($env.ONTOREF_ROOT)/reflection/schemas"
|
|
$"($env.ONTOREF_ROOT)/reflection/defaults"
|
|
$env.ONTOREF_ROOT
|
|
]
|
|
($entries | where { |p| $p | path exists } | uniq | str join ":")
|
|
}
|
|
|
|
def load-workflow [root: string]: nothing -> record {
|
|
let ncl = (workflow-ncl-path $root)
|
|
if not ($ncl | path exists) {
|
|
error make { msg: $"workflow.ncl not found at ($ncl) — run: ore workflow generate --help" }
|
|
}
|
|
let ip = (nickel-import-path $root)
|
|
let result = do { ^nickel export --import-path $ip $ncl } | complete
|
|
if $result.exit_code != 0 {
|
|
error make { msg: $"workflow.ncl export failed:\n($result.stderr)" }
|
|
}
|
|
$result.stdout | from json
|
|
}
|
|
|
|
# Resolve an ID to its spec from a catalog record; error with catalog-name context.
|
|
def resolve-spec [id: string, catalog: record, kind: string]: nothing -> record {
|
|
if ($catalog | get -o $id | is-not-empty) {
|
|
$catalog | get $id
|
|
} else {
|
|
error make { msg: $"($kind) '($id)' not found in catalog" }
|
|
}
|
|
}
|
|
|
|
def resolve-validation [id: string, catalog: record]: nothing -> record {
|
|
resolve-spec $id $catalog "validation"
|
|
}
|
|
|
|
def resolve-build [id: string, catalog: record]: nothing -> record {
|
|
resolve-spec $id $catalog "build"
|
|
}
|
|
|
|
# Resolve `{TARGET_DIR}` and `{ROOT}` placeholders in BuildSpec.artifacts paths.
|
|
# {TARGET_DIR} → `cargo metadata --format-version 1 --no-deps | .target_directory`
|
|
# {ROOT} → workspace root
|
|
# Falls back to "$root/target" if cargo metadata is unavailable.
|
|
def resolve-artifact-path [path: string, root: string]: nothing -> string {
|
|
let meta = do { cd $root; ^cargo metadata --format-version 1 --no-deps } | complete
|
|
let target_dir = if $meta.exit_code == 0 {
|
|
$meta.stdout | from json | get target_directory
|
|
} else {
|
|
$"($root)/target"
|
|
}
|
|
$path
|
|
| str replace --all "{TARGET_DIR}" $target_dir
|
|
| str replace --all "{ROOT}" $root
|
|
}
|
|
|
|
# ── Pre-commit generator ─────────────────────────────────────────────────────
|
|
|
|
# Translate a ValidationSpec into a pre-commit hook entry (YAML fragment).
|
|
def validation-to-precommit [v: record]: nothing -> string {
|
|
let entry_cmd = (tool-cmd $v.tool $v.args)
|
|
|
|
let env_prefix = if ($v.tool == "cargo" and ($v.args | any { |a| $a == "doc" })) {
|
|
'RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links -D rustdoc::private-intra-doc-links" '
|
|
} else {
|
|
""
|
|
}
|
|
|
|
let types_line = if ($v.when | is-empty) {
|
|
""
|
|
} else {
|
|
# Map glob patterns to pre-commit type hints or regex `files:`.
|
|
# Dots are escaped so `Cargo.toml` does not match `CargoXtoml`.
|
|
let has_rust = ($v.when | any { |p| $p =~ '\.rs' })
|
|
let has_md = ($v.when | any { |p| $p =~ '\.md' })
|
|
if $has_rust {
|
|
"\n types: [rust]"
|
|
} else if $has_md {
|
|
"\n types: [markdown]"
|
|
} else {
|
|
# Convert glob → Python re.search() pattern.
|
|
# Order matters: escape literal dots first, then convert glob * to regex .*
|
|
let files_pat = ($v.when | each { |p|
|
|
$p | str replace --all "." "\\." | str replace --all "*" ".*"
|
|
} | str join "|")
|
|
$"\n files: '($files_pat)'"
|
|
}
|
|
}
|
|
|
|
$" - id: ($v.id)
|
|
name: ($v.id)
|
|
entry: bash -c '($env_prefix)($entry_cmd)'
|
|
language: system
|
|
pass_filenames: false($types_line)
|
|
stages: [pre-commit]"
|
|
}
|
|
|
|
def generate-precommit [wf: record, stage: string, dry_run: bool, root: string]: nothing -> nothing {
|
|
let layers = ($wf.layers | where { |l|
|
|
($l.providers | any { |p| $p.tag == "PreCommit" and ($p.stage? | default "pre-commit") == $stage })
|
|
})
|
|
|
|
if ($layers | is-empty) {
|
|
print $" no layers with PreCommit/($stage) provider — skipping"
|
|
return
|
|
}
|
|
|
|
# Collect all validation IDs across matching layers (deduplicated, order preserved)
|
|
mut seen = []
|
|
mut specs = []
|
|
for layer in $layers {
|
|
for vid in $layer.validations {
|
|
if not ($seen | any { |s| $s == $vid }) {
|
|
$seen = ($seen | append $vid)
|
|
$specs = ($specs | append (resolve-validation $vid $wf.validations))
|
|
}
|
|
}
|
|
}
|
|
|
|
let hook_entries = ($specs | each { |v| validation-to-precommit $v } | str join "\n")
|
|
let section = $" - repo: local
|
|
hooks:
|
|
($hook_entries)"
|
|
|
|
let out_path = ([$root, ".pre-commit-config.yaml"] | path join)
|
|
|
|
if $dry_run {
|
|
print $"[dry-run] would update pre-commit hooks in ($out_path):"
|
|
print $section
|
|
return
|
|
}
|
|
|
|
# If file exists, replace the '- repo: local' block managed by workflow.
|
|
# If not, write a minimal pre-commit config.
|
|
if ($out_path | path exists) {
|
|
print $" pre-commit config exists — merge is not automatic"
|
|
print $" copy the following into the 'repo: local' section of ($out_path):"
|
|
print ""
|
|
print $section
|
|
} else {
|
|
let content = $"repos:
|
|
($section)
|
|
"
|
|
$content | save $out_path
|
|
print $" wrote ($out_path)"
|
|
}
|
|
}
|
|
|
|
# ── Woodpecker generator ─────────────────────────────────────────────────────
|
|
|
|
# Map a tool name to the shell command that installs it in a CI container.
|
|
def tool-install-cmd [tool: string]: nothing -> string {
|
|
match $tool {
|
|
"cargo" => "",
|
|
"cargo-nextest" => "cargo install cargo-nextest --locked",
|
|
"cargo-deny" => "cargo install cargo-deny --locked",
|
|
"cargo-geiger" => "cargo install cargo-geiger --locked",
|
|
"cargo-sbom" => "cargo install cargo-sbom --locked",
|
|
"cargo-llvm-cov" => "cargo install cargo-llvm-cov --locked",
|
|
"cross" => "cargo install cross --locked",
|
|
"nickel" => "cargo install nickel-lang-cli --locked",
|
|
"nu" => "cargo install nu --locked",
|
|
"markdownlint-cli2" => "npm install -g markdownlint-cli2",
|
|
_ => $"# install ($tool)",
|
|
}
|
|
}
|
|
|
|
# Map a tool to its required container image for CI.
|
|
# Most tools run from rust:latest; node-based tools need a node image.
|
|
def tool-image [tool: string]: nothing -> string {
|
|
match $tool {
|
|
"markdownlint-cli2" => "node:lts",
|
|
_ => "rust:latest",
|
|
}
|
|
}
|
|
|
|
# Build the CI command for a ValidationSpec.
|
|
# `nu --ide-check` requires a filename — the CI version wraps it in a find loop.
|
|
def validation-ci-cmd [v: record]: nothing -> string {
|
|
if $v.tool == "nu" and ($v.args | any { |a| $a == "--ide-check" }) {
|
|
let depth = ($v.args | window 2 | where { |w| $w.0 == "--ide-check" } | first | default ["--ide-check","100"] | last | default "100")
|
|
$"find . -name '*.nu' ! -path '*/target/*' -print0 | xargs -0 -I\\{\\} nu --ide-check ($depth) \\{\\}"
|
|
} else if ($v.tool == "cargo" and ($v.args | any { |a| $a == "doc" })) {
|
|
let cmd = (tool-cmd $v.tool $v.args)
|
|
$'RUSTDOCFLAGS="-D rustdoc::broken-intra-doc-links -D rustdoc::private-intra-doc-links" ($cmd)'
|
|
} else {
|
|
tool-cmd $v.tool $v.args
|
|
}
|
|
}
|
|
|
|
def validation-to-woodpecker-step [v: record]: nothing -> string {
|
|
let install = (tool-install-cmd $v.tool)
|
|
let install_lines = if ($install | is-not-empty) { $" - ($install)\n" } else { "" }
|
|
let image = (tool-image $v.tool)
|
|
let cmd = (validation-ci-cmd $v)
|
|
|
|
$" ($v.id):
|
|
image: ($image)
|
|
commands:
|
|
($install_lines) - ($cmd)"
|
|
}
|
|
|
|
def build-to-woodpecker-step [b: record]: nothing -> string {
|
|
let install = (tool-install-cmd $b.tool)
|
|
let install_lines = if ($install | is-not-empty) { $" - ($install)\n" } else { "" }
|
|
let image = (tool-image $b.tool)
|
|
let cmd = (tool-cmd $b.tool $b.args)
|
|
$" build-($b.id):
|
|
image: ($image)
|
|
commands:
|
|
($install_lines) - ($cmd)"
|
|
}
|
|
|
|
def trigger-to-woodpecker-when [trigger: string]: nothing -> string {
|
|
match $trigger {
|
|
"OnCommit" => " event: [push, manual]",
|
|
"OnPush" => " event: [push, manual]",
|
|
"OnPR" => " event: [push, pull_request, manual]",
|
|
"OnMainMerge" => " event: [push, tag, manual]\n branch: main",
|
|
"OnTag" => " event: [tag, manual]",
|
|
"OnManual" => " event: [manual]",
|
|
_ => " event: [push, pull_request, manual]",
|
|
}
|
|
}
|
|
|
|
def generate-woodpecker [layer: record, wf: record, file: string, dry_run: bool, root: string]: nothing -> nothing {
|
|
let when_block = (trigger-to-woodpecker-when $layer.trigger)
|
|
|
|
# Collect unique tools needed → depends_on for test after lint
|
|
let val_specs = ($layer.validations | each { |vid| resolve-validation $vid $wf.validations })
|
|
let build_specs = ($layer.builds | each { |bid| resolve-build $bid $wf.builds })
|
|
|
|
let lint_steps = ($val_specs | where { |v| $v.kind == "Lint" or $v.kind == "Compliance" })
|
|
let test_steps = ($val_specs | where { |v| $v.kind == "Test" })
|
|
let sec_steps = ($val_specs | where { |v| $v.kind == "Security" })
|
|
let doc_steps = ($val_specs | where { |v| $v.kind == "Docs" })
|
|
|
|
let lint_ids = ($lint_steps | get id)
|
|
let depends = if ($lint_ids | is-empty) { "" } else {
|
|
let ids_str = ($lint_ids | each { |id| $"\"($id)\"" } | str join ", ")
|
|
$"\n depends_on: [($ids_str)]"
|
|
}
|
|
|
|
mut steps_yaml = ""
|
|
|
|
# Lint steps (parallel)
|
|
for v in $lint_steps {
|
|
$steps_yaml = $steps_yaml + (validation-to-woodpecker-step $v) + "\n\n"
|
|
}
|
|
|
|
# Test steps (depends on lint)
|
|
for v in $test_steps {
|
|
let step = (validation-to-woodpecker-step $v)
|
|
$steps_yaml = $steps_yaml + $step + $depends + "\n environment:\n RUST_BACKTRACE: 1\n\n"
|
|
}
|
|
|
|
# Security steps (parallel — cargo deny / geiger do not need prior compilation)
|
|
for v in $sec_steps {
|
|
$steps_yaml = $steps_yaml + (validation-to-woodpecker-step $v) + "\n\n"
|
|
}
|
|
|
|
# Docs steps (depends on lint — can run in parallel with tests)
|
|
for v in $doc_steps {
|
|
let step = (validation-to-woodpecker-step $v)
|
|
$steps_yaml = $steps_yaml + $step + $depends + "\n\n"
|
|
}
|
|
|
|
# Build steps (depends on test)
|
|
for b in $build_specs {
|
|
let step = (build-to-woodpecker-step $b)
|
|
$steps_yaml = $steps_yaml + $step + $depends + "\n\n"
|
|
}
|
|
|
|
let yaml = $"# Generated by ore workflow generate — layer: ($layer.id)
|
|
# Source: .ontology/workflow.ncl
|
|
# Do not edit manually — regenerate with: ore workflow generate --layer ($layer.id)
|
|
|
|
when:
|
|
($when_block)
|
|
|
|
steps:
|
|
($steps_yaml)"
|
|
|
|
let out_path = ([$root, $file] | path join)
|
|
let out_dir = ($out_path | path dirname)
|
|
|
|
if $dry_run {
|
|
print $"[dry-run] would write ($out_path):"
|
|
print $yaml
|
|
return
|
|
}
|
|
|
|
if not ($out_dir | path exists) { mkdir $out_dir }
|
|
($yaml | str trim --right) + "\n" | save --force $out_path
|
|
print $" wrote ($out_path)"
|
|
}
|
|
|
|
# ── Justfile generator ───────────────────────────────────────────────────────
|
|
|
|
def generate-justfile [layers: list, wf: record, dry_run: bool, root: string]: nothing -> nothing {
|
|
let jf_layers = ($layers | where { |l|
|
|
$l.providers | any { |p| $p.tag == "Justfile" }
|
|
})
|
|
|
|
if ($jf_layers | is-empty) {
|
|
print " no layers with Justfile provider — skipping"
|
|
return
|
|
}
|
|
|
|
mut recipes = $"# Generated by ore workflow generate
|
|
# Source: .ontology/workflow.ncl
|
|
|
|
"
|
|
|
|
for layer in $jf_layers {
|
|
let recipe_name = ($layer.providers
|
|
| where { |p| $p.tag == "Justfile" }
|
|
| first
|
|
| get recipe)
|
|
|
|
let val_specs = ($layer.validations | each { |vid| resolve-validation $vid $wf.validations })
|
|
let build_specs = ($layer.builds | each { |bid| resolve-build $bid $wf.builds })
|
|
|
|
mut body = ""
|
|
for v in $val_specs {
|
|
$body = $body + $" (tool-cmd $v.tool $v.args)\n"
|
|
}
|
|
for b in $build_specs {
|
|
$body = $body + $" (tool-cmd $b.tool $b.args)\n"
|
|
}
|
|
|
|
$recipes = $recipes + $"# layer: ($layer.id)\n($recipe_name):\n($body)\n"
|
|
}
|
|
|
|
let out_path = ([$root, "justfiles", "workflow.just"] | path join)
|
|
if $dry_run {
|
|
print $"[dry-run] would write ($out_path)"
|
|
print $recipes
|
|
return
|
|
}
|
|
if not ($out_path | path dirname | path exists) { mkdir ($out_path | path dirname) }
|
|
($recipes | str trim --right) + "\n" | save --force $out_path
|
|
print $" wrote ($out_path)"
|
|
}
|
|
|
|
# ── Public commands ──────────────────────────────────────────────────────────
|
|
|
|
# Validate .ontology/workflow.ncl against the schema (nickel typecheck).
|
|
export def "workflow validate" [
|
|
--root: string # project root (default: ONTOREF_PROJECT_ROOT)
|
|
]: nothing -> nothing {
|
|
let r = ($root | default (project-root))
|
|
let ncl = (workflow-ncl-path $r)
|
|
let ip = (nickel-import-path $r)
|
|
print $"Validating ($ncl)..."
|
|
let result = do { ^nickel typecheck --import-path $ip $ncl } | complete
|
|
if $result.exit_code == 0 {
|
|
print " workflow.ncl OK"
|
|
} else {
|
|
error make { msg: $"workflow.ncl typecheck failed:\n($result.stderr)" }
|
|
}
|
|
}
|
|
|
|
# List declared layers and their providers.
|
|
export def "workflow list" [
|
|
--root: string
|
|
]: nothing -> table {
|
|
let r = ($root | default (project-root))
|
|
let wf = (load-workflow $r)
|
|
$wf.layers | each { |l|
|
|
{
|
|
id: $l.id,
|
|
trigger: $l.trigger,
|
|
validations: ($l.validations | length),
|
|
builds: ($l.builds | length),
|
|
providers: ($l.providers | each { |p| $p.tag } | str join ", "),
|
|
}
|
|
}
|
|
}
|
|
|
|
# Generate implementation artifacts from .ontology/workflow.ncl.
|
|
export def "workflow generate" [
|
|
--root: string # project root (default: ONTOREF_PROJECT_ROOT)
|
|
--layer: string # generate only this layer ID
|
|
--provider: string # generate only this provider tag (PreCommit, Woodpecker, Justfile)
|
|
--dry-run # show what would be written without writing
|
|
]: nothing -> nothing {
|
|
let r = ($root | default (project-root))
|
|
let wf = (load-workflow $r)
|
|
let dry = $dry_run
|
|
|
|
let target_layers = if ($layer | is-not-empty) {
|
|
let found = ($wf.layers | where { |l| $l.id == $layer })
|
|
if ($found | is-empty) {
|
|
error make { msg: $"layer '($layer)' not found in workflow.ncl" }
|
|
}
|
|
$found
|
|
} else {
|
|
$wf.layers
|
|
}
|
|
|
|
print $"Generating workflow artifacts from ($r)/.ontology/workflow.ncl"
|
|
|
|
mut justfile_needed = false
|
|
|
|
for layer in $target_layers {
|
|
print $"\n layer: ($layer.id) [($layer.trigger)]"
|
|
|
|
for prov in $layer.providers {
|
|
let tag = $prov.tag
|
|
if ($provider | is-not-empty) and $tag != $provider { continue }
|
|
|
|
match $tag {
|
|
"PreCommit" => {
|
|
let stage = ($prov | get -o stage | default "pre-commit")
|
|
print $" provider: PreCommit/($stage)"
|
|
generate-precommit $wf $stage $dry $r
|
|
},
|
|
"Woodpecker" => {
|
|
let file = $prov.file
|
|
print $" provider: Woodpecker → ($file)"
|
|
generate-woodpecker $layer $wf $file $dry $r
|
|
},
|
|
"Justfile" => {
|
|
# Defer: justfiles/workflow.just aggregates ALL Justfile layers.
|
|
# Generate once after the loop to avoid clobbering on multi-layer runs.
|
|
$justfile_needed = true
|
|
print " provider: Justfile → justfiles/workflow.just [deferred to post-loop]"
|
|
},
|
|
_ => {
|
|
print $" provider: ($tag) — not yet implemented"
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
# Justfile generation is hoisted here so it runs once with the full layer set.
|
|
if $justfile_needed {
|
|
print "\n generating justfiles/workflow.just"
|
|
generate-justfile $target_layers $wf $dry $r
|
|
}
|
|
|
|
print "\nDone."
|
|
}
|
|
|
|
# Show what would change without writing any files.
|
|
export def "workflow diff" [
|
|
--root: string
|
|
--layer: string
|
|
--provider: string
|
|
]: nothing -> nothing {
|
|
workflow generate --root ($root | default (project-root)) --dry-run
|
|
...(if ($layer | is-not-empty) { [--layer $layer] } else { [] })
|
|
...(if ($provider | is-not-empty) { [--provider $provider] } else { [] })
|
|
}
|