# Cache Core — reads from the shared plugin cache directory. # Written by ncl-sync daemon; read by this module and nu_plugin_nickel. # Single writer principle: Nu NEVER writes to the cache dir directly. # cache/metadata star-import was dead — dropped (ADR-025 Phase 3 Layer 2). # Check if a directory has workspace markers. def is-ws-dir [path: string]: nothing -> bool { if ($path | is-empty) or (not ($path | path exists)) { return false } let has_infra = ($path | path join "infra" | path exists) let has_config = ($path | path join "config" "provisioning.ncl" | path exists) let has_onto = ($path | path join ".ontology" | path exists) $has_infra or $has_config or $has_onto } # Walk up from PWD to find workspace root (recursive). def find-ws-up [path: string]: nothing -> string { if ($path | is-empty) or $path == "/" { return "" } if (is-ws-dir $path) { return $path } let parent = ($path | path dirname) if $parent == $path { return "" } find-ws-up $parent } # Global cache directory (shared across workspaces, for files under $PROVISIONING). def get-global-cache-dir []: nothing -> string { let home = ($env.HOME? | default "~" | path expand) let host_info = (do { sys host } | complete) let is_mac = if $host_info.exit_code == 0 { ($host_info.stdout | get name | str downcase | str contains "darwin") or ($host_info.stdout | get name | str downcase | str contains "macos") } else { ($home | path join "Library" | path exists) } if $is_mac { $home | path join "Library" "Caches" "provisioning" "config-cache" } else { $home | path join ".cache" "provisioning" "config-cache" } } # Resolve cache directory FOR A SPECIFIC FILE. Priority: # 1. $NCL_CACHE_DIR (explicit override, for CI/tests) # 2. File under $PROVISIONING → global cache (extensions, schemas — shared) # 3. File under a workspace (walk up from file path) → /.ncl-cache/ # 4. Fallback: global cache # # Must match resolve_cache_dir_for_file() in ncl-sync + plugin. def get-cache-dir-for-file [file_path: string]: nothing -> string { if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR } # File under $PROVISIONING → global cache let prov = ($env.PROVISIONING? | default "") if ($prov | is-not-empty) and ($file_path | str starts-with $prov) { return (get-global-cache-dir) } # File under a workspace → workspace-local cache let ws = (find-ws-up ($file_path | path dirname)) if ($ws | is-not-empty) { return ($ws | path join ".ncl-cache") } get-global-cache-dir } # Legacy helper (CWD-based) — kept for backwards compat in code paths that don't have # the file path at hand. Prefer get-cache-dir-for-file. def get-cache-base-dir []: nothing -> string { if ($env.NCL_CACHE_DIR? | is-not-empty) { return $env.NCL_CACHE_DIR } let ws = (find-ws-up $env.PWD) if ($ws | is-not-empty) { return ($ws | path join ".ncl-cache") } get-global-cache-dir } # Lookup a cache entry by pre-computed key. # Only "nickel" type is backed by the shared plugin cache. # Returns: { valid: bool, reason: string, data: any } export def cache-lookup [ cache_type: string cache_key: string --ttl: int = 0 ]: nothing -> record { if $cache_type != "nickel" { return { valid: false, reason: "type_not_supported", data: null } } let cache_file = ((get-cache-base-dir) | path join $"($cache_key).json") if not ($cache_file | path exists) { return { valid: false, reason: "cache_miss", data: null } } let result = (do { open $cache_file } | complete) if $result.exit_code != 0 { return { valid: false, reason: "read_error", data: null } } { valid: true, reason: "hit", data: ($result.stdout | from json) } } # Signal ncl-sync daemon to (re-)export a list of NCL files. # Nu never writes to the cache directly — only signals the daemon. # Uses pid-unique sidecar + atomic rename to prevent concurrent-write corruption. export def cache-write [ cache_type: string cache_key: string data: any source_files: list --ttl: int = 0 ]: nothing -> nothing { if $cache_type != "nickel" { return } write-sync-request ($source_files | each {|f| { path: $f, import_paths: [] }}) } # Write a sync-request sidecar file for ncl-sync to process. # Each Nu process writes .sync-.tmp then renames to .sync-.json atomically. export def write-sync-request [ requests: list # list of {path: string, import_paths: list} ]: nothing -> nothing { let cache_dir = (get-cache-base-dir) if not ($cache_dir | path exists) { return } let pid = $nu.pid let tmp_file = ($cache_dir | path join $".sync-($pid).tmp") let json_file = ($cache_dir | path join $".sync-($pid).json") $requests | to json | save --force $tmp_file ^mv $tmp_file $json_file } # Cache stats — count entries and total size in the shared cache dir. export def get-cache-stats []: nothing -> record { let cache_dir = (get-cache-base-dir) if not ($cache_dir | path exists) { return { total_entries: 0, total_size_mb: 0.0, by_type: {} } } let files = (do { ls $cache_dir } | complete) if $files.exit_code != 0 { return { total_entries: 0, total_size_mb: 0.0, by_type: {} } } let entries = ($files.stdout | where name =~ '\.json$' | where name !~ 'manifest' | length) let size_bytes = ($files.stdout | where name =~ '\.json$' | get size | math sum) { total_entries: $entries, total_size_mb: ($size_bytes / 1_048_576 | math round -p 2), by_type: { nickel: $entries } } } # Clear the shared cache directory (removes all .json files except manifest). export def cache-clear-type [cache_type: string]: nothing -> nothing { if $cache_type != "nickel" { return } let cache_dir = (get-cache-base-dir) if not ($cache_dir | path exists) { return } do { ls $cache_dir | where name =~ '\.json$' | where name !~ 'manifest' | each {|f| rm $f.name} } | ignore } # No-op — eviction is handled by ncl-sync daemon. export def cleanup-expired-cache [max_size_mb: int = 100]: nothing -> nothing {}