diff --git a/docs/design/README.md b/docs/design/README.md index 224d3c4..a8c260b 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -1,8 +1,8 @@ -# Design of `nupm` :warning: Work In Progress :warning: +# Design of `nupm` :warning: Work In Progress :warning: This file collects design ideas and directions. The intention is iterate on this document by PRs with discussion. -> **Note** +> **Note** > in the following, until we settle down on precise names, we use the following placeholders: > - `METADATA_FILE`: the file containing the metadata of a package, > e.g. `project.nuon`, `metadata.json` or `nupm.nuon` @@ -13,7 +13,7 @@ This file collects design ideas and directions. The intention is iterate on this # Table of content - [Project Structure](#project-structure-toc) - [Separate virtual environments](#separate-virtual-environments-toc) -- [Installation, bootstraping](#installation-bootstraping-toc) +- [Installation, bootstrapping](#installation-bootstrapping-toc) - [Dependency handling](#dependency-handling-toc) - [Package repository](#package-repository-toc) - [API / CLI Interface](#api--cli-interface-toc) @@ -24,7 +24,7 @@ This file collects design ideas and directions. The intention is iterate on this A `nupm` project is defined by `METADATA_FILE`. This is where you define name of the project, version, dependencies, etc., and the type of the project. -> **Note** +> **Note** > see [`METADATA.md`](references/METADATA.md) for a more in-depth description of > the `METADATA_FILE` @@ -73,9 +73,9 @@ The overlays could be used to achieve all three goals at the same time. When ins * This file can be installed into a global location that's in your `NU_LIB_DIRS` (e.g., `NUPM_HOME/overlays`) -- now you have a global Python-like virtual environment * Each overlay under `NUPM_HOME/overlays` will mimic the main NUPM_HOME structure, e.g., for an overlay `spam` there will be `NUPM_HOME/overlays/spam/bin`, `NUPM_HOME/overlays/spam/modules` (`NUPM_HOME/overlays/spam/overlays`? It might not be the best idea to have it recursive) -Each package would basically have its own overlay. This overlay file (it's just a module) could be used to also handle dependencies. If your project depends on `foo` and `bar` which both depend on `spam` but different versions, they could both import the different verions privately in their own overlay files and in your project's overlay file would be just `export use path/to/foo` and `export use path/to/bar`. This should prevent name clashing of `spam`. The only problem that needs to be figured out is how to tell `foo` to be aware of its overlay. +Each package would basically have its own overlay. This overlay file (it's just a module) could be used to also handle dependencies. If your project depends on `foo` and `bar` which both depend on `spam` but different versions, they could both import the different versions privately in their own overlay files and in your project's overlay file would be just `export use path/to/foo` and `export use path/to/bar`. This should prevent name clashing of `spam`. The only problem that needs to be figured out is how to tell `foo` to be aware of its overlay. -## Installation, bootstraping [[toc](#table-of-content)] +## Installation, bootstrapping [[toc](#table-of-content)] Requires these actions from the user (this should be kept as minimal as possible): * Add `NUPM_HOME/bin` to PATH (install location for binary projects) @@ -83,7 +83,7 @@ Requires these actions from the user (this should be kept as minimal as possible * Add `NUPM_HOME/overlays` to NU_LIB_DIRS * Make the `nupm` command available somehow (e.g., `use` inside `config.nu`) -> :warning: **WIP** +> :warning: **WIP** > The disadvantage of this is that the default install location is not an overlay. We could make `nupm` itself an overlay that adds itself as a command, so that you can activate/deactivate it. We might need a few attempts to get to the right solution. There are several approaches: @@ -98,7 +98,7 @@ There are several approaches: In compiled programming languages, there are two kinds of dependencies: static and dynamic. Static are included statically and compiled when compiling the project, dynamic are pre-compiled libraries linked to the project. -> **Note** +> **Note** > Nushell is [similar to compiled languages][Nushell compiled] rather than typical dynamic languages like Python, so these concepts are relevant for Nushell. Static dependencies: @@ -120,7 +120,7 @@ as long as it has `METADATA_FILE` telling `nupm` what to do. Nushell's module design conflates CLI interface with API -- they are the same. Not all of the below are of the same priority. -> **Note** +> **Note** > commands like `list`, `install`, `search`, `uninstall`, `update`, ..., i.e. should > - print short descriptions by default > - print long descriptions with `--long-description (-l)` @@ -161,7 +161,7 @@ Nushell's module design conflates CLI interface with API -- they are the same. N - publish package to a repository - **NOT SUPPORTED FOR NOW**: the repository will be a *GitHub* repo with packages submitted by PRs to start with -The following are for Python-style global overlays, we might need to re-think this for local package overlays: +The following are for Python-style global overlays, we might need to re-think this for local package overlays: - `nupm overlay new` - create a new global overlay (Python's virtual environment style) - `--local` flag could generate an overlay locally from the currently opened project @@ -178,7 +178,7 @@ The following are for Python-style global overlays, we might need to re-think th ### Other CLI-related notes [[toc](#table-of-content)] * We could later think about being able to extend `nupm`, like `cargo` has plugins. -* Mutable actions (like install) have by default Y/n prompt, but can be overriden with `--yes` +* Mutable actions (like install) have by default Y/n prompt, but can be overridden with `--yes` * By default, new projects are cross-platform: * Windows * MacOS diff --git a/docs/design/registry.md b/docs/design/registry.md index dc225ed..1bc8e38 100644 --- a/docs/design/registry.md +++ b/docs/design/registry.md @@ -92,7 +92,7 @@ _See the new `registry/` directory, the following example slightly differs from It is possible to only publish to a registry stored on your file system because we don't have a web service or anything like that. -The intented workflow for publishing a package is: +The intended workflow for publishing a package is: 1. Check out the git repository with the registry 2. `cd` into the package you want to publish 3. Run `nupm publish chosen_registry` to preview the changes diff --git a/nupm/install.nu b/nupm/install.nu index e880e8c..213582b 100644 --- a/nupm/install.nu +++ b/nupm/install.nu @@ -1,5 +1,5 @@ use utils/completions.nu complete-registries -use utils/dirs.nu [ nupm-home-prompt cache-dir module-dir script-dir tmp-dir ] +use utils/dirs.nu [ nupm-home-prompt cache-dir module-dir script-dir tmp-dir PACKAGE_FILENAME ] use utils/log.nu throw-error use utils/misc.nu [check-cols hash-fn url] use utils/package.nu open-package-file @@ -106,7 +106,7 @@ def install-path [ do { cd $tmp_dir - ^$nu.current-exe $build_file ($pkg_dir | path join 'nupm.nuon') + ^$nu.current-exe $build_file ($pkg_dir | path join $PACKAGE_FILENAME) } rm -rf $tmp_dir diff --git a/nupm/mod.nu b/nupm/mod.nu index de9f88a..9b4f79d 100644 --- a/nupm/mod.nu +++ b/nupm/mod.nu @@ -2,15 +2,19 @@ use std/log use utils/dirs.nu [ DEFAULT_NUPM_HOME DEFAULT_NUPM_TEMP DEFAULT_NUPM_CACHE - DEFAULT_NUPM_REGISTRIES nupm-home-prompt + DEFAULT_NUPM_REGISTRIES DEFAULT_NUPM_INDEX_PATH nupm-home-prompt ] +use utils/registry.nu open-index + export module install.nu export module publish.nu +export module registry.nu export module search.nu export module status.nu export module test.nu + export-env { # Ensure that $env.NUPM_HOME is always set when running nupm. Any missing # $env.NUPM_HOME during nupm execution is a bug. @@ -22,12 +26,12 @@ export-env { # Ensure install cache is set $env.NUPM_CACHE = ($env.NUPM_CACHE? | default $DEFAULT_NUPM_CACHE) - # TODO: Maybe this is not the best way to set registries, but should be - # good enough for now. - # TODO: Add `nupm registry` for showing info about registries - # TODO: Add `nupm registry add/remove` to add/remove registry from the env? - $env.NUPM_REGISTRIES = ($env.NUPM_REGISTRIES? - | default $DEFAULT_NUPM_REGISTRIES) + # check for the index path + $env.NUPM_INDEX_PATH = ($env.NUPM_INDEX_PATH? | default $DEFAULT_NUPM_INDEX_PATH) + + # read from registry index but don't overwrite registires already present in $env.NUPM_REGISTRIES + $env.NUPM_REGISTRIES = ($env.NUPM_INDEX_PATH | open-index + | merge ($env.NUPM_REGISTRIES? | default $DEFAULT_NUPM_REGISTRIES)) use std/log [] } diff --git a/nupm/registry.nu b/nupm/registry.nu new file mode 100644 index 0000000..fd57f9c --- /dev/null +++ b/nupm/registry.nu @@ -0,0 +1,191 @@ +# Registry management for nupm + +use utils/dirs.nu [nupm-home-prompt REGISTRY_FILENAME] +use utils/log.nu [throw-error append-help UNIMPLEMENTED] +use utils/registry.nu registry-cache + +# Manage nupm registires +@example "List all configured registries" { nupm registry } +export def main []: nothing -> table { + list +} + +# List all configured registries +@example "List all registries with details" { nupm registry list } +export def list []: nothing -> table { + $env.NUPM_REGISTRIES | transpose name url | sort-by name +} + +# Formatting convenience function that is passed in the presence of the `--save` flag, informing the user on +# how to make their changes permanent +export def success-msg [success_msg: string, write_path: path, saved_to_disk: bool]: nothing -> string { + if $saved_to_disk { + return $"($success_msg) and written to ($write_path)" + } + + $success_msg | append-help "To commit the change to disk, use the `--save` flag." +} + + +def registry-names [] { list | get name } +# Show detailed information about a specific registry +# returning a list of package names, type, and version +@example "Show registry information" { nupm registry describe nupm } +export def describe [ + registry: string@registry-names +]: nothing -> table { + use utils/dirs.nu cache-dir + + if not ($registry in $env.NUPM_REGISTRIES) { + throw-error $"Registry '($registry)' not found" + } + + let registry_url = $env.NUPM_REGISTRIES | get $registry + let cache = registry-cache $registry + let cached_registry = $cache.dir | path join $REGISTRY_FILENAME + + # Always check cache first, only fall back to URL if cache doesn't exist + let registry_data = if ($cache.file | path exists) { + open $cache.file + } else if ($registry_url | path exists) { + # Local registry file + open $registry_url + } else { + # Remote registry - fetch and cache + let data = http get $registry_url + mkdir $cache.dir + $data | save $cache.file + $data + } + + $registry_data | each {|entry| + let package_cache_path = $cache.dir | path join $"($entry.name).nuon" + + # Always check cache first for package data too + let package_file_data = if ($package_cache_path | path exists) { + open $package_cache_path + } else if ($registry_url | path exists) { + # Local package file + let package_path = $registry_url | path dirname | path join $entry.path + open $package_path + } else { + # Remote package - fetch and cache + let base_url = $registry_url | url parse + let package_url = $base_url | update path ($base_url.path | path dirname | path join $entry.path) | url join + let data = http get $package_url + $data | save $package_cache_path + $data + } + + # Package data is a table of versions for this package + $package_file_data | each {|pkg| + { + name: $pkg.name, + # TODO rename package metadata type field to source + # to avoid confusion with custom|script|module type enumberable + source: $pkg.type, + version: $pkg.version, + # description: ($pkg.description? | default "") + } + } + } | flatten +} + +# Add a new registry +@example "Add a new registry" { nupm registry add my-registry https://example.com/registry.nuon } +export def --env add [ + name: string, # Name of the registry + url: string, # URL or path to the registry + --save, # Whether to commit the change to the registry index +] { + if ($name in $env.NUPM_REGISTRIES) { + throw-error $"Registry '($name)' already exists. Use 'nupm registry update' to modify it." + } + $env.NUPM_REGISTRIES = $env.NUPM_REGISTRIES | insert $name $url + + if $save { + $env.NUPM_REGISTRIES | save --force $env.NUPM_INDEX_PATH + } + + print (success-msg $"Registry '($name)' added successfully" $env.NUPM_INDEX_PATH $save) + +} + +# Remove a registry +@example "Remove a registry" { nupm registry remove my-registry } +export def --env remove [ + name: string # Name of the registry to remove + --save, # Whether to commit the change to the registry index +] { + $env.NUPM_REGISTRIES = $env.NUPM_REGISTRIES | reject $name + + if $save { + $env.NUPM_REGISTRIES | save --force $env.NUPM_INDEX_PATH + } + + print (success-msg $"Registry '($name)' removed successfully" $env.NUPM_INDEX_PATH $save) +} + +# Update a given registry url +@example "Update registry URL" { nupm registry set-url my-registry https://new-url.com/registry.nuon } +export def --env set-url [ + name: string, # Name of the registry to update + url: string, # Registry target URL + --save, # Whether to commit the change to the registry index +]: nothing -> nothing { + $env.NUPM_REGISTRIES = $env.NUPM_REGISTRIES | update $name $url + + if $save { + $env.NUPM_REGISTRIES | save --force $env.NUPM_INDEX_PATH + } + + print (success-msg $"Registry '($name)' URL updated successfully" $env.NUPM_INDEX_PATH $save) +} + +# https://www.nushell.sh/book/configuration.html#macos-keeping-usr-bin-open-as-open +alias nu-rename = rename +# Rename a registry +@example "Rename a registry" { nupm registry rename my-registry our-registry } +export def --env rename [ + name: string, # Name of the registry to update + new_name: string, # New registry name + --save, # Whether to commit the change to the registry index +] { + $env.NUPM_REGISTRIES = $env.NUPM_REGISTRIES | nu-rename --column { $name: $new_name } + + if $save { + $env.NUPM_REGISTRIES | save --force $env.NUPM_INDEX_PATH + } + + print (success-msg $"Registry '($name)' renamed successfully" $env.NUPM_INDEX_PATH $save) +} + +def init-index [] { + if not (nupm-home-prompt) { + throw-error "Cannot create NUPM_HOME directory." + } + + + if ($env.NUPM_INDEX_PATH | path exists) { + print $"Registry list already exists at ($env.NUPM_INDEX_PATH)" + return + } + + $env.NUPM_REGISTRIES | save $env.NUPM_INDEX_PATH + + print $"Registry index initialized at ($env.NUPM_INDEX_PATH)" +} + + +# Initialize a new nupm registry or a registry index if the `--index` flag is +# passed in +@example "Initialize registry index" { nupm registry init --index } +export def init [--index] { + if $index { + init-index + return + } + # TODO initialize registry index here + throw-error UNIMPLEMENTED "`nupm registry --index` is not implemented." +} + diff --git a/nupm/test.nu b/nupm/test.nu index ea67e2e..9638d49 100644 --- a/nupm/test.nu +++ b/nupm/test.nu @@ -1,4 +1,4 @@ -use utils/dirs.nu [ tmp-dir find-root ] +use utils/dirs.nu [ tmp-dir find-root PACKAGE_FILENAME ] use utils/log.nu throw-error # Run tests for a nupm package @@ -28,7 +28,7 @@ export def main [ if $pkg_root == null { throw-error "package_file_not_found" ( - $'Could not find "nupm.nuon" in ($dir) or any parent directory.' + $'Could not find "($PACKAGE_FILENAME)" in ($dir) or any parent directory.' ) } diff --git a/nupm/utils/dirs.nu b/nupm/utils/dirs.nu index 5bcfae2..55b4244 100644 --- a/nupm/utils/dirs.nu +++ b/nupm/utils/dirs.nu @@ -10,18 +10,36 @@ export const DEFAULT_NUPM_CACHE = ($nu.default-config-dir # Default temporary path for various nupm purposes export const DEFAULT_NUPM_TEMP = ($nu.temp-path | path join "nupm") +# registry index filename +export const REGISTRY_IDX_FILENAME = "registry_index.nuon" +# registry filename +export const REGISTRY_FILENAME = "registry.nuon" +# package manifest filename +export const PACKAGE_FILENAME = "nupm.nuon" + # Default registry export const DEFAULT_NUPM_REGISTRIES = { - nupm_test: 'https://raw.githubusercontent.com/nushell/nupm/main/registry/registry.nuon' + nupm: 'https://raw.githubusercontent.com/nushell/nupm/main/registry/registry.nuon' } +# default path for the nupm registry index +export const DEFAULT_NUPM_INDEX_PATH = ($nu.default-config-dir | path join nupm $REGISTRY_IDX_FILENAME) + + +# pretty prints an environmental variable name +def env-colour [env_name: string]: nothing -> string { + $"(ansi purple)$env.($env_name)(ansi reset)" +} + +# Directories and related utilities used in nupm + # Prompt to create $env.NUPM_HOME if it does not exist and some sanity checks. # # returns true if the root directory exists or has been created, false otherwise export def nupm-home-prompt [--no-confirm]: nothing -> bool { if 'NUPM_HOME' not-in $env { error make --unspanned { - msg: "Internal error: NUPM_HOME environment variable is not set" + msg: $"Internal error: (env-colour "NUPM_HOME") is not set" } } @@ -29,7 +47,7 @@ export def nupm-home-prompt [--no-confirm]: nothing -> bool { if ($env.NUPM_HOME | path type) != 'dir' { error make --unspanned { msg: ($"Root directory ($env.NUPM_HOME) exists, but is not a" - + " directory. Make sure $env.NUPM_HOME points at a valid" + + $" directory. Make sure (env-colour "NUPM_HOME") points at a valid" + " directory and try again.") } } @@ -50,7 +68,7 @@ export def nupm-home-prompt [--no-confirm]: nothing -> bool { + ' Do you want to create it? [y/n] ')) } - if ($answer | str downcase) != 'y' { + if ($answer | str downcase) not-in [ y Y ] { return false } @@ -90,7 +108,7 @@ export def cache-dir [--ensure]: nothing -> path { } export def tmp-dir [subdir: string, --ensure]: nothing -> path { - let d = $env.NUPM_TEMP + let d = $DEFAULT_NUPM_TEMP | path join $subdir | path join (random chars -l 8) @@ -106,7 +124,7 @@ export def tmp-dir [subdir: string, --ensure]: nothing -> path { export def find-root [dir: path]: [ nothing -> path, nothing -> nothing] { let root_candidate = 1..($dir | path split | length) | reduce -f $dir {|_, acc| - if ($acc | path join nupm.nuon | path exists) { + if ($acc | path join $PACKAGE_FILENAME | path exists) { $acc } else { $acc | path dirname @@ -115,7 +133,7 @@ export def find-root [dir: path]: [ nothing -> path, nothing -> nothing] { # We need to do the last check in case the reduce loop ran to the end # without finding nupm.nuon - if ($root_candidate | path join nupm.nuon | path type) == 'file' { + if ($root_candidate | path join $PACKAGE_FILENAME | path type) == 'file' { $root_candidate } else { null diff --git a/nupm/utils/log.nu b/nupm/utils/log.nu index 9024116..4982e35 100644 --- a/nupm/utils/log.nu +++ b/nupm/utils/log.nu @@ -1,3 +1,6 @@ +# denotes an error msg that a section of code is yet to be implemented +export const UNIMPLEMENTED = "unimplemented" + export def throw-error [ error: string text?: string @@ -22,3 +25,8 @@ export def throw-error [ } } } + +# Append a formatted help line to mimic `error make` in core +export def append-help [help_msg: string]: string -> string { + $in + $"\n (ansi cyan)help:(ansi reset) " + $help_msg +} diff --git a/nupm/utils/misc.nu b/nupm/utils/misc.nu index 3f3e7be..2daa35d 100644 --- a/nupm/utils/misc.nu +++ b/nupm/utils/misc.nu @@ -58,10 +58,11 @@ export module url { export def update-name [new_name: string]: string -> string { url parse | update path {|url| - # skip the first '/' and replace last elemnt with the new name + # skip the first '/' and replace last element with the new name let parts = $url.path | path split | skip 1 | drop 1 $parts | append $new_name | str join '/' } | url join } } + diff --git a/nupm/utils/package.nu b/nupm/utils/package.nu index ded8d02..4e59fc4 100644 --- a/nupm/utils/package.nu +++ b/nupm/utils/package.nu @@ -1,3 +1,4 @@ +use dirs.nu PACKAGE_FILENAME # Open nupm.nuon export def open-package-file [dir: path] { if not ($dir | path exists) { @@ -6,11 +7,11 @@ export def open-package-file [dir: path] { ) } - let package_file = $dir | path join "nupm.nuon" + let package_file = $dir | path join $PACKAGE_FILENAME if not ($package_file | path exists) { throw-error "package_file_not_found" ( - $'Could not find "nupm.nuon" in ($dir) or any parent directory.' + $'Could not find "($PACKAGE_FILENAME)" in ($dir) or any parent directory.' ) } diff --git a/nupm/utils/registry.nu b/nupm/utils/registry.nu index df7e330..0672662 100644 --- a/nupm/utils/registry.nu +++ b/nupm/utils/registry.nu @@ -1,6 +1,6 @@ # Utilities related to nupm registries -use dirs.nu cache-dir +use dirs.nu [ cache-dir REGISTRY_FILENAME ] use log.nu throw-error use misc.nu [check-cols url hash-file hash-fn] @@ -10,6 +10,13 @@ export const REG_COLS = [ name path hash ] # Columns of a registry package file export const REG_PKG_COLS = [ name version path type info ] + +# return the respective registry cache directory and package index +export def registry-cache [name: string]: nothing -> record { + let dir = cache-dir --ensure | path join "registry" $name + { dir: $dir, file: ($dir | path join $REGISTRY_FILENAME) } +} + # Search for a package in a registry export def search-package [ package: string # Name of the package @@ -47,19 +54,22 @@ export def search-package [ } else { try { - let reg = http get $url_or_path - - # why didn't this line create the cache? - let reg_file = cache-dir --ensure - | path join registry $'($name).nuon' - - mkdir ($reg_file | path dirname) - $reg | save --force $reg_file + let cache = registry-cache $name + + let reg = if ($cache.file | path exists) { + open $cache.file + } else { + let data = http get $url_or_path + mkdir $cache.dir + $data | save --force $cache.file + $data + } { reg: $reg - path: $reg_file + path: $cache.file is_url: true + cache_dir: $cache.dir } } catch { throw-error $"Cannot open '($url_or_path)' as a file or URL." @@ -72,15 +82,17 @@ export def search-package [ let pkg_files = $registry.reg | where $name_matcher let pkgs = $pkg_files | each {|row| - let pkg_file_path = $registry.path - | path dirname - | path join $row.path + let pkg_file_path = if $registry.is_url { + $registry.cache_dir | path join $"($row.name).nuon" + } else { + $registry.path | path dirname | path join $row.path + } let hash = if ($pkg_file_path | path type) == file { $pkg_file_path | hash-file } - if $registry.is_url and $hash != $row.hash { + if $registry.is_url and (not ($pkg_file_path | path exists) or $hash != $row.hash) { let url = $url_or_path | url update-name $row.path http get $url | save --force $pkg_file_path } @@ -102,3 +114,16 @@ export def search-package [ $regs | where not ($it.pkgs | is-empty) } + + +export def open-index []: path -> record { + let index_file = $in + if ($index_file | path exists) { + if not (($index_file | path type) == "file") { + throw-error $"($index_file) is not a filepath" + } + return (open $index_file) + } + + {} +} diff --git a/tests/mod.nu b/tests/mod.nu index abee754..b84beab 100644 --- a/tests/mod.nu +++ b/tests/mod.nu @@ -1,9 +1,10 @@ use std assert -use ../nupm/utils/dirs.nu tmp-dir +use ../nupm/utils/dirs.nu +use ../nupm/utils/dirs.nu [ tmp-dir REGISTRY_FILENAME ] use ../nupm -const TEST_REGISTRY_PATH = ([tests packages registry registry.nuon] | path join) +const TEST_REGISTRY_PATH = ([tests packages registry $REGISTRY_FILENAME] | path join) def with-test-env [closure: closure]: nothing -> nothing { @@ -134,12 +135,12 @@ export def nupm-status-module [] { } export def env-vars-are-set [] { - $env.NUPM_HOME = null - $env.NUPM_TEMP = null - $env.NUPM_CACHE = null - $env.NUPM_REGISTRIES = null + (hide-env --ignore-errors + NUPM_HOME + NUPM_TEMP + NUPM_CACHE + NUPM_REGISTRIES) - use ../nupm/utils/dirs.nu use ../nupm assert equal $env.NUPM_HOME $dirs.DEFAULT_NUPM_HOME @@ -152,7 +153,7 @@ export def generate-local-registry [] { with-test-env { mkdir ($env.NUPM_TEMP | path join packages registry) - let reg_file = [tests packages registry registry.nuon] | path join + let reg_file = $TEST_REGISTRY_PATH let tmp_reg_file = [ $env.NUPM_TEMP packages registry test_registry.nuon ] @@ -171,3 +172,158 @@ export def generate-local-registry [] { assert equal $actual $expected } } + +export def registry-list [] { + with-test-env { + # Get list of registries + let registries = nupm registry list + + # Should have test registry from test environment + assert equal ($registries | length) 1 + assert equal $registries.0.name "test" + assert equal $registries.0.url $TEST_REGISTRY_PATH + } +} + +export def registry-add [] { + with-test-env { + # Add a new registry + nupm registry add test-registry https://example.com/test.nuon + + # Verify registry was added + let registries = nupm registry list + assert equal ($registries | length) 2 + + let test_reg = $registries | where name == "test-registry" | first + assert equal $test_reg.name "test-registry" + assert equal $test_reg.url "https://example.com/test.nuon" + + # Try to add duplicate registry (should fail) + let add_result = try { + nupm registry add test-registry https://duplicate.com/test.nuon + "should not reach here" + } catch {|err| + $err.msg + } + + assert ("Registry 'test-registry' already exists" in $add_result) + + # Add another registry + nupm registry add another-registry ./local-registry.nuon + + let registries_final = nupm registry list + assert equal ($registries_final | length) 3 + + let another_reg = $registries_final | where name == "another-registry" | first + assert equal $another_reg.name "another-registry" + assert equal $another_reg.url "./local-registry.nuon" + } +} + +export def registry-set-url [] { + with-test-env { + # Add a registry first + nupm registry add test-registry https://example.com/test.nuon + + # Update the registry URL + nupm registry set-url test-registry https://updated-example.com/registry.nuon + + # Verify URL was updated + let registries = nupm registry list + let test_reg = $registries | where name == "test-registry" | first + assert equal $test_reg.url "https://updated-example.com/registry.nuon" + + # Update again to different URL + nupm registry set-url test-registry ./local-path.nuon + + let registries_updated = nupm registry list + let test_reg_updated = $registries_updated | where name == "test-registry" | first + assert equal $test_reg_updated.url "./local-path.nuon" + } +} + +export def registry-remove [] { + with-test-env { + # Add registries first + nupm registry add test-registry https://example.com/test.nuon + nupm registry add another-registry https://another.com/registry.nuon + + # Verify both were added + let registries_before = nupm registry list + assert equal ($registries_before | length) 3 # 1 default + 2 added + + # Remove one registry + nupm registry remove test-registry + + # Verify registry was removed + let registries_after = nupm registry list + assert equal ($registries_after | length) 2 + assert equal ($registries_after | where name == "test-registry" | length) 0 + assert equal ($registries_after | where name == "another-registry" | length) 1 + + # Remove the other registry + nupm registry remove another-registry + + let registries_final = nupm registry list + assert equal ($registries_final | length) 1 # Only default registry left + } +} + +export def registry-rename [] { + with-test-env { + # Add a registry first + nupm registry add test-registry https://example.com/test.nuon + + # Rename the registry + nupm registry rename test-registry renamed-registry + + # Verify registry was renamed + let registries = nupm registry list + assert equal ($registries | where name == "test-registry" | length) 0 + assert equal ($registries | where name == "renamed-registry" | length) 1 + + let renamed_reg = $registries | where name == "renamed-registry" | first + assert equal $renamed_reg.url "https://example.com/test.nuon" + + # Rename again + nupm registry rename renamed-registry final-name + + let registries_final = nupm registry list + let final_reg = $registries_final | where name == "final-name" | first + assert equal $final_reg.url "https://example.com/test.nuon" + assert equal ($registries_final | where name == "renamed-registry" | length) 0 + } +} + +export def registry-describe [] { + with-test-env { + # Describe the test registry that's already configured + let description = nupm registry describe test + + # Verify we get package information + assert (($description | length) > 0) + + # Check for expected packages from the test registry + let spam_scripts = $description | where name == "spam_script" + assert (($spam_scripts | length) > 0) + + # Check that we have the latest version + let spam_script_latest = $spam_scripts | where version == "0.2.0" + if (($spam_script_latest | length) > 0) { + let pkg = $spam_script_latest | first + assert equal $pkg.name "spam_script" + assert equal $pkg.source "local" + assert equal $pkg.version "0.2.0" + } + + # Test error case with non-existent registry + let describe_result = try { + nupm registry describe non-existent-registry + "should not reach here" + } catch {|err| + $err.msg + } + + assert ("Registry 'non-existent-registry' not found" in $describe_result) + } +}