203 lines
6.5 KiB
Plaintext
203 lines
6.5 KiB
Plaintext
|
|
#!/usr/bin/env nu
|
||
|
|
# reflection/modules/migrate.nu — ontoref protocol migration runner.
|
||
|
|
#
|
||
|
|
# Migrations live in $ONTOREF_ROOT/reflection/migrations/NNN-slug.ncl
|
||
|
|
# Each has: id, slug, description, check, instructions
|
||
|
|
# "Applied" = check passes. No state file — checks are the source of truth.
|
||
|
|
#
|
||
|
|
# Commands:
|
||
|
|
# migrate list — all migrations with applied/pending status
|
||
|
|
# migrate pending — only pending migrations
|
||
|
|
# migrate show <id> — instructions for a specific migration
|
||
|
|
|
||
|
|
use env.nu *
|
||
|
|
use store.nu [daemon-export-safe]
|
||
|
|
use describe.nu [nickel-import-path]
|
||
|
|
|
||
|
|
def project-root []: nothing -> string {
|
||
|
|
$env.ONTOREF_PROJECT_ROOT? | default $env.ONTOREF_ROOT
|
||
|
|
}
|
||
|
|
|
||
|
|
def migrations-dir []: nothing -> string {
|
||
|
|
[$env.ONTOREF_ROOT, "reflection", "migrations"] | path join
|
||
|
|
}
|
||
|
|
|
||
|
|
# Run a check record against the given project root. Returns { passed, detail }.
|
||
|
|
def run-migration-check [check: record, root: string]: nothing -> record {
|
||
|
|
match $check.tag {
|
||
|
|
"FileExists" => {
|
||
|
|
let p = ([$root, $check.path] | path join)
|
||
|
|
let exists = ($p | path exists)
|
||
|
|
let want = ($check.present? | default true)
|
||
|
|
{
|
||
|
|
passed: ($exists == $want),
|
||
|
|
detail: (if ($exists == $want) { "ok" } else if $want { $"missing: ($p)" } else { $"unexpected: ($p)" })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"Grep" => {
|
||
|
|
let paths = ($check.paths | each { |p| [$root, $p] | path join } | where { |p| $p | path exists })
|
||
|
|
if ($paths | is-empty) {
|
||
|
|
return { passed: false, detail: "no target paths exist" }
|
||
|
|
}
|
||
|
|
let r = do { ^rg --no-heading -l $check.pattern ...$paths } | complete
|
||
|
|
let has_matches = ($r.exit_code == 0)
|
||
|
|
let must_empty = ($check.must_be_empty? | default false)
|
||
|
|
if $must_empty {
|
||
|
|
let files = ($r.stdout | str trim)
|
||
|
|
{ passed: (not $has_matches), detail: (if $has_matches { $"pattern found in: ($files)" } else { "ok" }) }
|
||
|
|
} else {
|
||
|
|
{ passed: $has_matches, detail: (if $has_matches { "ok" } else { "required pattern absent" }) }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
"NuCmd" => {
|
||
|
|
let r = do { nu -c $check.cmd } | complete
|
||
|
|
let expected = ($check.expect_exit? | default 0)
|
||
|
|
{
|
||
|
|
passed: ($r.exit_code == $expected),
|
||
|
|
detail: (if ($r.exit_code == $expected) { "ok" } else { $"exit ($r.exit_code) ≠ ($expected): ($r.stderr | str trim | str substring 0..120)" })
|
||
|
|
}
|
||
|
|
}
|
||
|
|
_ => { passed: false, detail: $"unknown check tag: ($check.tag)" }
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
def load-all-migrations []: nothing -> list<record> {
|
||
|
|
let dir = (migrations-dir)
|
||
|
|
if not ($dir | path exists) { return [] }
|
||
|
|
let ip = (nickel-import-path $env.ONTOREF_ROOT)
|
||
|
|
glob $"($dir)/*.ncl"
|
||
|
|
| sort
|
||
|
|
| each { |f|
|
||
|
|
try {
|
||
|
|
let m = (daemon-export-safe $f --import-path $ip)
|
||
|
|
if $m != null { [$m] } else { [] }
|
||
|
|
} catch { [] }
|
||
|
|
}
|
||
|
|
| flatten
|
||
|
|
}
|
||
|
|
|
||
|
|
def migration-status [migrations: list<record>, root: string]: nothing -> list<record> {
|
||
|
|
$migrations | each { |m|
|
||
|
|
let r = (run-migration-check $m.check $root)
|
||
|
|
{
|
||
|
|
id: $m.id,
|
||
|
|
slug: $m.slug,
|
||
|
|
description: $m.description,
|
||
|
|
applied: $r.passed,
|
||
|
|
detail: $r.detail,
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# List all protocol migrations with applied/pending status.
|
||
|
|
export def "migrate list" [
|
||
|
|
--fmt (-f): string = "",
|
||
|
|
--actor (-a): string = "",
|
||
|
|
]: nothing -> nothing {
|
||
|
|
let root = (project-root)
|
||
|
|
let actor = if ($actor | is-not-empty) { $actor } else { $env.ONTOREF_ACTOR? | default "developer" }
|
||
|
|
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
|
||
|
|
|
||
|
|
let statuses = (migration-status (load-all-migrations) $root)
|
||
|
|
|
||
|
|
if $fmt == "json" {
|
||
|
|
print ($statuses | to json)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
let n_applied = ($statuses | where applied == true | length)
|
||
|
|
let n_total = ($statuses | length)
|
||
|
|
print $" migrations: ($n_applied)/($n_total) applied"
|
||
|
|
print ""
|
||
|
|
for s in $statuses {
|
||
|
|
let mark = if $s.applied { "✓" } else { "○" }
|
||
|
|
print $" ($mark) ($s.id) ($s.slug)"
|
||
|
|
print $" ($s.description)"
|
||
|
|
if not $s.applied and $s.detail != "ok" {
|
||
|
|
print $" ($s.detail)"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# List only pending (not yet applied) migrations.
|
||
|
|
export def "migrate pending" [
|
||
|
|
--fmt (-f): string = "",
|
||
|
|
--actor (-a): string = "",
|
||
|
|
]: nothing -> nothing {
|
||
|
|
let root = (project-root)
|
||
|
|
let actor = if ($actor | is-not-empty) { $actor } else { $env.ONTOREF_ACTOR? | default "developer" }
|
||
|
|
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
|
||
|
|
|
||
|
|
let pending = (
|
||
|
|
migration-status (load-all-migrations) $root
|
||
|
|
| where applied == false
|
||
|
|
)
|
||
|
|
|
||
|
|
if $fmt == "json" {
|
||
|
|
print ($pending | to json)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
if ($pending | is-empty) {
|
||
|
|
print " all migrations applied"
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
print $" ($pending | length) pending:"
|
||
|
|
for p in $pending {
|
||
|
|
print $" ○ ($p.id) ($p.slug) — ($p.description)"
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
# Show the instructions for a specific migration. Accepts id ("0001") or slug.
|
||
|
|
export def "migrate show" [
|
||
|
|
id: string,
|
||
|
|
--fmt (-f): string = "",
|
||
|
|
--actor (-a): string = "",
|
||
|
|
]: nothing -> nothing {
|
||
|
|
let root = (project-root)
|
||
|
|
let actor = if ($actor | is-not-empty) { $actor } else { $env.ONTOREF_ACTOR? | default "developer" }
|
||
|
|
let fmt = if ($fmt | is-not-empty) { $fmt } else if $actor == "agent" { "json" } else { "text" }
|
||
|
|
|
||
|
|
let migrations = (load-all-migrations)
|
||
|
|
let id_norm = ($id | str replace --regex '^0+' '')
|
||
|
|
let id_norm = if $id_norm == "" { "0" } else { $id_norm }
|
||
|
|
let matching = ($migrations | where { |m| $m.id == $id or $m.slug == $id or (($m.id | str replace --regex '^0+' '') == $id_norm) })
|
||
|
|
if ($matching | is-empty) {
|
||
|
|
error make { msg: $"migration '($id)' not found" }
|
||
|
|
}
|
||
|
|
let m = ($matching | first)
|
||
|
|
let r = (run-migration-check $m.check $root)
|
||
|
|
|
||
|
|
# Interpolate runtime values into instruction text
|
||
|
|
let project_name = ($root | path basename)
|
||
|
|
let instructions = ($m.instructions
|
||
|
|
| str replace --all "{project_root}" $root
|
||
|
|
| str replace --all "{project_name}" $project_name
|
||
|
|
| str replace --all "PROJECT_NAME" $project_name
|
||
|
|
| str replace --all "$ONTOREF_PROJECT_ROOT" $root
|
||
|
|
)
|
||
|
|
|
||
|
|
if $fmt == "json" {
|
||
|
|
print ({
|
||
|
|
id: $m.id,
|
||
|
|
slug: $m.slug,
|
||
|
|
description: $m.description,
|
||
|
|
applied: $r.passed,
|
||
|
|
project_root: $root,
|
||
|
|
project_name: $project_name,
|
||
|
|
instructions: $instructions,
|
||
|
|
} | to json)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
let status = if $r.passed { "✓ applied" } else { "○ pending" }
|
||
|
|
print $" ($m.id) ($m.slug) — ($status)"
|
||
|
|
print $" ($m.description)"
|
||
|
|
if not $r.passed and $r.detail != "ok" {
|
||
|
|
print $" check: ($r.detail)"
|
||
|
|
}
|
||
|
|
print ""
|
||
|
|
print $instructions
|
||
|
|
}
|