diff --git a/nushell/modules/pj_completions.nu b/nushell/modules/pj_completions.nu index 4177422..05d4bcd 100644 --- a/nushell/modules/pj_completions.nu +++ b/nushell/modules/pj_completions.nu @@ -4,6 +4,9 @@ const pj_completion_base_dir = ($pj_cache_dir | path join 'nu_scripts' 'custom-c const pj_completion_commands = [git ssh cargo uv rustup make pytest rg tar vscode winget zig] const pj_completion_noop_module = ($pj_cache_dir | path join 'noop.nu') const pj_completion_registry_module = ($pj_cache_dir | path join 'pj_completions_registry.nu') +const pj_completion_marker_root = ($pj_cache_dir | path join '.pj-completion-ok') +const pj_completion_version = (version).version +const pj_completion_marker_dir = ($pj_completion_marker_root | path join $pj_completion_version) const pj_completion_bootstrap_module = if ($pj_completion_registry_module | path exists) { $pj_completion_registry_module } else { @@ -14,6 +17,54 @@ def _pj_completion_script_path [name: string] { ($pj_completion_base_dir | path join $name $"($name)-completions.nu") } +def _pj_completion_marker_path [name: string] { + ($pj_completion_marker_dir | path join $name) +} + +# Check whether a completion script can still be parsed by the running Nushell. +# Third-party completion scripts occasionally use syntax that a newer Nushell +# rejects; such files must never be pulled into the config or the whole shell +# fails to start. +def _pj_completion_file_valid [file: string] { + if not ($file | path exists) { + return false + } + let probe = (do -i { ^$nu.current-exe --no-config-file -c $"use \"($file)\"" } | complete) + ($probe.exit_code == 0) +} + +# Ensure per-version validity markers exist. A marker is only written for a +# completion file that parses under the current Nushell version, so the generated +# registry can safely gate each `use` on its marker. +def _pj_completion_ensure_markers [] { + if ($pj_completion_marker_dir | path exists) { + return {created: false, invalid: []} + } + + mkdir $pj_completion_marker_dir + let results = ($pj_completion_commands | each {|name| + let file = (_pj_completion_script_path $name) + let valid = (_pj_completion_file_valid $file) + if $valid { + touch (_pj_completion_marker_path $name) + } + {command: $name, valid: $valid} + }) + + let invalid = ($results | where {|it| not $it.valid } | get command) + {created: true, invalid: $invalid} +} + +def _pj_completion_report [result: record] { + if ($result.invalid | is-empty) { + print "[pj completion] all completion files are compatible with the current Nushell." + } else { + print "[pj completion] warning: incompatible completion files were disabled:" + $result.invalid | each {|name| print $" - ($name)" } + print "[pj completion] run `pj completion -u` to refresh the cached nu_scripts." + } +} + def _pj_required_completion_files [] { $pj_completion_commands | each {|name| { @@ -27,16 +78,20 @@ def _pj_completion_registry_lines [] { let header = [ "# Auto-generated by `pj completion sync`." "# Edit pj_completion_commands in pj_completions.nu, then re-run sync." + "# Each module is only loaded when it was validated for the running Nushell version." "const pj_registry_dir = (path self | path dirname)" "const pj_completion_base_dir = ($pj_registry_dir | path join 'nu_scripts' 'custom-completions')" "const pj_completion_noop_module = ($pj_registry_dir | path join 'noop.nu')" + "const pj_completion_marker_dir = ($pj_registry_dir | path join '.pj-completion-ok' (version).version)" "" ] let lines = ($pj_completion_commands | each {|name| [ ( - "export use (if ((($pj_completion_base_dir | path join '" + "export use (if ((($pj_completion_marker_dir | path join '" + + $name + + "') | path exists) and (($pj_completion_base_dir | path join '" + $name + "' '" + $name @@ -55,15 +110,17 @@ def _pj_completion_registry_lines [] { def _pj_completion_help [] { print "pj completion commands:" - print " pj completion Check required completion files from existing cache." + print " pj completion Validate cached completion files for the current Nushell." print " pj completion -u Update cache, or clone nu_scripts when cache is missing." print " pj completion -f Force re-clone cached nu_scripts repository." - print " pj completion sync Regenerate cache registry and restart Nushell." + print " pj completion sync Regenerate cache registry, revalidate, and restart Nushell." print " pj completion -s Same as `pj completion sync`." + print " pj completion doctor Show which completion files are enabled or disabled." print " pj completion -h Show this help message." } export def "pj completion sync" [ + --no-restart(-n) # Regenerate the registry without restarting Nushell. --help(-h) ] { if $help { @@ -75,14 +132,46 @@ export def "pj completion sync" [ let content = ((_pj_completion_registry_lines) | str join "\n") $content | save -f $pj_completion_registry_module print $"[pj completion] synced: ($pj_completion_registry_module)" - print "[pj completion] restarting Nushell..." - exec nu + + # Invalidate validation markers so files are re-checked against the current Nushell. + if ($pj_completion_marker_root | path exists) { + rm -rf $pj_completion_marker_root + } + let ensure = (_pj_completion_ensure_markers) + _pj_completion_report $ensure + + if not $no_restart { + print "[pj completion] restarting Nushell..." + exec nu + } +} + +# Show which completion files are enabled or disabled for the current Nushell version. +export def "pj completion doctor" [] { + let enabled = ($pj_completion_commands | where {|name| (_pj_completion_marker_path $name) | path exists }) + let disabled = ($pj_completion_commands | where {|name| not ((_pj_completion_marker_path $name) | path exists) }) + print $"Nushell version: ($pj_completion_version)" + print $"Marker dir: ($pj_completion_marker_dir)" + print "Enabled:" + if ($enabled | is-empty) { + print " (none)" + } else { + $enabled | each {|name| print $" - ($name)" } + } + print "Disabled:" + if ($disabled | is-empty) { + print " (none)" + } else { + $disabled | each {|name| print $" - ($name)" } + } + null } export def "pj completion" [ --update(-u) # Update cached repo, or clone when cache is missing. --force(-f) # Force re-clone cached nu_scripts repository. --sync(-s) # Regenerate static registry and restart Nushell. + --no-restart(-n) # Used with --sync: regenerate without restarting Nushell. --help(-h) # Show help. ] { if $help { @@ -91,7 +180,7 @@ export def "pj completion" [ } if $sync { - pj completion sync + pj completion sync --no-restart=($no_restart) return } @@ -154,6 +243,13 @@ export def "pj completion" [ $missing | each {|it| print $" - ($it.command): ($it.file)" } } + # The cache changed, so previously validated markers are no longer trustworthy. + if ($pj_completion_marker_root | path exists) { + rm -rf $pj_completion_marker_root + } + let ensure = (_pj_completion_ensure_markers) + _pj_completion_report $ensure + print "[pj completion] ok" { cache_dir: $cache_dir @@ -161,6 +257,7 @@ export def "pj completion" [ completion_dir: $completion_dir commands: $pj_completion_commands missing_files: $missing + disabled: $ensure.invalid } } @@ -178,14 +275,22 @@ export-env { } }) - if not ($pj_completion_registry_module | path exists) { + let registry_missing = (not ($pj_completion_registry_module | path exists)) + if $registry_missing { mkdir $pj_cache_dir let content = ((_pj_completion_registry_lines) | str join "\n") $content | save -f $pj_completion_registry_module + } + # A marker is only written when a file parses under the current Nushell, so a + # broken (outdated) third-party completion can never stop the shell from loading. + let ensure = (_pj_completion_ensure_markers) + + if ($registry_missing or $ensure.created) { if (($env.PJ_COMPLETION_BOOTSTRAPPED? | default "0") != "1") { load-env { PJ_COMPLETION_BOOTSTRAPPED: "1" } - print "[pj completion] generated cache registry, restarting Nushell..." + _pj_completion_report $ensure + print "[pj completion] restarting Nushell..." exec nu } } diff --git a/nushell/modules/pj_prompt.nu b/nushell/modules/pj_prompt.nu index 59c9181..54bd1c6 100644 --- a/nushell/modules/pj_prompt.nu +++ b/nushell/modules/pj_prompt.nu @@ -1,6 +1,6 @@ def _pj_detect_os_key [] { let os_name = ($nu.os-info.name? | default "" | into string | str lowercase) - if ($os_name | str contains "darwin") { + if (($os_name | str contains "macos") or ($os_name | str contains "darwin") or ($os_name | str contains "mac")) { return "Macos" } if ($os_name | str contains "windows") {