provisioning/tools/catalog/load-extensions.nu
2026-01-14 02:59:52 +00:00

303 lines
8.4 KiB
Plaintext

#!/usr/bin/env nu
# Load extension metadata and build Directed Acyclic Graph (DAG)
# Performs topological sort for initialization order
#
# Usage:
# nu load-extensions.nu --extensions-dir <path> --output-file <path>
# nu load-extensions.nu # Uses defaults
def main [
--extensions-dir: path = "extensions" # Path to extensions directory
--output-file: path = "config/extensions-dag.json" # Output DAG JSON file
--verbose # Enable verbose output
] {
print "🔍 Loading extension metadata..."
if $verbose { print $" Extensions dir: ($extensions_dir)" }
# Find all metadata.ncl files using glob (exclude polkadot deployment configs)
let metadata_files = (
glob ($extensions_dir + "/**/metadata.ncl")
| where { |f| not ($f | str contains "polkadot") }
| sort
)
if ($metadata_files | is-empty) {
print "⚠️ No extension metadata files found"
return 1
}
print $"📂 Found ($metadata_files | length) extensions"
# Load all metadata files
let extensions = (
$metadata_files
| each { |file|
try {
if $verbose { print $" Loading: ($file)" }
nickel export $file | from json
} catch {
print $"❌ Failed to load: ($file)"
error make { msg: $"Failed to export ($file)" }
}
}
)
print $"✅ Loaded ($extensions | length) extensions"
if $verbose {
print "\nExtension Summary:"
$extensions | each { |ext|
let deps_str = ($ext.dependencies | str join ', ')
if ($ext.dependencies | is-empty) {
print $" • ($ext.name) v($ext.version) [($ext.category)] - deps: []"
} else {
print $" • ($ext.name) v($ext.version) [($ext.category)] - deps: [($deps_str)]"
}
}
}
# Build DAG and detect cycles
let dag_result = (build_dag $extensions)
if ("error" in $dag_result) {
print $"❌ DAG Error: ($dag_result.error)"
return 1
}
let dag = $dag_result
# Topological sort
let sorted_result = (topological_sort $extensions $dag)
if ("error" in $sorted_result) {
print $"❌ Topological Sort Error: ($sorted_result.error)"
return 1
}
let init_order = $sorted_result
if $verbose {
print "\nInitialization Order (Topological Sort):"
$init_order | enumerate | each { |item|
print $" ($item.index). ($item.item)"
}
}
# Build output structure
let output = {
extensions: $extensions,
dag: $dag,
initialization_order: $init_order,
metadata: {
total_extensions: ($extensions | length),
exported_at: (date now | format date "%Y-%m-%dT%H:%M:%SZ"),
schema_version: "1.0",
statistics: (build_stats $extensions),
},
}
# Write output
let dir = $output_file | path dirname
if not ($dir | path exists) {
mkdir $dir
}
$output | to json --indent 2 | save --force $output_file
print $"✅ Extension DAG exported to: ($output_file)"
print $" Extensions: ($output.metadata.total_extensions)"
print $" Status: Ready for ai-service initialization"
0
}
# Build DAG from extension dependencies
def build_dag [extensions: list] {
let ext_names = ($extensions | each { |e| $e.name })
# Validate all dependencies exist
let invalid_deps = (
$extensions
| each { |ext|
$ext.dependencies
| each { |dep|
if not ($ext_names | any { |n| $n == $dep }) {
{extension: $ext.name, missing_dep: $dep}
} else {
null
}
}
| compact
}
| flatten
)
if not ($invalid_deps | is-empty) {
let missing = ($invalid_deps | each { |d| $"($d.extension)->($d.missing_dep)" } | str join ", ")
return {error: $"Invalid dependencies: ($missing)"}
}
# Build edges (node -> dependencies)
let edges = (
$extensions
| each { |ext|
$ext.dependencies
| each { |dep|
{from: $ext.name, to: $dep, label: "depends_on"}
}
}
| flatten
)
{
nodes: $ext_names,
edges: $edges,
}
}
# Topological sort using DFS
def topological_sort [extensions: list, dag: record] {
let ext_names = $dag.nodes
let edges = $dag.edges
# Build adjacency list (reverse: what depends on each node)
let adj_list = (
$ext_names
| each { |node|
let deps = (
$edges
| where { |e| $e.from == $node }
| each { |e| $e.to }
)
{node: $node, deps: $deps}
}
)
# Simple sort by counting in-degree and processing in order
# Extensions with no dependencies come first
let no_deps = ($adj_list | where { |n| ($n.deps | is-empty) } | each { |n| $n.node } | sort)
let with_deps = ($adj_list | where { |n| ($n.deps | length) > 0 } | each { |n| $n.node } | sort)
# Check for cycles (simple check: if an extension depends on something not in the list)
let all_ext_names = ($extensions | each { |e| $e.name })
let all_deps = (
$extensions
| each { |e| $e.dependencies }
| flatten
)
let invalid_deps = ($all_deps | where { |d| not ($all_ext_names | any { |n| $n == $d }) })
if not ($invalid_deps | is-empty) {
return {error: $"Invalid dependencies found: ($invalid_deps | str join ', ')"}
}
# Simple ordering: return no_deps first, then with_deps sorted by number of dependencies
let sorted = (
[$no_deps, (
$with_deps
| each { |name|
let dep_count = ($extensions | where { |e| $e.name == $name } | first | get dependencies | length)
{name: $name, dep_count: $dep_count}
}
| sort-by dep_count
| each { |n| $n.name }
)]
| flatten
)
$sorted
}
# Build statistics
def build_stats [extensions: list] {
let categories = ($extensions | each { |e| $e.category } | sort | uniq)
let by_category = (
$categories
| each { |cat|
let count = ($extensions | where { |e| $e.category == $cat } | length)
{category: $cat, count: $count}
}
)
let total_deps = (
$extensions
| each { |e| $e.dependencies | length }
| math sum
)
{
by_category: $by_category,
total_dependencies: $total_deps,
extension_count: ($extensions | length),
}
}
# Utility: List extensions by category
export def list_by_category [
--extensions-dir: path = "extensions"
] {
let metadata_files = (glob ($extensions_dir + "/**/metadata.ncl"))
let extensions = (
$metadata_files
| each { |file|
try {
nickel export $file | from json
} catch {
null
}
}
| compact
)
let categories = ($extensions | each { |e| $e.category } | sort | uniq)
$categories | each { |cat|
let exts = ($extensions | where { |e| $e.category == $cat })
print $"($cat):"
$exts | each { |ext|
print $" • ($ext.name) v($ext.version)"
}
print ""
}
}
# Utility: Check extension dependencies
export def check_dependencies [
--extensions-dir: path = "extensions"
] {
let metadata_files = (glob ($extensions_dir + "/**/metadata.ncl") | sort)
let extensions = (
$metadata_files
| each { |file|
nickel export $file | from json
}
)
print "🔍 Extension Dependency Check"
print "=============================="
print ""
let ext_names = ($extensions | each { |e| $e.name })
$extensions | each { |ext|
if ($ext.dependencies | is-empty) {
print $"✅ ($ext.name): No dependencies"
} else {
print $"($ext.name):"
$ext.dependencies | each { |dep|
if ($ext_names | any { |n| $n == $dep }) {
print $" ✅ → ($dep)"
} else {
print $" ❌ → ($dep) [NOT FOUND]"
}
}
}
print ""
}
}
main