diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..d741e40 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,9 @@ +root = true + +[*] +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_style = space +indent_size = 4 diff --git a/README.md b/README.md index 761e5d3..48382fc 100644 --- a/README.md +++ b/README.md @@ -32,29 +32,24 @@ Both of the above commands will make `nupm` and all its subcommands available in > ``` ## :gear: configuration [[toc](#table-of-content)] -One can change the location of the Nupm directory with `$env.NUPM_HOME`, e.g. +One can change the location of the Nupm directory with `$env.nupm.home`, e.g. ```nushell # env.nu -$env.NUPM_HOME = ($env.XDG_DATA_HOME | path join "nupm") +$env.nupm.home = ($env.XDG_DATA_HOME | path join "nupm") ``` -Because Nupm will install modules and scripts in `{{nupm-home}}/modules/` and `{{nupm-home}}/scripts/` respectively, it is a good idea to add these paths to `$env.NU_LIB_DIRS` and `$env.PATH` respectively, e.g. if you have `$env.NUPM_HOME` defined: +If you would like installed modules, scripts, and plugins to show up in [nushell search +paths](https://www.nushell.sh/book/configuration.html#launch-stages), set the +`nu_search_path` to `true` before calling `use nupm`: ```nushell # env.nu - -$env.NU_LIB_DIRS = [ - ... - ($env.NUPM_HOME | path join "modules") -] - -$env.PATH = ( - $env.PATH - | split row (char esep) - | .... - | prepend ($env.NUPM_HOME | path join "scripts") - | uniq -) +$env.nupm = { + home: "path/to/my_home" + config: { nu_search_path: true } +} +# ... +use path/to/nupm ``` ## :rocket: usage [[toc](#table-of-content)] diff --git a/docs/design/README.md b/docs/design/README.md index 224d3c4..5a51a13 100644 --- a/docs/design/README.md +++ b/docs/design/README.md @@ -1,19 +1,19 @@ -# 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` > (name inspired by Julia's `Project.toml` or Rust's `Cargo.toml`) -> - `NUPM_HOME`: the location of all the `nupm` files, overlays, scripts, libraries, ..., +> - `nupm.home`: the location of all the `nupm` files, overlays, scripts, libraries, ..., > e.g. `~/.nupm/`, `$env.XDG_DATA_HOME/nupm/` or `~/.local/share/nupm/` # 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` @@ -37,7 +37,7 @@ spam ``` * meant as a runnable script, equivalent of Rust's binary project * could use the `.nush` extension if we agree to support it -* installed under `NUPM_HOME/bin/` +* installed under `nupm.home/bin/` 2. Module ``` @@ -47,7 +47,7 @@ spam └── mod.nu ``` * meant as a library to be `use`d or `overlay use`d, equivalent of Rust's library project -* installed under `NUPM_HOME/modules/` +* installed under `nupm.home/modules/` You can also install non-Nushell packages as well using a "custom" project type where you specify a `build.nu` installation script (e.g., you can install Nushell itself with it). @@ -69,21 +69,21 @@ Related to that is a lock file: It is intended to describe exactly the dependenc The overlays could be used to achieve all three goals at the same time. When installing a dependency for a package * `nupm` adds entry to a **lock file** (this should be the only file you need to 100% replicate the environment) -* A .nu file (module) is auto-generated from the lock file and contains export statements like `export module NUPM_HOME/cache/packages/spam-v16.4.0-124ptnpbf/spam`. Calling `overlay use` on the file will activate your virtual environment, now you have a per-project environment -* 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) +* A .nu file (module) is auto-generated from the lock file and contains export statements like `export module nupm.home/cache/packages/spam-v16.4.0-124ptnpbf/spam`. Calling `overlay use` on the file will activate your virtual environment, now you have a per-project environment +* 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) -* Add `NUPM_HOME/modules` to NU_LIB_DIRS -* Add `NUPM_HOME/overlays` to NU_LIB_DIRS +* Add `nupm.home/bin` to PATH (install location for binary projects) +* Add `nupm.home/modules` to NU_LIB_DIRS +* 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..947a99c 100644 --- a/nupm/install.nu +++ b/nupm/install.nu @@ -1,7 +1,7 @@ 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/misc.nu [check-cols hash-fn url flatten-nupm-env] use utils/package.nu open-package-file use utils/registry.nu search-package use utils/version.nu filter-by-version @@ -105,8 +105,9 @@ def install-path [ let tmp_dir = tmp-dir build --ensure do { + flatten-nupm-env 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 @@ -235,7 +236,7 @@ def fetch-package [ export def main [ package # Name, path, or link to the package --registry: string@complete-registries # Which registry to use (either a name - # in $env.NUPM_REGISTRIES or a path) + # in $env.nupm.registries or a path) --pkg-version(-v): string # Package version to install --path # Install package from a directory with nupm.nuon given by 'name' --force(-f) # Overwrite already installed package diff --git a/nupm/mod.nu b/nupm/mod.nu index de9f88a..fe5a042 100644 --- a/nupm/mod.nu +++ b/nupm/mod.nu @@ -1,34 +1,34 @@ use std/log -use utils/dirs.nu [ - DEFAULT_NUPM_HOME DEFAULT_NUPM_TEMP DEFAULT_NUPM_CACHE - DEFAULT_NUPM_REGISTRIES nupm-home-prompt -] +use utils/dirs.nu [ nupm-home-prompt BASE_NUPM_CONFIG ] +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. - $env.NUPM_HOME = ($env.NUPM_HOME? | default $DEFAULT_NUPM_HOME) - - # Ensure temporary path is set. - $env.NUPM_TEMP = ($env.NUPM_TEMP? | default $DEFAULT_NUPM_TEMP) - - # 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) - + # Ensure that $env.nupm is always set when running nupm. Any missing variaables are set by `$BASE_NUPM_CONFIG` + $env.nupm = $BASE_NUPM_CONFIG | merge deep ($env.nupm? | default {}) + # set missing values to default while + # retaining defaults in $env.nupm.default + $env.nupm.default = $BASE_NUPM_CONFIG + # 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 + $env.ENV_CONVERSIONS.nupm = { + from_string: { |s| $s | from nuon } + to_string: { |v| $v | to nuon } + } + if $env.nupm.config.nu_search_path { + let nupm_lib_dirs = [modules, scripts] | each {|s| $env.nupm.home | path join $s } + $env.NU_LIB_DIRS = $env.NU_LIB_DIRS | prepend $nupm_lib_dirs | uniq + + let nupm_plugin_dir = $env.nupm.home| path join "plugins" + $env.NU_PLUGIN_DIRS = $env.NU_PLUGIN_DIRS | prepend $nupm_plugin_dir | uniq + } use std/log [] } @@ -38,8 +38,8 @@ export-env { # Nushell packages including modules, scripts, and custom packages. # # Configuration: -# Set `NUPM_HOME` environment variable to change installation directory -# Set `NUPM_REGISTRIES` to configure package registries +# Set `nupm.home` environment variable to change installation directory +# Set `nupm.registries` to configure package registries @example "Install a package from a local directory" { nupm install my-package --path } @example "Publish a package" { nupm publish my-registry.nuon --local --save } @example "Search for specific version" { nupm search my-package --pkg-version 1.2.0 } diff --git a/nupm/publish.nu b/nupm/publish.nu index 9ed7d19..da9089e 100644 --- a/nupm/publish.nu +++ b/nupm/publish.nu @@ -222,7 +222,7 @@ def guess-revision []: nothing -> string { def get-registry-path []: string -> path { let registry = $in - $env.NUPM_REGISTRIES | get -i $registry | default ($registry | path expand) + $env.nupm.registries | get -i $registry | default ($registry | path expand) } def open-registry-file []: path -> table { diff --git a/nupm/registry.nu b/nupm/registry.nu new file mode 100644 index 0000000..8f76eff --- /dev/null +++ b/nupm/registry.nu @@ -0,0 +1,251 @@ +# Registry management for nupm + +use utils/dirs.nu [nupm-home-prompt REGISTRY_FILENAME] +use utils/log.nu throw-error + +# 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 +} + + +def describe-comp [] { 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@describe-comp # Name of the registry +]: 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 registry_cache_dir = cache-dir --ensure | path join $registry + let cached_registry = $registry_cache_dir | path join $REGISTRY_FILENAME + + try { + # Always check cache first, only fall back to URL if cache doesn't exist + let registry_data = if ($cached_registry | path exists) { + open $cached_registry + } 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 $registry_cache_dir + $data | save $cached_registry + $data + } + + $registry_data | each {|entry| + let package_cache_path = $registry_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 + } catch {|err| + throw-error $"Failed to fetch registry data from '($registry_url)': ($err.msg)" + } +} + +# 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 $"Registry '($name)' added successfully." +} + +# 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 $"Registry '($name)' removed successfully." +} + +# 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, + --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 $"Registry '($name)' URL updated successfully." +} + +# 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, + --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 $"Registry '($name)' renamed successfully." +} + +# Fetch and cache registry data locally +@example "Fetch a specific registry" { nupm registry fetch nupm } +@example "Fetch all registries" { nupm registry fetch --all } +export def fetch [ + registry?: string, # Name of the registry to fetch (optional if --all is used) + --all, # Fetch all configured registries +] { + if $all { + # Fetch all registries + let registries = $env.nupm.registries | transpose name url + print $"Fetching ($registries | length) registries..." + + $registries | each {|reg| + fetch-registry $reg.name $reg.url + } + + print "All registries fetched successfully." + } else if ($registry | is-empty) { + throw-error "Please specify a registry name or use --all flag" + } else { + if not ($registry in $env.nupm.registries) { + throw-error $"Registry '($registry)' not found" + } + + let registry_url = $env.nupm.registries | get $registry + fetch-registry $registry $registry_url + + print $"Registry '($registry)' fetched successfully." + } +} + +# Helper function to fetch a single registry +def fetch-registry [name: string, url: string] { + use utils/dirs.nu cache-dir + + let registry_cache_dir = cache-dir --ensure | path join $name + mkdir $registry_cache_dir + + if ($url | path exists) { + print $"Registry '($name)' is local, copying to cache..." + cp $url ($registry_cache_dir | path join $REGISTRY_FILENAME) + + # Copy package files if they exist locally + let registry_data = open $url + $registry_data | each {|entry| + let package_path = $url | path dirname | path join $entry.path + if ($package_path | path exists) { + cp $package_path ($registry_cache_dir | path join $"($entry.name).nuon") + } + } + } else { + print $"Fetching registry '($name)' from ($url)..." + + # Fetch registry index + let registry_data = http get $url + $registry_data | save --force ($registry_cache_dir | path join $REGISTRY_FILENAME) + + # Fetch all package metadata files + $registry_data | each {|entry| + print $" Fetching package ($entry.name)..." + let base_url = $url | url parse + let package_url = $base_url | update path ($base_url.path | path dirname | path join $entry.path) | url join + let package_data = http get $package_url + $package_data | save --force ($registry_cache_dir | path join $"($entry.name).nuon") + } + } +} + + +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 +} + diff --git a/nupm/search.nu b/nupm/search.nu index e8739f5..e62db55 100644 --- a/nupm/search.nu +++ b/nupm/search.nu @@ -21,7 +21,7 @@ use utils/version.nu filter-by-version export def main [ package # Name, path, or link to the package --registry: string@complete-registries # Which registry to use (either a name - # in $env.NUPM_REGISTRIES or a path) + # in $env.nupm.registries or a path) --pkg-version(-v): string # Package version to install --exact-match(-e) # Match package name exactly ]: nothing -> table { 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/completions.nu b/nupm/utils/completions.nu index d1d37ab..9700bbd 100644 --- a/nupm/utils/completions.nu +++ b/nupm/utils/completions.nu @@ -1,3 +1,3 @@ export def complete-registries [] { - $env.NUPM_REGISTRIES? | default {} | columns + $env.nupm?.registries? | default {} | columns } diff --git a/nupm/utils/dirs.nu b/nupm/utils/dirs.nu index 5bcfae2..ba491ee 100644 --- a/nupm/utils/dirs.nu +++ b/nupm/utils/dirs.nu @@ -1,35 +1,45 @@ -# Directories and related utilities used in nupm - -# Default installation path for nupm packages -export const DEFAULT_NUPM_HOME = ($nu.default-config-dir | path join "nupm") - -# Default path for installation cache -export const DEFAULT_NUPM_CACHE = ($nu.default-config-dir - | path join nupm cache) - -# Default temporary path for various nupm purposes -export const DEFAULT_NUPM_TEMP = ($nu.temp-path | path join "nupm") +# Base values for nupm that are used as defaults if not present in `$env.nupm` +export const REGISTRY_IDX_FILENAME = "registry_index.nuon" +export const REGISTRY_FILENAME = "registry.nuon" +export const PACKAGE_FILENAME = "nupm.nuon" + +export const BASE_NUPM_CONFIG = { + home: ($nu.default-config-dir | path join nupm) + cache: ($nu.default-config-dir | path join nupm cache) + index-path: ($nu.default-config-dir | path join nupm $REGISTRY_IDX_FILENAME) + temp: ($nu.temp-path | path join nupm) + registries: { + nupm: 'https://raw.githubusercontent.com/nushell/nupm/main/registry/registry.nuon' + } + config: { + # TODO + # sync_list: { ... } + # sync_on_launch: false + nu_search_path: false + } +} -# Default registry -export const DEFAULT_NUPM_REGISTRIES = { - nupm_test: 'https://raw.githubusercontent.com/nushell/nupm/main/registry/registry.nuon' +def env-colour [env_name: string]: nothing -> string { + $"(ansi purple)($env_name)(ansi reset)" } -# Prompt to create $env.NUPM_HOME if it does not exist and some sanity checks. +# 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 { + if 'home' not-in $env.nupm { error make --unspanned { - msg: "Internal error: NUPM_HOME environment variable is not set" + msg: $"Internal error: (env-colour "$env.nupm.home") is not set" } } - if ($env.NUPM_HOME | path exists) { - if ($env.NUPM_HOME | path type) != 'dir' { + if ($env.nupm.home | path exists) { + 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" + msg: ($"Root directory ($env.nupm.home) exists, but is not a" + + $" directory. Make sure (env-colour "$env.nupm.home") points at a valid" + " directory and try again.") } } @@ -38,7 +48,7 @@ export def nupm-home-prompt [--no-confirm]: nothing -> bool { } if $no_confirm { - mkdir $env.NUPM_HOME + mkdir $env.nupm.home return true } @@ -46,21 +56,21 @@ export def nupm-home-prompt [--no-confirm]: nothing -> bool { while ($answer | str downcase) not-in [ y n ] { $answer = (input ( - $'Root directory "($env.NUPM_HOME)" does not exist.' + $'Root directory "($env.nupm.home)" does not exist.' + ' Do you want to create it? [y/n] ')) } - if ($answer | str downcase) != 'y' { + if ($answer | str downcase) not-in [ y Y ] { return false } - mkdir $env.NUPM_HOME + mkdir $env.nupm.home true } export def script-dir [--ensure]: nothing -> path { - let d = $env.NUPM_HOME | path join scripts + let d = $env.nupm.home | path join scripts if $ensure { mkdir $d @@ -70,7 +80,7 @@ export def script-dir [--ensure]: nothing -> path { } export def module-dir [--ensure]: nothing -> path { - let d = $env.NUPM_HOME | path join modules + let d = $env.nupm.home | path join modules if $ensure { mkdir $d @@ -80,7 +90,7 @@ export def module-dir [--ensure]: nothing -> path { } export def cache-dir [--ensure]: nothing -> path { - let d = $env.NUPM_CACHE + let d = $env.nupm.cache if $ensure { mkdir $d @@ -90,7 +100,7 @@ export def cache-dir [--ensure]: nothing -> path { } export def tmp-dir [subdir: string, --ensure]: nothing -> path { - let d = $env.NUPM_TEMP + let d = $BASE_NUPM_CONFIG.temp | path join $subdir | path join (random chars -l 8) @@ -106,7 +116,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 +125,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/misc.nu b/nupm/utils/misc.nu index 3f3e7be..730bb0f 100644 --- a/nupm/utils/misc.nu +++ b/nupm/utils/misc.nu @@ -58,10 +58,18 @@ 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 } } + +# workaround for https://github.com/nushell/nushell/issues/16036 +export def --env flatten-nupm-env [] { + $env.NUPM_HOME = $env.nupm.home + $env.NUPM_CACHE = $env.nupm.cache + $env.NUPM_TEMP = $env.nupm.temp + $env.NUPM_REGISTRIES = $env.nupm.registries | to nuon +} 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..469f813 100644 --- a/nupm/utils/registry.nu +++ b/nupm/utils/registry.nu @@ -16,16 +16,16 @@ export def search-package [ --registry: string # Which registry to use (name or path) --exact-match # Searched package name must match exactly ]: nothing -> table { - let registries = if (not ($registry | is-empty)) and ($registry in $env.NUPM_REGISTRIES) { - # If $registry is a valid column in $env.NUPM_REGISTRIES, use that - { $registry : ($env.NUPM_REGISTRIES | get $registry) } + let registries = if (not ($registry | is-empty)) and ($registry in $env.nupm.registries) { + # If $registry is a valid column in $env.nupm.registries, use that + { $registry : ($env.nupm.registries | get $registry) } } else if (not ($registry | is-empty)) and ($registry | path exists) { # If $registry is a path, use that let reg_name = $registry | path parse | get stem { $reg_name: $registry } } else { - # Otherwise use $env.NUPM_REGISTRIES as-is - $env.NUPM_REGISTRIES + # Otherwise use $env.nupm.registries as-is + $env.nupm.registries } let name_matcher: closure = if $exact_match { @@ -47,19 +47,23 @@ 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 registry_cache_dir = cache-dir --ensure | path join $name + let reg_file = $registry_cache_dir | path join "registry.nuon" + + let reg = if ($reg_file | path exists) { + open $reg_file + } else { + let data = http get $url_or_path + mkdir $registry_cache_dir + $data | save --force $reg_file + $data + } { reg: $reg path: $reg_file is_url: true + cache_dir: $registry_cache_dir } } catch { throw-error $"Cannot open '($url_or_path)' as a file or URL." @@ -72,15 +76,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 +108,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..a02de3b 100644 --- a/tests/mod.nu +++ b/tests/mod.nu @@ -1,9 +1,9 @@ use std assert -use ../nupm/utils/dirs.nu tmp-dir +use ../nupm/utils/dirs.nu [ tmp-dir BASE_NUPM_CONFIG 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 { @@ -13,10 +13,12 @@ def with-test-env [closure: closure]: nothing -> nothing { let reg = { test: $TEST_REGISTRY_PATH } with-env { - NUPM_HOME: $home - NUPM_CACHE: $cache - NUPM_TEMP: $temp - NUPM_REGISTRIES: $reg + nupm: { + home: $home + cache: $cache + temp: $temp + registries: $reg + } } $closure rm --recursive $temp @@ -25,14 +27,14 @@ def with-test-env [closure: closure]: nothing -> nothing { } # Examples: -# make sure `$env.NUPM_HOME/scripts/script.nu` exists +# make sure `$env.nupm.home/scripts/script.nu` exists # > assert installed [scripts script.nu] def "assert installed" [path_tokens: list] { - assert ($path_tokens | prepend $env.NUPM_HOME | path join | path exists) + assert ($path_tokens | prepend $env.nupm.home | path join | path exists) } def check-file-content [content: string] { - let file_str = open ($env.NUPM_HOME | path join scripts spam_script.nu) + let file_str = open ($env.nupm.home | path join scripts spam_script.nu) assert ($file_str | str contains $content) } @@ -66,7 +68,7 @@ export def install-custom [] { export def install-from-local-registry [] { with-test-env { - $env.NUPM_REGISTRIES = {} + $env.nupm.registries = {} nupm install --registry $TEST_REGISTRY_PATH spam_script check-file-content 0.2.0 } @@ -91,7 +93,7 @@ export def install-with-version [] { export def install-multiple-registries-fail [] { with-test-env { - $env.NUPM_REGISTRIES.test2 = $TEST_REGISTRY_PATH + $env.nupm.registries.test2 = $TEST_REGISTRY_PATH let out = try { nupm install spam_script @@ -134,27 +136,23 @@ 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 nupm --ignore-errors - use ../nupm/utils/dirs.nu use ../nupm - assert equal $env.NUPM_HOME $dirs.DEFAULT_NUPM_HOME - assert equal $env.NUPM_TEMP $dirs.DEFAULT_NUPM_TEMP - assert equal $env.NUPM_CACHE $dirs.DEFAULT_NUPM_CACHE - assert equal $env.NUPM_REGISTRIES $dirs.DEFAULT_NUPM_REGISTRIES + assert equal $env.nupm.home $BASE_NUPM_CONFIG.home + assert equal $env.nupm.temp $BASE_NUPM_CONFIG.temp + assert equal $env.nupm.cache $BASE_NUPM_CONFIG.cache + assert equal $env.nupm.registries $BASE_NUPM_CONFIG.registries } export def generate-local-registry [] { with-test-env { - mkdir ($env.NUPM_TEMP | path join packages registry) + mkdir ($env.nupm.temp | path join packages registry) let reg_file = [tests packages registry registry.nuon] | path join let tmp_reg_file = [ - $env.NUPM_TEMP packages registry test_registry.nuon + $env.nupm.temp packages registry test_registry.nuon ] | path join @@ -171,3 +169,239 @@ 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) + } +} + +export def registry-fetch [] { + with-test-env { + # Test fetch with local registry (test registry) + let fetch_result = try { + nupm registry fetch test + "success" + } catch {|err| + $err.msg + } + + # For local registry, fetch should succeed + assert equal $fetch_result "success" + + # Verify cache directory was created + let cache_dir = $env.nupm.cache | path join test + assert ($cache_dir | path exists) + assert (($cache_dir | path join "registry.nuon") | path exists) + + # Verify package files were cached + let spam_script_cache = $cache_dir | path join "spam_script.nuon" + assert ($spam_script_cache | path exists) + + # Test --all flag (only with local registries to avoid network issues) + let fetch_all_result = try { + nupm registry fetch --all + "success" + } catch {|err| + $err.msg + } + + assert equal $fetch_all_result "success" + + # Test error cases + let no_name_result = try { + nupm registry fetch + "should not reach here" + } catch {|err| + $err.msg + } + + assert ("Please specify a registry name or use --all flag" in $no_name_result) + + let invalid_registry_result = try { + nupm registry fetch invalid-registry + "should not reach here" + } catch {|err| + $err.msg + } + + assert ("Registry 'invalid-registry' not found" in $invalid_registry_result) + } +} + +export def config-nu-search-path [] { + with-test-env { + use ../nupm + # By default, nu_search_path should be false + assert equal $env.nupm.config.nu_search_path false + + # Verify nupm directories are NOT added to NU_LIB_DIRS when disabled + let modules_dir = $env.nupm.home | path join modules + let scripts_dir = $env.nupm.home | path join scripts + let plugins_dir = $env.nupm.home | path join plugins + + # Check that nupm dirs are not in the search paths + assert (not ($modules_dir in $env.NU_LIB_DIRS)) + assert (not ($scripts_dir in $env.NU_LIB_DIRS)) + assert (not ($plugins_dir in $env.NU_PLUGIN_DIRS)) + + # Manually set the config to true to test the functionality + $env.nupm.config.nu_search_path = true + + use ../nupm + assert equal $env.nupm.config.nu_search_path true + + assert ($modules_dir in $env.NU_LIB_DIRS) + assert ($scripts_dir in $env.NU_LIB_DIRS) + assert ($plugins_dir in $env.NU_PLUGIN_DIRS) + } +} diff --git a/toolkit.nu b/toolkit.nu index 93de04c..f313026 100644 --- a/toolkit.nu +++ b/toolkit.nu @@ -8,10 +8,10 @@ export def --env set-nupm-env [--clear] { rm -rf _nupm_dev } - $env.NUPM_HOME = ($env.PWD | path join _nupm_dev) - $env.NUPM_CACHE = ($env.PWD | path join _nupm_dev cache) - $env.NUPM_TEMP = ($env.PWD | path join _nupm_dev tmp) - $env.NUPM_REGISTRIES = { nupm_dev: ($env.PWD | path join registry registry.nuon) } + $env.nupm.home = ($env.PWD | path join _nupm_dev) + $env.nupm.cache = ($env.PWD | path join _nupm_dev cache) + $env.nupm.temp = ($env.PWD | path join _nupm_dev tmp) + $env.nupm.registries = { nupm_dev: ($env.PWD | path join registry registry.nuon) } if $nu.os-info.family == 'windows' and 'Path' in $env { $env.Path = ($env.Path | prepend ($env.PWD | path join _nupm_dev scripts)) @@ -24,12 +24,12 @@ export def --env set-nupm-env [--clear] { } export def print-nupm-env [] { - print $'NUPM_HOME: ($env.NUPM_HOME?)' - print $'NUPM_CACHE: ($env.NUPM_CACHE?)' - print $'NUPM_TEMP: ($env.NUPM_TEMP?)' + print $'nupm.home: ($env.nupm.home?)' + print $'nupm.cache: ($env.nupm.cache?)' + print $'nupm.temp: ($env.nupm.temp?)' print $"PATH: ($env.PATH? | default $env.Path? | default [])" print $'NU_LIB_DIRS: ($env.NU_LIB_DIRS?)' - print $'NUPM_REGISTRIES: ($env.NUPM_REGISTRIES?)' + print $'nupm.registires: ($env.nupm.registries?)' } # turn on pretty diffs for NUON data files