diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 3aadc1a1..ad39edeb 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -6,6 +6,8 @@ # Jobs: # e2e-opsm — offline E2E suite (runs on every push/PR) # runtime-api — runtime version API integration tests (external_api tag, no downloads) +# live-download — full runtime install pipeline, incl. nickel plugin +# evaluation (workflow_dispatch only, see below) # # :live_download tests (full tool installs ~10-50MB) are workflow_dispatch only # to avoid burning bandwidth on every commit; run manually when validating @@ -88,3 +90,49 @@ jobs: --include external_api \ --exclude live_download \ --trace + live-download: + name: Runtime — live install (workflow_dispatch only) + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Install Rust toolchain (to build nickel) + uses: dtolnay/rust-toolchain@4be9e76fd7c4901c61fb841f559994984270fce7 # stable + with: + toolchain: '1.97.0' + - name: Cache cargo-installed tools + uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2 + with: + key: nickel-lang-cli-1.16.0 + - name: Install nickel + # Pinned to opsm.toml [runtime] — see docs/TOOLCHAIN.adoc. Manager.install + # (opsm_ex/lib/opsm/runtime/manager.ex) shells out to the `nickel` binary + # to evaluate runtime/core/*.ncl plugin definitions; without it every + # install fails with {:error, {:nickel_not_installed, tool}}. + run: cargo install nickel-lang-cli --version 1.16.0 --locked + - name: Setup Erlang/Elixir + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.24.1 + with: + # Versions derive from opsm.toml [runtime] via .tool-versions + # (generated by `just toolchain-sync` — see docs/TOOLCHAIN.adoc) + version-file: '.tool-versions' + version-type: 'strict' + - name: Restore Mix cache + uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v4 + with: + path: | + opsm_ex/deps + opsm_ex/_build + key: ${{ runner.os }}-mix-${{ hashFiles('opsm_ex/mix.lock') }} + - name: Install dependencies + run: cd opsm_ex && mix deps.get + - name: Compile + run: cd opsm_ex && mix compile --warnings-as-errors + - name: Run live-download runtime install tests (full zig install via Manager.install) + run: | + cd opsm_ex && mix test test/opsm/runtime/integration_test.exs \ + --include external_api \ + --include live_download \ + --trace diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 96c73737..5ebcc752 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -6,6 +6,16 @@ # opsm.toml [runtime] — drift is gated by `just toolchain-check`. # Scanner images (trivy/semgrep/trufflehog) deliberately float: their # detection databases need to stay fresh, so pinning them is worse. +# +# There is no root mix.exs or Cargo.toml — mix.exs lives in opsm_ex/, and +# the estate's Rust code is nine independent first-party crates (the +# ratatui TUI, two Elixir NIFs, and six microservices under services/). +# `exists: mix.exs` / `exists: Cargo.toml` at repo root never matched +# either, so every mix-*/cargo-* job below was permanently dormant. +# mix-* jobs now target opsm_ex/ directly; cargo-* jobs loop over +# $RUST_CRATE_DIRS. Keep that variable and each job's `rules: exists:` +# list in sync when a crate is added or removed — `rules: exists:` needs +# literal paths, it does not expand CI variables. stages: - security @@ -14,11 +24,25 @@ stages: - build variables: CARGO_HOME: ${CI_PROJECT_DIR}/.cargo + # First-party Rust crates only — opsm_ex/deps/** is vendored (Mix deps + # cache), never linted/audited/built as our own code. + RUST_CRATE_DIRS: >- + opsm-ui/tui + opsm_ex/native/opsm_pq_nif + opsm_ex/native/quic_transport + services/checky-monkey + services/oikos + services/palimpsest-license + services/selur + services/svalinn + services/vordr cache: key: ${CI_COMMIT_REF_SLUG} paths: - .cargo/ - - target/ + - "*/target/" + - "*/*/target/" + - "*/*/*/target/" # ================== # Security Scanning # ================== @@ -39,32 +63,57 @@ cargo-audit: stage: security image: rust:1.97 script: - - cargo install cargo-audit - - cargo audit + - cargo install cargo-audit --locked + - | + for dir in $RUST_CRATE_DIRS; do + echo "== cargo audit: $dir ==" + (cd "$dir" && cargo audit) + done rules: - exists: - - Cargo.toml + - opsm-ui/tui/Cargo.toml + - opsm_ex/native/opsm_pq_nif/Cargo.toml + - opsm_ex/native/quic_transport/Cargo.toml + - services/checky-monkey/Cargo.toml + - services/oikos/Cargo.toml + - services/palimpsest-license/Cargo.toml + - services/selur/Cargo.toml + - services/svalinn/Cargo.toml + - services/vordr/Cargo.toml cargo-deny: stage: security image: rust:1.97 script: - - cargo install cargo-deny - - cargo deny check + - cargo install cargo-deny --locked + - | + for dir in $RUST_CRATE_DIRS; do + echo "== cargo deny: $dir ==" + (cd "$dir" && cargo deny check) + done rules: - exists: - - Cargo.toml + - opsm-ui/tui/Cargo.toml + - opsm_ex/native/opsm_pq_nif/Cargo.toml + - opsm_ex/native/quic_transport/Cargo.toml + - services/checky-monkey/Cargo.toml + - services/oikos/Cargo.toml + - services/palimpsest-license/Cargo.toml + - services/selur/Cargo.toml + - services/svalinn/Cargo.toml + - services/vordr/Cargo.toml allow_failure: true mix-audit: stage: security image: elixir:1.19.5-otp-28 script: + - cd opsm_ex - mix local.hex --force - mix archive.install hex mix_audit --force - mix deps.get - mix deps.audit rules: - exists: - - mix.exs + - opsm_ex/mix.exs allow_failure: true # ================== # Linting @@ -74,38 +123,64 @@ rustfmt: image: rust:1.97 script: - rustup component add rustfmt - - cargo fmt -- --check + - | + for dir in $RUST_CRATE_DIRS; do + echo "== cargo fmt --check: $dir ==" + (cd "$dir" && cargo fmt -- --check) + done rules: - exists: - - Cargo.toml + - opsm-ui/tui/Cargo.toml + - opsm_ex/native/opsm_pq_nif/Cargo.toml + - opsm_ex/native/quic_transport/Cargo.toml + - services/checky-monkey/Cargo.toml + - services/oikos/Cargo.toml + - services/palimpsest-license/Cargo.toml + - services/selur/Cargo.toml + - services/svalinn/Cargo.toml + - services/vordr/Cargo.toml clippy: stage: lint image: rust:1.97 script: - rustup component add clippy - - cargo clippy -- -D warnings + - | + for dir in $RUST_CRATE_DIRS; do + echo "== cargo clippy: $dir ==" + (cd "$dir" && cargo clippy -- -D warnings) + done rules: - exists: - - Cargo.toml + - opsm-ui/tui/Cargo.toml + - opsm_ex/native/opsm_pq_nif/Cargo.toml + - opsm_ex/native/quic_transport/Cargo.toml + - services/checky-monkey/Cargo.toml + - services/oikos/Cargo.toml + - services/palimpsest-license/Cargo.toml + - services/selur/Cargo.toml + - services/svalinn/Cargo.toml + - services/vordr/Cargo.toml allow_failure: true mix-format: stage: lint image: elixir:1.19.5-otp-28 script: + - cd opsm_ex - mix format --check-formatted rules: - exists: - - mix.exs + - opsm_ex/mix.exs credo: stage: lint image: elixir:1.19.5-otp-28 script: + - cd opsm_ex - mix local.hex --force - mix deps.get - mix credo --strict rules: - exists: - - mix.exs + - opsm_ex/mix.exs allow_failure: true # ================== # Testing @@ -114,20 +189,33 @@ cargo-test: stage: test image: rust:1.97 script: - - cargo test --all-features + - | + for dir in $RUST_CRATE_DIRS; do + echo "== cargo test: $dir ==" + (cd "$dir" && cargo test --all-features) + done rules: - exists: - - Cargo.toml + - opsm-ui/tui/Cargo.toml + - opsm_ex/native/opsm_pq_nif/Cargo.toml + - opsm_ex/native/quic_transport/Cargo.toml + - services/checky-monkey/Cargo.toml + - services/oikos/Cargo.toml + - services/palimpsest-license/Cargo.toml + - services/selur/Cargo.toml + - services/svalinn/Cargo.toml + - services/vordr/Cargo.toml mix-test: stage: test image: elixir:1.19.5-otp-28 script: + - cd opsm_ex - mix local.hex --force - mix deps.get - mix test rules: - exists: - - mix.exs + - opsm_ex/mix.exs # ================== # Build # ================== @@ -135,24 +223,45 @@ cargo-build: stage: build image: rust:1.97 script: - - cargo build --release + - | + for dir in $RUST_CRATE_DIRS; do + echo "== cargo build --release: $dir ==" + (cd "$dir" && cargo build --release) + done artifacts: paths: - - target/release/ + - opsm-ui/tui/target/release/ + - opsm_ex/native/opsm_pq_nif/target/release/ + - opsm_ex/native/quic_transport/target/release/ + - services/checky-monkey/target/release/ + - services/oikos/target/release/ + - services/palimpsest-license/target/release/ + - services/selur/target/release/ + - services/svalinn/target/release/ + - services/vordr/target/release/ expire_in: 1 week rules: - exists: - - Cargo.toml + - opsm-ui/tui/Cargo.toml + - opsm_ex/native/opsm_pq_nif/Cargo.toml + - opsm_ex/native/quic_transport/Cargo.toml + - services/checky-monkey/Cargo.toml + - services/oikos/Cargo.toml + - services/palimpsest-license/Cargo.toml + - services/selur/Cargo.toml + - services/svalinn/Cargo.toml + - services/vordr/Cargo.toml mix-build: stage: build image: elixir:1.19.5-otp-28 script: + - cd opsm_ex - mix local.hex --force - mix deps.get - MIX_ENV=prod mix compile rules: - exists: - - mix.exs + - opsm_ex/mix.exs trufflehog: stage: security image: trufflesecurity/trufflehog:latest diff --git a/opsm_ex/lib/opsm/api/mobile_router.ex b/opsm_ex/lib/opsm/api/mobile_router.ex index e537cfd4..2c6aff65 100644 --- a/opsm_ex/lib/opsm/api/mobile_router.ex +++ b/opsm_ex/lib/opsm/api/mobile_router.ex @@ -7,14 +7,15 @@ defmodule Opsm.Api.MobileRouter do use Plug.Router - plug Plug.Logger + plug(Plug.Logger) - plug Plug.Parsers, + plug(Plug.Parsers, parsers: [:json], json_decoder: Jason + ) - plug :match - plug :dispatch + plug(:match) + plug(:dispatch) get "/api/health" do send_json(conn, 200, %{ @@ -29,32 +30,33 @@ defmodule Opsm.Api.MobileRouter do registry = conn.query_params["registry"] # Mock search results for testing - packages = [ - %{ - name: "react", - version: "18.2.0", - registry: "npm", - description: "A JavaScript library for building user interfaces" - }, - %{ - name: "vue", - version: "3.3.4", - registry: "npm", - description: "Progressive JavaScript Framework" - }, - %{ - name: "axum", - version: "0.7.3", - registry: "crates", - description: "Web framework for Rust" - } - ] - |> Enum.filter(fn pkg -> - String.contains?(String.downcase(pkg.name), String.downcase(query)) - end) - |> Enum.filter(fn pkg -> - is_nil(registry) || pkg.registry == registry - end) + packages = + [ + %{ + name: "react", + version: "18.2.0", + registry: "npm", + description: "A JavaScript library for building user interfaces" + }, + %{ + name: "vue", + version: "3.3.4", + registry: "npm", + description: "Progressive JavaScript Framework" + }, + %{ + name: "axum", + version: "0.7.3", + registry: "crates", + description: "Web framework for Rust" + } + ] + |> Enum.filter(fn pkg -> + String.contains?(String.downcase(pkg.name), String.downcase(query)) + end) + |> Enum.filter(fn pkg -> + is_nil(registry) || pkg.registry == registry + end) send_json(conn, 200, %{ packages: packages, @@ -127,6 +129,7 @@ defmodule Opsm.Api.MobileRouter do defp send_json(conn, status, data) do body = Jason.encode!(data) + conn |> Plug.Conn.put_resp_content_type("application/json") |> Plug.Conn.send_resp(status, body) diff --git a/opsm_ex/lib/opsm/api/nickel.ex b/opsm_ex/lib/opsm/api/nickel.ex index bd2662f7..60703f36 100644 --- a/opsm_ex/lib/opsm/api/nickel.ex +++ b/opsm_ex/lib/opsm/api/nickel.ex @@ -13,7 +13,9 @@ defmodule Opsm.Api.Nickel do _ -> path = write_temp(body) - case Opsm.SafeExec.cmd("nickel", ["export", "--format", "json", path], stderr_to_stdout: true) do + case Opsm.SafeExec.cmd("nickel", ["export", "--format", "json", path], + stderr_to_stdout: true + ) do {output, 0} -> Jason.decode(output) diff --git a/opsm_ex/lib/opsm/api/router.ex b/opsm_ex/lib/opsm/api/router.ex index f0619c2e..63f879a3 100644 --- a/opsm_ex/lib/opsm/api/router.ex +++ b/opsm_ex/lib/opsm/api/router.ex @@ -10,15 +10,16 @@ defmodule Opsm.Api.Router do alias Opsm.SmartInstall alias Opsm.Api.Nickel - plug Plug.Logger + plug(Plug.Logger) - plug Plug.Parsers, + plug(Plug.Parsers, parsers: [:json, :urlencoded], pass: ["application/nickel", "text/nickel"], json_decoder: Jason + ) - plug :match - plug :dispatch + plug(:match) + plug(:dispatch) get "/health" do send_json(conn, 200, %{status: "ok"}) @@ -79,8 +80,13 @@ defmodule Opsm.Api.Router do defp fetch_tokens(%{"tokens" => tokens}) when is_list(tokens), do: {:ok, tokens} defp fetch_tokens(%{tokens: tokens}) when is_list(tokens), do: {:ok, tokens} - defp fetch_tokens(%{"command" => command}) when is_binary(command), do: {:ok, command_to_tokens(command)} - defp fetch_tokens(%{command: command}) when is_binary(command), do: {:ok, command_to_tokens(command)} + + defp fetch_tokens(%{"command" => command}) when is_binary(command), + do: {:ok, command_to_tokens(command)} + + defp fetch_tokens(%{command: command}) when is_binary(command), + do: {:ok, command_to_tokens(command)} + defp fetch_tokens(_), do: {:error, "payload must include tokens or command"} defp command_to_tokens(command) do @@ -92,6 +98,7 @@ defmodule Opsm.Api.Router do defp send_json(conn, status, data) do body = Jason.encode!(data) + conn |> Plug.Conn.put_resp_content_type("application/json") |> Plug.Conn.send_resp(status, body) diff --git a/opsm_ex/lib/opsm/application.ex b/opsm_ex/lib/opsm/application.ex index 40a77103..2e3c80d3 100644 --- a/opsm_ex/lib/opsm/application.ex +++ b/opsm_ex/lib/opsm/application.ex @@ -24,8 +24,10 @@ defmodule Opsm.Application do children = [ RegistryGateway.Store, Opsm.VeriSimDB, - {Bandit, plug: RegistryGateway.Router, scheme: :http, port: registry_port(), ip: {127, 0, 0, 1}}, - {Bandit, plug: Opsm.Api.MobileRouter, scheme: :http, port: mobile_api_port(), ip: {127, 0, 0, 1}} + {Bandit, + plug: RegistryGateway.Router, scheme: :http, port: registry_port(), ip: {127, 0, 0, 1}}, + {Bandit, + plug: Opsm.Api.MobileRouter, scheme: :http, port: mobile_api_port(), ip: {127, 0, 0, 1}} ] opts = [strategy: :one_for_one, name: Opsm.Supervisor] diff --git a/opsm_ex/lib/opsm/cache.ex b/opsm_ex/lib/opsm/cache.ex index 94e8228d..3871643b 100644 --- a/opsm_ex/lib/opsm/cache.ex +++ b/opsm_ex/lib/opsm/cache.ex @@ -14,7 +14,8 @@ defmodule Opsm.Cache do """ @table_name :opsm_cache - @default_ttl_ms 5 * 60 * 1000 # 5 minutes + # 5 minutes + @default_ttl_ms 5 * 60 * 1000 # Client API @@ -27,6 +28,7 @@ defmodule Opsm.Cache do :undefined -> :ets.new(@table_name, [:set, :public, :named_table, read_concurrency: true]) :ok + _ -> :ok end @@ -96,6 +98,7 @@ defmodule Opsm.Cache do """ def stats do info = :ets.info(@table_name) + %{ size: Keyword.get(info, :size, 0), memory_bytes: Keyword.get(info, :memory, 0) * :erlang.system_info(:wordsize) @@ -121,6 +124,7 @@ defmodule Opsm.Cache do {:ok, _} -> put(key, value, ttl_ms) _ -> :ok end + value end end @@ -147,5 +151,4 @@ defmodule Opsm.Cache do def exists_key(forth, name) do {:exists, forth, name} end - end diff --git a/opsm_ex/lib/opsm/cli.ex b/opsm_ex/lib/opsm/cli.ex index 911c8778..64b5cd3b 100644 --- a/opsm_ex/lib/opsm/cli.ex +++ b/opsm_ex/lib/opsm/cli.ex @@ -16,7 +16,6 @@ defmodule Opsm.CLI do alias Opsm.Maintenance alias Opsm.SmartInstall - def main(args) do # Suppress noisy OTP/Bandit/Req retry logs in CLI mode — only show errors Logger.configure(level: :error) @@ -27,123 +26,192 @@ defmodule Opsm.CLI do end defp parse_args(args) do - {opts, args, _invalid} = OptionParser.parse(args, - strict: [ - help: :boolean, - version: :string, - allow: :string, - systemwide: :boolean, - user: :boolean, - global: :boolean, - yes: :boolean, - quiet: :boolean, - verbose: :boolean, - dry_run: :boolean, - refresh: :boolean, - downloadonly: :boolean, - installed: :boolean, - available: :boolean, - updates: :boolean, - obsoletes: :boolean, - all: :boolean, - recursive: :boolean, - reverse: :boolean, - json: :boolean, - limit: :integer, - native: :boolean, - dev: :boolean, - apply: :boolean, - port: :integer, - sustainability: :boolean, - from: :string, - workspace: :boolean, - registry: :string - ], - aliases: [ - h: :help, - v: :version, - y: :yes, - q: :quiet, - n: :dry_run, - g: :global, - D: :dev, - s: :sustainability - ] - ) + {opts, args, _invalid} = + OptionParser.parse(args, + strict: [ + help: :boolean, + version: :string, + allow: :string, + systemwide: :boolean, + user: :boolean, + global: :boolean, + yes: :boolean, + quiet: :boolean, + verbose: :boolean, + dry_run: :boolean, + refresh: :boolean, + downloadonly: :boolean, + installed: :boolean, + available: :boolean, + updates: :boolean, + obsoletes: :boolean, + all: :boolean, + recursive: :boolean, + reverse: :boolean, + json: :boolean, + limit: :integer, + native: :boolean, + dev: :boolean, + apply: :boolean, + port: :integer, + sustainability: :boolean, + from: :string, + workspace: :boolean, + registry: :string + ], + aliases: [ + h: :help, + v: :version, + y: :yes, + q: :quiet, + n: :dry_run, + g: :global, + D: :dev, + s: :sustainability + ] + ) case args do # Help - ["help" | _] -> {:help, opts} - ["--help" | _] -> {:help, opts} - ["-h" | _] -> {:help, opts} - [] -> {:help, opts} + ["help" | _] -> + {:help, opts} + + ["--help" | _] -> + {:help, opts} + + ["-h" | _] -> + {:help, opts} + + [] -> + {:help, opts} # Status & Info - ["status" | _] -> {:status, opts} - ["repolist" | _] -> {:repolist, opts} - ["api" | _] -> {:api, opts} + ["status" | _] -> + {:status, opts} + + ["repolist" | _] -> + {:repolist, opts} + + ["api" | _] -> + {:api, opts} # Install/Remove - ["install" | rest] -> parse_install_args(rest, opts) + ["install" | rest] -> + parse_install_args(rest, opts) + + ["remove", package | _] -> + {:remove, package, opts} - ["remove", package | _] -> {:remove, package, opts} - ["uninstall", package | _] -> {:remove, package, opts} - ["remove"] -> {:error, "remove requires a package argument"} + ["uninstall", package | _] -> + {:remove, package, opts} - ["reinstall", package | _] -> {:reinstall, package, opts} + ["remove"] -> + {:error, "remove requires a package argument"} + + ["reinstall", package | _] -> + {:reinstall, package, opts} # Update/Upgrade - ["update" | packages] -> {:update, packages, opts} - ["upgrade" | packages] -> {:update, packages, opts} - ["check-update" | _] -> {:check_update, opts} + ["update" | packages] -> + {:update, packages, opts} + + ["upgrade" | packages] -> + {:update, packages, opts} + + ["check-update" | _] -> + {:check_update, opts} # Search/Query - ["search", query | _] -> {:search, query, opts} - ["search"] -> {:error, "search requires a query"} + ["search", query | _] -> + {:search, query, opts} + + ["search"] -> + {:error, "search requires a query"} + + ["info", package | _] -> + {:info, package, opts} + + ["show", package | _] -> + {:info, package, opts} + + ["info"] -> + {:error, "info requires a package argument"} + + ["list" | rest] -> + {:list, rest, opts} - ["info", package | _] -> {:info, package, opts} - ["show", package | _] -> {:info, package, opts} - ["info"] -> {:error, "info requires a package argument"} + ["provides", file | _] -> + {:provides, file, opts} - ["list" | rest] -> {:list, rest, opts} + ["whatprovides", file | _] -> + {:provides, file, opts} - ["provides", file | _] -> {:provides, file, opts} - ["whatprovides", file | _] -> {:provides, file, opts} + ["depends", package | _] -> + {:depends, package, opts} - ["depends", package | _] -> {:depends, package, opts} - ["deplist", package | _] -> {:depends, package, opts} + ["deplist", package | _] -> + {:depends, package, opts} - ["rdepends", package | _] -> {:rdepends, package, opts} - ["repoquery", "--whatrequires", package | _] -> {:rdepends, package, opts} + ["rdepends", package | _] -> + {:rdepends, package, opts} + + ["repoquery", "--whatrequires", package | _] -> + {:rdepends, package, opts} # Pin/Hold - ["pin", package, version | _] -> {:pin, package, version, opts} - ["pin", package | _] -> {:pin, package, nil, opts} - ["hold", package | _] -> {:pin, package, nil, opts} - ["pin"] -> {:error, "pin requires a package argument"} + ["pin", package, version | _] -> + {:pin, package, version, opts} + + ["pin", package | _] -> + {:pin, package, nil, opts} + + ["hold", package | _] -> + {:pin, package, nil, opts} + + ["pin"] -> + {:error, "pin requires a package argument"} - ["unpin", package | _] -> {:unpin, package, opts} - ["unhold", package | _] -> {:unpin, package, opts} + ["unpin", package | _] -> + {:unpin, package, opts} + + ["unhold", package | _] -> + {:unpin, package, opts} # Maintenance - ["clean", what | _] -> {:clean, what, opts} - ["clean"] -> {:clean, "all", opts} + ["clean", what | _] -> + {:clean, what, opts} + + ["clean"] -> + {:clean, "all", opts} + + ["autoremove" | _] -> + {:autoremove, opts} + + ["history" | rest] -> + {:history, rest, opts} - ["autoremove" | _] -> {:autoremove, opts} + ["download", package | _] -> + {:download, package, opts} - ["history" | rest] -> {:history, rest, opts} + ["download"] -> + {:error, "download requires a package argument"} - ["download", package | _] -> {:download, package, opts} - ["download"] -> {:error, "download requires a package argument"} + ["check" | _] -> + {:check, opts} - ["check" | _] -> {:check, opts} - ["verify" | _] -> {:check, opts} + ["verify" | _] -> + {:check, opts} # Publish/Audit (OPSM-specific) - ["publish", path | _] -> {:publish, path, opts} - ["publish"] -> {:error, "publish requires a path argument"} + ["publish", path | _] -> + {:publish, path, opts} + + ["publish"] -> + {:error, "publish requires a path argument"} + + ["audit", package | _] -> + {:audit, package, opts} - ["audit", package | _] -> {:audit, package, opts} ["audit"] -> if Keyword.get(opts, :workspace, false) do {:audit_workspace, opts} @@ -152,37 +220,73 @@ defmodule Opsm.CLI do end # Federation - ["ports" | _] -> {:ports, opts} - ["convert", path | _] -> {:convert, path, opts} - ["export", package, target | _] -> {:export, package, target, opts} + ["ports" | _] -> + {:ports, opts} + + ["convert", path | _] -> + {:convert, path, opts} + + ["export", package, target | _] -> + {:export, package, target, opts} # Container commands - ["container", "build", path | _] -> {:container_build, path, opts} - ["container", "scan", image | _] -> {:container_scan, image, opts} - ["container", "sign", image | _] -> {:container_sign, image, opts} - ["container", "verify", image | _] -> {:container_verify, image, opts} - ["container", "push", image | _] -> {:container_push, image, opts} - ["container", "pipeline", path | _] -> {:container_pipeline, path, opts} - ["container"] -> {:error, "container requires a subcommand (build|scan|sign|verify|push|pipeline)"} + ["container", "build", path | _] -> + {:container_build, path, opts} + + ["container", "scan", image | _] -> + {:container_scan, image, opts} + + ["container", "sign", image | _] -> + {:container_sign, image, opts} + + ["container", "verify", image | _] -> + {:container_verify, image, opts} + + ["container", "push", image | _] -> + {:container_push, image, opts} + + ["container", "pipeline", path | _] -> + {:container_pipeline, path, opts} + + ["container"] -> + {:error, "container requires a subcommand (build|scan|sign|verify|push|pipeline)"} # Runtime management (asdf replacement) - ["runtime", "install" | tools] -> {:runtime_install, tools, opts} - ["runtime", "list" | _] -> {:runtime_list, opts} - ["runtime", "update" | tools] -> {:runtime_update, tools, opts} - ["runtime", "remove", tool | _] -> {:runtime_remove, tool, opts} - ["runtime", "which", tool | _] -> {:runtime_which, tool, opts} - ["runtime", "current" | _] -> {:runtime_current, opts} - ["runtime"] -> {:error, "runtime requires a subcommand (install|list|update|remove|which|current)"} + ["runtime", "install" | tools] -> + {:runtime_install, tools, opts} + + ["runtime", "list" | _] -> + {:runtime_list, opts} + + ["runtime", "update" | tools] -> + {:runtime_update, tools, opts} + + ["runtime", "remove", tool | _] -> + {:runtime_remove, tool, opts} + + ["runtime", "which", tool | _] -> + {:runtime_which, tool, opts} + + ["runtime", "current" | _] -> + {:runtime_current, opts} + + ["runtime"] -> + {:error, "runtime requires a subcommand (install|list|update|remove|which|current)"} # Security scanning - ["scan", package | _] -> {:scan, package, opts} - ["scan"] -> {:error, "scan requires a package argument"} + ["scan", package | _] -> + {:scan, package, opts} + + ["scan"] -> + {:error, "scan requires a package argument"} # TUI - ["tui" | _] -> {:tui, opts} + ["tui" | _] -> + {:tui, opts} # Unknown - [cmd | _] -> {:error, "Unknown command: #{cmd}"} + [cmd | _] -> + {:error, "Unknown command: #{cmd}"} end end @@ -198,7 +302,8 @@ defmodule Opsm.CLI do end defp parse_install_args([package], opts) do - if String.contains?(package, ":") or String.contains?(package, "[") or String.contains?(package, "]") do + if String.contains?(package, ":") or String.contains?(package, "[") or + String.contains?(package, "]") do {:smart_install, [package], opts} else {:install, nil, package, opts} @@ -411,16 +516,18 @@ defmodule Opsm.CLI do else IO.puts("Configured Forths (Registries)") IO.puts("==============================") + for f <- forths do IO.puts(" @#{f.name}\t#{f.url}\t[#{f.status}]") end end + System.halt(0) end defp run({:install_workspace, opts}) do dry_run? = Keyword.get(opts, :dry_run, false) - registry = Keyword.get(opts, :registry) + registry = Keyword.get(opts, :registry) IO.puts("Reading workspace from opsm.toml...") @@ -435,15 +542,19 @@ defmodule Opsm.CLI do for member <- members do manifest = Path.join(member, "opsm.toml") + if File.exists?(manifest) do IO.puts("\nInstalling dependencies for #{member}...") + unless dry_run? do config = Config.load_config_or_example() forth = registry || "hf" + case Wiring.run_publish(config, member) do {:ok, _} -> IO.puts(" ✓ #{member}") {:error, reason} -> IO.puts(:stderr, " ✗ #{member}: #{reason}") end + _ = forth end else @@ -468,32 +579,47 @@ defmodule Opsm.CLI do if workspace? do manifest = Path.join(path, "opsm.toml") + case File.read(manifest) do {:ok, content} -> members = parse_workspace_members(content) + if members == [] do IO.puts("No workspace members found in #{manifest}.") else IO.puts("Publishing workspace members to @#{registry || "hf"}...") + for member <- members do member_path = Path.join(path, member) IO.puts(" Publishing #{member}...") + case Wiring.run_publish(config, member_path) do - {:ok, _} -> IO.puts(" ✓ #{member}") + {:ok, _} -> + IO.puts(" ✓ #{member}") + {:error, reason} -> - Errors.print_error({:internal, "Publish failed for #{member}: #{reason}", "Check path and credentials"}) + Errors.print_error( + {:internal, "Publish failed for #{member}: #{reason}", + "Check path and credentials"} + ) end end end + {:error, :enoent} -> IO.puts(:stderr, "No opsm.toml found at #{manifest}") System.halt(1) end else case Wiring.run_publish(config, path) do - {:ok, _} -> :ok + {:ok, _} -> + :ok + {:error, reason} -> - Errors.print_error({:internal, "Publish failed: #{reason}", "Check path and credentials"}) + Errors.print_error( + {:internal, "Publish failed: #{reason}", "Check path and credentials"} + ) + System.halt(1) end end @@ -538,7 +664,9 @@ defmodule Opsm.CLI do failures = Enum.filter(results, fn {_, r} -> r != :ok end) - IO.puts("\nWorkspace audit complete: #{length(members) - length(failures)}/#{length(members)} passed.") + IO.puts( + "\nWorkspace audit complete: #{length(members) - length(failures)}/#{length(members)} passed." + ) if failures != [] do IO.puts("Failed: #{Enum.map_join(failures, ", ", fn {m, _} -> m end)}") @@ -558,17 +686,33 @@ defmodule Opsm.CLI do global? = Keyword.get(opts, :global, false) dev? = Keyword.get(opts, :dev, false) sustainability? = Keyword.get(opts, :sustainability, false) - scope = cond do - Keyword.get(opts, :systemwide) -> "systemwide" - true -> "user" - end + + scope = + cond do + Keyword.get(opts, :systemwide) -> "systemwide" + true -> "user" + end if dry_run?, do: IO.puts("[DRY RUN]") - if sustainability?, do: IO.puts("[SUSTAINABILITY] Preferring packages with higher sustainability scores") + + if sustainability?, + do: IO.puts("[SUSTAINABILITY] Preferring packages with higher sustainability scores") if forth do # Specific registry requested - do_install_from_forth(forth, package, version, allow, scope, dry_run?, json?, native?, global?, dev?, sustainability?) + do_install_from_forth( + forth, + package, + version, + allow, + scope, + dry_run?, + json?, + native?, + global?, + dev?, + sustainability? + ) else # No registry specified - discover across all do_install_discover(package, version, allow, scope, dry_run?, json?, sustainability?) @@ -608,8 +752,12 @@ defmodule Opsm.CLI do dry_run? = Keyword.get(opts, :dry_run, false) case Installer.remove(package, dry_run: dry_run?) do - :ok -> System.halt(0) - {:ok, :dry_run} -> System.halt(0) + :ok -> + System.halt(0) + + {:ok, :dry_run} -> + System.halt(0) + {:error, reason} -> IO.puts(:stderr, "Error: #{reason}") System.halt(1) @@ -628,7 +776,8 @@ defmodule Opsm.CLI do case Lockfile.read() do {:ok, lockfile} -> # Find the package across any forth - entries = Lockfile.list_packages(lockfile) + entries = + Lockfile.list_packages(lockfile) |> Enum.filter(fn p -> p.name == package end) case entries do @@ -649,10 +798,23 @@ defmodule Opsm.CLI do IO.puts(" Removing...") _ = Installer.remove(package, dry_run: false) IO.puts(" Installing #{package}@#{version}...") - case do_install_from_forth(forth, package, version, nil, "user", false, false, false, false, false) do + + case do_install_from_forth( + forth, + package, + version, + nil, + "user", + false, + false, + false, + false, + false + ) do _ -> :ok end end + System.halt(0) end @@ -678,25 +840,29 @@ defmodule Opsm.CLI do entries = Lockfile.list_packages(lockfile) # Filter to requested packages or all - targets = if packages == [] do - IO.puts("Checking all #{length(entries)} installed packages for updates...") - entries - else - IO.puts("Checking: #{Enum.join(packages, ", ")}") - Enum.filter(entries, fn p -> p.name in packages end) - end + targets = + if packages == [] do + IO.puts("Checking all #{length(entries)} installed packages for updates...") + entries + else + IO.puts("Checking: #{Enum.join(packages, ", ")}") + Enum.filter(entries, fn p -> p.name in packages end) + end - updates = targets + updates = + targets |> Enum.map(fn entry -> # Skip packages pinned to a specific version if Maintenance.pinned?(entry.name) do pin = Maintenance.get_pin(entry.name) pinned_ver = pin && pin["version"] + if pinned_ver do IO.puts(" Skipping #{entry.name} (pinned to #{pinned_ver})") else IO.puts(" Skipping #{entry.name} (pinned)") end + nil else case Registry.fetch(entry.forth, entry.name) do @@ -706,7 +872,9 @@ defmodule Opsm.CLI do else nil end - _ -> nil + + _ -> + nil end end end) @@ -716,19 +884,35 @@ defmodule Opsm.CLI do IO.puts("\n✓ All packages are up to date") else IO.puts("\n#{length(updates)} update(s) available:\n") + for {entry, new_version} <- updates do IO.puts(" #{entry.name}: #{entry.version} → #{new_version} (@#{entry.forth})") end unless dry_run? do IO.puts("\nInstalling updates...") + for {entry, new_version} <- updates do IO.puts(" Updating #{entry.name} to #{new_version}...") - do_install_from_forth(entry.forth, entry.name, new_version, nil, "user", false, false, false, false, false) + + do_install_from_forth( + entry.forth, + entry.name, + new_version, + nil, + "user", + false, + false, + false, + false, + false + ) end + IO.puts("\n✓ #{length(updates)} package(s) updated") end end + System.halt(0) {:error, :not_found} -> @@ -752,16 +936,24 @@ defmodule Opsm.CLI do entries = Lockfile.list_packages(lockfile) IO.puts("Checking #{length(entries)} installed package(s) for updates...\n") - updates = entries + updates = + entries |> Enum.map(fn entry -> case Registry.fetch(entry.forth, entry.name) do {:ok, latest} -> if latest.version != entry.version do - %{name: entry.name, current: entry.version, latest: latest.version, forth: entry.forth} + %{ + name: entry.name, + current: entry.version, + latest: latest.version, + forth: entry.forth + } else nil end - _ -> nil + + _ -> + nil end end) |> Enum.reject(&is_nil/1) @@ -786,6 +978,7 @@ defmodule Opsm.CLI do IO.puts("\nRun `opsm update` to install all updates") end end + System.halt(0) {:error, :not_found} -> @@ -819,14 +1012,19 @@ defmodule Opsm.CLI do else for {forth, packages} <- results, packages != [] do IO.puts("@#{forth}:") + for pkg <- Enum.take(packages, 5) do name = pkg[:name] || pkg["name"] version = pkg[:version] || pkg["version"] || "" desc = pkg[:description] || pkg["description"] || "" - desc_short = if String.length(desc) > 60, do: String.slice(desc, 0, 57) <> "...", else: desc + + desc_short = + if String.length(desc) > 60, do: String.slice(desc, 0, 57) <> "...", else: desc + IO.puts(" #{name}@#{version}") if desc_short != "", do: IO.puts(" #{desc_short}") end + IO.puts("") end end @@ -847,18 +1045,20 @@ defmodule Opsm.CLI do results = Registry.fetch_all(package) if json? do - data = Enum.map(results, fn {forth, pkg} -> - %{ - forth: forth, - name: pkg.package, - version: pkg.version, - description: pkg.manifest.description, - license: pkg.manifest.license, - homepage: pkg.manifest.homepage, - repository: pkg.manifest.repository, - tarball_url: pkg.tarball_url - } - end) + data = + Enum.map(results, fn {forth, pkg} -> + %{ + forth: forth, + name: pkg.package, + version: pkg.version, + description: pkg.manifest.description, + license: pkg.manifest.license, + homepage: pkg.manifest.homepage, + repository: pkg.manifest.repository, + tarball_url: pkg.tarball_url + } + end) + IO.puts(Jason.encode!(data, pretty: true)) else if map_size(results) == 0 do @@ -877,6 +1077,7 @@ defmodule Opsm.CLI do # Show dependencies count deps_count = map_size(pkg.manifest.dependencies) dev_deps_count = map_size(pkg.manifest.dev_dependencies) + if deps_count > 0 or dev_deps_count > 0 do IO.puts(" Dependencies: #{deps_count} (#{dev_deps_count} dev)") end @@ -893,16 +1094,18 @@ defmodule Opsm.CLI do alias Opsm.Package.Installer json? = Keyword.get(opts, :json, false) - filter = cond do - Keyword.get(opts, :installed) -> :installed - Keyword.get(opts, :available) -> :available - Keyword.get(opts, :updates) -> :updates - Keyword.get(opts, :obsoletes) -> :obsoletes - "installed" in args -> :installed - "available" in args -> :available - "updates" in args -> :updates - true -> :installed - end + + filter = + cond do + Keyword.get(opts, :installed) -> :installed + Keyword.get(opts, :available) -> :available + Keyword.get(opts, :updates) -> :updates + Keyword.get(opts, :obsoletes) -> :obsoletes + "installed" in args -> :installed + "available" in args -> :available + "updates" in args -> :updates + true -> :installed + end case filter do :installed -> @@ -916,6 +1119,7 @@ defmodule Opsm.CLI do else IO.puts("Installed packages:") IO.puts("") + for pkg <- installed do IO.puts(" #{pkg["name"]}@#{pkg["version"]} (@#{pkg["forth"]})") IO.puts(" Installed: #{pkg["installed_at"]}") @@ -931,12 +1135,15 @@ defmodule Opsm.CLI do for pkg <- installed do forth = Opsm.Validation.safe_to_forth(pkg["forth"]) + case Opsm.Registries.Registry.fetch(forth, pkg["name"]) do {:ok, latest} -> if latest.version != pkg["version"] do IO.puts(" #{pkg["name"]}: #{pkg["version"]} -> #{latest.version}") end - _ -> :ok + + _ -> + :ok end end @@ -944,9 +1151,11 @@ defmodule Opsm.CLI do IO.puts("Available package registries:") IO.puts("") forths = [:npm, :hex, :cargo, :pypi, :nimble, :idris2, :eclexia, :git, :agentic] + for forth <- forths do IO.puts(" @#{forth}") end + IO.puts("") IO.puts("Use `opsm search ` to find packages across all registries") @@ -957,18 +1166,21 @@ defmodule Opsm.CLI do if installed == [] do IO.puts("No packages installed") else - obsolete = Enum.filter(installed, fn pkg -> - forth = Opsm.Validation.safe_to_forth(pkg["forth"]) - case Opsm.Registries.Registry.exists?(forth, pkg["name"]) do - false -> true - _ -> false - end - end) + obsolete = + Enum.filter(installed, fn pkg -> + forth = Opsm.Validation.safe_to_forth(pkg["forth"]) + + case Opsm.Registries.Registry.exists?(forth, pkg["name"]) do + false -> true + _ -> false + end + end) if obsolete == [] do IO.puts("No obsolete packages found") else IO.puts("#{length(obsolete)} obsolete package(s) (no longer in registry):\n") + for pkg <- obsolete do IO.puts(" #{pkg["name"]}@#{pkg["version"]} (@#{pkg["forth"]})") end @@ -986,45 +1198,59 @@ defmodule Opsm.CLI do IO.puts("Finding package that provides: #{file}\n") # Check installed packages first - providers = case Lockfile.read() do - {:ok, lockfile} -> - install_dir = Path.expand("~/.local/share/opsm/packages") - Lockfile.list_packages(lockfile) - |> Enum.filter(fn entry -> - pkg_dir = Path.join(install_dir, "#{entry.name}-#{entry.version}") - if File.dir?(pkg_dir) do - # Search for matching file in installed package - case File.ls(pkg_dir) do - {:ok, files} -> - Enum.any?(files, fn f -> - String.contains?(f, file) or f == file - end) - _ -> false + providers = + case Lockfile.read() do + {:ok, lockfile} -> + install_dir = Path.expand("~/.local/share/opsm/packages") + + Lockfile.list_packages(lockfile) + |> Enum.filter(fn entry -> + pkg_dir = Path.join(install_dir, "#{entry.name}-#{entry.version}") + + if File.dir?(pkg_dir) do + # Search for matching file in installed package + case File.ls(pkg_dir) do + {:ok, files} -> + Enum.any?(files, fn f -> + String.contains?(f, file) or f == file + end) + + _ -> + false + end + else + false end - else - false - end - end) - |> Enum.map(fn entry -> - %{name: entry.name, version: entry.version, forth: entry.forth, source: "installed"} - end) - _ -> [] - end + end) + |> Enum.map(fn entry -> + %{name: entry.name, version: entry.version, forth: entry.forth, source: "installed"} + end) + + _ -> + [] + end # Also search across registries by name (the file might be the package name) - registry_matches = case Opsm.Registries.Registry.search_all(file, limit: 5) do - results when is_map(results) -> - results - |> Enum.flat_map(fn {forth, pkgs} -> - case pkgs do - list when is_list(list) -> - Enum.map(list, fn p -> Map.put(p, :forth, forth) |> Map.put(:source, "registry") end) - _ -> [] - end - end) - |> Enum.take(10) - _ -> [] - end + registry_matches = + case Opsm.Registries.Registry.search_all(file, limit: 5) do + results when is_map(results) -> + results + |> Enum.flat_map(fn {forth, pkgs} -> + case pkgs do + list when is_list(list) -> + Enum.map(list, fn p -> + Map.put(p, :forth, forth) |> Map.put(:source, "registry") + end) + + _ -> + [] + end + end) + |> Enum.take(10) + + _ -> + [] + end all_results = providers ++ registry_matches @@ -1033,14 +1259,17 @@ defmodule Opsm.CLI do else if providers != [] do IO.puts("Installed packages providing '#{file}':") + for p <- providers do IO.puts(" #{p.name}@#{p.version} (@#{p.forth})") end + IO.puts("") end if registry_matches != [] do IO.puts("Registry packages matching '#{file}':") + for p <- registry_matches do IO.puts(" #{p[:name]} #{p[:version] || ""} (@#{p[:forth]})") end @@ -1050,6 +1279,7 @@ defmodule Opsm.CLI do IO.puts("No packages found providing '#{file}'") end end + System.halt(0) end @@ -1128,6 +1358,7 @@ defmodule Opsm.CLI do IO.puts("No packages depend on #{package}") else IO.puts("Packages that depend on #{Colour.cyan(package)}:") + Enum.each(dependents, fn pkg -> IO.puts(" - #{Colour.cyan("#{pkg.name}")}@#{pkg.version}") end) @@ -1153,7 +1384,9 @@ defmodule Opsm.CLI do defp run({:unpin, package, _opts}) do case Maintenance.unpin(package) do - :ok -> System.halt(0) + :ok -> + System.halt(0) + {:error, reason} -> Errors.print_error({:error, reason}) System.halt(1) @@ -1164,7 +1397,9 @@ defmodule Opsm.CLI do dry_run = Keyword.get(opts, :dry_run, false) case Maintenance.clean(what, dry_run: dry_run) do - {:ok, _} -> System.halt(0) + {:ok, _} -> + System.halt(0) + {:error, reason} -> Errors.print_error({:error, reason}) System.halt(1) @@ -1193,38 +1428,48 @@ defmodule Opsm.CLI do else IO.puts("Recent operations:") IO.puts("") + history |> Enum.with_index(1) |> Enum.each(fn {entry, pos} -> IO.puts(" #{pos} | #{entry["id"]} | #{entry["timestamp"]} | #{entry["operation"]}") + if entry["details"]["package"] do IO.puts(" Package: #{entry["details"]["package"]}") end end) + IO.puts("") - IO.puts("Use `opsm history undo ` or `opsm history undo ` to reverse an operation.") + + IO.puts( + "Use `opsm history undo ` or `opsm history undo ` to reverse an operation." + ) end end + System.halt(0) "undo" -> # Support: `history undo` (last), `history undo 3` (by position), `history undo ` target = Enum.at(args, 1) - result = cond do - is_nil(target) -> - Maintenance.undo_last() + result = + cond do + is_nil(target) -> + Maintenance.undo_last() - match?({n, ""} when n > 0, Integer.parse(target)) -> - {n, _} = Integer.parse(target) - Maintenance.undo_by_id(n) + match?({n, ""} when n > 0, Integer.parse(target)) -> + {n, _} = Integer.parse(target) + Maintenance.undo_by_id(n) - true -> - Maintenance.undo_by_id(target) - end + true -> + Maintenance.undo_by_id(target) + end case result do - {:ok, _, _} -> System.halt(0) + {:ok, _, _} -> + System.halt(0) + {:error, reason} -> Errors.print_error({:error, reason}) System.halt(1) @@ -1232,11 +1477,13 @@ defmodule Opsm.CLI do "info" -> id = Enum.at(args, 1) + if id do case Maintenance.get_history_entry(id) do nil -> IO.puts("History entry not found: #{id}") System.halt(1) + entry -> IO.puts(Jason.encode!(entry, pretty: true)) System.halt(0) @@ -1254,7 +1501,9 @@ defmodule Opsm.CLI do "redo" -> case Maintenance.redo_last() do - {:ok, _, _} -> System.halt(0) + {:ok, _, _} -> + System.halt(0) + {:error, reason} -> Errors.print_error({:error, reason}) System.halt(1) @@ -1312,11 +1561,14 @@ defmodule Opsm.CLI do case Lockfile.verify_integrity(lockfile) do :ok -> IO.puts("✓ Lockfile integrity: SHA3-512 hash verified") + {:ok, :no_integrity_hash} -> IO.puts("⚠ Lockfile integrity: No integrity hash (legacy lockfile)") + {:error, reason} -> IO.puts("✗ Lockfile integrity: #{reason}") end + {:error, _} -> IO.puts("⚠ No lockfile found") end @@ -1326,59 +1578,63 @@ defmodule Opsm.CLI do # 2. Verify installed packages against their recorded checksums installed = Installer.list_installed() - results = Enum.map(installed, fn pkg -> - name = pkg["name"] - version = pkg["version"] - forth = pkg["forth"] - path = pkg["path"] - recorded_checksum = pkg["checksum"] - - dir_exists = File.dir?(path) - - # Try to recompute checksum from cached tarball - checksum_status = cond do - not dir_exists -> - :missing - - is_nil(recorded_checksum) or recorded_checksum == "" -> - :no_checksum - - true -> - # Find cached tarball and recompute - cache_dir = Path.expand("~/.cache/opsm/packages") - cache_pattern = Path.join([cache_dir, forth, "#{name}-#{version}*"]) - - cached_files = Path.wildcard(cache_pattern) - case cached_files do - [cached_tarball | _] -> - recomputed = Downloader.compute_file_checksum(cached_tarball, :sha256) - # Check against SHA256 first, then SHA1 (npm uses SHA1) - if recomputed == recorded_checksum do - :verified - else - sha1 = Downloader.compute_file_checksum(cached_tarball, :sha1) - if sha1 == recorded_checksum do - :verified - else - :tampered - end - end + results = + Enum.map(installed, fn pkg -> + name = pkg["name"] + version = pkg["version"] + forth = pkg["forth"] + path = pkg["path"] + recorded_checksum = pkg["checksum"] + + dir_exists = File.dir?(path) + + # Try to recompute checksum from cached tarball + checksum_status = + cond do + not dir_exists -> + :missing + + is_nil(recorded_checksum) or recorded_checksum == "" -> + :no_checksum + + true -> + # Find cached tarball and recompute + cache_dir = Path.expand("~/.cache/opsm/packages") + cache_pattern = Path.join([cache_dir, forth, "#{name}-#{version}*"]) + + cached_files = Path.wildcard(cache_pattern) + + case cached_files do + [cached_tarball | _] -> + recomputed = Downloader.compute_file_checksum(cached_tarball, :sha256) + # Check against SHA256 first, then SHA1 (npm uses SHA1) + if recomputed == recorded_checksum do + :verified + else + sha1 = Downloader.compute_file_checksum(cached_tarball, :sha1) - [] -> - # No cached tarball — can't verify, but files exist - :cache_missing + if sha1 == recorded_checksum do + :verified + else + :tampered + end + end + + [] -> + # No cached tarball — can't verify, but files exist + :cache_missing + end end - end - %{ - name: name, - version: version, - forth: forth, - path: path, - installed: dir_exists, - checksum_status: checksum_status - } - end) + %{ + name: name, + version: version, + forth: forth, + path: path, + installed: dir_exists, + checksum_status: checksum_status + } + end) if json? do IO.puts(Jason.encode!(results, pretty: true)) @@ -1397,6 +1653,7 @@ defmodule Opsm.CLI do if cache_missing != [] do IO.puts(" ⚠ #{length(cache_missing)} not verifiable (cached tarball cleaned):") + for r <- cache_missing do IO.puts(" - #{r.name}@#{r.version}") end @@ -1404,6 +1661,7 @@ defmodule Opsm.CLI do if no_checksum != [] do IO.puts(" ⚠ #{length(no_checksum)} without checksums:") + for r <- no_checksum do IO.puts(" - #{r.name}@#{r.version}") end @@ -1411,6 +1669,7 @@ defmodule Opsm.CLI do if tampered != [] do IO.puts(" ✗ #{length(tampered)} CHECKSUM MISMATCH:") + for r <- tampered do IO.puts(" - #{r.name}@#{r.version} — REINSTALL RECOMMENDED") end @@ -1418,6 +1677,7 @@ defmodule Opsm.CLI do if missing != [] do IO.puts(" ✗ #{length(missing)} missing from disk:") + for r <- missing do IO.puts(" - #{r.name}@#{r.version}") end @@ -1428,7 +1688,9 @@ defmodule Opsm.CLI do end end - System.halt(if(Enum.any?(results, fn r -> r.checksum_status == :tampered end), do: 1, else: 0)) + System.halt( + if(Enum.any?(results, fn r -> r.checksum_status == :tampered end), do: 1, else: 0) + ) end defp run({:ports, opts}) do @@ -1436,13 +1698,16 @@ defmodule Opsm.CLI do available = Federation.available_connection_ports() if json? do - data = Enum.map(available, fn {name, info} -> - %{name: name, command: info.command, path: info.path} - end) + data = + Enum.map(available, fn {name, info} -> + %{name: name, command: info.command, path: info.path} + end) + IO.puts(Jason.encode!(data, pretty: true)) else IO.puts("Available Connection Ports (System Package Managers)") IO.puts("====================================================") + if map_size(available) == 0 do IO.puts(" No system package managers detected") else @@ -1450,9 +1715,11 @@ defmodule Opsm.CLI do IO.puts(" @#{name}\t#{info.command}\t(#{info.path})") end end + IO.puts("") IO.puts("Use 'opsm install @ ' to install via system PM") end + System.halt(0) end @@ -1473,12 +1740,18 @@ defmodule Opsm.CLI do if manifest.description, do: IO.puts("Description: #{manifest.description}") if manifest.license, do: IO.puts("License: #{manifest.license}") if manifest.repository, do: IO.puts("Repository: #{manifest.repository}") - if manifest.authors != [], do: IO.puts("Authors: #{Enum.join(manifest.authors, ", ")}") - if manifest.keywords != [], do: IO.puts("Keywords: #{Enum.join(manifest.keywords, ", ")}") + + if manifest.authors != [], + do: IO.puts("Authors: #{Enum.join(manifest.authors, ", ")}") + + if manifest.keywords != [], + do: IO.puts("Keywords: #{Enum.join(manifest.keywords, ", ")}") + IO.puts("") IO.puts("Dependencies: #{map_size(manifest.dependencies)}") IO.puts("Dev deps: #{map_size(manifest.dev_dependencies)}") end + System.halt(0) {:error, reason} -> @@ -1498,20 +1771,27 @@ defmodule Opsm.CLI do IO.puts(" Target: #{info.command} (#{info.path})") # Try to find the package in lockfile first, then resolve it - pkg = case Opsm.Lockfile.read() do - {:ok, lockfile} -> - entries = Opsm.Lockfile.list_packages(lockfile) - |> Enum.filter(fn p -> p.name == package end) - case entries do - [entry | _] -> - case Opsm.Registries.Registry.fetch(entry.forth, entry.name, entry.version) do - {:ok, resolved} -> resolved - _ -> nil - end - _ -> nil - end - _ -> nil - end + pkg = + case Opsm.Lockfile.read() do + {:ok, lockfile} -> + entries = + Opsm.Lockfile.list_packages(lockfile) + |> Enum.filter(fn p -> p.name == package end) + + case entries do + [entry | _] -> + case Opsm.Registries.Registry.fetch(entry.forth, entry.name, entry.version) do + {:ok, resolved} -> resolved + _ -> nil + end + + _ -> + nil + end + + _ -> + nil + end if pkg do if dry_run? do @@ -1519,10 +1799,12 @@ defmodule Opsm.CLI do IO.puts(" [DRY RUN] Would run: #{info.command} install #{package}") else IO.puts(" Installing #{package} via #{info.command}...") + case Federation.install_via_port(package, target_atom) do {:ok, output} -> IO.puts(" ✓ Exported and installed via #{info.command}") if is_binary(output) and output != "", do: IO.puts(" #{output}") + {:error, reason} -> IO.puts(:stderr, " Error: #{reason}") System.halt(1) @@ -1534,16 +1816,19 @@ defmodule Opsm.CLI do IO.puts(" [DRY RUN] Would install #{package} via #{info.command}") else IO.puts(" Installing #{package} via #{info.command}...") + case Federation.install_via_port(package, target_atom) do {:ok, output} -> IO.puts(" ✓ Installed via #{info.command}") if is_binary(output) and output != "", do: IO.puts(" #{output}") + {:error, reason} -> IO.puts(:stderr, " Error: #{reason}") System.halt(1) end end end + System.halt(0) {:error, _reason} -> @@ -1689,7 +1974,8 @@ defmodule Opsm.CLI do forth = if forth_str, do: Opsm.Validation.safe_to_forth(forth_str), else: :npm # Resolve version from lockfile if available and no --version given - version = Keyword.get(opts, :version) || resolve_installed_version(package, lockfile: Lockfile) + version = + Keyword.get(opts, :version) || resolve_installed_version(package, lockfile: Lockfile) IO.puts("Scanning #{package}#{if version, do: "@#{version}", else: ""}...") @@ -1747,7 +2033,9 @@ defmodule Opsm.CLI do else IO.puts("Installed runtime tools") IO.puts("=======================") - max_name = installed |> Enum.map(fn t -> String.length(t.name) end) |> Enum.max(fn -> 4 end) + + max_name = + installed |> Enum.map(fn t -> String.length(t.name) end) |> Enum.max(fn -> 4 end) for tool <- installed do name_pad = String.pad_trailing(tool.name, max_name) @@ -1777,21 +2065,25 @@ defmodule Opsm.CLI do {:ok, updates} -> IO.puts("#{length(updates)} update(s) available:\n") max_name = updates |> Enum.map(fn u -> String.length(u.name) end) |> Enum.max(fn -> 4 end) - max_cur = updates |> Enum.map(fn u -> String.length(u.current) end) |> Enum.max(fn -> 7 end) + + max_cur = + updates |> Enum.map(fn u -> String.length(u.current) end) |> Enum.max(fn -> 7 end) for u <- updates do name_pad = String.pad_trailing(u.name, max_name) - cur_pad = String.pad_trailing(u.current, max_cur) + cur_pad = String.pad_trailing(u.current, max_cur) IO.puts(" #{name_pad} #{cur_pad} → #{u.latest}") end unless dry_run? do IO.puts("") IO.puts("Updating...") + for u <- updates do case Manager.install(u.name, u.latest) do :ok -> IO.puts(" ✓ #{u.name} #{u.current} → #{u.latest}") + {:error, reason} -> IO.puts(:stderr, " ✗ #{u.name}: #{inspect(reason)}") end @@ -1815,14 +2107,17 @@ defmodule Opsm.CLI do case Manager.latest_version(tool) do {:ok, latest} -> current = Manager.current_version(tool) + if current == latest do IO.puts(" #{tool} is already at #{latest}") else IO.puts(" #{tool}: #{current} → #{latest}") + unless dry_run? do case Manager.install(tool, latest) do :ok -> IO.puts(" ✓ Updated #{tool} to #{latest}") + {:error, reason} -> IO.puts(:stderr, " ✗ #{tool}: #{inspect(reason)}") end @@ -1854,8 +2149,10 @@ defmodule Opsm.CLI do {:ok, tools} -> IO.puts("Installing #{length(tools)} pinned runtime tool(s)...") + for {tool, version} <- tools do IO.puts(" #{tool}@#{version}") + unless dry_run? do case Manager.install(tool, version) do :ok -> IO.puts(" ✓ #{tool}@#{version}") @@ -1879,14 +2176,16 @@ defmodule Opsm.CLI do for spec <- tool_specs do {tool, version} = parse_tool_spec(spec) - resolved_version = if version == "latest" do - case Manager.latest_version(tool) do - {:ok, v} -> v - {:error, _} -> "latest" + + resolved_version = + if version == "latest" do + case Manager.latest_version(tool) do + {:ok, v} -> v + {:error, _} -> "latest" + end + else + version end - else - version - end IO.puts("Installing #{tool}@#{resolved_version}...") @@ -1895,9 +2194,11 @@ defmodule Opsm.CLI do :ok -> IO.puts("✓ Installed #{tool}@#{resolved_version}") IO.puts(" Run: opsm runtime which #{tool}") + {:error, {:missing_system_dependencies, deps}} -> IO.puts(:stderr, "✗ #{tool}: missing system dependencies: #{Enum.join(deps, ", ")}") System.halt(1) + {:error, reason} -> IO.puts(:stderr, "✗ #{tool}: #{inspect(reason)}") System.halt(1) @@ -1916,8 +2217,10 @@ defmodule Opsm.CLI do case Manager.remove(tool) do :ok -> IO.puts("✓ Removed #{tool}") + {:error, :not_installed} -> IO.puts("#{tool} is not installed") + {:error, reason} -> IO.puts(:stderr, "Error removing #{tool}: #{inspect(reason)}") System.halt(1) @@ -1932,6 +2235,7 @@ defmodule Opsm.CLI do case Manager.which(tool) do {:ok, path} -> IO.puts(path) + {:error, :not_installed} -> IO.puts(:stderr, "#{tool} is not managed by OPSM runtime") System.halt(1) @@ -1954,6 +2258,7 @@ defmodule Opsm.CLI do else IO.puts("Active runtime tools") IO.puts("====================") + for {tool, version} <- current do IO.puts(" #{tool} #{version}") end @@ -1968,14 +2273,17 @@ defmodule Opsm.CLI do {:ok, lf} -> case lockfile_mod.packages_for_name(lf, package) do [entry | _] -> entry.version - [] -> nil + [] -> nil end - _ -> nil + + _ -> + nil end end defp find_tui_binary do env_override = System.get_env("OPSM_TUI_BIN") + cond do env_override != nil and File.exists?(env_override) -> env_override path = System.find_executable("opsm-tui") -> path @@ -1999,6 +2307,7 @@ defmodule Opsm.CLI do IO.puts(Jason.encode!(MapSet.to_list(all_deps), pretty: true)) else IO.puts("All dependencies (#{MapSet.size(all_deps)}):") + all_deps |> MapSet.to_list() |> Enum.sort() @@ -2042,6 +2351,7 @@ defmodule Opsm.CLI do IO.puts(Jason.encode!(all_packages, pretty: true)) else IO.puts("All dependencies (#{length(all_packages)}):") + Enum.each(all_packages, fn name -> {version, _} = Map.get(resolution, name) IO.puts(" - #{name}@#{version}") @@ -2088,7 +2398,19 @@ defmodule Opsm.CLI do # Install helpers - defp do_install_from_forth(forth, package, version, _allow, scope, dry_run?, _json?, native?, global?, dev?, sustainability? \\ false) do + defp do_install_from_forth( + forth, + package, + version, + _allow, + scope, + dry_run?, + _json?, + native?, + global?, + dev?, + sustainability? \\ false + ) do alias Opsm.Package.Installer alias Opsm.Package.Native @@ -2101,17 +2423,21 @@ defmodule Opsm.CLI do IO.puts("") if dry_run? do - {cmd, args} = Native.preview_command(forth_atom, package, - version: if(version == "latest", do: nil, else: version), - global: global?, - dev: dev?) + {cmd, args} = + Native.preview_command(forth_atom, package, + version: if(version == "latest", do: nil, else: version), + global: global?, + dev: dev? + ) + IO.puts("[DRY RUN] Would run: #{cmd} #{Enum.join(args, " ")}") System.halt(0) else case Native.install(forth_atom, package, version: if(version == "latest", do: nil, else: version), global: global?, - dev: dev?) do + dev: dev? + ) do {:ok, _} -> IO.puts("") IO.puts("✓ Installed #{package} via native toolchain") @@ -2128,7 +2454,8 @@ defmodule Opsm.CLI do version: version, scope: scope_atom, dry_run: dry_run?, - sustainability_preference: sustainability?) do + sustainability_preference: sustainability? + ) do {:ok, _} -> System.halt(0) @@ -2149,7 +2476,8 @@ defmodule Opsm.CLI do IO.puts("Checking registries...") existence = Registry.exists_all?(package) - found_in = existence + found_in = + existence |> Enum.filter(fn {_, exists} -> exists end) |> Enum.map(fn {forth, _} -> forth end) @@ -2159,6 +2487,7 @@ defmodule Opsm.CLI do found_in: found_in, availability: existence } + IO.puts(Jason.encode!(data, pretty: true)) System.halt(0) end @@ -2173,12 +2502,15 @@ defmodule Opsm.CLI do # Check for alternatives discovery = Federation.discover(package) + if map_size(discovery.alternatives) > 0 do IO.puts("Related packages in other ecosystems:") + for {forth, pkgs} <- discovery.alternatives, pkgs != [] do IO.puts(" @#{forth}: #{Enum.join(pkgs, ", ")}") end end + System.halt(1) length(found_in) == 1 -> @@ -2186,61 +2518,101 @@ defmodule Opsm.CLI do [forth] = found_in IO.puts("Found in @#{forth}") IO.puts("") - do_install_from_forth(to_string(forth), package, version, allow, scope, dry_run?, false, false, false, false, sustainability?) + + do_install_from_forth( + to_string(forth), + package, + version, + allow, + scope, + dry_run?, + false, + false, + false, + false, + sustainability? + ) true -> # Found in multiple registries — show options and auto-select best IO.puts("Package '#{package}' available from:") - registry_info = for forth <- found_in do - toolchain_ok = case Federation.check_toolchain(forth) do - {:ok, _} -> true - {:error, _} -> false - end - pkg_version = try do - case Registry.fetch(forth, package) do - {:ok, pkg} -> pkg.version - _ -> "unknown" - end - rescue - _ -> "unknown" + + registry_info = + for forth <- found_in do + toolchain_ok = + case Federation.check_toolchain(forth) do + {:ok, _} -> true + {:error, _} -> false + end + + pkg_version = + try do + case Registry.fetch(forth, package) do + {:ok, pkg} -> pkg.version + _ -> "unknown" + end + rescue + _ -> "unknown" + end + + IO.puts( + " @#{forth} v#{pkg_version} #{if toolchain_ok, do: "✓ toolchain ready", else: "✗ toolchain missing"}" + ) + + {forth, toolchain_ok, pkg_version} end - IO.puts(" @#{forth} v#{pkg_version} #{if toolchain_ok, do: "✓ toolchain ready", else: "✗ toolchain missing"}") - {forth, toolchain_ok, pkg_version} - end # Auto-select: prefer primary registries with toolchains, then by highest version # Primary registries are the canonical homes — npm for JS, cargo for Rust, etc. primary_forths = [:npm, :cargo, :hex, :pypi, :gem, :go, :pub, :hackage, :nuget, :maven] - selected = registry_info - |> Enum.filter(fn {_forth, toolchain_ok, _v} -> toolchain_ok end) - |> case do - [] -> registry_info - with_toolchain -> with_toolchain - end - |> Enum.sort_by(fn {forth, _ok, version} -> - primary_rank = case Enum.find_index(primary_forths, &(&1 == forth)) do - nil -> 100 - idx -> idx + selected = + registry_info + |> Enum.filter(fn {_forth, toolchain_ok, _v} -> toolchain_ok end) + |> case do + [] -> registry_info + with_toolchain -> with_toolchain end - # Prefer primary registries, then highest version - {primary_rank, version} - end) - |> List.first() + |> Enum.sort_by(fn {forth, _ok, version} -> + primary_rank = + case Enum.find_index(primary_forths, &(&1 == forth)) do + nil -> 100 + idx -> idx + end + + # Prefer primary registries, then highest version + {primary_rank, version} + end) + |> List.first() case selected do {forth, _ok, _v} -> IO.puts("") IO.puts("Auto-selected: @#{forth}") IO.puts("") - do_install_from_forth(to_string(forth), package, version, allow, scope, dry_run?, false, false, false, false, sustainability?) + + do_install_from_forth( + to_string(forth), + package, + version, + allow, + scope, + dry_run?, + false, + false, + false, + false, + sustainability? + ) nil -> IO.puts("") IO.puts("Could not auto-select a registry. Specify one:") + for forth <- found_in do IO.puts(" opsm install @#{forth} #{package}") end + System.halt(1) end end @@ -2258,12 +2630,13 @@ defmodule Opsm.CLI do defp parse_tool_spec(spec) do case String.split(spec, "@", parts: 2) do [tool, version] -> {tool, version} - [tool] -> {tool, "latest"} + [tool] -> {tool, "latest"} end end defp print_smart_plan(plan) do IO.puts("Smart install plan (grouped by backend):") + Enum.each(plan, fn {backend, pkgs} -> status = case SmartInstall.backend_availability(backend) do diff --git a/opsm_ex/lib/opsm/clients/checky_monkey.ex b/opsm_ex/lib/opsm/clients/checky_monkey.ex index 979ecebd..cc06850f 100644 --- a/opsm_ex/lib/opsm/clients/checky_monkey.ex +++ b/opsm_ex/lib/opsm/clients/checky_monkey.ex @@ -6,6 +6,7 @@ defmodule Opsm.Clients.CheckyMonkey do """ alias Opsm.Http + alias Opsm.Types.{ ServiceConfig, HttpConfig, @@ -39,14 +40,17 @@ defmodule Opsm.Clients.CheckyMonkey do case Http.post_json(client, "/verify/submit", body) do :ok -> # Return a queued response - actual status fetched via get_status - {:ok, %CheckyMonkeyResponse{ - request_id: "pending", - status: :queued, - started_at: nil, - completed_at: nil, - results: nil - }} - {:error, reason} -> {:error, reason} + {:ok, + %CheckyMonkeyResponse{ + request_id: "pending", + status: :queued, + started_at: nil, + completed_at: nil, + results: nil + }} + + {:error, reason} -> + {:error, reason} end end @@ -121,12 +125,15 @@ defmodule Opsm.Clients.CheckyMonkey do def health(%__MODULE__{client: client}) do case Http.get_json(client, "/health") do {:ok, json} -> - {:ok, %OikosHealthResponse{ - status: decode_status(json["status"]), - version: json["version"] || "unknown", - uptime: json["uptime"] || 0 - }} - {:error, reason} -> {:error, reason} + {:ok, + %OikosHealthResponse{ + status: decode_status(json["status"]), + version: json["version"] || "unknown", + uptime: json["uptime"] || 0 + }} + + {:error, reason} -> + {:error, reason} end end @@ -151,9 +158,11 @@ defmodule Opsm.Clients.CheckyMonkey do end defp decode_results(nil), do: nil + defp decode_results(results) when is_list(results) do Enum.map(results, &decode_verification_result/1) end + defp decode_results(_), do: nil defp decode_verification_result(json) when is_map(json) do @@ -165,6 +174,7 @@ defmodule Opsm.Clients.CheckyMonkey do duration: json["duration"] || 0 } end + defp decode_verification_result(_), do: nil defp decode_verification_type("PropertyTests"), do: :property_tests diff --git a/opsm_ex/lib/opsm/clients/oikos.ex b/opsm_ex/lib/opsm/clients/oikos.ex index 56b36e16..e4c5585e 100644 --- a/opsm_ex/lib/opsm/clients/oikos.ex +++ b/opsm_ex/lib/opsm/clients/oikos.ex @@ -6,6 +6,7 @@ defmodule Opsm.Clients.Oikos do """ alias Opsm.Http + alias Opsm.Types.{ ServiceConfig, HttpConfig, @@ -88,10 +89,11 @@ defmodule Opsm.Clients.Oikos do """ def analyze_packages(%__MODULE__{} = oikos, packages) do packages - |> Task.async_stream(fn {name, version, opts} -> - {:ok, score} = analyze_package(oikos, name, version, opts) - {"#{name}@#{version}", score} - end, max_concurrency: 5, timeout: 10_000, on_timeout: :kill_task) + |> Task.async_stream( + fn {name, version, opts} -> + {:ok, score} = analyze_package(oikos, name, version, opts) + {"#{name}@#{version}", score} + end, max_concurrency: 5, timeout: 10_000, on_timeout: :kill_task) |> Enum.reduce(%{}, fn {:ok, {key, score}}, acc -> Map.put(acc, key, score) {:exit, _}, acc -> acc @@ -103,18 +105,20 @@ defmodule Opsm.Clients.Oikos do base = 50 # Bonus for well-known ecosystems - ecosystem_bonus = case forth do - f when f in [:npm, :cargo, :hex, :pypi, :gem, :go] -> 10 - f when f in [:pub, :hackage, :nuget, :maven] -> 8 - _ -> 0 - end + ecosystem_bonus = + case forth do + f when f in [:npm, :cargo, :hex, :pypi, :gem, :go] -> 10 + f when f in [:pub, :hackage, :nuget, :maven] -> 8 + _ -> 0 + end # Bonus for semver-compliant versions (indicates maturity) - version_bonus = case Version.parse(version || "") do - {:ok, %{major: m}} when m >= 1 -> 15 - {:ok, _} -> 5 - :error -> 0 - end + version_bonus = + case Version.parse(version || "") do + {:ok, %{major: m}} when m >= 1 -> 15 + {:ok, _} -> 5 + :error -> 0 + end # Penalty for very short names (potential typosquatting) name_penalty = if String.length(name || "") < 3, do: -10, else: 0 @@ -136,6 +140,7 @@ defmodule Opsm.Clients.Oikos do end defp decode_scores(nil), do: %SustainabilityScores{} + defp decode_scores(json) do %SustainabilityScores{ maintainability: json["maintainability"] || 0, diff --git a/opsm_ex/lib/opsm/clients/palimpsest.ex b/opsm_ex/lib/opsm/clients/palimpsest.ex index 5fb7f88d..5a824654 100644 --- a/opsm_ex/lib/opsm/clients/palimpsest.ex +++ b/opsm_ex/lib/opsm/clients/palimpsest.ex @@ -7,6 +7,7 @@ defmodule Opsm.Clients.Palimpsest do """ alias Opsm.Http + alias Opsm.Types.{ ServiceConfig, HttpConfig, @@ -40,11 +41,14 @@ defmodule Opsm.Clients.Palimpsest do case Http.post_json(client, "/licenses/analyze", body) do :ok -> encoded_path = URI.encode_www_form(request.artifact_path) + case Http.get_json(client, "/licenses/analysis/#{encoded_path}") do {:ok, json} -> {:ok, decode_response(json)} {:error, reason} -> {:error, reason} end - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end @@ -54,12 +58,15 @@ defmodule Opsm.Clients.Palimpsest do def health(%__MODULE__{client: client}) do case Http.get_json(client, "/health") do {:ok, json} -> - {:ok, %OikosHealthResponse{ - status: decode_status(json["status"]), - version: json["version"] || "unknown", - uptime: json["uptime"] || 0 - }} - {:error, reason} -> {:error, reason} + {:ok, + %OikosHealthResponse{ + status: decode_status(json["status"]), + version: json["version"] || "unknown", + uptime: json["uptime"] || 0 + }} + + {:error, reason} -> + {:error, reason} end end @@ -73,6 +80,7 @@ defmodule Opsm.Clients.Palimpsest do {:ok, json} -> {:ok, json} {:error, _} -> {:ok, %{"raw" => output}} end + {error, _code} -> {:error, "license-checker failed: #{error}"} end @@ -94,9 +102,11 @@ defmodule Opsm.Clients.Palimpsest do end defp decode_licenses(nil), do: [] + defp decode_licenses(licenses) when is_list(licenses) do Enum.map(licenses, &decode_license/1) end + defp decode_licenses(_), do: [] defp decode_license(json) when is_map(json) do @@ -107,6 +117,7 @@ defmodule Opsm.Clients.Palimpsest do source: decode_license_source(json["source"]) } end + defp decode_license(_), do: nil defp decode_license_source("file_header"), do: :file_header @@ -128,9 +139,11 @@ defmodule Opsm.Clients.Palimpsest do end defp decode_conflicts(nil), do: [] + defp decode_conflicts(conflicts) when is_list(conflicts) do Enum.map(conflicts, &decode_conflict/1) end + defp decode_conflicts(_), do: [] defp decode_conflict(json) when is_map(json) do @@ -141,6 +154,7 @@ defmodule Opsm.Clients.Palimpsest do severity: decode_conflict_severity(json["severity"]) } end + defp decode_conflict(_), do: nil defp decode_conflict_severity("error"), do: :error diff --git a/opsm_ex/lib/opsm/config.ex b/opsm_ex/lib/opsm/config.ex index 2f7928ea..bb79de38 100644 --- a/opsm_ex/lib/opsm/config.ex +++ b/opsm_ex/lib/opsm/config.ex @@ -68,6 +68,7 @@ defmodule Opsm.Config do defp load_from_local do path = "opsm.toml" + if File.exists?(path) do load_config_from(path) else @@ -77,9 +78,12 @@ defmodule Opsm.Config do defp load_from_user do case System.get_env("HOME") do - nil -> {:error, "HOME not set"} + nil -> + {:error, "HOME not set"} + home -> path = Path.join([home, ".config", "opsm", "opsm.toml"]) + if File.exists?(path) do load_config_from(path) else @@ -148,10 +152,11 @@ defmodule Opsm.Config do defp format_toml_error(path, message) do # Extract line number if present in message - line_info = case Regex.run(~r/line (\d+)/i, to_string(message)) do - [_, line] -> " at line #{line}" - _ -> "" - end + line_info = + case Regex.run(~r/line (\d+)/i, to_string(message)) do + [_, line] -> " at line #{line}" + _ -> "" + end """ TOML syntax error in #{path}#{line_info} @@ -173,18 +178,21 @@ defmodule Opsm.Config do http = parse_http_config(raw["http"]) with {:ok, checky_monkey} <- parse_service_config(raw["checky_monkey"], :checky_monkey), - {:ok, palimpsest_license} <- parse_service_config(raw["palimpsest_license"], :palimpsest_license), + {:ok, palimpsest_license} <- + parse_service_config(raw["palimpsest_license"], :palimpsest_license), {:ok, oikos} <- parse_service_config(raw["oikos"], :oikos) do - {:ok, %OpsmConfig{ - http: http, - checky_monkey: checky_monkey, - palimpsest_license: palimpsest_license, - oikos: oikos - }} + {:ok, + %OpsmConfig{ + http: http, + checky_monkey: checky_monkey, + palimpsest_license: palimpsest_license, + oikos: oikos + }} end end defp parse_http_config(nil), do: @default_http_config + defp parse_http_config(raw) do %HttpConfig{ timeout_ms: Map.get(raw, "timeout_ms", @default_http_config.timeout_ms), @@ -203,10 +211,12 @@ defmodule Opsm.Config do case validate_url(base_url) do :ok -> - {:ok, %ServiceConfig{ - base_url: base_url, - token: Map.get(raw, "token") - }} + {:ok, + %ServiceConfig{ + base_url: base_url, + token: Map.get(raw, "token") + }} + {:error, reason} -> {:error, "Invalid URL for #{service_key}: #{reason}"} end @@ -223,6 +233,7 @@ defmodule Opsm.Config do case URI.parse(url) do %URI{scheme: scheme, host: host} when scheme in ["http", "https"] and not is_nil(host) -> :ok + _ -> {:error, "invalid URL format"} end diff --git a/opsm_ex/lib/opsm/container.ex b/opsm_ex/lib/opsm/container.ex index 05b88b08..1cc35846 100644 --- a/opsm_ex/lib/opsm/container.ex +++ b/opsm_ex/lib/opsm/container.ex @@ -113,7 +113,13 @@ defmodule Opsm.Container do case Http.post_json(client, "/sign", body) do :ok -> Logger.info("Image signed successfully") - {:ok, %SignatureResult{signature: "signed", algorithm: "cosign", signed_at: DateTime.utc_now() |> DateTime.to_iso8601()}} + + {:ok, + %SignatureResult{ + signature: "signed", + algorithm: "cosign", + signed_at: DateTime.utc_now() |> DateTime.to_iso8601() + }} {:error, reason} -> Logger.error("Signing failed: #{inspect(reason)}") diff --git a/opsm_ex/lib/opsm/crypto/api_key_storage.ex b/opsm_ex/lib/opsm/crypto/api_key_storage.ex index c3dd3a1c..ccaec0c0 100644 --- a/opsm_ex/lib/opsm/crypto/api_key_storage.ex +++ b/opsm_ex/lib/opsm/crypto/api_key_storage.ex @@ -65,12 +65,12 @@ defmodule Opsm.Crypto.ApiKeyStorage do @type api_key :: String.t() @type stored_key :: %{ - encrypted_key: binary(), - key_id: key_id(), - service: String.t(), - created_at: DateTime.t(), - expires_at: DateTime.t() | nil - } + encrypted_key: binary(), + key_id: key_id(), + service: String.t(), + created_at: DateTime.t(), + expires_at: DateTime.t() | nil + } @doc """ Generate a 256-bit master key for encrypting API keys. @@ -171,6 +171,7 @@ defmodule Opsm.Crypto.ApiKeyStorage do # Encrypt the API key context = "opsm-api-key-#{service}" + case Symmetric.encrypt(api_key, master_key, context) do {:ok, encrypted} -> stored_key = %{ @@ -239,7 +240,8 @@ defmodule Opsm.Crypto.ApiKeyStorage do updated_keys = Enum.reject(keys, fn key -> key["key_id"] == key_id end) write_storage_file(storage_path, updated_keys) else - {:error, :enoent} -> :ok # Already deleted + # Already deleted + {:error, :enoent} -> :ok {:error, reason} -> {:error, reason} end end @@ -351,7 +353,9 @@ defmodule Opsm.Crypto.ApiKeyStorage do defp check_expiration(stored_key) do case stored_key["expires_at"] do - nil -> :ok + nil -> + :ok + expires_at_str -> case DateTime.from_iso8601(expires_at_str) do {:ok, expires_at, _offset} -> @@ -369,7 +373,9 @@ defmodule Opsm.Crypto.ApiKeyStorage do defp is_expired?(stored_key) do case stored_key["expires_at"] do - nil -> false + nil -> + false + expires_at_str -> case DateTime.from_iso8601(expires_at_str) do {:ok, expires_at, _offset} -> diff --git a/opsm_ex/lib/opsm/crypto/hybrid_signatures.ex b/opsm_ex/lib/opsm/crypto/hybrid_signatures.ex index 5cb4bdac..da3e0bdc 100644 --- a/opsm_ex/lib/opsm/crypto/hybrid_signatures.ex +++ b/opsm_ex/lib/opsm/crypto/hybrid_signatures.ex @@ -57,22 +57,24 @@ defmodule Opsm.Crypto.HybridSignatures do case PostQuantum.dilithium5_keypair() do {:ok, pq_keys} -> - {:ok, %{ - ed25519_pk: ed_keys.public_key, - ed25519_sk: ed_keys.secret_key, - dilithium5_pk: pq_keys.public_key, - dilithium5_sk: pq_keys.secret_key, - algorithm: :hybrid_ed25519_dilithium5 - }} + {:ok, + %{ + ed25519_pk: ed_keys.public_key, + ed25519_sk: ed_keys.secret_key, + dilithium5_pk: pq_keys.public_key, + dilithium5_sk: pq_keys.secret_key, + algorithm: :hybrid_ed25519_dilithium5 + }} {:error, :pq_not_available} -> - {:ok, %{ - ed25519_pk: ed_keys.public_key, - ed25519_sk: ed_keys.secret_key, - dilithium5_pk: nil, - dilithium5_sk: nil, - algorithm: :ed25519_only - }} + {:ok, + %{ + ed25519_pk: ed_keys.public_key, + ed25519_sk: ed_keys.secret_key, + dilithium5_pk: nil, + dilithium5_sk: nil, + algorithm: :ed25519_only + }} end end @@ -98,10 +100,11 @@ defmodule Opsm.Crypto.HybridSignatures do if keypair.dilithium5_sk do case PostQuantum.dilithium5_sign(message, keypair.dilithium5_sk) do {:ok, pq_sig} -> - {:ok, %{ - signature: ed_sig <> pq_sig, - algorithm: :hybrid_ed25519_dilithium5 - }} + {:ok, + %{ + signature: ed_sig <> pq_sig, + algorithm: :hybrid_ed25519_dilithium5 + }} {:error, _} -> # PQ failed — fall back to classical only @@ -154,10 +157,13 @@ defmodule Opsm.Crypto.HybridSignatures do :ok -> # Verify Dilithium5 (post-quantum) case PostQuantum.dilithium5_verify(message, pq_sig, public_keys.dilithium5_pk) do - :ok -> :ok + :ok -> + :ok + {:error, :pq_not_available} -> # PQ NIF not loaded — classical verified, PQ unchecked {:ok, :pq_not_verified} + {:error, reason} -> {:error, "Dilithium5 verification failed: #{reason}"} end @@ -215,18 +221,23 @@ defmodule Opsm.Crypto.HybridSignatures do """ def decode_public_keys(encoded) when is_map(encoded) do with {:ok, ed_pk} <- Base.decode16(encoded["ed25519_pk"], case: :mixed) do - pq_pk = case encoded["dilithium5_pk"] do - nil -> nil - hex -> case Base.decode16(hex, case: :mixed) do - {:ok, pk} -> pk - :error -> nil + pq_pk = + case encoded["dilithium5_pk"] do + nil -> + nil + + hex -> + case Base.decode16(hex, case: :mixed) do + {:ok, pk} -> pk + :error -> nil + end end - end - algorithm = case encoded["algorithm"] do - "hybrid_ed25519_dilithium5" -> :hybrid_ed25519_dilithium5 - _ -> :ed25519_only - end + algorithm = + case encoded["algorithm"] do + "hybrid_ed25519_dilithium5" -> :hybrid_ed25519_dilithium5 + _ -> :ed25519_only + end {:ok, %{ed25519_pk: ed_pk, dilithium5_pk: pq_pk, algorithm: algorithm}} else diff --git a/opsm_ex/lib/opsm/crypto/password.ex b/opsm_ex/lib/opsm/crypto/password.ex index 78f5964f..d149f371 100644 --- a/opsm_ex/lib/opsm/crypto/password.ex +++ b/opsm_ex/lib/opsm/crypto/password.ex @@ -13,7 +13,8 @@ defmodule Opsm.Crypto.Password do Aligns with SECURITY-STANDARDS.scm PasswordHashing requirements. """ - @memory_cost 19 # 2^19 KiB = 512 MiB (argon2 uses log2 of memory in KiB) + # 2^19 KiB = 512 MiB (argon2 uses log2 of memory in KiB) + @memory_cost 19 @time_cost 8 @parallelism 4 @hash_length 64 diff --git a/opsm_ex/lib/opsm/crypto/post_quantum/nif.ex b/opsm_ex/lib/opsm/crypto/post_quantum/nif.ex index 3c93bfbd..ffab1c6e 100644 --- a/opsm_ex/lib/opsm/crypto/post_quantum/nif.ex +++ b/opsm_ex/lib/opsm/crypto/post_quantum/nif.ex @@ -31,7 +31,9 @@ defmodule Opsm.Crypto.PostQuantum.Nif do # SLH-DSA (SPHINCS+-256f) --- FIPS 205 def sphincs_plus_keypair, do: :erlang.nif_error(:nif_not_loaded) def sphincs_plus_sign(_message, _secret_key), do: :erlang.nif_error(:nif_not_loaded) - def sphincs_plus_verify(_message, _signature, _public_key), do: :erlang.nif_error(:nif_not_loaded) + + def sphincs_plus_verify(_message, _signature, _public_key), + do: :erlang.nif_error(:nif_not_loaded) # ML-KEM-1024 (Kyber-1024) --- FIPS 203 def kyber1024_keypair, do: :erlang.nif_error(:nif_not_loaded) diff --git a/opsm_ex/lib/opsm/crypto/pq_trust.ex b/opsm_ex/lib/opsm/crypto/pq_trust.ex index 8b033d4c..7d0a4245 100644 --- a/opsm_ex/lib/opsm/crypto/pq_trust.ex +++ b/opsm_ex/lib/opsm/crypto/pq_trust.ex @@ -54,12 +54,13 @@ defmodule Opsm.Crypto.PqTrust do case HybridSignatures.sign(package_data, kp) do {:ok, sig_info} -> - {:ok, %{ - signature: sig_info, - public_keys: HybridSignatures.encode_public_keys(kp), - signed_at: DateTime.utc_now() |> DateTime.to_iso8601(), - mode: HybridSignatures.mode() - }} + {:ok, + %{ + signature: sig_info, + public_keys: HybridSignatures.encode_public_keys(kp), + signed_at: DateTime.utc_now() |> DateTime.to_iso8601(), + mode: HybridSignatures.mode() + }} {:error, reason} -> {:error, reason} @@ -82,13 +83,14 @@ defmodule Opsm.Crypto.PqTrust do case HybridSignatures.sign(integrity_data, keypair) do {:ok, sig_info} -> - {:ok, %{ - lockfile_hash: lockfile.integrity_hash, - signature: HybridSignatures.encode_signature(sig_info), - public_keys: HybridSignatures.encode_public_keys(keypair), - algorithm: sig_info.algorithm, - signed_at: DateTime.utc_now() |> DateTime.to_iso8601() - }} + {:ok, + %{ + lockfile_hash: lockfile.integrity_hash, + signature: HybridSignatures.encode_signature(sig_info), + public_keys: HybridSignatures.encode_public_keys(keypair), + algorithm: sig_info.algorithm, + signed_at: DateTime.utc_now() |> DateTime.to_iso8601() + }} {:error, reason} -> {:error, reason} @@ -103,14 +105,17 @@ defmodule Opsm.Crypto.PqTrust do with {:ok, public_keys} <- HybridSignatures.decode_public_keys(sig_bundle["public_keys"]) do sig_hex = sig_bundle["signature"]["signature"] - algo = case sig_bundle["signature"]["algorithm"] do - "hybrid_ed25519_dilithium5" -> :hybrid_ed25519_dilithium5 - _ -> :ed25519_only - end + + algo = + case sig_bundle["signature"]["algorithm"] do + "hybrid_ed25519_dilithium5" -> :hybrid_ed25519_dilithium5 + _ -> :ed25519_only + end case Base.decode16(sig_hex, case: :mixed) do {:ok, sig_bytes} -> sig_info = %{signature: sig_bytes, algorithm: algo} + case HybridSignatures.verify(integrity_data, sig_info, public_keys) do :ok -> {:ok, :verified} {:ok, mode} -> {:ok, mode} @@ -139,11 +144,12 @@ defmodule Opsm.Crypto.PqTrust do recipient_public_key != nil and PostQuantum.available?() -> case PostQuantum.kyber1024_encapsulate(recipient_public_key) do {:ok, %{ciphertext: ct, shared_secret: ss}} -> - {:ok, %{ - shared_secret: ss, - ciphertext: ct, - method: :kyber1024_kem - }} + {:ok, + %{ + shared_secret: ss, + ciphertext: ct, + method: :kyber1024_kem + }} {:error, reason} -> {:error, "Kyber encapsulation failed: #{reason}"} @@ -155,13 +161,14 @@ defmodule Opsm.Crypto.PqTrust do {:ok, kp} -> case PostQuantum.kyber1024_encapsulate(kp.public_key) do {:ok, %{ciphertext: ct, shared_secret: ss}} -> - {:ok, %{ - shared_secret: ss, - ciphertext: ct, - secret_key: kp.secret_key, - public_key: kp.public_key, - method: :kyber1024_self - }} + {:ok, + %{ + shared_secret: ss, + ciphertext: ct, + secret_key: kp.secret_key, + public_key: kp.public_key, + method: :kyber1024_self + }} {:error, reason} -> {:error, reason} @@ -228,6 +235,7 @@ defmodule Opsm.Crypto.PqTrust do # If PQ available, sign the attestation itself if PostQuantum.available?() do {:ok, keypair} = HybridSignatures.generate_keypair() + case HybridSignatures.sign_payload(attestation, keypair) do {:ok, sig} -> Map.merge(attestation, %{ @@ -255,8 +263,10 @@ defmodule Opsm.Crypto.PqTrust do pq_available: PostQuantum.available?(), signature_mode: HybridSignatures.mode(), algorithms: PostQuantum.algorithms(), - lockfile_encryption: if(PostQuantum.available?(), do: :kyber1024_kem, else: :classical_random), - package_signatures: if(PostQuantum.available?(), do: :hybrid_ed25519_dilithium5, else: :ed25519_only) + lockfile_encryption: + if(PostQuantum.available?(), do: :kyber1024_kem, else: :classical_random), + package_signatures: + if(PostQuantum.available?(), do: :hybrid_ed25519_dilithium5, else: :ed25519_only) } end end diff --git a/opsm_ex/lib/opsm/crypto/rng.ex b/opsm_ex/lib/opsm/crypto/rng.ex index 7788370b..00f41aed 100644 --- a/opsm_ex/lib/opsm/crypto/rng.ex +++ b/opsm_ex/lib/opsm/crypto/rng.ex @@ -40,7 +40,8 @@ defmodule Opsm.Crypto.RNG do 32 """ def generate_key_256bit do - generate_bytes(32) # 256 bits + # 256 bits + generate_bytes(32) end @doc """ @@ -53,7 +54,8 @@ defmodule Opsm.Crypto.RNG do 24 """ def generate_nonce_192bit do - generate_bytes(24) # 192 bits (XChaCha20) + # 192 bits (XChaCha20) + generate_bytes(24) end @doc """ @@ -66,6 +68,7 @@ defmodule Opsm.Crypto.RNG do 32 """ def generate_salt do - generate_bytes(32) # 256 bits + # 256 bits + generate_bytes(32) end end diff --git a/opsm_ex/lib/opsm/crypto/symmetric.ex b/opsm_ex/lib/opsm/crypto/symmetric.ex index 1971e79e..50ef3068 100644 --- a/opsm_ex/lib/opsm/crypto/symmetric.ex +++ b/opsm_ex/lib/opsm/crypto/symmetric.ex @@ -16,9 +16,12 @@ defmodule Opsm.Crypto.Symmetric do (never reuse nonces with the same key). """ - @key_size 32 # 256 bits - @nonce_size 12 # 96 bits (ChaCha20-Poly1305 standard nonce) - @tag_size 16 # 128 bits (Poly1305 tag) + # 256 bits + @key_size 32 + # 96 bits (ChaCha20-Poly1305 standard nonce) + @nonce_size 12 + # 128 bits (Poly1305 tag) + @tag_size 16 @doc """ Encrypt plaintext with XChaCha20-Poly1305 AEAD. diff --git a/opsm_ex/lib/opsm/errors.ex b/opsm_ex/lib/opsm/errors.ex index deb58b19..b7b70bf9 100644 --- a/opsm_ex/lib/opsm/errors.ex +++ b/opsm_ex/lib/opsm/errors.ex @@ -109,8 +109,7 @@ defmodule Opsm.Errors do """ def missing_toolchain(forth, required_tools) do tools = Enum.join(required_tools, ", ") - {:toolchain, "Missing toolchain for @#{forth}", - "Install one of: #{tools}"} + {:toolchain, "Missing toolchain for @#{forth}", "Install one of: #{tools}"} end @doc """ @@ -141,6 +140,7 @@ defmodule Opsm.Errors do """ def trust_failed(package, reasons) do reasons_str = Enum.join(reasons, "\n - ") + {:security, "Trust verification failed for '#{package}'", "Issues found:\n - #{reasons_str}\n Use --skip-trust to bypass (not recommended)"} end @@ -226,8 +226,7 @@ defmodule Opsm.Errors do end def invalid_url(service, url) do - {:config, "Invalid URL for #{service}: #{url}", - "URLs must start with http:// or https://"} + {:config, "Invalid URL for #{service}: #{url}", "URLs must start with http:// or https://"} end @doc """ @@ -242,7 +241,6 @@ defmodule Opsm.Errors do Command not implemented. """ def not_implemented(command) do - {:internal, "'#{command}' is not yet implemented", - "This feature is coming soon"} + {:internal, "'#{command}' is not yet implemented", "This feature is coming soon"} end end diff --git a/opsm_ex/lib/opsm/events.ex b/opsm_ex/lib/opsm/events.ex index 60809743..d9bde41d 100644 --- a/opsm_ex/lib/opsm/events.ex +++ b/opsm_ex/lib/opsm/events.ex @@ -27,7 +27,12 @@ defmodule Opsm.Events do :dependency_update ] - @type event_type :: :security_advisory | :package_publish | :package_deprecate | :package_update | :dependency_update + @type event_type :: + :security_advisory + | :package_publish + | :package_deprecate + | :package_update + | :dependency_update @type event_data :: %{ required(:package) => String.t(), diff --git a/opsm_ex/lib/opsm/federation.ex b/opsm_ex/lib/opsm/federation.ex index 69e46ec7..9b7d34bf 100644 --- a/opsm_ex/lib/opsm/federation.ex +++ b/opsm_ex/lib/opsm/federation.ex @@ -257,15 +257,16 @@ defmodule Opsm.Federation do # Simple YAML key-value parsing for pubspec fields = parse_yaml_simple(content) - {:ok, %ManifestFormat{ - name: fields["name"] || "unknown", - version: fields["version"] || "0.0.0", - description: fields["description"], - homepage: fields["homepage"], - repository: fields["repository"], - source_forth: :pub, - raw_manifest: %{"raw" => content} - }} + {:ok, + %ManifestFormat{ + name: fields["name"] || "unknown", + version: fields["version"] || "0.0.0", + description: fields["description"], + homepage: fields["homepage"], + repository: fields["repository"], + source_forth: :pub, + raw_manifest: %{"raw" => content} + }} {:error, reason} -> {:error, "Failed to read pubspec.yaml: #{reason}"} @@ -307,13 +308,14 @@ defmodule Opsm.Federation do %{} end - {:ok, %ManifestFormat{ - name: module_name, - version: go_version, - dependencies: deps, - source_forth: :go, - raw_manifest: %{"raw" => content} - }} + {:ok, + %ManifestFormat{ + name: module_name, + version: go_version, + dependencies: deps, + source_forth: :go, + raw_manifest: %{"raw" => content} + }} {:error, reason} -> {:error, "Failed to read go.mod: #{reason}"} @@ -332,13 +334,14 @@ defmodule Opsm.Federation do end) |> Map.new() - {:ok, %ManifestFormat{ - name: Path.basename(Path.dirname(Path.expand(path))), - version: "0.0.0", - dependencies: deps, - source_forth: :gem, - raw_manifest: %{"raw" => content} - }} + {:ok, + %ManifestFormat{ + name: Path.basename(Path.dirname(Path.expand(path))), + version: "0.0.0", + dependencies: deps, + source_forth: :gem, + raw_manifest: %{"raw" => content} + }} {:error, reason} -> {:error, "Failed to read Gemfile: #{reason}"} @@ -375,19 +378,21 @@ defmodule Opsm.Federation do case Toml.decode(content) do {:ok, toml} -> pkg = toml["package"] || %{} - {:ok, %ManifestFormat{ - name: pkg["name"] || "", - version: pkg["version"] || "0.0.0", - description: pkg["description"], - license: pkg["license"], - repository: pkg["repository"], - authors: pkg["authors"] || [], - keywords: pkg["keywords"] || [], - dependencies: toml["dependencies"] || %{}, - dev_dependencies: toml["dev-dependencies"] || %{}, - source_forth: :cargo, - raw_manifest: toml - }} + + {:ok, + %ManifestFormat{ + name: pkg["name"] || "", + version: pkg["version"] || "0.0.0", + description: pkg["description"], + license: pkg["license"], + repository: pkg["repository"], + authors: pkg["authors"] || [], + keywords: pkg["keywords"] || [], + dependencies: toml["dependencies"] || %{}, + dev_dependencies: toml["dev-dependencies"] || %{}, + source_forth: :cargo, + raw_manifest: toml + }} {:error, reason} -> {:error, "Failed to parse Cargo.toml: #{inspect(reason)}"} @@ -410,15 +415,16 @@ defmodule Opsm.Federation do license = extract_mix_license(project_fields, content) deps = extract_mix_deps_ast(content) - {:ok, %ManifestFormat{ - name: to_string(name), - version: version, - description: description, - license: license, - dependencies: deps, - source_forth: :hex, - raw_manifest: %{"raw" => content} - }} + {:ok, + %ManifestFormat{ + name: to_string(name), + version: version, + description: description, + license: license, + dependencies: deps, + source_forth: :hex, + raw_manifest: %{"raw" => content} + }} {:error, reason} -> {:error, "Failed to read mix.exs: #{reason}"} @@ -432,13 +438,21 @@ defmodule Opsm.Federation do # Extract simple keyword pairs: key: value Regex.scan(~r/(\w+):\s*(?::(\w+)|"([^"]*)"|(~\w\[.*?\]))/m, block) |> Enum.map(fn - [_, key, atom_val, "", ""] -> {String.to_existing_atom(key), String.to_existing_atom(atom_val)} - [_, key, "", str_val, ""] -> {String.to_existing_atom(key), str_val} - [_, key, "", "", sigil] -> {String.to_existing_atom(key), sigil} - _ -> nil + [_, key, atom_val, "", ""] -> + {String.to_existing_atom(key), String.to_existing_atom(atom_val)} + + [_, key, "", str_val, ""] -> + {String.to_existing_atom(key), str_val} + + [_, key, "", "", sigil] -> + {String.to_existing_atom(key), sigil} + + _ -> + nil end) |> Enum.reject(&is_nil/1) |> Map.new() + _ -> %{} end @@ -452,6 +466,7 @@ defmodule Opsm.Federation do Regex.scan(~r/\{:(\w+),\s*"([^"]+)"/m, block) |> Enum.map(fn [_, name, version] -> {name, version} end) |> Map.new() + _ -> %{} end @@ -465,8 +480,12 @@ defmodule Opsm.Federation do [_, license] -> license _ -> extract_mix_field(content, "license") end - license when is_binary(license) -> license - _ -> nil + + license when is_binary(license) -> + license + + _ -> + nil end end @@ -476,18 +495,21 @@ defmodule Opsm.Federation do case Toml.decode(content) do {:ok, toml} -> proj = toml["project"] || toml["tool"]["poetry"] || %{} - {:ok, %ManifestFormat{ - name: proj["name"] || "", - version: proj["version"] || "0.0.0", - description: proj["description"], - license: extract_license(proj["license"]), - authors: extract_authors(proj["authors"]), - keywords: proj["keywords"] || [], - dependencies: extract_pypi_deps(proj["dependencies"]), - dev_dependencies: extract_pypi_deps(get_in(proj, ["optional-dependencies", "dev"]) || []), - source_forth: :pypi, - raw_manifest: toml - }} + + {:ok, + %ManifestFormat{ + name: proj["name"] || "", + version: proj["version"] || "0.0.0", + description: proj["description"], + license: extract_license(proj["license"]), + authors: extract_authors(proj["authors"]), + keywords: proj["keywords"] || [], + dependencies: extract_pypi_deps(proj["dependencies"]), + dev_dependencies: + extract_pypi_deps(get_in(proj, ["optional-dependencies", "dev"]) || []), + source_forth: :pypi, + raw_manifest: toml + }} {:error, reason} -> {:error, "Failed to parse pyproject.toml: #{inspect(reason)}"} @@ -514,45 +536,68 @@ defmodule Opsm.Federation do def agentic_resolve(query, opts \\ []) do alias Opsm.Registries.Registry - forths = Keyword.get(opts, :forths, [:npm, :cargo, :hex, :pypi, :gem, :go, :pub, :hackage, :nuget, :maven]) + forths = + Keyword.get(opts, :forths, [ + :npm, + :cargo, + :hex, + :pypi, + :gem, + :go, + :pub, + :hackage, + :nuget, + :maven + ]) + timeout = Keyword.get(opts, :timeout, 15_000) # Search across all specified registries in parallel - tasks = Enum.map(forths, fn forth -> - Task.async(fn -> - try do - case Registry.search(forth, query, limit: 5) do - {:ok, results} -> - Enum.map(results, fn r -> Map.put(r, :forth, forth) end) - {:error, _} -> [] + tasks = + Enum.map(forths, fn forth -> + Task.async(fn -> + try do + case Registry.search(forth, query, limit: 5) do + {:ok, results} -> + Enum.map(results, fn r -> Map.put(r, :forth, forth) end) + + {:error, _} -> + [] + end + rescue + _ -> [] end - rescue - _ -> [] - end + end) end) - end) results = Task.yield_many(tasks, timeout) candidates = results |> Enum.flat_map(fn - {_task, {:ok, items}} when is_list(items) -> items - {task, nil} -> Task.shutdown(task, :brutal_kill); [] - _ -> [] + {_task, {:ok, items}} when is_list(items) -> + items + + {task, nil} -> + Task.shutdown(task, :brutal_kill) + [] + + _ -> + [] end) |> Enum.sort_by(fn c -> # Exact name match scores highest if String.downcase(c[:name] || "") == String.downcase(query), do: 0, else: 1 end) - {:ok, %{ - query: query, - candidates: candidates, - forths_searched: forths, - status: :ok, - message: "Found #{length(candidates)} candidates across #{length(forths)} registries" - }} + {:ok, + %{ + query: query, + candidates: candidates, + forths_searched: forths, + status: :ok, + message: "Found #{length(candidates)} candidates across #{length(forths)} registries" + }} end # ============================================================================= @@ -673,7 +718,14 @@ defmodule Opsm.Federation do {:ok, manifest_str} -> if System.find_executable("fpm") do IO.puts("Converting #{pkg.package}@#{pkg.version} to #{target} via fpm...") - {:ok, %{manifest: manifest_str, format: target, converter: script, mapped_deps: mapped_deps}} + + {:ok, + %{ + manifest: manifest_str, + format: target, + converter: script, + mapped_deps: mapped_deps + }} else IO.puts("fpm not found — generated manifest only (install fpm for full conversion)") {:ok, %{manifest: manifest_str, format: target, mapped_deps: mapped_deps}} @@ -774,15 +826,19 @@ defmodule Opsm.Federation do """ def check_toolchain(forth) when is_atom(forth) do case Map.get(@toolchains, forth) do - nil -> {:ok, :no_toolchain_required} + nil -> + {:ok, :no_toolchain_required} + commands -> found = Enum.filter(commands, &System.find_executable/1) + if Enum.empty?(found) do - {:error, %{ - forth: forth, - required: commands, - message: "#{forth} toolchain not found. Install one of: #{Enum.join(commands, ", ")}" - }} + {:error, + %{ + forth: forth, + required: commands, + message: "#{forth} toolchain not found. Install one of: #{Enum.join(commands, ", ")}" + }} else {:ok, %{forth: forth, available: found}} end @@ -808,20 +864,22 @@ defmodule Opsm.Federation do Returns availability info for each registry. """ def discover(package_name, opts \\ []) do - forths_to_search = Keyword.get(opts, :forths, [:npm, :cargo, :hex, :pypi, :gem, :nuget, :maven, :pub, :go]) - - results = Enum.map(forths_to_search, fn forth -> - toolchain_status = check_toolchain(forth) - availability = check_registry_availability(forth, package_name) - - %{ - forth: forth, - available: match?({:ok, _}, availability), - availability_info: availability, - toolchain_installed: match?({:ok, _}, toolchain_status), - toolchain_info: toolchain_status - } - end) + forths_to_search = + Keyword.get(opts, :forths, [:npm, :cargo, :hex, :pypi, :gem, :nuget, :maven, :pub, :go]) + + results = + Enum.map(forths_to_search, fn forth -> + toolchain_status = check_toolchain(forth) + availability = check_registry_availability(forth, package_name) + + %{ + forth: forth, + available: match?({:ok, _}, availability), + availability_info: availability, + toolchain_installed: match?({:ok, _}, toolchain_status), + toolchain_info: toolchain_status + } + end) available = Enum.filter(results, & &1.available) missing_toolchain = Enum.filter(available, &(not &1.toolchain_installed)) @@ -844,6 +902,7 @@ defmodule Opsm.Federation do {:ok, pkg} -> {:ok, pkg} {:error, reason} -> {:error, reason} end + false -> {:error, :not_found} end @@ -852,25 +911,34 @@ defmodule Opsm.Federation do defp build_recommendations(package_name, available, missing_toolchain) do recs = [] - recs = if Enum.empty?(available) do - recs ++ ["Package '#{package_name}' not found in any registry. Try 'opsm search #{package_name}'"] - else - recs ++ ["Package '#{package_name}' available from: #{Enum.map(available, & &1.forth) |> Enum.join(", ")}"] - end + recs = + if Enum.empty?(available) do + recs ++ + [ + "Package '#{package_name}' not found in any registry. Try 'opsm search #{package_name}'" + ] + else + recs ++ + [ + "Package '#{package_name}' available from: #{Enum.map(available, & &1.forth) |> Enum.join(", ")}" + ] + end - recs = if not Enum.empty?(missing_toolchain) do - missing = Enum.map(missing_toolchain, fn r -> - case r.toolchain_info do - {:error, info} -> "#{r.forth}: #{info.message}" - _ -> nil - end - end) - |> Enum.reject(&is_nil/1) + recs = + if not Enum.empty?(missing_toolchain) do + missing = + Enum.map(missing_toolchain, fn r -> + case r.toolchain_info do + {:error, info} -> "#{r.forth}: #{info.message}" + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) - recs ++ ["Missing toolchains for some registries:"] ++ Enum.map(missing, &(" - " <> &1)) - else - recs - end + recs ++ ["Missing toolchains for some registries:"] ++ Enum.map(missing, &(" - " <> &1)) + else + recs + end recs end @@ -881,7 +949,11 @@ defmodule Opsm.Federation do known_alternatives = %{ "axios" => %{npm: ["axios"], cargo: ["reqwest", "ureq"], hex: ["req", "httpoison"]}, "lodash" => %{npm: ["lodash"], cargo: [], hex: [], pypi: ["toolz"]}, - "express" => %{npm: ["express", "fastify", "koa"], cargo: ["actix-web", "axum"], hex: ["phoenix", "plug"]}, + "express" => %{ + npm: ["express", "fastify", "koa"], + cargo: ["actix-web", "axum"], + hex: ["phoenix", "plug"] + }, "react" => %{npm: ["react"], cargo: ["dioxus", "yew", "leptos"]}, "django" => %{pypi: ["django", "flask", "fastapi"]}, "rails" => %{gem: ["rails", "sinatra", "hanami"]}, @@ -970,6 +1042,7 @@ defmodule Opsm.Federation do defp extract_authors(nil), do: [] defp extract_authors(author) when is_binary(author), do: [author] + defp extract_authors(authors) when is_list(authors) do Enum.map(authors, fn a when is_binary(a) -> a @@ -978,6 +1051,7 @@ defmodule Opsm.Federation do end) |> Enum.reject(&is_nil/1) end + defp extract_authors(_), do: [] defp extract_license(nil), do: nil @@ -986,12 +1060,14 @@ defmodule Opsm.Federation do defp extract_license(_), do: nil defp extract_pypi_deps(nil), do: %{} + defp extract_pypi_deps(deps) when is_list(deps) do deps |> Enum.map(&parse_pypi_dep/1) |> Enum.reject(&is_nil/1) |> Map.new() end + defp extract_pypi_deps(deps) when is_map(deps), do: deps defp parse_pypi_dep(dep) when is_binary(dep) do @@ -1000,18 +1076,22 @@ defmodule Opsm.Federation do _ -> nil end end + defp parse_pypi_dep(_), do: nil defp parse_ipkg(content) do lines = String.split(content, "\n") - fields = Enum.reduce(lines, %{}, fn line, acc -> - case String.split(line, "=", parts: 2) do - [key, value] -> - Map.put(acc, String.trim(key), String.trim(value)) - _ -> - acc - end - end) + + fields = + Enum.reduce(lines, %{}, fn line, acc -> + case String.split(line, "=", parts: 2) do + [key, value] -> + Map.put(acc, String.trim(key), String.trim(value)) + + _ -> + acc + end + end) %ManifestFormat{ name: fields["package"] || "unknown", @@ -1024,6 +1104,7 @@ defmodule Opsm.Federation do end defp parse_authors_field(nil), do: [] + defp parse_authors_field(authors) do authors |> String.split(",") @@ -1052,7 +1133,10 @@ defmodule Opsm.Federation do defp install_args(:guix, package, _opts), do: ["install", package] defp install_args(:flatpak, package, _opts), do: ["install", "-y", package] defp install_args(:snap, package, _opts), do: ["install", package] - defp install_args(:winget, package, _opts), do: ["install", "--accept-package-agreements", package] + + defp install_args(:winget, package, _opts), + do: ["install", "--accept-package-agreements", package] + defp install_args(:choco, package, _opts), do: ["install", "-y", package] defp install_args(:scoop, package, _opts), do: ["install", package] defp install_args(_, package, _opts), do: ["install", package] diff --git a/opsm_ex/lib/opsm/federation/dep_mapper.ex b/opsm_ex/lib/opsm/federation/dep_mapper.ex index 0d3ae798..343ee8af 100644 --- a/opsm_ex/lib/opsm/federation/dep_mapper.ex +++ b/opsm_ex/lib/opsm/federation/dep_mapper.ex @@ -81,12 +81,29 @@ defmodule Opsm.Federation.DepMapper do defp known_mappings do %{ # Python packages → system packages - {"numpy", :pypi} => %{deb: "python3-numpy", rpm: "python3-numpy", pacman: "python-numpy", homebrew: "numpy"}, - {"pandas", :pypi} => %{deb: "python3-pandas", rpm: "python3-pandas", pacman: "python-pandas"}, + {"numpy", :pypi} => %{ + deb: "python3-numpy", + rpm: "python3-numpy", + pacman: "python-numpy", + homebrew: "numpy" + }, + {"pandas", :pypi} => %{ + deb: "python3-pandas", + rpm: "python3-pandas", + pacman: "python-pandas" + }, {"scipy", :pypi} => %{deb: "python3-scipy", rpm: "python3-scipy", pacman: "python-scipy"}, - {"requests", :pypi} => %{deb: "python3-requests", rpm: "python3-requests", pacman: "python-requests"}, + {"requests", :pypi} => %{ + deb: "python3-requests", + rpm: "python3-requests", + pacman: "python-requests" + }, {"flask", :pypi} => %{deb: "python3-flask", rpm: "python3-flask", pacman: "python-flask"}, - {"django", :pypi} => %{deb: "python3-django", rpm: "python3-django", pacman: "python-django"}, + {"django", :pypi} => %{ + deb: "python3-django", + rpm: "python3-django", + pacman: "python-django" + }, {"pillow", :pypi} => %{deb: "python3-pil", rpm: "python3-pillow", pacman: "python-pillow"}, {"cryptography", :pypi} => %{deb: "python3-cryptography", rpm: "python3-cryptography"}, {"pyyaml", :pypi} => %{deb: "python3-yaml", rpm: "python3-pyyaml", pacman: "python-yaml"}, @@ -105,20 +122,48 @@ defmodule Opsm.Federation.DepMapper do {"nokogiri", :gem} => %{deb: "ruby-nokogiri", rpm: "rubygem-nokogiri"}, # Rust crates → system packages - {"ripgrep", :cargo} => %{deb: "ripgrep", rpm: "ripgrep", pacman: "ripgrep", homebrew: "ripgrep"}, + {"ripgrep", :cargo} => %{ + deb: "ripgrep", + rpm: "ripgrep", + pacman: "ripgrep", + homebrew: "ripgrep" + }, {"fd-find", :cargo} => %{deb: "fd-find", pacman: "fd", homebrew: "fd"}, {"bat", :cargo} => %{deb: "bat", pacman: "bat", homebrew: "bat"}, {"exa", :cargo} => %{deb: "exa", pacman: "exa", homebrew: "exa"}, {"tokei", :cargo} => %{deb: "tokei", pacman: "tokei", homebrew: "tokei"}, # Go modules → system packages - {"github.com/junegunn/fzf", :go} => %{deb: "fzf", rpm: "fzf", pacman: "fzf", homebrew: "fzf"}, + {"github.com/junegunn/fzf", :go} => %{ + deb: "fzf", + rpm: "fzf", + pacman: "fzf", + homebrew: "fzf" + }, {"github.com/jesseduffield/lazygit", :go} => %{pacman: "lazygit", homebrew: "lazygit"}, # Cross-ecosystem equivalents (same function, different ecosystems) - {"req", :hex} => %{npm: "axios", pypi: "requests", cargo: "reqwest", gem: "httparty", go: "net/http"}, - {"jason", :hex} => %{npm: "json5", pypi: "json", cargo: "serde_json", gem: "json", go: "encoding/json"}, - {"plug", :hex} => %{npm: "express", pypi: "flask", cargo: "actix-web", gem: "rack", go: "net/http"} + {"req", :hex} => %{ + npm: "axios", + pypi: "requests", + cargo: "reqwest", + gem: "httparty", + go: "net/http" + }, + {"jason", :hex} => %{ + npm: "json5", + pypi: "json", + cargo: "serde_json", + gem: "json", + go: "encoding/json" + }, + {"plug", :hex} => %{ + npm: "express", + pypi: "flask", + cargo: "actix-web", + gem: "rack", + go: "net/http" + } } end @@ -126,7 +171,9 @@ defmodule Opsm.Federation.DepMapper do key = {String.downcase(package_name), source_forth} case Map.get(known_mappings(), key) do - nil -> :not_found + nil -> + :not_found + mappings -> case Map.get(mappings, target_forth) do nil -> :not_found diff --git a/opsm_ex/lib/opsm/federation/system_query.ex b/opsm_ex/lib/opsm/federation/system_query.ex index fbb2236c..065d84ff 100644 --- a/opsm_ex/lib/opsm/federation/system_query.ex +++ b/opsm_ex/lib/opsm/federation/system_query.ex @@ -176,7 +176,9 @@ defmodule Opsm.Federation.SystemQuery do # Split from the right: name-version-release.arch # Strategy: find the last dash before a digit sequence case Regex.run(~r/^(.+)-(\d[^-]*)-[^-]+$/, line) do - [_, name, version] -> %{name: name, version: version} + [_, name, version] -> + %{name: name, version: version} + _ -> # Fallback: split on first dash-digit case Regex.run(~r/^(.+?)-(\d.*)$/, line) do @@ -203,8 +205,11 @@ defmodule Opsm.Federation.SystemQuery do |> String.split("\n", trim: true) |> Enum.map(fn line -> case String.split(line, ~r/\s+/, parts: 2) do - [name, versions] -> %{name: name, version: String.split(versions) |> List.last() || "unknown"} - [name] -> %{name: name, version: "unknown"} + [name, versions] -> + %{name: name, version: String.split(versions) |> List.last() || "unknown"} + + [name] -> + %{name: name, version: "unknown"} end end) end @@ -228,7 +233,8 @@ defmodule Opsm.Federation.SystemQuery do defp parse_snap_list(output) do output |> String.split("\n", trim: true) - |> Enum.drop(1) # Skip header line + # Skip header line + |> Enum.drop(1) |> Enum.map(fn line -> case String.split(line, ~r/\s+/) do [name, version | _] -> %{name: name, version: version} @@ -315,6 +321,7 @@ defmodule Opsm.Federation.SystemQuery do |> Enum.drop(1) |> Enum.flat_map(fn line -> parts = String.split(line, "\t") + case Enum.at(parts, 1) do nil -> [] v -> [String.trim(v)] diff --git a/opsm_ex/lib/opsm/git/builder.ex b/opsm_ex/lib/opsm/git/builder.ex index 00926767..44f74177 100644 --- a/opsm_ex/lib/opsm/git/builder.ex +++ b/opsm_ex/lib/opsm/git/builder.ex @@ -41,7 +41,8 @@ defmodule Opsm.Git.Builder do Returns `{:ok, output}` or `{:error, reason}`. """ - @spec run(String.t(), atom(), [String.t()], keyword()) :: {:ok, String.t()} | {:error, String.t()} + @spec run(String.t(), atom(), [String.t()], keyword()) :: + {:ok, String.t()} | {:error, String.t()} def run(repo_path, build_system, args \\ [], _opts \\ []) do {cmd, cmd_args} = run_command(build_system, args) diff --git a/opsm_ex/lib/opsm/git/pipeline.ex b/opsm_ex/lib/opsm/git/pipeline.ex index 9a6428ca..3d511b00 100644 --- a/opsm_ex/lib/opsm/git/pipeline.ex +++ b/opsm_ex/lib/opsm/git/pipeline.ex @@ -85,12 +85,13 @@ defmodule Opsm.Git.Pipeline do :ok <- log_detection(build_system, config_file), {:ok, _} <- maybe_install_deps(repo_path, build_system, opts), {:ok, output} <- do_build(repo_path, build_system, opts) do - {:ok, %{ - path: repo_path, - build_system: build_system, - config_file: config_file, - output: output - }} + {:ok, + %{ + path: repo_path, + build_system: build_system, + config_file: config_file, + output: output + }} end end diff --git a/opsm_ex/lib/opsm/har/web_scraper.ex b/opsm_ex/lib/opsm/har/web_scraper.ex index 604a39c3..88167807 100644 --- a/opsm_ex/lib/opsm/har/web_scraper.ex +++ b/opsm_ex/lib/opsm/har/web_scraper.ex @@ -254,21 +254,27 @@ defmodule Opsm.Har.WebScraper do @doc false def check_github(query, language) do lang_param = if language != "unknown", do: "+language:#{language}", else: "" - url = "https://api.github.com/search/repositories?q=#{URI.encode(query)}#{lang_param}&per_page=5" - case Http.get_json(url, timeout: @request_timeout, headers: [{"accept", "application/vnd.github.v3+json"}]) do + url = + "https://api.github.com/search/repositories?q=#{URI.encode(query)}#{lang_param}&per_page=5" + + case Http.get_json(url, + timeout: @request_timeout, + headers: [{"accept", "application/vnd.github.v3+json"}] + ) do {:ok, %{"items" => [first | _]}} -> confidence = github_confidence(first) - {:ok, %{ - url: first["html_url"], - type: :git, - ref: first["default_branch"], - description: first["description"], - license: get_in(first, ["license", "spdx_id"]), - stars: first["stargazers_count"], - confidence: confidence - }} + {:ok, + %{ + url: first["html_url"], + type: :git, + ref: first["default_branch"], + description: first["description"], + license: get_in(first, ["license", "spdx_id"]), + stars: first["stargazers_count"], + confidence: confidence + }} _ -> {:error, :not_found} @@ -281,13 +287,14 @@ defmodule Opsm.Har.WebScraper do case Http.get_json(url, timeout: @request_timeout) do {:ok, [first | _]} -> - {:ok, %{ - url: first["web_url"], - type: :git, - ref: first["default_branch"], - description: first["description"], - confidence: 0.5 - }} + {:ok, + %{ + url: first["web_url"], + type: :git, + ref: first["default_branch"], + description: first["description"], + confidence: 0.5 + }} _ -> {:error, :not_found} @@ -300,13 +307,14 @@ defmodule Opsm.Har.WebScraper do case Http.get_json(url, timeout: @request_timeout) do {:ok, %{"data" => [first | _]}} -> - {:ok, %{ - url: first["html_url"], - type: :git, - ref: first["default_branch"], - description: first["description"], - confidence: 0.4 - }} + {:ok, + %{ + url: first["html_url"], + type: :git, + ref: first["default_branch"], + description: first["description"], + confidence: 0.4 + }} _ -> {:error, :not_found} @@ -353,13 +361,15 @@ defmodule Opsm.Har.WebScraper do Enum.reduce_while(registry_checks, {:error, :not_found}, fn {url, registry}, _acc -> case Http.get_json(url, timeout: @request_timeout) do {:ok, body} when is_map(body) -> - {:halt, {:ok, %{ - url: url, - type: :registry, - registry: registry, - body: body, - confidence: 0.95 - }}} + {:halt, + {:ok, + %{ + url: url, + type: :registry, + registry: registry, + body: body, + confidence: 0.95 + }}} _ -> {:cont, {:error, :not_found}} diff --git a/opsm_ex/lib/opsm/har_queue.ex b/opsm_ex/lib/opsm/har_queue.ex index d15cd79e..1a2d7d17 100644 --- a/opsm_ex/lib/opsm/har_queue.ex +++ b/opsm_ex/lib/opsm/har_queue.ex @@ -18,8 +18,10 @@ defmodule Opsm.HarQueue do alias Opsm.Verified.Json @queue_dir "/tmp/opsm-har-ingest" - @default_poll_interval 1000 # 1 second - @cleanup_after 300_000 # 5 minutes + # 1 second + @default_poll_interval 1000 + # 5 minutes + @cleanup_after 300_000 @doc """ Submit a task to the HAR queue. @@ -79,7 +81,7 @@ defmodule Opsm.HarQueue do {:error, :timeout} """ @spec await_result(String.t(), keyword()) :: - {:ok, map()} | {:error, :timeout | term()} + {:ok, map()} | {:error, :timeout | term()} def await_result(task_id, opts \\ []) do timeout = Keyword.get(opts, :timeout, 300_000) poll_interval = Keyword.get(opts, :poll_interval, @default_poll_interval) @@ -150,18 +152,19 @@ defmodule Opsm.HarQueue do files = Path.wildcard(Path.join(@queue_dir, "*")) - removed = Enum.count(files, fn file -> - stat = File.stat!(file) - gregorian = stat.mtime |> NaiveDateTime.to_gregorian_seconds() - file_time = elem(gregorian, 0) * 1000 - - if file_time < cutoff_time do - File.rm(file) - true - else - false - end - end) + removed = + Enum.count(files, fn file -> + stat = File.stat!(file) + gregorian = stat.mtime |> NaiveDateTime.to_gregorian_seconds() + file_time = elem(gregorian, 0) * 1000 + + if file_time < cutoff_time do + File.rm(file) + true + else + false + end + end) Logger.debug("Cleaned up #{removed} old HAR queue files") {:ok, removed} diff --git a/opsm_ex/lib/opsm/imp.ex b/opsm_ex/lib/opsm/imp.ex index ed04c0e3..870ad149 100644 --- a/opsm_ex/lib/opsm/imp.ex +++ b/opsm_ex/lib/opsm/imp.ex @@ -16,16 +16,18 @@ defmodule Opsm.Imp do @doc """ Normalize a `ManifestFormat` into the IMP shape and validate required fields. """ - @spec normalize(ManifestFormat.t(), String.t(), String.t()) :: {:ok, map()} | {:error, String.t()} + @spec normalize(ManifestFormat.t(), String.t(), String.t()) :: + {:ok, map()} | {:error, String.t()} def normalize(%ManifestFormat{} = manifest, manifest_path, digest) do with :ok <- ensure_required_fields(manifest) do - {:ok, %{ - "name" => manifest.name, - "version" => manifest.version || "0.0.0", - "license" => manifest.license, - "dependencies" => format_dependencies(manifest.dependencies || %{}), - "provenance" => build_provenance(manifest, manifest_path, digest) - }} + {:ok, + %{ + "name" => manifest.name, + "version" => manifest.version || "0.0.0", + "license" => manifest.license, + "dependencies" => format_dependencies(manifest.dependencies || %{}), + "provenance" => build_provenance(manifest, manifest_path, digest) + }} end end diff --git a/opsm_ex/lib/opsm/lockfile.ex b/opsm_ex/lib/opsm/lockfile.ex index 51f265d8..9e0ff398 100644 --- a/opsm_ex/lib/opsm/lockfile.ex +++ b/opsm_ex/lib/opsm/lockfile.ex @@ -27,26 +27,27 @@ defmodule Opsm.Lockfile do alias Opsm.Crypto.Symmetric @lockfile_name "opsm.lock" - @lockfile_version "2" # v2: Added crypto integration + # v2: Added crypto integration + @lockfile_version "2" @type package_entry :: %{ - name: String.t(), - version: String.t(), - forth: atom(), - checksum: String.t() | nil, - checksum_algo: String.t() | nil, - source_url: String.t() | nil, - dependencies: [String.t()], - installed_at: String.t() - } + name: String.t(), + version: String.t(), + forth: atom(), + checksum: String.t() | nil, + checksum_algo: String.t() | nil, + source_url: String.t() | nil, + dependencies: [String.t()], + installed_at: String.t() + } @type lockfile :: %{ - version: String.t(), - generated_at: String.t(), - packages: %{String.t() => package_entry()}, - integrity_hash: String.t() | nil, - integrity_algo: String.t() | nil - } + version: String.t(), + generated_at: String.t(), + packages: %{String.t() => package_entry()}, + integrity_hash: String.t() | nil, + integrity_algo: String.t() | nil + } @doc """ Read the lock file from the current directory or specified path. @@ -87,7 +88,8 @@ defmodule Opsm.Lockfile do if Keyword.get(opts, :verify_integrity, true) do case verify_integrity(lockfile) do :ok -> {:ok, lockfile} - {:ok, :no_integrity_hash} -> {:ok, lockfile} # Allow old lockfiles without integrity + # Allow old lockfiles without integrity + {:ok, :no_integrity_hash} -> {:ok, lockfile} {:error, reason} -> {:error, reason} end else @@ -105,26 +107,28 @@ defmodule Opsm.Lockfile do """ def write(lockfile, path \\ @lockfile_name, opts \\ []) do # Compute integrity hash before writing (SHA3-512 for long-term security) - lockfile_with_integrity = if Keyword.get(opts, :compute_integrity, true) do - compute_integrity_hash(lockfile) - else - lockfile - end + lockfile_with_integrity = + if Keyword.get(opts, :compute_integrity, true) do + compute_integrity_hash(lockfile) + else + lockfile + end content = serialize_lockfile(lockfile_with_integrity) # Optionally encrypt the lockfile - final_content = if Keyword.get(opts, :encrypt, false) do - key = Keyword.fetch!(opts, :key) - context = "opsm-lockfile-v#{@lockfile_version}" - - case Symmetric.encrypt(content, key, context) do - {:ok, encrypted} -> encrypted - {:error, reason} -> raise "Lockfile encryption failed: #{reason}" + final_content = + if Keyword.get(opts, :encrypt, false) do + key = Keyword.fetch!(opts, :key) + context = "opsm-lockfile-v#{@lockfile_version}" + + case Symmetric.encrypt(content, key, context) do + {:ok, encrypted} -> encrypted + {:error, reason} -> raise "Lockfile encryption failed: #{reason}" + end + else + content end - else - content - end case File.write(path, final_content) do :ok -> @@ -151,10 +155,7 @@ defmodule Opsm.Lockfile do json = Jason.encode!(data, pretty: true) hash = Hash.hash_provenance(json) - %{lockfile | - integrity_hash: hash, - integrity_algo: "sha3-512" - } + %{lockfile | integrity_hash: hash, integrity_algo: "sha3-512"} end @doc """ @@ -204,7 +205,8 @@ defmodule Opsm.Lockfile do version: package_info.version, forth: package_info.forth, checksum: Map.get(package_info, :checksum), - checksum_algo: Map.get(package_info, :checksum_algo, "blake2b"), # v1.0.1: Default to BLAKE2b + # v1.0.1: Default to BLAKE2b + checksum_algo: Map.get(package_info, :checksum_algo, "blake2b"), source_url: Map.get(package_info, :source_url), dependencies: Map.get(package_info, :dependencies, []), slsa_level: Map.get(package_info, :slsa_level), @@ -215,10 +217,12 @@ defmodule Opsm.Lockfile do key = "#{package_info.name}@#{package_info.forth}" packages = Map.put(lockfile.packages, key, entry) - %{lockfile | - packages: packages, - generated_at: DateTime.utc_now() |> DateTime.to_iso8601(), - integrity_hash: nil # Will be computed on write + %{ + lockfile + | packages: packages, + generated_at: DateTime.utc_now() |> DateTime.to_iso8601(), + # Will be computed on write + integrity_hash: nil } end @@ -229,10 +233,12 @@ defmodule Opsm.Lockfile do key = "#{name}@#{forth}" packages = Map.delete(lockfile.packages, key) - %{lockfile | - packages: packages, - generated_at: DateTime.utc_now() |> DateTime.to_iso8601(), - integrity_hash: nil # Will be recomputed on write + %{ + lockfile + | packages: packages, + generated_at: DateTime.utc_now() |> DateTime.to_iso8601(), + # Will be recomputed on write + integrity_hash: nil } end @@ -306,13 +312,15 @@ defmodule Opsm.Lockfile do Returns list of differences. """ def check_sync(lockfile, installed_packages) do - locked_set = lockfile.packages - |> Map.keys() - |> MapSet.new() + locked_set = + lockfile.packages + |> Map.keys() + |> MapSet.new() - installed_set = installed_packages - |> Enum.map(fn p -> "#{p.name}@#{p.forth}" end) - |> MapSet.new() + installed_set = + installed_packages + |> Enum.map(fn p -> "#{p.name}@#{p.forth}" end) + |> MapSet.new() missing_from_lock = MapSet.difference(installed_set, locked_set) missing_from_install = MapSet.difference(locked_set, installed_set) @@ -343,6 +351,7 @@ defmodule Opsm.Lockfile do {:ok, path} else parent = Path.dirname(dir) + if parent == dir do {:error, :not_found} else @@ -361,6 +370,7 @@ defmodule Opsm.Lockfile do integrity_hash: Map.get(data, "integrity_hash"), integrity_algo: Map.get(data, "integrity_algo", "sha3-512") } + {:ok, lockfile} {:error, reason} -> @@ -381,6 +391,7 @@ defmodule Opsm.Lockfile do dependencies: Map.get(value, "dependencies", []), installed_at: Map.get(value, "installed_at", "") } + {key, entry} end) |> Map.new() @@ -399,6 +410,7 @@ defmodule Opsm.Lockfile do dependencies: Map.get(value, "dependencies", []), installed_at: Map.get(value, "installed_at", "") } + {key, entry} end) |> Map.new() @@ -429,6 +441,7 @@ defmodule Opsm.Lockfile do "dependencies" => entry.dependencies, "installed_at" => entry.installed_at } + {key, value} end) |> Map.new() diff --git a/opsm_ex/lib/opsm/maintenance.ex b/opsm_ex/lib/opsm/maintenance.ex index e0455be0..8b7b6c3e 100644 --- a/opsm_ex/lib/opsm/maintenance.ex +++ b/opsm_ex/lib/opsm/maintenance.ex @@ -11,7 +11,6 @@ defmodule Opsm.Maintenance do - Autoremove unused dependencies """ - @history_path Path.expand("~/.local/share/opsm/history.json") @pins_path Path.expand("~/.local/share/opsm/pins.json") @max_history_entries 100 @@ -95,6 +94,7 @@ defmodule Opsm.Maintenance do {:ok, _} -> IO.puts(" ✓ Removed #{label}: #{format_size(size)}") {:ok, size} + {:error, reason, p} -> IO.puts(" ✗ Failed to remove #{p}: #{reason}") {:error, reason} @@ -111,13 +111,16 @@ defmodule Opsm.Maintenance do {:ok, files} -> Enum.reduce(files, 0, fn file, acc -> full_path = Path.join(path, file) + case File.stat(full_path) do {:ok, %{type: :directory}} -> acc + dir_size(full_path) {:ok, %{size: size}} -> acc + size _ -> acc end end) - _ -> 0 + + _ -> + 0 end end @@ -175,6 +178,7 @@ defmodule Opsm.Maintenance do def undo_by_id(id_or_pos) when is_integer(id_or_pos) do history = load_history() + case Enum.at(history, id_or_pos - 1) do nil -> {:error, "No history entry at position #{id_or_pos}"} entry -> do_undo_entry(entry) @@ -183,6 +187,7 @@ defmodule Opsm.Maintenance do def undo_by_id(id) when is_binary(id) do history = load_history() + case Enum.find(history, fn e -> e["id"] == id end) do nil -> {:error, "No history entry with id #{id}"} entry -> do_undo_entry(entry) @@ -196,11 +201,13 @@ defmodule Opsm.Maintenance do "install" -> package = entry["details"]["package"] IO.puts("Undoing: install #{package}") + case Installer.remove(package) do :ok -> record_history("undo_install", %{"package" => package, "undone_id" => entry["id"]}) IO.puts("✓ Removed #{package}") {:ok, :removed, package} + {:error, reason} -> IO.puts("✗ Failed to undo install: #{reason}") {:error, reason} @@ -213,11 +220,13 @@ defmodule Opsm.Maintenance do forth = Opsm.Validation.safe_to_forth(forth_str) IO.puts("Undoing: remove #{package}") + case Installer.install(forth, package, version: version || "latest") do {:ok, _} -> record_history("undo_remove", %{"package" => package, "undone_id" => entry["id"]}) IO.puts("✓ Reinstalled #{package}") {:ok, :reinstalled, package} + {:error, reason} -> IO.puts("✗ Failed to undo remove: #{inspect(reason)}") {:error, reason} @@ -243,11 +252,13 @@ defmodule Opsm.Maintenance do "install" -> package = last["details"]["package"] IO.puts("Undoing: install #{package}") + case Installer.remove(package) do :ok -> record_history("undo_install", %{"package" => package, "undone_id" => last["id"]}) IO.puts("✓ Removed #{package}") {:ok, :removed, package} + {:error, reason} -> IO.puts("✗ Failed to undo install: #{reason}") {:error, reason} @@ -260,11 +271,13 @@ defmodule Opsm.Maintenance do forth = Opsm.Validation.safe_to_forth(forth_str) IO.puts("Undoing: remove #{package}") + case Installer.install(forth, package, version: version || "latest") do {:ok, _} -> record_history("undo_remove", %{"package" => package, "undone_id" => last["id"]}) IO.puts("✓ Reinstalled #{package}") {:ok, :reinstalled, package} + {:error, reason} -> IO.puts("✗ Failed to undo remove: #{inspect(reason)}") {:error, reason} @@ -298,11 +311,13 @@ defmodule Opsm.Maintenance do forth = Opsm.Validation.safe_to_forth(forth_str) IO.puts("Redoing: install #{package}") + case Installer.install(forth, package, version: version) do {:ok, _} -> record_history("redo_install", %{"package" => package, "redone_id" => last["id"]}) IO.puts("✓ Reinstalled #{package}") {:ok, :reinstalled, package} + {:error, reason} -> IO.puts("✗ Failed to redo: #{inspect(reason)}") {:error, reason} @@ -312,11 +327,13 @@ defmodule Opsm.Maintenance do # The undo reinstalled a package — redo means remove it again package = last["details"]["package"] IO.puts("Redoing: remove #{package}") + case Installer.remove(package) do :ok -> record_history("redo_remove", %{"package" => package, "redone_id" => last["id"]}) IO.puts("✓ Removed #{package}") {:ok, :removed, package} + {:error, reason} -> IO.puts("✗ Failed to redo: #{inspect(reason)}") {:error, reason} @@ -343,7 +360,9 @@ defmodule Opsm.Maintenance do {:ok, data} when is_list(data) -> data _ -> [] end - _ -> [] + + _ -> + [] end end @@ -429,7 +448,9 @@ defmodule Opsm.Maintenance do {:ok, data} when is_list(data) -> data _ -> [] end - _ -> [] + + _ -> + [] end end @@ -454,36 +475,46 @@ defmodule Opsm.Maintenance do IO.puts("Checking for unused dependencies...") # Load installed packages - installed = case File.read(db_path) do - {:ok, data} -> - case Jason.decode(data) do - {:ok, pkgs} when is_map(pkgs) -> pkgs - _ -> %{} - end - {:error, _} -> %{} - end + installed = + case File.read(db_path) do + {:ok, data} -> + case Jason.decode(data) do + {:ok, pkgs} when is_map(pkgs) -> pkgs + _ -> %{} + end + + {:error, _} -> + %{} + end # Load lockfile to determine dependency graph - lockfile_deps = case File.read(lockfile_path) do - {:ok, data} -> - case Jason.decode(data) do - {:ok, lock} when is_map(lock) -> - lock["packages"] || [] - _ -> [] - end - {:error, _} -> [] - end + lockfile_deps = + case File.read(lockfile_path) do + {:ok, data} -> + case Jason.decode(data) do + {:ok, lock} when is_map(lock) -> + lock["packages"] || [] + + _ -> + [] + end + + {:error, _} -> + [] + end # Build set of packages that are dependencies of other packages - all_dep_names = lockfile_deps + all_dep_names = + lockfile_deps |> Enum.flat_map(fn pkg -> - (pkg["dependencies"] || []) + pkg["dependencies"] || [] end) |> MapSet.new() # Find packages that were installed as dependencies (not explicitly) # and are no longer required by any other package - orphans = installed + orphans = + installed |> Enum.filter(fn {_name, info} -> info["auto_installed"] == true end) @@ -499,9 +530,11 @@ defmodule Opsm.Maintenance do else IO.puts("") IO.puts("Found #{length(orphans)} unused dependencies:") + for {name, version} <- orphans do IO.puts(" #{name}@#{version}") end + IO.puts("") if dry_run do @@ -509,15 +542,16 @@ defmodule Opsm.Maintenance do {:ok, orphans} else # Remove each orphan - removed = Enum.map(orphans, fn {name, _version} -> - updated = Map.delete(installed, name) - db_path |> Path.dirname() |> File.mkdir_p!() - File.write!(db_path, Jason.encode!(updated, pretty: true)) - - # Record in history - record_history("autoremove", %{"package" => name}) - name - end) + removed = + Enum.map(orphans, fn {name, _version} -> + updated = Map.delete(installed, name) + db_path |> Path.dirname() |> File.mkdir_p!() + File.write!(db_path, Jason.encode!(updated, pretty: true)) + + # Record in history + record_history("autoremove", %{"package" => name}) + name + end) IO.puts("Removed #{length(removed)} unused dependencies") {:ok, removed} @@ -546,14 +580,17 @@ defmodule Opsm.Maintenance do def upgrade_path(package_name, opts \\ []) do db_path = Keyword.get(opts, :db_path, Path.expand("~/.local/share/opsm/installed.json")) - installed = case File.read(db_path) do - {:ok, data} -> - case Jason.decode(data) do - {:ok, pkgs} when is_map(pkgs) -> pkgs - _ -> %{} - end - {:error, _} -> %{} - end + installed = + case File.read(db_path) do + {:ok, data} -> + case Jason.decode(data) do + {:ok, pkgs} when is_map(pkgs) -> pkgs + _ -> %{} + end + + {:error, _} -> + %{} + end case Map.get(installed, package_name) do nil -> @@ -571,33 +608,36 @@ defmodule Opsm.Maintenance do case Opsm.Registries.Registry.versions(forth, package_name) do {:ok, all_versions} -> # Filter to versions newer than current - newer = Enum.filter(all_versions, fn v -> - version_newer?(v, current_version) - end) + newer = + Enum.filter(all_versions, fn v -> + version_newer?(v, current_version) + end) latest = List.first(newer) || current_version - action = cond do - is_pinned and pinned_version != nil -> - :pinned_skip - - newer == [] -> - :up_to_date - - true -> - :upgrade_available - end - - {:ok, %{ - package: package_name, - forth: forth, - current: current_version, - latest: latest, - available: newer, - pinned: is_pinned, - pinned_version: pinned_version, - action: action - }} + action = + cond do + is_pinned and pinned_version != nil -> + :pinned_skip + + newer == [] -> + :up_to_date + + true -> + :upgrade_available + end + + {:ok, + %{ + package: package_name, + forth: forth, + current: current_version, + latest: latest, + available: newer, + pinned: is_pinned, + pinned_version: pinned_version, + action: action + }} {:error, reason} -> {:error, {:registry_error, reason}} diff --git a/opsm_ex/lib/opsm/manifest/opsm_toml.ex b/opsm_ex/lib/opsm/manifest/opsm_toml.ex index fb3eb50b..ffcf6586 100644 --- a/opsm_ex/lib/opsm/manifest/opsm_toml.ex +++ b/opsm_ex/lib/opsm/manifest/opsm_toml.ex @@ -100,7 +100,9 @@ defmodule Opsm.Manifest.OpsmToml do @spec build_config(ManifestFormat.t()) :: map() | nil def build_config(%ManifestFormat{raw_manifest: raw}) when is_map(raw) do case raw["build"] do - nil -> nil + nil -> + nil + build -> %{ system: safe_to_build_system(build["system"]), @@ -143,6 +145,7 @@ defmodule Opsm.Manifest.OpsmToml do @known_build_systems ~w(just make cargo mix npm python go zig bundler pub gradle maven cabal stack dune)a defp safe_to_build_system(nil), do: nil + defp safe_to_build_system(s) when is_binary(s) do atom = String.to_existing_atom(s) if atom in @known_build_systems, do: atom, else: nil diff --git a/opsm_ex/lib/opsm/manifest/writer.ex b/opsm_ex/lib/opsm/manifest/writer.ex index 697a924a..de7b3016 100644 --- a/opsm_ex/lib/opsm/manifest/writer.ex +++ b/opsm_ex/lib/opsm/manifest/writer.ex @@ -33,18 +33,19 @@ defmodule Opsm.Manifest.Writer do """ @spec to_package_json(ManifestFormat.t()) :: String.t() def to_package_json(%ManifestFormat{} = m) do - json = %{ - "name" => m.name, - "version" => m.version || "0.0.0", - "description" => m.description, - "license" => m.license, - "homepage" => m.homepage, - "repository" => m.repository, - "keywords" => m.keywords || [], - "dependencies" => format_npm_deps(m.dependencies), - "devDependencies" => format_npm_deps(m.dev_dependencies) - } - |> reject_nil_values() + json = + %{ + "name" => m.name, + "version" => m.version || "0.0.0", + "description" => m.description, + "license" => m.license, + "homepage" => m.homepage, + "repository" => m.repository, + "keywords" => m.keywords || [], + "dependencies" => format_npm_deps(m.dependencies), + "devDependencies" => format_npm_deps(m.dev_dependencies) + } + |> reject_nil_values() json = if m.authors != [] do @@ -75,7 +76,11 @@ defmodule Opsm.Manifest.Writer do ~s(edition = "2021") ] - lines = if m.description, do: lines ++ [~s(description = "#{escape_toml(m.description)}")], else: lines + lines = + if m.description, + do: lines ++ [~s(description = "#{escape_toml(m.description)}")], + else: lines + lines = if m.license, do: lines ++ [~s(license = "#{m.license}")], else: lines lines = if m.repository, do: lines ++ [~s(repository = "#{m.repository}")], else: lines @@ -193,7 +198,11 @@ defmodule Opsm.Manifest.Writer do ~s(version = "#{m.version || "0.0.0"}") ] - lines = if m.description, do: lines ++ [~s(description = "#{escape_toml(m.description)}")], else: lines + lines = + if m.description, + do: lines ++ [~s(description = "#{escape_toml(m.description)}")], + else: lines + lines = if m.license, do: lines ++ [~s(license = "#{m.license}")], else: lines lines = @@ -229,7 +238,7 @@ defmodule Opsm.Manifest.Writer do lines end - lines ++ [""] |> Enum.join("\n") + (lines ++ [""]) |> Enum.join("\n") end @doc """ @@ -242,7 +251,11 @@ defmodule Opsm.Manifest.Writer do "version: #{m.version || "0.0.0"}" ] - lines = if m.description, do: lines ++ ["description: \"#{escape_yaml(m.description)}\""], else: lines + lines = + if m.description, + do: lines ++ ["description: \"#{escape_yaml(m.description)}\""], + else: lines + lines = if m.homepage, do: lines ++ ["homepage: #{m.homepage}"], else: lines lines = if m.repository, do: lines ++ ["repository: #{m.repository}"], else: lines @@ -320,7 +333,11 @@ defmodule Opsm.Manifest.Writer do ~s(version = "#{m.version || "0.0.0"}") ] - lines = if m.description, do: lines ++ [~s(description = "#{escape_toml(m.description)}")], else: lines + lines = + if m.description, + do: lines ++ [~s(description = "#{escape_toml(m.description)}")], + else: lines + lines = if m.license, do: lines ++ [~s(license = "#{m.license}")], else: lines lines = if m.homepage, do: lines ++ [~s(homepage = "#{m.homepage}")], else: lines lines = if m.repository, do: lines ++ [~s(repository = "#{m.repository}")], else: lines @@ -398,11 +415,16 @@ defmodule Opsm.Manifest.Writer do end defp format_npm_deps(nil), do: %{} + defp format_npm_deps(deps) when is_map(deps) do Enum.map(deps, fn {name, version} when is_binary(version) -> # Ensure npm-style version prefix - {name, if(String.starts_with?(version, "^") or String.starts_with?(version, "~") or String.starts_with?(version, ">"), do: version, else: "^#{version}")} + {name, + if( + String.starts_with?(version, "^") or String.starts_with?(version, "~") or + String.starts_with?(version, ">"), do: version, else: "^#{version}")} + {name, version} -> {name, "^#{version}"} end) diff --git a/opsm_ex/lib/opsm/manifest_ingestion.ex b/opsm_ex/lib/opsm/manifest_ingestion.ex index b3318fdb..813e083b 100644 --- a/opsm_ex/lib/opsm/manifest_ingestion.ex +++ b/opsm_ex/lib/opsm/manifest_ingestion.ex @@ -99,9 +99,15 @@ defmodule Opsm.ManifestIngestion do # Create tarball using system tar command Logger.info("Creating tarball: #{tarball_path}") - case Opsm.SafeExec.cmd("tar", ["-czf", tarball_path, "-C", Path.dirname(package_path), Path.basename(package_path)], - stderr_to_stdout: true - ) do + case Opsm.SafeExec.cmd( + "tar", + [ + "-czf", + tarball_path, + "-C", + Path.dirname(package_path), + Path.basename(package_path) + ], stderr_to_stdout: true) do {_, 0} -> Logger.info("Tarball created successfully: #{tarball_path}") {:ok, tarball_path} diff --git a/opsm_ex/lib/opsm/network/ipv6.ex b/opsm_ex/lib/opsm/network/ipv6.ex index b574c715..de6864e5 100644 --- a/opsm_ex/lib/opsm/network/ipv6.ex +++ b/opsm_ex/lib/opsm/network/ipv6.ex @@ -219,9 +219,10 @@ defmodule Opsm.Network.Ipv6 do _ -> Logger.warning("IPv6 enforcement: #{host} has no AAAA records — connection blocked") - {:error, {:ipv6_required, host, + {:error, + {:ipv6_required, host, "Host #{host} has no IPv6 (AAAA) records. " <> - "Set OPSM_IPV6_MODE=prefer to allow IPv4 fallback."}} + "Set OPSM_IPV6_MODE=prefer to allow IPv4 fallback."}} end end diff --git a/opsm_ex/lib/opsm/package/cleanup.ex b/opsm_ex/lib/opsm/package/cleanup.ex index ba9d50ff..ddf57acc 100644 --- a/opsm_ex/lib/opsm/package/cleanup.ex +++ b/opsm_ex/lib/opsm/package/cleanup.ex @@ -56,10 +56,12 @@ defmodule Opsm.Package.Cleanup do case File.rm(desktop_file) do :ok -> # Update desktop database - System.cmd("update-desktop-database", + System.cmd( + "update-desktop-database", [Path.join(xdg_data, "applications")], stderr_to_stdout: true ) + Logger.info("Removed desktop shortcut: #{desktop_file}") [{:removed, :desktop_shortcut, desktop_file}] @@ -109,6 +111,7 @@ defmodule Opsm.Package.Cleanup do System.cmd("systemctl", ["--user", "stop", "#{package_name}#{ext}"], stderr_to_stdout: true ) + System.cmd("systemctl", ["--user", "disable", "#{package_name}#{ext}"], stderr_to_stdout: true ) @@ -145,6 +148,7 @@ defmodule Opsm.Package.Cleanup do System.cmd("update-mime-database", [Path.join(xdg_data, "mime")], stderr_to_stdout: true ) + Logger.info("Removed MIME associations: #{mime_file}") [{:removed, :mime_type, mime_file}] diff --git a/opsm_ex/lib/opsm/package/downloader.ex b/opsm_ex/lib/opsm/package/downloader.ex index b9e104d5..a5a47e79 100644 --- a/opsm_ex/lib/opsm/package/downloader.ex +++ b/opsm_ex/lib/opsm/package/downloader.ex @@ -39,8 +39,11 @@ defmodule Opsm.Package.Downloader do # Local hit: verify and return. if File.exists?(cache_path) and not force? do case verify_checksum(cache_path, checksum, algo) do - :ok -> {:ok, cache_path} - {:error, _} -> fresh_download(url, cache_path, storage_key, checksum, algo, StorageManager) + :ok -> + {:ok, cache_path} + + {:error, _} -> + fresh_download(url, cache_path, storage_key, checksum, algo, StorageManager) end else # Try remote backends before hitting the registry. @@ -71,7 +74,13 @@ defmodule Opsm.Package.Downloader do # Derive a content-addressed storage key that mirrors the local cache layout. defp storage_key_for(package) do forth = package.forth - name = package.package |> String.replace("/", "--") |> String.replace(":", "--") |> String.replace("@", "_at_") + + name = + package.package + |> String.replace("/", "--") + |> String.replace(":", "--") + |> String.replace("@", "_at_") + version = package.version ext = extension_for(forth) "#{forth}/#{name}-#{version}#{ext}" @@ -102,7 +111,8 @@ defmodule Opsm.Package.Downloader do ext = extension_for(forth) # Sanitize package name for filesystem (Go paths contain slashes, Maven uses colons) - safe_name = name + safe_name = + name |> String.replace("/", "--") |> String.replace(":", "--") |> String.replace("@", "_at_") @@ -124,6 +134,7 @@ defmodule Opsm.Package.Downloader do forth -> path = Path.join(@cache_dir, to_string(forth)) + case File.rm_rf(path) do {:ok, _} -> :ok {:error, reason, p} -> {:error, "Failed to clear cache at #{p}: #{reason}"} @@ -145,7 +156,8 @@ defmodule Opsm.Package.Downloader do list_cache_dir(Path.join(@cache_dir, dir), dir) end) - {:error, _} -> [] + {:error, _} -> + [] end forth -> @@ -162,7 +174,12 @@ defmodule Opsm.Package.Downloader do File.mkdir_p!(@temp_dir) # Download to temp file first (D3: atomic download) - temp_path = Path.join(@temp_dir, "download_#{:rand.uniform(1_000_000)}_#{System.system_time(:millisecond)}") + temp_path = + Path.join( + @temp_dir, + "download_#{:rand.uniform(1_000_000)}_#{System.system_time(:millisecond)}" + ) + started_at = System.monotonic_time(:millisecond) # Warn if no checksum provided (F3) @@ -170,57 +187,70 @@ defmodule Opsm.Package.Downloader do IO.puts(" \e[33m⚠ No checksum provided — cannot verify download integrity\e[0m") end - result = case Req.get(url, into: File.stream!(temp_path), receive_timeout: 60_000) do - {:ok, %{status: 200}} -> - elapsed_ms = System.monotonic_time(:millisecond) - started_at - size = File.stat!(temp_path).size - speed = if elapsed_ms > 0, do: size / (elapsed_ms / 1000), else: 0 + result = + case Req.get(url, into: File.stream!(temp_path), receive_timeout: 60_000) do + {:ok, %{status: 200}} -> + elapsed_ms = System.monotonic_time(:millisecond) - started_at + size = File.stat!(temp_path).size + speed = if elapsed_ms > 0, do: size / (elapsed_ms / 1000), else: 0 - case verify_checksum(temp_path, checksum, algo) do - :ok -> - # Atomic move from temp to final destination - case File.rename(temp_path, dest_path) do - :ok -> - IO.puts(" \e[32m✓\e[0m #{format_size(size)} in #{format_duration_ms(elapsed_ms)} (#{format_speed(speed)})") - {:ok, dest_path} - - {:error, :exdev} -> - case File.cp(temp_path, dest_path) do - :ok -> - File.rm(temp_path) - IO.puts(" \e[32m✓\e[0m #{format_size(size)} in #{format_duration_ms(elapsed_ms)} (#{format_speed(speed)})") - {:ok, dest_path} - {:error, reason} -> - {:error, "Failed to copy downloaded file: #{reason}"} - end - - {:error, reason} -> - {:error, "Failed to move downloaded file: #{reason}"} - end - - {:error, reason} -> - {:error, reason} - end + case verify_checksum(temp_path, checksum, algo) do + :ok -> + # Atomic move from temp to final destination + case File.rename(temp_path, dest_path) do + :ok -> + IO.puts( + " \e[32m✓\e[0m #{format_size(size)} in #{format_duration_ms(elapsed_ms)} (#{format_speed(speed)})" + ) - {:ok, %{status: 302, headers: headers}} -> - File.rm(temp_path) - case List.keyfind(headers, "location", 0) do - {"location", redirect_url} -> - do_download(redirect_url, dest_path, checksum, algo) - _ -> - {:error, "Redirect without location header"} - end + {:ok, dest_path} - {:ok, %{status: status}} -> - {:error, "Download failed with status #{status}"} + {:error, :exdev} -> + case File.cp(temp_path, dest_path) do + :ok -> + File.rm(temp_path) - {:error, reason} -> - {:error, "Download failed: #{inspect(reason)}"} - end + IO.puts( + " \e[32m✓\e[0m #{format_size(size)} in #{format_duration_ms(elapsed_ms)} (#{format_speed(speed)})" + ) + + {:ok, dest_path} + + {:error, reason} -> + {:error, "Failed to copy downloaded file: #{reason}"} + end + + {:error, reason} -> + {:error, "Failed to move downloaded file: #{reason}"} + end + + {:error, reason} -> + {:error, reason} + end + + {:ok, %{status: 302, headers: headers}} -> + File.rm(temp_path) + + case List.keyfind(headers, "location", 0) do + {"location", redirect_url} -> + do_download(redirect_url, dest_path, checksum, algo) + + _ -> + {:error, "Redirect without location header"} + end + + {:ok, %{status: status}} -> + {:error, "Download failed with status #{status}"} + + {:error, reason} -> + {:error, "Download failed: #{inspect(reason)}"} + end # Clean up temp file on any error case result do - {:ok, _} -> result + {:ok, _} -> + result + {:error, _} -> File.rm(temp_path) result @@ -228,7 +258,10 @@ defmodule Opsm.Package.Downloader do end defp format_speed(bytes_per_sec) when bytes_per_sec < 1024, do: "#{round(bytes_per_sec)} B/s" - defp format_speed(bytes_per_sec) when bytes_per_sec < 1024 * 1024, do: "#{Float.round(bytes_per_sec / 1024, 1)} KB/s" + + defp format_speed(bytes_per_sec) when bytes_per_sec < 1024 * 1024, + do: "#{Float.round(bytes_per_sec / 1024, 1)} KB/s" + defp format_speed(bytes_per_sec), do: "#{Float.round(bytes_per_sec / (1024 * 1024), 2)} MB/s" defp format_duration_ms(ms) when ms < 1000, do: "#{ms}ms" @@ -255,13 +288,14 @@ defmodule Opsm.Package.Downloader do end defp compute_checksum(path, algo) do - hash_algo = case algo do - :sha256 -> :sha256 - :sha512 -> :sha512 - :sha1 -> :sha - :md5 -> :md5 - _ -> :sha256 - end + hash_algo = + case algo do + :sha256 -> :sha256 + :sha512 -> :sha512 + :sha1 -> :sha + :md5 -> :md5 + _ -> :sha256 + end File.stream!(path, [], 65536) |> Enum.reduce(:crypto.hash_init(hash_algo), fn chunk, acc -> @@ -293,6 +327,7 @@ defmodule Opsm.Package.Downloader do Enum.map(files, fn file -> path = Path.join(dir, file) stat = File.stat!(path) + %{ forth: forth_name, file: file, @@ -302,7 +337,8 @@ defmodule Opsm.Package.Downloader do } end) - {:error, _} -> [] + {:error, _} -> + [] end end diff --git a/opsm_ex/lib/opsm/package/installer.ex b/opsm_ex/lib/opsm/package/installer.ex index 09a30986..621f4ce0 100644 --- a/opsm_ex/lib/opsm/package/installer.ex +++ b/opsm_ex/lib/opsm/package/installer.ex @@ -168,15 +168,20 @@ defmodule Opsm.Package.Installer do # CVE/OSV advisory scan + typosquat check (warn-only — never blocks install). try do - {:ok, sec_report} = Opsm.Security.Scanner.scan_resolved(package.package, version, package.forth) + {:ok, sec_report} = + Opsm.Security.Scanner.scan_resolved(package.package, version, package.forth) + if !Opsm.Security.Scanner.Report.clean?(sec_report) do IO.puts(" ⚠ Security warnings for #{package.package}@#{version}:") + if match?({:suspicious, _}, sec_report.typosquat) do IO.puts(" Possible typosquat — verify package origin before use") end + crit = Opsm.Security.Scanner.Report.critical_count(sec_report) high = Opsm.Security.Scanner.Report.high_count(sec_report) total = length(sec_report.vulnerabilities) + if total > 0 do IO.puts(" #{total} OSV advisory/ies (#{crit} critical, #{high} high)") IO.puts(" Run `opsm scan #{package.package}` for full details") @@ -189,47 +194,67 @@ defmodule Opsm.Package.Installer do # Run trust pipeline — gracefully degrade if services are unreachable. # Trust is advisory during install; only an explicit :failed blocks. IO.puts(" Running trust checks...") - {trust_result, enriched_package} = try do - # Pipeline.verify/2 cannot return {:error, _} — failed checks come back - # inside the {:ok, results} map; service failures raise into the rescue. - {:ok, trust_result} = Pipeline.verify(package) - {trust_result, package} - rescue - e -> - IO.puts(" ⚠ Trust pipeline unavailable: #{Exception.message(e)} — continuing") - {%{overall: :warning, warnings: ["Trust services unreachable"], checks: %{}, recommendations: []}, package} - end + + {trust_result, enriched_package} = + try do + # Pipeline.verify/2 cannot return {:error, _} — failed checks come back + # inside the {:ok, results} map; service failures raise into the rescue. + {:ok, trust_result} = Pipeline.verify(package) + {trust_result, package} + rescue + e -> + IO.puts(" ⚠ Trust pipeline unavailable: #{Exception.message(e)} — continuing") + + {%{ + overall: :warning, + warnings: ["Trust services unreachable"], + checks: %{}, + recommendations: [] + }, package} + end # Generate PQ trust attestation (non-blocking — failures don't halt install) - pq_attestation = try do - PqTrust.trust_attestation(package.package, version, package.checksum) - rescue - _ -> nil - end + pq_attestation = + try do + PqTrust.trust_attestation(package.package, version, package.checksum) + rescue + _ -> nil + end # Generate SLSA provenance (non-blocking) - slsa_provenance = try do - pkg_info = %{name: package.package, version: version, forth: package.forth, - tarball_url: package.tarball_url} - case Slsa.generate_provenance(pkg_info) do - {:ok, statement} -> statement + slsa_provenance = + try do + pkg_info = %{ + name: package.package, + version: version, + forth: package.forth, + tarball_url: package.tarball_url + } + + case Slsa.generate_provenance(pkg_info) do + {:ok, statement} -> statement + _ -> nil + end + rescue _ -> nil end - rescue - _ -> nil - end # Attach attestations to package for lockfile # Use Map.put for fields not in the ResolvedPackage struct - enriched = enriched_package - |> Map.put(:attestations, List.wrap(pq_attestation) ++ List.wrap(Map.get(enriched_package, :attestations))) - |> Map.put(:slsa_provenance, slsa_provenance) - - result = case handle_trust_result(trust_result, enriched, scope, dry_run) do - {:ok, :dry_run} -> {:ok, :dry_run} - {:ok, _} -> {:ok, enriched} - {:error, reason} -> {:error, {package.package, reason}} - end + enriched = + enriched_package + |> Map.put( + :attestations, + List.wrap(pq_attestation) ++ List.wrap(Map.get(enriched_package, :attestations)) + ) + |> Map.put(:slsa_provenance, slsa_provenance) + + result = + case handle_trust_result(trust_result, enriched, scope, dry_run) do + {:ok, :dry_run} -> {:ok, :dry_run} + {:ok, _} -> {:ok, enriched} + {:error, reason} -> {:error, {package.package, reason}} + end case result do {:ok, _} -> @@ -241,8 +266,10 @@ defmodule Opsm.Package.Installer do if installed != [] and not dry_run do IO.puts("") IO.puts("Rolling back #{length(installed)} previously installed package(s)...") + Enum.each(installed, fn pkg -> IO.puts(" Removing #{pkg.package}...") + case find_installed(pkg.package) do nil -> :ok entry -> do_remove(entry) @@ -279,23 +306,26 @@ defmodule Opsm.Package.Installer do Enum.reduce(packages, lockfile, fn {version, pkg}, acc -> dep_names = Map.keys(pkg.manifest.dependencies || %{}) - {checksum, algo} = if is_nil(pkg.checksum) do - # Compute checksum from cached download - cache_path = Downloader.cache_path_for(pkg) - if File.exists?(cache_path) do - {Downloader.compute_file_checksum(cache_path, :sha256), :sha256} + {checksum, algo} = + if is_nil(pkg.checksum) do + # Compute checksum from cached download + cache_path = Downloader.cache_path_for(pkg) + + if File.exists?(cache_path) do + {Downloader.compute_file_checksum(cache_path, :sha256), :sha256} + else + {nil, nil} + end else - {nil, nil} + {pkg.checksum, pkg.checksum_algo} end - else - {pkg.checksum, pkg.checksum_algo} - end # Extract SLSA metadata if provenance was generated - slsa_meta = case Map.get(pkg, :slsa_provenance) do - nil -> %{slsa_level: nil, slsa_provenance_uri: nil} - prov -> Slsa.lockfile_metadata(%{statement: prov}) - end + slsa_meta = + case Map.get(pkg, :slsa_provenance) do + nil -> %{slsa_level: nil, slsa_provenance_uri: nil} + prov -> Slsa.lockfile_metadata(%{statement: prov}) + end Lockfile.add_package(acc, %{ name: pkg.package, @@ -328,16 +358,20 @@ defmodule Opsm.Package.Installer do case trust_result.overall do :failed -> IO.puts(" ✗ Trust pipeline failed") + for warn <- trust_result.warnings do IO.puts(" - #{warn}") end + {:error, Errors.format(Errors.trust_failed(package.package, trust_result.warnings))} :warning -> IO.puts(" ⚠ Trust pipeline warnings:") + for warn <- trust_result.warnings do IO.puts(" - #{warn}") end + IO.puts(" Proceeding with installation...") continue_install(package, scope, dry_run) @@ -358,6 +392,7 @@ defmodule Opsm.Package.Installer do # Download IO.puts("") + case Downloader.download(package) do {:ok, tarball_path} -> # Compute checksum post-download if registry didn't provide one @@ -370,7 +405,9 @@ defmodule Opsm.Package.Installer do txn = Transaction.record_file(txn, tarball_path) # Unpack and install - install_path = Path.join([install_dir(scope), to_string(package.forth), package.package]) + install_path = + Path.join([install_dir(scope), to_string(package.forth), package.package]) + IO.puts(" Installing to: #{install_path}") # Create install directory with transaction tracking @@ -382,7 +419,9 @@ defmodule Opsm.Package.Installer do case link_binaries_safe(txn, install_path, package.forth, scope) do {:ok, txn, linked_bins} -> if linked_bins != [] do - IO.puts(" Linked #{length(linked_bins)} executable(s) to #{bin_dir(scope)}") + IO.puts( + " Linked #{length(linked_bins)} executable(s) to #{bin_dir(scope)}" + ) end # Register in installed db @@ -392,13 +431,24 @@ defmodule Opsm.Package.Installer do _txn = Transaction.complete(txn) IO.puts("") - IO.puts(Opsm.Colour.ok("Installed #{Opsm.Colour.cyan("#{package.package}@#{package.version}")}")) + + IO.puts( + Opsm.Colour.ok( + "Installed #{Opsm.Colour.cyan("#{package.package}@#{package.version}")}" + ) + ) + {:ok, package} {:error, reason} -> # Rollback on symlink failure Transaction.rollback(txn) - {:error, Errors.format({:install, "Failed to link binaries: #{reason}", "Check permissions on #{bin_dir(scope)}"})} + + {:error, + Errors.format( + {:install, "Failed to link binaries: #{reason}", + "Check permissions on #{bin_dir(scope)}"} + )} end {:error, reason} -> @@ -410,7 +460,11 @@ defmodule Opsm.Package.Installer do {:error, reason} -> # Rollback on mkdir failure Transaction.rollback(txn) - {:error, Errors.format({:install, "Failed to create install directory: #{reason}", "Check permissions"})} + + {:error, + Errors.format( + {:install, "Failed to create install directory: #{reason}", "Check permissions"} + )} end {:error, reason} -> @@ -444,7 +498,8 @@ defmodule Opsm.Package.Installer do defp unpack_npm(tarball_path, dest_path) do # npm packages are .tgz with a `package/` prefix case Opsm.SafeExec.cmd("tar", ["-xzf", tarball_path, "-C", dest_path, "--strip-components=1"], - stderr_to_stdout: true) do + stderr_to_stdout: true + ) do {_, 0} -> :ok {error, _} -> {:error, error} end @@ -453,7 +508,8 @@ defmodule Opsm.Package.Installer do defp unpack_crate(tarball_path, dest_path) do # Rust crates are .crate (gzipped tar) with package-version/ prefix case Opsm.SafeExec.cmd("tar", ["-xzf", tarball_path, "-C", dest_path, "--strip-components=1"], - stderr_to_stdout: true) do + stderr_to_stdout: true + ) do {_, 0} -> :ok {error, _} -> {:error, error} end @@ -462,18 +518,22 @@ defmodule Opsm.Package.Installer do defp unpack_hex(tarball_path, dest_path) do # Hex packages are .tar containing contents.tar.gz # First extract the outer tar - tmp_dir = Path.join(System.tmp_dir!(), "opsm_hex_#{:rand.uniform(100000)}") + tmp_dir = Path.join(System.tmp_dir!(), "opsm_hex_#{:rand.uniform(100_000)}") File.mkdir_p!(tmp_dir) case Opsm.SafeExec.cmd("tar", ["-xf", tarball_path, "-C", tmp_dir], stderr_to_stdout: true) do {_, 0} -> # Now extract contents.tar.gz contents_tar = Path.join(tmp_dir, "contents.tar.gz") + if File.exists?(contents_tar) do - case Opsm.SafeExec.cmd("tar", ["-xzf", contents_tar, "-C", dest_path], stderr_to_stdout: true) do + case Opsm.SafeExec.cmd("tar", ["-xzf", contents_tar, "-C", dest_path], + stderr_to_stdout: true + ) do {_, 0} -> File.rm_rf!(tmp_dir) :ok + {error, _} -> File.rm_rf!(tmp_dir) {:error, error} @@ -492,13 +552,18 @@ defmodule Opsm.Package.Installer do defp unpack_pypi(tarball_path, dest_path) do # Python packages are .tar.gz or .whl (zip) with package-version/ prefix if String.ends_with?(tarball_path, ".whl") do - case Opsm.SafeExec.cmd("unzip", ["-q", tarball_path, "-d", dest_path], stderr_to_stdout: true) do + case Opsm.SafeExec.cmd("unzip", ["-q", tarball_path, "-d", dest_path], + stderr_to_stdout: true + ) do {_, 0} -> :ok {error, _} -> {:error, error} end else - case Opsm.SafeExec.cmd("tar", ["-xzf", tarball_path, "-C", dest_path, "--strip-components=1"], - stderr_to_stdout: true) do + case Opsm.SafeExec.cmd( + "tar", + ["-xzf", tarball_path, "-C", dest_path, "--strip-components=1"], + stderr_to_stdout: true + ) do {_, 0} -> :ok {error, _} -> {:error, error} end @@ -507,17 +572,21 @@ defmodule Opsm.Package.Installer do defp unpack_gem(tarball_path, dest_path) do # Ruby gems are tar archives containing data.tar.gz + metadata.gz - tmp_dir = Path.join(System.tmp_dir!(), "opsm_gem_#{:rand.uniform(100000)}") + tmp_dir = Path.join(System.tmp_dir!(), "opsm_gem_#{:rand.uniform(100_000)}") File.mkdir_p!(tmp_dir) case Opsm.SafeExec.cmd("tar", ["-xf", tarball_path, "-C", tmp_dir], stderr_to_stdout: true) do {_, 0} -> data_tar = Path.join(tmp_dir, "data.tar.gz") + if File.exists?(data_tar) do - case Opsm.SafeExec.cmd("tar", ["-xzf", data_tar, "-C", dest_path], stderr_to_stdout: true) do + case Opsm.SafeExec.cmd("tar", ["-xzf", data_tar, "-C", dest_path], + stderr_to_stdout: true + ) do {_, 0} -> File.rm_rf!(tmp_dir) :ok + {error, _} -> File.rm_rf!(tmp_dir) {:error, error} @@ -538,7 +607,7 @@ defmodule Opsm.Package.Installer do # Go module zips contain: module/path@version/ as the prefix # e.g. github.com/fatih/color@v1.18.0/color.go # Unzip to temp dir, find the versioned dir, move its contents to dest - tmp_dir = Path.join(System.tmp_dir!(), "opsm_go_#{:rand.uniform(100000)}") + tmp_dir = Path.join(System.tmp_dir!(), "opsm_go_#{:rand.uniform(100_000)}") File.mkdir_p!(tmp_dir) case Opsm.SafeExec.cmd("unzip", ["-q", zip_path, "-d", tmp_dir], stderr_to_stdout: true) do @@ -559,8 +628,10 @@ defmodule Opsm.Package.Installer do File.cp!(src, dst) end end) + File.rm_rf!(tmp_dir) :ok + _ -> File.rm_rf!(tmp_dir) :ok @@ -575,8 +646,11 @@ defmodule Opsm.Package.Installer do dst = Path.join(dest_path, file) if File.dir?(src), do: File.cp_r!(src, dst), else: File.cp!(src, dst) end) - _ -> :ok + + _ -> + :ok end + File.rm_rf!(tmp_dir) :ok end @@ -592,21 +666,25 @@ defmodule Opsm.Package.Installer do case File.ls(dir) do {:ok, entries} -> # Check if any entry has @v pattern (module@version format) - versioned = Enum.find(entries, fn entry -> - String.contains?(entry, "@v") - end) + versioned = + Enum.find(entries, fn entry -> + String.contains?(entry, "@v") + end) if versioned do {:ok, Path.join(dir, versioned)} else # Recurse into single subdirectory subdirs = Enum.filter(entries, fn entry -> File.dir?(Path.join(dir, entry)) end) + case subdirs do [single] -> find_go_module_root(Path.join(dir, single)) _ -> :error end end - _ -> :error + + _ -> + :error end end @@ -620,11 +698,12 @@ defmodule Opsm.Package.Installer do defp unpack_tarball(tarball_path, dest_path, opts) do strip = Keyword.get(opts, :strip, 0) - args = if strip > 0 do - ["-xzf", tarball_path, "-C", dest_path, "--strip-components=#{strip}"] - else - ["-xzf", tarball_path, "-C", dest_path] - end + args = + if strip > 0 do + ["-xzf", tarball_path, "-C", dest_path, "--strip-components=#{strip}"] + else + ["-xzf", tarball_path, "-C", dest_path] + end case Opsm.SafeExec.cmd("tar", args, stderr_to_stdout: true) do {_, 0} -> :ok @@ -633,16 +712,20 @@ defmodule Opsm.Package.Installer do end defp unpack_generic(tarball_path, dest_path) do - {cmd, args} = cond do - String.ends_with?(tarball_path, ".tar.gz") or String.ends_with?(tarball_path, ".tgz") -> - {"tar", ["-xzf", tarball_path, "-C", dest_path]} - String.ends_with?(tarball_path, ".tar") -> - {"tar", ["-xf", tarball_path, "-C", dest_path]} - String.ends_with?(tarball_path, ".zip") or String.ends_with?(tarball_path, ".nupkg") -> - {"unzip", ["-q", tarball_path, "-d", dest_path]} - true -> - {"tar", ["-xzf", tarball_path, "-C", dest_path]} - end + {cmd, args} = + cond do + String.ends_with?(tarball_path, ".tar.gz") or String.ends_with?(tarball_path, ".tgz") -> + {"tar", ["-xzf", tarball_path, "-C", dest_path]} + + String.ends_with?(tarball_path, ".tar") -> + {"tar", ["-xf", tarball_path, "-C", dest_path]} + + String.ends_with?(tarball_path, ".zip") or String.ends_with?(tarball_path, ".nupkg") -> + {"unzip", ["-q", tarball_path, "-d", dest_path]} + + true -> + {"tar", ["-xzf", tarball_path, "-C", dest_path]} + end case Opsm.SafeExec.cmd(cmd, args, stderr_to_stdout: true) do {_, 0} -> :ok @@ -685,16 +768,20 @@ defmodule Opsm.Package.Installer do {:ok, %{type: :symlink}} -> # Remove old symlink File.rm(link_path) + {:ok, %{type: :regular}} -> # Regular file exists - don't overwrite, warn user IO.puts(" ⚠ Skipping #{exe_name}: regular file exists at #{link_path}") link_executables_safe(txn, rest, bin_target, acc) + {:ok, %{type: :directory}} -> IO.puts(" ⚠ Skipping #{exe_name}: directory exists at #{link_path}") link_executables_safe(txn, rest, bin_target, acc) + {:error, :enoent} -> # Doesn't exist - good :ok + {:error, _} -> :ok end @@ -732,7 +819,11 @@ defmodule Opsm.Package.Installer do package_json = Path.join(install_path, "package.json") if File.exists?(package_json) do - case File.read(package_json) |> then(fn {:ok, c} -> Jason.decode(c); e -> e end) do + case File.read(package_json) + |> then(fn + {:ok, c} -> Jason.decode(c) + e -> e + end) do {:ok, %{"bin" => bins}} when is_binary(bins) -> # Single binary with package name [Path.join(install_path, bins)] @@ -760,20 +851,23 @@ defmodule Opsm.Package.Installer do case File.read(cargo_toml) do {:ok, content} -> # Parse bin names from Cargo.toml - bins = Regex.scan(~r/\[\[bin\]\].*?name\s*=\s*"([^"]+)"/s, content) - |> Enum.map(fn [_, name] -> name end) + bins = + Regex.scan(~r/\[\[bin\]\].*?name\s*=\s*"([^"]+)"/s, content) + |> Enum.map(fn [_, name] -> name end) # Also check for default binary (same as package name) - pkg_name = case Regex.run(~r/name\s*=\s*"([^"]+)"/, content) do - [_, name] -> name - _ -> nil - end + pkg_name = + case Regex.run(~r/name\s*=\s*"([^"]+)"/, content) do + [_, name] -> name + _ -> nil + end - all_bins = if pkg_name && File.exists?(Path.join(install_path, "src/main.rs")) do - [pkg_name | bins] - else - bins - end + all_bins = + if pkg_name && File.exists?(Path.join(install_path, "src/main.rs")) do + [pkg_name | bins] + else + bins + end # Return paths (these would need to be built) Enum.map(Enum.uniq(all_bins), fn name -> @@ -781,7 +875,8 @@ defmodule Opsm.Package.Installer do end) |> Enum.filter(&File.exists?/1) - _ -> [] + _ -> + [] end else [] @@ -823,6 +918,7 @@ defmodule Opsm.Package.Installer do defp find_go_bins(install_path) do # Go source packages need `go build` — check for cmd/ directory pattern cmd_dir = Path.join(install_path, "cmd") + if File.dir?(cmd_dir) do case File.ls(cmd_dir) do {:ok, entries} -> @@ -835,7 +931,9 @@ defmodule Opsm.Package.Installer do built = Path.join(install_path, bin_name) if File.exists?(built), do: [built], else: [] end) - _ -> [] + + _ -> + [] end else find_in_bin_dir(install_path) @@ -845,6 +943,7 @@ defmodule Opsm.Package.Installer do defp find_nuget_bins(install_path) do # NuGet packages may have tools/ directory with executables tools_dir = Path.join(install_path, "tools") + if File.dir?(tools_dir) do list_executables(tools_dir) else @@ -861,6 +960,7 @@ defmodule Opsm.Package.Installer do defp find_hackage_bins(install_path) do # Haskell packages need cabal build; check for pre-built executables dist_dir = Path.join(install_path, "dist-newstyle") + if File.dir?(dist_dir) do # Walk dist-newstyle for built executables find_in_bin_dir(install_path) @@ -880,6 +980,7 @@ defmodule Opsm.Package.Installer do defp find_in_bin_dir(install_path) do bin_dir = Path.join(install_path, "bin") + if File.dir?(bin_dir) do list_executables(bin_dir) else @@ -897,11 +998,14 @@ defmodule Opsm.Package.Installer do {:ok, %{type: :regular, mode: mode}} -> # Check if executable (any execute bit set) (mode &&& 0o111) != 0 - _ -> false + + _ -> + false end end) - _ -> [] + _ -> + [] end end @@ -949,12 +1053,15 @@ defmodule Opsm.Package.Installer do # Unlink binaries first linked_bins = Map.get(installed, "linked_bins", []) + if linked_bins != [] do IO.puts(" Unlinking #{length(linked_bins)} executable(s)...") + Enum.each(linked_bins, fn link_path -> case File.rm(link_path) do :ok -> IO.puts(" ✓ #{Path.basename(link_path)}") - {:error, :enoent} -> :ok # Already gone + # Already gone + {:error, :enoent} -> :ok {:error, reason} -> IO.puts(" ⚠ Failed to unlink #{link_path}: #{reason}") end end) @@ -965,6 +1072,7 @@ defmodule Opsm.Package.Installer do {:ok, _} -> # Extended cleanup: desktop shortcuts, services, config, etc. cleanup_actions = Opsm.Package.Cleanup.cleanup(installed["name"]) + if cleanup_actions != [] do IO.puts("Extended cleanup:") IO.puts(Opsm.Package.Cleanup.format_report(cleanup_actions)) @@ -988,7 +1096,10 @@ defmodule Opsm.Package.Installer do :ok {:error, reason, path} -> - {:error, Errors.format({:install, "Failed to remove #{path}: #{reason}", "Check file permissions"})} + {:error, + Errors.format( + {:install, "Failed to remove #{path}: #{reason}", "Check file permissions"} + )} end end @@ -1000,7 +1111,8 @@ defmodule Opsm.Package.Installer do {:error, _} -> [] end - {:error, _} -> [] + {:error, _} -> + [] end end diff --git a/opsm_ex/lib/opsm/package/native.ex b/opsm_ex/lib/opsm/package/native.ex index 609004b4..1ac70b28 100644 --- a/opsm_ex/lib/opsm/package/native.ex +++ b/opsm_ex/lib/opsm/package/native.ex @@ -144,7 +144,10 @@ defmodule Opsm.Package.Native do defp do_native_remove(:hex, package, _global) do # mix deps.unlock + remove from mix.exs (interactive) - IO.puts(" Note: Remove '#{package}' from mix.exs deps, then run 'mix deps.unlock #{package}'") + IO.puts( + " Note: Remove '#{package}' from mix.exs deps, then run 'mix deps.unlock #{package}'" + ) + {:ok, :manual_required} end @@ -166,22 +169,24 @@ defmodule Opsm.Package.Native do defp build_install_command(:npm, package, version, global, dev) do pkg_spec = if version, do: "#{package}@#{version}", else: package - args = cond do - global && dev -> ["install", "-g", "--save-dev", pkg_spec] - global -> ["install", "-g", pkg_spec] - dev -> ["install", "--save-dev", pkg_spec] - true -> ["install", pkg_spec] - end + args = + cond do + global && dev -> ["install", "-g", "--save-dev", pkg_spec] + global -> ["install", "-g", pkg_spec] + dev -> ["install", "--save-dev", pkg_spec] + true -> ["install", pkg_spec] + end {"npm", args} end defp build_install_command(:cargo, package, version, _global, _dev) do - args = if version do - ["install", package, "--version", version] - else - ["install", package] - end + args = + if version do + ["install", package, "--version", version] + else + ["install", package] + end {"cargo", args} end @@ -197,22 +202,24 @@ defmodule Opsm.Package.Native do defp build_install_command(:pypi, package, version, global, _dev) do pkg_spec = if version, do: "#{package}==#{version}", else: package - args = if global do - ["install", pkg_spec] - else - ["install", "--user", pkg_spec] - end + args = + if global do + ["install", pkg_spec] + else + ["install", "--user", pkg_spec] + end {"pip", args} end defp build_install_command(:gem, package, version, global, _dev) do - args = cond do - version && global -> ["install", package, "-v", version] - version -> ["install", package, "-v", version, "--user-install"] - global -> ["install", package] - true -> ["install", package, "--user-install"] - end + args = + cond do + version && global -> ["install", package, "-v", version] + version -> ["install", package, "-v", version, "--user-install"] + global -> ["install", package] + true -> ["install", package, "--user-install"] + end {"gem", args} end @@ -223,11 +230,12 @@ defmodule Opsm.Package.Native do end defp build_install_command(:pub, package, version, _global, dev) do - args = if dev do - ["pub", "add", "--dev", package] - else - ["pub", "add", package] - end + args = + if dev do + ["pub", "add", "--dev", package] + else + ["pub", "add", package] + end args = if version, do: args ++ ["--version", version], else: args {"dart", args} @@ -248,9 +256,10 @@ defmodule Opsm.Package.Native do IO.puts(" $ #{cmd} #{Enum.join(args, " ")}") # Use Port for better control and timeout handling - task = Task.async(fn -> - run_command_sync(cmd, args) - end) + task = + Task.async(fn -> + run_command_sync(cmd, args) + end) case Task.yield(task, timeout) do {:ok, result} -> @@ -270,12 +279,13 @@ defmodule Opsm.Package.Native do {:error, "Command not found: #{cmd}"} exe_path -> - port = Port.open({:spawn_executable, exe_path}, [ - :binary, - :exit_status, - :stderr_to_stdout, - args: args - ]) + port = + Port.open({:spawn_executable, exe_path}, [ + :binary, + :exit_status, + :stderr_to_stdout, + args: args + ]) collect_output(port, []) end diff --git a/opsm_ex/lib/opsm/package/transaction.ex b/opsm_ex/lib/opsm/package/transaction.ex index 9522a2de..89bd8654 100644 --- a/opsm_ex/lib/opsm/package/transaction.ex +++ b/opsm_ex/lib/opsm/package/transaction.ex @@ -25,14 +25,14 @@ defmodule Opsm.Package.Transaction do ] @type t :: %__MODULE__{ - package_name: String.t(), - started_at: DateTime.t(), - directories: [String.t()], - files: [String.t()], - symlinks: [String.t()], - db_entries: [String.t()], - completed: boolean() - } + package_name: String.t(), + started_at: DateTime.t(), + directories: [String.t()], + files: [String.t()], + symlinks: [String.t()], + db_entries: [String.t()], + completed: boolean() + } @doc """ Start a new transaction for package installation. @@ -129,6 +129,7 @@ defmodule Opsm.Package.Transaction do case File.mkdir_p(path) do :ok -> {:ok, record_directory(txn, path)} + {:error, reason} -> {:error, "Failed to create directory #{path}: #{reason}"} end @@ -147,8 +148,10 @@ defmodule Opsm.Package.Transaction do {:ok, ^source} -> # Already points to correct location, just record it {:ok, record_symlink(txn, target)} + {:ok, other} -> {:error, "Target #{target} is a symlink to #{other}, not #{source}"} + {:error, reason} -> {:error, "Cannot read symlink #{target}: #{reason}"} end @@ -162,6 +165,7 @@ defmodule Opsm.Package.Transaction do case File.ln_s(source, target) do :ok -> {:ok, record_symlink(txn, target)} + {:error, reason} -> {:error, "Failed to create symlink #{target}: #{reason}"} end @@ -179,15 +183,18 @@ defmodule Opsm.Package.Transaction do case File.rename(source, dest) do :ok -> {:ok, record_file(txn, dest)} + {:error, :exdev} -> # Cross-device move - need to copy and delete case File.cp(source, dest) do :ok -> File.rm(source) {:ok, record_file(txn, dest)} + {:error, reason} -> {:error, "Failed to copy #{source} to #{dest}: #{reason}"} end + {:error, reason} -> {:error, "Failed to move #{source} to #{dest}: #{reason}"} end @@ -201,9 +208,11 @@ defmodule Opsm.Package.Transaction do :ok -> IO.puts(" ✓ Removed symlink: #{Path.basename(path)}") [] + {:error, :enoent} -> # Already gone [] + {:error, reason} -> IO.puts(" ⚠ Failed to remove symlink #{path}: #{reason}") [{path, reason}] @@ -217,8 +226,10 @@ defmodule Opsm.Package.Transaction do :ok -> IO.puts(" ✓ Removed file: #{Path.basename(path)}") [] + {:error, :enoent} -> [] + {:error, reason} -> IO.puts(" ⚠ Failed to remove file #{path}: #{reason}") [{path, reason}] @@ -233,14 +244,18 @@ defmodule Opsm.Package.Transaction do :ok -> IO.puts(" ✓ Removed directory: #{path}") [] + {:error, :enoent} -> [] + {:error, :enotempty} -> # Directory not empty - that's fine, leave it [] + {:error, :eexist} -> # Same as enotempty on some systems [] + {:error, reason} -> IO.puts(" ⚠ Failed to remove directory #{path}: #{reason}") [{path, reason}] diff --git a/opsm_ex/lib/opsm/progress.ex b/opsm_ex/lib/opsm/progress.ex index 8c6afd91..67a045b7 100644 --- a/opsm_ex/lib/opsm/progress.ex +++ b/opsm_ex/lib/opsm/progress.ex @@ -75,7 +75,9 @@ defmodule Opsm.Progress do speed_str = format_speed(speed) eta_str = format_eta(eta) - output = "#{state.label}: [#{bar}] #{percent_int}% #{size_str}/#{total_str} #{speed_str} #{eta_str}" + output = + "#{state.label}: [#{bar}] #{percent_int}% #{size_str}/#{total_str} #{speed_str} #{eta_str}" + IO.write("\r #{output} ") output end @@ -134,11 +136,13 @@ defmodule Opsm.Progress do def next_step(state, description \\ nil) do new_current = min(state.current + 1, state.total) state = %{state | current: new_current} + if description do IO.puts(" [#{state.current}/#{state.total}] #{description}") else IO.puts(" [#{state.current}/#{state.total}] #{state.label} #{state.current}") end + state end @@ -178,12 +182,15 @@ defmodule Opsm.Progress do Format bytes as human-readable string (B, KB, MB, GB). """ def format_bytes(bytes) when bytes < 1024, do: "#{bytes} B" + def format_bytes(bytes) when bytes < 1024 * 1024 do "#{Float.round(bytes / 1024, 1)} KB" end + def format_bytes(bytes) when bytes < 1024 * 1024 * 1024 do "#{Float.round(bytes / (1024 * 1024), 2)} MB" end + def format_bytes(bytes) do "#{Float.round(bytes / (1024 * 1024 * 1024), 2)} GB" end @@ -194,11 +201,13 @@ defmodule Opsm.Progress do def format_duration(seconds) when seconds < 60 do "#{round(seconds)}s" end + def format_duration(seconds) when seconds < 3600 do mins = div(round(seconds), 60) secs = rem(round(seconds), 60) "#{mins}m #{secs}s" end + def format_duration(seconds) do hours = div(round(seconds), 3600) mins = rem(div(round(seconds), 60), 60) @@ -214,22 +223,27 @@ defmodule Opsm.Progress do defp format_speed(bytes_per_sec) when bytes_per_sec < 1024 do "#{round(bytes_per_sec)}B/s" end + defp format_speed(bytes_per_sec) when bytes_per_sec < 1024 * 1024 do "#{Float.round(bytes_per_sec / 1024, 1)}KB/s" end + defp format_speed(bytes_per_sec) do "#{Float.round(bytes_per_sec / (1024 * 1024), 2)}MB/s" end defp format_eta(seconds) when seconds < 1, do: "" + defp format_eta(seconds) when seconds < 60 do "ETA: #{round(seconds)}s" end + defp format_eta(seconds) when seconds < 3600 do mins = div(round(seconds), 60) secs = rem(round(seconds), 60) "ETA: #{mins}m#{secs}s" end + defp format_eta(seconds) do hours = div(round(seconds), 3600) mins = rem(div(round(seconds), 60), 60) diff --git a/opsm_ex/lib/opsm/registries/affinescript.ex b/opsm_ex/lib/opsm/registries/affinescript.ex index 4fc1c841..8eea6f8c 100644 --- a/opsm_ex/lib/opsm/registries/affinescript.ex +++ b/opsm_ex/lib/opsm/registries/affinescript.ex @@ -22,7 +22,8 @@ defmodule Opsm.Registries.AffineScript do alias Opsm.Verified.Http, as: VerifiedHttp @base_url "https://packages.affinescript.dev/api/v1" - @fallback_mode :git # Switch to :registry once packages.affinescript.dev is live + # Switch to :registry once packages.affinescript.dev is live + @fallback_mode :git # --------------------------------------------------------------------------- # Curated package catalogue @@ -78,7 +79,8 @@ defmodule Opsm.Registries.AffineScript do %{ name: "affine-crypto", url: "https://github.com/hyperpolymath/affine-crypto", - description: "Post-quantum cryptographic primitives (Dilithium5, Kyber-1024) for AffineScript" + description: + "Post-quantum cryptographic primitives (Dilithium5, Kyber-1024) for AffineScript" }, %{ name: "typed-wasm-rt", @@ -187,6 +189,7 @@ defmodule Opsm.Registries.AffineScript do defp registry_exists?(name) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -454,6 +457,7 @@ defmodule Opsm.Registries.AffineScript do # Parse a TOML inline array string ["a", "b", "c"] into a list. defp parse_toml_array(nil), do: nil + defp parse_toml_array(str) when is_binary(str) do str |> String.trim("[") diff --git a/opsm_ex/lib/opsm/registries/agentic.ex b/opsm_ex/lib/opsm/registries/agentic.ex index a75e7981..d4891226 100644 --- a/opsm_ex/lib/opsm/registries/agentic.ex +++ b/opsm_ex/lib/opsm/registries/agentic.ex @@ -50,10 +50,11 @@ defmodule Opsm.Registries.Agentic do """ @spec fetch_package(String.t(), map()) :: - {:ok, ResolvedPackage.t()} | {:error, term()} + {:ok, ResolvedPackage.t()} | {:error, term()} def fetch_package(name, hints \\ %{}) do task_id = generate_task_id() - timeout = Map.get(hints, :timeout, 300_000) # 5 minutes default + # 5 minutes default + timeout = Map.get(hints, :timeout, 300_000) Logger.info("Submitting agentic fetch task for #{name} (task_id: #{task_id})") @@ -143,15 +144,18 @@ defmodule Opsm.Registries.Agentic do language: Map.get(hints, :language, "unknown") }, strategy: "agentic", - hints: %{ - search_terms: Map.get(hints, :search_hints, [name]), - common_domains: Map.get(hints, :common_domains, []), - file_patterns: Map.get(hints, :file_patterns, []), - maintainer: Map.get(hints, :maintainer), - last_known_url: Map.get(hints, :last_known_url), - build_commands: Map.get(hints, :build_commands, []), - ecosystem: Map.get(hints, :ecosystem) - } |> Enum.reject(fn {_, v} -> is_nil(v) end) |> Map.new(), + hints: + %{ + search_terms: Map.get(hints, :search_hints, [name]), + common_domains: Map.get(hints, :common_domains, []), + file_patterns: Map.get(hints, :file_patterns, []), + maintainer: Map.get(hints, :maintainer), + last_known_url: Map.get(hints, :last_known_url), + build_commands: Map.get(hints, :build_commands, []), + ecosystem: Map.get(hints, :ecosystem) + } + |> Enum.reject(fn {_, v} -> is_nil(v) end) + |> Map.new(), callback: %{ url: callback_url(task_id), method: "POST", @@ -214,8 +218,10 @@ defmodule Opsm.Registries.Agentic do resolved_deps: [] } - Logger.info("Agentic fetch succeeded: #{package.package}@#{package.version} " <> - "(method: #{disc["method"]}, confidence: #{disc["confidence"]})") + Logger.info( + "Agentic fetch succeeded: #{package.package}@#{package.version} " <> + "(method: #{disc["method"]}, confidence: #{disc["confidence"]})" + ) {:ok, package} end @@ -227,8 +233,9 @@ defmodule Opsm.Registries.Agentic do Logger.warning("Agentic fetch failed (task: #{task_id}): #{error["message"]}") Logger.debug("Attempts: #{inspect(error["attempts"])}") - message = "#{error["message"]}\n\nSuggestions:\n" <> - Enum.join(result["suggestions"] || [], "\n") + message = + "#{error["message"]}\n\nSuggestions:\n" <> + Enum.join(result["suggestions"] || [], "\n") {:error, message} end diff --git a/opsm_ex/lib/opsm/registries/alire.ex b/opsm_ex/lib/opsm/registries/alire.ex index 30c54813..908478c1 100644 --- a/opsm_ex/lib/opsm/registries/alire.ex +++ b/opsm_ex/lib/opsm/registries/alire.ex @@ -115,9 +115,14 @@ defmodule Opsm.Registries.Alire do v -> {:ok, [v]} end - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -136,9 +141,14 @@ defmodule Opsm.Registries.Alire do ver = if version == "latest", do: body["version"] || "0.0.0", else: version {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/alpine.ex b/opsm_ex/lib/opsm/registries/alpine.ex index c69800f7..5794fcbb 100644 --- a/opsm_ex/lib/opsm/registries/alpine.ex +++ b/opsm_ex/lib/opsm/registries/alpine.ex @@ -17,11 +17,14 @@ defmodule Opsm.Registries.Alpine do """ def fetch_package(name, version \\ "latest") do url = "#{@api_url}/package/edge/main/x86_64/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: body["version"] || body["pkgver"], - else: version + ver = + if version == "latest", + do: body["version"] || body["pkgver"], + else: version + {:ok, parse_alpine_package(name, body, ver)} {:error, :not_found} -> @@ -38,26 +41,35 @@ defmodule Opsm.Registries.Alpine do defp fetch_community(name, version) do url = "#{@api_url}/package/edge/community/x86_64/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: body["version"] || body["pkgver"], - else: version + ver = + if version == "latest", + do: body["version"] || body["pkgver"], + else: version + {:ok, parse_alpine_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp parse_alpine_package(name, body, version) do - deps = (body["depends"] || []) - |> Enum.map(fn d -> - dep_name = String.replace(d, ~r/[><=].*/, "") - {dep_name, "*"} - end) - |> Map.new() + deps = + (body["depends"] || []) + |> Enum.map(fn d -> + dep_name = String.replace(d, ~r/[><=].*/, "") + {dep_name, "*"} + end) + |> Map.new() manifest = %ManifestFormat{ name: name, @@ -76,7 +88,7 @@ defmodule Opsm.Registries.Alpine do manifest: manifest, tarball_url: nil, checksum: nil, - attestations: [], + attestations: [] } end @@ -95,17 +107,23 @@ defmodule Opsm.Registries.Alpine do """ def search(query, _opts \\ []) do url = "#{@api_url}/packages?name=#{URI.encode(query)}*&branch=edge&arch=x86_64" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn p -> - %{name: p["name"], version: p["version"], description: p["description"]} - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn p -> + %{name: p["name"], version: p["version"], description: p["description"]} + end) + {:ok, results} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/ansible_galaxy.ex b/opsm_ex/lib/opsm/registries/ansible_galaxy.ex index 0d854e88..b2aaca67 100644 --- a/opsm_ex/lib/opsm/registries/ansible_galaxy.ex +++ b/opsm_ex/lib/opsm/registries/ansible_galaxy.ex @@ -20,15 +20,18 @@ defmodule Opsm.Registries.AnsibleGalaxy do """ def fetch_package(name, version \\ "latest") do {namespace, collection} = split_collection_name(name) - url = "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/" + + url = + "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - target_version = if version == "latest" do - body["highest_version"] && body["highest_version"]["version"] - else - version - end + target_version = + if version == "latest" do + body["highest_version"] && body["highest_version"]["version"] + else + version + end # Fetch version-specific metadata for dependencies {deps, version_meta} = fetch_version_metadata(namespace, collection, target_version) @@ -54,12 +57,14 @@ defmodule Opsm.Registries.AnsibleGalaxy do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => [role | _]}} -> - ver = if version == "latest" do - get_in(role, ["summary_fields", "versions", Access.at(0), "name"]) || - role["version"] - else - version - end + ver = + if version == "latest" do + get_in(role, ["summary_fields", "versions", Access.at(0), "name"]) || + role["version"] + else + version + end + {:ok, parse_role(name, role, ver)} {:ok, _} -> @@ -71,7 +76,8 @@ defmodule Opsm.Registries.AnsibleGalaxy do end defp fetch_version_metadata(namespace, collection, version) do - url = "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/versions/#{version}/" + url = + "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/versions/#{version}/" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> @@ -90,33 +96,39 @@ defmodule Opsm.Registries.AnsibleGalaxy do limit = Keyword.get(opts, :limit, 20) # Search collections via v3 API - url = "#{@api_url}/plugin/ansible/search/collection-versions/?q=#{URI.encode_www_form(query)}&limit=#{limit}" + url = + "#{@api_url}/plugin/ansible/search/collection-versions/?q=#{URI.encode_www_form(query)}&limit=#{limit}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"data" => results}} when is_list(results) -> - packages = results - |> Enum.take(limit) - |> Enum.map(fn r -> - %{ - name: "#{r["namespace"]}.#{r["name"]}", - version: r["version"], - description: r["description"] || "", - downloads: r["download_count"] || 0 - } - end) + packages = + results + |> Enum.take(limit) + |> Enum.map(fn r -> + %{ + name: "#{r["namespace"]}.#{r["name"]}", + version: r["version"], + description: r["description"] || "", + downloads: r["download_count"] || 0 + } + end) + {:ok, packages} {:ok, %{"results" => results}} when is_list(results) -> - packages = results - |> Enum.take(limit) - |> Enum.map(fn r -> - %{ - name: r["namespace"] && r["name"] && "#{r["namespace"]}.#{r["name"]}" || r["name"], - version: r["version"] || get_in(r, ["latest_version", "version"]), - description: r["description"] || "", - downloads: r["download_count"] || 0 - } - end) + packages = + results + |> Enum.take(limit) + |> Enum.map(fn r -> + %{ + name: + (r["namespace"] && r["name"] && "#{r["namespace"]}.#{r["name"]}") || r["name"], + version: r["version"] || get_in(r, ["latest_version", "version"]), + description: r["description"] || "", + downloads: r["download_count"] || 0 + } + end) + {:ok, packages} {:ok, _} -> @@ -135,13 +147,18 @@ defmodule Opsm.Registries.AnsibleGalaxy do """ def exists?(name) do {namespace, collection} = split_collection_name(name) - url = "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/" + + url = + "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/" case VerifiedHttp.get(url, receive_timeout: 5_000) do - {:ok, _} -> true + {:ok, _} -> + true + _ -> # Fallback: check v2 roles API role_url = "#{@api_v2_url}/roles/?search=#{URI.encode_www_form(name)}" + case VerifiedHttp.get_json(role_url, receive_timeout: 5_000) do {:ok, %{"count" => count}} when count > 0 -> true _ -> false @@ -154,17 +171,23 @@ defmodule Opsm.Registries.AnsibleGalaxy do """ def versions(name) do {namespace, collection} = split_collection_name(name) - url = "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/versions/" + + url = + "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/versions/" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"data" => versions}} when is_list(versions) -> - ver_list = Enum.map(versions, fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + ver_list = + Enum.map(versions, fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_list} {:ok, %{"results" => versions}} when is_list(versions) -> - ver_list = Enum.map(versions, fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + ver_list = + Enum.map(versions, fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_list} {:error, :not_found} -> @@ -183,7 +206,9 @@ defmodule Opsm.Registries.AnsibleGalaxy do """ def tarball_url(name, version) do {namespace, collection} = split_collection_name(name) - url = "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/versions/#{version}/" + + url = + "#{@api_url}/plugin/ansible/content/published/collections/index/#{namespace}/#{collection}/versions/#{version}/" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"download_url" => dl_url}} when is_binary(dl_url) -> diff --git a/opsm_ex/lib/opsm/registries/apt.ex b/opsm_ex/lib/opsm/registries/apt.ex index 81823875..49c29027 100644 --- a/opsm_ex/lib/opsm/registries/apt.ex +++ b/opsm_ex/lib/opsm/registries/apt.ex @@ -19,24 +19,35 @@ defmodule Opsm.Registries.Apt do """ def fetch_package(name, version \\ "latest") do url = "#{@debian_api}/src/#{name}/" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> versions = body["versions"] || [] + case versions do - [] -> {:error, :not_found} + [] -> + {:error, :not_found} + _ -> - ver = if version == "latest" do - latest = List.first(versions) - latest["version"] - else - version - end + ver = + if version == "latest" do + latest = List.first(versions) + latest["version"] + else + version + end + {:ok, parse_debian_package(name, body, ver, versions)} end - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -58,7 +69,7 @@ defmodule Opsm.Registries.Apt do manifest: manifest, tarball_url: nil, checksum: nil, - attestations: [], + attestations: [] } end @@ -67,14 +78,18 @@ defmodule Opsm.Registries.Apt do """ def get_versions(name) do url = "#{@debian_api}/src/#{name}/" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - versions = (body["versions"] || []) - |> Enum.map(fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + versions = + (body["versions"] || []) + |> Enum.map(fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, versions} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -83,24 +98,37 @@ defmodule Opsm.Registries.Apt do """ def search(query, _opts \\ []) do url = "#{@debian_api}/search/#{URI.encode(query)}/" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - results = (body["results"] || %{}) - |> then(fn r when is_map(r) -> r; _ -> %{} end) - |> Map.get("exact", %{}) - |> then(fn r when is_map(r) -> r; _ -> %{} end) - |> Map.get("source", []) - |> then(fn r when is_list(r) -> r; _ -> [] end) - |> Enum.take(20) - |> Enum.map(fn pkg when is_map(pkg) -> - %{name: pkg["name"], version: nil, description: nil} - end) + results = + (body["results"] || %{}) + |> then(fn + r when is_map(r) -> r + _ -> %{} + end) + |> Map.get("exact", %{}) + |> then(fn + r when is_map(r) -> r + _ -> %{} + end) + |> Map.get("source", []) + |> then(fn + r when is_list(r) -> r + _ -> [] + end) + |> Enum.take(20) + |> Enum.map(fn pkg when is_map(pkg) -> + %{name: pkg["name"], version: nil, description: nil} + end) + {:ok, results} {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/astrolabe.ex b/opsm_ex/lib/opsm/registries/astrolabe.ex index e740a475..36cc4032 100644 --- a/opsm_ex/lib/opsm/registries/astrolabe.ex +++ b/opsm_ex/lib/opsm/registries/astrolabe.ex @@ -20,11 +20,12 @@ defmodule Opsm.Registries.Astrolabe do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - target_version = if version == "latest" do - fetch_latest_version(body) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(body) + else + version + end deps = extract_deps(body, target_version) {:ok, parse_package(name, body, target_version, deps)} @@ -61,29 +62,33 @@ defmodule Opsm.Registries.Astrolabe do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"packages" => packages}} when is_list(packages) -> - results = packages - |> Enum.take(limit) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: pkg["latest_version"] || pkg["version"], - description: pkg["description"] || "", - downloads: pkg["downloads"] || 0 - } - end) + results = + packages + |> Enum.take(limit) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: pkg["latest_version"] || pkg["version"], + description: pkg["description"] || "", + downloads: pkg["downloads"] || 0 + } + end) + {:ok, results} {:ok, packages} when is_list(packages) -> - results = packages - |> Enum.take(limit) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: pkg["latest_version"] || pkg["version"], - description: pkg["description"] || "", - downloads: pkg["downloads"] || 0 - } - end) + results = + packages + |> Enum.take(limit) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: pkg["latest_version"] || pkg["version"], + description: pkg["description"] || "", + downloads: pkg["downloads"] || 0 + } + end) + {:ok, results} {:ok, _} -> @@ -117,25 +122,29 @@ defmodule Opsm.Registries.Astrolabe do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions}} when is_list(versions) -> - ver_list = Enum.map(versions, fn v -> - case v do - %{"version" => ver} -> ver - ver when is_binary(ver) -> ver - _ -> nil - end - end) - |> Enum.reject(&is_nil/1) + ver_list = + Enum.map(versions, fn v -> + case v do + %{"version" => ver} -> ver + ver when is_binary(ver) -> ver + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) + {:ok, ver_list} {:ok, versions} when is_list(versions) -> - ver_list = Enum.map(versions, fn v -> - case v do - %{"version" => ver} -> ver - ver when is_binary(ver) -> ver - _ -> nil - end - end) - |> Enum.reject(&is_nil/1) + ver_list = + Enum.map(versions, fn v -> + case v do + %{"version" => ver} -> ver + ver when is_binary(ver) -> ver + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) + {:ok, ver_list} {:error, :not_found} -> @@ -162,11 +171,13 @@ defmodule Opsm.Registries.Astrolabe do case body do %{"dependencies" => deps} when is_map(deps) -> Enum.reduce(deps, %{}, fn {dep_name, constraint}, acc -> - ver = case constraint do - c when is_binary(c) -> c - %{"version" => v} -> v - _ -> ">= 0.0.0" - end + ver = + case constraint do + c when is_binary(c) -> c + %{"version" => v} -> v + _ -> ">= 0.0.0" + end + Map.put(acc, dep_name, ver) end) diff --git a/opsm_ex/lib/opsm/registries/aur.ex b/opsm_ex/lib/opsm/registries/aur.ex index e93fd8e0..e0028992 100644 --- a/opsm_ex/lib/opsm/registries/aur.ex +++ b/opsm_ex/lib/opsm/registries/aur.ex @@ -22,11 +22,13 @@ defmodule Opsm.Registries.Aur do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => [pkg_data | _]}} -> - ver = if version == "latest" do - pkg_data["Version"] || "0.0.0" - else - version - end + ver = + if version == "latest" do + pkg_data["Version"] || "0.0.0" + else + version + end + {:ok, parse_aur_package(name, pkg_data, ver)} {:ok, %{"results" => []}} -> @@ -59,15 +61,17 @@ defmodule Opsm.Registries.Aur do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> - parsed = results - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["Name"], - version: pkg["Version"], - description: pkg["Description"] - } - end) + parsed = + results + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["Name"], + version: pkg["Version"], + description: pkg["Description"] + } + end) + {:ok, parsed} {:ok, _} -> @@ -143,10 +147,11 @@ defmodule Opsm.Registries.Aur do raw_manifest: data } - tarball_url = case data["URLPath"] do - nil -> nil - path -> "https://aur.archlinux.org#{path}" - end + tarball_url = + case data["URLPath"] do + nil -> nil + path -> "https://aur.archlinux.org#{path}" + end %ResolvedPackage{ package: name, diff --git a/opsm_ex/lib/opsm/registries/betlang.ex b/opsm_ex/lib/opsm/registries/betlang.ex index f8ca7dd1..d05d7a24 100644 --- a/opsm_ex/lib/opsm/registries/betlang.ex +++ b/opsm_ex/lib/opsm/registries/betlang.ex @@ -30,7 +30,8 @@ defmodule Opsm.Registries.Betlang do name: "betlang-rt", url: "https://github.com/hyperpolymath/betlang", path: "runtime", - description: "Betlang real-time runtime — deterministic scheduler, WCET guarantees, static allocation" + description: + "Betlang real-time runtime — deterministic scheduler, WCET guarantees, static allocation" }, %{ name: "betlang-hal", @@ -48,13 +49,15 @@ defmodule Opsm.Registries.Betlang do name: "betlang-timing", url: "https://github.com/hyperpolymath/betlang", path: "timing", - description: "Static timing analysis primitives — WCET types, deadline proofs, jitter bounds" + description: + "Static timing analysis primitives — WCET types, deadline proofs, jitter bounds" }, %{ name: "betlang-std", url: "https://github.com/hyperpolymath/betlang", path: "stdlib", - description: "Betlang standard library — deterministic collections, no-alloc I/O, safe concurrency" + description: + "Betlang standard library — deterministic collections, no-alloc I/O, safe concurrency" }, %{ name: "betlang-test", @@ -100,17 +103,21 @@ defmodule Opsm.Registries.Betlang do defp fetch_from_registry(name, version) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> target = if version == "latest", do: body["latest_version"], else: version {:ok, parse_registry_package(body, target)} - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end defp search_registry(query, opts) do limit = Keyword.get(opts, :limit, 20) url = "#{@base_url}/packages?q=#{URI.encode(query)}&limit=#{limit}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, &parse_search_result/1)} {:error, reason} -> {:error, reason} @@ -126,6 +133,7 @@ defmodule Opsm.Registries.Betlang do defp registry_versions(name) do url = "#{@base_url}/packages/#{URI.encode(name)}/versions" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, & &1["version"])} {:error, reason} -> {:error, reason} @@ -141,9 +149,12 @@ defmodule Opsm.Registries.Betlang do defp search_curated(query, _opts) do q = String.downcase(query) - results = Enum.filter(@known_packages, fn p -> - String.contains?(String.downcase("#{p.name} #{p.description}"), q) - end) + + results = + Enum.filter(@known_packages, fn p -> + String.contains?(String.downcase("#{p.name} #{p.description}"), q) + end) + {:ok, Enum.map(results, &curated_to_resolved(&1, "latest"))} end @@ -156,13 +167,18 @@ defmodule Opsm.Registries.Betlang do pkg_info.url |> String.replace("https://github.com/", "https://api.github.com/repos/") |> Kernel.<>("/tags") + case VerifiedHttp.get_json(tags_url, receive_timeout: 10_000) do {:ok, tags} when is_list(tags) -> versions = Enum.map(tags, & &1["name"]) |> Enum.reject(&is_nil/1) {:ok, if(versions == [], do: ["main"], else: versions)} - _ -> {:ok, ["main"]} + + _ -> + {:ok, ["main"]} end - :not_found -> {:error, :not_found} + + :not_found -> + {:error, :not_found} end end @@ -171,6 +187,7 @@ defmodule Opsm.Registries.Betlang do "https://github.com/hyperpolymath/#{name}", "https://github.com/hyperpolymath/betlang-#{name}" ] + Enum.find_value(urls, {:error, :not_found}, fn url -> case fetch_git_manifest(%{name: name, url: url, description: nil}, version) do {:ok, pkg} -> {:ok, pkg} @@ -185,8 +202,11 @@ defmodule Opsm.Registries.Betlang do base = pkg_info.url for_manifest = fn manifest_name -> - url = if path, do: "#{base}/raw/#{branch}/#{path}/#{manifest_name}", - else: "#{base}/raw/#{branch}/#{manifest_name}" + url = + if path, + do: "#{base}/raw/#{branch}/#{path}/#{manifest_name}", + else: "#{base}/raw/#{branch}/#{manifest_name}" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: text}} -> {:ok, text} _ -> :not_found @@ -240,6 +260,7 @@ defmodule Opsm.Registries.Betlang do attestations: [], resolved_deps: [] } + {:ok, pkg} end @@ -300,8 +321,12 @@ defmodule Opsm.Registries.Betlang do end defp parse_search_result(r) do - %{name: r["name"], version: r["latest_version"], - description: r["description"], downloads: r["downloads"] || 0} + %{ + name: r["name"], + version: r["latest_version"], + description: r["description"], + downloads: r["downloads"] || 0 + } end defp find_curated(name) do diff --git a/opsm_ex/lib/opsm/registries/bioconductor.ex b/opsm_ex/lib/opsm/registries/bioconductor.ex index b35d103e..0116edb0 100644 --- a/opsm_ex/lib/opsm/registries/bioconductor.ex +++ b/opsm_ex/lib/opsm/registries/bioconductor.ex @@ -22,11 +22,12 @@ defmodule Opsm.Registries.Bioconductor do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - target_version = if version == "latest" do - body["Version"] || extract_version_from_body(body) - else - version - end + target_version = + if version == "latest" do + body["Version"] || extract_version_from_body(body) + else + version + end deps = parse_dependencies(body) {:ok, parse_package(name, body, target_version, deps)} @@ -70,29 +71,33 @@ defmodule Opsm.Registries.Bioconductor do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, results} when is_list(results) -> - packages = results - |> Enum.take(limit) - |> Enum.map(fn pkg -> - %{ - name: pkg["Package"] || pkg["name"], - version: pkg["Version"] || pkg["version"], - description: pkg["Title"] || pkg["description"] || "", - downloads: pkg["downloads"] || 0 - } - end) + packages = + results + |> Enum.take(limit) + |> Enum.map(fn pkg -> + %{ + name: pkg["Package"] || pkg["name"], + version: pkg["Version"] || pkg["version"], + description: pkg["Title"] || pkg["description"] || "", + downloads: pkg["downloads"] || 0 + } + end) + {:ok, packages} {:ok, %{"packages" => packages}} when is_list(packages) -> - results = packages - |> Enum.take(limit) - |> Enum.map(fn pkg -> - %{ - name: pkg["Package"] || pkg["name"], - version: pkg["Version"] || pkg["version"], - description: pkg["Title"] || pkg["description"] || "", - downloads: 0 - } - end) + results = + packages + |> Enum.take(limit) + |> Enum.map(fn pkg -> + %{ + name: pkg["Package"] || pkg["name"], + version: pkg["Version"] || pkg["version"], + description: pkg["Title"] || pkg["description"] || "", + downloads: 0 + } + end) + {:ok, results} {:ok, _} -> @@ -113,10 +118,13 @@ defmodule Opsm.Registries.Bioconductor do url = "#{@packages_url}/#{URI.encode(name)}" case VerifiedHttp.get(url, receive_timeout: 5_000) do - {:ok, _} -> true + {:ok, _} -> + true + _ -> # Fallback: check release HTML page views_url = "#{@release_url}/html/#{URI.encode(name)}.html" + case VerifiedHttp.get(views_url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -179,6 +187,7 @@ defmodule Opsm.Registries.Bioconductor do end end) end + defp parse_dep_field(_), do: %{} defp parse_single_dep(dep_str) do @@ -228,6 +237,7 @@ defmodule Opsm.Registries.Bioconductor do end defp parse_authors(nil), do: [] + defp parse_authors(author) when is_binary(author) do author |> String.split(",") @@ -236,6 +246,7 @@ defmodule Opsm.Registries.Bioconductor do end) |> Enum.reject(&(&1 == "")) end + defp parse_authors(_), do: [] # biocViews is a comma-separated list of classification terms @@ -245,5 +256,6 @@ defmodule Opsm.Registries.Bioconductor do |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) end + defp parse_bioc_views(_), do: [] end diff --git a/opsm_ex/lib/opsm/registries/bower.ex b/opsm_ex/lib/opsm/registries/bower.ex index db1866ac..30bff3f5 100644 --- a/opsm_ex/lib/opsm/registries/bower.ex +++ b/opsm_ex/lib/opsm/registries/bower.ex @@ -36,9 +36,14 @@ defmodule Opsm.Registries.Bower do {:ok, parse_bower_basic(name, body, version)} end - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -63,7 +68,8 @@ defmodule Opsm.Registries.Bower do ver = if resolved_ver == "latest", do: "0.0.0", else: resolved_ver {:ok, body, ver} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -147,19 +153,24 @@ defmodule Opsm.Registries.Bower do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, results} when is_list(results) -> - parsed = results - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: nil, - description: pkg["url"] - } - end) + parsed = + results + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: nil, + description: pkg["url"] + } + end) + {:ok, parsed} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/buildpacks.ex b/opsm_ex/lib/opsm/registries/buildpacks.ex index 6e91029a..e4530547 100644 --- a/opsm_ex/lib/opsm/registries/buildpacks.ex +++ b/opsm_ex/lib/opsm/registries/buildpacks.ex @@ -62,27 +62,31 @@ defmodule Opsm.Registries.Buildpacks do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"buildpacks" => buildpacks}} when is_list(buildpacks) -> - results = buildpacks - |> Enum.take(20) - |> Enum.map(fn bp -> - %{ - name: bp["id"] || bp["name"], - version: get_in(bp, ["latest", "version"]), - description: bp["description"] - } - end) + results = + buildpacks + |> Enum.take(20) + |> Enum.map(fn bp -> + %{ + name: bp["id"] || bp["name"], + version: get_in(bp, ["latest", "version"]), + description: bp["description"] + } + end) + {:ok, results} {:ok, buildpacks} when is_list(buildpacks) -> - results = buildpacks - |> Enum.take(20) - |> Enum.map(fn bp -> - %{ - name: bp["id"] || bp["name"], - version: get_in(bp, ["latest", "version"]), - description: bp["description"] - } - end) + results = + buildpacks + |> Enum.take(20) + |> Enum.map(fn bp -> + %{ + name: bp["id"] || bp["name"], + version: get_in(bp, ["latest", "version"]), + description: bp["description"] + } + end) + {:ok, results} {:ok, _} -> @@ -114,9 +118,11 @@ defmodule Opsm.Registries.Buildpacks do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions_list}} when is_list(versions_list) -> - version_strs = versions_list - |> Enum.map(fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + version_strs = + versions_list + |> Enum.map(fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, version_strs} {:ok, _} -> @@ -180,6 +186,7 @@ defmodule Opsm.Registries.Buildpacks do end defp extract_stacks(nil), do: [] + defp extract_stacks(data) do (data["stacks"] || []) |> Enum.map(fn @@ -202,6 +209,7 @@ defmodule Opsm.Registries.Buildpacks do end defp extract_order_deps(nil), do: %{} + defp extract_order_deps(data) do (data["order"] || []) |> List.flatten() diff --git a/opsm_ex/lib/opsm/registries/cargo_binstall.ex b/opsm_ex/lib/opsm/registries/cargo_binstall.ex index f02ae8e8..9bbc8994 100644 --- a/opsm_ex/lib/opsm/registries/cargo_binstall.ex +++ b/opsm_ex/lib/opsm/registries/cargo_binstall.ex @@ -30,11 +30,12 @@ defmodule Opsm.Registries.CargoBinstall do crate = body["crate"] || %{} versions_list = body["versions"] || [] - ver = if version == "latest" do - crate["newest_version"] || crate["max_version"] || "0.0.0" - else - version - end + ver = + if version == "latest" do + crate["newest_version"] || crate["max_version"] || "0.0.0" + else + version + end version_info = Enum.find(versions_list, fn v -> v["num"] == ver end) @@ -43,9 +44,14 @@ defmodule Opsm.Registries.CargoBinstall do {:ok, parse_binstall(name, crate, version_info, ver, binary_info)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -57,9 +63,12 @@ defmodule Opsm.Registries.CargoBinstall do case VerifiedHttp.get_json(url, receive_timeout: 5_000) do {:ok, body} -> targets = body["targets"] || [] - matching = Enum.find(targets, fn t -> - t["target"] == target || String.contains?(t["target"] || "", target) - end) + + matching = + Enum.find(targets, fn t -> + t["target"] == target || String.contains?(t["target"] || "", target) + end) + if matching do %{ binary_url: matching["url"] || build_quickinstall_url(name, version, target), @@ -91,17 +100,19 @@ defmodule Opsm.Registries.CargoBinstall do end defp parse_binstall(name, crate, version_info, version, binary_info) do - checksum = if binary_info[:prebuilt] do - binary_info[:binary_hash] - else - if version_info, do: version_info["checksum"], else: nil - end - - tarball = if binary_info[:prebuilt] do - binary_info[:binary_url] - else - "#{@crates_dl}/#{name}/#{name}-#{version}.crate" - end + checksum = + if binary_info[:prebuilt] do + binary_info[:binary_hash] + else + if version_info, do: version_info["checksum"], else: nil + end + + tarball = + if binary_info[:prebuilt] do + binary_info[:binary_url] + else + "#{@crates_dl}/#{name}/#{name}-#{version}.crate" + end # Fetch dependencies from the version-specific endpoint deps = fetch_deps(name, version) @@ -147,7 +158,8 @@ defmodule Opsm.Registries.CargoBinstall do |> Enum.map(fn d -> {d["crate_id"], d["req"]} end) |> Map.new() - _ -> %{} + _ -> + %{} end end @@ -160,17 +172,20 @@ defmodule Opsm.Registries.CargoBinstall do case VerifiedHttp.get_json(url, headers: @headers, receive_timeout: 10_000) do {:ok, body} -> - results = (body["crates"] || []) - |> Enum.map(fn crate -> - %{ - name: crate["id"] || crate["name"], - version: crate["newest_version"] || crate["max_version"], - description: crate["description"] - } - end) + results = + (body["crates"] || []) + |> Enum.map(fn crate -> + %{ + name: crate["id"] || crate["name"], + version: crate["newest_version"] || crate["max_version"], + description: crate["description"] + } + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end @@ -194,14 +209,21 @@ defmodule Opsm.Registries.CargoBinstall do case VerifiedHttp.get_json(url, headers: @headers, receive_timeout: 10_000) do {:ok, body} -> - vers = (body["versions"] || []) - |> Enum.map(& &1["num"]) - |> Enum.reject(&is_nil/1) + vers = + (body["versions"] || []) + |> Enum.map(& &1["num"]) + |> Enum.reject(&is_nil/1) + {:ok, vers} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end end diff --git a/opsm_ex/lib/opsm/registries/cdnjs.ex b/opsm_ex/lib/opsm/registries/cdnjs.ex index 51b9d930..5c6b7185 100644 --- a/opsm_ex/lib/opsm/registries/cdnjs.ex +++ b/opsm_ex/lib/opsm/registries/cdnjs.ex @@ -18,18 +18,21 @@ defmodule Opsm.Registries.Cdnjs do The `name` is the library name (e.g., "jquery", "lodash"). """ def fetch_package(name, version \\ "latest") do - url = "#{@api_url}/#{URI.encode(name)}?fields=name,version,description,homepage,repository,license,author,keywords,versions,filename,sri" + url = + "#{@api_url}/#{URI.encode(name)}?fields=name,version,description,homepage,repository,license,author,keywords,versions,filename,sri" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"error" => true}} -> {:error, :not_found} {:ok, %{"name" => _} = body} -> - resolved_version = if version == "latest" do - body["version"] - else - version - end + resolved_version = + if version == "latest" do + body["version"] + else + version + end + {:ok, parse_library(body, resolved_version)} {:error, :not_found} -> @@ -52,7 +55,8 @@ defmodule Opsm.Registries.Cdnjs do def search(query, opts \\ []) do limit = Keyword.get(opts, :limit, 20) - url = "#{@api_url}?search=#{URI.encode_www_form(query)}&fields=name,version,description&limit=#{limit}" + url = + "#{@api_url}?search=#{URI.encode_www_form(query)}&fields=name,version,description&limit=#{limit}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> diff --git a/opsm_ex/lib/opsm/registries/chef_supermarket.ex b/opsm_ex/lib/opsm/registries/chef_supermarket.ex index 2f7f7c53..319d3755 100644 --- a/opsm_ex/lib/opsm/registries/chef_supermarket.ex +++ b/opsm_ex/lib/opsm/registries/chef_supermarket.ex @@ -17,11 +17,12 @@ defmodule Opsm.Registries.ChefSupermarket do Returns the specified version or the latest if version is "latest". """ def fetch_package(name, version \\ "latest") do - url = if version == "latest" do - "#{@api_url}/cookbooks/#{URI.encode(name)}" - else - "#{@api_url}/cookbooks/#{URI.encode(name)}/versions/#{URI.encode(version)}" - end + url = + if version == "latest" do + "#{@api_url}/cookbooks/#{URI.encode(name)}" + else + "#{@api_url}/cookbooks/#{URI.encode(name)}/versions/#{URI.encode(version)}" + end case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> @@ -63,14 +64,17 @@ defmodule Opsm.Registries.ChefSupermarket do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"items" => items_list}} when is_list(items_list) -> - results = Enum.map(items_list, fn item -> - cookbook = item["cookbook"] || %{} - %{ - name: cookbook["name"] || item["cookbook_name"], - version: cookbook["latest_version"] || item["cookbook_maintained_version"], - description: cookbook["description"] || item["cookbook_description"] - } - end) + results = + Enum.map(items_list, fn item -> + cookbook = item["cookbook"] || %{} + + %{ + name: cookbook["name"] || item["cookbook_name"], + version: cookbook["latest_version"] || item["cookbook_maintained_version"], + description: cookbook["description"] || item["cookbook_description"] + } + end) + {:ok, results} {:ok, _} -> @@ -104,13 +108,15 @@ defmodule Opsm.Registries.ChefSupermarket do {:ok, %{"versions" => version_urls}} when is_list(version_urls) -> # Chef Supermarket returns version URLs like # "https://supermarket.chef.io/api/v1/cookbooks/NAME/versions/1.0.0" - version_list = version_urls - |> Enum.map(fn url_str -> - url_str - |> String.split("/versions/") - |> List.last() - |> URI.decode() - end) + version_list = + version_urls + |> Enum.map(fn url_str -> + url_str + |> String.split("/versions/") + |> List.last() + |> URI.decode() + end) + {:ok, version_list} {:ok, _} -> diff --git a/opsm_ex/lib/opsm/registries/chicken.ex b/opsm_ex/lib/opsm/registries/chicken.ex index 9657962f..70714a06 100644 --- a/opsm_ex/lib/opsm/registries/chicken.ex +++ b/opsm_ex/lib/opsm/registries/chicken.ex @@ -129,9 +129,14 @@ defmodule Opsm.Registries.Chicken do v -> {:ok, [v]} end - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -147,9 +152,14 @@ defmodule Opsm.Registries.Chicken do ver = if version == "latest", do: body["version"] || "0.0.0", else: version {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/clojars.ex b/opsm_ex/lib/opsm/registries/clojars.ex index e0f3db4b..88b9be31 100644 --- a/opsm_ex/lib/opsm/registries/clojars.ex +++ b/opsm_ex/lib/opsm/registries/clojars.ex @@ -21,11 +21,12 @@ defmodule Opsm.Registries.Clojars do # Clojars artifacts can be grouped (e.g., "org.clojure/clojure") or simple (e.g., "compojure") {group_id, artifact_id} = parse_artifact_name(name) - target_version = if version == "latest" do - fetch_latest_version(group_id, artifact_id) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(group_id, artifact_id) + else + version + end case target_version do nil -> @@ -34,6 +35,7 @@ defmodule Opsm.Registries.Clojars do ver -> # Fetch artifact metadata url = build_artifact_url(group_id, artifact_id) + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> {:ok, parse_artifact(name, body, ver)} @@ -55,6 +57,7 @@ defmodule Opsm.Registries.Clojars do defp fetch_latest_version(group_id, artifact_id) do url = build_artifact_url(group_id, artifact_id) + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"latest_version" => version}} -> version {:ok, %{"versions" => [latest | _]}} -> latest @@ -65,7 +68,8 @@ defmodule Opsm.Registries.Clojars do defp parse_artifact_name(name) do case String.split(name, "/", parts: 2) do [group, artifact] -> {group, artifact} - [artifact] -> {artifact, artifact} # Simple artifact name uses itself as group + # Simple artifact name uses itself as group + [artifact] -> {artifact, artifact} end end @@ -87,15 +91,19 @@ defmodule Opsm.Registries.Clojars do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> - packages = results - |> Enum.take(limit) - |> Enum.map(&parse_search_result/1) + packages = + results + |> Enum.take(limit) + |> Enum.map(&parse_search_result/1) + {:ok, packages} {:ok, results} when is_list(results) -> - packages = results - |> Enum.take(limit) - |> Enum.map(&parse_search_result/1) + packages = + results + |> Enum.take(limit) + |> Enum.map(&parse_search_result/1) + {:ok, packages} {:error, :not_found} -> @@ -121,6 +129,7 @@ defmodule Opsm.Registries.Clojars do defp build_full_name(%{"group_name" => group, "jar_name" => artifact}) when group != artifact do "#{group}/#{artifact}" end + defp build_full_name(%{"jar_name" => artifact}), do: artifact defp build_full_name(%{"artifact_id" => artifact}), do: artifact defp build_full_name(_), do: "unknown" @@ -152,13 +161,15 @@ defmodule Opsm.Registries.Clojars do {:ok, %{"recent_versions" => versions}} when is_list(versions) -> # Some endpoints might only return recent versions - versions_list = versions - |> Enum.map(fn - %{"version" => v} -> v - v when is_binary(v) -> v - _ -> nil - end) - |> Enum.reject(&is_nil/1) + versions_list = + versions + |> Enum.map(fn + %{"version" => v} -> v + v when is_binary(v) -> v + _ -> nil + end) + |> Enum.reject(&is_nil/1) + {:ok, Enum.reverse(versions_list)} {:error, :not_found} -> @@ -199,18 +210,20 @@ defmodule Opsm.Registries.Clojars do {:ok, jar_url} = tarball_url(name, version) # Build web URL for the artifact - web_url = if group_id == artifact_id do - "#{@web_url}/#{artifact_id}" - else - "#{@web_url}/#{group_id}/#{artifact_id}" - end + web_url = + if group_id == artifact_id do + "#{@web_url}/#{artifact_id}" + else + "#{@web_url}/#{group_id}/#{artifact_id}" + end # Repository URL (often hosted on GitHub) - repo_url = case metadata do - %{"scm" => %{"url" => url}} -> url - %{"homepage" => url} when is_binary(url) -> url - _ -> nil - end + repo_url = + case metadata do + %{"scm" => %{"url" => url}} -> url + %{"homepage" => url} when is_binary(url) -> url + _ -> nil + end %ResolvedPackage{ package: name, @@ -218,7 +231,8 @@ defmodule Opsm.Registries.Clojars do forth: :clojars, registry_url: web_url, tarball_url: jar_url, - checksum: nil, # Clojars API doesn't provide checksums directly + # Clojars API doesn't provide checksums directly + checksum: nil, checksum_algo: nil, manifest: %ManifestFormat{ name: name, @@ -263,10 +277,12 @@ defmodule Opsm.Registries.Clojars do # or [{"group/artifact", "version"}] deps end + defp normalize_deps(deps) when is_list(deps) do # Convert list of tuples to map Enum.into(deps, %{}) end + defp normalize_deps(_), do: %{} defp extract_license(metadata) do diff --git a/opsm_ex/lib/opsm/registries/cocoapods.ex b/opsm_ex/lib/opsm/registries/cocoapods.ex index 0a0368f1..72d41cde 100644 --- a/opsm_ex/lib/opsm/registries/cocoapods.ex +++ b/opsm_ex/lib/opsm/registries/cocoapods.ex @@ -16,11 +16,12 @@ defmodule Opsm.Registries.CocoaPods do Fetch pod metadata from the CocoaPods Trunk API. """ def fetch_package(name, version \\ "latest") do - target_version = if version == "latest" do - fetch_latest_version(name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(name) + else + version + end case target_version do nil -> @@ -29,6 +30,7 @@ defmodule Opsm.Registries.CocoaPods do ver -> # Fetch pod info url = "#{@api_url}/pods/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> deps = fetch_podspec_deps(name, ver) @@ -51,6 +53,7 @@ defmodule Opsm.Registries.CocoaPods do defp fetch_latest_version(name) do url = "#{@api_url}/pods/#{name}/specs" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, specs} when is_list(specs) -> # Specs are typically returned in reverse chronological order @@ -75,15 +78,19 @@ defmodule Opsm.Registries.CocoaPods do defp fetch_podspec_deps(name, version) do # Try to get the specific version's podspec url = "#{@api_url}/pods/#{name}/specs/#{version}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, spec} when is_map(spec) -> parse_podspec_dependencies(spec) + _ -> # Fallback: try to get from main pod endpoint case VerifiedHttp.get_json("#{@api_url}/pods/#{name}", receive_timeout: 10_000) do {:ok, pod_info} when is_map(pod_info) -> parse_podspec_dependencies(pod_info) - _ -> %{} + + _ -> + %{} end end end @@ -96,16 +103,19 @@ defmodule Opsm.Registries.CocoaPods do case spec do %{"dependencies" => deps} when is_map(deps) -> Enum.reduce(deps, %{}, fn {name, constraint}, acc -> - version_str = case constraint do - [ver | _] when is_binary(ver) -> ver - ver when is_binary(ver) -> ver - [] -> ">= 0.0.0" - _ -> ">= 0.0.0" - end + version_str = + case constraint do + [ver | _] when is_binary(ver) -> ver + ver when is_binary(ver) -> ver + [] -> ">= 0.0.0" + _ -> ">= 0.0.0" + end + Map.put(acc, name, version_str) end) - _ -> %{} + _ -> + %{} end end @@ -117,31 +127,36 @@ defmodule Opsm.Registries.CocoaPods do limit = Keyword.get(opts, :limit, 20) url = "#{@api_url}/pods/search?query=#{URI.encode_www_form(query)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, results} when is_list(results) -> - pods = results - |> Enum.take(limit) - |> Enum.map(fn pod -> - %{ - name: pod["name"] || pod["pod"], - version: pod["version"] || pod["latest_version"], - description: pod["summary"] || pod["description"], - downloads: pod["stats"]["download_total"] || 0 - } - end) + pods = + results + |> Enum.take(limit) + |> Enum.map(fn pod -> + %{ + name: pod["name"] || pod["pod"], + version: pod["version"] || pod["latest_version"], + description: pod["summary"] || pod["description"], + downloads: pod["stats"]["download_total"] || 0 + } + end) + {:ok, pods} {:ok, %{"pods" => pods}} when is_list(pods) -> - results = pods - |> Enum.take(limit) - |> Enum.map(fn pod -> - %{ - name: pod["name"] || pod["pod"], - version: pod["version"] || pod["latest_version"], - description: pod["summary"] || pod["description"], - downloads: pod["stats"]["download_total"] || 0 - } - end) + results = + pods + |> Enum.take(limit) + |> Enum.map(fn pod -> + %{ + name: pod["name"] || pod["pod"], + version: pod["version"] || pod["latest_version"], + description: pod["summary"] || pod["description"], + downloads: pod["stats"]["download_total"] || 0 + } + end) + {:ok, results} _ -> @@ -173,12 +188,13 @@ defmodule Opsm.Registries.CocoaPods do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, specs} when is_list(specs) -> - versions = specs - |> Enum.map(fn spec -> - spec["version"] || spec["name"] - end) - |> Enum.filter(&(&1 != nil)) - |> Enum.reverse() + versions = + specs + |> Enum.map(fn spec -> + spec["version"] || spec["name"] + end) + |> Enum.filter(&(&1 != nil)) + |> Enum.reverse() if versions == [] do # Fallback: try to get from main pod endpoint @@ -228,6 +244,7 @@ defmodule Opsm.Registries.CocoaPods do """ def tarball_url(name, version) do url = "#{@api_url}/pods/#{name}/specs/#{version}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"source" => %{"http" => http_url}}} -> {:ok, http_url} @@ -240,7 +257,8 @@ defmodule Opsm.Registries.CocoaPods do _ -> # Generic fallback (may not work for all pods) - {:ok, "https://github.com/CocoaPods/Specs/raw/master/Specs/#{name}/#{version}/#{name}.podspec.json"} + {:ok, + "https://github.com/CocoaPods/Specs/raw/master/Specs/#{name}/#{version}/#{name}.podspec.json"} end end @@ -248,20 +266,23 @@ defmodule Opsm.Registries.CocoaPods do defp parse_pod(name, info, version, deps) do homepage = info["homepage"] || info["url"] - repo = case info["source"] do - %{"git" => git_url} -> git_url - _ -> nil - end + + repo = + case info["source"] do + %{"git" => git_url} -> git_url + _ -> nil + end %ResolvedPackage{ package: name, version: version, forth: :cocoapods, registry_url: "https://cocoapods.org/pods/#{name}", - tarball_url: case tarball_url(name, version) do - {:ok, url} -> url - _ -> nil - end, + tarball_url: + case tarball_url(name, version) do + {:ok, url} -> url + _ -> nil + end, checksum: nil, checksum_algo: nil, manifest: %ManifestFormat{ @@ -290,9 +311,11 @@ defmodule Opsm.Registries.CocoaPods do defp extract_authors(nil), do: [] defp extract_authors(authors) when is_binary(authors), do: [authors] + defp extract_authors(authors) when is_map(authors) do Enum.map(authors, fn {name, email} -> "#{name} <#{email}>" end) end + defp extract_authors(authors) when is_list(authors), do: authors defp extract_authors(_), do: [] end diff --git a/opsm_ex/lib/opsm/registries/conan.ex b/opsm_ex/lib/opsm/registries/conan.ex index 5103091d..958b1b2d 100644 --- a/opsm_ex/lib/opsm/registries/conan.ex +++ b/opsm_ex/lib/opsm/registries/conan.ex @@ -14,56 +14,65 @@ defmodule Opsm.Registries.Conan do @web_url "https://conan.io/center/recipes" def fetch_package(name, version \\ "latest") do - target_version = if version == "latest" do - case versions(name) do - {:ok, [latest | _]} -> latest - _ -> nil + target_version = + if version == "latest" do + case versions(name) do + {:ok, [latest | _]} -> latest + _ -> nil + end + else + version end - else - version - end case target_version do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + ver -> - {:ok, %ResolvedPackage{ - package: name, - version: ver, - forth: :conan, - registry_url: "#{@web_url}/#{name}", - tarball_url: nil, - checksum: nil, - checksum_algo: nil, - manifest: %ManifestFormat{ - name: name, - version: ver, - description: nil, - license: nil, - homepage: "#{@web_url}/#{name}", - repository: nil, - authors: [], - keywords: [], - dependencies: %{}, - dev_dependencies: %{}, - source_forth: :conan, - raw_manifest: %{} - }, - attestations: [], - resolved_deps: [] - }} + {:ok, + %ResolvedPackage{ + package: name, + version: ver, + forth: :conan, + registry_url: "#{@web_url}/#{name}", + tarball_url: nil, + checksum: nil, + checksum_algo: nil, + manifest: %ManifestFormat{ + name: name, + version: ver, + description: nil, + license: nil, + homepage: "#{@web_url}/#{name}", + repository: nil, + authors: [], + keywords: [], + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :conan, + raw_manifest: %{} + }, + attestations: [], + resolved_deps: [] + }} end end def search(query, _opts \\ []) do url = "https://center2.conan.io/v2/conans/search?q=#{URI.encode(query)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> - packages = Enum.map(results, fn ref -> - name = ref |> String.split("/") |> List.first() - %{name: name, version: nil, description: "Conan C/C++ package", downloads: 0} - end) + packages = + Enum.map(results, fn ref -> + name = ref |> String.split("/") |> List.first() + %{name: name, version: nil, description: "Conan C/C++ package", downloads: 0} + end) + {:ok, packages} - _ -> {:ok, []} + + _ -> + {:ok, []} end end @@ -76,17 +85,22 @@ defmodule Opsm.Registries.Conan do def versions(name) do url = "#{@api_url}/#{URI.encode(name)}/search" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> - versions = results + versions = + results |> Enum.map(fn ref -> ref |> String.split("/") |> Enum.at(1) end) |> Enum.reject(&is_nil/1) |> Enum.uniq() |> Enum.reverse() + {:ok, versions} - _ -> {:ok, []} + + _ -> + {:ok, []} end end diff --git a/opsm_ex/lib/opsm/registries/conda.ex b/opsm_ex/lib/opsm/registries/conda.ex index b1b32aaa..b274df46 100644 --- a/opsm_ex/lib/opsm/registries/conda.ex +++ b/opsm_ex/lib/opsm/registries/conda.ex @@ -20,11 +20,12 @@ defmodule Opsm.Registries.Conda do def fetch_package(name, version \\ "latest") do {owner, pkg_name} = parse_package_name(name) - target_version = if version == "latest" do - fetch_latest_version(owner, pkg_name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(owner, pkg_name) + else + version + end case target_version do nil -> @@ -33,6 +34,7 @@ defmodule Opsm.Registries.Conda do ver -> # Fetch package metadata url = "#{@api_url}/package/#{owner}/#{pkg_name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> # Fetch file/version details @@ -63,10 +65,14 @@ defmodule Opsm.Registries.Conda do defp fetch_latest_version(owner, pkg_name) do url = "#{@api_url}/package/#{owner}/#{pkg_name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do - {:ok, %{"latest_version" => version}} -> version + {:ok, %{"latest_version" => version}} -> + version + {:ok, %{"versions" => versions}} when is_list(versions) and versions != [] -> List.last(versions) + _ -> # Fallback: get version list from files case versions_internal(owner, pkg_name) do @@ -78,6 +84,7 @@ defmodule Opsm.Registries.Conda do defp fetch_files(owner, pkg_name) do url = "#{@api_url}/package/#{owner}/#{pkg_name}/files" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, files} when is_list(files) -> files {:ok, %{"files" => files}} when is_list(files) -> files @@ -92,6 +99,7 @@ defmodule Opsm.Registries.Conda do limit = Keyword.get(opts, :limit, 20) url = "#{@api_url}/search?name=#{URI.encode(query)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, results} when is_list(results) -> {:ok, parse_search_results(results, limit)} @@ -147,6 +155,7 @@ defmodule Opsm.Registries.Conda do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, files} when is_list(files) -> versions = extract_versions_from_files(files) + if versions == [] do {:error, :not_found} else @@ -155,6 +164,7 @@ defmodule Opsm.Registries.Conda do {:ok, %{"files" => files}} when is_list(files) -> versions = extract_versions_from_files(files) + if versions == [] do {:error, :not_found} else @@ -194,6 +204,7 @@ defmodule Opsm.Registries.Conda do nil -> {:error, :not_found} url -> {:ok, url} end + _ -> {:error, :not_found} end @@ -253,8 +264,12 @@ defmodule Opsm.Registries.Conda do # Try to extract dependencies from package info # Conda dependencies can be in various places cond do - is_map(info["depends"]) -> info["depends"] - is_list(info["depends"]) -> parse_conda_depends_list(info["depends"]) + is_map(info["depends"]) -> + info["depends"] + + is_list(info["depends"]) -> + parse_conda_depends_list(info["depends"]) + true -> # Try to find dependencies in files metadata case Enum.find(files, fn f -> f["version"] == version end) do @@ -273,6 +288,7 @@ defmodule Opsm.Registries.Conda do end end) end + defp parse_conda_depends_list(_), do: %{} # Parse Conda dependency strings like "python >=3.8" or "numpy" @@ -282,6 +298,7 @@ defmodule Opsm.Registries.Conda do [pkg] -> {String.trim(pkg), "*"} end end + defp parse_conda_dependency(_), do: nil defp extract_checksum(files, version) do diff --git a/opsm_ex/lib/opsm/registries/cpan.ex b/opsm_ex/lib/opsm/registries/cpan.ex index 608a4ef8..e926e40f 100644 --- a/opsm_ex/lib/opsm/registries/cpan.ex +++ b/opsm_ex/lib/opsm/registries/cpan.ex @@ -16,11 +16,12 @@ defmodule Opsm.Registries.Cpan do Fetch distribution metadata from MetaCPAN. """ def fetch_package(name, version \\ "latest") do - target_version = if version == "latest" do - fetch_latest_version(name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(name) + else + version + end case target_version do nil -> @@ -29,6 +30,7 @@ defmodule Opsm.Registries.Cpan do ver -> # Fetch release info url = "#{@api_url}/release/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> # Verify version matches if specific version requested @@ -57,7 +59,9 @@ defmodule Opsm.Registries.Cpan do defp fetch_specific_version(name, version) do # Try searching for exact version - search_url = "#{@api_url}/release/_search?q=distribution:#{name}+AND+version:#{version}&size=1" + search_url = + "#{@api_url}/release/_search?q=distribution:#{name}+AND+version:#{version}&size=1" + case VerifiedHttp.get_json(search_url, receive_timeout: 10_000) do {:ok, %{"hits" => %{"hits" => [%{"_source" => body} | _]}}} -> deps = extract_dependencies(body) @@ -70,8 +74,11 @@ defmodule Opsm.Registries.Cpan do defp fetch_latest_version(name) do url = "#{@api_url}/release/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do - {:ok, %{"version" => version}} -> version + {:ok, %{"version" => version}} -> + version + _ -> # Fallback: get version list case versions_internal(name) do @@ -89,11 +96,12 @@ defmodule Opsm.Registries.Cpan do |> Enum.filter(fn dep -> # Only runtime dependencies, not test/build/configure Map.get(dep, "phase") == "runtime" and - Map.get(dep, "relationship") == "requires" + Map.get(dep, "relationship") == "requires" end) |> Enum.reduce(%{}, fn dep, acc -> module = Map.get(dep, "module") version = Map.get(dep, "version", "0") + if module do Map.put(acc, module, ">= #{version}") else @@ -112,14 +120,16 @@ defmodule Opsm.Registries.Cpan do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"hits" => %{"hits" => hits}}} -> - results = Enum.map(hits, fn %{"_source" => release} -> - %{ - name: Map.get(release, "distribution"), - version: Map.get(release, "version"), - description: Map.get(release, "abstract", ""), - downloads: 0 - } - end) + results = + Enum.map(hits, fn %{"_source" => release} -> + %{ + name: Map.get(release, "distribution"), + version: Map.get(release, "version"), + description: Map.get(release, "abstract", ""), + downloads: 0 + } + end) + {:ok, results} {:error, reason} -> @@ -192,6 +202,7 @@ defmodule Opsm.Registries.Cpan do def tarball_url(name, _version) do # Fetch release info to get download URL url = "#{@api_url}/release/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> case Map.get(body, "download_url") do diff --git a/opsm_ex/lib/opsm/registries/cran.ex b/opsm_ex/lib/opsm/registries/cran.ex index 14f24f9d..be7cac9b 100644 --- a/opsm_ex/lib/opsm/registries/cran.ex +++ b/opsm_ex/lib/opsm/registries/cran.ex @@ -17,11 +17,12 @@ defmodule Opsm.Registries.Cran do Fetch package metadata from CRAN. """ def fetch_package(name, version \\ "latest") do - target_version = if version == "latest" do - fetch_latest_version(name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(name) + else + version + end case target_version do nil -> @@ -30,6 +31,7 @@ defmodule Opsm.Registries.Cran do ver -> # Fetch version-specific metadata url = "#{@crandb_url}/#{name}/#{ver}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> {:ok, parse_package(name, body, ver)} @@ -51,8 +53,11 @@ defmodule Opsm.Registries.Cran do defp fetch_latest_version(name) do url = "#{@crandb_url}/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do - {:ok, %{"Version" => version}} -> version + {:ok, %{"Version" => version}} -> + version + _ -> # Fallback: get version list case versions_internal(name) do @@ -69,16 +74,19 @@ defmodule Opsm.Registries.Cran do limit = Keyword.get(opts, :limit, 20) url = "#{@crandb_url}/-/search?q=#{URI.encode(query)}&size=#{limit}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"rows" => rows}} when is_list(rows) -> - results = Enum.map(rows, fn row -> - %{ - name: Map.get(row, "Package") || Map.get(row, "name"), - version: Map.get(row, "Version") || Map.get(row, "version"), - description: Map.get(row, "Title") || Map.get(row, "description", ""), - downloads: 0 - } - end) + results = + Enum.map(rows, fn row -> + %{ + name: Map.get(row, "Package") || Map.get(row, "name"), + version: Map.get(row, "Version") || Map.get(row, "version"), + description: Map.get(row, "Title") || Map.get(row, "description", ""), + downloads: 0 + } + end) + {:ok, results} {:ok, _} -> @@ -114,9 +122,11 @@ defmodule Opsm.Registries.Cran do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions_map}} when is_map(versions_map) -> # Extract version strings and sort (newest first) - version_list = versions_map - |> Map.keys() - |> Enum.sort(:desc) + version_list = + versions_map + |> Map.keys() + |> Enum.sort(:desc) + {:ok, version_list} {:ok, _} -> @@ -209,6 +219,7 @@ defmodule Opsm.Registries.Cran do end end) end + defp parse_dep_field(_), do: %{} # Parse single dependency: "pkg (>= 1.0)" -> {"pkg", ">= 1.0"} @@ -227,6 +238,7 @@ defmodule Opsm.Registries.Cran do # Parse R author field - can be complex, extract names defp parse_authors(nil), do: [] + defp parse_authors(author) when is_binary(author) do # Simple extraction - R author fields can be very complex # Format examples: "John Doe ", "John Doe [aut, cre]" @@ -240,5 +252,6 @@ defmodule Opsm.Registries.Cran do end) |> Enum.reject(&(&1 == "")) end + defp parse_authors(_), do: [] end diff --git a/opsm_ex/lib/opsm/registries/crates.ex b/opsm_ex/lib/opsm/registries/crates.ex index c89fcb5b..7e3f8507 100644 --- a/opsm_ex/lib/opsm/registries/crates.ex +++ b/opsm_ex/lib/opsm/registries/crates.ex @@ -26,11 +26,12 @@ defmodule Opsm.Registries.Crates do crate = body["crate"] versions = body["versions"] || [] - target_version = if version == "latest" do - crate["newest_version"] || crate["max_version"] - else - version - end + target_version = + if version == "latest" do + crate["newest_version"] || crate["max_version"] + else + version + end version_info = Enum.find(versions, fn v -> v["num"] == target_version end) @@ -209,9 +210,11 @@ defmodule Opsm.Registries.Crates do deps |> Enum.group_by(& &1["kind"]) |> Enum.map(fn {kind, deps_list} -> - dep_map = deps_list + dep_map = + deps_list |> Enum.map(fn d -> {d["crate_id"], d["req"]} end) |> Map.new() + {kind, dep_map} end) |> Map.new() diff --git a/opsm_ex/lib/opsm/registries/deno_x.ex b/opsm_ex/lib/opsm/registries/deno_x.ex index 6ae8df49..daf65546 100644 --- a/opsm_ex/lib/opsm/registries/deno_x.ex +++ b/opsm_ex/lib/opsm/registries/deno_x.ex @@ -23,11 +23,12 @@ defmodule Opsm.Registries.DenoX do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - resolved_version = if version == "latest" do - body["latest_version"] || body["latestVersion"] - else - version - end + resolved_version = + if version == "latest" do + body["latest_version"] || body["latestVersion"] + else + version + end case resolved_version do nil -> {:error, :not_found} diff --git a/opsm_ex/lib/opsm/registries/docker_hub.ex b/opsm_ex/lib/opsm/registries/docker_hub.ex index e815256f..13540b4d 100644 --- a/opsm_ex/lib/opsm/registries/docker_hub.ex +++ b/opsm_ex/lib/opsm/registries/docker_hub.ex @@ -64,17 +64,21 @@ defmodule Opsm.Registries.DockerHub do """ def search(query, opts \\ []) do page_size = Keyword.get(opts, :limit, 20) - url = "#{@api_url}/search/repositories/?query=#{URI.encode_www_form(query)}&page_size=#{page_size}" + + url = + "#{@api_url}/search/repositories/?query=#{URI.encode_www_form(query)}&page_size=#{page_size}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> - parsed = Enum.map(results, fn item -> - %{ - name: item["repo_name"] || item["name"], - version: "latest", - description: item["short_description"] || item["description"] - } - end) + parsed = + Enum.map(results, fn item -> + %{ + name: item["repo_name"] || item["name"], + version: "latest", + description: item["short_description"] || item["description"] + } + end) + {:ok, parsed} {:ok, _} -> @@ -104,7 +108,9 @@ defmodule Opsm.Registries.DockerHub do """ def versions(name) do {namespace, repo} = parse_image_name(name) - url = "#{@api_url}/repositories/#{namespace}/#{repo}/tags?page_size=100&ordering=-last_updated" + + url = + "#{@api_url}/repositories/#{namespace}/#{repo}/tags?page_size=100&ordering=-last_updated" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => tags}} when is_list(tags) -> @@ -139,8 +145,9 @@ defmodule Opsm.Registries.DockerHub do defp parse_image(namespace, repo, tag_data, repo_data, tag) do full_name = if namespace == "library", do: repo, else: "#{namespace}/#{repo}" - digest = tag_data["digest"] || - get_in(tag_data, ["images", Access.at(0), "digest"]) + digest = + tag_data["digest"] || + get_in(tag_data, ["images", Access.at(0), "digest"]) manifest = %ManifestFormat{ name: full_name, diff --git a/opsm_ex/lib/opsm/registries/dub.ex b/opsm_ex/lib/opsm/registries/dub.ex index af8833d2..6da8b7ff 100644 --- a/opsm_ex/lib/opsm/registries/dub.ex +++ b/opsm_ex/lib/opsm/registries/dub.ex @@ -28,9 +28,14 @@ defmodule Opsm.Registries.Dub do ver = resolve_version(body, version) {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -88,9 +93,14 @@ defmodule Opsm.Registries.Dub do {:ok, vers} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/eclexia.ex b/opsm_ex/lib/opsm/registries/eclexia.ex index 5374f6c4..f28dce87 100644 --- a/opsm_ex/lib/opsm/registries/eclexia.ex +++ b/opsm_ex/lib/opsm/registries/eclexia.ex @@ -21,7 +21,8 @@ defmodule Opsm.Registries.Eclexia do alias Opsm.Verified.Http, as: VerifiedHttp @base_url "https://packages.eclexia.org/api/v1" - @fallback_mode :git # Until registry deployed + # Until registry deployed + @fallback_mode :git @doc """ Fetch package metadata from eclexia registry. @@ -99,6 +100,7 @@ defmodule Opsm.Registries.Eclexia do defp registry_exists?(name) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -164,11 +166,12 @@ defmodule Opsm.Registries.Eclexia do defp git_fetch(repo_url, version) do # Eclexia uses Cargo.toml (Rust workspace) for manifest # The [package] section contains name, version, description, etc. - manifest_url = case version do - "latest" -> "#{repo_url}/raw/main/Cargo.toml" - "main" -> "#{repo_url}/raw/main/Cargo.toml" - tag -> "#{repo_url}/raw/#{tag}/Cargo.toml" - end + manifest_url = + case version do + "latest" -> "#{repo_url}/raw/main/Cargo.toml" + "main" -> "#{repo_url}/raw/main/Cargo.toml" + tag -> "#{repo_url}/raw/#{tag}/Cargo.toml" + end case VerifiedHttp.get(manifest_url, receive_timeout: 10_000) do {:ok, %{body: body}} -> @@ -206,7 +209,7 @@ defmodule Opsm.Registries.Eclexia do } end - pkg_name = manifest.name || (repo_url |> String.split("/") |> List.last() |> String.trim()) + pkg_name = manifest.name || repo_url |> String.split("/") |> List.last() |> String.trim() pkg = %ResolvedPackage{ package: pkg_name, diff --git a/opsm_ex/lib/opsm/registries/eclipse_marketplace.ex b/opsm_ex/lib/opsm/registries/eclipse_marketplace.ex index 39deabd5..f69efc5f 100644 --- a/opsm_ex/lib/opsm/registries/eclipse_marketplace.ex +++ b/opsm_ex/lib/opsm/registries/eclipse_marketplace.ex @@ -23,11 +23,13 @@ defmodule Opsm.Registries.EclipseMarketplace do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - resolved_version = if version == "latest" do - extract_latest_version(body) - else - version - end + resolved_version = + if version == "latest" do + extract_latest_version(body) + else + version + end + {:ok, parse_listing(body, resolved_version)} {:error, :not_found} -> diff --git a/opsm_ex/lib/opsm/registries/elm.ex b/opsm_ex/lib/opsm/registries/elm.ex index dae69dc9..8b21e9c8 100644 --- a/opsm_ex/lib/opsm/registries/elm.ex +++ b/opsm_ex/lib/opsm/registries/elm.ex @@ -13,51 +13,62 @@ defmodule Opsm.Registries.Elm do @api_url "https://package.elm-lang.org" def fetch_package(name, version \\ "latest") do - target_version = if version == "latest" do - case versions(name) do - {:ok, [latest | _]} -> latest - _ -> nil + target_version = + if version == "latest" do + case versions(name) do + {:ok, [latest | _]} -> latest + _ -> nil + end + else + version end - else - version - end case target_version do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + ver -> url = "#{@api_url}/packages/#{name}/#{ver}/elm.json" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> deps = extract_deps(body) - {:ok, %ResolvedPackage{ - package: name, - version: ver, - forth: :elm, - registry_url: "#{@api_url}/packages/#{name}/#{ver}", - tarball_url: "#{@api_url}/packages/#{name}/#{ver}/endpoint.json", - checksum: nil, - checksum_algo: nil, - manifest: %ManifestFormat{ - name: name, - version: ver, - description: body["summary"], - license: body["license"], - homepage: "#{@api_url}/packages/#{name}", - repository: nil, - authors: [], - keywords: [], - dependencies: deps, - dev_dependencies: %{}, - source_forth: :elm, - raw_manifest: body - }, - attestations: [], - resolved_deps: [] - }} - - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + + {:ok, + %ResolvedPackage{ + package: name, + version: ver, + forth: :elm, + registry_url: "#{@api_url}/packages/#{name}/#{ver}", + tarball_url: "#{@api_url}/packages/#{name}/#{ver}/endpoint.json", + checksum: nil, + checksum_algo: nil, + manifest: %ManifestFormat{ + name: name, + version: ver, + description: body["summary"], + license: body["license"], + homepage: "#{@api_url}/packages/#{name}", + repository: nil, + authors: [], + keywords: [], + dependencies: deps, + dev_dependencies: %{}, + source_forth: :elm, + raw_manifest: body + }, + attestations: [], + resolved_deps: [] + }} + + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end end @@ -65,26 +76,33 @@ defmodule Opsm.Registries.Elm do def search(query, _opts \\ []) do # Elm has no search API — fetch all packages and filter url = "#{@api_url}/search.json" + case VerifiedHttp.get_json(url, receive_timeout: 15_000) do {:ok, packages} when is_list(packages) -> - results = packages + results = + packages |> Enum.filter(fn p -> name = p["name"] || "" summary = p["summary"] || "" + String.contains?(String.downcase(name), String.downcase(query)) || - String.contains?(String.downcase(summary), String.downcase(query)) + String.contains?(String.downcase(summary), String.downcase(query)) end) |> Enum.take(20) |> Enum.map(fn p -> %{name: p["name"], version: nil, description: p["summary"], downloads: 0} end) + {:ok, results} - _ -> {:ok, []} + + _ -> + {:ok, []} end end def exists?(name) do url = "#{@api_url}/packages/#{name}/releases.json" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -93,12 +111,17 @@ defmodule Opsm.Registries.Elm do def versions(name) do url = "#{@api_url}/packages/#{name}/releases.json" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, releases} when is_list(releases) -> - vers = Enum.map(releases, fn r -> r["version"] end) + vers = + Enum.map(releases, fn r -> r["version"] end) |> Enum.reject(&is_nil/1) + {:ok, vers} - _ -> {:error, :not_found} + + _ -> + {:error, :not_found} end end @@ -108,6 +131,7 @@ defmodule Opsm.Registries.Elm do defp extract_deps(body) do deps = body["dependencies"] || %{} + Enum.into(deps, %{}, fn {name, constraint} -> {name, constraint} end) diff --git a/opsm_ex/lib/opsm/registries/elpa.ex b/opsm_ex/lib/opsm/registries/elpa.ex index fb5ed83c..7853f2ab 100644 --- a/opsm_ex/lib/opsm/registries/elpa.ex +++ b/opsm_ex/lib/opsm/registries/elpa.ex @@ -21,11 +21,12 @@ defmodule Opsm.Registries.Elpa do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - target_version = if version == "latest" do - extract_latest_version(body) - else - version - end + target_version = + if version == "latest" do + extract_latest_version(body) + else + version + end deps = extract_deps(body) {:ok, parse_package(name, body, target_version, deps)} @@ -56,45 +57,50 @@ defmodule Opsm.Registries.Elpa do {:ok, packages} when is_list(packages) -> downcased = String.downcase(query) - results = packages - |> Enum.filter(fn pkg -> - name = pkg["name"] || "" - desc = pkg["description"] || pkg["summary"] || "" - String.contains?(String.downcase(name), downcased) || - String.contains?(String.downcase(desc), downcased) - end) - |> Enum.take(limit) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: pkg["version"], - description: pkg["description"] || pkg["summary"] || "", - downloads: 0 - } - end) + results = + packages + |> Enum.filter(fn pkg -> + name = pkg["name"] || "" + desc = pkg["description"] || pkg["summary"] || "" + + String.contains?(String.downcase(name), downcased) || + String.contains?(String.downcase(desc), downcased) + end) + |> Enum.take(limit) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: pkg["version"], + description: pkg["description"] || pkg["summary"] || "", + downloads: 0 + } + end) {:ok, results} {:ok, packages} when is_map(packages) -> downcased = String.downcase(query) - results = packages - |> Enum.filter(fn {name, _data} -> - String.contains?(String.downcase(name), downcased) - end) - |> Enum.take(limit) - |> Enum.map(fn {name, data} -> - ver = case data do - %{"version" => v} -> v - _ -> nil - end - %{ - name: name, - version: ver, - description: Map.get(data, "description", ""), - downloads: 0 - } - end) + results = + packages + |> Enum.filter(fn {name, _data} -> + String.contains?(String.downcase(name), downcased) + end) + |> Enum.take(limit) + |> Enum.map(fn {name, data} -> + ver = + case data do + %{"version" => v} -> v + _ -> nil + end + + %{ + name: name, + version: ver, + description: Map.get(data, "description", ""), + downloads: 0 + } + end) {:ok, results} @@ -163,11 +169,13 @@ defmodule Opsm.Registries.Elpa do case Map.get(body, "dependencies") || Map.get(body, "requires") do deps when is_map(deps) -> Enum.reduce(deps, %{}, fn {dep_name, constraint}, acc -> - ver = case constraint do - c when is_binary(c) -> c - c when is_list(c) -> ">= #{Enum.join(c, ".")}" - _ -> ">= 0.0.0" - end + ver = + case constraint do + c when is_binary(c) -> c + c when is_list(c) -> ">= #{Enum.join(c, ".")}" + _ -> ">= 0.0.0" + end + Map.put(acc, dep_name, ver) end) @@ -176,8 +184,10 @@ defmodule Opsm.Registries.Elpa do case dep do [dep_name | ver_parts] -> Map.put(acc, to_string(dep_name), ">= #{Enum.join(ver_parts, ".")}") + dep_name when is_binary(dep_name) -> Map.put(acc, dep_name, ">= 0.0.0") + _ -> acc end diff --git a/opsm_ex/lib/opsm/registries/ephapax.ex b/opsm_ex/lib/opsm/registries/ephapax.ex index 911e18d2..cea61d71 100644 --- a/opsm_ex/lib/opsm/registries/ephapax.ex +++ b/opsm_ex/lib/opsm/registries/ephapax.ex @@ -104,17 +104,21 @@ defmodule Opsm.Registries.Ephapax do defp fetch_from_registry(name, version) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> target = if version == "latest", do: body["latest_version"], else: version {:ok, parse_registry_package(body, target)} - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end defp search_registry(query, opts) do limit = Keyword.get(opts, :limit, 20) url = "#{@base_url}/packages?q=#{URI.encode(query)}&limit=#{limit}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, &parse_search_result/1)} {:error, reason} -> {:error, reason} @@ -130,6 +134,7 @@ defmodule Opsm.Registries.Ephapax do defp registry_versions(name) do url = "#{@base_url}/packages/#{URI.encode(name)}/versions" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, & &1["version"])} {:error, reason} -> {:error, reason} @@ -145,9 +150,12 @@ defmodule Opsm.Registries.Ephapax do defp search_curated(query, _opts) do q = String.downcase(query) - results = Enum.filter(@known_packages, fn p -> - String.contains?(String.downcase("#{p.name} #{p.description}"), q) - end) + + results = + Enum.filter(@known_packages, fn p -> + String.contains?(String.downcase("#{p.name} #{p.description}"), q) + end) + {:ok, Enum.map(results, &curated_to_resolved(&1, "latest"))} end @@ -160,13 +168,18 @@ defmodule Opsm.Registries.Ephapax do pkg_info.url |> String.replace("https://github.com/", "https://api.github.com/repos/") |> Kernel.<>("/tags") + case VerifiedHttp.get_json(tags_url, receive_timeout: 10_000) do {:ok, tags} when is_list(tags) -> versions = Enum.map(tags, & &1["name"]) |> Enum.reject(&is_nil/1) {:ok, if(versions == [], do: ["main"], else: versions)} - _ -> {:ok, ["main"]} + + _ -> + {:ok, ["main"]} end - :not_found -> {:error, :not_found} + + :not_found -> + {:error, :not_found} end end @@ -175,6 +188,7 @@ defmodule Opsm.Registries.Ephapax do "https://github.com/hyperpolymath/#{name}", "https://github.com/hyperpolymath/ephapax-#{name}" ] + Enum.find_value(urls, {:error, :not_found}, fn url -> case fetch_git_manifest(%{name: name, url: url, description: nil}, version) do {:ok, pkg} -> {:ok, pkg} @@ -189,8 +203,11 @@ defmodule Opsm.Registries.Ephapax do base = pkg_info.url try_url = fn manifest -> - url = if path, do: "#{base}/raw/#{branch}/#{path}/#{manifest}", - else: "#{base}/raw/#{branch}/#{manifest}" + url = + if path, + do: "#{base}/raw/#{branch}/#{path}/#{manifest}", + else: "#{base}/raw/#{branch}/#{manifest}" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: text}} -> {:ok, text} _ -> :not_found @@ -244,6 +261,7 @@ defmodule Opsm.Registries.Ephapax do attestations: [], resolved_deps: [] } + {:ok, pkg} end @@ -304,8 +322,12 @@ defmodule Opsm.Registries.Ephapax do end defp parse_search_result(r) do - %{name: r["name"], version: r["latest_version"], - description: r["description"], downloads: r["downloads"] || 0} + %{ + name: r["name"], + version: r["latest_version"], + description: r["description"], + downloads: r["downloads"] || 0 + } end defp find_curated(name) do @@ -316,5 +338,7 @@ defmodule Opsm.Registries.Ephapax do end defp default_authors, do: ["Jonathan D.A. Jewell "] - defp default_keywords, do: ["ephapax", "linear-types", "affine", "wasmgc", "formal-verification"] + + defp default_keywords, + do: ["ephapax", "linear-types", "affine", "wasmgc", "formal-verification"] end diff --git a/opsm_ex/lib/opsm/registries/error_lang.ex b/opsm_ex/lib/opsm/registries/error_lang.ex index 3d6c6fd9..3d6f4796 100644 --- a/opsm_ex/lib/opsm/registries/error_lang.ex +++ b/opsm_ex/lib/opsm/registries/error_lang.ex @@ -17,7 +17,8 @@ defmodule Opsm.Registries.ErrorLang do alias Opsm.Verified.Http, as: VerifiedHttp @base_url "https://packages.error-lang.dev/api/v1" - @fallback_mode :git # Until registry deployed + # Until registry deployed + @fallback_mode :git @doc """ Fetch package metadata from error-lang registry. @@ -93,6 +94,7 @@ defmodule Opsm.Registries.ErrorLang do defp registry_exists?(name) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -150,17 +152,19 @@ defmodule Opsm.Registries.ErrorLang do defp git_versions(_name) do # For git mode, versions are git tags - {:ok, ["main", "master"]} # Minimal fallback + # Minimal fallback + {:ok, ["main", "master"]} end defp git_fetch(repo_url, version) do # error-lang uses error.toml for manifest - manifest_url = case version do - "latest" -> "#{repo_url}/raw/main/error.toml" - "main" -> "#{repo_url}/raw/main/error.toml" - "master" -> "#{repo_url}/raw/master/error.toml" - tag -> "#{repo_url}/raw/#{tag}/error.toml" - end + manifest_url = + case version do + "latest" -> "#{repo_url}/raw/main/error.toml" + "main" -> "#{repo_url}/raw/main/error.toml" + "master" -> "#{repo_url}/raw/master/error.toml" + tag -> "#{repo_url}/raw/#{tag}/error.toml" + end case VerifiedHttp.get(manifest_url, receive_timeout: 10_000) do {:ok, %{body: body}} -> @@ -199,7 +203,7 @@ defmodule Opsm.Registries.ErrorLang do end pkg = %ResolvedPackage{ - package: manifest.name || (repo_url |> String.split("/") |> List.last() |> String.trim()), + package: manifest.name || repo_url |> String.split("/") |> List.last() |> String.trim(), version: manifest.version || version, forth: :error_lang, registry_url: repo_url, diff --git a/opsm_ex/lib/opsm/registries/flatpak.ex b/opsm_ex/lib/opsm/registries/flatpak.ex index 3bc6381e..7acfe17f 100644 --- a/opsm_ex/lib/opsm/registries/flatpak.ex +++ b/opsm_ex/lib/opsm/registries/flatpak.ex @@ -18,22 +18,31 @@ defmodule Opsm.Registries.Flatpak do """ def fetch_package(name, version \\ "latest") do url = "#{@api_url}/appstream/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest" do - releases = get_in(body, ["releases"]) || [] - case releases do - [latest | _] -> latest["version"] - _ -> "0.0.0" + ver = + if version == "latest" do + releases = get_in(body, ["releases"]) || [] + + case releases do + [latest | _] -> latest["version"] + _ -> "0.0.0" + end + else + version end - else - version - end + {:ok, parse_flatpak(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -55,7 +64,7 @@ defmodule Opsm.Registries.Flatpak do manifest: manifest, tarball_url: nil, checksum: nil, - attestations: [], + attestations: [] } end @@ -64,14 +73,18 @@ defmodule Opsm.Registries.Flatpak do """ def get_versions(name) do url = "#{@api_url}/appstream/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - versions = (body["releases"] || []) - |> Enum.map(fn r -> r["version"] end) - |> Enum.reject(&is_nil/1) + versions = + (body["releases"] || []) + |> Enum.map(fn r -> r["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, versions} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -80,25 +93,33 @@ defmodule Opsm.Registries.Flatpak do """ def search(query, _opts \\ []) do url = "#{@api_url}/search?q=#{URI.encode(query)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn app -> - %{name: app["id"], version: nil, description: app["summary"]} - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn app -> + %{name: app["id"], version: nil, description: app["summary"]} + end) + {:ok, results} {:ok, %{"hits" => hits}} when is_list(hits) -> - results = hits - |> Enum.take(20) - |> Enum.map(fn app -> - %{name: app["app_id"] || app["id"], version: nil, description: app["summary"]} - end) + results = + hits + |> Enum.take(20) + |> Enum.map(fn app -> + %{name: app["app_id"] || app["id"], version: nil, description: app["summary"]} + end) + {:ok, results} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/fpm_registry.ex b/opsm_ex/lib/opsm/registries/fpm_registry.ex index 41ea75b0..604dd91c 100644 --- a/opsm_ex/lib/opsm/registries/fpm_registry.ex +++ b/opsm_ex/lib/opsm/registries/fpm_registry.ex @@ -97,9 +97,14 @@ defmodule Opsm.Registries.FpmRegistry do v -> {:ok, [v]} end - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -115,9 +120,14 @@ defmodule Opsm.Registries.FpmRegistry do ver = resolve_version(body, version) {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/freebsd.ex b/opsm_ex/lib/opsm/registries/freebsd.ex index b8bd9be6..1ef00f02 100644 --- a/opsm_ex/lib/opsm/registries/freebsd.ex +++ b/opsm_ex/lib/opsm/registries/freebsd.ex @@ -23,14 +23,21 @@ defmodule Opsm.Registries.Freebsd do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: body["version"] || body["portversion"], - else: version + ver = + if version == "latest", + do: body["version"] || body["portversion"], + else: version + {:ok, parse_freebsd_package(name, body, ver)} - {:error, :not_found} -> fetch_from_pkg_api(name, version) - {:error, %{status: 404}} -> fetch_from_pkg_api(name, version) - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + fetch_from_pkg_api(name, version) + + {:error, %{status: 404}} -> + fetch_from_pkg_api(name, version) + + {:error, reason} -> + {:error, reason} end end @@ -38,21 +45,28 @@ defmodule Opsm.Registries.Freebsd do _url = "#{@api_url}/#{@repo_branch}/latest/All/#{URI.encode(name)}.pkg" case VerifiedHttp.get_json( - "#{@api_url}/#{@repo_branch}/latest/packagesite.json", - receive_timeout: 10_000 - ) do + "#{@api_url}/#{@repo_branch}/latest/packagesite.json", + receive_timeout: 10_000 + ) do {:ok, body} when is_map(body) -> case find_package_in_index(body, name) do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + pkg_data -> - ver = if version == "latest", - do: pkg_data["version"] || "0.0.0", - else: version + ver = + if version == "latest", + do: pkg_data["version"] || "0.0.0", + else: version + {:ok, build_resolved_from_index(name, pkg_data, ver)} end - {:ok, _} -> {:error, :not_found} - {:error, _} -> {:error, :not_found} + {:ok, _} -> + {:error, :not_found} + + {:error, _} -> + {:error, :not_found} end end @@ -61,18 +75,22 @@ defmodule Opsm.Registries.Freebsd do end defp parse_freebsd_package(name, body, version) do - deps = (body["run_depends"] || body["depends"] || []) - |> Enum.map(fn - d when is_binary(d) -> - dep_name = d |> String.split(":") |> hd() |> String.trim() - {dep_name, "*"} - d when is_map(d) -> - {d["name"] || "", "*"} - _ -> nil - end) - |> Enum.reject(&is_nil/1) - |> Enum.reject(fn {n, _} -> n == "" end) - |> Map.new() + deps = + (body["run_depends"] || body["depends"] || []) + |> Enum.map(fn + d when is_binary(d) -> + dep_name = d |> String.split(":") |> hd() |> String.trim() + {dep_name, "*"} + + d when is_map(d) -> + {d["name"] || "", "*"} + + _ -> + nil + end) + |> Enum.reject(&is_nil/1) + |> Enum.reject(fn {n, _} -> n == "" end) + |> Map.new() origin = body["origin"] || body["port_origin"] || name @@ -93,7 +111,7 @@ defmodule Opsm.Registries.Freebsd do manifest: manifest, tarball_url: "#{@api_url}/#{@repo_branch}/latest/All/#{name}-#{version || "0.0.0"}.pkg", checksum: nil, - attestations: [], + attestations: [] } end @@ -116,7 +134,7 @@ defmodule Opsm.Registries.Freebsd do tarball_url: data["repopath"], checksum: data["sum"], checksum_algo: if(data["sum"], do: :sha256), - attestations: [], + attestations: [] } end @@ -146,23 +164,34 @@ defmodule Opsm.Registries.Freebsd do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{name: pkg["name"] || pkg["package_name"], version: pkg["version"], description: pkg["comment"]} - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"] || pkg["package_name"], + version: pkg["version"], + description: pkg["comment"] + } + end) + {:ok, results} {:ok, %{"results" => results}} when is_list(results) -> - hits = results - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{name: pkg["name"], version: pkg["version"], description: pkg["comment"]} - end) + hits = + results + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{name: pkg["name"], version: pkg["version"], description: pkg["comment"]} + end) + {:ok, hits} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/git.ex b/opsm_ex/lib/opsm/registries/git.ex index 5dbfb735..ef5d4184 100644 --- a/opsm_ex/lib/opsm/registries/git.ex +++ b/opsm_ex/lib/opsm/registries/git.ex @@ -58,7 +58,7 @@ defmodule Opsm.Registries.Git do {:ok, %ResolvedPackage{...}} """ @spec fetch_package(String.t(), String.t(), keyword()) :: - {:ok, ResolvedPackage.t()} | {:error, term()} + {:ok, ResolvedPackage.t()} | {:error, term()} def fetch_package(url, version \\ "latest", opts \\ []) do Logger.debug("Fetching package from git: #{url}@#{version}") @@ -91,9 +91,10 @@ defmodule Opsm.Registries.Git do with {:ok, cache_path} <- ensure_cloned(url, shallow: false) do case Opsm.SafeExec.cmd("git", ["tag", "--sort=-v:refname"], cd: cache_path) do {output, 0} -> - tags = output - |> String.split("\n", trim: true) - |> Enum.map(&String.trim/1) + tags = + output + |> String.split("\n", trim: true) + |> Enum.map(&String.trim/1) {:ok, tags} @@ -116,9 +117,10 @@ defmodule Opsm.Registries.Git do with {:ok, cache_path} <- ensure_cloned(url, []) do case Opsm.SafeExec.cmd("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], cd: cache_path) do {output, 0} -> - branch = output - |> String.trim() - |> String.replace("refs/remotes/origin/", "") + branch = + output + |> String.trim() + |> String.replace("refs/remotes/origin/", "") {:ok, branch} @@ -174,11 +176,12 @@ defmodule Opsm.Registries.Git do defp clone_repo(url, dest, shallow) do File.mkdir_p!(Path.dirname(dest)) - args = if shallow do - ["clone", "--depth", "1", url, dest] - else - ["clone", url, dest] - end + args = + if shallow do + ["clone", "--depth", "1", url, dest] + else + ["clone", url, dest] + end case Opsm.SafeExec.cmd("git", args, stderr_to_stdout: true) do {_, 0} -> @@ -186,7 +189,8 @@ defmodule Opsm.Registries.Git do {:ok, dest} {error, _code} -> - File.rm_rf(dest) # Clean up partial clone + # Clean up partial clone + File.rm_rf(dest) {:error, "Git clone failed: #{error}"} end end @@ -198,7 +202,8 @@ defmodule Opsm.Registries.Git do {error, _code} -> Logger.warning("Failed to update git cache: #{error}") - :ok # Non-fatal, use existing cache + # Non-fatal, use existing cache + :ok end end @@ -211,7 +216,9 @@ defmodule Opsm.Registries.Git do {_, _} -> # Fallback to main/master case Opsm.SafeExec.cmd("git", ["rev-parse", "origin/main"], cd: cache_path) do - {output, 0} -> {:ok, String.trim(output)} + {output, 0} -> + {:ok, String.trim(output)} + {_, _} -> case Opsm.SafeExec.cmd("git", ["rev-parse", "origin/master"], cd: cache_path) do {output, 0} -> {:ok, String.trim(output)} @@ -251,11 +258,13 @@ defmodule Opsm.Registries.Git do defp find_manifest(cache_path, opts) do subpath = Keyword.get(opts, :subpath) - search_path = if subpath do - Path.join(cache_path, subpath) - else - cache_path - end + + search_path = + if subpath do + Path.join(cache_path, subpath) + else + cache_path + end manifest_file = Keyword.get(opts, :manifest_file) @@ -272,25 +281,39 @@ defmodule Opsm.Registries.Git do defp detect_manifest(search_path) do # Common manifest filenames by ecosystem candidates = [ - "package.json", # npm - "Cargo.toml", # Rust - "mix.exs", # Elixir - "*.nimble", # Nim - "*.ipkg", # Idris2 - "*.cabal", # Haskell - "dune-project", # OCaml - "*.gpr", # Ada - "_CoqProject", # Coq - "lakefile.lean", # Lean 4 - "META.json", # Mercury - "*.asd", # Common Lisp - "shard.yml" # Crystal + # npm + "package.json", + # Rust + "Cargo.toml", + # Elixir + "mix.exs", + # Nim + "*.nimble", + # Idris2 + "*.ipkg", + # Haskell + "*.cabal", + # OCaml + "dune-project", + # Ada + "*.gpr", + # Coq + "_CoqProject", + # Lean 4 + "lakefile.lean", + # Mercury + "META.json", + # Common Lisp + "*.asd", + # Crystal + "shard.yml" ] - found = Enum.find_value(candidates, fn pattern -> - matches = Path.wildcard(Path.join(search_path, pattern)) - if matches != [], do: List.first(matches) - end) + found = + Enum.find_value(candidates, fn pattern -> + matches = Path.wildcard(Path.join(search_path, pattern)) + if matches != [], do: List.first(matches) + end) case found do nil -> {:error, "No manifest file found in repository"} @@ -378,6 +401,7 @@ defmodule Opsm.Registries.Git do ref |> String.replace("refs/tags/", "") |> String.replace("refs/heads/", "") - |> String.slice(0..10) # Truncate SHA if needed + # Truncate SHA if needed + |> String.slice(0..10) end end diff --git a/opsm_ex/lib/opsm/registries/github_packages.ex b/opsm_ex/lib/opsm/registries/github_packages.ex index bb85dde8..fc137bc7 100644 --- a/opsm_ex/lib/opsm/registries/github_packages.ex +++ b/opsm_ex/lib/opsm/registries/github_packages.ex @@ -20,7 +20,9 @@ defmodule Opsm.Registries.GithubPackages do """ def fetch_package(name, version \\ "latest") do {owner, package_name, package_type} = parse_package_name(name) - url = "#{@api_url}/users/#{owner}/packages/#{package_type}/#{URI.encode(package_name)}/versions" + + url = + "#{@api_url}/users/#{owner}/packages/#{package_type}/#{URI.encode(package_name)}/versions" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, versions_list} when is_list(versions_list) -> @@ -69,17 +71,21 @@ defmodule Opsm.Registries.GithubPackages do """ def search(query, opts \\ []) do per_page = Keyword.get(opts, :limit, 20) - url = "#{@api_url}/search/repositories?q=#{URI.encode_www_form(query)}+has:packages&per_page=#{per_page}" + + url = + "#{@api_url}/search/repositories?q=#{URI.encode_www_form(query)}+has:packages&per_page=#{per_page}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"items" => items}} when is_list(items) -> - results = Enum.map(items, fn item -> - %{ - name: item["full_name"], - version: nil, - description: item["description"] - } - end) + results = + Enum.map(items, fn item -> + %{ + name: item["full_name"], + version: nil, + description: item["description"] + } + end) + {:ok, results} {:ok, _} -> @@ -109,13 +115,17 @@ defmodule Opsm.Registries.GithubPackages do """ def versions(name) do {owner, package_name, package_type} = parse_package_name(name) - url = "#{@api_url}/users/#{owner}/packages/#{package_type}/#{URI.encode(package_name)}/versions" + + url = + "#{@api_url}/users/#{owner}/packages/#{package_type}/#{URI.encode(package_name)}/versions" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, versions_list} when is_list(versions_list) -> - version_names = Enum.map(versions_list, fn v -> - extract_version_tag(v) - end) + version_names = + Enum.map(versions_list, fn v -> + extract_version_tag(v) + end) + {:ok, version_names} {:ok, _} -> @@ -169,11 +179,12 @@ defmodule Opsm.Registries.GithubPackages do defp parse_github_package(owner, package_name, package_type, version_data, version) do full_name = "#{owner}/#{package_name}" - registry_base = case package_type do - "container" -> "https://ghcr.io/#{owner}/#{package_name}" - "npm" -> "https://npm.pkg.github.com/#{owner}/#{package_name}" - _ -> "https://github.com/#{owner}/#{package_name}/packages" - end + registry_base = + case package_type do + "container" -> "https://ghcr.io/#{owner}/#{package_name}" + "npm" -> "https://npm.pkg.github.com/#{owner}/#{package_name}" + _ -> "https://github.com/#{owner}/#{package_name}/packages" + end manifest = %ManifestFormat{ name: full_name, diff --git a/opsm_ex/lib/opsm/registries/gitlab_packages.ex b/opsm_ex/lib/opsm/registries/gitlab_packages.ex index 5c4d8542..4ee7cd05 100644 --- a/opsm_ex/lib/opsm/registries/gitlab_packages.ex +++ b/opsm_ex/lib/opsm/registries/gitlab_packages.ex @@ -19,7 +19,9 @@ defmodule Opsm.Registries.GitlabPackages do """ def fetch_package(name, version \\ "latest") do {project_id, package_name} = parse_package_ref(name) - url = "#{@api_url}/projects/#{URI.encode(project_id, &URI.char_unreserved?/1)}/packages?package_name=#{URI.encode_www_form(package_name)}&sort=desc" + + url = + "#{@api_url}/projects/#{URI.encode(project_id, &URI.char_unreserved?/1)}/packages?package_name=#{URI.encode_www_form(package_name)}&sort=desc" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, packages} when is_list(packages) and length(packages) > 0 -> @@ -67,17 +69,21 @@ defmodule Opsm.Registries.GitlabPackages do """ def search(query, opts \\ []) do per_page = Keyword.get(opts, :limit, 20) - url = "#{@api_url}/projects?search=#{URI.encode_www_form(query)}&with_packages=true&per_page=#{per_page}" + + url = + "#{@api_url}/projects?search=#{URI.encode_www_form(query)}&with_packages=true&per_page=#{per_page}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, projects} when is_list(projects) -> - results = Enum.map(projects, fn project -> - %{ - name: project["path_with_namespace"], - version: nil, - description: project["description"] - } - end) + results = + Enum.map(projects, fn project -> + %{ + name: project["path_with_namespace"], + version: nil, + description: project["description"] + } + end) + {:ok, results} {:ok, _} -> @@ -93,7 +99,9 @@ defmodule Opsm.Registries.GitlabPackages do """ def exists?(name) do {project_id, package_name} = parse_package_ref(name) - url = "#{@api_url}/projects/#{URI.encode(project_id, &URI.char_unreserved?/1)}/packages?package_name=#{URI.encode_www_form(package_name)}&per_page=1" + + url = + "#{@api_url}/projects/#{URI.encode(project_id, &URI.char_unreserved?/1)}/packages?package_name=#{URI.encode_www_form(package_name)}&per_page=1" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, packages} when is_list(packages) and length(packages) > 0 -> true @@ -108,14 +116,18 @@ defmodule Opsm.Registries.GitlabPackages do """ def versions(name) do {project_id, package_name} = parse_package_ref(name) - url = "#{@api_url}/projects/#{URI.encode(project_id, &URI.char_unreserved?/1)}/packages?package_name=#{URI.encode_www_form(package_name)}&sort=desc&per_page=100" + + url = + "#{@api_url}/projects/#{URI.encode(project_id, &URI.char_unreserved?/1)}/packages?package_name=#{URI.encode_www_form(package_name)}&sort=desc&per_page=100" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, packages} when is_list(packages) -> - version_list = packages - |> Enum.map(fn pkg -> pkg["version"] end) - |> Enum.reject(&is_nil/1) - |> Enum.uniq() + version_list = + packages + |> Enum.map(fn pkg -> pkg["version"] end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + {:ok, version_list} {:ok, _} -> diff --git a/opsm_ex/lib/opsm/registries/go_modules.ex b/opsm_ex/lib/opsm/registries/go_modules.ex index fa072b4f..03c02737 100644 --- a/opsm_ex/lib/opsm/registries/go_modules.ex +++ b/opsm_ex/lib/opsm/registries/go_modules.ex @@ -20,11 +20,12 @@ defmodule Opsm.Registries.GoModules do # Go modules use paths like "github.com/gin-gonic/gin" encoded = encode_module_path(name) - target_version = if version == "latest" do - fetch_latest_version(encoded) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(encoded) + else + version + end case target_version do nil -> @@ -33,6 +34,7 @@ defmodule Opsm.Registries.GoModules do ver -> # Fetch version info url = "#{@proxy_url}/#{encoded}/@v/#{ver}.info" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> deps = fetch_go_mod_deps(encoded, ver) @@ -56,8 +58,11 @@ defmodule Opsm.Registries.GoModules do defp fetch_latest_version(encoded) do url = "#{@proxy_url}/#{encoded}/@latest" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do - {:ok, %{"Version" => version}} -> version + {:ok, %{"Version" => version}} -> + version + _ -> # Fallback: get version list case versions_internal(encoded) do @@ -69,12 +74,16 @@ defmodule Opsm.Registries.GoModules do defp fetch_go_mod_deps(encoded, version) do url = "#{@proxy_url}/#{encoded}/@v/#{version}.mod" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: body}} when is_binary(body) -> parse_go_mod(body) + {:ok, body} when is_binary(body) -> parse_go_mod(body) - _ -> %{} + + _ -> + %{} end end @@ -86,6 +95,7 @@ defmodule Opsm.Registries.GoModules do |> String.split("\n") |> Enum.reduce({false, %{}}, fn line, {in_require, deps} -> trimmed = String.trim(line) + cond do trimmed == "require (" -> {true, deps} @@ -106,7 +116,9 @@ defmodule Opsm.Registries.GoModules do ver = String.trim(ver, "\"") {true, Map.put(deps, mod, ">= #{ver}")} end - _ -> {true, deps} + + _ -> + {true, deps} end String.starts_with?(trimmed, "require ") -> @@ -115,7 +127,9 @@ defmodule Opsm.Registries.GoModules do mod = String.trim(mod, "\"") ver = String.trim(ver, "\"") {false, Map.put(deps, mod, ">= #{ver}")} - _ -> {false, deps} + + _ -> + {false, deps} end true -> @@ -135,6 +149,7 @@ defmodule Opsm.Registries.GoModules do # The Go proxy has no search API. Check if the query is a direct module path. encoded = encode_module_path(query) + case VerifiedHttp.get_json("#{@proxy_url}/#{encoded}/@latest", receive_timeout: 10_000) do {:ok, %{"Version" => version}} -> {:ok, [%{name: query, version: version, description: "Go module", downloads: 0}]} @@ -171,6 +186,7 @@ defmodule Opsm.Registries.GoModules do case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: body}} when is_binary(body) -> versions = body |> String.split("\n", trim: true) |> Enum.reverse() + if versions == [] do # Some Go modules only have pseudo-versions (no tags) # Fall back to @latest @@ -181,6 +197,7 @@ defmodule Opsm.Registries.GoModules do {:ok, body} when is_binary(body) -> versions = body |> String.split("\n", trim: true) |> Enum.reverse() + if versions == [] do fetch_latest_as_list(encoded) else @@ -233,12 +250,16 @@ defmodule Opsm.Registries.GoModules do defp fetch_ziphash(name, version) do # Use the Go checksum database (sum.golang.org) for zip hash url = "#{@sum_db}/lookup/#{name}@#{version}" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: body}} when is_binary(body) -> parse_sum_db_hash(body, name, version) + {:ok, body} when is_binary(body) -> parse_sum_db_hash(body, name, version) - _ -> nil + + _ -> + nil end end @@ -250,6 +271,7 @@ defmodule Opsm.Registries.GoModules do |> String.split("\n") |> Enum.find_value(fn line -> prefix = "#{name} #{version} h1:" + if String.starts_with?(line, prefix) do "h1:" <> _ = String.trim_leading(line, "#{name} #{version} ") parse_h1_hash(String.trim(line) |> String.split(" ") |> List.last()) @@ -261,11 +283,13 @@ defmodule Opsm.Registries.GoModules do # Convert to hex-encoded SHA256 for compatibility with our checksum system defp parse_h1_hash("h1:" <> b64_hash) do b64_clean = String.trim_trailing(b64_hash, "=") |> pad_base64() + case Base.decode64(b64_clean) do {:ok, raw_hash} -> Base.encode16(raw_hash, case: :lower) :error -> nil end end + defp parse_h1_hash(_), do: nil defp pad_base64(s) do @@ -292,7 +316,8 @@ defmodule Opsm.Registries.GoModules do description: nil, license: nil, homepage: "#{@pkg_url}/#{name}", - repository: if(String.starts_with?(name, "github.com/"), do: "https://#{name}", else: nil), + repository: + if(String.starts_with?(name, "github.com/"), do: "https://#{name}", else: nil), authors: [], keywords: [], dependencies: deps, diff --git a/opsm_ex/lib/opsm/registries/godot.ex b/opsm_ex/lib/opsm/registries/godot.ex index b8b312c8..8359a2b0 100644 --- a/opsm_ex/lib/opsm/registries/godot.ex +++ b/opsm_ex/lib/opsm/registries/godot.ex @@ -20,15 +20,17 @@ defmodule Opsm.Registries.Godot do def fetch_package(name, version \\ "latest") do case fetch_by_id_or_search(name) do {:ok, body} -> - ver = if version == "latest" do - body["version_string"] || body["version"] || "0.0.0" - else - version - end + ver = + if version == "latest" do + body["version_string"] || body["version"] || "0.0.0" + else + version + end {:ok, parse_godot_asset(name, body, ver)} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -57,16 +59,19 @@ defmodule Opsm.Registries.Godot do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> results = body["result"] || [] + case results do [asset | _] -> # Fetch full asset details by ID asset_id = asset["asset_id"] if asset_id, do: fetch_by_id(asset_id), else: {:ok, asset} - [] -> {:error, :not_found} + [] -> + {:error, :not_found} end - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end @@ -86,11 +91,13 @@ defmodule Opsm.Registries.Godot do homepage: body["browse_url"], repository: body["browse_url"], authors: if(body["author"], do: [body["author"]], else: []), - keywords: [ - "godot", - "godot-#{body["godot_version"] || "unknown"}", - category_label(category) - ] |> Enum.reject(&is_nil/1), + keywords: + [ + "godot", + "godot-#{body["godot_version"] || "unknown"}", + category_label(category) + ] + |> Enum.reject(&is_nil/1), dependencies: %{}, source_forth: :godot, raw_manifest: body @@ -144,17 +151,20 @@ defmodule Opsm.Registries.Godot do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - results = (body["result"] || []) - |> Enum.map(fn asset -> - %{ - name: asset["title"], - version: asset["version_string"] || asset["version"], - description: asset["description"] || asset["blurb"] - } - end) + results = + (body["result"] || []) + |> Enum.map(fn asset -> + %{ + name: asset["title"], + version: asset["version_string"] || asset["version"], + description: asset["description"] || asset["blurb"] + } + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/gradle_plugins.ex b/opsm_ex/lib/opsm/registries/gradle_plugins.ex index d2d1f9fa..bf96e5fd 100644 --- a/opsm_ex/lib/opsm/registries/gradle_plugins.ex +++ b/opsm_ex/lib/opsm/registries/gradle_plugins.ex @@ -25,11 +25,12 @@ defmodule Opsm.Registries.GradlePlugins do marker_artifact = "#{name}.gradle.plugin" marker_path = String.replace(marker_artifact, ".", "/") - target_version = if version == "latest" do - fetch_latest_version(group_path, marker_path) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(group_path, marker_path) + else + version + end case target_version do nil -> @@ -227,6 +228,7 @@ defmodule Opsm.Registries.GradlePlugins do defp extract_scm(_), do: nil defp extract_developers(nil), do: [] + defp extract_developers(devs) when is_list(devs) do Enum.map(devs, fn %{"name" => name} -> name @@ -235,14 +237,17 @@ defmodule Opsm.Registries.GradlePlugins do end) |> Enum.reject(&is_nil/1) end + defp extract_developers(_), do: [] defp extract_gradle_deps(nil), do: %{} + defp extract_gradle_deps(deps) when is_list(deps) do Enum.reduce(deps, %{}, fn dep, acc -> key = "#{dep["groupId"]}:#{dep["artifactId"]}" Map.put(acc, key, dep["version"] || "latest") end) end + defp extract_gradle_deps(_), do: %{} end diff --git a/opsm_ex/lib/opsm/registries/grafana.ex b/opsm_ex/lib/opsm/registries/grafana.ex index 8d317373..fba75725 100644 --- a/opsm_ex/lib/opsm/registries/grafana.ex +++ b/opsm_ex/lib/opsm/registries/grafana.ex @@ -41,9 +41,12 @@ defmodule Opsm.Registries.Grafana do defp resolve_version(body, "latest") do case body["version"] do - v when is_binary(v) -> v + v when is_binary(v) -> + v + _ -> versions_list = body["versions"] || [] + case versions_list do [%{"version" => v} | _] -> v _ -> "0.0.0" @@ -61,30 +64,35 @@ defmodule Opsm.Registries.Grafana do page_size = Keyword.get(opts, :limit, 20) type_filter = Keyword.get(opts, :type, nil) - url = "#{@api_url}?q=#{URI.encode_www_form(query)}&pageSize=#{page_size}" <> - if(type_filter, do: "&type=#{type_filter}", else: "") + url = + "#{@api_url}?q=#{URI.encode_www_form(query)}&pageSize=#{page_size}" <> + if(type_filter, do: "&type=#{type_filter}", else: "") case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"items" => plugins}} when is_list(plugins) -> - results = Enum.map(plugins, fn plugin -> - %{ - name: plugin["slug"] || plugin["id"], - version: plugin["version"], - description: plugin["description"] - } - end) + results = + Enum.map(plugins, fn plugin -> + %{ + name: plugin["slug"] || plugin["id"], + version: plugin["version"], + description: plugin["description"] + } + end) + {:ok, results} {:ok, plugins} when is_list(plugins) -> - results = plugins - |> Enum.take(page_size) - |> Enum.map(fn plugin -> - %{ - name: plugin["slug"] || plugin["id"], - version: plugin["version"], - description: plugin["description"] - } - end) + results = + plugins + |> Enum.take(page_size) + |> Enum.map(fn plugin -> + %{ + name: plugin["slug"] || plugin["id"], + version: plugin["version"], + description: plugin["description"] + } + end) + {:ok, results} {:ok, _} -> @@ -116,15 +124,19 @@ defmodule Opsm.Registries.Grafana do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"items" => versions_list}} when is_list(versions_list) -> - ver_strs = versions_list - |> Enum.map(fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + ver_strs = + versions_list + |> Enum.map(fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_strs} {:ok, versions_list} when is_list(versions_list) -> - ver_strs = versions_list - |> Enum.map(fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + ver_strs = + versions_list + |> Enum.map(fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_strs} {:ok, _} -> @@ -147,9 +159,11 @@ defmodule Opsm.Registries.Grafana do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions_list}} when is_list(versions_list) -> - ver_strs = versions_list - |> Enum.map(fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + ver_strs = + versions_list + |> Enum.map(fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_strs} {:ok, body} -> @@ -179,15 +193,16 @@ defmodule Opsm.Registries.Grafana do homepage: body["url"] || "https://grafana.com/grafana/plugins/#{name}", repository: body["links"]["source"] || body["sourceUrl"], authors: extract_authors(body), - keywords: [plugin_type | (body["keywords"] || [])], + keywords: [plugin_type | body["keywords"] || []], dependencies: deps, dev_dependencies: %{}, source_forth: :grafana, raw_manifest: body } - download_url = body["downloadUrl"] || - "https://grafana.com/api/plugins/#{name}/versions/#{version}/download" + download_url = + body["downloadUrl"] || + "https://grafana.com/api/plugins/#{name}/versions/#{version}/download" %ResolvedPackage{ package: name, @@ -204,17 +219,19 @@ defmodule Opsm.Registries.Grafana do end defp extract_dependencies(body) do - grafana_dep = case body["grafanaDependency"] || body["grafanaVersion"] do - nil -> %{} - constraint -> %{"grafana" => constraint} - end - - plugin_deps = (body["dependencies"] || body["pluginDependencies"] || []) - |> Enum.reduce(grafana_dep, fn - %{"id" => id, "version" => ver}, acc -> Map.put(acc, id, ver) - %{"id" => id}, acc -> Map.put(acc, id, "*") - _, acc -> acc - end) + grafana_dep = + case body["grafanaDependency"] || body["grafanaVersion"] do + nil -> %{} + constraint -> %{"grafana" => constraint} + end + + plugin_deps = + (body["dependencies"] || body["pluginDependencies"] || []) + |> Enum.reduce(grafana_dep, fn + %{"id" => id, "version" => ver}, acc -> Map.put(acc, id, ver) + %{"id" => id}, acc -> Map.put(acc, id, "*") + _, acc -> acc + end) plugin_deps end diff --git a/opsm_ex/lib/opsm/registries/guix.ex b/opsm_ex/lib/opsm/registries/guix.ex index cd6567bc..df855533 100644 --- a/opsm_ex/lib/opsm/registries/guix.ex +++ b/opsm_ex/lib/opsm/registries/guix.ex @@ -17,25 +17,37 @@ defmodule Opsm.Registries.Guix do """ def fetch_package(name, version \\ "latest") do url = "#{@api_url}/packages?name=#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 15_000) do {:ok, body} when is_list(body) -> case body do - [] -> {:error, :not_found} + [] -> + {:error, :not_found} + packages -> - pkg = if version == "latest" do - List.first(packages) - else - Enum.find(packages, fn p -> p["version"] == version end) || - List.first(packages) - end + pkg = + if version == "latest" do + List.first(packages) + else + Enum.find(packages, fn p -> p["version"] == version end) || + List.first(packages) + end + ver = if version == "latest", do: pkg["version"], else: version {:ok, parse_guix_package(name, pkg, ver)} end - {:ok, %{"error" => _}} -> {:error, :not_found} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:ok, %{"error" => _}} -> + {:error, :not_found} + + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -57,7 +69,7 @@ defmodule Opsm.Registries.Guix do manifest: manifest, tarball_url: nil, checksum: nil, - attestations: [], + attestations: [] } end @@ -66,15 +78,19 @@ defmodule Opsm.Registries.Guix do """ def get_versions(name) do url = "#{@api_url}/packages?name=#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 15_000) do {:ok, body} when is_list(body) -> - versions = body - |> Enum.map(fn p -> p["version"] end) - |> Enum.reject(&is_nil/1) - |> Enum.uniq() + versions = + body + |> Enum.map(fn p -> p["version"] end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + {:ok, versions} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -83,17 +99,23 @@ defmodule Opsm.Registries.Guix do """ def search(query, _opts \\ []) do url = "#{@api_url}/packages?search=#{URI.encode(query)}" + case VerifiedHttp.get_json(url, receive_timeout: 15_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn p -> - %{name: p["name"], version: p["version"], description: p["synopsis"]} - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn p -> + %{name: p["name"], version: p["version"], description: p["synopsis"]} + end) + {:ok, results} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/hackage.ex b/opsm_ex/lib/opsm/registries/hackage.ex index 2f3ac907..cab1fe9b 100644 --- a/opsm_ex/lib/opsm/registries/hackage.ex +++ b/opsm_ex/lib/opsm/registries/hackage.ex @@ -32,16 +32,19 @@ defmodule Opsm.Registries.Hackage do # body is a map of version => {normal-version, ...} all_versions = Map.keys(body) |> Enum.sort(:desc) - target_version = if version == "latest" do - List.first(all_versions) - else - version - end + target_version = + if version == "latest" do + List.first(all_versions) + else + version + end # Fetch cabal metadata for the specific version - {description, license, deps, homepage, authors} = fetch_cabal_metadata(name, target_version) + {description, license, deps, homepage, authors} = + fetch_cabal_metadata(name, target_version) - {:ok, parse_package(name, target_version, description, license, deps, homepage, authors, body)} + {:ok, + parse_package(name, target_version, description, license, deps, homepage, authors, body)} {:error, :not_found} -> {:error, :not_found} @@ -63,8 +66,10 @@ defmodule Opsm.Registries.Hackage do case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: body}} when is_binary(body) -> parse_cabal(body) + {:ok, body} when is_binary(body) -> parse_cabal(body) + _ -> {nil, nil, %{}, nil, []} end @@ -91,6 +96,7 @@ defmodule Opsm.Registries.Hackage do Enum.find_value(lines, fn line -> lower = String.downcase(String.trim(line)) + if String.starts_with?(lower, pattern) do line |> String.trim() @@ -147,22 +153,23 @@ defmodule Opsm.Registries.Hackage do dep = String.trim(dep) # Split on first space, or on first constraint operator character - {name, constraint} = cond do - String.contains?(dep, " ") -> - case String.split(dep, " ", parts: 2) do - [n, c] -> {String.trim(n), String.trim(c)} - [n] -> {String.trim(n), ">= 0"} - end - - Regex.match?(~r/[><=]/, dep) -> - case Regex.split(~r/(?=[><=])/, dep, parts: 2) do - [n, c] -> {String.trim(n), String.trim(c)} - [n] -> {String.trim(n), ">= 0"} - end - - true -> - {dep, ">= 0"} - end + {name, constraint} = + cond do + String.contains?(dep, " ") -> + case String.split(dep, " ", parts: 2) do + [n, c] -> {String.trim(n), String.trim(c)} + [n] -> {String.trim(n), ">= 0"} + end + + Regex.match?(~r/[><=]/, dep) -> + case Regex.split(~r/(?=[><=])/, dep, parts: 2) do + [n, c] -> {String.trim(n), String.trim(c)} + [n] -> {String.trim(n), ">= 0"} + end + + true -> + {dep, ">= 0"} + end {name, constraint} end) @@ -179,7 +186,8 @@ defmodule Opsm.Registries.Hackage do case VerifiedHttp.get_json(url, headers: headers, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body + results = + body |> Enum.take(limit) |> Enum.map(fn pkg -> %{ @@ -189,6 +197,7 @@ defmodule Opsm.Registries.Hackage do downloads: 0 } end) + {:ok, results} {:ok, _} -> diff --git a/opsm_ex/lib/opsm/registries/helm.ex b/opsm_ex/lib/opsm/registries/helm.ex index fe53bb25..d037a5e8 100644 --- a/opsm_ex/lib/opsm/registries/helm.ex +++ b/opsm_ex/lib/opsm/registries/helm.ex @@ -20,11 +20,12 @@ defmodule Opsm.Registries.Helm do def fetch_package(name, version \\ "latest") do {repo_name, chart_name} = split_chart_name(name) - url = if version == "latest" do - "#{@api_url}/packages/helm/#{repo_name}/#{URI.encode(chart_name)}" - else - "#{@api_url}/packages/helm/#{repo_name}/#{URI.encode(chart_name)}/#{version}" - end + url = + if version == "latest" do + "#{@api_url}/packages/helm/#{repo_name}/#{URI.encode(chart_name)}" + else + "#{@api_url}/packages/helm/#{repo_name}/#{URI.encode(chart_name)}/#{version}" + end case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> @@ -53,23 +54,28 @@ defmodule Opsm.Registries.Helm do limit = Keyword.get(opts, :limit, 20) offset = Keyword.get(opts, :offset, 0) - url = "#{@api_url}/packages/search?" <> - "ts_query_web=#{URI.encode_www_form(query)}" <> - "&kind=0" <> - "&limit=#{limit}" <> - "&offset=#{offset}" + url = + "#{@api_url}/packages/search?" <> + "ts_query_web=#{URI.encode_www_form(query)}" <> + "&kind=0" <> + "&limit=#{limit}" <> + "&offset=#{offset}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"packages" => packages}} when is_list(packages) -> - results = packages - |> Enum.take(limit) - |> Enum.map(&parse_search_result/1) + results = + packages + |> Enum.take(limit) + |> Enum.map(&parse_search_result/1) + {:ok, results} {:ok, packages} when is_list(packages) -> - results = packages - |> Enum.take(limit) - |> Enum.map(&parse_search_result/1) + results = + packages + |> Enum.take(limit) + |> Enum.map(&parse_search_result/1) + {:ok, results} {:ok, _} -> @@ -105,15 +111,17 @@ defmodule Opsm.Registries.Helm do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"available_versions" => versions}} when is_list(versions) -> - ver_list = versions - |> Enum.map(fn v -> - case v do - %{"version" => ver} -> ver - ver when is_binary(ver) -> ver - _ -> nil - end - end) - |> Enum.reject(&is_nil/1) + ver_list = + versions + |> Enum.map(fn v -> + case v do + %{"version" => ver} -> ver + ver when is_binary(ver) -> ver + _ -> nil + end + end) + |> Enum.reject(&is_nil/1) + {:ok, ver_list} {:ok, %{"version" => ver}} -> @@ -229,11 +237,15 @@ defmodule Opsm.Registries.Helm do defp extract_repository(body) do case body do - %{"repository" => %{"url" => url}} -> url + %{"repository" => %{"url" => url}} -> + url + %{"links" => links} when is_list(links) -> source = Enum.find(links, fn l -> l["name"] == "source" end) if source, do: source["url"], else: nil - _ -> nil + + _ -> + nil end end diff --git a/opsm_ex/lib/opsm/registries/hex.ex b/opsm_ex/lib/opsm/registries/hex.ex index 00545368..02061383 100644 --- a/opsm_ex/lib/opsm/registries/hex.ex +++ b/opsm_ex/lib/opsm/registries/hex.ex @@ -22,20 +22,21 @@ defmodule Opsm.Registries.Hex do {:ok, body} -> releases = body["releases"] || [] - target_version = if version == "latest" do - case releases do - [latest | _] -> latest["version"] - [] -> nil + target_version = + if version == "latest" do + case releases do + [latest | _] -> latest["version"] + [] -> nil + end + else + version end - else - version - end # Fetch release-specific data to get dependencies and checksum {deps, release_checksum} = fetch_release_data(name, target_version) release = Enum.find(releases, fn r -> r["version"] == target_version end) # Prefer checksum from release-specific endpoint (outer_checksum/checksum) - checksum = release_checksum || (if release, do: release["checksum"], else: nil) + checksum = release_checksum || if release, do: release["checksum"], else: nil {:ok, parse_package(body, checksum, target_version, deps)} {:error, :not_found} -> @@ -59,14 +60,17 @@ defmodule Opsm.Registries.Hex do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> requirements = body["requirements"] || %{} - deps = Enum.reduce(requirements, %{}, fn {dep_name, req}, acc -> - constraint = req["requirement"] || ">= 0.0.0" - if req["optional"] do - acc - else - Map.put(acc, dep_name, constraint) - end - end) + + deps = + Enum.reduce(requirements, %{}, fn {dep_name, req}, acc -> + constraint = req["requirement"] || ">= 0.0.0" + + if req["optional"] do + acc + else + Map.put(acc, dep_name, constraint) + end + end) # Extract checksum — Hex uses "checksum" (outer tarball checksum) checksum = body["checksum"] @@ -159,7 +163,8 @@ defmodule Opsm.Registries.Hex do description: meta["description"], license: extract_licenses(meta["licenses"]), homepage: meta["links"]["Homepage"] || meta["links"]["homepage"], - repository: meta["links"]["GitHub"] || meta["links"]["github"] || meta["links"]["Repository"], + repository: + meta["links"]["GitHub"] || meta["links"]["github"] || meta["links"]["Repository"], authors: meta["maintainers"] || [], keywords: [], dependencies: deps, diff --git a/opsm_ex/lib/opsm/registries/homebrew.ex b/opsm_ex/lib/opsm/registries/homebrew.ex index 3b51355a..d01276e8 100644 --- a/opsm_ex/lib/opsm/registries/homebrew.ex +++ b/opsm_ex/lib/opsm/registries/homebrew.ex @@ -27,38 +27,55 @@ defmodule Opsm.Registries.Homebrew do defp fetch_formula(name, version) do url = "#{@formula_api}/#{name}.json" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: get_in(body, ["versions", "stable"]), - else: version + ver = + if version == "latest", + do: get_in(body, ["versions", "stable"]), + else: version + {:ok, parse_formula(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp fetch_cask(name, version) do url = "#{@cask_api}/#{name}.json" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: body["version"], - else: version + ver = + if version == "latest", + do: body["version"], + else: version + {:ok, parse_cask(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp parse_formula(name, body, version) do - deps = (body["dependencies"] || []) - |> Enum.map(fn d -> {d, "*"} end) - |> Map.new() + deps = + (body["dependencies"] || []) + |> Enum.map(fn d -> {d, "*"} end) + |> Map.new() manifest = %ManifestFormat{ name: name, @@ -77,7 +94,7 @@ defmodule Opsm.Registries.Homebrew do manifest: manifest, tarball_url: get_in(body, ["urls", "stable", "url"]), checksum: nil, - attestations: [], + attestations: [] } end @@ -92,10 +109,11 @@ defmodule Opsm.Registries.Homebrew do dependencies: %{} } - url = case body["url"] do - url when is_binary(url) -> url - _ -> nil - end + url = + case body["url"] do + url when is_binary(url) -> url + _ -> nil + end %ResolvedPackage{ package: name, @@ -105,7 +123,7 @@ defmodule Opsm.Registries.Homebrew do tarball_url: url, checksum: body["sha256"], checksum_algo: if(body["sha256"], do: :sha256), - attestations: [], + attestations: [] } end @@ -124,21 +142,27 @@ defmodule Opsm.Registries.Homebrew do """ def search(query, _opts \\ []) do url = "#{@formula_api}.json" + case VerifiedHttp.get_json(url, receive_timeout: 30_000) do {:ok, formulae} when is_list(formulae) -> - matches = formulae - |> Enum.filter(fn f -> - name = f["name"] || "" - desc = f["desc"] || "" - String.contains?(String.downcase(name), String.downcase(query)) or - String.contains?(String.downcase(desc), String.downcase(query)) - end) - |> Enum.take(20) - |> Enum.map(fn f -> %{name: f["name"], version: get_in(f, ["versions", "stable"]), description: f["desc"]} end) + matches = + formulae + |> Enum.filter(fn f -> + name = f["name"] || "" + desc = f["desc"] || "" + + String.contains?(String.downcase(name), String.downcase(query)) or + String.contains?(String.downcase(desc), String.downcase(query)) + end) + |> Enum.take(20) + |> Enum.map(fn f -> + %{name: f["name"], version: get_in(f, ["versions", "stable"]), description: f["desc"]} + end) {:ok, matches} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/homebrew_cask.ex b/opsm_ex/lib/opsm/registries/homebrew_cask.ex index e18f7023..9a90147a 100644 --- a/opsm_ex/lib/opsm/registries/homebrew_cask.ex +++ b/opsm_ex/lib/opsm/registries/homebrew_cask.ex @@ -22,14 +22,21 @@ defmodule Opsm.Registries.HomebrewCask do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: body["version"], - else: version + ver = + if version == "latest", + do: body["version"], + else: version + {:ok, parse_cask(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -42,14 +49,16 @@ defmodule Opsm.Registries.HomebrewCask do description: build_description(body), license: nil, homepage: body["homepage"], - repository: "https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/#{cask_dir_prefix(name)}/#{name}.rb", + repository: + "https://github.com/Homebrew/homebrew-cask/blob/HEAD/Casks/#{cask_dir_prefix(name)}/#{name}.rb", dependencies: deps } - download_url = case body["url"] do - url when is_binary(url) -> url - _ -> nil - end + download_url = + case body["url"] do + url when is_binary(url) -> url + _ -> nil + end %ResolvedPackage{ package: name, @@ -59,19 +68,22 @@ defmodule Opsm.Registries.HomebrewCask do tarball_url: download_url, checksum: body["sha256"], checksum_algo: if(body["sha256"] && body["sha256"] != "no_check", do: :sha256), - attestations: [], + attestations: [] } end defp build_description(body) do _name_parts = [body["name"] | List.wrap(nil)] - primary_name = case body["name"] do - names when is_list(names) -> List.first(names) - name when is_binary(name) -> name - _ -> nil - end + + primary_name = + case body["name"] do + names when is_list(names) -> List.first(names) + name when is_binary(name) -> name + _ -> nil + end desc = body["desc"] + case {primary_name, desc} do {nil, nil} -> nil {nil, d} -> d @@ -87,11 +99,13 @@ defmodule Opsm.Registries.HomebrewCask do defp extract_cask_dependencies(body) do deps = body["depends_on"] || %{} - formula_deps = (deps["formula"] || []) - |> Enum.map(fn d -> {d, "*"} end) + formula_deps = + (deps["formula"] || []) + |> Enum.map(fn d -> {d, "*"} end) - cask_deps = (deps["cask"] || []) - |> Enum.map(fn d -> {d, "*"} end) + cask_deps = + (deps["cask"] || []) + |> Enum.map(fn d -> {d, "*"} end) (formula_deps ++ cask_deps) |> Map.new() end @@ -118,30 +132,34 @@ defmodule Opsm.Registries.HomebrewCask do {:ok, casks} when is_list(casks) -> query_down = String.downcase(query) - results = casks - |> Enum.filter(fn cask -> - token = String.downcase(cask["token"] || "") - desc = String.downcase(cask["desc"] || "") - names = (cask["name"] || []) - |> List.wrap() - |> Enum.map(&String.downcase/1) - - String.contains?(token, query_down) or - String.contains?(desc, query_down) or - Enum.any?(names, &String.contains?(&1, query_down)) - end) - |> Enum.take(20) - |> Enum.map(fn cask -> - %{ - name: cask["token"], - version: cask["version"], - description: cask["desc"] - } - end) + results = + casks + |> Enum.filter(fn cask -> + token = String.downcase(cask["token"] || "") + desc = String.downcase(cask["desc"] || "") + + names = + (cask["name"] || []) + |> List.wrap() + |> Enum.map(&String.downcase/1) + + String.contains?(token, query_down) or + String.contains?(desc, query_down) or + Enum.any?(names, &String.contains?(&1, query_down)) + end) + |> Enum.take(20) + |> Enum.map(fn cask -> + %{ + name: cask["token"], + version: cask["version"], + description: cask["desc"] + } + end) {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/hyperpolymath_forge.ex b/opsm_ex/lib/opsm/registries/hyperpolymath_forge.ex index 19776a75..a4110cbf 100644 --- a/opsm_ex/lib/opsm/registries/hyperpolymath_forge.ex +++ b/opsm_ex/lib/opsm/registries/hyperpolymath_forge.ex @@ -42,7 +42,8 @@ defmodule Opsm.Registries.HyperpPolymathForge do @github_org "hyperpolymath" @github_api "https://api.github.com" @cache_table :hfr_cache - @cache_ttl_ms 30 * 60 * 1_000 # 30 minutes + # 30 minutes + @cache_ttl_ms 30 * 60 * 1_000 @per_page 100 @opsm_manifest_name "opsm.toml" @@ -58,6 +59,7 @@ defmodule Opsm.Registries.HyperpPolymathForge do case :ets.info(@cache_table) do :undefined -> :ets.new(@cache_table, [:named_table, :public, :set, read_concurrency: true]) + _ -> :ok end @@ -149,6 +151,7 @@ defmodule Opsm.Registries.HyperpPolymathForge do """ def exists?(name) do ensure_index() + case lookup_cached(name) do {:hit, _} -> true :miss -> false @@ -186,6 +189,7 @@ defmodule Opsm.Registries.HyperpPolymathForge do if :ets.info(@cache_table, :size) == 0 do refresh_index() end + :ok end @@ -244,7 +248,9 @@ defmodule Opsm.Registries.HyperpPolymathForge do defp index_repo(repo) do repo_name = repo["name"] default_branch = repo["default_branch"] || "main" - raw_url = "https://raw.githubusercontent.com/#{@github_org}/#{repo_name}/#{default_branch}/#{@opsm_manifest_name}" + + raw_url = + "https://raw.githubusercontent.com/#{@github_org}/#{repo_name}/#{default_branch}/#{@opsm_manifest_name}" case VerifiedHttp.get(raw_url, headers: github_headers(), receive_timeout: 5_000) do {:ok, %{status: 200, body: toml_text}} -> @@ -291,8 +297,7 @@ defmodule Opsm.Registries.HyperpPolymathForge do defp github_headers do token = System.get_env("GITHUB_TOKEN") || Application.get_env(:opsm, :github_token, nil) - base = [{"Accept", "application/vnd.github+json"}, - {"X-GitHub-Api-Version", "2022-11-28"}] + base = [{"Accept", "application/vnd.github+json"}, {"X-GitHub-Api-Version", "2022-11-28"}] if token do [{"Authorization", "Bearer #{token}"} | base] @@ -335,14 +340,17 @@ defmodule Opsm.Registries.HyperpPolymathForge do defp resolve_package(pkg_info, version) do repo_name = pkg_info[:repo_name] - branch = if version in ["latest", "main"], do: pkg_info[:default_branch] || "main", else: version + + branch = + if version in ["latest", "main"], do: pkg_info[:default_branch] || "main", else: version pkg = %ResolvedPackage{ package: pkg_info[:pkg_name], version: version, forth: pkg_info[:forth] || :hyperpolymath, registry_url: pkg_info[:repo_url], - tarball_url: "https://github.com/#{@github_org}/#{repo_name}/archive/refs/heads/#{branch}.tar.gz", + tarball_url: + "https://github.com/#{@github_org}/#{repo_name}/archive/refs/heads/#{branch}.tar.gz", checksum: nil, checksum_algo: :sha256, manifest: %ManifestFormat{ @@ -372,7 +380,8 @@ defmodule Opsm.Registries.HyperpPolymathForge do version: entry[:version] || version, forth: entry[:forth] || :hyperpolymath, registry_url: entry[:repo_url], - tarball_url: "https://github.com/#{@github_org}/#{entry[:repo_name]}/archive/refs/heads/main.tar.gz", + tarball_url: + "https://github.com/#{@github_org}/#{entry[:repo_name]}/archive/refs/heads/main.tar.gz", checksum: nil, checksum_algo: :sha256, manifest: %ManifestFormat{ @@ -482,6 +491,7 @@ defmodule Opsm.Registries.HyperpPolymathForge do defp default_authors, do: ["Jonathan D.A. Jewell "] defp safe_to_atom(nil), do: nil + defp safe_to_atom(str) when is_binary(str) do # Fallback to a string if the atom doesn't exist to prevent atom exhaustion. # The caller must be prepared to handle strings. @@ -491,5 +501,6 @@ defmodule Opsm.Registries.HyperpPolymathForge do ArgumentError -> str end end + defp safe_to_atom(atom) when is_atom(atom), do: atom end diff --git a/opsm_ex/lib/opsm/registries/idris2.ex b/opsm_ex/lib/opsm/registries/idris2.ex index 1b02bf83..a6de7e4b 100644 --- a/opsm_ex/lib/opsm/registries/idris2.ex +++ b/opsm_ex/lib/opsm/registries/idris2.ex @@ -228,24 +228,28 @@ defmodule Opsm.Registries.Idris2 do end defp fetch_from_git(pkg_info, version) do - opts = if Map.has_key?(pkg_info, :path) do - [subpath: pkg_info.path] - else - [] - end + opts = + if Map.has_key?(pkg_info, :path) do + [subpath: pkg_info.path] + else + [] + end case Git.fetch_package(pkg_info.url, version, opts) do {:ok, git_pkg} -> # Enhance with Idris2-specific metadata - enhanced = %{git_pkg | - metadata: Map.merge(git_pkg.metadata, %{ - "registry" => "idris2-curated", - "language" => "idris2", - "manifest_format" => "ipkg", - "build_system" => "idris2", - "subpath" => Map.get(pkg_info, :path) - }) + enhanced = %{ + git_pkg + | metadata: + Map.merge(git_pkg.metadata, %{ + "registry" => "idris2-curated", + "language" => "idris2", + "manifest_format" => "ipkg", + "build_system" => "idris2", + "subpath" => Map.get(pkg_info, :path) + }) } + {:ok, enhanced} {:error, reason} -> diff --git a/opsm_ex/lib/opsm/registries/jetbrains.ex b/opsm_ex/lib/opsm/registries/jetbrains.ex index 247c0f83..85da42fd 100644 --- a/opsm_ex/lib/opsm/registries/jetbrains.ex +++ b/opsm_ex/lib/opsm/registries/jetbrains.ex @@ -22,22 +22,31 @@ defmodule Opsm.Registries.Jetbrains do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest" do - extract_latest_version(body) - else - version - end + ver = + if version == "latest" do + extract_latest_version(body) + else + version + end + {:ok, parse_plugin(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp extract_latest_version(body) do case body["updates"] || body["releases"] do - [latest | _] -> latest["version"] || "0.0.0" + [latest | _] -> + latest["version"] || "0.0.0" + _ -> case body["version"] do nil -> "0.0.0" @@ -50,23 +59,25 @@ defmodule Opsm.Registries.Jetbrains do plugin_id = body["id"] || name xml_id = body["xmlId"] || body["pluginId"] || to_string(name) - deps = (body["dependencies"] || []) - |> Enum.map(fn - d when is_binary(d) -> {d, "*"} - d when is_map(d) -> {d["xmlId"] || d["id"] || "", d["version"] || "*"} - _ -> nil - end) - |> Enum.reject(&is_nil/1) - |> Enum.reject(fn {n, _} -> n == "" end) - |> Map.new() - - compatible_products = (body["compatibleProducts"] || body["products"] || []) - |> Enum.map(fn - p when is_binary(p) -> p - p when is_map(p) -> p["code"] || p["name"] - _ -> nil - end) - |> Enum.reject(&is_nil/1) + deps = + (body["dependencies"] || []) + |> Enum.map(fn + d when is_binary(d) -> {d, "*"} + d when is_map(d) -> {d["xmlId"] || d["id"] || "", d["version"] || "*"} + _ -> nil + end) + |> Enum.reject(&is_nil/1) + |> Enum.reject(fn {n, _} -> n == "" end) + |> Map.new() + + compatible_products = + (body["compatibleProducts"] || body["products"] || []) + |> Enum.map(fn + p when is_binary(p) -> p + p when is_map(p) -> p["code"] || p["name"] + _ -> nil + end) + |> Enum.reject(&is_nil/1) description = build_description(body, compatible_products) @@ -80,10 +91,14 @@ defmodule Opsm.Registries.Jetbrains do dependencies: deps } - download_url = case body["updates"] || body["releases"] do - [_latest | _] -> "#{@marketplace_url}/plugin/download?pluginId=#{plugin_id}&version=#{version}" - _ -> nil - end + download_url = + case body["updates"] || body["releases"] do + [_latest | _] -> + "#{@marketplace_url}/plugin/download?pluginId=#{plugin_id}&version=#{version}" + + _ -> + nil + end %ResolvedPackage{ package: to_string(name), @@ -92,17 +107,20 @@ defmodule Opsm.Registries.Jetbrains do manifest: manifest, tarball_url: download_url, checksum: nil, - attestations: [], + attestations: [] } end defp build_description(body, compatible_products) do base = body["description"] || body["preview"] || body["name"] - products_str = if compatible_products != [] do - " (#{Enum.join(compatible_products, ", ")})" - else - "" - end + + products_str = + if compatible_products != [] do + " (#{Enum.join(compatible_products, ", ")})" + else + "" + end + "#{base}#{products_str}" end @@ -118,10 +136,12 @@ defmodule Opsm.Registries.Jetbrains do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - versions_list = body - |> Enum.map(fn u -> u["version"] end) - |> Enum.reject(&is_nil/1) - |> Enum.uniq() + versions_list = + body + |> Enum.map(fn u -> u["version"] end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + {:ok, versions_list} {:ok, _} -> @@ -130,7 +150,8 @@ defmodule Opsm.Registries.Jetbrains do {:error, _} = err -> err end - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -142,31 +163,38 @@ defmodule Opsm.Registries.Jetbrains do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn plugin -> - %{ - name: plugin["xmlId"] || plugin["name"] || to_string(plugin["id"]), - version: extract_latest_version(plugin), - description: plugin["preview"] || plugin["name"] - } - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn plugin -> + %{ + name: plugin["xmlId"] || plugin["name"] || to_string(plugin["id"]), + version: extract_latest_version(plugin), + description: plugin["preview"] || plugin["name"] + } + end) + {:ok, results} {:ok, %{"plugins" => plugins}} when is_list(plugins) -> - results = plugins - |> Enum.take(20) - |> Enum.map(fn plugin -> - %{ - name: plugin["xmlId"] || plugin["name"] || to_string(plugin["id"]), - version: nil, - description: plugin["preview"] || plugin["name"] - } - end) + results = + plugins + |> Enum.take(20) + |> Enum.map(fn plugin -> + %{ + name: plugin["xmlId"] || plugin["name"] || to_string(plugin["id"]), + version: nil, + description: plugin["preview"] || plugin["name"] + } + end) + {:ok, results} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/jsdelivr.ex b/opsm_ex/lib/opsm/registries/jsdelivr.ex index de6f90a6..ea0b977a 100644 --- a/opsm_ex/lib/opsm/registries/jsdelivr.ex +++ b/opsm_ex/lib/opsm/registries/jsdelivr.ex @@ -22,11 +22,12 @@ defmodule Opsm.Registries.JsDelivr do def fetch_package(name, version \\ "latest") do {source, pkg_name} = parse_source(name) - target_version = if version == "latest" do - fetch_latest_version(source, pkg_name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(source, pkg_name) + else + version + end case target_version do nil -> @@ -74,7 +75,8 @@ defmodule Opsm.Registries.JsDelivr do # jsDelivr does not have a native search API; proxy through npm search # and augment with jsDelivr stats - url = "https://registry.npmjs.org/-/v1/search?text=#{URI.encode_www_form(query)}&size=#{limit}" + url = + "https://registry.npmjs.org/-/v1/search?text=#{URI.encode_www_form(query)}&size=#{limit}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"objects" => objects}} when is_list(objects) -> diff --git a/opsm_ex/lib/opsm/registries/jsr.ex b/opsm_ex/lib/opsm/registries/jsr.ex index 1dc8b7ca..abb9d095 100644 --- a/opsm_ex/lib/opsm/registries/jsr.ex +++ b/opsm_ex/lib/opsm/registries/jsr.ex @@ -23,11 +23,12 @@ defmodule Opsm.Registries.Jsr do {:error, reason} {:ok, {scope, pkg_name}} -> - target_version = if version == "latest" do - fetch_latest_version(scope, pkg_name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(scope, pkg_name) + else + version + end case target_version do nil -> @@ -35,6 +36,7 @@ defmodule Opsm.Registries.Jsr do ver -> url = "#{@api_url}/scopes/#{scope}/packages/#{pkg_name}/versions/#{ver}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> {:ok, parse_package(name, scope, pkg_name, body, ver)} @@ -71,34 +73,38 @@ defmodule Opsm.Registries.Jsr do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"items" => items}} when is_list(items) -> - results = Enum.map(items, fn item -> - scope = item["scope"] - name = item["name"] - full_name = "@#{scope}/#{name}" - - %{ - name: full_name, - version: item["latestVersion"], - description: item["description"] || "", - downloads: item["score"] || 0 - } - end) + results = + Enum.map(items, fn item -> + scope = item["scope"] + name = item["name"] + full_name = "@#{scope}/#{name}" + + %{ + name: full_name, + version: item["latestVersion"], + description: item["description"] || "", + downloads: item["score"] || 0 + } + end) + {:ok, results} {:ok, items} when is_list(items) -> # Alternative response format - results = Enum.map(items, fn item -> - scope = item["scope"] - name = item["name"] - full_name = "@#{scope}/#{name}" - - %{ - name: full_name, - version: item["latestVersion"], - description: item["description"] || "", - downloads: item["score"] || 0 - } - end) + results = + Enum.map(items, fn item -> + scope = item["scope"] + name = item["name"] + full_name = "@#{scope}/#{name}" + + %{ + name: full_name, + version: item["latestVersion"], + description: item["description"] || "", + downloads: item["score"] || 0 + } + end) + {:ok, results} {:ok, _} -> @@ -119,6 +125,7 @@ defmodule Opsm.Registries.Jsr do {:ok, {scope, pkg_name}} -> url = "#{@api_url}/scopes/#{scope}/packages/#{pkg_name}/versions" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -188,26 +195,31 @@ defmodule Opsm.Registries.Jsr do {:error, "Invalid scoped package name format. Expected @scope/name"} end end + defp parse_scoped_name(_), do: {:error, "JSR packages must be scoped (start with @)"} # Parsers defp parse_package(full_name, scope, pkg_name, metadata, version) do # Extract dependencies from metadata - deps = case metadata do - %{"dependencies" => deps_map} when is_map(deps_map) -> - Map.new(deps_map, fn {dep, ver} -> {dep, ver} end) - _ -> - %{} - end + deps = + case metadata do + %{"dependencies" => deps_map} when is_map(deps_map) -> + Map.new(deps_map, fn {dep, ver} -> {dep, ver} end) + + _ -> + %{} + end # Extract checksum if available - checksum = case metadata do - %{"integrity" => integrity} when is_binary(integrity) -> - parse_integrity_hash(integrity) - _ -> - nil - end + checksum = + case metadata do + %{"integrity" => integrity} when is_binary(integrity) -> + parse_integrity_hash(integrity) + + _ -> + nil + end %ResolvedPackage{ package: full_name, @@ -240,8 +252,10 @@ defmodule Opsm.Registries.Jsr do case metadata do %{"author" => author} when is_binary(author) -> [author] + %{"authors" => authors} when is_list(authors) -> authors + _ -> [] end @@ -251,11 +265,13 @@ defmodule Opsm.Registries.Jsr do # Convert to hex-encoded SHA256 defp parse_integrity_hash("sha256-" <> b64_hash) do b64_clean = String.trim_trailing(b64_hash, "=") |> pad_base64() + case Base.decode64(b64_clean) do {:ok, raw_hash} -> Base.encode16(raw_hash, case: :lower) :error -> nil end end + defp parse_integrity_hash(_), do: nil defp pad_base64(s) do diff --git a/opsm_ex/lib/opsm/registries/julia_general.ex b/opsm_ex/lib/opsm/registries/julia_general.ex index 9c0db060..a8b8b6d0 100644 --- a/opsm_ex/lib/opsm/registries/julia_general.ex +++ b/opsm_ex/lib/opsm/registries/julia_general.ex @@ -15,65 +15,79 @@ defmodule Opsm.Registries.JuliaGeneral do def fetch_package(name, version \\ "latest") do url = "#{@api_url}/info?name=#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest" do - body["version"] || "0.0.0" - else - version - end + ver = + if version == "latest" do + body["version"] || "0.0.0" + else + version + end deps = parse_deps(body["deps"] || []) - {:ok, %ResolvedPackage{ - package: name, - version: ver, - forth: :julia, - registry_url: "#{@web_url}/#{name}", - tarball_url: nil, - checksum: nil, - checksum_algo: nil, - manifest: %ManifestFormat{ - name: name, - version: ver, - description: body["description"], - license: body["license"], - homepage: body["homepage"] || "#{@web_url}/#{name}", - repository: body["repo"], - authors: body["authors"] || [], - keywords: [], - dependencies: deps, - dev_dependencies: %{}, - source_forth: :julia, - raw_manifest: body - }, - attestations: [], - resolved_deps: [] - }} - - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:ok, + %ResolvedPackage{ + package: name, + version: ver, + forth: :julia, + registry_url: "#{@web_url}/#{name}", + tarball_url: nil, + checksum: nil, + checksum_algo: nil, + manifest: %ManifestFormat{ + name: name, + version: ver, + description: body["description"], + license: body["license"], + homepage: body["homepage"] || "#{@web_url}/#{name}", + repository: body["repo"], + authors: body["authors"] || [], + keywords: [], + dependencies: deps, + dev_dependencies: %{}, + source_forth: :julia, + raw_manifest: body + }, + attestations: [], + resolved_deps: [] + }} + + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end def search(query, opts \\ []) do limit = Keyword.get(opts, :limit, 20) url = "#{@api_url}/search?q=#{URI.encode(query)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, results} when is_list(results) -> - packages = results + packages = + results |> Enum.take(limit) |> Enum.map(fn r -> %{name: r["name"], version: r["version"], description: r["description"], downloads: 0} end) + {:ok, packages} - _ -> {:ok, []} + + _ -> + {:ok, []} end end def exists?(name) do url = "#{@api_url}/info?name=#{URI.encode(name)}" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -82,13 +96,17 @@ defmodule Opsm.Registries.JuliaGeneral do def versions(name) do url = "#{@api_url}/info?name=#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions}} when is_list(versions) -> vers = Enum.map(versions, fn v -> v["version"] || v end) |> Enum.reject(&is_nil/1) {:ok, vers} + {:ok, %{"version" => ver}} when is_binary(ver) -> {:ok, [ver]} - _ -> {:error, :not_found} + + _ -> + {:error, :not_found} end end @@ -102,5 +120,6 @@ defmodule Opsm.Registries.JuliaGeneral do end) |> Map.delete("") end + defp parse_deps(_), do: %{} end diff --git a/opsm_ex/lib/opsm/registries/julia_the_viper.ex b/opsm_ex/lib/opsm/registries/julia_the_viper.ex index 48ea22d9..e477c21c 100644 --- a/opsm_ex/lib/opsm/registries/julia_the_viper.ex +++ b/opsm_ex/lib/opsm/registries/julia_the_viper.ex @@ -17,7 +17,8 @@ defmodule Opsm.Registries.JuliaTheViper do alias Opsm.Verified.Http, as: VerifiedHttp @base_url "https://packages.julia-the-viper.dev/api/v1" - @fallback_mode :git # Until registry deployed + # Until registry deployed + @fallback_mode :git @doc """ Fetch package metadata from julia-the-viper registry. @@ -93,6 +94,7 @@ defmodule Opsm.Registries.JuliaTheViper do defp registry_exists?(name) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -150,17 +152,19 @@ defmodule Opsm.Registries.JuliaTheViper do defp git_versions(_name) do # For git mode, versions are git tags - {:ok, ["main", "master"]} # Minimal fallback + # Minimal fallback + {:ok, ["main", "master"]} end defp git_fetch(repo_url, version) do # julia-the-viper uses Viper.toml for manifest - manifest_url = case version do - "latest" -> "#{repo_url}/raw/main/Viper.toml" - "main" -> "#{repo_url}/raw/main/Viper.toml" - "master" -> "#{repo_url}/raw/master/Viper.toml" - tag -> "#{repo_url}/raw/#{tag}/Viper.toml" - end + manifest_url = + case version do + "latest" -> "#{repo_url}/raw/main/Viper.toml" + "main" -> "#{repo_url}/raw/main/Viper.toml" + "master" -> "#{repo_url}/raw/master/Viper.toml" + tag -> "#{repo_url}/raw/#{tag}/Viper.toml" + end case VerifiedHttp.get(manifest_url, receive_timeout: 10_000) do {:ok, %{body: body}} -> @@ -199,7 +203,7 @@ defmodule Opsm.Registries.JuliaTheViper do end pkg = %ResolvedPackage{ - package: manifest.name || (repo_url |> String.split("/") |> List.last() |> String.trim()), + package: manifest.name || repo_url |> String.split("/") |> List.last() |> String.trim(), version: manifest.version || version, forth: :julia_the_viper, registry_url: repo_url, diff --git a/opsm_ex/lib/opsm/registries/k8s_operators.ex b/opsm_ex/lib/opsm/registries/k8s_operators.ex index 40775995..c1cda434 100644 --- a/opsm_ex/lib/opsm/registries/k8s_operators.ex +++ b/opsm_ex/lib/opsm/registries/k8s_operators.ex @@ -28,21 +28,28 @@ defmodule Opsm.Registries.K8sOperators do spec = csv["spec"] || %{} _annotations = metadata["annotations"] || %{} - ver = if version == "latest" do - spec["version"] || metadata["name"] |> extract_version_from_name() || "0.0.0" - else - version - end + ver = + if version == "latest" do + spec["version"] || metadata["name"] |> extract_version_from_name() || "0.0.0" + else + version + end {:ok, parse_operator(name, operator, csv, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp extract_version_from_name(nil), do: nil + defp extract_version_from_name(name) do # OLM CSVs are named like "prometheus-operator.v0.65.1" case Regex.run(~r/\.v?(\d+\.\d+\.\d+.*)$/, name) do @@ -60,18 +67,21 @@ defmodule Opsm.Registries.K8sOperators do _required_apis = spec["apiservicedefinitions"] || %{} required_crds = get_in(spec, ["customresourcedefinitions", "required"]) || [] - deps = required_crds - |> Enum.map(fn crd -> - crd_name = crd["name"] || crd["kind"] || "unknown" - {crd_name, crd["version"] || "*"} - end) - |> Map.new() + deps = + required_crds + |> Enum.map(fn crd -> + crd_name = crd["name"] || crd["kind"] || "unknown" + {crd_name, crd["version"] || "*"} + end) + |> Map.new() # Extract container images as keywords related_images = operator["related_images"] || [] - image_names = related_images - |> Enum.map(fn img -> img["name"] end) - |> Enum.reject(&is_nil/1) + + image_names = + related_images + |> Enum.map(fn img -> img["name"] end) + |> Enum.reject(&is_nil/1) provider = get_in(spec, ["provider", "name"]) @@ -106,9 +116,11 @@ defmodule Opsm.Registries.K8sOperators do end defp truncate_description(nil), do: nil + defp truncate_description(desc) when byte_size(desc) > 500 do String.slice(desc, 0, 497) <> "..." end + defp truncate_description(desc), do: desc @doc """ @@ -120,32 +132,39 @@ defmodule Opsm.Registries.K8sOperators do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> operators = body["operators"] || body["items"] || [] - results = operators - |> Enum.take(20) - |> Enum.map(fn op -> - csv = op["csv"] || %{} - spec = csv["spec"] || %{} - %{ - name: op["name"] || op["packageName"], - version: spec["version"] || op["version"], - description: op["description"] || spec["description"] - } - end) + + results = + operators + |> Enum.take(20) + |> Enum.map(fn op -> + csv = op["csv"] || %{} + spec = csv["spec"] || %{} + + %{ + name: op["name"] || op["packageName"], + version: spec["version"] || op["version"], + description: op["description"] || spec["description"] + } + end) + {:ok, results} {:ok, items} when is_list(items) -> - results = items - |> Enum.take(20) - |> Enum.map(fn op -> - %{ - name: op["name"] || op["packageName"], - version: op["version"], - description: op["description"] - } - end) + results = + items + |> Enum.take(20) + |> Enum.map(fn op -> + %{ + name: op["name"] || op["packageName"], + version: op["version"], + description: op["description"] + } + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end @@ -170,31 +189,35 @@ defmodule Opsm.Registries.K8sOperators do operator = body["operator"] || body channels = operator["channels"] || [] - vers = channels - |> Enum.flat_map(fn ch -> - entries = ch["entries"] || ch["versions"] || [] - Enum.map(entries, fn - e when is_map(e) -> e["version"] || e["name"] - e when is_binary(e) -> e - _ -> nil - end) - end) - |> Enum.reject(&is_nil/1) - |> Enum.uniq() + vers = + channels + |> Enum.flat_map(fn ch -> + entries = ch["entries"] || ch["versions"] || [] + + Enum.map(entries, fn + e when is_map(e) -> e["version"] || e["name"] + e when is_binary(e) -> e + _ -> nil + end) + end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() # If no channel entries, fall back to single version - vers = if vers == [] do - case fetch_package(name) do - {:ok, pkg} -> [pkg.version] - _ -> [] + vers = + if vers == [] do + case fetch_package(name) do + {:ok, pkg} -> [pkg.version] + _ -> [] + end + else + vers end - else - vers - end {:ok, vers} - {:error, _} = err -> err + {:error, _} = err -> + err end end end diff --git a/opsm_ex/lib/opsm/registries/lithoglyph.ex b/opsm_ex/lib/opsm/registries/lithoglyph.ex index 3d900e28..320af4f3 100644 --- a/opsm_ex/lib/opsm/registries/lithoglyph.ex +++ b/opsm_ex/lib/opsm/registries/lithoglyph.ex @@ -81,17 +81,21 @@ defmodule Opsm.Registries.Lithoglyph do defp fetch_from_registry(name, version) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> target = if version == "latest", do: body["latest_version"], else: version {:ok, parse_registry_package(body, target)} - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end defp search_registry(query, opts) do limit = Keyword.get(opts, :limit, 20) url = "#{@base_url}/packages?q=#{URI.encode(query)}&limit=#{limit}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, &parse_search_result/1)} {:error, reason} -> {:error, reason} @@ -107,6 +111,7 @@ defmodule Opsm.Registries.Lithoglyph do defp registry_versions(name) do url = "#{@base_url}/packages/#{URI.encode(name)}/versions" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, & &1["version"])} {:error, reason} -> {:error, reason} @@ -122,9 +127,12 @@ defmodule Opsm.Registries.Lithoglyph do defp search_curated(query, _opts) do q = String.downcase(query) - results = Enum.filter(@known_packages, fn p -> - String.contains?(String.downcase("#{p.name} #{p.description}"), q) - end) + + results = + Enum.filter(@known_packages, fn p -> + String.contains?(String.downcase("#{p.name} #{p.description}"), q) + end) + {:ok, Enum.map(results, &curated_to_resolved(&1, "latest"))} end @@ -137,13 +145,18 @@ defmodule Opsm.Registries.Lithoglyph do pkg_info.url |> String.replace("https://github.com/", "https://api.github.com/repos/") |> Kernel.<>("/tags") + case VerifiedHttp.get_json(tags_url, receive_timeout: 10_000) do {:ok, tags} when is_list(tags) -> versions = Enum.map(tags, & &1["name"]) |> Enum.reject(&is_nil/1) {:ok, if(versions == [], do: ["main"], else: versions)} - _ -> {:ok, ["main"]} + + _ -> + {:ok, ["main"]} end - :not_found -> {:error, :not_found} + + :not_found -> + {:error, :not_found} end end @@ -152,6 +165,7 @@ defmodule Opsm.Registries.Lithoglyph do "https://github.com/hyperpolymath/#{name}", "https://github.com/hyperpolymath/lithoglyph-#{name}" ] + Enum.find_value(urls, {:error, :not_found}, fn url -> case fetch_git_manifest(%{name: name, url: url, description: nil}, version) do {:ok, pkg} -> {:ok, pkg} @@ -166,8 +180,11 @@ defmodule Opsm.Registries.Lithoglyph do base = pkg_info.url try_url = fn manifest -> - url = if path, do: "#{base}/raw/#{branch}/#{path}/#{manifest}", - else: "#{base}/raw/#{branch}/#{manifest}" + url = + if path, + do: "#{base}/raw/#{branch}/#{path}/#{manifest}", + else: "#{base}/raw/#{branch}/#{manifest}" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: text}} -> {:ok, text} _ -> :not_found @@ -187,62 +204,97 @@ defmodule Opsm.Registries.Lithoglyph do pkg_name = fields["name"] || pkg_info.name pkg = %ResolvedPackage{ - package: pkg_name, version: fields["version"] || version, forth: :lithoglyph, + package: pkg_name, + version: fields["version"] || version, + forth: :lithoglyph, registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + checksum: nil, + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: pkg_name, version: fields["version"] || version, + name: pkg_name, + version: fields["version"] || version, description: fields["description"] || pkg_info[:description], license: fields["license"] || "MPL-2.0", homepage: fields["homepage"] || pkg_info.url, repository: fields["repository"] || pkg_info.url, authors: parse_toml_array(fields["authors"]) || default_authors(), keywords: parse_toml_array(fields["keywords"]) || default_keywords(), - dependencies: %{}, dev_dependencies: %{}, - source_forth: :lithoglyph, raw_manifest: fields + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :lithoglyph, + raw_manifest: fields }, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } + {:ok, pkg} end defp curated_to_resolved(pkg_info, version) do %ResolvedPackage{ - package: pkg_info.name, version: version, forth: :lithoglyph, - registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + package: pkg_info.name, + version: version, + forth: :lithoglyph, + registry_url: pkg_info.url, + tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", + checksum: nil, + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: pkg_info.name, version: version, description: pkg_info.description, - license: "MPL-2.0", homepage: pkg_info.url, repository: pkg_info.url, - authors: default_authors(), keywords: default_keywords(), - dependencies: %{}, dev_dependencies: %{}, - source_forth: :lithoglyph, raw_manifest: %{"registry" => "lithoglyph-curated"} + name: pkg_info.name, + version: version, + description: pkg_info.description, + license: "MPL-2.0", + homepage: pkg_info.url, + repository: pkg_info.url, + authors: default_authors(), + keywords: default_keywords(), + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :lithoglyph, + raw_manifest: %{"registry" => "lithoglyph-curated"} }, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } end defp parse_registry_package(data, version) do %ResolvedPackage{ - package: data["name"], version: version, forth: :lithoglyph, registry_url: @base_url, + package: data["name"], + version: version, + forth: :lithoglyph, + registry_url: @base_url, tarball_url: "#{@base_url}/packages/#{data["name"]}/#{version}/download", - checksum: data["checksum"], checksum_algo: :sha256, + checksum: data["checksum"], + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: data["name"], version: version, description: data["description"], - license: data["license"], homepage: data["homepage"], repository: data["repository"], - authors: data["authors"] || [], keywords: data["keywords"] || [], + name: data["name"], + version: version, + description: data["description"], + license: data["license"], + homepage: data["homepage"], + repository: data["repository"], + authors: data["authors"] || [], + keywords: data["keywords"] || [], dependencies: data["dependencies"] || %{}, dev_dependencies: data["dev_dependencies"] || %{}, - source_forth: :lithoglyph, raw_manifest: data + source_forth: :lithoglyph, + raw_manifest: data }, - attestations: data["attestations"] || [], resolved_deps: [] + attestations: data["attestations"] || [], + resolved_deps: [] } end defp parse_search_result(r) do - %{name: r["name"], version: r["latest_version"], - description: r["description"], downloads: r["downloads"] || 0} + %{ + name: r["name"], + version: r["latest_version"], + description: r["description"], + downloads: r["downloads"] || 0 + } end defp find_curated(name) do @@ -257,23 +309,34 @@ defmodule Opsm.Registries.Lithoglyph do String.split(toml_text, "\n") |> Enum.reduce({false, %{}}, fn line, {inside, acc} -> t = String.trim(line) + cond do - t == "[#{section_name}]" -> {true, acc} - String.starts_with?(t, "[") -> {false, acc} + t == "[#{section_name}]" -> + {true, acc} + + String.starts_with?(t, "[") -> + {false, acc} + inside -> case String.split(t, " = ", parts: 2) do [k, v] -> {true, Map.put(acc, String.trim(k), String.trim(v, "\""))} _ -> {inside, acc} end - true -> {inside, acc} + + true -> + {inside, acc} end end) + fields end defp parse_toml_array(nil), do: nil + defp parse_toml_array(str) when is_binary(str) do - str |> String.trim("[") |> String.trim("]") + str + |> String.trim("[") + |> String.trim("]") |> String.split(",") |> Enum.map(&(&1 |> String.trim() |> String.trim("\""))) |> Enum.reject(&(&1 == "")) diff --git a/opsm_ex/lib/opsm/registries/luarocks.ex b/opsm_ex/lib/opsm/registries/luarocks.ex index 2542164e..bbcb773f 100644 --- a/opsm_ex/lib/opsm/registries/luarocks.ex +++ b/opsm_ex/lib/opsm/registries/luarocks.ex @@ -16,11 +16,12 @@ defmodule Opsm.Registries.LuaRocks do Fetch package metadata from LuaRocks. """ def fetch_package(name, version \\ "latest") do - target_version = if version == "latest" do - fetch_latest_version(name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(name) + else + version + end case target_version do nil -> @@ -29,6 +30,7 @@ defmodule Opsm.Registries.LuaRocks do ver -> # Try to fetch rockspec file rockspec_url = "#{@registry_url}/manifests/#{name}/#{name}-#{ver}.rockspec" + case VerifiedHttp.get(rockspec_url, receive_timeout: 10_000) do {:ok, %{body: body}} when is_binary(body) -> parse_rockspec(name, ver, body) @@ -55,6 +57,7 @@ defmodule Opsm.Registries.LuaRocks do defp fetch_latest_version(name) do # Use the modules API to get latest version url = "#{@registry_url}/modules/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> # Extract latest version from module info @@ -78,10 +81,14 @@ defmodule Opsm.Registries.LuaRocks do is_map(module_info) and Map.has_key?(module_info, "versions") -> case module_info["versions"] do - [latest | _] when is_binary(latest) -> latest + [latest | _] when is_binary(latest) -> + latest + versions when is_map(versions) -> versions |> Map.keys() |> Enum.sort(:desc) |> List.first() - _ -> nil + + _ -> + nil end true -> @@ -91,6 +98,7 @@ defmodule Opsm.Registries.LuaRocks do defp fetch_from_module_info(name, version) do url = "#{@registry_url}/modules/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> parse_module_info(name, version, body) @@ -252,16 +260,19 @@ defmodule Opsm.Registries.LuaRocks do limit = Keyword.get(opts, :limit, 20) url = "#{@registry_url}/search?q=#{URI.encode(query)}&format=json" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body + results = + body |> Enum.take(limit) |> Enum.map(&parse_search_result/1) {:ok, results} {:ok, %{"results" => results}} when is_list(results) -> - parsed = results + parsed = + results |> Enum.take(limit) |> Enum.map(&parse_search_result/1) @@ -303,7 +314,8 @@ defmodule Opsm.Registries.LuaRocks do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions}} when is_map(versions) -> - version_list = versions + version_list = + versions |> Map.keys() |> Enum.sort(:desc) diff --git a/opsm_ex/lib/opsm/registries/macports.ex b/opsm_ex/lib/opsm/registries/macports.ex index 11be22da..12c80d6a 100644 --- a/opsm_ex/lib/opsm/registries/macports.ex +++ b/opsm_ex/lib/opsm/registries/macports.ex @@ -20,14 +20,21 @@ defmodule Opsm.Registries.Macports do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: body["version"], - else: version + ver = + if version == "latest", + do: body["version"], + else: version + {:ok, parse_macport(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -40,7 +47,8 @@ defmodule Opsm.Registries.Macports do description: body["description"] || body["long_description"], license: format_license(body["license"]), homepage: body["homepage"], - repository: "https://github.com/macports/macports-ports/tree/master/#{body["portdir"] || name}", + repository: + "https://github.com/macports/macports-ports/tree/master/#{body["portdir"] || name}", dependencies: deps } @@ -51,7 +59,7 @@ defmodule Opsm.Registries.Macports do manifest: manifest, tarball_url: build_distfile_url(body), checksum: nil, - attestations: [], + attestations: [] } end @@ -65,8 +73,10 @@ defmodule Opsm.Registries.Macports do d when is_binary(d) -> dep_name = d |> String.split(":") |> List.last() |> String.trim() {dep_name, "*"} + d when is_map(d) -> {d["port_name"] || d["name"] || "", "*"} + _ -> nil end) @@ -84,7 +94,9 @@ defmodule Opsm.Registries.Macports do case body["distfiles"] do [first | _] when is_binary(first) -> "https://distfiles.macports.org/#{body["name"] || "unknown"}/#{first}" - _ -> nil + + _ -> + nil end end @@ -97,10 +109,12 @@ defmodule Opsm.Registries.Macports do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - versions_list = body - |> Enum.map(fn entry -> entry["version"] end) - |> Enum.reject(&is_nil/1) - |> Enum.uniq() + versions_list = + body + |> Enum.map(fn entry -> entry["version"] end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + {:ok, versions_list} {:ok, _} -> @@ -110,7 +124,8 @@ defmodule Opsm.Registries.Macports do {:error, _} = err -> err end - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -122,23 +137,30 @@ defmodule Opsm.Registries.Macports do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> - hits = results - |> Enum.take(20) - |> Enum.map(fn port -> - %{name: port["name"], version: port["version"], description: port["description"]} - end) + hits = + results + |> Enum.take(20) + |> Enum.map(fn port -> + %{name: port["name"], version: port["version"], description: port["description"]} + end) + {:ok, hits} {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn port -> - %{name: port["name"], version: port["version"], description: port["description"]} - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn port -> + %{name: port["name"], version: port["version"], description: port["description"]} + end) + {:ok, results} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/maven.ex b/opsm_ex/lib/opsm/registries/maven.ex index a88e1b24..678a5b23 100644 --- a/opsm_ex/lib/opsm/registries/maven.ex +++ b/opsm_ex/lib/opsm/registries/maven.ex @@ -19,20 +19,23 @@ defmodule Opsm.Registries.Maven do """ def fetch_package(name, version \\ "latest") do {group_id, artifact_id} = parse_coordinate(name) - url = "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&core=gav&rows=200&wt=json" + + url = + "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&core=gav&rows=200&wt=json" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> docs = get_in(body, ["response", "docs"]) || [] - target_version = if version == "latest" do - case docs do - [first | _] -> first["v"] - [] -> nil + target_version = + if version == "latest" do + case docs do + [first | _] -> first["v"] + [] -> nil + end + else + version end - else - version - end if target_version do doc = Enum.find(docs, fn d -> d["v"] == target_version end) || %{} @@ -64,9 +67,12 @@ defmodule Opsm.Registries.Maven do case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: body}} when is_binary(body) -> parse_pom_dependencies(body) + {:ok, body} when is_binary(body) -> parse_pom_dependencies(body) - _ -> %{} + + _ -> + %{} end end @@ -100,14 +106,17 @@ defmodule Opsm.Registries.Maven do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> docs = get_in(body, ["response", "docs"]) || [] - results = Enum.map(docs, fn doc -> - %{ - name: "#{doc["g"]}:#{doc["a"]}", - version: doc["latestVersion"], - description: nil, - downloads: 0 - } - end) + + results = + Enum.map(docs, fn doc -> + %{ + name: "#{doc["g"]}:#{doc["a"]}", + version: doc["latestVersion"], + description: nil, + downloads: 0 + } + end) + {:ok, results} {:error, %{status: status}} -> @@ -123,13 +132,17 @@ defmodule Opsm.Registries.Maven do """ def exists?(name) do {group_id, artifact_id} = parse_coordinate(name) - url = "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&rows=1&wt=json" + + url = + "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&rows=1&wt=json" case VerifiedHttp.get_json(url, receive_timeout: 5_000) do {:ok, body} -> num_found = get_in(body, ["response", "numFound"]) || 0 num_found > 0 - _ -> false + + _ -> + false end end @@ -138,7 +151,9 @@ defmodule Opsm.Registries.Maven do """ def versions(name) do {group_id, artifact_id} = parse_coordinate(name) - url = "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&core=gav&rows=200&wt=json" + + url = + "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&core=gav&rows=200&wt=json" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> @@ -177,16 +192,21 @@ defmodule Opsm.Registries.Maven do defp fetch_jar_sha1(group_id, artifact_id, version) do group_path = String.replace(group_id, ".", "/") - url = "#{@repo_url}/#{group_path}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar.sha1" + + url = + "#{@repo_url}/#{group_path}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar.sha1" case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: body}} when is_binary(body) -> hash = body |> String.trim() |> String.split() |> List.first() if hash && Regex.match?(~r/^[0-9a-fA-F]{40}$/, hash), do: hash, else: nil + {:ok, body} when is_binary(body) -> hash = body |> String.trim() |> String.split() |> List.first() if hash && Regex.match?(~r/^[0-9a-fA-F]{40}$/, hash), do: hash, else: nil - _ -> nil + + _ -> + nil end end @@ -201,7 +221,8 @@ defmodule Opsm.Registries.Maven do version: version, forth: :maven, registry_url: "https://central.sonatype.com/artifact/#{group_id}/#{artifact_id}", - tarball_url: "#{@repo_url}/#{group_path}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar", + tarball_url: + "#{@repo_url}/#{group_path}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar", checksum: sha1, checksum_algo: if(sha1, do: :sha1, else: nil), manifest: %ManifestFormat{ diff --git a/opsm_ex/lib/opsm/registries/melpa.ex b/opsm_ex/lib/opsm/registries/melpa.ex index eae7d5d1..3df30623 100644 --- a/opsm_ex/lib/opsm/registries/melpa.ex +++ b/opsm_ex/lib/opsm/registries/melpa.ex @@ -26,11 +26,12 @@ defmodule Opsm.Registries.Melpa do {:error, :not_found} pkg_data -> - target_version = if version == "latest" do - extract_version(pkg_data) - else - version - end + target_version = + if version == "latest" do + extract_version(pkg_data) + else + version + end deps = extract_deps(pkg_data) {:ok, parse_package(name, pkg_data, target_version, deps)} @@ -61,21 +62,23 @@ defmodule Opsm.Registries.Melpa do {:ok, archive} when is_map(archive) -> downcased = String.downcase(query) - results = archive - |> Enum.filter(fn {name, data} -> - desc = Map.get(data, "desc", "") || "" - String.contains?(String.downcase(name), downcased) || - String.contains?(String.downcase(desc), downcased) - end) - |> Enum.take(limit) - |> Enum.map(fn {name, data} -> - %{ - name: name, - version: extract_version(data), - description: Map.get(data, "desc", ""), - downloads: 0 - } - end) + results = + archive + |> Enum.filter(fn {name, data} -> + desc = Map.get(data, "desc", "") || "" + + String.contains?(String.downcase(name), downcased) || + String.contains?(String.downcase(desc), downcased) + end) + |> Enum.take(limit) + |> Enum.map(fn {name, data} -> + %{ + name: name, + version: extract_version(data), + description: Map.get(data, "desc", ""), + downloads: 0 + } + end) {:ok, results} @@ -143,10 +146,12 @@ defmodule Opsm.Registries.Melpa do case Map.get(data, "deps") do deps when is_map(deps) -> Enum.reduce(deps, %{}, fn {dep_name, ver_list}, acc -> - constraint = case ver_list do - ver when is_list(ver) -> ">= #{Enum.join(ver, ".")}" - _ -> ">= 0.0.0" - end + constraint = + case ver_list do + ver when is_list(ver) -> ">= #{Enum.join(ver, ".")}" + _ -> ">= 0.0.0" + end + Map.put(acc, dep_name, constraint) end) diff --git a/opsm_ex/lib/opsm/registries/my_lang.ex b/opsm_ex/lib/opsm/registries/my_lang.ex index ea4d27b3..6eff6f33 100644 --- a/opsm_ex/lib/opsm/registries/my_lang.ex +++ b/opsm_ex/lib/opsm/registries/my_lang.ex @@ -18,7 +18,8 @@ defmodule Opsm.Registries.MyLang do alias Opsm.Verified.Http, as: VerifiedHttp @base_url "https://packages.my-lang.dev/api/v1" - @fallback_mode :git # Until registry deployed + # Until registry deployed + @fallback_mode :git @doc """ Fetch package metadata from my-lang registry. @@ -94,6 +95,7 @@ defmodule Opsm.Registries.MyLang do defp registry_exists?(name) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -151,17 +153,19 @@ defmodule Opsm.Registries.MyLang do defp git_versions(_name) do # For git mode, versions are git tags - {:ok, ["main", "master"]} # Minimal fallback + # Minimal fallback + {:ok, ["main", "master"]} end defp git_fetch(repo_url, version) do # my-lang uses Cargo.toml for manifest - manifest_url = case version do - "latest" -> "#{repo_url}/raw/main/Cargo.toml" - "main" -> "#{repo_url}/raw/main/Cargo.toml" - "master" -> "#{repo_url}/raw/master/Cargo.toml" - tag -> "#{repo_url}/raw/#{tag}/Cargo.toml" - end + manifest_url = + case version do + "latest" -> "#{repo_url}/raw/main/Cargo.toml" + "main" -> "#{repo_url}/raw/main/Cargo.toml" + "master" -> "#{repo_url}/raw/master/Cargo.toml" + tag -> "#{repo_url}/raw/#{tag}/Cargo.toml" + end case VerifiedHttp.get(manifest_url, receive_timeout: 10_000) do {:ok, %{body: body}} -> @@ -200,7 +204,7 @@ defmodule Opsm.Registries.MyLang do end pkg = %ResolvedPackage{ - package: manifest.name || (repo_url |> String.split("/") |> List.last() |> String.trim()), + package: manifest.name || repo_url |> String.split("/") |> List.last() |> String.trim(), version: manifest.version || version, forth: :my_lang, registry_url: repo_url, diff --git a/opsm_ex/lib/opsm/registries/nimble.ex b/opsm_ex/lib/opsm/registries/nimble.ex index 9a2118ff..8f825499 100644 --- a/opsm_ex/lib/opsm/registries/nimble.ex +++ b/opsm_ex/lib/opsm/registries/nimble.ex @@ -32,7 +32,7 @@ defmodule Opsm.Registries.Nimble do {:ok, %ResolvedPackage{version: "0.5.0", ...}} """ @spec fetch_package(String.t(), String.t()) :: - {:ok, ResolvedPackage.t()} | {:error, term()} + {:ok, ResolvedPackage.t()} | {:error, term()} def fetch_package(name, version \\ "latest") do Logger.debug("Fetching Nimble package: #{name}@#{version}") @@ -73,7 +73,7 @@ defmodule Opsm.Registries.Nimble do {:ok, [%ResolvedPackage{name: "jester", ...}, ...]} """ @spec search(String.t(), keyword()) :: - {:ok, [ResolvedPackage.t()]} | {:error, term()} + {:ok, [ResolvedPackage.t()]} | {:error, term()} def search(query, opts \\ []) do limit = Keyword.get(opts, :limit, 20) @@ -83,10 +83,12 @@ defmodule Opsm.Registries.Nimble do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - packages = body - |> get_in(["packages"]) || [] - |> Enum.take(limit) - |> Enum.map(&parse_search_result/1) + packages = + body + |> get_in(["packages"]) || + [] + |> Enum.take(limit) + |> Enum.map(&parse_search_result/1) {:ok, packages} @@ -157,19 +159,21 @@ defmodule Opsm.Registries.Nimble do versions_data = body["versions"] || [] # Resolve target version - target_version = if requested_version == "latest" do - case versions_data do - [latest | _] -> latest["version"] || latest["name"] - [] -> "0.0.0" + target_version = + if requested_version == "latest" do + case versions_data do + [latest | _] -> latest["version"] || latest["name"] + [] -> "0.0.0" + end + else + requested_version end - else - requested_version - end # Find version data - version_info = Enum.find(versions_data, fn v -> - (v["version"] || v["name"]) == target_version - end) || List.first(versions_data) || %{} + version_info = + Enum.find(versions_data, fn v -> + (v["version"] || v["name"]) == target_version + end) || List.first(versions_data) || %{} # Extract dependencies deps_list = parse_dependencies(version_info["requires"] || []) @@ -196,9 +200,10 @@ defmodule Opsm.Registries.Nimble do "language" => "nim", "manifest_format" => "nimble", "build_system" => "nimble", - "versions" => Enum.map(versions_data, fn v -> - v["version"] || v["name"] - end), + "versions" => + Enum.map(versions_data, fn v -> + v["version"] || v["name"] + end), "tags" => body["tags"] || [], "author" => body["author"], "nimble_file" => "#{pkg_name}.nimble" diff --git a/opsm_ex/lib/opsm/registries/nix.ex b/opsm_ex/lib/opsm/registries/nix.ex index 464ceb09..0cf57278 100644 --- a/opsm_ex/lib/opsm/registries/nix.ex +++ b/opsm_ex/lib/opsm/registries/nix.ex @@ -26,12 +26,16 @@ defmodule Opsm.Registries.Nix do case VerifiedHttp.post_json(@search_api, query, receive_timeout: 10_000) do {:ok, body} -> hits = get_in(body, ["hits", "hits"]) || [] + case hits do [hit | _] -> source = hit["_source"] || %{} - ver = if version == "latest", - do: source["package_version"], - else: version + + ver = + if version == "latest", + do: source["package_version"], + else: version + {:ok, parse_nix_package(name, source, ver)} [] -> @@ -44,22 +48,24 @@ defmodule Opsm.Registries.Nix do end defp parse_nix_package(name, source, version) do - licenses = case source["package_license"] do - l when is_list(l) -> Enum.join(l, " AND ") - l when is_binary(l) -> l - _ -> nil - end + licenses = + case source["package_license"] do + l when is_list(l) -> Enum.join(l, " AND ") + l when is_binary(l) -> l + _ -> nil + end manifest = %ManifestFormat{ name: name, version: version || "0.0.0", description: source["package_description"], license: licenses, - homepage: case source["package_homepage"] do - [url | _] -> url - url when is_binary(url) -> url - _ -> nil - end, + homepage: + case source["package_homepage"] do + [url | _] -> url + url when is_binary(url) -> url + _ -> nil + end, repository: nil, dependencies: %{} } @@ -71,7 +77,7 @@ defmodule Opsm.Registries.Nix do manifest: manifest, tarball_url: nil, checksum: nil, - attestations: [], + attestations: [] } end @@ -93,14 +99,18 @@ defmodule Opsm.Registries.Nix do case VerifiedHttp.post_json(@search_api, search_query, receive_timeout: 10_000) do {:ok, body} -> hits = get_in(body, ["hits", "hits"]) || [] - results = Enum.map(hits, fn hit -> - s = hit["_source"] || %{} - %{ - name: s["package_attr_name"], - version: s["package_version"], - description: s["package_description"] - } - end) + + results = + Enum.map(hits, fn hit -> + s = hit["_source"] || %{} + + %{ + name: s["package_attr_name"], + version: s["package_version"], + description: s["package_description"] + } + end) + {:ok, results} {:error, reason} -> diff --git a/opsm_ex/lib/opsm/registries/nix_darwin.ex b/opsm_ex/lib/opsm/registries/nix_darwin.ex index 97a05af9..a4e59f79 100644 --- a/opsm_ex/lib/opsm/registries/nix_darwin.ex +++ b/opsm_ex/lib/opsm/registries/nix_darwin.ex @@ -38,9 +38,11 @@ defmodule Opsm.Registries.NixDarwin do case Map.get(options, name) do nil -> # Try partial match - matches = options - |> Enum.filter(fn {k, _v} -> String.contains?(k, name) end) - |> Enum.take(1) + matches = + options + |> Enum.filter(fn {k, _v} -> String.contains?(k, name) end) + |> Enum.take(1) + case matches do [{key, opt} | _] -> {:ok, parse_hm_option(key, opt)} [] -> {:error, :not_found} @@ -51,24 +53,28 @@ defmodule Opsm.Registries.NixDarwin do end {:ok, options} when is_list(options) -> - match = Enum.find(options, fn opt -> - (opt["name"] || opt["option"]) == name - end) + match = + Enum.find(options, fn opt -> + (opt["name"] || opt["option"]) == name + end) + case match do nil -> {:error, :not_found} opt -> {:ok, parse_hm_option(name, opt)} end - {:error, _} = err -> err + {:error, _} = err -> + err end end defp parse_hm_option(name, opt) do - description = case opt do - o when is_map(o) -> o["description"] || o["type"] || "Home Manager option" - o when is_binary(o) -> o - _ -> "Home Manager option" - end + description = + case opt do + o when is_map(o) -> o["description"] || o["type"] || "Home Manager option" + o when is_binary(o) -> o + _ -> "Home Manager option" + end option_type = if is_map(opt), do: opt["type"], else: nil default_val = if is_map(opt), do: opt["default"], else: nil @@ -80,7 +86,8 @@ defmodule Opsm.Registries.NixDarwin do license: "MIT", homepage: "https://nix-community.github.io/home-manager/", repository: "https://github.com/nix-community/home-manager", - keywords: ["nix-darwin", "home-manager", "nix-option", option_type] |> Enum.reject(&is_nil/1), + keywords: + ["nix-darwin", "home-manager", "nix-option", option_type] |> Enum.reject(&is_nil/1), dependencies: %{}, source_forth: :nix_darwin, raw_manifest: %{ @@ -120,33 +127,41 @@ defmodule Opsm.Registries.NixDarwin do case VerifiedHttp.post_json(@nixpkgs_search_api, query, receive_timeout: 10_000) do {:ok, body} -> hits = get_in(body, ["hits", "hits"]) || [] + case hits do [hit | _] -> source = hit["_source"] || %{} - ver = if version == "latest", - do: source["package_version"] || "0.0.0", - else: version + + ver = + if version == "latest", + do: source["package_version"] || "0.0.0", + else: version + {:ok, parse_nixpkgs(name, source, ver)} - [] -> {:error, :not_found} + [] -> + {:error, :not_found} end - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end defp parse_nixpkgs(name, source, version) do - licenses = case source["package_license"] do - l when is_list(l) -> Enum.join(l, " AND ") - l when is_binary(l) -> l - _ -> nil - end - - homepage = case source["package_homepage"] do - [url | _] -> url - url when is_binary(url) -> url - _ -> nil - end + licenses = + case source["package_license"] do + l when is_list(l) -> Enum.join(l, " AND ") + l when is_binary(l) -> l + _ -> nil + end + + homepage = + case source["package_homepage"] do + [url | _] -> url + url when is_binary(url) -> url + _ -> nil + end manifest = %ManifestFormat{ name: name, @@ -193,17 +208,22 @@ defmodule Opsm.Registries.NixDarwin do case VerifiedHttp.post_json(@nixpkgs_search_api, search_query, receive_timeout: 10_000) do {:ok, body} -> hits = get_in(body, ["hits", "hits"]) || [] - results = Enum.map(hits, fn hit -> - s = hit["_source"] || %{} - %{ - name: s["package_attr_name"], - version: s["package_version"], - description: s["package_description"] - } - end) + + results = + Enum.map(hits, fn hit -> + s = hit["_source"] || %{} + + %{ + name: s["package_attr_name"], + version: s["package_version"], + description: s["package_description"] + } + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/nix_flakes.ex b/opsm_ex/lib/opsm/registries/nix_flakes.ex index 92e26f40..be99c453 100644 --- a/opsm_ex/lib/opsm/registries/nix_flakes.ex +++ b/opsm_ex/lib/opsm/registries/nix_flakes.ex @@ -24,20 +24,26 @@ defmodule Opsm.Registries.NixFlakes do {:ok, body} -> releases = body["releases"] || body["versions"] || [] - ver = if version == "latest" do - case releases do - [latest | _] -> latest["version"] || latest["ref"] || "0.0.0" - _ -> body["version"] || "0.0.0" + ver = + if version == "latest" do + case releases do + [latest | _] -> latest["version"] || latest["ref"] || "0.0.0" + _ -> body["version"] || "0.0.0" + end + else + version end - else - version - end {:ok, parse_flake(name, body, ver, releases)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -47,12 +53,14 @@ defmodule Opsm.Registries.NixFlakes do # Extract flake inputs as dependencies inputs = body["inputs"] || %{} - deps = inputs - |> Enum.map(fn {k, v} -> - ref = if is_map(v), do: v["url"] || v["ref"] || "*", else: "*" - {k, ref} - end) - |> Map.new() + + deps = + inputs + |> Enum.map(fn {k, v} -> + ref = if is_map(v), do: v["url"] || v["ref"] || "*", else: "*" + {k, ref} + end) + |> Map.new() manifest = %ManifestFormat{ name: name, @@ -67,15 +75,17 @@ defmodule Opsm.Registries.NixFlakes do raw_manifest: body } - tarball = case releases do - [latest | _] -> latest["tarball_url"] || latest["archive_url"] - _ -> nil - end + tarball = + case releases do + [latest | _] -> latest["tarball_url"] || latest["archive_url"] + _ -> nil + end - checksum = case releases do - [latest | _] -> latest["hash"] || latest["narHash"] - _ -> nil - end + checksum = + case releases do + [latest | _] -> latest["hash"] || latest["narHash"] + _ -> nil + end %ResolvedPackage{ package: name, @@ -99,30 +109,35 @@ defmodule Opsm.Registries.NixFlakes do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, items} when is_list(items) -> - results = items - |> Enum.take(20) - |> Enum.map(fn flake -> - %{ - name: flake["name"] || flake["full_name"], - version: flake["version"], - description: flake["description"] - } - end) + results = + items + |> Enum.take(20) + |> Enum.map(fn flake -> + %{ + name: flake["name"] || flake["full_name"], + version: flake["version"], + description: flake["description"] + } + end) + {:ok, results} {:ok, body} -> - results = (body["results"] || body["flakes"] || []) - |> Enum.take(20) - |> Enum.map(fn flake -> - %{ - name: flake["name"] || flake["full_name"], - version: flake["version"] || flake["ref"], - description: flake["description"] - } - end) + results = + (body["results"] || body["flakes"] || []) + |> Enum.take(20) + |> Enum.map(fn flake -> + %{ + name: flake["name"] || flake["full_name"], + version: flake["version"] || flake["ref"], + description: flake["description"] + } + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end @@ -145,12 +160,16 @@ defmodule Opsm.Registries.NixFlakes do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> releases = body["releases"] || body["versions"] || [] - vers = releases - |> Enum.map(fn r -> r["version"] || r["ref"] end) - |> Enum.reject(&is_nil/1) + + vers = + releases + |> Enum.map(fn r -> r["version"] || r["ref"] end) + |> Enum.reject(&is_nil/1) + {:ok, vers} - {:error, _} = err -> err + {:error, _} = err -> + err end end end diff --git a/opsm_ex/lib/opsm/registries/npm.ex b/opsm_ex/lib/opsm/registries/npm.ex index 680d2e4c..98127497 100644 --- a/opsm_ex/lib/opsm/registries/npm.ex +++ b/opsm_ex/lib/opsm/registries/npm.ex @@ -15,11 +15,12 @@ defmodule Opsm.Registries.Npm do Fetch package metadata from npm registry. """ def fetch_package(name, version \\ "latest") do - url = if version == "latest" do - "#{@base_url}/#{URI.encode(name)}/latest" - else - "#{@base_url}/#{URI.encode(name)}/#{version}" - end + url = + if version == "latest" do + "#{@base_url}/#{URI.encode(name)}/latest" + else + "#{@base_url}/#{URI.encode(name)}/#{version}" + end case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> @@ -142,6 +143,7 @@ defmodule Opsm.Registries.Npm do defp parse_search_result(obj) do pkg = obj["package"] || %{} + %{ name: pkg["name"], version: pkg["version"], @@ -169,24 +171,30 @@ defmodule Opsm.Registries.Npm do end defp extract_authors(author, maintainers) do - author_list = case author do - nil -> [] - a when is_binary(a) -> [a] - %{"name" => name} -> [name] - _ -> [] - end - - maintainer_list = case maintainers do - nil -> [] - list when is_list(list) -> - Enum.map(list, fn - %{"name" => name} -> name - name when is_binary(name) -> name - _ -> nil - end) - |> Enum.reject(&is_nil/1) - _ -> [] - end + author_list = + case author do + nil -> [] + a when is_binary(a) -> [a] + %{"name" => name} -> [name] + _ -> [] + end + + maintainer_list = + case maintainers do + nil -> + [] + + list when is_list(list) -> + Enum.map(list, fn + %{"name" => name} -> name + name when is_binary(name) -> name + _ -> nil + end) + |> Enum.reject(&is_nil/1) + + _ -> + [] + end (author_list ++ maintainer_list) |> Enum.uniq() end diff --git a/opsm_ex/lib/opsm/registries/nqc.ex b/opsm_ex/lib/opsm/registries/nqc.ex index b3ee9ec2..48e8d5fc 100644 --- a/opsm_ex/lib/opsm/registries/nqc.ex +++ b/opsm_ex/lib/opsm/registries/nqc.ex @@ -27,7 +27,8 @@ defmodule Opsm.Registries.Nqc do name: "nqc-core", url: "https://github.com/hyperpolymath/nextgen-databases", path: "nqc", - description: "NQC core — categorical query semantics, typed relational algebra, functor queries" + description: + "NQC core — categorical query semantics, typed relational algebra, functor queries" }, %{ name: "nqc-parser", @@ -79,17 +80,21 @@ defmodule Opsm.Registries.Nqc do defp fetch_from_registry(name, version) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> target = if version == "latest", do: body["latest_version"], else: version {:ok, parse_registry_package(body, target)} - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end defp search_registry(query, opts) do limit = Keyword.get(opts, :limit, 20) url = "#{@base_url}/packages?q=#{URI.encode(query)}&limit=#{limit}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, &parse_search_result/1)} {:error, reason} -> {:error, reason} @@ -105,6 +110,7 @@ defmodule Opsm.Registries.Nqc do defp registry_versions(name) do url = "#{@base_url}/packages/#{URI.encode(name)}/versions" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, & &1["version"])} {:error, reason} -> {:error, reason} @@ -120,9 +126,12 @@ defmodule Opsm.Registries.Nqc do defp search_curated(query, _opts) do q = String.downcase(query) - results = Enum.filter(@known_packages, fn p -> - String.contains?(String.downcase("#{p.name} #{p.description}"), q) - end) + + results = + Enum.filter(@known_packages, fn p -> + String.contains?(String.downcase("#{p.name} #{p.description}"), q) + end) + {:ok, Enum.map(results, &curated_to_resolved(&1, "latest"))} end @@ -135,13 +144,18 @@ defmodule Opsm.Registries.Nqc do pkg_info.url |> String.replace("https://github.com/", "https://api.github.com/repos/") |> Kernel.<>("/tags") + case VerifiedHttp.get_json(tags_url, receive_timeout: 10_000) do {:ok, tags} when is_list(tags) -> versions = Enum.map(tags, & &1["name"]) |> Enum.reject(&is_nil/1) {:ok, if(versions == [], do: ["main"], else: versions)} - _ -> {:ok, ["main"]} + + _ -> + {:ok, ["main"]} end - :not_found -> {:error, :not_found} + + :not_found -> + {:error, :not_found} end end @@ -150,6 +164,7 @@ defmodule Opsm.Registries.Nqc do "https://github.com/hyperpolymath/#{name}", "https://github.com/hyperpolymath/nqc-#{name}" ] + Enum.find_value(urls, {:error, :not_found}, fn url -> case fetch_git_manifest(%{name: name, url: url, description: nil}, version) do {:ok, pkg} -> {:ok, pkg} @@ -164,8 +179,11 @@ defmodule Opsm.Registries.Nqc do base = pkg_info.url try_url = fn manifest -> - url = if path, do: "#{base}/raw/#{branch}/#{path}/#{manifest}", - else: "#{base}/raw/#{branch}/#{manifest}" + url = + if path, + do: "#{base}/raw/#{branch}/#{path}/#{manifest}", + else: "#{base}/raw/#{branch}/#{manifest}" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: text}} -> {:ok, text} _ -> :not_found @@ -185,61 +203,97 @@ defmodule Opsm.Registries.Nqc do pkg_name = fields["name"] || pkg_info.name pkg = %ResolvedPackage{ - package: pkg_name, version: fields["version"] || version, forth: :nqc, - registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + package: pkg_name, + version: fields["version"] || version, + forth: :nqc, + registry_url: pkg_info.url, + tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", + checksum: nil, + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: pkg_name, version: fields["version"] || version, + name: pkg_name, + version: fields["version"] || version, description: fields["description"] || pkg_info[:description], license: fields["license"] || "MPL-2.0", homepage: fields["homepage"] || pkg_info.url, repository: fields["repository"] || pkg_info.url, authors: parse_toml_array(fields["authors"]) || default_authors(), keywords: parse_toml_array(fields["keywords"]) || default_keywords(), - dependencies: %{}, dev_dependencies: %{}, - source_forth: :nqc, raw_manifest: fields + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :nqc, + raw_manifest: fields }, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } + {:ok, pkg} end defp curated_to_resolved(pkg_info, version) do %ResolvedPackage{ - package: pkg_info.name, version: version, forth: :nqc, - registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + package: pkg_info.name, + version: version, + forth: :nqc, + registry_url: pkg_info.url, + tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", + checksum: nil, + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: pkg_info.name, version: version, description: pkg_info.description, - license: "MPL-2.0", homepage: pkg_info.url, repository: pkg_info.url, - authors: default_authors(), keywords: default_keywords(), - dependencies: %{}, dev_dependencies: %{}, - source_forth: :nqc, raw_manifest: %{"registry" => "nqc-curated"} + name: pkg_info.name, + version: version, + description: pkg_info.description, + license: "MPL-2.0", + homepage: pkg_info.url, + repository: pkg_info.url, + authors: default_authors(), + keywords: default_keywords(), + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :nqc, + raw_manifest: %{"registry" => "nqc-curated"} }, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } end defp parse_registry_package(data, version) do %ResolvedPackage{ - package: data["name"], version: version, forth: :nqc, registry_url: @base_url, + package: data["name"], + version: version, + forth: :nqc, + registry_url: @base_url, tarball_url: "#{@base_url}/packages/#{data["name"]}/#{version}/download", - checksum: data["checksum"], checksum_algo: :sha256, + checksum: data["checksum"], + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: data["name"], version: version, description: data["description"], - license: data["license"], homepage: data["homepage"], repository: data["repository"], - authors: data["authors"] || [], keywords: data["keywords"] || [], + name: data["name"], + version: version, + description: data["description"], + license: data["license"], + homepage: data["homepage"], + repository: data["repository"], + authors: data["authors"] || [], + keywords: data["keywords"] || [], dependencies: data["dependencies"] || %{}, dev_dependencies: data["dev_dependencies"] || %{}, - source_forth: :nqc, raw_manifest: data + source_forth: :nqc, + raw_manifest: data }, - attestations: data["attestations"] || [], resolved_deps: [] + attestations: data["attestations"] || [], + resolved_deps: [] } end defp parse_search_result(r) do - %{name: r["name"], version: r["latest_version"], - description: r["description"], downloads: r["downloads"] || 0} + %{ + name: r["name"], + version: r["latest_version"], + description: r["description"], + downloads: r["downloads"] || 0 + } end defp find_curated(name) do @@ -254,23 +308,34 @@ defmodule Opsm.Registries.Nqc do String.split(toml_text, "\n") |> Enum.reduce({false, %{}}, fn line, {inside, acc} -> t = String.trim(line) + cond do - t == "[#{section_name}]" -> {true, acc} - String.starts_with?(t, "[") -> {false, acc} + t == "[#{section_name}]" -> + {true, acc} + + String.starts_with?(t, "[") -> + {false, acc} + inside -> case String.split(t, " = ", parts: 2) do [k, v] -> {true, Map.put(acc, String.trim(k), String.trim(v, "\""))} _ -> {inside, acc} end - true -> {inside, acc} + + true -> + {inside, acc} end end) + fields end defp parse_toml_array(nil), do: nil + defp parse_toml_array(str) when is_binary(str) do - str |> String.trim("[") |> String.trim("]") + str + |> String.trim("[") + |> String.trim("]") |> String.split(",") |> Enum.map(&(&1 |> String.trim() |> String.trim("\""))) |> Enum.reject(&(&1 == "")) diff --git a/opsm_ex/lib/opsm/registries/nuget.ex b/opsm_ex/lib/opsm/registries/nuget.ex index 7024cac0..7073714d 100644 --- a/opsm_ex/lib/opsm/registries/nuget.ex +++ b/opsm_ex/lib/opsm/registries/nuget.ex @@ -26,41 +26,49 @@ defmodule Opsm.Registries.NuGet do # Registration pages contain version catalogs # Some pages inline items, others have only @id and need separate fetching items = body["items"] || [] - all_entries = items + + all_entries = + items |> Enum.flat_map(fn page -> case page["items"] do nil -> # Page doesn't inline items — fetch the page by @id case page["@id"] do - nil -> [] + nil -> + [] + page_url -> case VerifiedHttp.get_json(page_url, receive_timeout: 10_000) do {:ok, page_body} -> page_body["items"] || [] _ -> [] end end - inlined -> inlined + + inlined -> + inlined end end) - target_version = if version == "latest" do - # Find the latest non-prerelease version - all_entries - |> Enum.map(fn entry -> - catalog = entry["catalogEntry"] || %{} - catalog["version"] + target_version = + if version == "latest" do + # Find the latest non-prerelease version + all_entries + |> Enum.map(fn entry -> + catalog = entry["catalogEntry"] || %{} + catalog["version"] + end) + |> Enum.reject(&is_nil/1) + |> Enum.reject(fn v -> String.contains?(v, "-") end) + |> List.last() + else + version + end + + entry = + Enum.find(all_entries, fn e -> + catalog = e["catalogEntry"] || %{} + catalog["version"] == target_version end) - |> Enum.reject(&is_nil/1) - |> Enum.reject(fn v -> String.contains?(v, "-") end) - |> List.last() - else - version - end - - entry = Enum.find(all_entries, fn e -> - catalog = e["catalogEntry"] || %{} - catalog["version"] == target_version - end) if is_nil(target_version) do {:error, :not_found} @@ -92,6 +100,7 @@ defmodule Opsm.Registries.NuGet do groups |> Enum.flat_map(fn group -> deps = group["dependencies"] || [] + Enum.map(deps, fn dep -> {dep["id"], dep["range"] || ">= 0.0.0"} end) @@ -110,14 +119,17 @@ defmodule Opsm.Registries.NuGet do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> data = body["data"] || [] - results = Enum.map(data, fn pkg -> - %{ - name: pkg["id"], - version: pkg["version"], - description: pkg["description"], - downloads: pkg["totalDownloads"] || 0 - } - end) + + results = + Enum.map(data, fn pkg -> + %{ + name: pkg["id"], + version: pkg["version"], + description: pkg["description"], + downloads: pkg["totalDownloads"] || 0 + } + end) + {:ok, results} {:error, %{status: status}} -> @@ -178,17 +190,19 @@ defmodule Opsm.Registries.NuGet do if hash do # NuGet provides base64-encoded hashes; convert to hex - hex_hash = case Base.decode64(hash) do - {:ok, raw} -> Base.encode16(raw, case: :lower) - :error -> hash - end - - algo = case algo_str do - "SHA512" -> :sha512 - "SHA256" -> :sha256 - "SHA1" -> :sha1 - _ -> :sha512 - end + hex_hash = + case Base.decode64(hash) do + {:ok, raw} -> Base.encode16(raw, case: :lower) + :error -> hash + end + + algo = + case algo_str do + "SHA512" -> :sha512 + "SHA256" -> :sha256 + "SHA1" -> :sha1 + _ -> :sha512 + end {hex_hash, algo} else @@ -204,7 +218,8 @@ defmodule Opsm.Registries.NuGet do version: version, forth: :nuget, registry_url: "https://www.nuget.org/packages/#{name}", - tarball_url: "#{@download_base}/#{String.downcase(name)}/#{String.downcase(version)}/#{String.downcase(name)}.#{String.downcase(version)}.nupkg", + tarball_url: + "#{@download_base}/#{String.downcase(name)}/#{String.downcase(version)}/#{String.downcase(name)}.#{String.downcase(version)}.nupkg", checksum: hash, checksum_algo: hash_algo, manifest: %ManifestFormat{ @@ -227,8 +242,10 @@ defmodule Opsm.Registries.NuGet do end defp parse_authors(nil), do: [] + defp parse_authors(authors) when is_binary(authors) do String.split(authors, ",") |> Enum.map(&String.trim/1) end + defp parse_authors(authors) when is_list(authors), do: authors end diff --git a/opsm_ex/lib/opsm/registries/oblibeny.ex b/opsm_ex/lib/opsm/registries/oblibeny.ex index d855c01e..3273a5a0 100644 --- a/opsm_ex/lib/opsm/registries/oblibeny.ex +++ b/opsm_ex/lib/opsm/registries/oblibeny.ex @@ -17,7 +17,8 @@ defmodule Opsm.Registries.Oblibeny do alias Opsm.Verified.Http, as: VerifiedHttp @base_url "https://registry.oblibeny.org/api/v1" - @fallback_mode :git # Until registry deployed + # Until registry deployed + @fallback_mode :git @doc """ Fetch package metadata from oblibeny registry. @@ -93,6 +94,7 @@ defmodule Opsm.Registries.Oblibeny do defp registry_exists?(name) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -151,17 +153,19 @@ defmodule Opsm.Registries.Oblibeny do defp git_versions(_name) do # For git mode, versions are git tags # This requires git ls-remote or GitHub API - {:ok, ["main", "master"]} # Minimal fallback + # Minimal fallback + {:ok, ["main", "master"]} end defp git_fetch(repo_url, version) do # Construct oblibeny.toml URL - manifest_url = case version do - "latest" -> "#{repo_url}/raw/main/oblibeny.toml" - "main" -> "#{repo_url}/raw/main/oblibeny.toml" - "master" -> "#{repo_url}/raw/master/oblibeny.toml" - tag -> "#{repo_url}/raw/#{tag}/oblibeny.toml" - end + manifest_url = + case version do + "latest" -> "#{repo_url}/raw/main/oblibeny.toml" + "main" -> "#{repo_url}/raw/main/oblibeny.toml" + "master" -> "#{repo_url}/raw/master/oblibeny.toml" + tag -> "#{repo_url}/raw/#{tag}/oblibeny.toml" + end case VerifiedHttp.get(manifest_url, receive_timeout: 10_000) do {:ok, %{body: body}} -> @@ -200,7 +204,7 @@ defmodule Opsm.Registries.Oblibeny do end pkg = %ResolvedPackage{ - package: manifest.name || (repo_url |> String.split("/") |> List.last() |> String.trim()), + package: manifest.name || repo_url |> String.split("/") |> List.last() |> String.trim(), version: manifest.version || version, forth: :oblibeny, registry_url: repo_url, @@ -225,7 +229,8 @@ defmodule Opsm.Registries.Oblibeny do registry_url: @base_url, tarball_url: "#{@base_url}/packages/#{pkg_data["name"]}/#{version}/download", checksum: pkg_data["checksum"], - checksum_algo: :sha3_512, # Oblibeny uses SHA3-512 + # Oblibeny uses SHA3-512 + checksum_algo: :sha3_512, manifest: %ManifestFormat{ name: pkg_data["name"], version: version, diff --git a/opsm_ex/lib/opsm/registries/opam.ex b/opsm_ex/lib/opsm/registries/opam.ex index 9470241b..1f5a141c 100644 --- a/opsm_ex/lib/opsm/registries/opam.ex +++ b/opsm_ex/lib/opsm/registries/opam.ex @@ -16,11 +16,12 @@ defmodule Opsm.Registries.Opam do Fetch package metadata from the OPAM registry. """ def fetch_package(name, version \\ "latest") do - target_version = if version == "latest" do - fetch_latest_version(name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(name) + else + version + end case target_version do nil -> @@ -29,6 +30,7 @@ defmodule Opsm.Registries.Opam do ver -> # Fetch OPAM file for package metadata url = "#{@base_url}/packages/#{name}/#{name}.#{ver}/opam" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: body}} when is_binary(body) -> deps = parse_opam_deps(body) @@ -68,10 +70,12 @@ defmodule Opsm.Registries.Opam do |> String.split("\n") |> Enum.reduce({false, %{}}, fn line, {in_depends, deps} -> trimmed = String.trim(line) + cond do String.starts_with?(trimmed, "depends:") -> # Start of depends block rest = String.replace_prefix(trimmed, "depends:", "") |> String.trim() + if String.starts_with?(rest, "[") do # Parse inline dependencies parse_depends_line(rest, deps) @@ -137,6 +141,7 @@ defmodule Opsm.Registries.Opam do case versions_internal(query) do {:ok, [latest | _]} -> {:ok, [%{name: query, version: latest, description: "OCaml package", downloads: 0}]} + _ -> {:ok, []} end @@ -175,6 +180,7 @@ defmodule Opsm.Registries.Opam do case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: body}} when is_binary(body) -> versions = parse_versions_from_html(body, name) + if versions == [] do {:error, :not_found} else @@ -183,6 +189,7 @@ defmodule Opsm.Registries.Opam do {:ok, body} when is_binary(body) -> versions = parse_versions_from_html(body, name) + if versions == [] do {:error, :not_found} else @@ -254,10 +261,12 @@ defmodule Opsm.Registries.Opam do trimmed = String.trim(line) if String.starts_with?(trimmed, "src:") do - url = trimmed + url = + trimmed |> String.replace_prefix("src:", "") |> String.trim() |> String.trim("\"") + {:halt, url} else {:cont, nil} @@ -312,17 +321,19 @@ defmodule Opsm.Registries.Opam do trimmed = String.trim(line) if String.starts_with?(trimmed, "checksum:") do - raw = trimmed + raw = + trimmed |> String.replace_prefix("checksum:", "") |> String.trim() |> String.trim("\"") - result = case String.split(raw, "=", parts: 2) do - ["sha256", value] -> {value, :sha256} - ["sha512", value] -> {value, :sha512} - ["md5", value] -> {value, :md5} - _ -> {nil, nil} - end + result = + case String.split(raw, "=", parts: 2) do + ["sha256", value] -> {value, :sha256} + ["sha512", value] -> {value, :sha512} + ["md5", value] -> {value, :md5} + _ -> {nil, nil} + end {:halt, result} else @@ -349,9 +360,11 @@ defmodule Opsm.Registries.Opam do |> String.split("\n") |> Enum.reduce({false, []}, fn line, {in_authors, authors} -> trimmed = String.trim(line) + cond do String.starts_with?(trimmed, "authors:") -> rest = String.replace_prefix(trimmed, "authors:", "") |> String.trim() + if String.starts_with?(rest, "[") and String.contains?(rest, "]") do # Single line authors parsed = parse_authors_line(rest) diff --git a/opsm_ex/lib/opsm/registries/openupm.ex b/opsm_ex/lib/opsm/registries/openupm.ex index eacf678c..8f3aff58 100644 --- a/opsm_ex/lib/opsm/registries/openupm.ex +++ b/opsm_ex/lib/opsm/registries/openupm.ex @@ -23,11 +23,12 @@ defmodule Opsm.Registries.OpenUpm do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - target_version = if version == "latest" do - get_in(body, ["dist-tags", "latest"]) - else - version - end + target_version = + if version == "latest" do + get_in(body, ["dist-tags", "latest"]) + else + version + end version_data = get_in(body, ["versions", target_version]) || %{} deps = extract_deps(version_data) @@ -57,17 +58,20 @@ defmodule Opsm.Registries.OpenUpm do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"objects" => objects}} when is_list(objects) -> - results = objects - |> Enum.take(limit) - |> Enum.map(fn obj -> - pkg = obj["package"] || %{} - %{ - name: pkg["name"], - version: pkg["version"], - description: pkg["description"] || "", - downloads: get_in(obj, ["score", "detail", "popularity"]) || 0 - } - end) + results = + objects + |> Enum.take(limit) + |> Enum.map(fn obj -> + pkg = obj["package"] || %{} + + %{ + name: pkg["name"], + version: pkg["version"], + description: pkg["description"] || "", + downloads: get_in(obj, ["score", "detail", "popularity"]) || 0 + } + end) + {:ok, results} {:ok, _} -> @@ -87,16 +91,18 @@ defmodule Opsm.Registries.OpenUpm do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, packages} when is_list(packages) -> - results = packages - |> Enum.take(limit) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: pkg["version"] || pkg["ver"], - description: pkg["description"] || pkg["displayName"] || "", - downloads: pkg["downloads"] || 0 - } - end) + results = + packages + |> Enum.take(limit) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: pkg["version"] || pkg["ver"], + description: pkg["description"] || pkg["displayName"] || "", + downloads: pkg["downloads"] || 0 + } + end) + {:ok, results} _ -> @@ -124,9 +130,11 @@ defmodule Opsm.Registries.OpenUpm do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions}} when is_map(versions) -> - ver_list = versions - |> Map.keys() - |> Enum.sort(:desc) + ver_list = + versions + |> Map.keys() + |> Enum.sort(:desc) + {:ok, ver_list} {:error, :not_found} -> @@ -210,6 +218,7 @@ defmodule Opsm.Registries.OpenUpm do defp extract_authors(nil), do: [] defp extract_authors(author) when is_binary(author), do: [author] defp extract_authors(%{"name" => name}), do: [name] + defp extract_authors(authors) when is_list(authors) do Enum.map(authors, fn a when is_binary(a) -> a @@ -218,5 +227,6 @@ defmodule Opsm.Registries.OpenUpm do end) |> Enum.reject(&is_nil/1) end + defp extract_authors(_), do: [] end diff --git a/opsm_ex/lib/opsm/registries/packagist.ex b/opsm_ex/lib/opsm/registries/packagist.ex index c9f6b84b..32cc6a98 100644 --- a/opsm_ex/lib/opsm/registries/packagist.ex +++ b/opsm_ex/lib/opsm/registries/packagist.ex @@ -20,11 +20,12 @@ defmodule Opsm.Registries.Packagist do # Packagist packages use vendor/package format like "symfony/console" case String.split(name, "/") do [vendor, package] -> - target_version = if version == "latest" do - fetch_latest_version(vendor, package) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(vendor, package) + else + version + end case target_version do nil -> @@ -33,6 +34,7 @@ defmodule Opsm.Registries.Packagist do ver -> # Fetch package metadata url = "#{@api_url}/p2/#{vendor}/#{package}.json" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> parse_package(name, body, ver) @@ -59,6 +61,7 @@ defmodule Opsm.Registries.Packagist do defp fetch_latest_version(vendor, package) do full_name = "#{vendor}/#{package}" url = "#{@api_url}/p2/#{full_name}.json" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"packages" => %{^full_name => versions}}} when is_list(versions) -> # Packagist v2 returns a list of version objects, newest first @@ -89,9 +92,12 @@ defmodule Opsm.Registries.Packagist do |> Enum.map(& &1["version"]) |> Enum.reject(&is_nil/1) |> Enum.reject(&String.contains?(&1, "dev")) - _ -> [] + + _ -> + [] end end + defp get_all_versions_from_body(_), do: [] @doc """ @@ -104,14 +110,16 @@ defmodule Opsm.Registries.Packagist do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> - packages = Enum.map(results, fn pkg -> - %{ - name: Map.get(pkg, "name", ""), - version: Map.get(pkg, "version", "unknown"), - description: Map.get(pkg, "description", ""), - downloads: Map.get(pkg, "downloads", 0) - } - end) + packages = + Enum.map(results, fn pkg -> + %{ + name: Map.get(pkg, "name", ""), + version: Map.get(pkg, "version", "unknown"), + description: Map.get(pkg, "description", ""), + downloads: Map.get(pkg, "downloads", 0) + } + end) + {:ok, packages} {:ok, _} -> @@ -151,6 +159,7 @@ defmodule Opsm.Registries.Packagist do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> versions = get_all_versions_from_body(body) + if versions == [] do {:error, :not_found} else @@ -181,6 +190,7 @@ defmodule Opsm.Registries.Packagist do case String.split(name, "/") do [vendor, package] -> url = "#{@api_url}/p2/#{vendor}/#{package}.json" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> extract_dist_url(body, name, version) @@ -201,9 +211,12 @@ defmodule Opsm.Registries.Packagist do %{"dist" => %{"url" => url}} -> {:ok, url} _ -> {:error, :not_found} end - _ -> {:error, :not_found} + + _ -> + {:error, :not_found} end end + defp extract_dist_url(_, _, _), do: {:error, :not_found} # Parsers @@ -214,31 +227,32 @@ defmodule Opsm.Registries.Packagist do deps = parse_composer_deps(version_data) {dist_url, checksum} = extract_dist_info(version_data) - {:ok, %ResolvedPackage{ - package: name, - version: version, - forth: :packagist, - registry_url: "#{@search_url}/packages/#{name}", - tarball_url: dist_url, - checksum: checksum, - checksum_algo: if(checksum, do: :sha256, else: nil), - manifest: %ManifestFormat{ - name: name, - version: version, - description: Map.get(version_data, "description"), - license: parse_license(Map.get(version_data, "license")), - homepage: Map.get(version_data, "homepage"), - repository: extract_repo_url(version_data), - authors: parse_authors(Map.get(version_data, "authors", [])), - keywords: Map.get(version_data, "keywords", []), - dependencies: deps, - dev_dependencies: %{}, - source_forth: :packagist, - raw_manifest: version_data - }, - attestations: [], - resolved_deps: [] - }} + {:ok, + %ResolvedPackage{ + package: name, + version: version, + forth: :packagist, + registry_url: "#{@search_url}/packages/#{name}", + tarball_url: dist_url, + checksum: checksum, + checksum_algo: if(checksum, do: :sha256, else: nil), + manifest: %ManifestFormat{ + name: name, + version: version, + description: Map.get(version_data, "description"), + license: parse_license(Map.get(version_data, "license")), + homepage: Map.get(version_data, "homepage"), + repository: extract_repo_url(version_data), + authors: parse_authors(Map.get(version_data, "authors", [])), + keywords: Map.get(version_data, "keywords", []), + dependencies: deps, + dev_dependencies: %{}, + source_forth: :packagist, + raw_manifest: version_data + }, + attestations: [], + resolved_deps: [] + }} {:error, reason} -> {:error, reason} @@ -252,9 +266,12 @@ defmodule Opsm.Registries.Packagist do nil -> {:error, :version_not_found} version_data -> {:ok, version_data} end - _ -> {:error, :not_found} + + _ -> + {:error, :not_found} end end + defp extract_version_data(_, _, _), do: {:error, :invalid_response} defp parse_composer_deps(%{"require" => require}) when is_map(require) do @@ -262,14 +279,17 @@ defmodule Opsm.Registries.Packagist do |> Enum.reject(fn {pkg, _} -> pkg == "php" or String.starts_with?(pkg, "ext-") end) |> Enum.into(%{}) end + defp parse_composer_deps(_), do: %{} defp extract_dist_info(%{"dist" => %{"url" => url, "shasum" => shasum}}) do {url, shasum} end + defp extract_dist_info(%{"dist" => %{"url" => url}}) do {url, nil} end + defp extract_dist_info(_), do: {nil, nil} defp parse_license(nil), do: nil @@ -284,6 +304,7 @@ defmodule Opsm.Registries.Packagist do _ -> "Unknown" end) end + defp parse_authors(_), do: [] defp extract_repo_url(%{"source" => %{"url" => url}}), do: url diff --git a/opsm_ex/lib/opsm/registries/pacstall.ex b/opsm_ex/lib/opsm/registries/pacstall.ex index 2bc1963f..fa9d28d9 100644 --- a/opsm_ex/lib/opsm/registries/pacstall.ex +++ b/opsm_ex/lib/opsm/registries/pacstall.ex @@ -22,30 +22,41 @@ defmodule Opsm.Registries.Pacstall do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest" do - body["version"] || body["latestVersion"] || "0.0.0" - else - version - end + ver = + if version == "latest" do + body["version"] || body["latestVersion"] || "0.0.0" + else + version + end {:ok, parse_pacstall(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp parse_pacstall(name, body, version) do # Pacstall packages declare dependencies as space-separated strings raw_deps = body["dependencies"] || body["depends"] || [] - deps = case raw_deps do - d when is_binary(d) -> - d |> String.split() |> Enum.map(fn dep -> {dep, "*"} end) |> Map.new() - d when is_list(d) -> - d |> Enum.map(fn dep -> {dep, "*"} end) |> Map.new() - _ -> %{} - end + + deps = + case raw_deps do + d when is_binary(d) -> + d |> String.split() |> Enum.map(fn dep -> {dep, "*"} end) |> Map.new() + + d when is_list(d) -> + d |> Enum.map(fn dep -> {dep, "*"} end) |> Map.new() + + _ -> + %{} + end manifest = %ManifestFormat{ name: name, @@ -85,31 +96,38 @@ defmodule Opsm.Registries.Pacstall do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"] || pkg["packageName"], - version: pkg["version"] || pkg["latestVersion"], - description: pkg["description"] || pkg["pkgdesc"] - } - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"] || pkg["packageName"], + version: pkg["version"] || pkg["latestVersion"], + description: pkg["description"] || pkg["pkgdesc"] + } + end) + {:ok, results} {:ok, %{"packages" => packages}} when is_list(packages) -> - results = packages - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"] || pkg["packageName"], - version: pkg["version"] || pkg["latestVersion"], - description: pkg["description"] || pkg["pkgdesc"] - } - end) + results = + packages + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"] || pkg["packageName"], + version: pkg["version"] || pkg["latestVersion"], + description: pkg["description"] || pkg["pkgdesc"] + } + end) + {:ok, results} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/pear.ex b/opsm_ex/lib/opsm/registries/pear.ex index 557231a0..9d538557 100644 --- a/opsm_ex/lib/opsm/registries/pear.ex +++ b/opsm_ex/lib/opsm/registries/pear.ex @@ -107,9 +107,14 @@ defmodule Opsm.Registries.Pear do vers = Enum.map(releases, fn r -> r["version"] || r["v"] end) {:ok, Enum.reject(vers, &is_nil/1)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/pecl.ex b/opsm_ex/lib/opsm/registries/pecl.ex index 06248faf..a88910e4 100644 --- a/opsm_ex/lib/opsm/registries/pecl.ex +++ b/opsm_ex/lib/opsm/registries/pecl.ex @@ -98,9 +98,14 @@ defmodule Opsm.Registries.Pecl do vers = Enum.map(releases, fn r -> r["version"] || r["v"] end) {:ok, Enum.reject(vers, &is_nil/1)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/phronesis.ex b/opsm_ex/lib/opsm/registries/phronesis.ex index f527c1bb..51122a64 100644 --- a/opsm_ex/lib/opsm/registries/phronesis.ex +++ b/opsm_ex/lib/opsm/registries/phronesis.ex @@ -85,17 +85,21 @@ defmodule Opsm.Registries.Phronesis do defp fetch_from_registry(name, version) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> target = if version == "latest", do: body["latest_version"], else: version {:ok, parse_registry_package(body, target)} - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end defp search_registry(query, opts) do limit = Keyword.get(opts, :limit, 20) url = "#{@base_url}/packages?q=#{URI.encode(query)}&limit=#{limit}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, &parse_search_result/1)} {:error, reason} -> {:error, reason} @@ -111,6 +115,7 @@ defmodule Opsm.Registries.Phronesis do defp registry_versions(name) do url = "#{@base_url}/packages/#{URI.encode(name)}/versions" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, & &1["version"])} {:error, reason} -> {:error, reason} @@ -126,9 +131,12 @@ defmodule Opsm.Registries.Phronesis do defp search_curated(query, _opts) do q = String.downcase(query) - results = Enum.filter(@known_packages, fn p -> - String.contains?(String.downcase("#{p.name} #{p.description}"), q) - end) + + results = + Enum.filter(@known_packages, fn p -> + String.contains?(String.downcase("#{p.name} #{p.description}"), q) + end) + {:ok, Enum.map(results, &curated_to_resolved(&1, "latest"))} end @@ -141,13 +149,18 @@ defmodule Opsm.Registries.Phronesis do pkg_info.url |> String.replace("https://github.com/", "https://api.github.com/repos/") |> Kernel.<>("/tags") + case VerifiedHttp.get_json(tags_url, receive_timeout: 10_000) do {:ok, tags} when is_list(tags) -> versions = Enum.map(tags, & &1["name"]) |> Enum.reject(&is_nil/1) {:ok, if(versions == [], do: ["main"], else: versions)} - _ -> {:ok, ["main"]} + + _ -> + {:ok, ["main"]} end - :not_found -> {:error, :not_found} + + :not_found -> + {:error, :not_found} end end @@ -156,6 +169,7 @@ defmodule Opsm.Registries.Phronesis do "https://github.com/hyperpolymath/#{name}", "https://github.com/hyperpolymath/phronesis-#{name}" ] + Enum.find_value(urls, {:error, :not_found}, fn url -> case fetch_git_manifest(%{name: name, url: url, description: nil}, version) do {:ok, pkg} -> {:ok, pkg} @@ -170,8 +184,11 @@ defmodule Opsm.Registries.Phronesis do base = pkg_info.url try_url = fn manifest -> - url = if path, do: "#{base}/raw/#{branch}/#{path}/#{manifest}", - else: "#{base}/raw/#{branch}/#{manifest}" + url = + if path, + do: "#{base}/raw/#{branch}/#{path}/#{manifest}", + else: "#{base}/raw/#{branch}/#{manifest}" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: text}} -> {:ok, text} _ -> :not_found @@ -215,49 +232,73 @@ defmodule Opsm.Registries.Phronesis do attestations: [], resolved_deps: [] } + {:ok, pkg} end defp curated_to_resolved(pkg_info, version) do %ResolvedPackage{ - package: pkg_info.name, version: version, forth: :phronesis, + package: pkg_info.name, + version: version, + forth: :phronesis, registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + checksum: nil, + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: pkg_info.name, version: version, - description: pkg_info.description, license: "MPL-2.0", - homepage: pkg_info.url, repository: pkg_info.url, - authors: default_authors(), keywords: default_keywords(), - dependencies: %{}, dev_dependencies: %{}, - source_forth: :phronesis, raw_manifest: %{"registry" => "phronesis-curated"} + name: pkg_info.name, + version: version, + description: pkg_info.description, + license: "MPL-2.0", + homepage: pkg_info.url, + repository: pkg_info.url, + authors: default_authors(), + keywords: default_keywords(), + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :phronesis, + raw_manifest: %{"registry" => "phronesis-curated"} }, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } end defp parse_registry_package(data, version) do %ResolvedPackage{ - package: data["name"], version: version, forth: :phronesis, + package: data["name"], + version: version, + forth: :phronesis, registry_url: @base_url, tarball_url: "#{@base_url}/packages/#{data["name"]}/#{version}/download", - checksum: data["checksum"], checksum_algo: :sha256, + checksum: data["checksum"], + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: data["name"], version: version, description: data["description"], - license: data["license"], homepage: data["homepage"], + name: data["name"], + version: version, + description: data["description"], + license: data["license"], + homepage: data["homepage"], repository: data["repository"], - authors: data["authors"] || [], keywords: data["keywords"] || [], + authors: data["authors"] || [], + keywords: data["keywords"] || [], dependencies: data["dependencies"] || %{}, dev_dependencies: data["dev_dependencies"] || %{}, - source_forth: :phronesis, raw_manifest: data + source_forth: :phronesis, + raw_manifest: data }, - attestations: data["attestations"] || [], resolved_deps: [] + attestations: data["attestations"] || [], + resolved_deps: [] } end defp parse_search_result(r) do - %{name: r["name"], version: r["latest_version"], - description: r["description"], downloads: r["downloads"] || 0} + %{ + name: r["name"], + version: r["latest_version"], + description: r["description"], + downloads: r["downloads"] || 0 + } end defp find_curated(name) do @@ -272,23 +313,34 @@ defmodule Opsm.Registries.Phronesis do String.split(toml_text, "\n") |> Enum.reduce({false, %{}}, fn line, {inside, acc} -> t = String.trim(line) + cond do - t == "[#{section_name}]" -> {true, acc} - String.starts_with?(t, "[") -> {false, acc} + t == "[#{section_name}]" -> + {true, acc} + + String.starts_with?(t, "[") -> + {false, acc} + inside -> case String.split(t, " = ", parts: 2) do [k, v] -> {true, Map.put(acc, String.trim(k), String.trim(v, "\""))} _ -> {inside, acc} end - true -> {inside, acc} + + true -> + {inside, acc} end end) + fields end defp parse_toml_array(nil), do: nil + defp parse_toml_array(str) when is_binary(str) do - str |> String.trim("[") |> String.trim("]") + str + |> String.trim("[") + |> String.trim("]") |> String.split(",") |> Enum.map(&(&1 |> String.trim() |> String.trim("\""))) |> Enum.reject(&(&1 == "")) diff --git a/opsm_ex/lib/opsm/registries/pkgsrc.ex b/opsm_ex/lib/opsm/registries/pkgsrc.ex index 19b08464..061a91e7 100644 --- a/opsm_ex/lib/opsm/registries/pkgsrc.ex +++ b/opsm_ex/lib/opsm/registries/pkgsrc.ex @@ -23,14 +23,21 @@ defmodule Opsm.Registries.Pkgsrc do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: body["version"] || body["pkgversion"], - else: version + ver = + if version == "latest", + do: body["version"] || body["pkgversion"], + else: version + {:ok, parse_pkgsrc_package(name, body, ver)} - {:error, :not_found} -> fetch_from_cdn(name, version) - {:error, %{status: 404}} -> fetch_from_cdn(name, version) - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + fetch_from_cdn(name, version) + + {:error, %{status: 404}} -> + fetch_from_cdn(name, version) + + {:error, reason} -> + {:error, reason} end end @@ -39,18 +46,25 @@ defmodule Opsm.Registries.Pkgsrc do _url = "#{@cdn_url}/current/#{category}/#{pkg_name}/DESCR" case VerifiedHttp.get_json( - "#{@cdn_url}/packages/NetBSD/amd64/current/All/#{pkg_name}.json", - receive_timeout: 10_000 - ) do + "#{@cdn_url}/packages/NetBSD/amd64/current/All/#{pkg_name}.json", + receive_timeout: 10_000 + ) do {:ok, body} -> - ver = if version == "latest", - do: body["version"] || "0.0.0", - else: version + ver = + if version == "latest", + do: body["version"] || "0.0.0", + else: version + {:ok, build_resolved_from_cdn(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -62,18 +76,22 @@ defmodule Opsm.Registries.Pkgsrc do end defp parse_pkgsrc_package(name, body, version) do - deps = (body["depends"] || body["build_depends"] || []) - |> Enum.map(fn - d when is_binary(d) -> - dep_name = d |> String.replace(~r/[><=\[\{].*/, "") |> String.trim() - {dep_name, "*"} - d when is_map(d) -> - {d["pkgpath"] || d["name"] || "", "*"} - _ -> nil - end) - |> Enum.reject(&is_nil/1) - |> Enum.reject(fn {n, _} -> n == "" end) - |> Map.new() + deps = + (body["depends"] || body["build_depends"] || []) + |> Enum.map(fn + d when is_binary(d) -> + dep_name = d |> String.replace(~r/[><=\[\{].*/, "") |> String.trim() + {dep_name, "*"} + + d when is_map(d) -> + {d["pkgpath"] || d["name"] || "", "*"} + + _ -> + nil + end) + |> Enum.reject(&is_nil/1) + |> Enum.reject(fn {n, _} -> n == "" end) + |> Map.new() pkgpath = body["pkgpath"] || name @@ -95,7 +113,7 @@ defmodule Opsm.Registries.Pkgsrc do tarball_url: build_distfile_url(body), checksum: body["digest"] || body["sha256"], checksum_algo: if(body["digest"] || body["sha256"], do: :sha256), - attestations: [], + attestations: [] } end @@ -118,7 +136,7 @@ defmodule Opsm.Registries.Pkgsrc do tarball_url: body["file_name"], checksum: body["digest"], checksum_algo: if(body["digest"], do: :sha256), - attestations: [], + attestations: [] } end @@ -147,27 +165,38 @@ defmodule Opsm.Registries.Pkgsrc do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["pkgpath"] || pkg["name"], - version: pkg["version"], - description: pkg["comment"] || pkg["description"] - } - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["pkgpath"] || pkg["name"], + version: pkg["version"], + description: pkg["comment"] || pkg["description"] + } + end) + {:ok, results} {:ok, %{"results" => results}} when is_list(results) -> - hits = results - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{name: pkg["pkgpath"] || pkg["name"], version: pkg["version"], description: pkg["comment"]} - end) + hits = + results + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["pkgpath"] || pkg["name"], + version: pkg["version"], + description: pkg["comment"] + } + end) + {:ok, hits} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/portage.ex b/opsm_ex/lib/opsm/registries/portage.ex index 7ad87599..af763168 100644 --- a/opsm_ex/lib/opsm/registries/portage.ex +++ b/opsm_ex/lib/opsm/registries/portage.ex @@ -22,24 +22,34 @@ defmodule Opsm.Registries.Portage do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest" do - versions_list = body["versions"] || [] - case Enum.find(versions_list, fn v -> v["arch"] == "amd64" end) do - nil -> - case versions_list do - [latest | _] -> latest["version"] - _ -> "0.0.0" - end - stable -> stable["version"] + ver = + if version == "latest" do + versions_list = body["versions"] || [] + + case Enum.find(versions_list, fn v -> v["arch"] == "amd64" end) do + nil -> + case versions_list do + [latest | _] -> latest["version"] + _ -> "0.0.0" + end + + stable -> + stable["version"] + end + else + version end - else - version - end + {:ok, parse_portage_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -51,13 +61,14 @@ defmodule Opsm.Registries.Portage do end defp parse_portage_package(name, body, version) do - deps = (body["dependencies"] || []) - |> Enum.map(fn d -> - dep_atom = d["atom"] || d["name"] || "" - {dep_atom, d["condition"] || "*"} - end) - |> Enum.reject(fn {n, _} -> n == "" end) - |> Map.new() + deps = + (body["dependencies"] || []) + |> Enum.map(fn d -> + dep_atom = d["atom"] || d["name"] || "" + {dep_atom, d["condition"] || "*"} + end) + |> Enum.reject(fn {n, _} -> n == "" end) + |> Map.new() manifest = %ManifestFormat{ name: name, @@ -76,7 +87,7 @@ defmodule Opsm.Registries.Portage do manifest: manifest, tarball_url: nil, checksum: nil, - attestations: [], + attestations: [] } end @@ -89,13 +100,16 @@ defmodule Opsm.Registries.Portage do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - versions_list = (body["versions"] || []) - |> Enum.map(fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) - |> Enum.uniq() + versions_list = + (body["versions"] || []) + |> Enum.map(fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + {:ok, versions_list} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -107,31 +121,38 @@ defmodule Opsm.Registries.Portage do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: "#{pkg["category"]}/#{pkg["name"]}", - version: get_in(pkg, ["versions", Access.at(0), "version"]), - description: pkg["description"] - } - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: "#{pkg["category"]}/#{pkg["name"]}", + version: get_in(pkg, ["versions", Access.at(0), "version"]), + description: pkg["description"] + } + end) + {:ok, results} {:ok, %{"results" => results}} when is_list(results) -> - hits = results - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: "#{pkg["category"]}/#{pkg["name"]}", - version: nil, - description: pkg["description"] - } - end) + hits = + results + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: "#{pkg["category"]}/#{pkg["name"]}", + version: nil, + description: pkg["description"] + } + end) + {:ok, hits} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/pub_dev.ex b/opsm_ex/lib/opsm/registries/pub_dev.ex index 4dbab55b..2345ff5d 100644 --- a/opsm_ex/lib/opsm/registries/pub_dev.ex +++ b/opsm_ex/lib/opsm/registries/pub_dev.ex @@ -23,14 +23,17 @@ defmodule Opsm.Registries.PubDev do versions_list = body["versions"] || [] latest_info = body["latest"] || %{} - target_version = if version == "latest" do - latest_info["version"] - else - version - end + target_version = + if version == "latest" do + latest_info["version"] + else + version + end version_info = Enum.find(versions_list, fn v -> v["version"] == target_version end) - pubspec = if version_info, do: version_info["pubspec"] || %{}, else: latest_info["pubspec"] || %{} + + pubspec = + if version_info, do: version_info["pubspec"] || %{}, else: latest_info["pubspec"] || %{} deps = parse_pubspec_deps(pubspec["dependencies"]) dev_deps = parse_pubspec_deps(pubspec["dev_dependencies"]) @@ -52,6 +55,7 @@ defmodule Opsm.Registries.PubDev do end defp parse_pubspec_deps(nil), do: %{} + defp parse_pubspec_deps(deps) when is_map(deps) do deps |> Enum.reject(fn {_name, constraint} -> @@ -75,7 +79,8 @@ defmodule Opsm.Registries.PubDev do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"packages" => packages}} when is_list(packages) -> - results = packages + results = + packages |> Enum.take(limit) |> Enum.map(fn pkg -> %{ @@ -85,6 +90,7 @@ defmodule Opsm.Registries.PubDev do downloads: 0 } end) + {:ok, results} {:ok, _} -> @@ -121,10 +127,14 @@ defmodule Opsm.Registries.PubDev do case VerifiedHttp.get_json(url, headers: headers, receive_timeout: 10_000) do {:ok, body} -> versions_list = body["versions"] || [] - versions = versions_list + + versions = + versions_list |> Enum.map(& &1["version"]) |> Enum.reject(&is_nil/1) - |> Enum.reverse() # Newest first + # Newest first + |> Enum.reverse() + {:ok, versions} {:error, :not_found} -> diff --git a/opsm_ex/lib/opsm/registries/pulumi.ex b/opsm_ex/lib/opsm/registries/pulumi.ex index efe9226d..4d5ebe3d 100644 --- a/opsm_ex/lib/opsm/registries/pulumi.ex +++ b/opsm_ex/lib/opsm/registries/pulumi.ex @@ -66,6 +66,7 @@ defmodule Opsm.Registries.Pulumi do end defp find_github_release(releases, "latest"), do: List.first(releases) + defp find_github_release(releases, target_version) do Enum.find(releases, List.first(releases), fn r -> tag = String.trim_leading(r["tag_name"] || "", "v") @@ -90,27 +91,31 @@ defmodule Opsm.Registries.Pulumi do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"packages" => packages}} when is_list(packages) -> - results = packages - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: pkg["version"], - description: pkg["description"] - } - end) + results = + packages + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: pkg["version"], + description: pkg["description"] + } + end) + {:ok, results} {:ok, packages} when is_list(packages) -> - results = packages - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: pkg["version"], - description: pkg["description"] - } - end) + results = + packages + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: pkg["version"], + description: pkg["description"] + } + end) + {:ok, results} {:ok, _} -> @@ -128,10 +133,13 @@ defmodule Opsm.Registries.Pulumi do url = "#{@api_url}/packages/#{URI.encode(name)}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do - {:ok, _} -> true + {:ok, _} -> + true + {:error, _} -> # Fallback: check GitHub github_url = "#{@github_api}/repos/pulumi/pulumi-#{name}" + case VerifiedHttp.get_json(github_url, receive_timeout: 10_000) do {:ok, _} -> true {:error, _} -> false @@ -148,15 +156,17 @@ defmodule Opsm.Registries.Pulumi do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions_list}} when is_list(versions_list) -> - ver_strs = versions_list - |> Enum.map(fn v -> - cond do - is_binary(v) -> v - is_map(v) -> v["version"] - true -> nil - end - end) - |> Enum.reject(&is_nil/1) + ver_strs = + versions_list + |> Enum.map(fn v -> + cond do + is_binary(v) -> v + is_map(v) -> v["version"] + true -> nil + end + end) + |> Enum.reject(&is_nil/1) + {:ok, ver_strs} {:ok, _} -> @@ -179,9 +189,11 @@ defmodule Opsm.Registries.Pulumi do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, releases} when is_list(releases) -> - ver_strs = releases - |> Enum.map(fn r -> String.trim_leading(r["tag_name"] || "", "v") end) - |> Enum.reject(fn v -> v == "" end) + ver_strs = + releases + |> Enum.map(fn r -> String.trim_leading(r["tag_name"] || "", "v") end) + |> Enum.reject(fn v -> v == "" end) + {:ok, ver_strs} {:error, _} -> @@ -267,6 +279,7 @@ defmodule Opsm.Registries.Pulumi do end defp truncate_description(nil), do: nil + defp truncate_description(text) do if String.length(text) > 200 do String.slice(text, 0, 197) <> "..." diff --git a/opsm_ex/lib/opsm/registries/puppet_forge.ex b/opsm_ex/lib/opsm/registries/puppet_forge.ex index d9353f89..8ed83ff4 100644 --- a/opsm_ex/lib/opsm/registries/puppet_forge.ex +++ b/opsm_ex/lib/opsm/registries/puppet_forge.ex @@ -23,18 +23,20 @@ defmodule Opsm.Registries.PuppetForge do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - target_version = if version == "latest" do - get_in(body, ["current_release", "version"]) - else - version - end + target_version = + if version == "latest" do + get_in(body, ["current_release", "version"]) + else + version + end # Use current_release or fetch specific version - release_data = if version == "latest" do - body["current_release"] || %{} - else - fetch_release(slug, version) - end + release_data = + if version == "latest" do + body["current_release"] || %{} + else + fetch_release(slug, version) + end deps = extract_deps(release_data) {:ok, parse_package(name, body, release_data, target_version, deps)} @@ -71,16 +73,18 @@ defmodule Opsm.Registries.PuppetForge do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> - packages = results - |> Enum.take(limit) - |> Enum.map(fn mod -> - %{ - name: mod["slug"] || mod["name"], - version: get_in(mod, ["current_release", "version"]), - description: get_in(mod, ["current_release", "metadata", "summary"]) || "", - downloads: mod["downloads"] || 0 - } - end) + packages = + results + |> Enum.take(limit) + |> Enum.map(fn mod -> + %{ + name: mod["slug"] || mod["name"], + version: get_in(mod, ["current_release", "version"]), + description: get_in(mod, ["current_release", "metadata", "summary"]) || "", + downloads: mod["downloads"] || 0 + } + end) + {:ok, packages} {:ok, _} -> @@ -116,9 +120,11 @@ defmodule Opsm.Registries.PuppetForge do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"releases" => releases}} when is_list(releases) -> - ver_list = releases - |> Enum.map(fn r -> r["version"] end) - |> Enum.reject(&is_nil/1) + ver_list = + releases + |> Enum.map(fn r -> r["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_list} {:ok, body} when is_map(body) -> @@ -141,9 +147,11 @@ defmodule Opsm.Registries.PuppetForge do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => releases}} when is_list(releases) -> - ver_list = releases - |> Enum.map(fn r -> r["version"] end) - |> Enum.reject(&is_nil/1) + ver_list = + releases + |> Enum.map(fn r -> r["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_list} _ -> @@ -201,16 +209,18 @@ defmodule Opsm.Registries.PuppetForge do homepage = metadata["project_page"] || body["homepage_url"] repository = metadata["source"] || body["source_url"] authors = extract_authors(metadata) - keywords = metadata["tags"] || body["endorsement"] && [body["endorsement"]] || [] + keywords = metadata["tags"] || (body["endorsement"] && [body["endorsement"]]) || [] file_uri = release_data["file_uri"] download_url = if file_uri, do: "https://forgeapi.puppet.com#{file_uri}", else: nil checksum = release_data["file_md5"] || release_data["file_sha256"] - checksum_algo = cond do - release_data["file_sha256"] -> :sha256 - release_data["file_md5"] -> :md5 - true -> nil - end + + checksum_algo = + cond do + release_data["file_sha256"] -> :sha256 + release_data["file_md5"] -> :md5 + true -> nil + end %ResolvedPackage{ package: name, diff --git a/opsm_ex/lib/opsm/registries/pypi.ex b/opsm_ex/lib/opsm/registries/pypi.ex index 0c38b4ae..fe8192cd 100644 --- a/opsm_ex/lib/opsm/registries/pypi.ex +++ b/opsm_ex/lib/opsm/registries/pypi.ex @@ -15,11 +15,12 @@ defmodule Opsm.Registries.Pypi do Fetch package metadata from PyPI. """ def fetch_package(name, version \\ "latest") do - url = if version == "latest" do - "#{@base_url}/#{URI.encode(name)}/json" - else - "#{@base_url}/#{URI.encode(name)}/#{version}/json" - end + url = + if version == "latest" do + "#{@base_url}/#{URI.encode(name)}/json" + else + "#{@base_url}/#{URI.encode(name)}/#{version}/json" + end case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> @@ -52,11 +53,14 @@ defmodule Opsm.Registries.Pypi do url = "https://pypi.org/search/?q=#{URI.encode(query)}" # For now, return a message - in production would scrape or use alternative - {:ok, [%{ - name: query, - description: "Search PyPI directly at #{url}", - note: "PyPI search API deprecated - use 'pip search' or visit pypi.org" - }]} + {:ok, + [ + %{ + name: query, + description: "Search PyPI directly at #{url}", + note: "PyPI search API deprecated - use 'pip search' or visit pypi.org" + } + ]} end @doc """ @@ -112,9 +116,10 @@ defmodule Opsm.Registries.Pypi do version = info["version"] # Prefer wheel, fall back to sdist - dist = Enum.find(urls, fn u -> u["packagetype"] == "bdist_wheel" end) || - Enum.find(urls, fn u -> u["packagetype"] == "sdist" end) || - List.first(urls) + dist = + Enum.find(urls, fn u -> u["packagetype"] == "bdist_wheel" end) || + Enum.find(urls, fn u -> u["packagetype"] == "sdist" end) || + List.first(urls) tarball = if dist, do: dist["url"], else: nil checksum = if dist, do: dist["digests"]["sha256"], else: nil @@ -147,10 +152,12 @@ defmodule Opsm.Registries.Pypi do end defp extract_repo_url(nil), do: nil + defp extract_repo_url(urls) when is_map(urls) do urls["Source"] || urls["Repository"] || urls["GitHub"] || - urls["source"] || urls["repository"] || urls["github"] + urls["source"] || urls["repository"] || urls["github"] end + defp extract_repo_url(_), do: nil defp extract_author(nil, nil), do: [] @@ -159,15 +166,18 @@ defmodule Opsm.Registries.Pypi do defp extract_author(name, email), do: ["#{name} <#{email}>"] defp parse_keywords(nil), do: [] + defp parse_keywords(keywords) when is_binary(keywords) do keywords |> String.split(~r/[,\s]+/) |> Enum.map(&String.trim/1) |> Enum.reject(&(&1 == "")) end + defp parse_keywords(keywords) when is_list(keywords), do: keywords defp parse_requires(nil), do: %{} + defp parse_requires(requires) when is_list(requires) do requires |> Enum.map(&parse_requirement/1) @@ -185,6 +195,7 @@ defmodule Opsm.Registries.Pypi do else {name, String.trim(version_spec)} end + _ -> nil end @@ -199,7 +210,8 @@ defmodule Opsm.Registries.Pypi do defp normalize_version(v) do v - |> String.replace(~r/[a-zA-Z].*$/, "") # Remove alpha/beta/rc suffixes + # Remove alpha/beta/rc suffixes + |> String.replace(~r/[a-zA-Z].*$/, "") |> String.split(".") |> Enum.take(3) |> Enum.map(fn part -> diff --git a/opsm_ex/lib/opsm/registries/quandledb.ex b/opsm_ex/lib/opsm/registries/quandledb.ex index 948945c0..f62ca842 100644 --- a/opsm_ex/lib/opsm/registries/quandledb.ex +++ b/opsm_ex/lib/opsm/registries/quandledb.ex @@ -29,7 +29,8 @@ defmodule Opsm.Registries.QuandleDB do name: "quandledb-core", url: "https://github.com/hyperpolymath/nextgen-databases", path: "quandledb", - description: "QuandleDB core — quandle algebra, semantic identity, self-distributive operations" + description: + "QuandleDB core — quandle algebra, semantic identity, self-distributive operations" }, %{ name: "quandledb-client", @@ -41,7 +42,8 @@ defmodule Opsm.Registries.QuandleDB do name: "quandledb-idris2", url: "https://github.com/hyperpolymath/nextgen-databases", path: "quandledb/connectors/idris2", - description: "Idris2 connector for QuandleDB — dependent-typed algebraic queries with proofs" + description: + "Idris2 connector for QuandleDB — dependent-typed algebraic queries with proofs" }, %{ name: "quandledb-rust", @@ -81,17 +83,21 @@ defmodule Opsm.Registries.QuandleDB do defp fetch_from_registry(name, version) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> target = if version == "latest", do: body["latest_version"], else: version {:ok, parse_registry_package(body, target)} - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end defp search_registry(query, opts) do limit = Keyword.get(opts, :limit, 20) url = "#{@base_url}/packages?q=#{URI.encode(query)}&limit=#{limit}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, &parse_search_result/1)} {:error, reason} -> {:error, reason} @@ -107,6 +113,7 @@ defmodule Opsm.Registries.QuandleDB do defp registry_versions(name) do url = "#{@base_url}/packages/#{URI.encode(name)}/versions" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, & &1["version"])} {:error, reason} -> {:error, reason} @@ -122,9 +129,12 @@ defmodule Opsm.Registries.QuandleDB do defp search_curated(query, _opts) do q = String.downcase(query) - results = Enum.filter(@known_packages, fn p -> - String.contains?(String.downcase("#{p.name} #{p.description}"), q) - end) + + results = + Enum.filter(@known_packages, fn p -> + String.contains?(String.downcase("#{p.name} #{p.description}"), q) + end) + {:ok, Enum.map(results, &curated_to_resolved(&1, "latest"))} end @@ -137,13 +147,18 @@ defmodule Opsm.Registries.QuandleDB do pkg_info.url |> String.replace("https://github.com/", "https://api.github.com/repos/") |> Kernel.<>("/tags") + case VerifiedHttp.get_json(tags_url, receive_timeout: 10_000) do {:ok, tags} when is_list(tags) -> versions = Enum.map(tags, & &1["name"]) |> Enum.reject(&is_nil/1) {:ok, if(versions == [], do: ["main"], else: versions)} - _ -> {:ok, ["main"]} + + _ -> + {:ok, ["main"]} end - :not_found -> {:error, :not_found} + + :not_found -> + {:error, :not_found} end end @@ -152,6 +167,7 @@ defmodule Opsm.Registries.QuandleDB do "https://github.com/hyperpolymath/#{name}", "https://github.com/hyperpolymath/quandledb-#{name}" ] + Enum.find_value(urls, {:error, :not_found}, fn url -> case fetch_git_manifest(%{name: name, url: url, description: nil}, version) do {:ok, pkg} -> {:ok, pkg} @@ -166,8 +182,11 @@ defmodule Opsm.Registries.QuandleDB do base = pkg_info.url try_url = fn manifest -> - url = if path, do: "#{base}/raw/#{branch}/#{path}/#{manifest}", - else: "#{base}/raw/#{branch}/#{manifest}" + url = + if path, + do: "#{base}/raw/#{branch}/#{path}/#{manifest}", + else: "#{base}/raw/#{branch}/#{manifest}" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: text}} -> {:ok, text} _ -> :not_found @@ -187,61 +206,97 @@ defmodule Opsm.Registries.QuandleDB do pkg_name = fields["name"] || pkg_info.name pkg = %ResolvedPackage{ - package: pkg_name, version: fields["version"] || version, forth: :quandledb, - registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + package: pkg_name, + version: fields["version"] || version, + forth: :quandledb, + registry_url: pkg_info.url, + tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", + checksum: nil, + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: pkg_name, version: fields["version"] || version, + name: pkg_name, + version: fields["version"] || version, description: fields["description"] || pkg_info[:description], license: fields["license"] || "MPL-2.0", homepage: fields["homepage"] || pkg_info.url, repository: fields["repository"] || pkg_info.url, authors: parse_toml_array(fields["authors"]) || default_authors(), keywords: parse_toml_array(fields["keywords"]) || default_keywords(), - dependencies: %{}, dev_dependencies: %{}, - source_forth: :quandledb, raw_manifest: fields + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :quandledb, + raw_manifest: fields }, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } + {:ok, pkg} end defp curated_to_resolved(pkg_info, version) do %ResolvedPackage{ - package: pkg_info.name, version: version, forth: :quandledb, - registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + package: pkg_info.name, + version: version, + forth: :quandledb, + registry_url: pkg_info.url, + tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", + checksum: nil, + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: pkg_info.name, version: version, description: pkg_info.description, - license: "MPL-2.0", homepage: pkg_info.url, repository: pkg_info.url, - authors: default_authors(), keywords: default_keywords(), - dependencies: %{}, dev_dependencies: %{}, - source_forth: :quandledb, raw_manifest: %{"registry" => "quandledb-curated"} + name: pkg_info.name, + version: version, + description: pkg_info.description, + license: "MPL-2.0", + homepage: pkg_info.url, + repository: pkg_info.url, + authors: default_authors(), + keywords: default_keywords(), + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :quandledb, + raw_manifest: %{"registry" => "quandledb-curated"} }, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } end defp parse_registry_package(data, version) do %ResolvedPackage{ - package: data["name"], version: version, forth: :quandledb, registry_url: @base_url, + package: data["name"], + version: version, + forth: :quandledb, + registry_url: @base_url, tarball_url: "#{@base_url}/packages/#{data["name"]}/#{version}/download", - checksum: data["checksum"], checksum_algo: :sha256, + checksum: data["checksum"], + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: data["name"], version: version, description: data["description"], - license: data["license"], homepage: data["homepage"], repository: data["repository"], - authors: data["authors"] || [], keywords: data["keywords"] || [], + name: data["name"], + version: version, + description: data["description"], + license: data["license"], + homepage: data["homepage"], + repository: data["repository"], + authors: data["authors"] || [], + keywords: data["keywords"] || [], dependencies: data["dependencies"] || %{}, dev_dependencies: data["dev_dependencies"] || %{}, - source_forth: :quandledb, raw_manifest: data + source_forth: :quandledb, + raw_manifest: data }, - attestations: data["attestations"] || [], resolved_deps: [] + attestations: data["attestations"] || [], + resolved_deps: [] } end defp parse_search_result(r) do - %{name: r["name"], version: r["latest_version"], - description: r["description"], downloads: r["downloads"] || 0} + %{ + name: r["name"], + version: r["latest_version"], + description: r["description"], + downloads: r["downloads"] || 0 + } end defp find_curated(name) do @@ -256,23 +311,34 @@ defmodule Opsm.Registries.QuandleDB do String.split(toml_text, "\n") |> Enum.reduce({false, %{}}, fn line, {inside, acc} -> t = String.trim(line) + cond do - t == "[#{section_name}]" -> {true, acc} - String.starts_with?(t, "[") -> {false, acc} + t == "[#{section_name}]" -> + {true, acc} + + String.starts_with?(t, "[") -> + {false, acc} + inside -> case String.split(t, " = ", parts: 2) do [k, v] -> {true, Map.put(acc, String.trim(k), String.trim(v, "\""))} _ -> {inside, acc} end - true -> {inside, acc} + + true -> + {inside, acc} end end) + fields end defp parse_toml_array(nil), do: nil + defp parse_toml_array(str) when is_binary(str) do - str |> String.trim("[") |> String.trim("]") + str + |> String.trim("[") + |> String.trim("]") |> String.split(",") |> Enum.map(&(&1 |> String.trim() |> String.trim("\""))) |> Enum.reject(&(&1 == "")) diff --git a/opsm_ex/lib/opsm/registries/raco.ex b/opsm_ex/lib/opsm/registries/raco.ex index 3ace13bc..4630b187 100644 --- a/opsm_ex/lib/opsm/registries/raco.ex +++ b/opsm_ex/lib/opsm/registries/raco.ex @@ -30,9 +30,14 @@ defmodule Opsm.Registries.Raco do ver = resolve_version(body, version) {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/raku.ex b/opsm_ex/lib/opsm/registries/raku.ex index 0e1c828b..088406b7 100644 --- a/opsm_ex/lib/opsm/registries/raku.ex +++ b/opsm_ex/lib/opsm/registries/raku.ex @@ -31,9 +31,14 @@ defmodule Opsm.Registries.Raku do ver = resolve_version(body, version) {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -89,9 +94,14 @@ defmodule Opsm.Registries.Raku do v -> {:ok, [v]} end - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/rattlescript.ex b/opsm_ex/lib/opsm/registries/rattlescript.ex index 74d3ec69..8e44890b 100644 --- a/opsm_ex/lib/opsm/registries/rattlescript.ex +++ b/opsm_ex/lib/opsm/registries/rattlescript.ex @@ -452,6 +452,7 @@ defmodule Opsm.Registries.RattleScript do end defp parse_toml_array(nil), do: nil + defp parse_toml_array(str) when is_binary(str) do str |> String.trim("[") diff --git a/opsm_ex/lib/opsm/registries/registry.ex b/opsm_ex/lib/opsm/registries/registry.ex index 075749e2..d3b256ef 100644 --- a/opsm_ex/lib/opsm/registries/registry.ex +++ b/opsm_ex/lib/opsm/registries/registry.ex @@ -17,39 +17,149 @@ defmodule Opsm.Registries.Registry do """ # Major ecosystems - alias Opsm.Registries.{Npm, Crates, Hex, Pypi, RubyGems, GoModules, PubDev, - Hackage, NuGet, Maven} + alias Opsm.Registries.{ + Npm, + Crates, + Hex, + Pypi, + RubyGems, + GoModules, + PubDev, + Hackage, + NuGet, + Maven + } # Extended language ecosystems - alias Opsm.Registries.{Packagist, Cpan, Cran, Conda, CocoaPods, Opam, Clojars, - LuaRocks, Terraform, Jsr, Conan, SwiftPM, Elm, Vcpkg, JuliaGeneral, - Dub, Shard, Raku, Raco, Chicken, Alire, Stackage, Pear, Pecl, - SbtPlugins, GradlePlugins, DenoX, CargoBinstall, Bower} + alias Opsm.Registries.{ + Packagist, + Cpan, + Cran, + Conda, + CocoaPods, + Opam, + Clojars, + LuaRocks, + Terraform, + Jsr, + Conan, + SwiftPM, + Elm, + Vcpkg, + JuliaGeneral, + Dub, + Shard, + Raku, + Raco, + Chicken, + Alire, + Stackage, + Pear, + Pecl, + SbtPlugins, + GradlePlugins, + DenoX, + CargoBinstall, + Bower + } # System package managers - alias Opsm.Registries.{Homebrew, HomebrewCask, Nix, NixFlakes, NixDarwin, - Apt, Rpm, Alpine, Flatpak, Snap, Guix, Macports, Portage, Xbps, - Zypper, Aur, Pacstall, Solus, Spack, Pkgsrc, Freebsd, FpmRegistry} + alias Opsm.Registries.{ + Homebrew, + HomebrewCask, + Nix, + NixFlakes, + NixDarwin, + Apt, + Rpm, + Alpine, + Flatpak, + Snap, + Guix, + Macports, + Portage, + Xbps, + Zypper, + Aur, + Pacstall, + Solus, + Spack, + Pkgsrc, + Freebsd, + FpmRegistry + } # Container/cloud/infra registries - alias Opsm.Registries.{DockerHub, Helm, Buildpacks, K8sOperators, Pulumi, - TektonHub, AnsibleGalaxy, ChefSupermarket, PuppetForge, ScoopApi, WingetApi} + alias Opsm.Registries.{ + DockerHub, + Helm, + Buildpacks, + K8sOperators, + Pulumi, + TektonHub, + AnsibleGalaxy, + ChefSupermarket, + PuppetForge, + ScoopApi, + WingetApi + } # IDE/editor/plugin registries - alias Opsm.Registries.{VscodeMarketplace, Jetbrains, Sublime, VimPlugins, - EclipseMarketplace, Melpa, Elpa, Grafana, OpenUpm, Godot} + alias Opsm.Registries.{ + VscodeMarketplace, + Jetbrains, + Sublime, + VimPlugins, + EclipseMarketplace, + Melpa, + Elpa, + Grafana, + OpenUpm, + Godot + } # CDN/asset/meta registries - alias Opsm.Registries.{JsDelivr, Cdnjs, WebJars, GithubPackages, GitlabPackages, - WordPress, WordPressThemes, Wapm, Bioconductor, Astrolabe, Vpm} + alias Opsm.Registries.{ + JsDelivr, + Cdnjs, + WebJars, + GithubPackages, + GitlabPackages, + WordPress, + WordPressThemes, + Wapm, + Bioconductor, + Astrolabe, + Vpm + } # Niche/custom ecosystems - alias Opsm.Registries.{Nimble, Idris2, Git, Agentic, Oblibeny, MyLang, - JuliaTheViper, ErrorLang, Eclexia, AffineScript, RattleScript} + alias Opsm.Registries.{ + Nimble, + Idris2, + Git, + Agentic, + Oblibeny, + MyLang, + JuliaTheViper, + ErrorLang, + Eclexia, + AffineScript, + RattleScript + } # Hyperpolymath Forge Registry + new nextgen language/database adapters - alias Opsm.Registries.{HyperpPolymathForge, Betlang, Ephapax, Phronesis, - Tangle, Wokelang, Lithoglyph, Nqc, QuandleDB} + alias Opsm.Registries.{ + HyperpPolymathForge, + Betlang, + Ephapax, + Phronesis, + Tangle, + Wokelang, + Lithoglyph, + Nqc, + QuandleDB + } alias Opsm.Cache @@ -339,33 +449,122 @@ defmodule Opsm.Registries.Registry do # All primary forth names (for search_all / exists_all? defaults) @all_primary_forths [ # Major - :npm, :cargo, :hex, :pypi, :gem, :go, :pub, :hackage, :nuget, :maven, + :npm, + :cargo, + :hex, + :pypi, + :gem, + :go, + :pub, + :hackage, + :nuget, + :maven, # Extended - :packagist, :cpan, :cran, :conda, :cocoapods, :opam, :clojars, - :luarocks, :terraform, :jsr, :conan, :swift, :elm, :vcpkg, :julia, - :dub, :shard, :raku, :raco, :chicken, :alire, :stackage, :pear, :pecl, - :sbt_plugins, :gradle_plugins, :deno_x, :cargo_binstall, :bower, + :packagist, + :cpan, + :cran, + :conda, + :cocoapods, + :opam, + :clojars, + :luarocks, + :terraform, + :jsr, + :conan, + :swift, + :elm, + :vcpkg, + :julia, + :dub, + :shard, + :raku, + :raco, + :chicken, + :alire, + :stackage, + :pear, + :pecl, + :sbt_plugins, + :gradle_plugins, + :deno_x, + :cargo_binstall, + :bower, # System - :homebrew, :homebrew_cask, :nix, :nix_flakes, :nix_darwin, - :apt, :rpm, :alpine, :flatpak, :snap, :guix, :macports, :portage, - :xbps, :zypper, :aur, :pacstall, :solus, :spack, :pkgsrc, :freebsd, :fpm, + :homebrew, + :homebrew_cask, + :nix, + :nix_flakes, + :nix_darwin, + :apt, + :rpm, + :alpine, + :flatpak, + :snap, + :guix, + :macports, + :portage, + :xbps, + :zypper, + :aur, + :pacstall, + :solus, + :spack, + :pkgsrc, + :freebsd, + :fpm, # Container/cloud - :docker, :helm, :buildpacks, :k8s_operators, :pulumi, :tekton, - :ansible, :chef, :puppet, :scoop, :winget, + :docker, + :helm, + :buildpacks, + :k8s_operators, + :pulumi, + :tekton, + :ansible, + :chef, + :puppet, + :scoop, + :winget, # IDE/editor - :vscode, :jetbrains, :sublime, :vim, :eclipse, :melpa, :elpa, - :grafana, :openupm, :godot, + :vscode, + :jetbrains, + :sublime, + :vim, + :eclipse, + :melpa, + :elpa, + :grafana, + :openupm, + :godot, # CDN/meta - :jsdelivr, :cdnjs, :webjars, :github_packages, :gitlab_packages, - :wordpress, :wordpress_themes, :wapm, :bioconductor, :astrolabe, :vpm, + :jsdelivr, + :cdnjs, + :webjars, + :github_packages, + :gitlab_packages, + :wordpress, + :wordpress_themes, + :wapm, + :bioconductor, + :astrolabe, + :vpm, # Niche / custom ecosystems - :nimble, :idris2, :eclexia, :affinescript, :rattlescript, + :nimble, + :idris2, + :eclexia, + :affinescript, + :rattlescript, # Hyperpolymath Forge Registry (primary for all HP packages) :hf, # nextgen-languages - :betlang, :ephapax, :phronesis, :tangle, :wokelang, + :betlang, + :ephapax, + :phronesis, + :tangle, + :wokelang, # nextgen-databases - :lithoglyph, :quandledb, :nqc + :lithoglyph, + :quandledb, + :nqc ] @doc """ @@ -432,18 +631,19 @@ defmodule Opsm.Registries.Registry do forths = Keyword.get(opts, :forths, @all_primary_forths) timeout = Keyword.get(opts, :timeout, 15_000) - tasks = Enum.map(forths, fn forth -> - Task.async(fn -> - try do - result = search(forth, query, opts) - {forth, result} - rescue - e -> {forth, {:error, Exception.message(e)}} - catch - kind, reason -> {forth, {:error, "#{kind}: #{inspect(reason)}"}} - end + tasks = + Enum.map(forths, fn forth -> + Task.async(fn -> + try do + result = search(forth, query, opts) + {forth, result} + rescue + e -> {forth, {:error, Exception.message(e)}} + catch + kind, reason -> {forth, {:error, "#{kind}: #{inspect(reason)}"}} + end + end) end) - end) results = safe_await_many(tasks, timeout) @@ -466,11 +666,12 @@ defmodule Opsm.Registries.Registry do forths = Keyword.get(opts, :forths, @all_primary_forths) timeout = Keyword.get(opts, :timeout, 10_000) - tasks = Enum.map(forths, fn forth -> - Task.async(fn -> - {forth, exists?(forth, package)} + tasks = + Enum.map(forths, fn forth -> + Task.async(fn -> + {forth, exists?(forth, package)} + end) end) - end) results = safe_await_many(tasks, timeout) @@ -490,18 +691,19 @@ defmodule Opsm.Registries.Registry do forths = Keyword.get(opts, :forths, @all_primary_forths) timeout = Keyword.get(opts, :timeout, 15_000) - tasks = Enum.map(forths, fn forth -> - Task.async(fn -> - try do - result = fetch(forth, package, version) - {forth, result} - rescue - e -> {forth, {:error, Exception.message(e)}} - catch - kind, reason -> {forth, {:error, "#{kind}: #{inspect(reason)}"}} - end + tasks = + Enum.map(forths, fn forth -> + Task.async(fn -> + try do + result = fetch(forth, package, version) + {forth, result} + rescue + e -> {forth, {:error, Exception.message(e)}} + catch + kind, reason -> {forth, {:error, "#{kind}: #{inspect(reason)}"}} + end + end) end) - end) results = safe_await_many(tasks, timeout) @@ -517,6 +719,7 @@ defmodule Opsm.Registries.Registry do Get the registry module for a forth. """ def get_module(forth) when is_atom(forth), do: Map.get(@registry_modules, forth) + def get_module(forth) when is_binary(forth) do case safe_to_atom(forth) do nil -> nil diff --git a/opsm_ex/lib/opsm/registries/rpm.ex b/opsm_ex/lib/opsm/registries/rpm.ex index 361b9012..5b560e39 100644 --- a/opsm_ex/lib/opsm/registries/rpm.ex +++ b/opsm_ex/lib/opsm/registries/rpm.ex @@ -18,25 +18,34 @@ defmodule Opsm.Registries.Rpm do """ def fetch_package(name, version \\ "latest") do url = "#{@mdapi}/rawhide/pkg/#{name}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: "#{body["version"]}-#{body["release"]}", - else: version + ver = + if version == "latest", + do: "#{body["version"]}-#{body["release"]}", + else: version + {:ok, parse_rpm_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp parse_rpm_package(name, body, version) do - requires = (body["requires"] || []) - |> Enum.filter(&is_map/1) - |> Enum.map(fn r -> {r["name"] || "", "*"} end) - |> Enum.reject(fn {n, _} -> n == "" end) - |> Map.new() + requires = + (body["requires"] || []) + |> Enum.filter(&is_map/1) + |> Enum.map(fn r -> {r["name"] || "", "*"} end) + |> Enum.reject(fn {n, _} -> n == "" end) + |> Map.new() manifest = %ManifestFormat{ name: name, @@ -55,7 +64,7 @@ defmodule Opsm.Registries.Rpm do manifest: manifest, tarball_url: nil, checksum: nil, - attestations: [], + attestations: [] } end @@ -64,17 +73,21 @@ defmodule Opsm.Registries.Rpm do """ def get_versions(name) do url = "#{@bodhi_api}/updates/?packages=#{name}&rows_per_page=20" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - versions = (body["updates"] || []) - |> Enum.flat_map(fn u -> - (u["builds"] || []) - |> Enum.map(fn b -> b["nvr"] end) - end) - |> Enum.reject(&is_nil/1) + versions = + (body["updates"] || []) + |> Enum.flat_map(fn u -> + (u["builds"] || []) + |> Enum.map(fn b -> b["nvr"] end) + end) + |> Enum.reject(&is_nil/1) + {:ok, versions} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -83,19 +96,23 @@ defmodule Opsm.Registries.Rpm do """ def search(query, _opts \\ []) do url = "#{@mdapi}/rawhide/srcpkg/#{URI.encode(query)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{name: pkg["name"], version: pkg["version"], description: pkg["summary"]} - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{name: pkg["name"], version: pkg["version"], description: pkg["summary"]} + end) + {:ok, results} {:ok, body} when is_map(body) -> {:ok, [%{name: body["name"], version: body["version"], description: body["summary"]}]} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/rubygems.ex b/opsm_ex/lib/opsm/registries/rubygems.ex index 34c33183..3cf2de20 100644 --- a/opsm_ex/lib/opsm/registries/rubygems.ex +++ b/opsm_ex/lib/opsm/registries/rubygems.ex @@ -20,11 +20,12 @@ defmodule Opsm.Registries.RubyGems do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - target_version = if version == "latest" do - body["version"] - else - version - end + target_version = + if version == "latest" do + body["version"] + else + version + end deps = fetch_release_deps(name, target_version) {:ok, parse_gem(body, target_version, deps)} @@ -55,11 +56,13 @@ defmodule Opsm.Registries.RubyGems do {:ok, %{"dependencies" => deps}} when is_map(deps) -> runtime = deps["runtime"] || [] + runtime |> Enum.map(fn d -> {d["name"], d["requirements"] || ">= 0"} end) |> Map.new() - _ -> %{} + _ -> + %{} end end @@ -72,9 +75,11 @@ defmodule Opsm.Registries.RubyGems do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body + results = + body |> Enum.take(limit) |> Enum.map(&parse_search_result/1) + {:ok, results} {:ok, _} -> @@ -108,9 +113,11 @@ defmodule Opsm.Registries.RubyGems do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - versions = body + versions = + body |> Enum.map(& &1["number"]) |> Enum.reject(&is_nil/1) + {:ok, versions} {:error, :not_found} -> @@ -174,8 +181,10 @@ defmodule Opsm.Registries.RubyGems do end defp parse_authors(nil), do: [] + defp parse_authors(authors) when is_binary(authors) do String.split(authors, ",") |> Enum.map(&String.trim/1) end + defp parse_authors(authors) when is_list(authors), do: authors end diff --git a/opsm_ex/lib/opsm/registries/sbt_plugins.ex b/opsm_ex/lib/opsm/registries/sbt_plugins.ex index 2d674774..2a148039 100644 --- a/opsm_ex/lib/opsm/registries/sbt_plugins.ex +++ b/opsm_ex/lib/opsm/registries/sbt_plugins.ex @@ -25,11 +25,12 @@ defmodule Opsm.Registries.SbtPlugins do {group_id, artifact_id} = parse_coordinate(name) group_path = String.replace(group_id, ".", "/") - target_version = if version == "latest" do - fetch_latest_version(group_id, artifact_id) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(group_id, artifact_id) + else + version + end case target_version do nil -> @@ -59,7 +60,8 @@ defmodule Opsm.Registries.SbtPlugins do end defp fetch_latest_version(group_id, artifact_id) do - url = "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&rows=1&wt=json" + url = + "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&rows=1&wt=json" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"response" => %{"docs" => [%{"latestVersion" => ver} | _]}}} -> @@ -74,7 +76,8 @@ defmodule Opsm.Registries.SbtPlugins do end defp fetch_from_search(group_id, artifact_id, version) do - url = "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}+AND+v:#{URI.encode(version)}&rows=1&wt=json" + url = + "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}+AND+v:#{URI.encode(version)}&rows=1&wt=json" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"response" => %{"docs" => [doc | _]}}} -> @@ -128,7 +131,8 @@ defmodule Opsm.Registries.SbtPlugins do def exists?(name) do {group_id, artifact_id} = parse_coordinate(name) - url = "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&rows=1&wt=json" + url = + "#{@search_url}?q=g:#{URI.encode(group_id)}+AND+a:#{URI.encode(artifact_id)}&rows=1&wt=json" case VerifiedHttp.get_json(url, receive_timeout: 5_000) do {:ok, %{"response" => %{"numFound" => n}}} when n > 0 -> true @@ -191,7 +195,8 @@ defmodule Opsm.Registries.SbtPlugins do version: version, forth: :sbt_plugins, registry_url: "https://search.maven.org/artifact/#{group_id}/#{artifact_id}/#{version}/jar", - tarball_url: "#{@maven_url}/#{group_path}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar", + tarball_url: + "#{@maven_url}/#{group_path}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar", checksum: nil, checksum_algo: nil, manifest: %ManifestFormat{ @@ -222,7 +227,8 @@ defmodule Opsm.Registries.SbtPlugins do version: version, forth: :sbt_plugins, registry_url: "https://search.maven.org/artifact/#{group_id}/#{artifact_id}/#{version}/jar", - tarball_url: "#{@maven_url}/#{group_path}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar", + tarball_url: + "#{@maven_url}/#{group_path}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar", checksum: nil, checksum_algo: nil, manifest: %ManifestFormat{ @@ -253,6 +259,7 @@ defmodule Opsm.Registries.SbtPlugins do defp extract_scm(_), do: nil defp extract_developers(nil), do: [] + defp extract_developers(devs) when is_list(devs) do Enum.map(devs, fn %{"name" => name} -> name @@ -261,14 +268,17 @@ defmodule Opsm.Registries.SbtPlugins do end) |> Enum.reject(&is_nil/1) end + defp extract_developers(_), do: [] defp extract_deps(nil), do: %{} + defp extract_deps(deps) when is_list(deps) do Enum.reduce(deps, %{}, fn dep, acc -> key = "#{dep["groupId"]}:#{dep["artifactId"]}" Map.put(acc, key, dep["version"] || "latest") end) end + defp extract_deps(_), do: %{} end diff --git a/opsm_ex/lib/opsm/registries/scoop_api.ex b/opsm_ex/lib/opsm/registries/scoop_api.ex index 74ae8cb7..f04e713b 100644 --- a/opsm_ex/lib/opsm/registries/scoop_api.ex +++ b/opsm_ex/lib/opsm/registries/scoop_api.ex @@ -16,8 +16,10 @@ defmodule Opsm.Registries.ScoopApi do @extras_raw "https://raw.githubusercontent.com/ScoopInstaller/Extras/master/bucket" @scoop_search_api "https://scoopsearch.github.io/api/search" - @headers [{"accept", "application/vnd.github.v3+json"}, - {"user-agent", "opsm/0.1.0 (https://github.com/hyperpolymath/opsm)"}] + @headers [ + {"accept", "application/vnd.github.v3+json"}, + {"user-agent", "opsm/0.1.0 (https://github.com/hyperpolymath/opsm)"} + ] @doc """ Fetch package manifest from Scoop buckets. @@ -25,13 +27,17 @@ defmodule Opsm.Registries.ScoopApi do """ def fetch_package(name, version \\ "latest") do case fetch_from_bucket(name, @main_raw, :main) do - {:ok, _} = result -> maybe_pin_version(result, version) + {:ok, _} = result -> + maybe_pin_version(result, version) + {:error, :not_found} -> case fetch_from_bucket(name, @extras_raw, :extras) do {:ok, _} = result -> maybe_pin_version(result, version) {:error, _} = err -> err end - {:error, _} = err -> err + + {:error, _} = err -> + err end end @@ -42,13 +48,19 @@ defmodule Opsm.Registries.ScoopApi do {:ok, body} -> {:ok, parse_scoop_manifest(name, body, bucket)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp maybe_pin_version({:ok, pkg}, "latest"), do: {:ok, pkg} + defp maybe_pin_version({:ok, pkg}, version) do updated_manifest = %{pkg.manifest | version: version} {:ok, %{pkg | version: version, manifest: updated_manifest}} @@ -63,27 +75,31 @@ defmodule Opsm.Registries.ScoopApi do hash = arch_64["hash"] || body["hash"] # Normalize URL (can be string or list) - download_url = case url do - u when is_binary(u) -> u - [u | _] when is_binary(u) -> u - _ -> nil - end - - download_hash = case hash do - h when is_binary(h) -> h - [h | _] when is_binary(h) -> h - _ -> nil - end - - deps = (body["depends"] || []) - |> List.wrap() - |> Enum.map(fn dep -> {dep, "*"} end) - |> Map.new() - - bucket_name = case bucket do - :main -> "ScoopInstaller/Main" - :extras -> "ScoopInstaller/Extras" - end + download_url = + case url do + u when is_binary(u) -> u + [u | _] when is_binary(u) -> u + _ -> nil + end + + download_hash = + case hash do + h when is_binary(h) -> h + [h | _] when is_binary(h) -> h + _ -> nil + end + + deps = + (body["depends"] || []) + |> List.wrap() + |> Enum.map(fn dep -> {dep, "*"} end) + |> Map.new() + + bucket_name = + case bucket do + :main -> "ScoopInstaller/Main" + :extras -> "ScoopInstaller/Extras" + end manifest = %ManifestFormat{ name: name, @@ -130,26 +146,32 @@ defmodule Opsm.Registries.ScoopApi do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> hits = body["hits"] || body["results"] || [] - results = hits - |> Enum.take(20) - |> Enum.map(fn hit -> - %{ - name: hit["name"] || hit["_source"]["name"], - version: hit["version"] || hit["_source"]["version"], - description: hit["description"] || hit["_source"]["description"] - } - end) + + results = + hits + |> Enum.take(20) + |> Enum.map(fn hit -> + %{ + name: hit["name"] || hit["_source"]["name"], + version: hit["version"] || hit["_source"]["version"], + description: hit["description"] || hit["_source"]["description"] + } + end) + {:ok, results} {:ok, items} when is_list(items) -> - results = items - |> Enum.take(20) - |> Enum.map(fn item -> - %{name: item["name"], version: item["version"], description: item["description"]} - end) + results = + items + |> Enum.take(20) + |> Enum.map(fn item -> + %{name: item["name"], version: item["version"], description: item["description"]} + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/shard.ex b/opsm_ex/lib/opsm/registries/shard.ex index f1d8d83b..1ea88fba 100644 --- a/opsm_ex/lib/opsm/registries/shard.ex +++ b/opsm_ex/lib/opsm/registries/shard.ex @@ -32,9 +32,14 @@ defmodule Opsm.Registries.Shard do ver = resolve_version(body, version) {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -105,9 +110,14 @@ defmodule Opsm.Registries.Shard do {:ok, %{"versions" => vers}} when is_list(vers) -> {:ok, Enum.map(vers, fn v -> v["version"] || v["number"] end)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/snap.ex b/opsm_ex/lib/opsm/registries/snap.ex index 6f0d2a68..2b190cda 100644 --- a/opsm_ex/lib/opsm/registries/snap.ex +++ b/opsm_ex/lib/opsm/registries/snap.ex @@ -17,6 +17,7 @@ defmodule Opsm.Registries.Snap do """ def fetch_package(name, version \\ "latest") do url = "#{@api_url}/#{name}" + headers = [ {"Snap-Device-Series", "16"}, {"Snap-Device-Architecture", "amd64"} @@ -25,21 +26,29 @@ defmodule Opsm.Registries.Snap do case VerifiedHttp.get_json(url, headers: headers, receive_timeout: 10_000) do {:ok, body} -> channel_map = body["channel-map"] || [] - stable = Enum.find(channel_map, fn c -> - get_in(c, ["channel", "name"]) == "stable" - end) - ver = if version == "latest" do - if stable, do: stable["version"], else: "0.0.0" - else - version - end + stable = + Enum.find(channel_map, fn c -> + get_in(c, ["channel", "name"]) == "stable" + end) + + ver = + if version == "latest" do + if stable, do: stable["version"], else: "0.0.0" + else + version + end {:ok, parse_snap(name, body, ver, stable)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -56,13 +65,15 @@ defmodule Opsm.Registries.Snap do dependencies: %{} } - download_url = if stable_channel do - get_in(stable_channel, ["download", "url"]) - end + download_url = + if stable_channel do + get_in(stable_channel, ["download", "url"]) + end - download_sha = if stable_channel do - get_in(stable_channel, ["download", "sha3-384"]) - end + download_sha = + if stable_channel do + get_in(stable_channel, ["download", "sha3-384"]) + end %ResolvedPackage{ package: name, @@ -72,7 +83,7 @@ defmodule Opsm.Registries.Snap do tarball_url: download_url, checksum: download_sha, checksum_algo: if(download_sha, do: :"sha3-384"), - attestations: [], + attestations: [] } end @@ -85,13 +96,16 @@ defmodule Opsm.Registries.Snap do case VerifiedHttp.get_json(url, headers: headers, receive_timeout: 10_000) do {:ok, body} -> - versions = (body["channel-map"] || []) - |> Enum.map(fn c -> c["version"] end) - |> Enum.reject(&is_nil/1) - |> Enum.uniq() + versions = + (body["channel-map"] || []) + |> Enum.map(fn c -> c["version"] end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() + {:ok, versions} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -99,20 +113,25 @@ defmodule Opsm.Registries.Snap do Search for snaps in the Snap Store. """ def search(query, _opts \\ []) do - url = "https://api.snapcraft.io/v2/snaps/find?q=#{URI.encode(query)}&fields=title,summary,version" + url = + "https://api.snapcraft.io/v2/snaps/find?q=#{URI.encode(query)}&fields=title,summary,version" + headers = [{"Snap-Device-Series", "16"}] case VerifiedHttp.get_json(url, headers: headers, receive_timeout: 10_000) do {:ok, body} -> - results = (body["results"] || []) - |> Enum.take(20) - |> Enum.map(fn s -> - snap = s["snap"] || %{} - %{name: s["name"], version: s["version"], description: snap["summary"]} - end) + results = + (body["results"] || []) + |> Enum.take(20) + |> Enum.map(fn s -> + snap = s["snap"] || %{} + %{name: s["name"], version: s["version"], description: snap["summary"]} + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/solus.ex b/opsm_ex/lib/opsm/registries/solus.ex index 4a1887d1..0542edf8 100644 --- a/opsm_ex/lib/opsm/registries/solus.ex +++ b/opsm_ex/lib/opsm/registries/solus.ex @@ -25,22 +25,29 @@ defmodule Opsm.Registries.Solus do {:ok, body} -> pkg = body["package"] || body - ver = if version == "latest" do - pkg["version"] || get_latest_version(pkg) || "0.0.0" - else - version - end + ver = + if version == "latest" do + pkg["version"] || get_latest_version(pkg) || "0.0.0" + else + version + end {:ok, parse_solus_package(name, pkg, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp get_latest_version(pkg) do history = pkg["history"] || pkg["releases"] || [] + case history do [latest | _] -> latest["version"] _ -> nil @@ -49,8 +56,9 @@ defmodule Opsm.Registries.Solus do defp parse_solus_package(name, pkg, version) do # eopkg packages declare runtime and build dependencies - runtime_deps = (pkg["rundeps"] || pkg["runtimeDependencies"] || []) - |> normalize_deps() + runtime_deps = + (pkg["rundeps"] || pkg["runtimeDependencies"] || []) + |> normalize_deps() source = pkg["source"] || %{} @@ -72,9 +80,10 @@ defmodule Opsm.Registries.Solus do release = (pkg["history"] || pkg["releases"] || []) |> List.first() || %{} release_num = release["release"] || pkg["release"] - download_url = if release_num do - "#{@repo_url}/shannon/#{name}-#{version}-#{release_num}-1-x86_64.eopkg" - end + download_url = + if release_num do + "#{@repo_url}/shannon/#{name}-#{version}-#{release_num}-1-x86_64.eopkg" + end %ResolvedPackage{ package: name, @@ -107,6 +116,7 @@ defmodule Opsm.Registries.Solus do defp extract_packager(pkg) do packager = pkg["packager"] || pkg["maintainer"] + case packager do p when is_binary(p) -> [p] %{"name" => name} -> [name] @@ -123,30 +133,36 @@ defmodule Opsm.Registries.Solus do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> packages = body["packages"] || body["results"] || [] - results = packages - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: pkg["version"], - description: pkg["description"] || pkg["summary"] - } - end) + + results = + packages + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: pkg["version"], + description: pkg["description"] || pkg["summary"] + } + end) + {:ok, results} {:ok, items} when is_list(items) -> - results = items - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: pkg["version"], - description: pkg["description"] || pkg["summary"] - } - end) + results = + items + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: pkg["version"], + description: pkg["description"] || pkg["summary"] + } + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end @@ -170,22 +186,26 @@ defmodule Opsm.Registries.Solus do {:ok, body} -> pkg = body["package"] || body history = pkg["history"] || pkg["releases"] || [] - vers = history - |> Enum.map(fn r -> r["version"] end) - |> Enum.reject(&is_nil/1) - |> Enum.uniq() + + vers = + history + |> Enum.map(fn r -> r["version"] end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() # Fall back to single version if no history - vers = if vers == [] do - v = pkg["version"] - if v, do: [v], else: [] - else - vers - end + vers = + if vers == [] do + v = pkg["version"] + if v, do: [v], else: [] + else + vers + end {:ok, vers} - {:error, _} = err -> err + {:error, _} = err -> + err end end end diff --git a/opsm_ex/lib/opsm/registries/spack.ex b/opsm_ex/lib/opsm/registries/spack.ex index 3cfaa945..95367913 100644 --- a/opsm_ex/lib/opsm/registries/spack.ex +++ b/opsm_ex/lib/opsm/registries/spack.ex @@ -61,38 +61,42 @@ defmodule Opsm.Registries.Spack do {:ok, packages} when is_list(packages) -> query_lower = String.downcase(query) - results = packages - |> Enum.filter(fn pkg -> - name = String.downcase(pkg["name"] || "") - desc = String.downcase(pkg["description"] || "") - String.contains?(name, query_lower) or String.contains?(desc, query_lower) - end) - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: List.first(pkg["versions"] || []), - description: pkg["description"] - } - end) + results = + packages + |> Enum.filter(fn pkg -> + name = String.downcase(pkg["name"] || "") + desc = String.downcase(pkg["description"] || "") + String.contains?(name, query_lower) or String.contains?(desc, query_lower) + end) + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: List.first(pkg["versions"] || []), + description: pkg["description"] + } + end) + {:ok, results} {:ok, %{"packages" => packages}} when is_list(packages) -> query_lower = String.downcase(query) - results = packages - |> Enum.filter(fn pkg -> - name = String.downcase(pkg["name"] || "") - String.contains?(name, query_lower) - end) - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: nil, - description: pkg["description"] - } - end) + results = + packages + |> Enum.filter(fn pkg -> + name = String.downcase(pkg["name"] || "") + String.contains?(name, query_lower) + end) + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: nil, + description: pkg["description"] + } + end) + {:ok, results} {:ok, _} -> diff --git a/opsm_ex/lib/opsm/registries/stackage.ex b/opsm_ex/lib/opsm/registries/stackage.ex index 530653cc..4111dc89 100644 --- a/opsm_ex/lib/opsm/registries/stackage.ex +++ b/opsm_ex/lib/opsm/registries/stackage.ex @@ -34,9 +34,14 @@ defmodule Opsm.Registries.Stackage do ver = resolve_version(body, name, version) {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -91,9 +96,14 @@ defmodule Opsm.Registries.Stackage do vers = extract_versions(body, name) {:ok, vers} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/sublime.ex b/opsm_ex/lib/opsm/registries/sublime.ex index 186355d0..3aa5b36c 100644 --- a/opsm_ex/lib/opsm/registries/sublime.ex +++ b/opsm_ex/lib/opsm/registries/sublime.ex @@ -20,47 +20,59 @@ defmodule Opsm.Registries.Sublime do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest" do - extract_latest_version(body) - else - version - end + ver = + if version == "latest" do + extract_latest_version(body) + else + version + end + {:ok, parse_sublime_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp extract_latest_version(body) do releases = body["releases"] || body["versions"] || [] + case releases do [latest | _] -> latest["version"] || latest["date"] || "0.0.0" + _ -> body["version"] || "0.0.0" end end defp parse_sublime_package(name, body, version) do - deps = (body["dependencies"] || []) - |> Enum.map(fn - d when is_binary(d) -> {d, "*"} - d when is_map(d) -> {d["name"] || "", "*"} - _ -> nil - end) - |> Enum.reject(&is_nil/1) - |> Enum.reject(fn {n, _} -> n == "" end) - |> Map.new() + deps = + (body["dependencies"] || []) + |> Enum.map(fn + d when is_binary(d) -> {d, "*"} + d when is_map(d) -> {d["name"] || "", "*"} + _ -> nil + end) + |> Enum.reject(&is_nil/1) + |> Enum.reject(fn {n, _} -> n == "" end) + |> Map.new() labels = (body["labels"] || []) |> Enum.join(", ") description = body["description"] || "" - full_description = if labels != "" do - "#{description} [#{labels}]" - else - description - end + + full_description = + if labels != "" do + "#{description} [#{labels}]" + else + description + end homepage = body["homepage"] || "#{@api_url}/packages/#{URI.encode(name)}" repository = extract_repository(body) @@ -84,7 +96,7 @@ defmodule Opsm.Registries.Sublime do manifest: manifest, tarball_url: tarball_url, checksum: nil, - attestations: [], + attestations: [] } end @@ -98,15 +110,19 @@ defmodule Opsm.Registries.Sublime do defp build_download_url(body, repository) do releases = body["releases"] || body["versions"] || [] + case releases do [latest | _] -> latest["url"] || latest["download_url"] + _ -> # Attempt to build a GitHub archive URL from the repository case repository do "https://github.com/" <> _ = repo -> "#{repo}/archive/refs/heads/master.zip" - _ -> nil + + _ -> + nil end end end @@ -120,10 +136,12 @@ defmodule Opsm.Registries.Sublime do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> releases = body["releases"] || body["versions"] || [] - versions_list = releases - |> Enum.map(fn r -> r["version"] || r["date"] end) - |> Enum.reject(&is_nil/1) - |> Enum.uniq() + + versions_list = + releases + |> Enum.map(fn r -> r["version"] || r["date"] end) + |> Enum.reject(&is_nil/1) + |> Enum.uniq() if versions_list == [] do case fetch_package(name) do @@ -134,7 +152,8 @@ defmodule Opsm.Registries.Sublime do {:ok, versions_list} end - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -146,31 +165,38 @@ defmodule Opsm.Registries.Sublime do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: extract_latest_version(pkg), - description: pkg["description"] - } - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: extract_latest_version(pkg), + description: pkg["description"] + } + end) + {:ok, results} {:ok, %{"packages" => packages}} when is_list(packages) -> - results = packages - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["name"], - version: nil, - description: pkg["description"] - } - end) + results = + packages + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["name"], + version: nil, + description: pkg["description"] + } + end) + {:ok, results} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/swift_pm.ex b/opsm_ex/lib/opsm/registries/swift_pm.ex index ebf45e20..0060c4f9 100644 --- a/opsm_ex/lib/opsm/registries/swift_pm.ex +++ b/opsm_ex/lib/opsm/registries/swift_pm.ex @@ -20,42 +20,49 @@ defmodule Opsm.Registries.SwiftPM do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - target_version = if version == "latest" do - get_in(body, ["releases", "stable", "reference", "version"]) || - get_in(body, ["releases", "latest", "reference", "version"]) - else - version - end - - {:ok, %ResolvedPackage{ - package: name, - version: target_version || version, - forth: :swift, - registry_url: "#{@web_url}/#{owner}/#{repo}", - tarball_url: body["url"], - checksum: nil, - checksum_algo: nil, - manifest: %ManifestFormat{ - name: repo, - version: target_version || version, - description: body["summary"], - license: nil, - homepage: "#{@web_url}/#{owner}/#{repo}", - repository: body["url"], - authors: [owner], - keywords: body["keywords"] || [], - dependencies: %{}, - dev_dependencies: %{}, - source_forth: :swift, - raw_manifest: body - }, - attestations: [], - resolved_deps: [] - }} - - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + target_version = + if version == "latest" do + get_in(body, ["releases", "stable", "reference", "version"]) || + get_in(body, ["releases", "latest", "reference", "version"]) + else + version + end + + {:ok, + %ResolvedPackage{ + package: name, + version: target_version || version, + forth: :swift, + registry_url: "#{@web_url}/#{owner}/#{repo}", + tarball_url: body["url"], + checksum: nil, + checksum_algo: nil, + manifest: %ManifestFormat{ + name: repo, + version: target_version || version, + description: body["summary"], + license: nil, + homepage: "#{@web_url}/#{owner}/#{repo}", + repository: body["url"], + authors: [owner], + keywords: body["keywords"] || [], + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :swift, + raw_manifest: body + }, + attestations: [], + resolved_deps: [] + }} + + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -65,7 +72,8 @@ defmodule Opsm.Registries.SwiftPM do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"results" => results}} when is_list(results) -> - packages = results + packages = + results |> Enum.take(limit) |> Enum.map(fn r -> %{ @@ -75,14 +83,18 @@ defmodule Opsm.Registries.SwiftPM do downloads: 0 } end) + {:ok, packages} - _ -> {:ok, []} + + _ -> + {:ok, []} end end def exists?(name) do {owner, repo} = parse_package_id(name) url = "#{@api_url}/packages/#{URI.encode(owner)}/#{URI.encode(repo)}" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -92,12 +104,17 @@ defmodule Opsm.Registries.SwiftPM do def versions(name) do {owner, repo} = parse_package_id(name) url = "#{@api_url}/packages/#{URI.encode(owner)}/#{URI.encode(repo)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions}} when is_list(versions) -> - vers = Enum.map(versions, fn v -> v["reference"]["version"] || v["version"] end) + vers = + Enum.map(versions, fn v -> v["reference"]["version"] || v["version"] end) |> Enum.reject(&is_nil/1) + {:ok, vers} - _ -> {:ok, []} + + _ -> + {:ok, []} end end diff --git a/opsm_ex/lib/opsm/registries/tangle.ex b/opsm_ex/lib/opsm/registries/tangle.ex index b2afeb61..502beda0 100644 --- a/opsm_ex/lib/opsm/registries/tangle.ex +++ b/opsm_ex/lib/opsm/registries/tangle.ex @@ -58,7 +58,8 @@ defmodule Opsm.Registries.Tangle do %{ name: "tangle-julia", url: "https://github.com/hyperpolymath/KRLAdapter.jl", - description: "Julia adapter for Tangle/KRL — KnotTheory.jl integration, verisim modular bridge" + description: + "Julia adapter for Tangle/KRL — KnotTheory.jl integration, verisim modular bridge" }, %{ name: "tangle-groovebind", @@ -98,17 +99,21 @@ defmodule Opsm.Registries.Tangle do defp fetch_from_registry(name, version) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> target = if version == "latest", do: body["latest_version"], else: version {:ok, parse_registry_package(body, target)} - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end defp search_registry(query, opts) do limit = Keyword.get(opts, :limit, 20) url = "#{@base_url}/packages?q=#{URI.encode(query)}&limit=#{limit}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, &parse_search_result/1)} {:error, reason} -> {:error, reason} @@ -124,6 +129,7 @@ defmodule Opsm.Registries.Tangle do defp registry_versions(name) do url = "#{@base_url}/packages/#{URI.encode(name)}/versions" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, & &1["version"])} {:error, reason} -> {:error, reason} @@ -139,9 +145,12 @@ defmodule Opsm.Registries.Tangle do defp search_curated(query, _opts) do q = String.downcase(query) - results = Enum.filter(@known_packages, fn p -> - String.contains?(String.downcase("#{p.name} #{p.description}"), q) - end) + + results = + Enum.filter(@known_packages, fn p -> + String.contains?(String.downcase("#{p.name} #{p.description}"), q) + end) + {:ok, Enum.map(results, &curated_to_resolved(&1, "latest"))} end @@ -154,13 +163,18 @@ defmodule Opsm.Registries.Tangle do pkg_info.url |> String.replace("https://github.com/", "https://api.github.com/repos/") |> Kernel.<>("/tags") + case VerifiedHttp.get_json(tags_url, receive_timeout: 10_000) do {:ok, tags} when is_list(tags) -> versions = Enum.map(tags, & &1["name"]) |> Enum.reject(&is_nil/1) {:ok, if(versions == [], do: ["main"], else: versions)} - _ -> {:ok, ["main"]} + + _ -> + {:ok, ["main"]} end - :not_found -> {:error, :not_found} + + :not_found -> + {:error, :not_found} end end @@ -170,6 +184,7 @@ defmodule Opsm.Registries.Tangle do "https://github.com/hyperpolymath/tangle-#{name}", "https://github.com/hyperpolymath/krl-#{name}" ] + Enum.find_value(urls, {:error, :not_found}, fn url -> case fetch_git_manifest(%{name: name, url: url, description: nil}, version) do {:ok, pkg} -> {:ok, pkg} @@ -184,8 +199,11 @@ defmodule Opsm.Registries.Tangle do base = pkg_info.url try_url = fn manifest -> - url = if path, do: "#{base}/raw/#{branch}/#{path}/#{manifest}", - else: "#{base}/raw/#{branch}/#{manifest}" + url = + if path, + do: "#{base}/raw/#{branch}/#{path}/#{manifest}", + else: "#{base}/raw/#{branch}/#{manifest}" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: text}} -> {:ok, text} _ -> :not_found @@ -229,53 +247,84 @@ defmodule Opsm.Registries.Tangle do resolved_version = manifest.version || version pkg = %ResolvedPackage{ - package: pkg_name, version: resolved_version, forth: :tangle, + package: pkg_name, + version: resolved_version, + forth: :tangle, registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + checksum: nil, + checksum_algo: :sha256, manifest: manifest, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } + {:ok, pkg} end defp curated_to_resolved(pkg_info, version) do %ResolvedPackage{ - package: pkg_info.name, version: version, forth: :tangle, + package: pkg_info.name, + version: version, + forth: :tangle, registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + checksum: nil, + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: pkg_info.name, version: version, description: pkg_info.description, - license: "MPL-2.0", homepage: pkg_info.url, repository: pkg_info.url, - authors: default_authors(), keywords: default_keywords(), - dependencies: %{}, dev_dependencies: %{}, - source_forth: :tangle, raw_manifest: %{"registry" => "tangle-curated"} + name: pkg_info.name, + version: version, + description: pkg_info.description, + license: "MPL-2.0", + homepage: pkg_info.url, + repository: pkg_info.url, + authors: default_authors(), + keywords: default_keywords(), + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :tangle, + raw_manifest: %{"registry" => "tangle-curated"} }, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } end defp parse_registry_package(data, version) do %ResolvedPackage{ - package: data["name"], version: version, forth: :tangle, registry_url: @base_url, + package: data["name"], + version: version, + forth: :tangle, + registry_url: @base_url, tarball_url: "#{@base_url}/packages/#{data["name"]}/#{version}/download", - checksum: data["checksum"], checksum_algo: :sha256, + checksum: data["checksum"], + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: data["name"], version: version, description: data["description"], - license: data["license"], homepage: data["homepage"], repository: data["repository"], - authors: data["authors"] || [], keywords: data["keywords"] || [], + name: data["name"], + version: version, + description: data["description"], + license: data["license"], + homepage: data["homepage"], + repository: data["repository"], + authors: data["authors"] || [], + keywords: data["keywords"] || [], dependencies: data["dependencies"] || %{}, dev_dependencies: data["dev_dependencies"] || %{}, - source_forth: :tangle, raw_manifest: data + source_forth: :tangle, + raw_manifest: data }, - attestations: data["attestations"] || [], resolved_deps: [] + attestations: data["attestations"] || [], + resolved_deps: [] } end defp parse_search_result(r) do - %{name: r["name"], version: r["latest_version"], - description: r["description"], downloads: r["downloads"] || 0} + %{ + name: r["name"], + version: r["latest_version"], + description: r["description"], + downloads: r["downloads"] || 0 + } end defp find_curated(name) do diff --git a/opsm_ex/lib/opsm/registries/tekton_hub.ex b/opsm_ex/lib/opsm/registries/tekton_hub.ex index 25b0678a..b397759a 100644 --- a/opsm_ex/lib/opsm/registries/tekton_hub.ex +++ b/opsm_ex/lib/opsm/registries/tekton_hub.ex @@ -21,11 +21,12 @@ defmodule Opsm.Registries.TektonHub do def fetch_package(name, version \\ "latest") do {catalog, resource_name} = parse_resource_name(name) - url = if version == "latest" do - "#{@api_url}/resource/#{catalog}/#{resource_name}" - else - "#{@api_url}/resource/#{catalog}/#{resource_name}/#{version}" - end + url = + if version == "latest" do + "#{@api_url}/resource/#{catalog}/#{resource_name}" + else + "#{@api_url}/resource/#{catalog}/#{resource_name}/#{version}" + end case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"data" => data}} -> @@ -54,10 +55,15 @@ defmodule Opsm.Registries.TektonHub do latest = data["latestVersion"] || get_in(data, ["latest", "version"]) case latest do - %{"version" => v} -> v - v when is_binary(v) -> v + %{"version" => v} -> + v + + v when is_binary(v) -> + v + _ -> versions = data["versions"] || [] + case versions do [%{"version" => v} | _] -> v _ -> "0.1" @@ -76,26 +82,31 @@ defmodule Opsm.Registries.TektonHub do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"data" => resources}} when is_list(resources) -> - results = Enum.map(resources, fn res -> - latest_ver = get_in(res, ["latestVersion", "version"]) || - get_in(res, ["latest", "version"]) - - %{ - name: "#{res["catalog"]["name"]}/#{res["name"]}", - version: latest_ver, - description: res["description"] || res["summary"] - } - end) + results = + Enum.map(resources, fn res -> + latest_ver = + get_in(res, ["latestVersion", "version"]) || + get_in(res, ["latest", "version"]) + + %{ + name: "#{res["catalog"]["name"]}/#{res["name"]}", + version: latest_ver, + description: res["description"] || res["summary"] + } + end) + {:ok, results} {:ok, resources} when is_list(resources) -> - results = Enum.map(resources, fn res -> - %{ - name: res["name"], - version: get_in(res, ["latestVersion", "version"]), - description: res["description"] - } - end) + results = + Enum.map(resources, fn res -> + %{ + name: res["name"], + version: get_in(res, ["latestVersion", "version"]), + description: res["description"] + } + end) + {:ok, results} {:ok, _} -> @@ -129,13 +140,17 @@ defmodule Opsm.Registries.TektonHub do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"data" => %{"versions" => versions_list}}} when is_list(versions_list) -> - ver_strs = Enum.map(versions_list, fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + ver_strs = + Enum.map(versions_list, fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_strs} {:ok, %{"data" => versions_list}} when is_list(versions_list) -> - ver_strs = Enum.map(versions_list, fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + ver_strs = + Enum.map(versions_list, fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_strs} {:ok, _} -> diff --git a/opsm_ex/lib/opsm/registries/terraform.ex b/opsm_ex/lib/opsm/registries/terraform.ex index cff0fbe3..6f44a320 100644 --- a/opsm_ex/lib/opsm/registries/terraform.ex +++ b/opsm_ex/lib/opsm/registries/terraform.ex @@ -30,11 +30,12 @@ defmodule Opsm.Registries.Terraform do end defp fetch_module(namespace, name, provider, version) do - target_version = if version == "latest" do - fetch_latest_module_version(namespace, name, provider) - else - version - end + target_version = + if version == "latest" do + fetch_latest_module_version(namespace, name, provider) + else + version + end case target_version do nil -> @@ -42,6 +43,7 @@ defmodule Opsm.Registries.Terraform do ver -> url = "#{@registry_url}/modules/#{namespace}/#{name}/#{provider}/#{ver}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> {:ok, parse_module(namespace, name, provider, body, ver)} @@ -62,11 +64,12 @@ defmodule Opsm.Registries.Terraform do end defp fetch_provider(namespace, name, version) do - target_version = if version == "latest" do - fetch_latest_provider_version(namespace, name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_provider_version(namespace, name) + else + version + end case target_version do nil -> @@ -74,6 +77,7 @@ defmodule Opsm.Registries.Terraform do ver -> url = "#{@registry_url}/providers/#{namespace}/#{name}/#{ver}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> {:ok, parse_provider(namespace, name, body, ver)} @@ -114,10 +118,11 @@ defmodule Opsm.Registries.Terraform do limit = Keyword.get(opts, :limit, 20) type = Keyword.get(opts, :type, :module) - endpoint = case type do - :provider -> "/providers" - _ -> "/modules" - end + endpoint = + case type do + :provider -> "/providers" + _ -> "/modules" + end url = "#{@registry_url}#{endpoint}?q=#{URI.encode_www_form(query)}&limit=#{limit}" @@ -184,9 +189,11 @@ defmodule Opsm.Registries.Terraform do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"modules" => [%{"versions" => versions}]}} when is_list(versions) -> - version_list = versions - |> Enum.map(fn %{"version" => v} -> v end) - |> Enum.reverse() + version_list = + versions + |> Enum.map(fn %{"version" => v} -> v end) + |> Enum.reverse() + {:ok, version_list} {:ok, _} -> @@ -208,9 +215,11 @@ defmodule Opsm.Registries.Terraform do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"versions" => versions}} when is_list(versions) -> - version_list = versions - |> Enum.map(fn %{"version" => v} -> v end) - |> Enum.reverse() + version_list = + versions + |> Enum.map(fn %{"version" => v} -> v end) + |> Enum.reverse() + {:ok, version_list} {:ok, _} -> @@ -262,7 +271,8 @@ defmodule Opsm.Registries.Terraform do {:provider, namespace, pkg_name} _ -> - {:error, "Invalid package name format. Expected 'namespace/name' or 'namespace/name/provider'"} + {:error, + "Invalid package name format. Expected 'namespace/name' or 'namespace/name/provider'"} end end @@ -332,31 +342,37 @@ defmodule Opsm.Registries.Terraform do defp extract_module_dependencies(data) do # Terraform modules can have dependencies on other modules or providers # These are typically in the "dependencies" or "providers" fields - providers = data - |> Map.get("providers", []) - |> Enum.reduce(%{}, fn provider, acc -> - case provider do - %{"name" => name, "version" => version} -> - Map.put(acc, name, version) - %{"name" => name} -> - Map.put(acc, name, "*") - _ -> - acc - end - end) - - dependencies = data - |> Map.get("dependencies", []) - |> Enum.reduce(providers, fn dep, acc -> - case dep do - %{"name" => name, "version" => version} -> - Map.put(acc, name, version) - %{"name" => name} -> - Map.put(acc, name, "*") - _ -> - acc - end - end) + providers = + data + |> Map.get("providers", []) + |> Enum.reduce(%{}, fn provider, acc -> + case provider do + %{"name" => name, "version" => version} -> + Map.put(acc, name, version) + + %{"name" => name} -> + Map.put(acc, name, "*") + + _ -> + acc + end + end) + + dependencies = + data + |> Map.get("dependencies", []) + |> Enum.reduce(providers, fn dep, acc -> + case dep do + %{"name" => name, "version" => version} -> + Map.put(acc, name, version) + + %{"name" => name} -> + Map.put(acc, name, "*") + + _ -> + acc + end + end) dependencies end diff --git a/opsm_ex/lib/opsm/registries/vcpkg.ex b/opsm_ex/lib/opsm/registries/vcpkg.ex index acbc5b57..f07ac51e 100644 --- a/opsm_ex/lib/opsm/registries/vcpkg.ex +++ b/opsm_ex/lib/opsm/registries/vcpkg.ex @@ -15,45 +15,53 @@ defmodule Opsm.Registries.Vcpkg do def fetch_package(name, version \\ "latest") do url = "#{@api_url}/#{URI.encode(name)}.json" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest" do - body["Version"] || body["Version-semver"] || body["Version-string"] || "0.0.0" - else - version - end + ver = + if version == "latest" do + body["Version"] || body["Version-semver"] || body["Version-string"] || "0.0.0" + else + version + end deps = extract_deps(body) - {:ok, %ResolvedPackage{ - package: name, - version: ver, - forth: :vcpkg, - registry_url: "#{@web_url}/#{name}", - tarball_url: nil, - checksum: nil, - checksum_algo: nil, - manifest: %ManifestFormat{ - name: name, - version: ver, - description: body["Description"], - license: body["License"], - homepage: body["Homepage"], - repository: nil, - authors: [], - keywords: [], - dependencies: deps, - dev_dependencies: %{}, - source_forth: :vcpkg, - raw_manifest: body - }, - attestations: [], - resolved_deps: [] - }} - - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:ok, + %ResolvedPackage{ + package: name, + version: ver, + forth: :vcpkg, + registry_url: "#{@web_url}/#{name}", + tarball_url: nil, + checksum: nil, + checksum_algo: nil, + manifest: %ManifestFormat{ + name: name, + version: ver, + description: body["Description"], + license: body["License"], + homepage: body["Homepage"], + repository: nil, + authors: [], + keywords: [], + dependencies: deps, + dev_dependencies: %{}, + source_forth: :vcpkg, + raw_manifest: body + }, + attestations: [], + resolved_deps: [] + }} + + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -62,12 +70,15 @@ defmodule Opsm.Registries.Vcpkg do case fetch_package(query) do {:ok, pkg} -> {:ok, [%{name: pkg.package, version: pkg.version, description: nil, downloads: 0}]} - _ -> {:ok, []} + + _ -> + {:ok, []} end end def exists?(name) do url = "#{@api_url}/#{URI.encode(name)}.json" + case VerifiedHttp.get(url, receive_timeout: 5_000) do {:ok, _} -> true _ -> false @@ -86,6 +97,7 @@ defmodule Opsm.Registries.Vcpkg do defp extract_deps(body) do deps = body["Dependencies"] || [] + Enum.into(deps, %{}, fn dep when is_binary(dep) -> {dep, "*"} dep when is_map(dep) -> {dep["name"] || "", dep["version>="] || "*"} diff --git a/opsm_ex/lib/opsm/registries/vim_plugins.ex b/opsm_ex/lib/opsm/registries/vim_plugins.ex index cc0c6a68..82c80d2f 100644 --- a/opsm_ex/lib/opsm/registries/vim_plugins.ex +++ b/opsm_ex/lib/opsm/registries/vim_plugins.ex @@ -22,11 +22,13 @@ defmodule Opsm.Registries.VimPlugins do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - resolved_version = if version == "latest" do - extract_version(body) - else - version - end + resolved_version = + if version == "latest" do + extract_version(body) + else + version + end + {:ok, parse_plugin(body, resolved_version)} {:error, :not_found} -> diff --git a/opsm_ex/lib/opsm/registries/vpm.ex b/opsm_ex/lib/opsm/registries/vpm.ex index 4f0cf96a..653d1b77 100644 --- a/opsm_ex/lib/opsm/registries/vpm.ex +++ b/opsm_ex/lib/opsm/registries/vpm.ex @@ -21,11 +21,12 @@ defmodule Opsm.Registries.Vpm do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_map(body) -> - target_version = if version == "latest" do - body["version"] || body["latest_version"] || fetch_latest_version(name) - else - version - end + target_version = + if version == "latest" do + body["version"] || body["latest_version"] || fetch_latest_version(name) + else + version + end deps = extract_deps(body) {:ok, parse_package(name, body, target_version, deps)} @@ -60,15 +61,19 @@ defmodule Opsm.Registries.Vpm do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, %{"packages" => packages}} when is_list(packages) -> - results = packages - |> Enum.take(limit) - |> Enum.map(&parse_search_result/1) + results = + packages + |> Enum.take(limit) + |> Enum.map(&parse_search_result/1) + {:ok, results} {:ok, packages} when is_list(packages) -> - results = packages - |> Enum.take(limit) - |> Enum.map(&parse_search_result/1) + results = + packages + |> Enum.take(limit) + |> Enum.map(&parse_search_result/1) + {:ok, results} {:ok, _} -> @@ -149,10 +154,13 @@ defmodule Opsm.Registries.Vpm do case dep do %{"name" => dep_name, "version" => ver} -> Map.put(acc, dep_name, ver) + %{"name" => dep_name} -> Map.put(acc, dep_name, ">= 0.0.0") + dep when is_binary(dep) -> Map.put(acc, dep, ">= 0.0.0") + _ -> acc end @@ -186,10 +194,11 @@ defmodule Opsm.Registries.Vpm do version: version, forth: :vpm, registry_url: "#{@web_url}/packages/#{name}", - tarball_url: case tarball_url(name, version) do - {:ok, url} -> url - _ -> nil - end, + tarball_url: + case tarball_url(name, version) do + {:ok, url} -> url + _ -> nil + end, checksum: body["checksum"], checksum_algo: if(body["checksum"], do: :sha256, else: nil), manifest: %ManifestFormat{ diff --git a/opsm_ex/lib/opsm/registries/vscode_marketplace.ex b/opsm_ex/lib/opsm/registries/vscode_marketplace.ex index e5a37ea3..83238104 100644 --- a/opsm_ex/lib/opsm/registries/vscode_marketplace.ex +++ b/opsm_ex/lib/opsm/registries/vscode_marketplace.ex @@ -19,24 +19,34 @@ defmodule Opsm.Registries.VscodeMarketplace do """ def fetch_package(name, version \\ "latest") do {publisher, ext_name} = split_extension_id(name) - url = "#{@api_url}/publishers/#{URI.encode(publisher)}/extensions/#{URI.encode(ext_name)}?api-version=#{@api_version}" + + url = + "#{@api_url}/publishers/#{URI.encode(publisher)}/extensions/#{URI.encode(ext_name)}?api-version=#{@api_version}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest" do - versions_list = body["versions"] || [] - case versions_list do - [latest | _] -> latest["version"] - _ -> "0.0.0" + ver = + if version == "latest" do + versions_list = body["versions"] || [] + + case versions_list do + [latest | _] -> latest["version"] + _ -> "0.0.0" + end + else + version end - else - version - end + {:ok, parse_extension(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -52,10 +62,11 @@ defmodule Opsm.Registries.VscodeMarketplace do display_name = body["displayName"] || ext_name description = body["shortDescription"] || body["description"] - latest_version_data = case body["versions"] do - [v | _] -> v - _ -> %{} - end + latest_version_data = + case body["versions"] do + [v | _] -> v + _ -> %{} + end properties = extract_properties(latest_version_data) deps = extract_extension_dependencies(properties) @@ -79,7 +90,7 @@ defmodule Opsm.Registries.VscodeMarketplace do manifest: manifest, tarball_url: vsix_url, checksum: nil, - attestations: [], + attestations: [] } end @@ -102,6 +113,7 @@ defmodule Opsm.Registries.VscodeMarketplace do defp find_asset_url(version_data, asset_type) do files = version_data["files"] || [] + case Enum.find(files, fn f -> f["assetType"] == asset_type end) do nil -> nil file -> file["source"] @@ -113,16 +125,21 @@ defmodule Opsm.Registries.VscodeMarketplace do """ def get_versions(name) do {publisher, ext_name} = split_extension_id(name) - url = "#{@api_url}/publishers/#{URI.encode(publisher)}/extensions/#{URI.encode(ext_name)}?api-version=#{@api_version}" + + url = + "#{@api_url}/publishers/#{URI.encode(publisher)}/extensions/#{URI.encode(ext_name)}?api-version=#{@api_version}" case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - versions_list = (body["versions"] || []) - |> Enum.map(fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + versions_list = + (body["versions"] || []) + |> Enum.map(fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, versions_list} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -134,7 +151,8 @@ defmodule Opsm.Registries.VscodeMarketplace do _url = "#{@api_url}/extensionquery?api-version=#{@api_version}" # The Marketplace uses POST for search queries; fall back to a GET-based search - search_url = "https://marketplace.visualstudio.com/search?term=#{URI.encode(query)}&target=VSCode&sortBy=Relevance" + search_url = + "https://marketplace.visualstudio.com/search?term=#{URI.encode(query)}&target=VSCode&sortBy=Relevance" case VerifiedHttp.get_json(search_url, receive_timeout: 10_000) do {:ok, body} -> @@ -143,10 +161,13 @@ defmodule Opsm.Registries.VscodeMarketplace do {:error, _} -> # Fallback: direct query - fallback_url = "#{@api_url}/extensionquery/#{URI.encode(query)}?api-version=#{@api_version}" + fallback_url = + "#{@api_url}/extensionquery/#{URI.encode(query)}?api-version=#{@api_version}" + case VerifiedHttp.get_json(fallback_url, receive_timeout: 10_000) do {:ok, body} -> {:ok, extract_search_results(body)} + {:error, reason} -> {:error, reason} end @@ -155,19 +176,23 @@ defmodule Opsm.Registries.VscodeMarketplace do defp extract_search_results(body) do results = body["results"] || [body] - extensions = results - |> Enum.flat_map(fn r -> r["extensions"] || [] end) - |> Enum.take(20) - |> Enum.map(fn ext -> - publisher_name = get_in(ext, ["publisher", "publisherName"]) || "" - ext_name = ext["extensionName"] || ext["name"] || "" - full_name = if publisher_name != "", do: "#{publisher_name}.#{ext_name}", else: ext_name - %{ - name: full_name, - version: get_in(ext, ["versions", Access.at(0), "version"]), - description: ext["shortDescription"] || ext["displayName"] - } - end) + + extensions = + results + |> Enum.flat_map(fn r -> r["extensions"] || [] end) + |> Enum.take(20) + |> Enum.map(fn ext -> + publisher_name = get_in(ext, ["publisher", "publisherName"]) || "" + ext_name = ext["extensionName"] || ext["name"] || "" + full_name = if publisher_name != "", do: "#{publisher_name}.#{ext_name}", else: ext_name + + %{ + name: full_name, + version: get_in(ext, ["versions", Access.at(0), "version"]), + description: ext["shortDescription"] || ext["displayName"] + } + end) + extensions end diff --git a/opsm_ex/lib/opsm/registries/wapm.ex b/opsm_ex/lib/opsm/registries/wapm.ex index 961d27aa..95bc4415 100644 --- a/opsm_ex/lib/opsm/registries/wapm.ex +++ b/opsm_ex/lib/opsm/registries/wapm.ex @@ -57,11 +57,12 @@ defmodule Opsm.Registries.Wapm do {:error, :not_found} {:ok, %{"data" => %{"getPackage" => pkg}}} when is_map(pkg) -> - target_version = if version == "latest" do - get_in(pkg, ["lastVersion", "version"]) - else - version - end + target_version = + if version == "latest" do + get_in(pkg, ["lastVersion", "version"]) + else + version + end deps = extract_deps(pkg) {:ok, parse_package(pkg, target_version, deps)} @@ -109,14 +110,16 @@ defmodule Opsm.Registries.Wapm do case post_graphql(body) do {:ok, %{"data" => %{"searchPackages" => %{"edges" => edges}}}} when is_list(edges) -> - results = Enum.map(edges, fn %{"node" => node} -> - %{ - name: node["name"], - version: get_in(node, ["lastVersion", "version"]), - description: node["description"] || "", - downloads: 0 - } - end) + results = + Enum.map(edges, fn %{"node" => node} -> + %{ + name: node["name"], + version: get_in(node, ["lastVersion", "version"]), + description: node["description"] || "", + downloads: 0 + } + end) + {:ok, results} {:ok, _} -> @@ -158,9 +161,11 @@ defmodule Opsm.Registries.Wapm do case post_graphql(body) do {:ok, %{"data" => %{"getPackage" => %{"versions" => versions}}}} when is_list(versions) -> - ver_list = versions - |> Enum.map(fn v -> v["version"] end) - |> Enum.reject(&is_nil/1) + ver_list = + versions + |> Enum.map(fn v -> v["version"] end) + |> Enum.reject(&is_nil/1) + {:ok, ver_list} {:ok, %{"data" => %{"getPackage" => nil}}} -> @@ -206,8 +211,10 @@ defmodule Opsm.Registries.Wapm do case dep do %{"name" => dep_name, "version" => ver} -> Map.put(acc, dep_name, ver) + %{"name" => dep_name} -> Map.put(acc, dep_name, ">= 0.0.0") + _ -> acc end diff --git a/opsm_ex/lib/opsm/registries/webjars.ex b/opsm_ex/lib/opsm/registries/webjars.ex index 9b9edbbc..99916598 100644 --- a/opsm_ex/lib/opsm/registries/webjars.ex +++ b/opsm_ex/lib/opsm/registries/webjars.ex @@ -19,11 +19,12 @@ defmodule Opsm.Registries.WebJars do The `name` is the WebJar artifact name (e.g., "jquery", "bootstrap"). """ def fetch_package(name, version \\ "latest") do - target_version = if version == "latest" do - fetch_latest_version(name) - else - version - end + target_version = + if version == "latest" do + fetch_latest_version(name) + else + version + end case target_version do nil -> @@ -215,6 +216,7 @@ defmodule Opsm.Registries.WebJars do defp build_jar_url(group_id, artifact_id, version) do group_path = String.replace(group_id, ".", "/") + "https://repo1.maven.org/maven2/#{group_path}/#{artifact_id}/#{version}/#{artifact_id}-#{version}.jar" end @@ -225,6 +227,7 @@ defmodule Opsm.Registries.WebJars do defp extract_deps(nil), do: %{} defp extract_deps(deps) when is_map(deps), do: deps + defp extract_deps(deps) when is_list(deps) do Enum.reduce(deps, %{}, fn %{"name" => name, "version" => ver}, acc -> Map.put(acc, name, ver) @@ -232,5 +235,6 @@ defmodule Opsm.Registries.WebJars do _, acc -> acc end) end + defp extract_deps(_), do: %{} end diff --git a/opsm_ex/lib/opsm/registries/winget_api.ex b/opsm_ex/lib/opsm/registries/winget_api.ex index 9f7f6d02..1a685790 100644 --- a/opsm_ex/lib/opsm/registries/winget_api.ex +++ b/opsm_ex/lib/opsm/registries/winget_api.ex @@ -27,22 +27,29 @@ defmodule Opsm.Registries.WingetApi do pkg = body["package"] || body versions_list = pkg["versions"] || body["versions"] || [] - ver = if version == "latest" do - case versions_list do - [latest | _] -> - if is_map(latest), do: latest["version"], else: latest - _ -> - pkg["version"] || pkg["latestVersion"] || "0.0.0" + ver = + if version == "latest" do + case versions_list do + [latest | _] -> + if is_map(latest), do: latest["version"], else: latest + + _ -> + pkg["version"] || pkg["latestVersion"] || "0.0.0" + end + else + version end - else - version - end {:ok, parse_winget(name, pkg, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -51,13 +58,15 @@ defmodule Opsm.Registries.WingetApi do installers = pkg["installers"] || [] primary_installer = List.first(installers) - installer_url = if primary_installer do - primary_installer["url"] || primary_installer["installerUrl"] - end + installer_url = + if primary_installer do + primary_installer["url"] || primary_installer["installerUrl"] + end - installer_hash = if primary_installer do - primary_installer["sha256"] || primary_installer["installerSha256"] - end + installer_hash = + if primary_installer do + primary_installer["sha256"] || primary_installer["installerSha256"] + end manifest = %ManifestFormat{ name: name, @@ -95,6 +104,7 @@ defmodule Opsm.Registries.WingetApi do end defp parse_dependencies(nil), do: %{} + defp parse_dependencies(deps) when is_list(deps) do deps |> Enum.map(fn @@ -103,6 +113,7 @@ defmodule Opsm.Registries.WingetApi do end) |> Map.new() end + defp parse_dependencies(deps) when is_map(deps), do: deps defp parse_dependencies(_), do: %{} @@ -114,31 +125,37 @@ defmodule Opsm.Registries.WingetApi do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, items} when is_list(items) -> - results = items - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["id"] || pkg["packageIdentifier"], - version: pkg["version"], - description: pkg["description"] - } - end) + results = + items + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["id"] || pkg["packageIdentifier"], + version: pkg["version"], + description: pkg["description"] + } + end) + {:ok, results} {:ok, body} -> packages = body["packages"] || body["results"] || [] - results = packages - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{ - name: pkg["id"] || pkg["packageIdentifier"] || pkg["name"], - version: pkg["version"] || pkg["latestVersion"], - description: pkg["description"] || pkg["shortDescription"] - } - end) + + results = + packages + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["id"] || pkg["packageIdentifier"] || pkg["name"], + version: pkg["version"] || pkg["latestVersion"], + description: pkg["description"] || pkg["shortDescription"] + } + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end @@ -162,16 +179,20 @@ defmodule Opsm.Registries.WingetApi do {:ok, body} -> pkg = body["package"] || body versions_list = pkg["versions"] || body["versions"] || [] - vers = versions_list - |> Enum.map(fn - v when is_map(v) -> v["version"] - v when is_binary(v) -> v - _ -> nil - end) - |> Enum.reject(&is_nil/1) + + vers = + versions_list + |> Enum.map(fn + v when is_map(v) -> v["version"] + v when is_binary(v) -> v + _ -> nil + end) + |> Enum.reject(&is_nil/1) + {:ok, vers} - {:error, _} = err -> err + {:error, _} = err -> + err end end end diff --git a/opsm_ex/lib/opsm/registries/wokelang.ex b/opsm_ex/lib/opsm/registries/wokelang.ex index 1f302492..5812c50b 100644 --- a/opsm_ex/lib/opsm/registries/wokelang.ex +++ b/opsm_ex/lib/opsm/registries/wokelang.ex @@ -92,17 +92,21 @@ defmodule Opsm.Registries.Wokelang do defp fetch_from_registry(name, version) do url = "#{@base_url}/packages/#{URI.encode(name)}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> target = if version == "latest", do: body["latest_version"], else: version {:ok, parse_registry_package(body, target)} - {:error, reason} -> {:error, reason} + + {:error, reason} -> + {:error, reason} end end defp search_registry(query, opts) do limit = Keyword.get(opts, :limit, 20) url = "#{@base_url}/packages?q=#{URI.encode(query)}&limit=#{limit}" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, &parse_search_result/1)} {:error, reason} -> {:error, reason} @@ -118,6 +122,7 @@ defmodule Opsm.Registries.Wokelang do defp registry_versions(name) do url = "#{@base_url}/packages/#{URI.encode(name)}/versions" + case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> {:ok, Enum.map(body, & &1["version"])} {:error, reason} -> {:error, reason} @@ -133,9 +138,12 @@ defmodule Opsm.Registries.Wokelang do defp search_curated(query, _opts) do q = String.downcase(query) - results = Enum.filter(@known_packages, fn p -> - String.contains?(String.downcase("#{p.name} #{p.description}"), q) - end) + + results = + Enum.filter(@known_packages, fn p -> + String.contains?(String.downcase("#{p.name} #{p.description}"), q) + end) + {:ok, Enum.map(results, &curated_to_resolved(&1, "latest"))} end @@ -148,13 +156,18 @@ defmodule Opsm.Registries.Wokelang do pkg_info.url |> String.replace("https://github.com/", "https://api.github.com/repos/") |> Kernel.<>("/tags") + case VerifiedHttp.get_json(tags_url, receive_timeout: 10_000) do {:ok, tags} when is_list(tags) -> versions = Enum.map(tags, & &1["name"]) |> Enum.reject(&is_nil/1) {:ok, if(versions == [], do: ["main"], else: versions)} - _ -> {:ok, ["main"]} + + _ -> + {:ok, ["main"]} end - :not_found -> {:error, :not_found} + + :not_found -> + {:error, :not_found} end end @@ -163,6 +176,7 @@ defmodule Opsm.Registries.Wokelang do "https://github.com/hyperpolymath/#{name}", "https://github.com/hyperpolymath/wokelang-#{name}" ] + Enum.find_value(urls, {:error, :not_found}, fn url -> case fetch_git_manifest(%{name: name, url: url, description: nil}, version) do {:ok, pkg} -> {:ok, pkg} @@ -177,8 +191,11 @@ defmodule Opsm.Registries.Wokelang do base = pkg_info.url try_url = fn manifest -> - url = if path, do: "#{base}/raw/#{branch}/#{path}/#{manifest}", - else: "#{base}/raw/#{branch}/#{manifest}" + url = + if path, + do: "#{base}/raw/#{branch}/#{path}/#{manifest}", + else: "#{base}/raw/#{branch}/#{manifest}" + case VerifiedHttp.get(url, receive_timeout: 10_000) do {:ok, %{body: text}} -> {:ok, text} _ -> :not_found @@ -221,53 +238,84 @@ defmodule Opsm.Registries.Wokelang do resolved_version = manifest.version || version pkg = %ResolvedPackage{ - package: pkg_name, version: resolved_version, forth: :wokelang, + package: pkg_name, + version: resolved_version, + forth: :wokelang, registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + checksum: nil, + checksum_algo: :sha256, manifest: manifest, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } + {:ok, pkg} end defp curated_to_resolved(pkg_info, version) do %ResolvedPackage{ - package: pkg_info.name, version: version, forth: :wokelang, + package: pkg_info.name, + version: version, + forth: :wokelang, registry_url: pkg_info.url, tarball_url: "#{pkg_info.url}/archive/#{version}.tar.gz", - checksum: nil, checksum_algo: :sha256, + checksum: nil, + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: pkg_info.name, version: version, description: pkg_info.description, - license: "MPL-2.0", homepage: pkg_info.url, repository: pkg_info.url, - authors: default_authors(), keywords: default_keywords(), - dependencies: %{}, dev_dependencies: %{}, - source_forth: :wokelang, raw_manifest: %{"registry" => "wokelang-curated"} + name: pkg_info.name, + version: version, + description: pkg_info.description, + license: "MPL-2.0", + homepage: pkg_info.url, + repository: pkg_info.url, + authors: default_authors(), + keywords: default_keywords(), + dependencies: %{}, + dev_dependencies: %{}, + source_forth: :wokelang, + raw_manifest: %{"registry" => "wokelang-curated"} }, - attestations: [], resolved_deps: [] + attestations: [], + resolved_deps: [] } end defp parse_registry_package(data, version) do %ResolvedPackage{ - package: data["name"], version: version, forth: :wokelang, registry_url: @base_url, + package: data["name"], + version: version, + forth: :wokelang, + registry_url: @base_url, tarball_url: "#{@base_url}/packages/#{data["name"]}/#{version}/download", - checksum: data["checksum"], checksum_algo: :sha256, + checksum: data["checksum"], + checksum_algo: :sha256, manifest: %ManifestFormat{ - name: data["name"], version: version, description: data["description"], - license: data["license"], homepage: data["homepage"], repository: data["repository"], - authors: data["authors"] || [], keywords: data["keywords"] || [], + name: data["name"], + version: version, + description: data["description"], + license: data["license"], + homepage: data["homepage"], + repository: data["repository"], + authors: data["authors"] || [], + keywords: data["keywords"] || [], dependencies: data["dependencies"] || %{}, dev_dependencies: data["dev_dependencies"] || %{}, - source_forth: :wokelang, raw_manifest: data + source_forth: :wokelang, + raw_manifest: data }, - attestations: data["attestations"] || [], resolved_deps: [] + attestations: data["attestations"] || [], + resolved_deps: [] } end defp parse_search_result(r) do - %{name: r["name"], version: r["latest_version"], - description: r["description"], downloads: r["downloads"] || 0} + %{ + name: r["name"], + version: r["latest_version"], + description: r["description"], + downloads: r["downloads"] || 0 + } end defp find_curated(name) do diff --git a/opsm_ex/lib/opsm/registries/wordpress.ex b/opsm_ex/lib/opsm/registries/wordpress.ex index b76601b7..ef8545f0 100644 --- a/opsm_ex/lib/opsm/registries/wordpress.ex +++ b/opsm_ex/lib/opsm/registries/wordpress.ex @@ -25,11 +25,13 @@ defmodule Opsm.Registries.WordPress do {:error, :not_found} {:ok, body} when is_map(body) -> - resolved_version = if version == "latest" do - body["version"] - else - version - end + resolved_version = + if version == "latest" do + body["version"] + else + version + end + {:ok, parse_plugin(body, resolved_version)} {:error, :not_found} -> diff --git a/opsm_ex/lib/opsm/registries/wordpress_themes.ex b/opsm_ex/lib/opsm/registries/wordpress_themes.ex index e08f18df..5eca7fa0 100644 --- a/opsm_ex/lib/opsm/registries/wordpress_themes.ex +++ b/opsm_ex/lib/opsm/registries/wordpress_themes.ex @@ -25,11 +25,13 @@ defmodule Opsm.Registries.WordPressThemes do {:error, :not_found} {:ok, body} when is_map(body) -> - resolved_version = if version == "latest" do - body["version"] - else - version - end + resolved_version = + if version == "latest" do + body["version"] + else + version + end + {:ok, parse_theme(body, resolved_version)} {:error, :not_found} -> @@ -191,7 +193,8 @@ defmodule Opsm.Registries.WordPressThemes do end defp normalize_version(v) do - parts = v |> String.replace(~r/^v/, "") |> String.split("-") |> List.first() |> String.split(".") + parts = + v |> String.replace(~r/^v/, "") |> String.split("-") |> List.first() |> String.split(".") case length(parts) do 1 -> Enum.at(parts, 0) <> ".0.0" diff --git a/opsm_ex/lib/opsm/registries/xbps.ex b/opsm_ex/lib/opsm/registries/xbps.ex index e307aa13..7e2936c2 100644 --- a/opsm_ex/lib/opsm/registries/xbps.ex +++ b/opsm_ex/lib/opsm/registries/xbps.ex @@ -28,8 +28,11 @@ defmodule Opsm.Registries.Xbps do pkg_data -> parse_xbps_entry(name, pkg_data, version) end - {:ok, _} -> fetch_from_api(name, version) - {:error, _} -> fetch_from_api(name, version) + {:ok, _} -> + fetch_from_api(name, version) + + {:error, _} -> + fetch_from_api(name, version) end end @@ -38,31 +41,41 @@ defmodule Opsm.Registries.Xbps do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: body["version"] || body["pkgver"], - else: version + ver = + if version == "latest", + do: body["version"] || body["pkgver"], + else: version + {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end defp parse_xbps_entry(name, data, version) do - ver = if version == "latest", - do: data["pkgver"] || data["version"] || "0.0.0", - else: version + ver = + if version == "latest", + do: data["pkgver"] || data["version"] || "0.0.0", + else: version + {:ok, build_resolved_package(name, data, ver)} end defp build_resolved_package(name, body, version) do - deps = (body["run_depends"] || body["dependencies"] || []) - |> Enum.map(fn d -> - dep_name = String.replace(d, ~r/[><=].*/, "") - {dep_name, "*"} - end) - |> Map.new() + deps = + (body["run_depends"] || body["dependencies"] || []) + |> Enum.map(fn d -> + dep_name = String.replace(d, ~r/[><=].*/, "") + {dep_name, "*"} + end) + |> Map.new() manifest = %ManifestFormat{ name: name, @@ -82,7 +95,7 @@ defmodule Opsm.Registries.Xbps do tarball_url: build_tarball_url(name, version), checksum: body["filename-sha256"], checksum_algo: if(body["filename-sha256"], do: :sha256), - attestations: [], + attestations: [] } end @@ -109,15 +122,24 @@ defmodule Opsm.Registries.Xbps do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} when is_list(body) -> - results = body - |> Enum.take(20) - |> Enum.map(fn pkg -> - %{name: pkg["pkgname"] || pkg["name"], version: pkg["version"], description: pkg["short_desc"]} - end) + results = + body + |> Enum.take(20) + |> Enum.map(fn pkg -> + %{ + name: pkg["pkgname"] || pkg["name"], + version: pkg["version"], + description: pkg["short_desc"] + } + end) + {:ok, results} - {:ok, _} -> {:ok, []} - {:error, reason} -> {:error, reason} + {:ok, _} -> + {:ok, []} + + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registries/zypper.ex b/opsm_ex/lib/opsm/registries/zypper.ex index 9c4d3e4e..52e7636d 100644 --- a/opsm_ex/lib/opsm/registries/zypper.ex +++ b/opsm_ex/lib/opsm/registries/zypper.ex @@ -23,14 +23,21 @@ defmodule Opsm.Registries.Zypper do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> - ver = if version == "latest", - do: extract_version(body), - else: version + ver = + if version == "latest", + do: extract_version(body), + else: version + {:ok, build_resolved_package(name, body, ver)} - {:error, :not_found} -> fetch_from_factory(name, version) - {:error, %{status: 404}} -> fetch_from_factory(name, version) - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + fetch_from_factory(name, version) + + {:error, %{status: 404}} -> + fetch_from_factory(name, version) + + {:error, reason} -> + {:error, reason} end end @@ -40,18 +47,28 @@ defmodule Opsm.Registries.Zypper do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> binaries = body["binary"] || body["collection"] || [] + case extract_binary_info(binaries, name) do - nil -> {:error, :not_found} + nil -> + {:error, :not_found} + info -> - ver = if version == "latest", - do: info["version"] || "0.0.0", - else: version + ver = + if version == "latest", + do: info["version"] || "0.0.0", + else: version + {:ok, build_resolved_from_binary(name, info, ver)} end - {:error, :not_found} -> {:error, :not_found} - {:error, %{status: 404}} -> {:error, :not_found} - {:error, reason} -> {:error, reason} + {:error, :not_found} -> + {:error, :not_found} + + {:error, %{status: 404}} -> + {:error, :not_found} + + {:error, reason} -> + {:error, reason} end end @@ -86,7 +103,7 @@ defmodule Opsm.Registries.Zypper do manifest: manifest, tarball_url: nil, checksum: nil, - attestations: [], + attestations: [] } end @@ -108,13 +125,17 @@ defmodule Opsm.Registries.Zypper do manifest: manifest, tarball_url: info["filepath"], checksum: nil, - attestations: [], + attestations: [] } end defp extract_dependencies(body) do (body["requires"] || body["buildrequires"] || []) - |> Enum.map(fn d when is_binary(d) -> {d, "*"}; d when is_map(d) -> {d["name"] || "", "*"}; _ -> nil end) + |> Enum.map(fn + d when is_binary(d) -> {d, "*"} + d when is_map(d) -> {d["name"] || "", "*"} + _ -> nil + end) |> Enum.reject(&is_nil/1) |> Enum.reject(fn {n, _} -> n == "" end) |> Map.new() @@ -132,7 +153,8 @@ defmodule Opsm.Registries.Zypper do ver = extract_version(body) {:ok, [ver]} - {:error, _} = err -> err + {:error, _} = err -> + err end end @@ -145,19 +167,23 @@ defmodule Opsm.Registries.Zypper do case VerifiedHttp.get_json(url, receive_timeout: 10_000) do {:ok, body} -> binaries = body["binary"] || body["collection"] || [] - results = binaries - |> List.wrap() - |> Enum.take(20) - |> Enum.map(fn b -> - %{ - name: b["name"] || b["@name"], - version: b["version"], - description: b["description"] || b["summary"] - } - end) + + results = + binaries + |> List.wrap() + |> Enum.take(20) + |> Enum.map(fn b -> + %{ + name: b["name"] || b["@name"], + version: b["version"], + description: b["description"] || b["summary"] + } + end) + {:ok, results} - {:error, reason} -> {:error, reason} + {:error, reason} -> + {:error, reason} end end diff --git a/opsm_ex/lib/opsm/registry_gateway.ex b/opsm_ex/lib/opsm/registry_gateway.ex index 4b8d714f..2d7052fa 100644 --- a/opsm_ex/lib/opsm/registry_gateway.ex +++ b/opsm_ex/lib/opsm/registry_gateway.ex @@ -11,6 +11,7 @@ defmodule Opsm.RegistryGateway do def publish(manifest, imp, digest) do manifest_map = normalize_manifest(manifest) name = manifest_map["name"] + entry = %{ manifest: manifest_map, imp: imp, diff --git a/opsm_ex/lib/opsm/registry_gateway/router.ex b/opsm_ex/lib/opsm/registry_gateway/router.ex index f8472633..a72cb86a 100644 --- a/opsm_ex/lib/opsm/registry_gateway/router.ex +++ b/opsm_ex/lib/opsm/registry_gateway/router.ex @@ -5,9 +5,9 @@ defmodule Opsm.RegistryGateway.Router do alias Opsm.RegistryGateway - plug :match - plug Plug.Parsers, parsers: [:json], json_decoder: Jason - plug :dispatch + plug(:match) + plug(Plug.Parsers, parsers: [:json], json_decoder: Jason) + plug(:dispatch) post "/packages/publish" do case RegistryGateway.handle_publish(conn.body_params) do diff --git a/opsm_ex/lib/opsm/resolver.ex b/opsm_ex/lib/opsm/resolver.ex index 8424b762..1b765515 100644 --- a/opsm_ex/lib/opsm/resolver.ex +++ b/opsm_ex/lib/opsm/resolver.ex @@ -102,14 +102,18 @@ defmodule Opsm.Resolver do resolution = final_state.resolved # Persist resolution event to VeriSimDB - root_name = case root_dependencies do - [first | _] -> first.name - _ -> "unknown" - end - root_constraint = case root_dependencies do - [first | _] -> first.constraint - _ -> "*" - end + root_name = + case root_dependencies do + [first | _] -> first.name + _ -> "unknown" + end + + root_constraint = + case root_dependencies do + [first | _] -> first.constraint + _ -> "*" + end + Opsm.VeriSimDB.record_resolution(%{ root_package: root_name, root_constraint: root_constraint, @@ -195,13 +199,14 @@ defmodule Opsm.Resolver do {:conflict, "Circular dependency detected: #{key}"} state.depth >= state.max_depth -> - {:conflict, "Dependency tree depth (#{state.max_depth}) exceeded at #{package_name}. Possible circular dependency."} + {:conflict, + "Dependency tree depth (#{state.max_depth}) exceeded at #{package_name}. Possible circular dependency."} true -> # Fetch package metadata (cached) case RegistryCache.fetch_or_compute({:fetch, forth, package_name, version}, fn -> - Registry.fetch(forth, package_name, version) - end) do + Registry.fetch(forth, package_name, version) + end) do {:ok, resolved_pkg} -> # Add to resolved set, remove from unresolved, increment tree depth new_state = %{ @@ -253,9 +258,10 @@ defmodule Opsm.Resolver do do: state.unresolved, else: MapSet.put(state.unresolved, package_name) - %{state | - constraints: Map.put(state.constraints, package_name, updated_constraints), - unresolved: unresolved + %{ + state + | constraints: Map.put(state.constraints, package_name, updated_constraints), + unresolved: unresolved } end @@ -283,9 +289,7 @@ defmodule Opsm.Resolver do if violating == [] do {:cont, :ok} else - {:halt, - {:conflict, - format_constraint_violation(pkg_name, version, violating)}} + {:halt, {:conflict, format_constraint_violation(pkg_name, version, violating)}} end :error -> @@ -301,7 +305,9 @@ defmodule Opsm.Resolver do defp next_unresolved(state) do # Pick from tracked unresolved set (O(1) membership check, no full scan) case MapSet.size(state.unresolved) do - 0 -> nil + 0 -> + nil + _ -> # Pick the package with the most constraints (most constrained first = fail fast) state.unresolved @@ -363,39 +369,48 @@ defmodule Opsm.Resolver do # Fetch oikos sustainability scores for candidate versions. # Combines sustainability score (0-100) with version recency. # Falls back to version-only sorting if oikos is unavailable. - config = try do - Opsm.Config.load_config_or_example() - rescue - _ -> nil - end - - oikos_scores = if config do + config = try do - client = Opsm.Clients.Oikos.new(config.oikos, config.http) - case Opsm.Clients.Oikos.health(client) do - {:ok, _} -> - # Oikos is up — score each version via package-level analysis - versions - |> Enum.map(fn ver -> - score = RegistryCache.fetch_or_compute({:oikos, ver}, fn -> - case Opsm.Clients.Oikos.analyze_package(client, "pkg", ver) do - {:ok, s} when is_number(s) -> s - _ -> 50 - end - end, 60_000) - {ver, score} - end) - |> Map.new() - - {:error, _} -> - nil - end + Opsm.Config.load_config_or_example() rescue _ -> nil end - else - nil - end + + oikos_scores = + if config do + try do + client = Opsm.Clients.Oikos.new(config.oikos, config.http) + + case Opsm.Clients.Oikos.health(client) do + {:ok, _} -> + # Oikos is up — score each version via package-level analysis + versions + |> Enum.map(fn ver -> + score = + RegistryCache.fetch_or_compute( + {:oikos, ver}, + fn -> + case Opsm.Clients.Oikos.analyze_package(client, "pkg", ver) do + {:ok, s} when is_number(s) -> s + _ -> 50 + end + end, + 60_000 + ) + + {ver, score} + end) + |> Map.new() + + {:error, _} -> + nil + end + rescue + _ -> nil + end + else + nil + end case oikos_scores do nil -> @@ -409,7 +424,9 @@ defmodule Opsm.Resolver do s2 = Map.get(scores, v2, 0) cond do - s1 != s2 -> s1 > s2 + s1 != s2 -> + s1 > s2 + true -> case {parse_version_lenient(v1), parse_version_lenient(v2)} do {{:ok, ver1}, {:ok, ver2}} -> Version.compare(ver1, ver2) == :gt @@ -461,14 +478,18 @@ defmodule Opsm.Resolver do # Lenient version parsing for non-semver formats (Go v-prefix, Hackage 4-part) defp parse_version_lenient(version_string) do - normalized = version_string + normalized = + version_string |> String.trim_leading("v") |> String.trim_leading("V") case Version.parse(normalized) do - {:ok, version} -> {:ok, version} + {:ok, version} -> + {:ok, version} + :error -> parts = String.split(normalized, ".") + if length(parts) > 3 do truncated = parts |> Enum.take(3) |> Enum.join(".") Version.parse(truncated) diff --git a/opsm_ex/lib/opsm/runtime/manager.ex b/opsm_ex/lib/opsm/runtime/manager.ex index fe544e38..c3231250 100644 --- a/opsm_ex/lib/opsm/runtime/manager.ex +++ b/opsm_ex/lib/opsm/runtime/manager.ex @@ -25,8 +25,8 @@ defmodule Opsm.Runtime.Manager do alias Opsm.Verified.Http, as: VerifiedHttp @runtimes_dir Path.expand("~/.opsm/runtimes") - @active_file Path.join(@runtimes_dir, ".active") - @plugins_dir Path.join(@runtimes_dir, ".plugins") + @active_file Path.join(@runtimes_dir, ".active") + @plugins_dir Path.join(@runtimes_dir, ".plugins") # --------------------------------------------------------------------------- # Listing @@ -46,18 +46,23 @@ defmodule Opsm.Runtime.Manager do |> Enum.reject(&String.starts_with?(&1, ".")) |> Enum.flat_map(fn tool_dir -> tool_path = Path.join(@runtimes_dir, tool_dir) + case File.ls(tool_path) do {:ok, versions} -> active_ver = Map.get(active, tool_dir) + Enum.map(versions, fn ver -> %{name: tool_dir, version: ver, active: ver == active_ver} end) - _ -> [] + + _ -> + [] end end) |> Enum.sort_by(& &1.name) - _ -> [] + _ -> + [] end end @@ -73,7 +78,9 @@ defmodule Opsm.Runtime.Manager do case File.ls(@runtimes_dir) do {:ok, entries} -> entries |> Enum.reject(&String.starts_with?(&1, ".")) |> MapSet.new() - _ -> MapSet.new() + + _ -> + MapSet.new() end active @@ -133,6 +140,7 @@ defmodule Opsm.Runtime.Manager do case latest_version(tool) do {:ok, latest} when latest != current -> [%{name: tool, current: current, latest: latest}] + _ -> [] end @@ -162,13 +170,15 @@ defmodule Opsm.Runtime.Manager do :ok else with {:ok, plugin} <- load_plugin(tool) do - strategy = get_in(plugin, ["install", "strategy"]) || - get_in(plugin, [:install, :strategy]) + strategy = + get_in(plugin, ["install", "strategy"]) || + get_in(plugin, [:install, :strategy]) result = case to_string(strategy) do s when s in ["BuildFromSource", "DelegateToManager", "Both"] -> SourceBuilder.install(plugin, version) + _ -> install_prebuilt(tool, version, plugin, install_dir) end @@ -177,6 +187,7 @@ defmodule Opsm.Runtime.Manager do :ok -> set_active(tool, version) :ok + error -> error end @@ -194,6 +205,7 @@ defmodule Opsm.Runtime.Manager do {:ok, content} -> pins = parse_runtime_section(content) {:ok, pins} + {:error, reason} -> {:error, reason} end @@ -217,6 +229,7 @@ defmodule Opsm.Runtime.Manager do active = load_active() write_active(Map.delete(active, tool)) :ok + {:error, reason, _} -> {:error, reason} end @@ -234,7 +247,9 @@ defmodule Opsm.Runtime.Manager do """ def which(tool) do case current_version(tool) do - "none" -> {:error, :not_installed} + "none" -> + {:error, :not_installed} + version -> bin_dir = Path.join([@runtimes_dir, tool, version, "bin"]) {:ok, Path.join(bin_dir, tool)} @@ -257,16 +272,20 @@ defmodule Opsm.Runtime.Manager do _ -> acc end end) - _ -> %{} + + _ -> + %{} end end defp write_active(active_map) do File.mkdir_p!(Path.dirname(@active_file)) + content = active_map |> Enum.map(fn {k, v} -> "#{k} = #{v}" end) |> Enum.join("\n") + File.write!(@active_file, content <> "\n") end @@ -288,14 +307,18 @@ defmodule Opsm.Runtime.Manager do else # Try evaluating the Nickel plugin directly plugin_ncl = find_plugin_ncl(tool) + case plugin_ncl do nil -> {:error, {:no_plugin, tool}} + path -> # System.cmd/3 raises ErlangError (:enoent) when the nickel binary # is not installed — surface that as a structured error instead. try do - case System.cmd("nickel", ["export", "--format", "json", path], stderr_to_stdout: true) do + case System.cmd("nickel", ["export", "--format", "json", path], + stderr_to_stdout: true + ) do {json_str, 0} -> case Jason.decode(json_str) do {:ok, plugin} -> @@ -303,8 +326,11 @@ defmodule Opsm.Runtime.Manager do File.mkdir_p!(@plugins_dir) File.write!(cached, json_str) {:ok, plugin} - err -> {:error, {:json_decode, err}} + + err -> + {:error, {:json_decode, err}} end + {err_str, _} -> {:error, {:nickel_eval, err_str}} end @@ -377,6 +403,7 @@ defmodule Opsm.Runtime.Manager do |> Enum.reject(&is_nil/1) |> Enum.map(&String.trim_leading(&1, "v")) |> Enum.sort(:desc) + {:ok, versions} {:error, reason} -> @@ -403,10 +430,10 @@ defmodule Opsm.Runtime.Manager do arch = uname_m |> String.trim() case {os, arch} do - {"linux", "x86_64"} -> :linux_amd64 + {"linux", "x86_64"} -> :linux_amd64 {"linux", "aarch64"} -> :linux_arm64 {"darwin", "x86_64"} -> :darwin_amd64 - {"darwin", "arm64"} -> :darwin_arm64 + {"darwin", "arm64"} -> :darwin_arm64 _other -> :linux_amd64 end end @@ -418,17 +445,22 @@ defmodule Opsm.Runtime.Manager do defp resolve_archive_url(_tool, version, platform, plugin, nil) do # Fall back to archive_name_template from the platforms list - platforms = get_in(plugin, ["install", "platforms"]) || - get_in(plugin, [:install, :platforms]) || [] + platforms = + get_in(plugin, ["install", "platforms"]) || + get_in(plugin, [:install, :platforms]) || [] platform_str = Atom.to_string(platform) - entry = Enum.find(platforms, fn p -> - p_str = to_string(p["platform"] || p[:platform] || "") - String.downcase(p_str) == String.downcase(platform_str) - end) + + entry = + Enum.find(platforms, fn p -> + p_str = to_string(p["platform"] || p[:platform] || "") + String.downcase(p_str) == String.downcase(platform_str) + end) case entry do - nil -> {:error, {:unsupported_platform, platform}} + nil -> + {:error, {:unsupported_platform, platform}} + e -> template = e["archive_name_template"] || e[:archive_name_template] repo = plugin["repository"] || plugin[:repository] @@ -450,7 +482,9 @@ defmodule Opsm.Runtime.Manager do # Use Req (via VerifiedHttp.get/2) with streaming to avoid loading the # entire archive into memory. Falls back to :httpc if Req is unavailable. case VerifiedHttp.get(url, receive_timeout: 120_000, into: File.stream!(dest)) do - {:ok, _} -> {:ok, dest} + {:ok, _} -> + {:ok, dest} + {:error, reason} -> File.rm(dest) {:error, {:download_failed, reason}} @@ -459,21 +493,41 @@ defmodule Opsm.Runtime.Manager do end defp extract_archive(archive_path, install_dir, plugin) do - strip = get_in(plugin, ["install", "strip_components"]) || - get_in(plugin, [:install, :strip_components]) || 1 + strip = + get_in(plugin, ["install", "strip_components"]) || + get_in(plugin, [:install, :strip_components]) || 1 File.mkdir_p!(install_dir) cond do String.ends_with?(archive_path, ".tar.xz") -> - {_, 0} = System.cmd("tar", ["-xJf", archive_path, "--strip-components=#{strip}", "-C", install_dir]) + {_, 0} = + System.cmd("tar", [ + "-xJf", + archive_path, + "--strip-components=#{strip}", + "-C", + install_dir + ]) + :ok + String.ends_with?(archive_path, ".tar.gz") -> - {_, 0} = System.cmd("tar", ["-xzf", archive_path, "--strip-components=#{strip}", "-C", install_dir]) + {_, 0} = + System.cmd("tar", [ + "-xzf", + archive_path, + "--strip-components=#{strip}", + "-C", + install_dir + ]) + :ok + String.ends_with?(archive_path, ".zip") -> {_, 0} = System.cmd("unzip", ["-q", archive_path, "-d", install_dir]) :ok + true -> # Raw binary — just make executable and put in bin/ bin_dir = Path.join(install_dir, "bin") @@ -492,22 +546,29 @@ defmodule Opsm.Runtime.Manager do |> String.split("\n") |> Enum.reduce({false, []}, fn line, {in_section, acc} -> stripped = String.trim(line) + cond do stripped == "[runtime]" -> {true, acc} + String.starts_with?(stripped, "#") -> {in_section, acc} + String.starts_with?(stripped, "[") and in_section -> {false, acc} + in_section and String.contains?(stripped, "=") -> [tool, version] = String.split(stripped, "=", parts: 2) + version = version |> String.split("#", parts: 2) |> hd() |> String.trim() |> String.trim("\"") + {true, [{String.trim(tool), version} | acc]} + true -> {in_section, acc} end diff --git a/opsm_ex/lib/opsm/runtime/source_builder.ex b/opsm_ex/lib/opsm/runtime/source_builder.ex index 16ae37e1..e4e444b0 100644 --- a/opsm_ex/lib/opsm/runtime/source_builder.ex +++ b/opsm_ex/lib/opsm/runtime/source_builder.ex @@ -98,8 +98,10 @@ defmodule Opsm.Runtime.SourceBuilder do defp delegate_install(plugin, version, _install_dir) do delegate = plugin["install"]["delegate_to"] || plugin[:install][:delegate_to] - cmd_template = plugin["install"]["delegate_install_command"] || - plugin[:install][:delegate_install_command] + + cmd_template = + plugin["install"]["delegate_install_command"] || + plugin[:install][:delegate_install_command] cmd = String.replace(cmd_template, "{{version}}", version) @@ -160,6 +162,7 @@ defmodule Opsm.Runtime.SourceBuilder do defp resolve_source_url(_tool_name, version, url_handler) do template = url_handler["archive_url_template"] || url_handler[:archive_url_template] + if template do String.replace(template, "{{version}}", version) else @@ -189,6 +192,7 @@ defmodule Opsm.Runtime.SourceBuilder do defp verify_health_check(plugin) do check = plugin["health_check"] || plugin[:health_check] + if check && command_succeeds?(check) do :ok else @@ -219,8 +223,12 @@ defmodule Opsm.Runtime.SourceBuilder do defp extract_archive(archive_path, dest_dir) do File.mkdir_p!(dest_dir) - result = System.cmd("tar", ["-xzf", archive_path, "-C", dest_dir, "--strip-components=1"], - stderr_to_stdout: true) + + result = + System.cmd("tar", ["-xzf", archive_path, "-C", dest_dir, "--strip-components=1"], + stderr_to_stdout: true + ) + case result do {_, 0} -> :ok {output, code} -> {:error, {:tar_failed, code, output}} @@ -239,6 +247,7 @@ defmodule Opsm.Runtime.SourceBuilder do end defp command_succeeds?(nil), do: true + defp command_succeeds?(cmd) do case System.cmd("sh", ["-c", "#{cmd} >/dev/null 2>&1"], []) do {_, 0} -> true @@ -260,13 +269,14 @@ defmodule Opsm.Runtime.SourceBuilder do defp github_repo(tool), do: tool defp get_strategy(plugin) do - strategy_str = get_in(plugin, ["install", "strategy"]) || - get_in(plugin, [:install, :strategy]) + strategy_str = + get_in(plugin, ["install", "strategy"]) || + get_in(plugin, [:install, :strategy]) case strategy_str do - "PrebuiltBinary" -> :prebuilt_binary - "BuildFromSource" -> :build_from_source - "Both" -> :both + "PrebuiltBinary" -> :prebuilt_binary + "BuildFromSource" -> :build_from_source + "Both" -> :both "DelegateToManager" -> :delegate_to_manager atom when is_atom(atom) -> atom _ -> :unknown diff --git a/opsm_ex/lib/opsm/runtime/url_handler.ex b/opsm_ex/lib/opsm/runtime/url_handler.ex index 26349c80..4861d90b 100644 --- a/opsm_ex/lib/opsm/runtime/url_handler.ex +++ b/opsm_ex/lib/opsm/runtime/url_handler.ex @@ -96,7 +96,8 @@ defmodule Opsm.Runtime.UrlHandler do # Public for testing only — not part of the external API. @doc false - def process_versions_body(body, pattern, tool_name), do: extract_versions(body, pattern, tool_name) + def process_versions_body(body, pattern, tool_name), + do: extract_versions(body, pattern, tool_name) defp extract_versions(body, pattern, tool_name) when is_map(body) do # JSON object: keys are version strings (e.g., Zig's index.json) @@ -120,6 +121,7 @@ defmodule Opsm.Runtime.UrlHandler do end defp filter_versions(versions, nil), do: versions + defp filter_versions(versions, pattern) do {:ok, re} = Regex.compile(pattern) Enum.filter(versions, &Regex.match?(re, &1)) @@ -135,6 +137,7 @@ defmodule Opsm.Runtime.UrlHandler do defp parse_ver(v) do # Strip leading "v" or "go" prefixes, then parse as semver cleaned = v |> String.trim_leading("v") |> String.trim_leading("go") + parts = cleaned |> String.split(".") @@ -143,6 +146,7 @@ defmodule Opsm.Runtime.UrlHandler do {n, _} -> n :error -> 0 end) + List.to_tuple(parts) rescue _ -> {0, 0, 0} @@ -154,8 +158,8 @@ defmodule Opsm.Runtime.UrlHandler do defp platform_vars("zig", platform) do case platform do - :linux_amd64 -> {:ok, %{"zig_platform" => "linux-x86_64"}} - :linux_arm64 -> {:ok, %{"zig_platform" => "linux-aarch64"}} + :linux_amd64 -> {:ok, %{"zig_platform" => "linux-x86_64"}} + :linux_arm64 -> {:ok, %{"zig_platform" => "linux-aarch64"}} :darwin_amd64 -> {:ok, %{"zig_platform" => "macos-x86_64"}} :darwin_arm64 -> {:ok, %{"zig_platform" => "macos-aarch64"}} :windows_amd64 -> {:ok, %{"zig_platform" => "windows-x86_64"}} @@ -165,46 +169,86 @@ defmodule Opsm.Runtime.UrlHandler do defp platform_vars("julia", platform) do case platform do - :linux_amd64 -> {:ok, %{"julia_os" => "linux", "julia_arch" => "x64", - "julia_arch_suffix" => ".tar.gz", "julia_minor" => "1"}} - :linux_arm64 -> {:ok, %{"julia_os" => "linux", "julia_arch" => "aarch64", - "julia_arch_suffix" => ".tar.gz", "julia_minor" => "1"}} - :darwin_amd64 -> {:ok, %{"julia_os" => "mac", "julia_arch" => "x64", - "julia_arch_suffix" => ".dmg", "julia_minor" => "1"}} - :darwin_arm64 -> {:ok, %{"julia_os" => "mac", "julia_arch" => "aarch64", - "julia_arch_suffix" => ".dmg", "julia_minor" => "1"}} - _ -> {:error, :unsupported_platform} + :linux_amd64 -> + {:ok, + %{ + "julia_os" => "linux", + "julia_arch" => "x64", + "julia_arch_suffix" => ".tar.gz", + "julia_minor" => "1" + }} + + :linux_arm64 -> + {:ok, + %{ + "julia_os" => "linux", + "julia_arch" => "aarch64", + "julia_arch_suffix" => ".tar.gz", + "julia_minor" => "1" + }} + + :darwin_amd64 -> + {:ok, + %{ + "julia_os" => "mac", + "julia_arch" => "x64", + "julia_arch_suffix" => ".dmg", + "julia_minor" => "1" + }} + + :darwin_arm64 -> + {:ok, + %{ + "julia_os" => "mac", + "julia_arch" => "aarch64", + "julia_arch_suffix" => ".dmg", + "julia_minor" => "1" + }} + + _ -> + {:error, :unsupported_platform} end end defp platform_vars("golang", platform) do case platform do - :linux_amd64 -> {:ok, %{"go_os" => "linux", "go_arch" => "amd64", "go_version" => "go{{version}}"}} - :linux_arm64 -> {:ok, %{"go_os" => "linux", "go_arch" => "arm64", "go_version" => "go{{version}}"}} - :darwin_amd64 -> {:ok, %{"go_os" => "darwin", "go_arch" => "amd64", "go_version" => "go{{version}}"}} - :darwin_arm64 -> {:ok, %{"go_os" => "darwin", "go_arch" => "arm64", "go_version" => "go{{version}}"}} - :windows_amd64 -> {:ok, %{"go_os" => "windows", "go_arch" => "amd64", "go_version" => "go{{version}}"}} - _ -> {:error, :unsupported_platform} + :linux_amd64 -> + {:ok, %{"go_os" => "linux", "go_arch" => "amd64", "go_version" => "go{{version}}"}} + + :linux_arm64 -> + {:ok, %{"go_os" => "linux", "go_arch" => "arm64", "go_version" => "go{{version}}"}} + + :darwin_amd64 -> + {:ok, %{"go_os" => "darwin", "go_arch" => "amd64", "go_version" => "go{{version}}"}} + + :darwin_arm64 -> + {:ok, %{"go_os" => "darwin", "go_arch" => "arm64", "go_version" => "go{{version}}"}} + + :windows_amd64 -> + {:ok, %{"go_os" => "windows", "go_arch" => "amd64", "go_version" => "go{{version}}"}} + + _ -> + {:error, :unsupported_platform} end end defp platform_vars("nodejs", platform) do case platform do - :linux_amd64 -> {:ok, %{"node_os" => "linux", "node_arch" => "x64"}} - :linux_arm64 -> {:ok, %{"node_os" => "linux", "node_arch" => "arm64"}} - :darwin_amd64 -> {:ok, %{"node_os" => "darwin", "node_arch" => "x64"}} - :darwin_arm64 -> {:ok, %{"node_os" => "darwin", "node_arch" => "arm64"}} - :windows_amd64 -> {:ok, %{"node_os" => "win", "node_arch" => "x64"}} + :linux_amd64 -> {:ok, %{"node_os" => "linux", "node_arch" => "x64"}} + :linux_arm64 -> {:ok, %{"node_os" => "linux", "node_arch" => "arm64"}} + :darwin_amd64 -> {:ok, %{"node_os" => "darwin", "node_arch" => "x64"}} + :darwin_arm64 -> {:ok, %{"node_os" => "darwin", "node_arch" => "arm64"}} + :windows_amd64 -> {:ok, %{"node_os" => "win", "node_arch" => "x64"}} _ -> {:error, :unsupported_platform} end end defp platform_vars("dart", platform) do case platform do - :linux_amd64 -> {:ok, %{"dart_os" => "linux", "dart_arch" => "x64"}} - :linux_arm64 -> {:ok, %{"dart_os" => "linux", "dart_arch" => "arm64"}} - :darwin_amd64 -> {:ok, %{"dart_os" => "macos", "dart_arch" => "x64"}} - :darwin_arm64 -> {:ok, %{"dart_os" => "macos", "dart_arch" => "arm64"}} + :linux_amd64 -> {:ok, %{"dart_os" => "linux", "dart_arch" => "x64"}} + :linux_arm64 -> {:ok, %{"dart_os" => "linux", "dart_arch" => "arm64"}} + :darwin_amd64 -> {:ok, %{"dart_os" => "macos", "dart_arch" => "x64"}} + :darwin_arm64 -> {:ok, %{"dart_os" => "macos", "dart_arch" => "arm64"}} :windows_amd64 -> {:ok, %{"dart_os" => "windows", "dart_arch" => "x64"}} _ -> {:error, :unsupported_platform} end @@ -213,10 +257,10 @@ defmodule Opsm.Runtime.UrlHandler do defp platform_vars("nim", platform) do # Nim archives: nim-2.0.2_linux_x64.tar.xz or nim-2.0.2_macos_arm64.tar.xz case platform do - :linux_amd64 -> {:ok, %{"nim_os" => "linux", "nim_arch" => "x64"}} - :linux_arm64 -> {:ok, %{"nim_os" => "linux", "nim_arch" => "arm64"}} - :darwin_amd64 -> {:ok, %{"nim_os" => "macos", "nim_arch" => "x64"}} - :darwin_arm64 -> {:ok, %{"nim_os" => "macos", "nim_arch" => "arm64"}} + :linux_amd64 -> {:ok, %{"nim_os" => "linux", "nim_arch" => "x64"}} + :linux_arm64 -> {:ok, %{"nim_os" => "linux", "nim_arch" => "arm64"}} + :darwin_amd64 -> {:ok, %{"nim_os" => "macos", "nim_arch" => "x64"}} + :darwin_arm64 -> {:ok, %{"nim_os" => "macos", "nim_arch" => "arm64"}} _ -> {:error, :unsupported_platform} end end @@ -224,10 +268,10 @@ defmodule Opsm.Runtime.UrlHandler do defp platform_vars("kubectl", platform) do # kubectl binary at: dl.k8s.io/release/v{{version}}/bin/linux/amd64/kubectl case platform do - :linux_amd64 -> {:ok, %{"kubectl_os" => "linux", "kubectl_arch" => "amd64"}} - :linux_arm64 -> {:ok, %{"kubectl_os" => "linux", "kubectl_arch" => "arm64"}} - :darwin_amd64 -> {:ok, %{"kubectl_os" => "darwin", "kubectl_arch" => "amd64"}} - :darwin_arm64 -> {:ok, %{"kubectl_os" => "darwin", "kubectl_arch" => "arm64"}} + :linux_amd64 -> {:ok, %{"kubectl_os" => "linux", "kubectl_arch" => "amd64"}} + :linux_arm64 -> {:ok, %{"kubectl_os" => "linux", "kubectl_arch" => "arm64"}} + :darwin_amd64 -> {:ok, %{"kubectl_os" => "darwin", "kubectl_arch" => "amd64"}} + :darwin_arm64 -> {:ok, %{"kubectl_os" => "darwin", "kubectl_arch" => "arm64"}} :windows_amd64 -> {:ok, %{"kubectl_os" => "windows", "kubectl_arch" => "amd64"}} _ -> {:error, :unsupported_platform} end diff --git a/opsm_ex/lib/opsm/safe_exec.ex b/opsm_ex/lib/opsm/safe_exec.ex index a686eceb..73d2690e 100644 --- a/opsm_ex/lib/opsm/safe_exec.ex +++ b/opsm_ex/lib/opsm/safe_exec.ex @@ -78,6 +78,7 @@ defmodule Opsm.SafeExec do defp validate_command(command, allowlist) do name = Path.basename(command) + cond do String.trim(command) == "" -> {:error, "command is empty"} diff --git a/opsm_ex/lib/opsm/security/osv.ex b/opsm_ex/lib/opsm/security/osv.ex index 0302e5bb..eb5a70eb 100644 --- a/opsm_ex/lib/opsm/security/osv.ex +++ b/opsm_ex/lib/opsm/security/osv.ex @@ -16,17 +16,17 @@ defmodule Opsm.Security.Osv do # Maps OPSM forth atoms to OSV ecosystem identifiers. @forth_to_ecosystem %{ - npm: "npm", - cargo: "crates.io", - hex: "Hex", - pypi: "PyPI", - gem: "RubyGems", - go: "Go", - pub: "Pub", + npm: "npm", + cargo: "crates.io", + hex: "Hex", + pypi: "PyPI", + gem: "RubyGems", + go: "Go", + pub: "Pub", hackage: "Hackage", - maven: "Maven", - nuget: "NuGet", - nimble: "Nim" + maven: "Maven", + nuget: "NuGet", + nimble: "Nim" } defmodule Vulnerability do @@ -35,14 +35,14 @@ defmodule Opsm.Security.Osv do @type severity :: :critical | :high | :medium | :low | :none | :unknown @type t :: %__MODULE__{ - id: String.t(), - summary: String.t() | nil, - severity: severity(), - aliases: [String.t()], - fixed_in: String.t() | nil, - url: String.t(), - published: String.t() | nil - } + id: String.t(), + summary: String.t() | nil, + severity: severity(), + aliases: [String.t()], + fixed_in: String.t() | nil, + url: String.t(), + published: String.t() | nil + } end @doc """ @@ -57,7 +57,7 @@ defmodule Opsm.Security.Osv do when is_binary(package_name) and is_binary(version) and is_atom(forth) do body = %{ "package" => %{ - "name" => package_name, + "name" => package_name, "ecosystem" => ecosystem_for(forth) }, "version" => version @@ -76,7 +76,7 @@ defmodule Opsm.Security.Osv do when is_binary(package_name) and is_atom(forth) do body = %{ "package" => %{ - "name" => package_name, + "name" => package_name, "ecosystem" => ecosystem_for(forth) } } @@ -113,12 +113,12 @@ defmodule Opsm.Security.Osv do defp decode_vuln(json) do %Vulnerability{ - id: json["id"] || "unknown", - summary: json["summary"], - severity: extract_severity(json), - aliases: json["aliases"] || [], - fixed_in: extract_fixed_in(json), - url: "https://osv.dev/vulnerability/#{json["id"] || "unknown"}", + id: json["id"] || "unknown", + summary: json["summary"], + severity: extract_severity(json), + aliases: json["aliases"] || [], + fixed_in: extract_fixed_in(json), + url: "https://osv.dev/vulnerability/#{json["id"] || "unknown"}", published: json["published"] } end @@ -135,7 +135,7 @@ defmodule Opsm.Security.Osv do defp extract_severity(%{"affected" => [%{"ecosystem_specific" => %{"severity" => s}} | _]}) when is_binary(s), - do: label_to_severity(s) + do: label_to_severity(s) defp extract_severity(_), do: :unknown @@ -146,8 +146,8 @@ defmodule Opsm.Security.Osv do score >= 9.0 -> :critical score >= 7.0 -> :high score >= 4.0 -> :medium - score > 0.0 -> :low - true -> :none + score > 0.0 -> :low + true -> :none end end @@ -156,21 +156,21 @@ defmodule Opsm.Security.Osv do # The numeric score lives in database_specific, which we handle first. cond do String.contains?(score, "CRITICAL") -> :critical - String.contains?(score, "HIGH") -> :high - String.contains?(score, "MEDIUM") -> :medium - String.contains?(score, "LOW") -> :low - true -> :unknown + String.contains?(score, "HIGH") -> :high + String.contains?(score, "MEDIUM") -> :medium + String.contains?(score, "LOW") -> :low + true -> :unknown end end defp cvss_score_to_severity(_), do: :unknown defp label_to_severity("CRITICAL"), do: :critical - defp label_to_severity("HIGH"), do: :high - defp label_to_severity("MEDIUM"), do: :medium - defp label_to_severity("LOW"), do: :low - defp label_to_severity("NONE"), do: :none - defp label_to_severity(_), do: :unknown + defp label_to_severity("HIGH"), do: :high + defp label_to_severity("MEDIUM"), do: :medium + defp label_to_severity("LOW"), do: :low + defp label_to_severity("NONE"), do: :none + defp label_to_severity(_), do: :unknown # Extract the first "fixed" version from affected ranges. defp extract_fixed_in(%{"affected" => [%{"ranges" => ranges} | _]}) do @@ -178,7 +178,7 @@ defmodule Opsm.Security.Osv do |> Enum.flat_map(fn r -> r["events"] || [] end) |> Enum.find_value(fn %{"fixed" => ver} -> ver - _ -> nil + _ -> nil end) end diff --git a/opsm_ex/lib/opsm/security/scanner.ex b/opsm_ex/lib/opsm/security/scanner.ex index 3a842271..8faf607c 100644 --- a/opsm_ex/lib/opsm/security/scanner.ex +++ b/opsm_ex/lib/opsm/security/scanner.ex @@ -17,13 +17,13 @@ defmodule Opsm.Security.Scanner do defstruct [:package, :version, :forth, :vulnerabilities, :typosquat, :scanned_at] @type t :: %__MODULE__{ - package: String.t(), - version: String.t() | nil, - forth: atom(), - vulnerabilities: [Osv.Vulnerability.t()], - typosquat: {:clean} | {:suspicious, [Typosquat.Match.t()]}, - scanned_at: DateTime.t() - } + package: String.t(), + version: String.t() | nil, + forth: atom(), + vulnerabilities: [Osv.Vulnerability.t()], + typosquat: {:clean} | {:suspicious, [Typosquat.Match.t()]}, + scanned_at: DateTime.t() + } def critical_count(%__MODULE__{vulnerabilities: vulns}), do: Enum.count(vulns, &(&1.severity == :critical)) @@ -53,7 +53,9 @@ defmodule Opsm.Security.Scanner do vulns = case osv_query(package, version, forth) do - {:ok, list} -> list + {:ok, list} -> + list + {:error, reason} -> require Logger Logger.warning("OSV lookup failed for #{package}: #{inspect(reason)}") @@ -63,12 +65,12 @@ defmodule Opsm.Security.Scanner do typosquat = Typosquat.check(package, forth) report = %Report{ - package: package, - version: version, - forth: forth, + package: package, + version: version, + forth: forth, vulnerabilities: vulns, - typosquat: typosquat, - scanned_at: DateTime.utc_now() + typosquat: typosquat, + scanned_at: DateTime.utc_now() } {:ok, report} @@ -92,7 +94,11 @@ defmodule Opsm.Security.Scanner do @spec print_report(Report.t()) :: :ok def print_report(%Report{} = r) do IO.puts("") - IO.puts("Security scan: #{r.package}#{if r.version, do: "@#{r.version}", else: ""} (@#{r.forth})") + + IO.puts( + "Security scan: #{r.package}#{if r.version, do: "@#{r.version}", else: ""} (@#{r.forth})" + ) + IO.puts(String.duplicate("─", 60)) case r.typosquat do @@ -101,12 +107,15 @@ defmodule Opsm.Security.Scanner do {:suspicious, matches} -> IO.puts(" Typosquat: ⚠ SUSPICIOUS — name resembles known package(s):") + for m <- matches do - tag = case m.similarity do - :edit_distance_1 -> "1 edit away from" - :edit_distance_2 -> "2 edits away from" - :homoglyph -> "homoglyph of" - end + tag = + case m.similarity do + :edit_distance_1 -> "1 edit away from" + :edit_distance_2 -> "2 edits away from" + :homoglyph -> "homoglyph of" + end + IO.puts(" #{tag} '#{m.package}'") end end @@ -115,7 +124,9 @@ defmodule Opsm.Security.Scanner do IO.puts(" OSV vulns: ✓ none found") else sev_order = [:critical, :high, :medium, :low, :none, :unknown] - sorted = Enum.sort_by(r.vulnerabilities, &Enum.find_index(sev_order, fn s -> s == &1.severity end)) + + sorted = + Enum.sort_by(r.vulnerabilities, &Enum.find_index(sev_order, fn s -> s == &1.severity end)) IO.puts(" OSV vulns: #{length(r.vulnerabilities)} advisory/ies") IO.puts("") @@ -146,9 +157,9 @@ defmodule Opsm.Security.Scanner do do: Osv.query(package, version, forth) defp severity_label(:critical), do: "[CRIT]" - defp severity_label(:high), do: "[HIGH]" - defp severity_label(:medium), do: "[MED] " - defp severity_label(:low), do: "[LOW] " - defp severity_label(:none), do: "[NONE]" - defp severity_label(_), do: "[?] " + defp severity_label(:high), do: "[HIGH]" + defp severity_label(:medium), do: "[MED] " + defp severity_label(:low), do: "[LOW] " + defp severity_label(:none), do: "[NONE]" + defp severity_label(_), do: "[?] " end diff --git a/opsm_ex/lib/opsm/security/typosquat.ex b/opsm_ex/lib/opsm/security/typosquat.ex index 50b04ff7..d478d742 100644 --- a/opsm_ex/lib/opsm/security/typosquat.ex +++ b/opsm_ex/lib/opsm/security/typosquat.ex @@ -64,12 +64,12 @@ defmodule Opsm.Security.Typosquat do ] @popular %{ - npm: @popular_npm, - cargo: @popular_cargo, - pypi: @popular_pypi, - hex: @popular_hex, - gem: @popular_gem, - go: @popular_go + npm: @popular_npm, + cargo: @popular_cargo, + pypi: @popular_pypi, + hex: @popular_hex, + gem: @popular_gem, + go: @popular_go } defmodule Match do @@ -78,10 +78,10 @@ defmodule Opsm.Security.Typosquat do @type similarity :: :edit_distance_1 | :edit_distance_2 | :homoglyph @type t :: %__MODULE__{ - package: String.t(), - similarity: similarity(), - flags: [atom()] - } + package: String.t(), + similarity: similarity(), + flags: [atom()] + } end @doc """ @@ -100,7 +100,7 @@ defmodule Opsm.Security.Typosquat do |> Enum.flat_map(&suspect_match(name_lower, &1)) case matches do - [] -> {:clean} + [] -> {:clean} hits -> {:suspicious, hits} end end diff --git a/opsm_ex/lib/opsm/slsa.ex b/opsm_ex/lib/opsm/slsa.ex index 5fe66613..210cb3b6 100644 --- a/opsm_ex/lib/opsm/slsa.ex +++ b/opsm_ex/lib/opsm/slsa.ex @@ -89,11 +89,12 @@ defmodule Opsm.Slsa do def sign_provenance(statement, keypair) do case HybridSignatures.sign_payload(statement, keypair) do {:ok, sig_info} -> - {:ok, %{ - statement: statement, - signature: HybridSignatures.encode_signature(sig_info), - signed_at: DateTime.utc_now() |> DateTime.to_iso8601() - }} + {:ok, + %{ + statement: statement, + signature: HybridSignatures.encode_signature(sig_info), + signed_at: DateTime.utc_now() |> DateTime.to_iso8601() + }} {:error, reason} -> {:error, "Provenance signing failed: #{reason}"} @@ -128,14 +129,17 @@ defmodule Opsm.Slsa do defp verify_signature(bundle, public_keys) do sig_hex = bundle.signature["signature"] - algo = case bundle.signature["algorithm"] do - "hybrid_ed25519_dilithium5" -> :hybrid_ed25519_dilithium5 - _ -> :ed25519_only - end + + algo = + case bundle.signature["algorithm"] do + "hybrid_ed25519_dilithium5" -> :hybrid_ed25519_dilithium5 + _ -> :ed25519_only + end case Base.decode16(sig_hex, case: :mixed) do {:ok, sig_bytes} -> sig_info = %{signature: sig_bytes, algorithm: algo} + case HybridSignatures.verify_payload(bundle.statement, sig_info, public_keys) do :ok -> :ok {:ok, _mode} -> :ok @@ -187,7 +191,7 @@ defmodule Opsm.Slsa do # L3: Hardened build with isolated builder and no external parameters beyond package spec builder["id"] != nil and map_size(build_def["internalParameters"] || %{}) > 0 and - build_def["buildType"] == @slsa_predicate_type -> + build_def["buildType"] == @slsa_predicate_type -> 3 # L2: Hosted build platform with builder ID @@ -219,11 +223,16 @@ defmodule Opsm.Slsa do case action do :block -> - {:error, "Package #{package_info[:name]} has SLSA level #{actual_level}, requires #{required_level}"} + {:error, + "Package #{package_info[:name]} has SLSA level #{actual_level}, requires #{required_level}"} :warn -> require Logger - Logger.warning("Package #{package_info[:name]} has SLSA level #{actual_level}, expected #{required_level}") + + Logger.warning( + "Package #{package_info[:name]} has SLSA level #{actual_level}, expected #{required_level}" + ) + :ok :ignore -> @@ -239,6 +248,7 @@ defmodule Opsm.Slsa do """ def lockfile_metadata(provenance_bundle) when is_map(provenance_bundle) do level = determine_level(provenance_bundle[:statement] || provenance_bundle.statement) + %{ slsa_level: level, slsa_provenance_uri: provenance_bundle[:uri] || nil @@ -252,12 +262,16 @@ defmodule Opsm.Slsa do # ========================================================================== defp compute_digest(package_info) do - data = "#{package_info[:name] || package_info.name}@#{package_info[:version] || package_info.version}" + data = + "#{package_info[:name] || package_info.name}@#{package_info[:version] || package_info.version}" + Hash.hash_hot(data) end defp compute_digest_sha3(package_info) do - data = "#{package_info[:name] || package_info.name}@#{package_info[:version] || package_info.version}" + data = + "#{package_info[:name] || package_info.name}@#{package_info[:version] || package_info.version}" + Hash.hash_cold(data) end diff --git a/opsm_ex/lib/opsm/slsa/github_attestation.ex b/opsm_ex/lib/opsm/slsa/github_attestation.ex index cee77219..b2aeb358 100644 --- a/opsm_ex/lib/opsm/slsa/github_attestation.ex +++ b/opsm_ex/lib/opsm/slsa/github_attestation.ex @@ -145,7 +145,13 @@ defmodule Opsm.Slsa.GithubAttestation do {:ok, bundles} -> subject = verification_subject(package, opts) - verify_subject(subject, owner_repo, config, Keyword.put(opts, :bundle_count, length(bundles))) + + verify_subject( + subject, + owner_repo, + config, + Keyword.put(opts, :bundle_count, length(bundles)) + ) {:error, reason} -> {:error, reason} @@ -300,7 +306,10 @@ defmodule Opsm.Slsa.GithubAttestation do end def decode_gh_output(_), - do: %GithubAttestationVerification{verified: false, message: "gh returned no verification results"} + do: %GithubAttestationVerification{ + verified: false, + message: "gh returned no verification results" + } # ========================================================================== # Package helpers @@ -310,7 +319,10 @@ defmodule Opsm.Slsa.GithubAttestation do Extract `"owner/repo"` from a package's manifest repository URL. """ def github_owner_repo(%{manifest: %{repository: repo}}) when is_binary(repo) do - case Regex.run(~r{\Ahttps?://github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+?)(?:\.git)?/?\z}, repo) do + case Regex.run( + ~r{\Ahttps?://github\.com/([A-Za-z0-9_.-]+)/([A-Za-z0-9_.-]+?)(?:\.git)?/?\z}, + repo + ) do [_, owner, name] -> {:ok, "#{owner}/#{name}"} _ -> {:none, "Package repository is not a GitHub URL"} end @@ -330,7 +342,8 @@ defmodule Opsm.Slsa.GithubAttestation do end end - def artifact_digest(_package), do: {:none, "Package has no sha256 checksum for attestation lookup"} + def artifact_digest(_package), + do: {:none, "Package has no sha256 checksum for attestation lookup"} defp verification_subject(package, opts) do cond do diff --git a/opsm_ex/lib/opsm/slsa/provenance.ex b/opsm_ex/lib/opsm/slsa/provenance.ex index 53c24f96..7e4b6f99 100644 --- a/opsm_ex/lib/opsm/slsa/provenance.ex +++ b/opsm_ex/lib/opsm/slsa/provenance.ex @@ -84,28 +84,31 @@ defmodule Opsm.Slsa.Provenance do # Sign if key provided (supports both classical and hybrid) hybrid_keypair = Keyword.get(opts, :hybrid_keypair) - provenance = cond do - hybrid_keypair -> - # Hybrid Ed25519 + Dilithium5 signing - case sign_provenance_hybrid(provenance, hybrid_keypair) do - {:ok, sig, algo} -> - %{provenance | signature: sig, signature_algo: algo} - {:error, _} -> - provenance - end - - signing_key -> - # Classical Ed25519 signing - case sign_provenance(provenance, signing_key) do - {:ok, sig} -> - %{provenance | signature: sig, signature_algo: :ed25519} - {:error, _} -> - provenance - end - - true -> - provenance - end + provenance = + cond do + hybrid_keypair -> + # Hybrid Ed25519 + Dilithium5 signing + case sign_provenance_hybrid(provenance, hybrid_keypair) do + {:ok, sig, algo} -> + %{provenance | signature: sig, signature_algo: algo} + + {:error, _} -> + provenance + end + + signing_key -> + # Classical Ed25519 signing + case sign_provenance(provenance, signing_key) do + {:ok, sig} -> + %{provenance | signature: sig, signature_algo: :ed25519} + + {:error, _} -> + provenance + end + + true -> + provenance + end {:ok, provenance} end @@ -137,28 +140,32 @@ defmodule Opsm.Slsa.Provenance do {materials_match, result} = check_materials(provenance, package, result, opts) # Check signature - {sig_valid, result} = if provenance.signature && public_key do - check_signature(provenance, public_key, result) - else - if provenance.signature do - {false, add_warning(result, "Provenance is signed but no public key provided for verification")} + {sig_valid, result} = + if provenance.signature && public_key do + check_signature(provenance, public_key, result) else - {:not_checked, add_warning(result, "Provenance is unsigned")} + if provenance.signature do + {false, + add_warning(result, "Provenance is signed but no public key provided for verification")} + else + {:not_checked, add_warning(result, "Provenance is unsigned")} + end end - end # Calculate verified level verified_level = verified_slsa_level(builder_trusted, materials_match, sig_valid) verified = verified_level >= 1 and result.errors == [] - {:ok, %{result | - verified: verified, - slsa_level: verified_level, - builder_trusted: builder_trusted, - materials_match: materials_match, - signature_valid: sig_valid - }} + {:ok, + %{ + result + | verified: verified, + slsa_level: verified_level, + builder_trusted: builder_trusted, + materials_match: materials_match, + signature_valid: sig_valid + }} end @doc """ @@ -268,29 +275,40 @@ defmodule Opsm.Slsa.Provenance do materials = [] # Source material - materials = if source_uri != "" do - [%BuildMaterial{ - uri: source_uri, - digest: digest_map(source_digest) - } | materials] - else - materials - end + materials = + if source_uri != "" do + [ + %BuildMaterial{ + uri: source_uri, + digest: digest_map(source_digest) + } + | materials + ] + else + materials + end # Package tarball material - materials = if package.tarball_url do - [%BuildMaterial{ - uri: package.tarball_url, - digest: if(package.checksum, - do: %{to_string(package.checksum_algo || "sha256") => package.checksum}, - else: %{}) - } | materials] - else - materials - end + materials = + if package.tarball_url do + [ + %BuildMaterial{ + uri: package.tarball_url, + digest: + if(package.checksum, + do: %{to_string(package.checksum_algo || "sha256") => package.checksum}, + else: %{} + ) + } + | materials + ] + else + materials + end # Dependency materials - dep_materials = (package.manifest.dependencies || %{}) + dep_materials = + (package.manifest.dependencies || %{}) |> Enum.map(fn {name, version} -> %BuildMaterial{uri: "pkg:#{package.forth}/#{name}@#{version}", digest: %{}} end) @@ -362,12 +380,13 @@ defmodule Opsm.Slsa.Provenance do nil -> # Check if package tarball appears in materials - has_tarball = Enum.any?(provenance.materials, fn - %BuildMaterial{uri: uri} -> uri == package.tarball_url - %{uri: uri} -> uri == package.tarball_url - %{"uri" => uri} -> uri == package.tarball_url - _ -> false - end) + has_tarball = + Enum.any?(provenance.materials, fn + %BuildMaterial{uri: uri} -> uri == package.tarball_url + %{uri: uri} -> uri == package.tarball_url + %{"uri" => uri} -> uri == package.tarball_url + _ -> false + end) if has_tarball or is_nil(package.tarball_url) do {true, result} @@ -385,18 +404,28 @@ defmodule Opsm.Slsa.Provenance do :hybrid_ed25519_dilithium5 -> # Hybrid verification — public_key should be a map with :ed25519_pk and :dilithium5_pk sig_info = %{signature: provenance.signature, algorithm: algo} + case HybridSignatures.verify_payload(envelope, sig_info, public_key) do - :ok -> {true, result} - {:ok, :classical_only} -> {true, add_warning(result, "Only classical Ed25519 verified (PQ NIF not loaded)")} - {:ok, :pq_not_verified} -> {true, add_warning(result, "PQ signature not verified (NIF not loaded)")} - {:error, reason} -> {false, add_error(result, "Hybrid signature invalid: #{reason}")} + :ok -> + {true, result} + + {:ok, :classical_only} -> + {true, add_warning(result, "Only classical Ed25519 verified (PQ NIF not loaded)")} + + {:ok, :pq_not_verified} -> + {true, add_warning(result, "PQ signature not verified (NIF not loaded)")} + + {:error, reason} -> + {false, add_error(result, "Hybrid signature invalid: #{reason}")} end :ed25519_only -> # Classical Ed25519 from hybrid keypair - pk = if is_map(public_key) and Map.has_key?(public_key, :ed25519_pk), - do: public_key.ed25519_pk, - else: public_key + pk = + if is_map(public_key) and Map.has_key?(public_key, :ed25519_pk), + do: public_key.ed25519_pk, + else: public_key + case Signatures.verify_payload(envelope, provenance.signature, pk, :ed25519) do :ok -> {true, result} {:error, reason} -> {false, add_error(result, "Signature invalid: #{reason}")} @@ -418,6 +447,7 @@ defmodule Opsm.Slsa.Provenance do defp sign_provenance_hybrid(provenance, hybrid_keypair) do envelope = to_envelope(provenance) + case HybridSignatures.sign_payload(envelope, hybrid_keypair) do {:ok, %{signature: sig, algorithm: algo}} -> {:ok, sig, algo} {:error, reason} -> {:error, reason} diff --git a/opsm_ex/lib/opsm/smart_install.ex b/opsm_ex/lib/opsm/smart_install.ex index 3e661b5f..49f678b5 100644 --- a/opsm_ex/lib/opsm/smart_install.ex +++ b/opsm_ex/lib/opsm/smart_install.ex @@ -240,6 +240,7 @@ defmodule Opsm.SmartInstall do defp execute_non_port_backend("container", pkgs, dry_run, _scope, _native, _global, _dev) do image = System.get_env("OPSM_CONTAINER_IMAGE") base_cmd = System.get_env("OPSM_CONTAINER_CMD", "dnf install -y") + runtime = cond do System.find_executable("podman") -> "podman" @@ -252,25 +253,25 @@ defmodule Opsm.SmartInstall do {:error, pkg, "podman or docker is required to use container backend"} end) else - if is_nil(image) or image == "" do - Enum.map(pkgs, fn pkg -> - {:error, pkg, "OPSM_CONTAINER_IMAGE is required to use container backend"} - end) - else - Enum.map(pkgs, fn pkg -> - cmd = runtime - args = ["run", "--rm", image, "sh", "-lc", "#{base_cmd} #{pkg}"] + if is_nil(image) or image == "" do + Enum.map(pkgs, fn pkg -> + {:error, pkg, "OPSM_CONTAINER_IMAGE is required to use container backend"} + end) + else + Enum.map(pkgs, fn pkg -> + cmd = runtime + args = ["run", "--rm", image, "sh", "-lc", "#{base_cmd} #{pkg}"] - if dry_run do - {:dry_run, pkg, "#{cmd} #{Enum.join(args, " ")}"} - else - case Opsm.SafeExec.cmd(cmd, args, stderr_to_stdout: true) do - {output, 0} -> {:ok, pkg, output} - {error, code} -> {:error, pkg, "#{cmd} failed (#{code}): #{error}"} + if dry_run do + {:dry_run, pkg, "#{cmd} #{Enum.join(args, " ")}"} + else + case Opsm.SafeExec.cmd(cmd, args, stderr_to_stdout: true) do + {output, 0} -> {:ok, pkg, output} + {error, code} -> {:error, pkg, "#{cmd} failed (#{code}): #{error}"} + end end - end - end) - end + end) + end end end @@ -317,7 +318,18 @@ defmodule Opsm.SmartInstall do end defp pick_auto_port do - candidates = [:rpm_ostree, :rpm, :deb, :pacman, :homebrew, :nix, :guix, :winget, :choco, :scoop] + candidates = [ + :rpm_ostree, + :rpm, + :deb, + :pacman, + :homebrew, + :nix, + :guix, + :winget, + :choco, + :scoop + ] Enum.reduce_while(candidates, {:error, "no suitable backend available"}, fn port, _acc -> case Federation.check_connection_port(port) do diff --git a/opsm_ex/lib/opsm/storage/ipfs.ex b/opsm_ex/lib/opsm/storage/ipfs.ex index 75573216..430fa39d 100644 --- a/opsm_ex/lib/opsm/storage/ipfs.ex +++ b/opsm_ex/lib/opsm/storage/ipfs.ex @@ -73,8 +73,8 @@ defmodule Opsm.Storage.Ipfs do case Req.post(url, into: File.stream!(dest_path), receive_timeout: @timeout_ms) do {:ok, %{status: 200}} -> {:ok, dest_path} - {:ok, %{status: s}} -> {:error, "IPFS cat returned #{s}"} - {:error, reason} -> {:error, "IPFS cat failed: #{inspect(reason)}"} + {:ok, %{status: s}} -> {:error, "IPFS cat returned #{s}"} + {:error, reason} -> {:error, "IPFS cat failed: #{inspect(reason)}"} end end end @@ -82,13 +82,16 @@ defmodule Opsm.Storage.Ipfs do @impl true def exists?(key, _opts) do case load_cid(key) do - nil -> false + nil -> + false + cid -> api = api_url() url = "#{api}/api/v0/pin/ls?arg=#{cid}&type=recursive" + case Req.post(url, receive_timeout: 5_000) do {:ok, %{status: 200}} -> true - _ -> false + _ -> false end end end @@ -96,7 +99,9 @@ defmodule Opsm.Storage.Ipfs do @impl true def url(key, _opts) do case load_cid(key) do - nil -> nil + nil -> + nil + cid -> gateway = System.get_env("OPSM_IPFS_GATEWAY", @default_gateway) "#{gateway}/ipfs/#{cid}" @@ -110,12 +115,14 @@ defmodule Opsm.Storage.Ipfs do defp api_url, do: System.get_env("OPSM_IPFS_API", @default_api) defp extract_cid(resp) when is_map(resp), do: resp["Hash"] || resp["Cid"] || resp["cid"] + defp extract_cid(resp) when is_binary(resp) do case Jason.decode(resp) do {:ok, map} -> extract_cid(map) - _ -> nil + _ -> nil end end + defp extract_cid(_), do: nil # Build a minimal multipart/form-data body for /api/v0/add. @@ -153,10 +160,11 @@ defmodule Opsm.Storage.Ipfs do {:ok, content} -> case Jason.decode(content) do {:ok, map} when is_map(map) -> map - _ -> %{} + _ -> %{} end - {:error, _} -> %{} + {:error, _} -> + %{} end end end diff --git a/opsm_ex/lib/opsm/storage/local.ex b/opsm_ex/lib/opsm/storage/local.ex index 51dfc4f8..52610fb6 100644 --- a/opsm_ex/lib/opsm/storage/local.ex +++ b/opsm_ex/lib/opsm/storage/local.ex @@ -17,7 +17,7 @@ defmodule Opsm.Storage.Local do dest |> Path.dirname() |> File.mkdir_p!() case File.cp(local_path, dest) do - :ok -> {:ok, key} + :ok -> {:ok, key} {:error, reason} -> {:error, "Local put failed: #{reason}"} end end @@ -30,7 +30,7 @@ defmodule Opsm.Storage.Local do dest_path |> Path.dirname() |> File.mkdir_p!() case File.cp(src, dest_path) do - :ok -> {:ok, dest_path} + :ok -> {:ok, dest_path} {:error, reason} -> {:error, "Local copy failed: #{reason}"} end else diff --git a/opsm_ex/lib/opsm/storage/manager.ex b/opsm_ex/lib/opsm/storage/manager.ex index 8ff93b58..4f7cf73b 100644 --- a/opsm_ex/lib/opsm/storage/manager.ex +++ b/opsm_ex/lib/opsm/storage/manager.ex @@ -61,14 +61,14 @@ defmodule Opsm.Storage.Manager do # Push to remote backends silently. if s3_configured?() do case S3.put(key, local_path, []) do - {:ok, _} -> Logger.debug("Stored #{key} in S3") + {:ok, _} -> Logger.debug("Stored #{key} in S3") {:error, reason} -> Logger.warning("S3 store failed for #{key}: #{inspect(reason)}") end end if ipfs_active?() do case Ipfs.put(key, local_path, []) do - {:ok, _} -> Logger.debug("Stored #{key} in IPFS") + {:ok, _} -> Logger.debug("Stored #{key} in IPFS") {:error, reason} -> Logger.warning("IPFS store failed for #{key}: #{inspect(reason)}") end end @@ -82,9 +82,9 @@ defmodule Opsm.Storage.Manager do @spec public_url(String.t()) :: String.t() | nil def public_url(key) do cond do - s3_configured?() -> S3.url(key, []) - ipfs_active?() -> Ipfs.url(key, []) - true -> nil + s3_configured?() -> S3.url(key, []) + ipfs_active?() -> Ipfs.url(key, []) + true -> nil end end @@ -95,8 +95,8 @@ defmodule Opsm.Storage.Manager do def status do %{ local: %{active: true, path: Local.cache_root()}, - s3: %{active: s3_configured?(), bucket: System.get_env("OPSM_S3_BUCKET")}, - ipfs: %{active: ipfs_active?(), api: System.get_env("OPSM_IPFS_API", "not set")} + s3: %{active: s3_configured?(), bucket: System.get_env("OPSM_S3_BUCKET")}, + ipfs: %{active: ipfs_active?(), api: System.get_env("OPSM_IPFS_API", "not set")} } end @@ -108,23 +108,31 @@ defmodule Opsm.Storage.Manager do cond do s3_configured?() -> case S3.get(key, dest_path, []) do - {:ok, _} = ok -> ok - {:error, :not_found} -> try_ipfs_fetch(key, dest_path) + {:ok, _} = ok -> + ok + + {:error, :not_found} -> + try_ipfs_fetch(key, dest_path) + {:error, reason} -> Logger.debug("S3 fetch miss for #{key}: #{inspect(reason)}") try_ipfs_fetch(key, dest_path) end - ipfs_active?() -> try_ipfs_fetch(key, dest_path) + ipfs_active?() -> + try_ipfs_fetch(key, dest_path) - true -> {:error, :not_found} + true -> + {:error, :not_found} end end defp try_ipfs_fetch(key, dest_path) do if ipfs_active?() do case Ipfs.get(key, dest_path, []) do - {:ok, _} = ok -> ok + {:ok, _} = ok -> + ok + {:error, reason} -> Logger.debug("IPFS fetch miss for #{key}: #{inspect(reason)}") {:error, :not_found} diff --git a/opsm_ex/lib/opsm/storage/s3.ex b/opsm_ex/lib/opsm/storage/s3.ex index 4a99788b..a0fef7d3 100644 --- a/opsm_ex/lib/opsm/storage/s3.ex +++ b/opsm_ex/lib/opsm/storage/s3.ex @@ -37,10 +37,10 @@ defmodule Opsm.Storage.S3 do date = String.slice(now, 0, 8) headers = [ - {"host", URI.parse(url).host}, + {"host", URI.parse(url).host}, {"x-amz-content-sha256", content_sha}, - {"x-amz-date", now}, - {"content-type", "application/octet-stream"} + {"x-amz-date", now}, + {"content-type", "application/octet-stream"} ] auth = sigv4_auth("PUT", URI.parse(url).path, "", headers, content_sha, now, date, cfg) @@ -52,12 +52,12 @@ defmodule Opsm.Storage.S3 do receive_timeout: @timeout_ms ) do {:ok, %{status: s}} when s in 200..299 -> {:ok, key} - {:ok, %{status: s, body: b}} -> {:error, "S3 PUT returned #{s}: #{inspect(b)}"} - {:error, reason} -> {:error, "S3 PUT failed: #{inspect(reason)}"} + {:ok, %{status: s, body: b}} -> {:error, "S3 PUT returned #{s}: #{inspect(b)}"} + {:error, reason} -> {:error, "S3 PUT failed: #{inspect(reason)}"} end else {:error, :not_configured} -> {:error, :not_configured} - {:error, reason} -> {:error, "S3 PUT: #{inspect(reason)}"} + {:error, reason} -> {:error, "S3 PUT: #{inspect(reason)}"} end end @@ -70,9 +70,9 @@ defmodule Opsm.Storage.S3 do empty_hash = sha256_hex("") headers = [ - {"host", URI.parse(url).host}, + {"host", URI.parse(url).host}, {"x-amz-content-sha256", empty_hash}, - {"x-amz-date", now} + {"x-amz-date", now} ] auth = sigv4_auth("GET", URI.parse(url).path, "", headers, empty_hash, now, date, cfg) @@ -85,10 +85,10 @@ defmodule Opsm.Storage.S3 do into: File.stream!(dest_path), receive_timeout: @timeout_ms ) do - {:ok, %{status: 200}} -> {:ok, dest_path} - {:ok, %{status: 404}} -> {:error, :not_found} - {:ok, %{status: s}} -> {:error, "S3 GET returned #{s}"} - {:error, reason} -> {:error, "S3 GET failed: #{inspect(reason)}"} + {:ok, %{status: 200}} -> {:ok, dest_path} + {:ok, %{status: 404}} -> {:error, :not_found} + {:ok, %{status: s}} -> {:error, "S3 GET returned #{s}"} + {:error, reason} -> {:error, "S3 GET failed: #{inspect(reason)}"} end else {:error, :not_configured} -> {:error, :not_configured} @@ -98,7 +98,9 @@ defmodule Opsm.Storage.S3 do @impl true def exists?(key, _opts) do case config() do - {:error, :not_configured} -> false + {:error, :not_configured} -> + false + {:ok, cfg} -> url = object_url(cfg, key) now = utc_now_iso() @@ -106,9 +108,9 @@ defmodule Opsm.Storage.S3 do empty_hash = sha256_hex("") headers = [ - {"host", URI.parse(url).host}, + {"host", URI.parse(url).host}, {"x-amz-content-sha256", empty_hash}, - {"x-amz-date", now} + {"x-amz-date", now} ] auth = sigv4_auth("HEAD", URI.parse(url).path, "", headers, empty_hash, now, date, cfg) @@ -116,7 +118,7 @@ defmodule Opsm.Storage.S3 do case Req.head(url, headers: req_headers, receive_timeout: 5_000) do {:ok, %{status: 200}} -> true - _ -> false + _ -> false end end end @@ -125,7 +127,7 @@ defmodule Opsm.Storage.S3 do def url(key, _opts) do case config() do {:ok, cfg} -> object_url(cfg, key) - _ -> nil + _ -> nil end end @@ -181,21 +183,23 @@ defmodule Opsm.Storage.S3 do # --------------------------------------------------------------------------- defp config do - bucket = System.get_env("OPSM_S3_BUCKET") - region = System.get_env("OPSM_S3_REGION", "us-east-1") + bucket = System.get_env("OPSM_S3_BUCKET") + region = System.get_env("OPSM_S3_REGION", "us-east-1") access_key = System.get_env("AWS_ACCESS_KEY_ID") secret_key = System.get_env("AWS_SECRET_ACCESS_KEY") if bucket && access_key && secret_key do - endpoint = System.get_env("OPSM_S3_ENDPOINT", "https://#{bucket}.s3.#{region}.amazonaws.com") - - {:ok, %{ - bucket: bucket, - region: region, - access_key: access_key, - secret_key: secret_key, - endpoint: endpoint - }} + endpoint = + System.get_env("OPSM_S3_ENDPOINT", "https://#{bucket}.s3.#{region}.amazonaws.com") + + {:ok, + %{ + bucket: bucket, + region: region, + access_key: access_key, + secret_key: secret_key, + endpoint: endpoint + }} else {:error, :not_configured} end @@ -207,6 +211,7 @@ defmodule Opsm.Storage.S3 do defp utc_now_iso do {{y, mo, d}, {h, mi, s}} = :calendar.universal_time() + :io_lib.format("~4..0B~2..0B~2..0BT~2..0B~2..0B~2..0BZ", [y, mo, d, h, mi, s]) |> IO.iodata_to_binary() end diff --git a/opsm_ex/lib/opsm/topo_sort.ex b/opsm_ex/lib/opsm/topo_sort.ex index 277ec525..acda63c1 100644 --- a/opsm_ex/lib/opsm/topo_sort.ex +++ b/opsm_ex/lib/opsm/topo_sort.ex @@ -38,15 +38,17 @@ defmodule Opsm.TopoSort do # Build edges: for each package, add edges from its dependencies to itself {edges, in_degree} = - Enum.reduce(resolved_packages, {%{}, in_degree}, fn {name, {_ver, pkg}}, {edges_acc, deg_acc} -> + Enum.reduce(resolved_packages, {%{}, in_degree}, fn {name, {_ver, pkg}}, + {edges_acc, deg_acc} -> dep_names = extract_dep_names(pkg) # Only count edges to packages that are in our resolved set resolved_deps = Enum.filter(dep_names, fn d -> Map.has_key?(resolved_packages, d) end) # Each resolved dep has an edge to this package - new_edges = Enum.reduce(resolved_deps, edges_acc, fn dep, e_acc -> - Map.update(e_acc, dep, [name], fn existing -> [name | existing] end) - end) + new_edges = + Enum.reduce(resolved_deps, edges_acc, fn dep, e_acc -> + Map.update(e_acc, dep, [name], fn existing -> [name | existing] end) + end) new_deg = Map.update!(deg_acc, name, fn d -> d + length(resolved_deps) end) @@ -59,7 +61,8 @@ defmodule Opsm.TopoSort do in_degree |> Enum.filter(fn {_name, deg} -> deg == 0 end) |> Enum.map(fn {name, _} -> name end) - |> Enum.sort() # Deterministic order for same-level packages + # Deterministic order for same-level packages + |> Enum.sort() kahn(queue, edges, in_degree, [], resolved_packages) end diff --git a/opsm_ex/lib/opsm/trust/pipeline.ex b/opsm_ex/lib/opsm/trust/pipeline.ex index 0bf83bcb..fdc54713 100644 --- a/opsm_ex/lib/opsm/trust/pipeline.ex +++ b/opsm_ex/lib/opsm/trust/pipeline.ex @@ -38,35 +38,43 @@ defmodule Opsm.Trust.Pipeline do # Run checks in parallel tasks = [] - tasks = if :attestation not in skip_checks do - [Task.async(fn -> {:attestation, check_attestations(package, config)} end) | tasks] - else - tasks - end + tasks = + if :attestation not in skip_checks do + [Task.async(fn -> {:attestation, check_attestations(package, config)} end) | tasks] + else + tasks + end - tasks = if :license not in skip_checks do - [Task.async(fn -> {:license, check_license(package, config)} end) | tasks] - else - tasks - end + tasks = + if :license not in skip_checks do + [Task.async(fn -> {:license, check_license(package, config)} end) | tasks] + else + tasks + end - tasks = if :sustainability not in skip_checks do - [Task.async(fn -> {:sustainability, check_sustainability(package, config)} end) | tasks] - else - tasks - end + tasks = + if :sustainability not in skip_checks do + [Task.async(fn -> {:sustainability, check_sustainability(package, config)} end) | tasks] + else + tasks + end - tasks = if :slsa not in skip_checks do - [Task.async(fn -> {:slsa, check_slsa_provenance(package, config)} end) | tasks] - else - tasks - end + tasks = + if :slsa not in skip_checks do + [Task.async(fn -> {:slsa, check_slsa_provenance(package, config)} end) | tasks] + else + tasks + end - tasks = if :github_attestation not in skip_checks do - [Task.async(fn -> {:github_attestation, check_github_attestation(package, config)} end) | tasks] - else - tasks - end + tasks = + if :github_attestation not in skip_checks do + [ + Task.async(fn -> {:github_attestation, check_github_attestation(package, config)} end) + | tasks + ] + else + tasks + end # Collect results safely (D2: use yield_many to handle task crashes/timeouts) # 3s timeout — trust checks are advisory, install should not stall on unreachable services @@ -91,11 +99,7 @@ defmodule Opsm.Trust.Pipeline do # Determine overall status {overall, recommendations, warnings} = evaluate_results(check_results) - results = %{results | - overall: overall, - recommendations: recommendations, - warnings: warnings - } + results = %{results | overall: overall, recommendations: recommendations, warnings: warnings} if verbose do print_verification_results(results) @@ -128,27 +132,33 @@ defmodule Opsm.Trust.Pipeline do # Step 1: Check licenses IO.puts("Step 1: Checking licenses...") - license_result = case Palimpsest.cli_check_licenses(path) do - {:ok, licenses} -> - IO.puts(" ✓ License check passed") - {:ok, licenses} - {:error, reason} -> - IO.puts(" ⚠ License check failed: #{reason}") - {:warning, reason} - end + + license_result = + case Palimpsest.cli_check_licenses(path) do + {:ok, licenses} -> + IO.puts(" ✓ License check passed") + {:ok, licenses} + + {:error, reason} -> + IO.puts(" ⚠ License check failed: #{reason}") + {:warning, reason} + end # Step 2: Code verification (if services available) IO.puts("") IO.puts("Step 2: Code verification...") checky_client = CheckyMonkey.new(config.checky_monkey, config.http) - verification_result = case CheckyMonkey.health(checky_client) do - {:ok, _} -> - IO.puts(" ✓ Checky-monkey available (would run verification)") - {:ok, :available} - {:error, _} -> - IO.puts(" ⊘ Checky-monkey not available (skipped)") - {:skipped, :service_unavailable} - end + + verification_result = + case CheckyMonkey.health(checky_client) do + {:ok, _} -> + IO.puts(" ✓ Checky-monkey available (would run verification)") + {:ok, :available} + + {:error, _} -> + IO.puts(" ⊘ Checky-monkey not available (skipped)") + {:skipped, :service_unavailable} + end %{ license: license_result, @@ -190,11 +200,12 @@ defmodule Opsm.Trust.Pipeline do defp check_slsa_provenance(package, _config) do try do # Check if package has SLSA provenance attestation - slsa_attestation = Enum.find(package.attestations || [], fn - %{attestation_type: :in_toto} -> true - %{attestation_type: :sigstore} -> true - _ -> false - end) + slsa_attestation = + Enum.find(package.attestations || [], fn + %{attestation_type: :in_toto} -> true + %{attestation_type: :sigstore} -> true + _ -> false + end) cond do is_nil(slsa_attestation) -> @@ -275,8 +286,8 @@ defmodule Opsm.Trust.Pipeline do # Clamp negative scores (defensive — heuristic should not produce them) {:ok, %{score: 0, level: :critical, source: :oikos_clamped}} - # No {:error, _} clause: analyze_package/4 is deliberately infallible — - # it falls back to heuristic scoring; exceptions hit the rescue below. + # No {:error, _} clause: analyze_package/4 is deliberately infallible — + # it falls back to heuristic scoring; exceptions hit the rescue below. end rescue e -> @@ -288,7 +299,9 @@ defmodule Opsm.Trust.Pipeline do # Extract repository URL from the package manifest when available. # This lets oikos perform full repository-level analysis rather than # falling back to heuristic scoring. - defp extract_repository_url(%{manifest: %{repository: repo}}) when is_binary(repo) and repo != "", do: repo + defp extract_repository_url(%{manifest: %{repository: repo}}) + when is_binary(repo) and repo != "", do: repo + defp extract_repository_url(_package), do: nil # Map a numeric sustainability score (0-100) to a human-readable level. @@ -311,6 +324,7 @@ defmodule Opsm.Trust.Pipeline do _ -> 0 end end + defp extract_slsa_level(_), do: 0 # Extract the numeric sustainability score from the check results map. @@ -329,21 +343,23 @@ defmodule Opsm.Trust.Pipeline do _warnings = [] # Check each result - {recs, warns} = Enum.reduce(checks, {[], []}, fn {name, result}, {r, w} -> - case result do - {:ok, _msg} -> {r, w} - {:info, msg} -> {[msg | r], w} - {:warning, msg} -> {r, [msg | w]} - {:error, msg} -> {r, ["#{name}: #{msg}" | w]} - {:skipped, _msg} -> {r, w} - end - end) + {recs, warns} = + Enum.reduce(checks, {[], []}, fn {name, result}, {r, w} -> + case result do + {:ok, _msg} -> {r, w} + {:info, msg} -> {[msg | r], w} + {:warning, msg} -> {r, [msg | w]} + {:error, msg} -> {r, ["#{name}: #{msg}" | w]} + {:skipped, _msg} -> {r, w} + end + end) - overall = cond do - Enum.any?(checks, fn {_, r} -> match?({:error, _}, r) end) -> :failed - Enum.any?(checks, fn {_, r} -> match?({:warning, _}, r) end) -> :warning - true -> :passed - end + overall = + cond do + Enum.any?(checks, fn {_, r} -> match?({:error, _}, r) end) -> :failed + Enum.any?(checks, fn {_, r} -> match?({:warning, _}, r) end) -> :warning + true -> :passed + end {overall, Enum.reverse(recs), Enum.reverse(warns)} end @@ -357,19 +373,22 @@ defmodule Opsm.Trust.Pipeline do IO.puts("") for {name, result} <- results.checks do - status = case result do - {:ok, msg} -> "✓ #{msg}" - {:info, msg} -> "ℹ #{msg}" - {:warning, msg} -> "⚠ #{msg}" - {:error, msg} -> "✗ #{msg}" - {:skipped, msg} -> "⊘ #{msg}" - end + status = + case result do + {:ok, msg} -> "✓ #{msg}" + {:info, msg} -> "ℹ #{msg}" + {:warning, msg} -> "⚠ #{msg}" + {:error, msg} -> "✗ #{msg}" + {:skipped, msg} -> "⊘ #{msg}" + end + IO.puts(" #{name}: #{status}") end if results.warnings != [] do IO.puts("") IO.puts("Warnings:") + for warn <- results.warnings do IO.puts(" - #{warn}") end @@ -378,6 +397,7 @@ defmodule Opsm.Trust.Pipeline do if results.recommendations != [] do IO.puts("") IO.puts("Recommendations:") + for rec <- results.recommendations do IO.puts(" - #{rec}") end @@ -396,6 +416,7 @@ defmodule Opsm.Trust.Pipeline do defp known_permissive?(license) do normalized = normalize_license(license) + Enum.any?(@permissive_licenses, fn l -> String.contains?(normalized, String.downcase(l)) end) @@ -403,6 +424,7 @@ defmodule Opsm.Trust.Pipeline do defp known_copyleft?(license) do normalized = normalize_license(license) + Enum.any?(@copyleft_licenses, fn l -> String.contains?(normalized, String.downcase(l)) end) diff --git a/opsm_ex/lib/opsm/types.ex b/opsm_ex/lib/opsm/types.ex index de1ddc13..2bfe139c 100644 --- a/opsm_ex/lib/opsm/types.ex +++ b/opsm_ex/lib/opsm/types.ex @@ -96,7 +96,14 @@ defmodule Opsm.Types do recommendations: [String.t()], risks: [OikosRisk.t()] } - defstruct [:repository_url, :analyzed_at, :overall_score, :scores, recommendations: [], risks: []] + defstruct [ + :repository_url, + :analyzed_at, + :overall_score, + :scores, + recommendations: [], + risks: [] + ] end defmodule OikosHealthResponse do @@ -150,16 +157,29 @@ defmodule Opsm.Types do dependencies: map(), dev_dependencies: map() | nil } - defstruct [:name, :version, :description, :license, :repository, - authors: [], keywords: [], dependencies: %{}, dev_dependencies: nil] + defstruct [ + :name, + :version, + :description, + :license, + :repository, + authors: [], + keywords: [], + dependencies: %{}, + dev_dependencies: nil + ] end # ============================================================================= # CHECKY-MONKEY - Code Verification # ============================================================================= - @type verification_type :: :property_tests | :fuzz_testing | :type_checking - | :formal_verification | :mutation_testing + @type verification_type :: + :property_tests + | :fuzz_testing + | :type_checking + | :formal_verification + | :mutation_testing @type verification_status :: :queued | :running | :completed | :failed @type finding_severity :: :low | :medium | :high | :critical @@ -291,8 +311,7 @@ defmodule Opsm.Types do obligations: [LicenseObligation.t()], risks: [LicenseRisk.t()] } - defstruct [:analyzed_at, :compatibility, - detected_licenses: [], obligations: [], risks: []] + defstruct [:analyzed_at, :compatibility, detected_licenses: [], obligations: [], risks: []] end # ============================================================================= @@ -301,9 +320,29 @@ defmodule Opsm.Types do @type federation_mode :: :manifest_convert | :agentic_fetch | :connection_port - @type forth_type :: :npm | :cargo | :hex | :pypi | :gem | :nuget | :maven | :pub | :go - | :deb | :rpm | :winget | :choco | :scoop | :pacman | :homebrew - | :nix | :guix | :flatpak | :snap | :custom | :eclexia + @type forth_type :: + :npm + | :cargo + | :hex + | :pypi + | :gem + | :nuget + | :maven + | :pub + | :go + | :deb + | :rpm + | :winget + | :choco + | :scoop + | :pacman + | :homebrew + | :nix + | :guix + | :flatpak + | :snap + | :custom + | :eclexia @type release_channel :: :snapshot | :alpha | :beta | :rc | :esr | :stable @@ -321,8 +360,14 @@ defmodule Opsm.Types do manifest_converter: String.t() | nil, enabled: boolean() } - defstruct [:name, :forth_type, :base_url, :federation_mode, - :manifest_converter, enabled: true] + defstruct [ + :name, + :forth_type, + :base_url, + :federation_mode, + :manifest_converter, + enabled: true + ] end defmodule ManifestFormat do @@ -348,10 +393,24 @@ defmodule Opsm.Types do source_forth: Opsm.Types.forth_type() | nil, raw_manifest: map() | nil } - defstruct [:name, :version, :description, :license, :homepage, :repository, - :source_forth, :raw_manifest, - authors: [], keywords: [], dependencies: %{}, dev_dependencies: %{}, - optional_dependencies: %{}, peer_dependencies: %{}, bin: %{}, scripts: %{}] + defstruct [ + :name, + :version, + :description, + :license, + :homepage, + :repository, + :source_forth, + :raw_manifest, + authors: [], + keywords: [], + dependencies: %{}, + dev_dependencies: %{}, + optional_dependencies: %{}, + peer_dependencies: %{}, + bin: %{}, + scripts: %{} + ] end defmodule InstallRequest do @@ -369,9 +428,17 @@ defmodule Opsm.Types do optional_deps: boolean(), dev_deps: boolean() } - defstruct [:package, :version, :forth, - channel: :stable, scope: :user, dry_run: false, - force: false, optional_deps: false, dev_deps: false] + defstruct [ + :package, + :version, + :forth, + channel: :stable, + scope: :user, + dry_run: false, + force: false, + optional_deps: false, + dev_deps: false + ] end defmodule ResolvedPackage do @@ -390,9 +457,18 @@ defmodule Opsm.Types do attestations: [AttestationRef.t()], resolved_deps: [__MODULE__.t()] } - defstruct [:package, :version, :forth, :registry_url, :tarball_url, - :checksum, :manifest, - checksum_algo: :sha256, attestations: [], resolved_deps: []] + defstruct [ + :package, + :version, + :forth, + :registry_url, + :tarball_url, + :checksum, + :manifest, + checksum_algo: :sha256, + attestations: [], + resolved_deps: [] + ] end defmodule PinSpec do @@ -426,8 +502,17 @@ defmodule Opsm.Types do user: String.t() | nil, scope: Opsm.Types.install_scope() } - defstruct [:id, :action, :package, :version, :previous_version, - :forth, :timestamp, :user, :scope] + defstruct [ + :id, + :action, + :package, + :version, + :previous_version, + :forth, + :timestamp, + :user, + :scope + ] end # ============================================================================= @@ -449,9 +534,14 @@ defmodule Opsm.Types do signature_algo: atom() | nil } defstruct [ - :builder_id, :build_type, :invocation, :metadata, - :signature, :signature_algo, - materials: [], slsa_level: 0 + :builder_id, + :build_type, + :invocation, + :metadata, + :signature, + :signature_algo, + materials: [], + slsa_level: 0 ] end @@ -475,11 +565,13 @@ defmodule Opsm.Types do warnings: [String.t()], errors: [String.t()] } - defstruct [ - verified: false, slsa_level: 0, builder_trusted: false, - materials_match: false, signature_valid: :not_checked, - warnings: [], errors: [] - ] + defstruct verified: false, + slsa_level: 0, + builder_trusted: false, + materials_match: false, + signature_valid: :not_checked, + warnings: [], + errors: [] end defmodule ConnectionPort do diff --git a/opsm_ex/lib/opsm/validation.ex b/opsm_ex/lib/opsm/validation.ex index 14cc9c5b..c71330f7 100644 --- a/opsm_ex/lib/opsm/validation.ex +++ b/opsm_ex/lib/opsm/validation.ex @@ -197,13 +197,13 @@ defmodule Opsm.Validation do case parse_ipv4(host) do {:ok, {a, b, _c, _d}} -> # Loopback: 127.0.0.0/8 - a == 127 or # Private: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 - a == 10 or - (a == 172 and b >= 16 and b <= 31) or - (a == 192 and b == 168) or # Link-local: 169.254.0.0/16 - (a == 169 and b == 254) + a == 127 or + a == 10 or + (a == 172 and b >= 16 and b <= 31) or + (a == 192 and b == 168) or + (a == 169 and b == 254) :error -> # Check for localhost or IPv6 loopback @@ -279,26 +279,32 @@ defmodule Opsm.Validation do defp valid_for_registry?(name, forth) when forth in [:maven, :java, :kotlin] do Regex.match?(~r/^[a-zA-Z0-9._-]+(:[a-zA-Z0-9._-]+)?$/, name) end + # Go: module paths (e.g., github.com/user/repo) defp valid_for_registry?(name, forth) when forth in [:go, :golang] do Regex.match?(~r|^[a-zA-Z0-9][a-zA-Z0-9._/-]*$|, name) end + # RubyGems: alphanumeric with hyphens/underscores defp valid_for_registry?(name, forth) when forth in [:gem, :rubygems, :ruby] do Regex.match?(~r/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/, name) end + # Hackage: identifiers with hyphens defp valid_for_registry?(name, forth) when forth in [:hackage, :haskell] do Regex.match?(~r/^[a-zA-Z][a-zA-Z0-9-]*$/, name) end + # NuGet: alphanumeric with dots/hyphens defp valid_for_registry?(name, forth) when forth in [:nuget, :dotnet] do Regex.match?(~r/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/, name) end + # pub.dev: lowercase with underscores defp valid_for_registry?(name, forth) when forth in [:pub, :dart, :flutter] do Regex.match?(~r/^[a-z][a-z0-9_]*$/, name) end + defp valid_for_registry?(name, _), do: Regex.match?(@generic_pattern, name) # =========================================================================== diff --git a/opsm_ex/lib/opsm/verified.ex b/opsm_ex/lib/opsm/verified.ex index 91c632a3..06f75442 100644 --- a/opsm_ex/lib/opsm/verified.ex +++ b/opsm_ex/lib/opsm/verified.ex @@ -103,11 +103,11 @@ defmodule Opsm.Verified.Url do case parse_ipv4(host) do {:ok, {a, b, _c, _d}} -> # Loopback: 127.0.0.0/8 - a == 127 or # Private: 10.0.0.0/8, 172.16.0.0/12, 192.168.0.0/16 - a == 10 or - (a == 172 and b >= 16 and b <= 31) or - (a == 192 and b == 168) + a == 127 or + a == 10 or + (a == 172 and b >= 16 and b <= 31) or + (a == 192 and b == 168) :error -> false @@ -135,7 +135,6 @@ defmodule Opsm.Verified.Url do :error end end - end defmodule Opsm.Verified.Json do @@ -152,7 +151,8 @@ defmodule Opsm.Verified.Json do """ @max_depth 20 - @max_size 10_485_760 # 10 MB + # 10 MB + @max_size 10_485_760 @doc """ Decode JSON string safely. @@ -208,14 +208,16 @@ defmodule Opsm.Verified.Json do # Check nesting depth recursively defp check_depth(_data, 0), do: false + defp check_depth(data, depth) when is_map(data) do Enum.all?(data, fn {_k, v} -> check_depth(v, depth - 1) end) end + defp check_depth(data, depth) when is_list(data) do Enum.all?(data, fn v -> check_depth(v, depth - 1) end) end - defp check_depth(_data, _depth), do: true + defp check_depth(_data, _depth), do: true end defmodule Opsm.Verified.Result do diff --git a/opsm_ex/lib/opsm/verified/http.ex b/opsm_ex/lib/opsm/verified/http.ex index c44f3390..270bdd42 100644 --- a/opsm_ex/lib/opsm/verified/http.ex +++ b/opsm_ex/lib/opsm/verified/http.ex @@ -123,18 +123,21 @@ defmodule Opsm.Verified.Http do # ============================================================================= defp do_get(url, opts) do - timeout = Keyword.get(opts, :timeout, 30_000) + # :receive_timeout is Req's actual option name — callers pass it + # throughout the codebase, so read that key, not a nonexistent :timeout. + timeout = Keyword.get(opts, :receive_timeout, Keyword.get(opts, :timeout, 30_000)) headers = Keyword.get(opts, :headers, []) retry_count = Keyword.get(opts, :retry, 2) + # :into (e.g. File.stream!/1 for streaming a download to disk) must be + # forwarded, not silently dropped, or the caller gets a success tuple + # with nothing ever written to the destination. + req_opts = + [receive_timeout: timeout, retry: :transient, max_retries: retry_count, headers: headers] + |> maybe_put_into(opts) + try do - result = - Req.get(url, - receive_timeout: timeout, - retry: :transient, - max_retries: retry_count, - headers: headers - ) + result = Req.get(url, req_opts) case result do {:ok, %Req.Response{status: status, body: body}} when status in 200..299 -> @@ -170,7 +173,7 @@ defmodule Opsm.Verified.Http do end defp do_post(url, json_body, opts) do - timeout = Keyword.get(opts, :timeout, 30_000) + timeout = Keyword.get(opts, :receive_timeout, Keyword.get(opts, :timeout, 30_000)) headers = Keyword.get(opts, :headers, []) retry_count = Keyword.get(opts, :retry, 2) @@ -208,4 +211,11 @@ defmodule Opsm.Verified.Http do {:error, {:exception, exception}} end end + + defp maybe_put_into(req_opts, opts) do + case Keyword.fetch(opts, :into) do + {:ok, into} -> Keyword.put(req_opts, :into, into) + :error -> req_opts + end + end end diff --git a/opsm_ex/lib/opsm/verisimdb.ex b/opsm_ex/lib/opsm/verisimdb.ex index 46496e21..57340118 100644 --- a/opsm_ex/lib/opsm/verisimdb.ex +++ b/opsm_ex/lib/opsm/verisimdb.ex @@ -193,7 +193,6 @@ defmodule Opsm.VeriSimDB do %{ name: "opsm:install:#{event[:name]}@#{event[:version]}", description: "Package install: #{event[:name]}@#{event[:version]} from @#{event[:forth]}", - document: %{ title: "Install #{event[:name]}@#{event[:version]}", body: """ @@ -217,21 +216,18 @@ defmodule Opsm.VeriSimDB do tarball_url: event[:tarball_url] } }, - graph: %{ relationships: [%{predicate: "installed_from", target: "registry:#{event[:forth]}"}] ++ - Enum.map(event[:dependencies] || [], fn dep -> - %{predicate: "depends_on", target: "package:#{dep}"} - end) + Enum.map(event[:dependencies] || [], fn dep -> + %{predicate: "depends_on", target: "package:#{dep}"} + end) }, - temporal: %{ timestamp: DateTime.utc_now() |> DateTime.to_iso8601(), version: 1, author: "opsm-installer" }, - provenance: %{ origin: "opsm:installer", chain: [ @@ -241,7 +237,6 @@ defmodule Opsm.VeriSimDB do ], actors: ["opsm-cli"] }, - semantic: %{ types: ["opsm:event:install", "opsm:package:#{event[:forth]}"], properties: %{ @@ -257,7 +252,6 @@ defmodule Opsm.VeriSimDB do %{ name: "opsm:uninstall:#{event[:name]}@#{event[:version]}", description: "Package removal: #{event[:name]}@#{event[:version]}", - document: %{ title: "Uninstall #{event[:name]}@#{event[:version]}", body: """ @@ -274,19 +268,16 @@ defmodule Opsm.VeriSimDB do forth: to_string(event[:forth]) } }, - temporal: %{ timestamp: DateTime.utc_now() |> DateTime.to_iso8601(), version: 1, author: "opsm-installer" }, - provenance: %{ origin: "opsm:installer", chain: ["uninstall:#{event[:name]}"], actors: ["opsm-cli"] }, - semantic: %{ types: ["opsm:event:uninstall"], properties: %{} @@ -300,7 +291,6 @@ defmodule Opsm.VeriSimDB do %{ name: "opsm:resolve:#{event[:root_package]}", description: "Dependency resolution: #{event[:root_package]} → #{resolved_count} packages", - document: %{ title: "Resolve #{event[:root_package]}", body: """ @@ -318,26 +308,22 @@ defmodule Opsm.VeriSimDB do resolved_packages: Enum.join(event[:resolved] || [], ", ") } }, - graph: %{ relationships: Enum.map(event[:resolved] || [], fn pkg_name -> %{predicate: "resolved", target: "package:#{pkg_name}"} end) }, - temporal: %{ timestamp: DateTime.utc_now() |> DateTime.to_iso8601(), version: 1, author: "opsm-resolver" }, - provenance: %{ origin: "opsm:resolver", chain: ["resolve:#{event[:root_package]}"], actors: ["opsm-cli"] }, - semantic: %{ types: ["opsm:event:resolution"], properties: %{ @@ -351,7 +337,6 @@ defmodule Opsm.VeriSimDB do %{ name: "opsm:trust:#{event[:name]}@#{event[:version]}", description: "Trust check: #{event[:name]}@#{event[:version]} → #{event[:overall]}", - document: %{ title: "Trust #{event[:overall]}: #{event[:name]}@#{event[:version]}", body: """ @@ -374,13 +359,11 @@ defmodule Opsm.VeriSimDB do pq_signed: event[:pq_signed] || false } }, - temporal: %{ timestamp: DateTime.utc_now() |> DateTime.to_iso8601(), version: 1, author: "opsm-trust-pipeline" }, - provenance: %{ origin: "opsm:trust-pipeline", chain: [ @@ -392,7 +375,6 @@ defmodule Opsm.VeriSimDB do ], actors: ["opsm-cli"] }, - semantic: %{ types: ["opsm:event:trust_check", "opsm:trust:#{event[:overall]}"], properties: %{ @@ -411,10 +393,12 @@ defmodule Opsm.VeriSimDB do app_config = Application.get_env(:opsm, :verisimdb, []) %{ - base_url: System.get_env("OPSM_VERISIMDB_URL") || - Keyword.get(app_config, :base_url, "http://127.0.0.1:6077"), - enabled: (System.get_env("OPSM_VERISIMDB_ENABLED") || "true") != "false" && - Keyword.get(app_config, :enabled, true), + base_url: + System.get_env("OPSM_VERISIMDB_URL") || + Keyword.get(app_config, :base_url, "http://127.0.0.1:6077"), + enabled: + (System.get_env("OPSM_VERISIMDB_ENABLED") || "true") != "false" && + Keyword.get(app_config, :enabled, true), timeout: Keyword.get(app_config, :timeout, 5_000) } end diff --git a/opsm_ex/lib/opsm/version_constraint.ex b/opsm_ex/lib/opsm/version_constraint.ex index 54acbe07..9cb341f7 100644 --- a/opsm_ex/lib/opsm/version_constraint.ex +++ b/opsm_ex/lib/opsm/version_constraint.ex @@ -108,15 +108,19 @@ defmodule Opsm.VersionConstraint do # - Hackage: "2.2.3.0" → take first 3 segments # - Pre-release: "1.2.3-beta.1" → standard semver defp parse_version_lenient(version_string) do - normalized = version_string + normalized = + version_string |> String.trim_leading("v") |> String.trim_leading("V") case Version.parse(normalized) do - {:ok, version} -> {:ok, version} + {:ok, version} -> + {:ok, version} + :error -> # Try truncating to 3 segments (handles Hackage 4-part versions) parts = String.split(normalized, ".") + if length(parts) > 3 do truncated = parts |> Enum.take(3) |> Enum.join(".") Version.parse(truncated) @@ -189,7 +193,8 @@ defmodule Opsm.VersionConstraint do # Exact version: 1.2.3 or v1.2.3 (Go-style) true -> - normalized = str |> String.trim_leading("v") |> String.trim_leading("V") |> normalize_version() + normalized = + str |> String.trim_leading("v") |> String.trim_leading("V") |> normalize_version() case Version.parse(normalized) do {:ok, version} -> @@ -207,7 +212,12 @@ defmodule Opsm.VersionConstraint do end defp parse_comparison(op, version_str, original) do - normalized = version_str |> String.trim() |> String.trim_leading("v") |> String.trim_leading("V") |> normalize_version() + normalized = + version_str + |> String.trim() + |> String.trim_leading("v") + |> String.trim_leading("V") + |> normalize_version() case Version.parse(normalized) do {:ok, version} -> @@ -226,28 +236,33 @@ defmodule Opsm.VersionConstraint do # Normalize version to x.y.z format (Elixir Version module requirement) # Handles: "1.0" → "1.0.0", "v1.2.3" → "1.2.3", "2.2.3.0" → "2.2.3" defp normalize_version(version_str) do - cleaned = version_str + cleaned = + version_str |> String.trim_leading("v") |> String.trim_leading("V") # Split on "." but preserve pre-release/build metadata - {base, suffix} = case String.split(cleaned, "-", parts: 2) do - [base, rest] -> {base, "-" <> rest} - [base] -> - case String.split(base, "+", parts: 2) do - [b, meta] -> {b, "+" <> meta} - [b] -> {b, ""} - end - end + {base, suffix} = + case String.split(cleaned, "-", parts: 2) do + [base, rest] -> + {base, "-" <> rest} + + [base] -> + case String.split(base, "+", parts: 2) do + [b, meta] -> {b, "+" <> meta} + [b] -> {b, ""} + end + end parts = String.split(base, ".") - normalized_base = case length(parts) do - 1 -> "#{base}.0.0" - 2 -> "#{base}.0" - n when n > 3 -> parts |> Enum.take(3) |> Enum.join(".") - _ -> base - end + normalized_base = + case length(parts) do + 1 -> "#{base}.0.0" + 2 -> "#{base}.0" + n when n > 3 -> parts |> Enum.take(3) |> Enum.join(".") + _ -> base + end normalized_base <> suffix end diff --git a/opsm_ex/lib/opsm/wiring.ex b/opsm_ex/lib/opsm/wiring.ex index 085da37b..1b78c071 100644 --- a/opsm_ex/lib/opsm/wiring.ex +++ b/opsm_ex/lib/opsm/wiring.ex @@ -9,6 +9,7 @@ defmodule Opsm.Wiring do alias Opsm.Clients.{CheckyMonkey, Oikos, Palimpsest} alias Opsm.{Errors, ManifestIngestion} + alias Opsm.Types.{ OpsmConfig, CheckyMonkeyRequest, @@ -23,8 +24,10 @@ defmodule Opsm.Wiring do def run_status(%OpsmConfig{} = config) do clients = [ {"oikos", Oikos.new(config.oikos, config.http), &Oikos.health/1}, - {"checky-monkey", CheckyMonkey.new(config.checky_monkey, config.http), &CheckyMonkey.health/1}, - {"palimpsest-license", Palimpsest.new(config.palimpsest_license, config.http), &Palimpsest.health/1} + {"checky-monkey", CheckyMonkey.new(config.checky_monkey, config.http), + &CheckyMonkey.health/1}, + {"palimpsest-license", Palimpsest.new(config.palimpsest_license, config.http), + &Palimpsest.health/1} ] IO.puts("OPSM Service Status") @@ -53,7 +56,8 @@ defmodule Opsm.Wiring do IO.puts("") with {:ok, ingestion} <- ManifestIngestion.ingest(path), - {:ok, _license_result} <- run_license_check(config, ingestion.manifest_path, ingestion.manifest), + {:ok, _license_result} <- + run_license_check(config, ingestion.manifest_path, ingestion.manifest), :ok <- run_sustainability_check(config, ingestion.manifest), :ok <- validate_publish_metadata(ingestion.manifest) do maybe_run_checky(config, ingestion.manifest_path) @@ -76,13 +80,19 @@ defmodule Opsm.Wiring do IO.puts("") oikos_client = Oikos.new(config.oikos, config.http) - analysis_request = %OikosAnalysisRequest{repository_url: package, branch: nil, commit_sha: nil} + + analysis_request = %OikosAnalysisRequest{ + repository_url: package, + branch: nil, + commit_sha: nil + } case Oikos.analyze_repository(oikos_client, analysis_request) do {:ok, resp} -> IO.puts("Sustainability Analysis (oikos)") IO.puts("--------------------------------") print_oikos_summary(resp) + {:error, reason} -> IO.puts("Sustainability Analysis (oikos)") IO.puts("--------------------------------") @@ -95,7 +105,12 @@ defmodule Opsm.Wiring do artifact_path = if File.exists?(package), do: package, else: File.cwd!() palimpsest_client = Palimpsest.new(config.palimpsest_license, config.http) - request = %PalimpsestRequest{artifact_path: artifact_path, include_transitive: true, target_license: nil} + + request = %PalimpsestRequest{ + artifact_path: artifact_path, + include_transitive: true, + target_license: nil + } case Palimpsest.analyze(palimpsest_client, request) do {:ok, resp} -> @@ -103,11 +118,13 @@ defmodule Opsm.Wiring do compat = resp.compatibility status = if compat.compatible, do: "compatible", else: "conflicts" IO.puts(" ✓ Compatibility: #{status}") + if not compat.compatible do for conflict <- compat.conflicts || [] do IO.puts(" - #{conflict.license1} vs #{conflict.license2}: #{conflict.reason}") end end + {:error, reason} -> IO.puts(" ⚠ License service unavailable: #{reason}") end @@ -180,8 +197,8 @@ defmodule Opsm.Wiring do IO.puts(" ⚠ Low sustainability score: #{score}/100 — consider improving before publish") :ok - # No {:error, _} clause: analyze_package/4 is deliberately infallible — - # it falls back to heuristic scoring when the oikos service is unreachable. + # No {:error, _} clause: analyze_package/4 is deliberately infallible — + # it falls back to heuristic scoring when the oikos service is unreachable. end end @@ -201,7 +218,9 @@ defmodule Opsm.Wiring do else: errors case errors do - [] -> :ok + [] -> + :ok + _ -> combined = Enum.join(errors, "; ") {:error, "Publish metadata validation failed: #{combined}"} @@ -284,6 +303,7 @@ defmodule Opsm.Wiring do if results[:property_tests] do IO.puts(" property tests: #{results.property_tests.status}") + if results.property_tests.tests_passed do IO.puts(" passed: #{results.property_tests.tests_passed}") end @@ -291,6 +311,7 @@ defmodule Opsm.Wiring do if results[:type_checking] do IO.puts(" type checking: #{results.type_checking.status}") + if results.type_checking.errors do IO.puts(" errors: #{length(results.type_checking.errors)}") end @@ -302,7 +323,9 @@ defmodule Opsm.Wiring do defp git_metadata(dir) do case Opsm.SafeExec.cmd("git", ["-C", dir, "rev-parse", "HEAD"], stderr_to_stdout: true) do {commit, 0} -> - case Opsm.SafeExec.cmd("git", ["-C", dir, "remote", "get-url", "origin"], stderr_to_stdout: true) do + case Opsm.SafeExec.cmd("git", ["-C", dir, "remote", "get-url", "origin"], + stderr_to_stdout: true + ) do {url, 0} -> {:ok, %{repo_url: String.trim(url), commit_sha: String.trim(commit)}} diff --git a/opsm_ex/mix.exs b/opsm_ex/mix.exs index eb6ecd8d..360b1517 100644 --- a/opsm_ex/mix.exs +++ b/opsm_ex/mix.exs @@ -35,7 +35,8 @@ defmodule Opsm.MixProject do {:optimus, "~> 0.5"}, {:jason, "~> 1.4"}, {:proven, git: "https://github.com/hyperpolymath/proven.git", subdir: "bindings/elixir"}, - {:verisim_client, git: "https://github.com/hyperpolymath/verisimdb.git", sparse: "connectors/clients/elixir"}, + {:verisim_client, + git: "https://github.com/hyperpolymath/verisimdb.git", sparse: "connectors/clients/elixir"}, {:stream_data, "~> 0.6", only: :test}, {:plug, "~> 1.15"}, {:bandit, "~> 1.5"}, @@ -78,7 +79,8 @@ defmodule Opsm.MixProject do links: %{ "GitHub" => "https://github.com/hyperpolymath/odds-and-sods-package-manager", "Docs" => "https://github.com/hyperpolymath/odds-and-sods-package-manager#readme", - "Roadmap" => "https://github.com/hyperpolymath/odds-and-sods-package-manager/blob/main/ROADMAP.adoc", + "Roadmap" => + "https://github.com/hyperpolymath/odds-and-sods-package-manager/blob/main/ROADMAP.adoc", "Changelog" => "https://github.com/hyperpolymath/odds-and-sods-package-manager/releases" }, maintainers: ["Jonathan D.A. Jewell "] diff --git a/opsm_ex/test/aspect/concurrency_test.exs b/opsm_ex/test/aspect/concurrency_test.exs index 66baac92..95f73307 100644 --- a/opsm_ex/test/aspect/concurrency_test.exs +++ b/opsm_ex/test/aspect/concurrency_test.exs @@ -14,22 +14,24 @@ defmodule Opsm.Aspect.ConcurrencyTest do base = Lockfile.new() # Simulate concurrent additions (serialized in Elixir, but tests the logic) - tasks = Enum.map(1..10, fn i -> - Task.async(fn -> - %{ - name: "pkg-#{i}", - version: "1.0.#{i}", - forth: :npm, - checksum: "hash-#{i}" - } + tasks = + Enum.map(1..10, fn i -> + Task.async(fn -> + %{ + name: "pkg-#{i}", + version: "1.0.#{i}", + forth: :npm, + checksum: "hash-#{i}" + } + end) end) - end) packages = Enum.map(tasks, &Task.await/1) # Add all packages to lockfile - lockfile = packages - |> Enum.reduce(base, &Lockfile.add_package(&2, &1)) + lockfile = + packages + |> Enum.reduce(base, &Lockfile.add_package(&2, &1)) # Verify all packages are present assert Lockfile.list_packages(lockfile) |> length() == 10 @@ -44,21 +46,23 @@ defmodule Opsm.Aspect.ConcurrencyTest do test "parallel package reads don't corrupt state" do # Build a lockfile - lockfile = Enum.reduce(1..20, Lockfile.new(), fn i, acc -> - Lockfile.add_package(acc, %{ - name: "pkg-#{i}", - version: "1.0.0", - forth: :npm, - checksum: "hash-#{i}" - }) - end) + lockfile = + Enum.reduce(1..20, Lockfile.new(), fn i, acc -> + Lockfile.add_package(acc, %{ + name: "pkg-#{i}", + version: "1.0.0", + forth: :npm, + checksum: "hash-#{i}" + }) + end) # Read from multiple "parallel" tasks - tasks = Enum.map(1..10, fn _ -> - Task.async(fn -> - Lockfile.list_packages(lockfile) + tasks = + Enum.map(1..10, fn _ -> + Task.async(fn -> + Lockfile.list_packages(lockfile) + end) end) - end) results = Enum.map(tasks, &Task.await/1) @@ -71,16 +75,18 @@ defmodule Opsm.Aspect.ConcurrencyTest do end test "concurrent integrity hash computation is idempotent" do - base = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) - |> Lockfile.add_package(%{name: "pkg2", version: "2.0.0", forth: :npm}) + base = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) + |> Lockfile.add_package(%{name: "pkg2", version: "2.0.0", forth: :npm}) # Compute hash multiple times in parallel - tasks = Enum.map(1..5, fn _ -> - Task.async(fn -> - Lockfile.compute_integrity_hash(base) + tasks = + Enum.map(1..5, fn _ -> + Task.async(fn -> + Lockfile.compute_integrity_hash(base) + end) end) - end) hashes = Enum.map(tasks, &Task.await/1) @@ -102,11 +108,12 @@ defmodule Opsm.Aspect.ConcurrencyTest do ] # Sequential "downloads" with lockfile updates - final_lockfile = Enum.reduce(packages_to_download, lockfile, fn pkg, acc -> - # Simulate: download, verify checksum, add to lockfile - verify_mock_checksum(pkg.checksum) - Lockfile.add_package(acc, pkg) - end) + final_lockfile = + Enum.reduce(packages_to_download, lockfile, fn pkg, acc -> + # Simulate: download, verify checksum, add to lockfile + verify_mock_checksum(pkg.checksum) + Lockfile.add_package(acc, pkg) + end) # All packages should be in lockfile assert length(Lockfile.list_packages(final_lockfile)) == 3 @@ -118,13 +125,14 @@ defmodule Opsm.Aspect.ConcurrencyTest do test "package download with integrity check" do # Simulate downloading a package and verifying integrity - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "verified-pkg", - version: "1.0.0", - forth: :npm, - checksum: "sha256:abc123def456" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "verified-pkg", + version: "1.0.0", + forth: :npm, + checksum: "sha256:abc123def456" + }) # Download package and verify downloaded_checksum = "sha256:abc123def456" @@ -134,13 +142,14 @@ defmodule Opsm.Aspect.ConcurrencyTest do end test "corrupted download is detected before install" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "critical-pkg", - version: "1.0.0", - forth: :npm, - checksum: "sha256:expected" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "critical-pkg", + version: "1.0.0", + forth: :npm, + checksum: "sha256:expected" + }) # Download received corrupted checksum actual_checksum = "sha256:corrupted" @@ -153,20 +162,22 @@ defmodule Opsm.Aspect.ConcurrencyTest do describe "Lockfile Serialization Under Load" do test "multiple serializations produce consistent output" do - lockfile = Enum.reduce(1..50, Lockfile.new(), fn i, acc -> - Lockfile.add_package(acc, %{ - name: "pkg-#{i}", - version: "#{i}.0.0", - forth: Enum.random([:npm, :cargo, :hex]), - checksum: "hash-#{i}" - }) - end) + lockfile = + Enum.reduce(1..50, Lockfile.new(), fn i, acc -> + Lockfile.add_package(acc, %{ + name: "pkg-#{i}", + version: "#{i}.0.0", + forth: Enum.random([:npm, :cargo, :hex]), + checksum: "hash-#{i}" + }) + end) # Serialize multiple times - serializations = Enum.map(1..3, fn _ -> - packages = Lockfile.list_packages(lockfile) - Jason.encode!(packages) - end) + serializations = + Enum.map(1..3, fn _ -> + packages = Lockfile.list_packages(lockfile) + Jason.encode!(packages) + end) # All serializations should be identical first = List.first(serializations) @@ -175,14 +186,15 @@ defmodule Opsm.Aspect.ConcurrencyTest do test "large lockfile remains consistent" do # Create a large lockfile - large_lockfile = Enum.reduce(1..100, Lockfile.new(), fn i, acc -> - Lockfile.add_package(acc, %{ - name: "pkg-#{i}", - version: "#{i}.0.0", - forth: Enum.random([:npm, :cargo, :hex, :pypi]), - checksum: "hash-#{i}" - }) - end) + large_lockfile = + Enum.reduce(1..100, Lockfile.new(), fn i, acc -> + Lockfile.add_package(acc, %{ + name: "pkg-#{i}", + version: "#{i}.0.0", + forth: Enum.random([:npm, :cargo, :hex, :pypi]), + checksum: "hash-#{i}" + }) + end) packages = Lockfile.list_packages(large_lockfile) @@ -201,25 +213,26 @@ defmodule Opsm.Aspect.ConcurrencyTest do describe "Conflict Handling" do test "same package from different forths doesn't conflict" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "utility", - version: "1.0.0", - forth: :npm, - checksum: "npm-hash" - }) - |> Lockfile.add_package(%{ - name: "utility", - version: "1.0.0", - forth: :cargo, - checksum: "cargo-hash" - }) - |> Lockfile.add_package(%{ - name: "utility", - version: "1.0.0", - forth: :hex, - checksum: "hex-hash" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "utility", + version: "1.0.0", + forth: :npm, + checksum: "npm-hash" + }) + |> Lockfile.add_package(%{ + name: "utility", + version: "1.0.0", + forth: :cargo, + checksum: "cargo-hash" + }) + |> Lockfile.add_package(%{ + name: "utility", + version: "1.0.0", + forth: :hex, + checksum: "hex-hash" + }) # All three should exist without conflict assert Lockfile.has_package?(lockfile, "utility", :npm) @@ -231,21 +244,23 @@ defmodule Opsm.Aspect.ConcurrencyTest do end test "package version update in lockfile is atomic" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "evolving-pkg", - version: "1.0.0", - forth: :npm, - checksum: "hash-1" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "evolving-pkg", + version: "1.0.0", + forth: :npm, + checksum: "hash-1" + }) # Update package version - updated = Lockfile.add_package(lockfile, %{ - name: "evolving-pkg", - version: "2.0.0", - forth: :npm, - checksum: "hash-2" - }) + updated = + Lockfile.add_package(lockfile, %{ + name: "evolving-pkg", + version: "2.0.0", + forth: :npm, + checksum: "hash-2" + }) # Package should be updated to new version pkg = Lockfile.get_package(updated, "evolving-pkg", :npm) @@ -254,25 +269,27 @@ defmodule Opsm.Aspect.ConcurrencyTest do # Old version should not exist refute Lockfile.has_package?(updated, "evolving-pkg", :npm) and - (Lockfile.get_package(updated, "evolving-pkg", :npm).version == "1.0.0") + Lockfile.get_package(updated, "evolving-pkg", :npm).version == "1.0.0" end end describe "Sync Checking Under Load" do test "lockfile sync check with many packages" do - lockfile = Enum.reduce(1..50, Lockfile.new(), fn i, acc -> - Lockfile.add_package(acc, %{ - name: "pkg-#{i}", - version: "1.0.0", - forth: :npm, - checksum: "hash-#{i}" - }) - end) + lockfile = + Enum.reduce(1..50, Lockfile.new(), fn i, acc -> + Lockfile.add_package(acc, %{ + name: "pkg-#{i}", + version: "1.0.0", + forth: :npm, + checksum: "hash-#{i}" + }) + end) # Simulate installed packages - installed = Enum.map(1..50, fn i -> - %{name: "pkg-#{i}", forth: :npm} - end) + installed = + Enum.map(1..50, fn i -> + %{name: "pkg-#{i}", forth: :npm} + end) result = Lockfile.check_sync(lockfile, installed) @@ -283,19 +300,21 @@ defmodule Opsm.Aspect.ConcurrencyTest do end test "detects missing packages in large lockfile" do - lockfile = Enum.reduce(1..50, Lockfile.new(), fn i, acc -> - Lockfile.add_package(acc, %{ - name: "pkg-#{i}", - version: "1.0.0", - forth: :npm, - checksum: "hash-#{i}" - }) - end) + lockfile = + Enum.reduce(1..50, Lockfile.new(), fn i, acc -> + Lockfile.add_package(acc, %{ + name: "pkg-#{i}", + version: "1.0.0", + forth: :npm, + checksum: "hash-#{i}" + }) + end) # Simulate missing packages 25-50 - installed = Enum.map(1..25, fn i -> - %{name: "pkg-#{i}", forth: :npm} - end) + installed = + Enum.map(1..25, fn i -> + %{name: "pkg-#{i}", forth: :npm} + end) result = Lockfile.check_sync(lockfile, installed) diff --git a/opsm_ex/test/aspect/security_test.exs b/opsm_ex/test/aspect/security_test.exs index 32230836..76cce8d6 100644 --- a/opsm_ex/test/aspect/security_test.exs +++ b/opsm_ex/test/aspect/security_test.exs @@ -11,34 +11,37 @@ defmodule Opsm.Aspect.SecurityTest do describe "Package Tampering Detection" do test "detects checksum mismatch when package is altered" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "critical-lib", - version: "1.0.0", - forth: :npm, - checksum: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", - checksum_algo: "sha256" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "critical-lib", + version: "1.0.0", + forth: :npm, + checksum: "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", + checksum_algo: "sha256" + }) # Tampered checksum should not match - {:mismatch, details} = Lockfile.verify_package( - lockfile, - "critical-lib", - :npm, - "sha256:0000000000000000000000000000000000000000000000000000000000000000" - ) + {:mismatch, details} = + Lockfile.verify_package( + lockfile, + "critical-lib", + :npm, + "sha256:0000000000000000000000000000000000000000000000000000000000000000" + ) assert details.expected =~ "e3b0c44" assert details.actual =~ "00000000" end test "rejects package with missing checksum" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "untrusted-lib", - version: "1.0.0", - forth: :npm - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "untrusted-lib", + version: "1.0.0", + forth: :npm + }) result = Lockfile.verify_package(lockfile, "untrusted-lib", :npm, "any-checksum") @@ -58,23 +61,26 @@ defmodule Opsm.Aspect.SecurityTest do # The resolver should check internal registries first assert :internal in registry_order assert :npm in registry_order - assert Enum.find_index(registry_order, &(&1 == :internal)) < Enum.find_index(registry_order, &(&1 == :npm)) + + assert Enum.find_index(registry_order, &(&1 == :internal)) < + Enum.find_index(registry_order, &(&1 == :npm)) end test "validates registry source for each package" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "business-logic", - version: "2.5.3", - forth: :npm, - source_url: "https://trusted-registry.example.com/business-logic.tgz" - }) - |> Lockfile.add_package(%{ - name: "util-lib", - version: "1.0.0", - forth: :internal, - source_url: "https://internal.company.local/util-lib.tgz" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "business-logic", + version: "2.5.3", + forth: :npm, + source_url: "https://trusted-registry.example.com/business-logic.tgz" + }) + |> Lockfile.add_package(%{ + name: "util-lib", + version: "1.0.0", + forth: :internal, + source_url: "https://internal.company.local/util-lib.tgz" + }) npm_pkg = Lockfile.get_package(lockfile, "business-logic", :npm) internal_pkg = Lockfile.get_package(lockfile, "util-lib", :internal) @@ -89,24 +95,27 @@ defmodule Opsm.Aspect.SecurityTest do describe "Lockfile Poisoning Detection" do test "detects when lockfile has been modified after generation" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "pkg1", - version: "1.0.0", - forth: :npm, - checksum: "abc123" - }) - |> Lockfile.compute_integrity_hash() + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "pkg1", + version: "1.0.0", + forth: :npm, + checksum: "abc123" + }) + |> Lockfile.compute_integrity_hash() original_hash = lockfile.integrity_hash # Simulate tampering: add a malicious package - tampered = Lockfile.add_package(lockfile, %{ - name: "malware", - version: "1.0.0", - forth: :npm, - checksum: "evil" - }) + tampered = + Lockfile.add_package(lockfile, %{ + name: "malware", + version: "1.0.0", + forth: :npm, + checksum: "evil" + }) + tampered = %{tampered | integrity_hash: original_hash} # Integrity verification should fail @@ -115,41 +124,46 @@ defmodule Opsm.Aspect.SecurityTest do end test "detects when dependency list is modified" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "app", - version: "1.0.0", - forth: :npm, - dependencies: ["safe-dep-1", "safe-dep-2"] - }) - |> Lockfile.compute_integrity_hash() + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "app", + version: "1.0.0", + forth: :npm, + dependencies: ["safe-dep-1", "safe-dep-2"] + }) + |> Lockfile.compute_integrity_hash() original_hash = lockfile.integrity_hash # Tamper: modify dependencies - tampered = Lockfile.add_package(lockfile, %{ - name: "app", - version: "1.0.0", - forth: :npm, - dependencies: ["safe-dep-1", "safe-dep-2", "malicious-dep"] - }) + tampered = + Lockfile.add_package(lockfile, %{ + name: "app", + version: "1.0.0", + forth: :npm, + dependencies: ["safe-dep-1", "safe-dep-2", "malicious-dep"] + }) + tampered = %{tampered | integrity_hash: original_hash} assert {:error, _} = Lockfile.verify_integrity(tampered) end test "lockfile integrity hash prevents silent corruption" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc"}) - |> Lockfile.compute_integrity_hash() + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc"}) + |> Lockfile.compute_integrity_hash() # Compute new hash after modification - modified = Lockfile.add_package(lockfile, %{ - name: "pkg", - version: "2.0.0", - forth: :npm, - checksum: "def" - }) + modified = + Lockfile.add_package(lockfile, %{ + name: "pkg", + version: "2.0.0", + forth: :npm, + checksum: "def" + }) # Hash without recomputation should fail tampered = %{modified | integrity_hash: lockfile.integrity_hash} @@ -194,7 +208,8 @@ defmodule Opsm.Aspect.SecurityTest do alias Opsm.Verified.Url # data: scheme should be rejected - assert {:error, {:invalid_scheme, "data"}} = Url.validate("data:text/html,") + assert {:error, {:invalid_scheme, "data"}} = + Url.validate("data:text/html,") end end @@ -233,9 +248,12 @@ defmodule Opsm.Aspect.SecurityTest do test "path normalization prevents traversal attacks" do # When extracting a tarball, paths must be checked for traversal paths_to_check = [ - {"package/lib/main.js", false}, # Safe - {"package/../../../etc/passwd", true}, # Dangerous - {"package/./lib/index.js", false}, # Safe + # Safe + {"package/lib/main.js", false}, + # Dangerous + {"package/../../../etc/passwd", true}, + # Safe + {"package/./lib/index.js", false} ] Enum.each(paths_to_check, fn {path, is_dangerous} -> @@ -255,17 +273,25 @@ defmodule Opsm.Aspect.SecurityTest do describe "Cryptographic Verification" do test "uses SHA3-512 for lockfile integrity" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) - |> Lockfile.compute_integrity_hash() + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + |> Lockfile.compute_integrity_hash() assert lockfile.integrity_algo == "sha3-512" - assert String.length(lockfile.integrity_hash) == 128 # 512 bits = 128 hex + # 512 bits = 128 hex + assert String.length(lockfile.integrity_hash) == 128 end test "packages default to BLAKE2b checksums" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "hash123"}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "pkg", + version: "1.0.0", + forth: :npm, + checksum: "hash123" + }) pkg = Lockfile.get_package(lockfile, "pkg", :npm) assert pkg.checksum_algo == "blake2b" @@ -274,29 +300,30 @@ defmodule Opsm.Aspect.SecurityTest do describe "Supply Chain Integrity" do test "lockfile records full dependency tree with integrity" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "root", - version: "1.0.0", - forth: :npm, - checksum: "root-hash", - dependencies: ["dep-a", "dep-b"] - }) - |> Lockfile.add_package(%{ - name: "dep-a", - version: "2.0.0", - forth: :npm, - checksum: "dep-a-hash", - dependencies: ["deep-dep"] - }) - |> Lockfile.add_package(%{ - name: "deep-dep", - version: "1.0.0", - forth: :npm, - checksum: "deep-hash", - dependencies: [] - }) - |> Lockfile.compute_integrity_hash() + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "root", + version: "1.0.0", + forth: :npm, + checksum: "root-hash", + dependencies: ["dep-a", "dep-b"] + }) + |> Lockfile.add_package(%{ + name: "dep-a", + version: "2.0.0", + forth: :npm, + checksum: "dep-a-hash", + dependencies: ["deep-dep"] + }) + |> Lockfile.add_package(%{ + name: "deep-dep", + version: "1.0.0", + forth: :npm, + checksum: "deep-hash", + dependencies: [] + }) + |> Lockfile.compute_integrity_hash() # Every package in the chain has checksum root = Lockfile.get_package(lockfile, "root", :npm) @@ -313,13 +340,14 @@ defmodule Opsm.Aspect.SecurityTest do end test "prevents version substitution attacks" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "pkg", - version: "1.0.0", - forth: :npm, - checksum: "checksum-for-1.0.0" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "pkg", + version: "1.0.0", + forth: :npm, + checksum: "checksum-for-1.0.0" + }) # Cannot trick into accepting 2.0.0 with old checksum result = Lockfile.verify_package(lockfile, "pkg", :npm, "checksum-for-1.0.0") diff --git a/opsm_ex/test/e2e/pq_sign_verify_e2e_test.exs b/opsm_ex/test/e2e/pq_sign_verify_e2e_test.exs index adbe510e..ee1350c1 100644 --- a/opsm_ex/test/e2e/pq_sign_verify_e2e_test.exs +++ b/opsm_ex/test/e2e/pq_sign_verify_e2e_test.exs @@ -34,11 +34,19 @@ defmodule Opsm.E2E.PqSignVerifyE2ETest do # Step 4: Verify that a tampered message is rejected tampered = original_message <> " TAMPERED" - assert {:error, _reason} = PostQuantum.dilithium5_verify(tampered, signed_blob, keys.public_key) + + assert {:error, _reason} = + PostQuantum.dilithium5_verify(tampered, signed_blob, keys.public_key) # Step 5: Verify that a different key is rejected {:ok, other_keys} = PostQuantum.dilithium5_keypair() - assert {:error, _reason} = PostQuantum.dilithium5_verify(original_message, signed_blob, other_keys.public_key) + + assert {:error, _reason} = + PostQuantum.dilithium5_verify( + original_message, + signed_blob, + other_keys.public_key + ) else # NIF not loaded -- graceful degradation assert {:error, :pq_not_available} = PostQuantum.dilithium5_keypair() @@ -56,10 +64,12 @@ defmodule Opsm.E2E.PqSignVerifyE2ETest do {:ok, signed_blob} = PostQuantum.sphincs_plus_sign(original_message, keys.secret_key) # Verify succeeds for correct message - assert :ok = PostQuantum.sphincs_plus_verify(original_message, signed_blob, keys.public_key) + assert :ok = + PostQuantum.sphincs_plus_verify(original_message, signed_blob, keys.public_key) # Verify fails for tampered message - assert {:error, _} = PostQuantum.sphincs_plus_verify("TAMPERED", signed_blob, keys.public_key) + assert {:error, _} = + PostQuantum.sphincs_plus_verify("TAMPERED", signed_blob, keys.public_key) else assert {:error, :pq_not_available} = PostQuantum.sphincs_plus_keypair() end @@ -110,7 +120,10 @@ defmodule Opsm.E2E.PqSignVerifyE2ETest do # Verify fails with different payload tampered_payload = Map.put(payload, "checksum", "sha256:TAMPERED") - tampered_result = HybridSignatures.verify_payload(tampered_payload, sig_info, public_keys) + + tampered_result = + HybridSignatures.verify_payload(tampered_payload, sig_info, public_keys) + assert match?({:error, _}, tampered_result) {:error, _} -> diff --git a/opsm_ex/test/e2e/registry_e2e_test.exs b/opsm_ex/test/e2e/registry_e2e_test.exs index 50e593e6..934a8433 100644 --- a/opsm_ex/test/e2e/registry_e2e_test.exs +++ b/opsm_ex/test/e2e/registry_e2e_test.exs @@ -65,7 +65,8 @@ defmodule Opsm.E2E.RegistryE2ETest do @tag :e2e test "exists?/1 returns false for non-existent package" do - assert Npm.exists?("this-package-does-not-exist-opsm-test-#{System.system_time(:second)}") == false + assert Npm.exists?("this-package-does-not-exist-opsm-test-#{System.system_time(:second)}") == + false end @tag :e2e @@ -319,7 +320,10 @@ defmodule Opsm.E2E.RegistryE2ETest do # Manifest should be consistent assert is_binary(pkg.manifest.name), "#{registry}: manifest.name should be binary" - assert is_binary(pkg.manifest.version), "#{registry}: manifest.version should be binary" + + assert is_binary(pkg.manifest.version), + "#{registry}: manifest.version should be binary" + assert is_map(pkg.manifest.dependencies), "#{registry}: dependencies should be map" assert is_atom(pkg.manifest.source_forth), "#{registry}: source_forth should be atom" diff --git a/opsm_ex/test/integration/e2e_test.exs b/opsm_ex/test/integration/e2e_test.exs index 8302681f..79fb3ce1 100644 --- a/opsm_ex/test/integration/e2e_test.exs +++ b/opsm_ex/test/integration/e2e_test.exs @@ -24,7 +24,8 @@ defmodule Opsm.Integration.E2ETest do end describe "Dependency Resolution - npm ecosystem" do - @tag :skip # Skip by default, requires network + # Skip by default, requires network + @tag :skip test "resolves transitive dependencies for express", %{test_dir: _test_dir} do # express depends on: accepts, content-type, cookie, etc. # accepts depends on: mime-types, negotiator @@ -603,7 +604,7 @@ defmodule Opsm.Integration.E2ETest do test "complete install: resolve -> verify -> lockfile update" do # Simulate full install flow deps = [ - %{name: "express", constraint: "^4.18.0", forth: :npm}, + %{name: "express", constraint: "^4.18.0", forth: :npm} ] # Step 1: Resolve dependencies (mocked - would query registry) @@ -612,31 +613,32 @@ defmodule Opsm.Integration.E2ETest do # Step 4: Install # Step 5: Write lockfile - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "express", - version: "4.18.2", - forth: :npm, - checksum: "sha256:express4182hash", - checksum_algo: "sha256", - dependencies: ["accepts", "body-parser"] - }) - |> Lockfile.add_package(%{ - name: "accepts", - version: "1.3.8", - forth: :npm, - checksum: "sha256:acceptshash", - checksum_algo: "sha256", - dependencies: [] - }) - |> Lockfile.add_package(%{ - name: "body-parser", - version: "1.20.0", - forth: :npm, - checksum: "sha256:bodyparserhash", - checksum_algo: "sha256", - dependencies: [] - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "express", + version: "4.18.2", + forth: :npm, + checksum: "sha256:express4182hash", + checksum_algo: "sha256", + dependencies: ["accepts", "body-parser"] + }) + |> Lockfile.add_package(%{ + name: "accepts", + version: "1.3.8", + forth: :npm, + checksum: "sha256:acceptshash", + checksum_algo: "sha256", + dependencies: [] + }) + |> Lockfile.add_package(%{ + name: "body-parser", + version: "1.20.0", + forth: :npm, + checksum: "sha256:bodyparserhash", + checksum_algo: "sha256", + dependencies: [] + }) # Verify installation recorded correctly assert Lockfile.has_package?(lockfile, "express", :npm) @@ -655,39 +657,43 @@ defmodule Opsm.Integration.E2ETest do lockfile = Lockfile.new() # Root package - lockfile = Lockfile.add_package(lockfile, %{ - name: "myapp", - version: "1.0.0", - forth: :npm, - checksum: "myapp-hash", - dependencies: ["react", "lodash"] - }) + lockfile = + Lockfile.add_package(lockfile, %{ + name: "myapp", + version: "1.0.0", + forth: :npm, + checksum: "myapp-hash", + dependencies: ["react", "lodash"] + }) # Level 1 dependencies - lockfile = Lockfile.add_package(lockfile, %{ - name: "react", - version: "18.2.0", - forth: :npm, - checksum: "react-hash", - dependencies: ["scheduler"] - }) - - lockfile = Lockfile.add_package(lockfile, %{ - name: "lodash", - version: "4.17.21", - forth: :npm, - checksum: "lodash-hash", - dependencies: [] - }) + lockfile = + Lockfile.add_package(lockfile, %{ + name: "react", + version: "18.2.0", + forth: :npm, + checksum: "react-hash", + dependencies: ["scheduler"] + }) + + lockfile = + Lockfile.add_package(lockfile, %{ + name: "lodash", + version: "4.17.21", + forth: :npm, + checksum: "lodash-hash", + dependencies: [] + }) # Level 2 dependencies - lockfile = Lockfile.add_package(lockfile, %{ - name: "scheduler", - version: "0.23.0", - forth: :npm, - checksum: "scheduler-hash", - dependencies: [] - }) + lockfile = + Lockfile.add_package(lockfile, %{ + name: "scheduler", + version: "0.23.0", + forth: :npm, + checksum: "scheduler-hash", + dependencies: [] + }) # Verify tree structure app = Lockfile.get_package(lockfile, "myapp", :npm) @@ -705,10 +711,11 @@ defmodule Opsm.Integration.E2ETest do describe "Full Uninstall Flow" do test "uninstall: remove -> cleanup -> lockfile update" do # Build lockfile with packages - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) - |> Lockfile.add_package(%{name: "pkg2", version: "2.0.0", forth: :npm}) - |> Lockfile.add_package(%{name: "pkg3", version: "3.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) + |> Lockfile.add_package(%{name: "pkg2", version: "2.0.0", forth: :npm}) + |> Lockfile.add_package(%{name: "pkg3", version: "3.0.0", forth: :npm}) # Uninstall pkg2 updated = Lockfile.remove_package(lockfile, "pkg2", :npm) @@ -726,25 +733,26 @@ defmodule Opsm.Integration.E2ETest do test "uninstall preserves orphan detection" do # Create dependency chain: app -> dep-a -> dep-b - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "app", - version: "1.0.0", - forth: :npm, - dependencies: ["dep-a"] - }) - |> Lockfile.add_package(%{ - name: "dep-a", - version: "1.0.0", - forth: :npm, - dependencies: ["dep-b"] - }) - |> Lockfile.add_package(%{ - name: "dep-b", - version: "1.0.0", - forth: :npm, - dependencies: [] - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "app", + version: "1.0.0", + forth: :npm, + dependencies: ["dep-a"] + }) + |> Lockfile.add_package(%{ + name: "dep-a", + version: "1.0.0", + forth: :npm, + dependencies: ["dep-b"] + }) + |> Lockfile.add_package(%{ + name: "dep-b", + version: "1.0.0", + forth: :npm, + dependencies: [] + }) # Uninstall app after_remove = Lockfile.remove_package(lockfile, "app", :npm) @@ -784,8 +792,10 @@ defmodule Opsm.Integration.E2ETest do test "compatible version constraints can be resolved" do # Both require overlapping versions - constraint1 = "^1.5.0" # Allows 1.5.0 - 1.999.999 - constraint2 = "^1.0.0" # Allows 1.0.0 - 1.999.999 + # Allows 1.5.0 - 1.999.999 + constraint1 = "^1.5.0" + # Allows 1.0.0 - 1.999.999 + constraint2 = "^1.0.0" {:ok, c1} = VersionConstraint.parse(constraint1, :semver) {:ok, c2} = VersionConstraint.parse(constraint2, :semver) @@ -800,10 +810,11 @@ defmodule Opsm.Integration.E2ETest do describe "Lockfile Integrity in E2E Flow" do test "lockfile integrity is maintained through install cycle" do # Create initial lockfile - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm, checksum: "hash1"}) - |> Lockfile.add_package(%{name: "pkg2", version: "2.0.0", forth: :npm, checksum: "hash2"}) - |> Lockfile.compute_integrity_hash() + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm, checksum: "hash1"}) + |> Lockfile.add_package(%{name: "pkg2", version: "2.0.0", forth: :npm, checksum: "hash2"}) + |> Lockfile.compute_integrity_hash() original_hash = lockfile.integrity_hash @@ -811,13 +822,14 @@ defmodule Opsm.Integration.E2ETest do assert :ok = Lockfile.verify_integrity(lockfile) # Simulate: Add new package during install - modified = Lockfile.add_package(lockfile, %{ - name: "pkg3", - version: "3.0.0", - forth: :npm, - checksum: "hash3" - }) - |> Lockfile.compute_integrity_hash() + modified = + Lockfile.add_package(lockfile, %{ + name: "pkg3", + version: "3.0.0", + forth: :npm, + checksum: "hash3" + }) + |> Lockfile.compute_integrity_hash() # Hash should change (not equal to original) assert modified.integrity_hash != original_hash @@ -831,21 +843,22 @@ defmodule Opsm.Integration.E2ETest do try do # Step 1: Create lockfile during install - original = Lockfile.new() - |> Lockfile.add_package(%{ - name: "react", - version: "18.2.0", - forth: :npm, - checksum: "sha256:react-hash", - dependencies: ["scheduler"] - }) - |> Lockfile.add_package(%{ - name: "scheduler", - version: "0.23.0", - forth: :npm, - checksum: "sha256:scheduler-hash", - dependencies: [] - }) + original = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "react", + version: "18.2.0", + forth: :npm, + checksum: "sha256:react-hash", + dependencies: ["scheduler"] + }) + |> Lockfile.add_package(%{ + name: "scheduler", + version: "0.23.0", + forth: :npm, + checksum: "sha256:scheduler-hash", + dependencies: [] + }) # Step 2: Write to disk {:ok, _path} = Lockfile.write(original, path) @@ -872,40 +885,44 @@ defmodule Opsm.Integration.E2ETest do describe "Multi-Registry E2E" do test "install from multiple registries simultaneously" do - lockfile = Lockfile.new() + lockfile = + Lockfile.new() - # NPM package - |> Lockfile.add_package(%{ - name: "lodash", - version: "4.17.21", - forth: :npm, - checksum: "npm-hash" - }) - |> Lockfile.add_package(%{ - name: "serde", - version: "1.0.163", - forth: :cargo, - checksum: "cargo-hash" - }) - |> Lockfile.add_package(%{ - name: "poison", - version: "5.0.0", - forth: :hex, - checksum: "hex-hash" - }) + # NPM package + |> Lockfile.add_package(%{ + name: "lodash", + version: "4.17.21", + forth: :npm, + checksum: "npm-hash" + }) + |> Lockfile.add_package(%{ + name: "serde", + version: "1.0.163", + forth: :cargo, + checksum: "cargo-hash" + }) + |> Lockfile.add_package(%{ + name: "poison", + version: "5.0.0", + forth: :hex, + checksum: "hex-hash" + }) # All should be present - npm_count = lockfile - |> Lockfile.packages_for_forth(:npm) - |> length() + npm_count = + lockfile + |> Lockfile.packages_for_forth(:npm) + |> length() - cargo_count = lockfile - |> Lockfile.packages_for_forth(:cargo) - |> length() + cargo_count = + lockfile + |> Lockfile.packages_for_forth(:cargo) + |> length() - hex_count = lockfile - |> Lockfile.packages_for_forth(:hex) - |> length() + hex_count = + lockfile + |> Lockfile.packages_for_forth(:hex) + |> length() assert npm_count == 1 assert cargo_count == 1 @@ -953,13 +970,14 @@ defmodule Opsm.Integration.E2ETest do describe "Error Handling — manifest and lockfile robustness" do test "lockfile with unknown forth is readable without crash" do # Simulate a lockfile that includes a registry we don't know about - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "future-pkg", - version: "1.0.0", - forth: :future_unknown_registry, - checksum: "sha256:abc123" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "future-pkg", + version: "1.0.0", + forth: :future_unknown_registry, + checksum: "sha256:abc123" + }) assert Lockfile.has_package?(lockfile, "future-pkg", :future_unknown_registry) end @@ -994,7 +1012,9 @@ defmodule Opsm.Integration.E2ETest do test "version constraint parse rejects obviously invalid input" do for bad <- ["", "!@#$%", ">>>invalid<<<"] do result = VersionConstraint.parse(bad, :semver) - assert match?({:error, _}, result), "expected error for #{inspect(bad)}, got #{inspect(result)}" + + assert match?({:error, _}, result), + "expected error for #{inspect(bad)}, got #{inspect(result)}" end end end @@ -1035,6 +1055,7 @@ defmodule Opsm.Integration.E2ETest do capture_io(fn -> {member, Opsm.Wiring.run_audit(config, member)} end) + {member, :checked} end) diff --git a/opsm_ex/test/integration/git_pipeline_test.exs b/opsm_ex/test/integration/git_pipeline_test.exs index 7ffe2915..b4cb9ae6 100644 --- a/opsm_ex/test/integration/git_pipeline_test.exs +++ b/opsm_ex/test/integration/git_pipeline_test.exs @@ -24,6 +24,7 @@ defmodule Opsm.Integration.GitPipelineTest do version = "0.1.0" edition = "2021" """) + File.mkdir_p!(Path.join(dir, "src")) File.write!(Path.join(dir, "src/main.rs"), ~s[fn main() { println!("hello"); }]) diff --git a/opsm_ex/test/integration/manifest_roundtrip_test.exs b/opsm_ex/test/integration/manifest_roundtrip_test.exs index 73615c2b..c9c2ff53 100644 --- a/opsm_ex/test/integration/manifest_roundtrip_test.exs +++ b/opsm_ex/test/integration/manifest_roundtrip_test.exs @@ -93,11 +93,20 @@ defmodule Opsm.Integration.ManifestRoundtripTest do describe "cross-format conversion" do @tag :integration test "all writer targets produce non-empty output" do - targets = [:package_json, :cargo_toml, :mix_exs, :pyproject_toml, :pubspec_yaml, :go_mod, :opsm_toml] + targets = [ + :package_json, + :cargo_toml, + :mix_exs, + :pyproject_toml, + :pubspec_yaml, + :go_mod, + :opsm_toml + ] for target <- targets do assert {:ok, output} = Writer.convert(@base_manifest, target), "Failed for target: #{target}" + assert byte_size(output) > 10, "Output too small for target: #{target}" end diff --git a/opsm_ex/test/integration/pipeline_test.exs b/opsm_ex/test/integration/pipeline_test.exs index 1e454649..e20961d5 100644 --- a/opsm_ex/test/integration/pipeline_test.exs +++ b/opsm_ex/test/integration/pipeline_test.exs @@ -138,20 +138,22 @@ defmodule Opsm.Integration.PipelineTest do lockfile = Lockfile.new() # Add packages - lockfile = Lockfile.add_package(lockfile, %{ - name: "lodash", - version: "4.17.21", - forth: :npm, - checksum: "sha256:abc123", - source_url: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" - }) - - lockfile = Lockfile.add_package(lockfile, %{ - name: "serde", - version: "1.0.195", - forth: :cargo, - checksum: "sha256:def456" - }) + lockfile = + Lockfile.add_package(lockfile, %{ + name: "lodash", + version: "4.17.21", + forth: :npm, + checksum: "sha256:abc123", + source_url: "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" + }) + + lockfile = + Lockfile.add_package(lockfile, %{ + name: "serde", + version: "1.0.195", + forth: :cargo, + checksum: "sha256:def456" + }) # Save {:ok, _} = Lockfile.write(lockfile, lock_path) @@ -173,9 +175,10 @@ defmodule Opsm.Integration.PipelineTest do test "lockfile sync check", %{tmp_dir: tmp_dir} do lock_path = Path.join(tmp_dir, "opsm.lock") - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) - |> Lockfile.add_package(%{name: "pkg2", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) + |> Lockfile.add_package(%{name: "pkg2", version: "1.0.0", forth: :npm}) {:ok, _} = Lockfile.write(lockfile, lock_path) {:ok, loaded} = Lockfile.read(lock_path) @@ -208,11 +211,12 @@ defmodule Opsm.Integration.PipelineTest do test "history recording" do # Record an operation - id = Maintenance.record_history("test_integration", %{ - package: "test-pkg", - version: "1.0.0", - action: "install" - }) + id = + Maintenance.record_history("test_integration", %{ + package: "test-pkg", + version: "1.0.0", + action: "install" + }) assert is_binary(id) assert String.length(id) == 16 @@ -271,13 +275,14 @@ defmodule Opsm.Integration.PipelineTest do _txn = Transaction.complete(txn) # Step 4: Update lockfile - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "fake-pkg", - version: "1.0.0", - forth: :npm, - checksum: "sha256:fakechecksum" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "fake-pkg", + version: "1.0.0", + forth: :npm, + checksum: "sha256:fakechecksum" + }) {:ok, _} = Lockfile.write(lockfile, lock_path) diff --git a/opsm_ex/test/integration/trust_pipeline_live_e2e_test.exs b/opsm_ex/test/integration/trust_pipeline_live_e2e_test.exs index ace2d511..31b6db29 100644 --- a/opsm_ex/test/integration/trust_pipeline_live_e2e_test.exs +++ b/opsm_ex/test/integration/trust_pipeline_live_e2e_test.exs @@ -24,8 +24,8 @@ defmodule Opsm.Integration.TrustPipelineLiveE2ETest do # --------------------------------------------------------------------------- @checky_monkey System.get_env("CHECKY_MONKEY_URL", "http://localhost:8081") - @palimpsest System.get_env("PALIMPSEST_URL", "http://localhost:8082") - @oikos System.get_env("OIKOS_URL", "http://localhost:8084") + @palimpsest System.get_env("PALIMPSEST_URL", "http://localhost:8082") + @oikos System.get_env("OIKOS_URL", "http://localhost:8084") @req_base [receive_timeout: 10_000, retry: false] @@ -69,22 +69,26 @@ defmodule Opsm.Integration.TrustPipelineLiveE2ETest do end test "POST /verify queues job and returns request_id" do - {:ok, resp} = rpost(@checky_monkey <> "/verify", %{ - repo_url: "https://github.com/hyperpolymath/odds-and-sods-package-manager", - commit_sha: "abc123def456", - verification_types: ["property-tests"] - }) + {:ok, resp} = + rpost(@checky_monkey <> "/verify", %{ + repo_url: "https://github.com/hyperpolymath/odds-and-sods-package-manager", + commit_sha: "abc123def456", + verification_types: ["property-tests"] + }) + assert resp.status in [200, 201, 202] assert is_binary(resp.body["request_id"]) assert resp.body["status"] in ["queued", "running"] end test "GET /verify/{request_id} returns job detail after submission" do - {:ok, sub} = rpost(@checky_monkey <> "/verify", %{ - repo_url: "https://github.com/hyperpolymath/test-repo", - commit_sha: "cafebabe", - verification_types: ["type-checking"] - }) + {:ok, sub} = + rpost(@checky_monkey <> "/verify", %{ + repo_url: "https://github.com/hyperpolymath/test-repo", + commit_sha: "cafebabe", + verification_types: ["type-checking"] + }) + assert sub.status in [200, 201, 202] request_id = sub.body["request_id"] @@ -149,13 +153,16 @@ defmodule Opsm.Integration.TrustPipelineLiveE2ETest do describe "oikos sustainability" do test "POST /analysis/repository returns structured scores" do - {:ok, resp} = rpost(@oikos <> "/analysis/repository", %{ - repo_url: "https://github.com/hyperpolymath/odds-and-sods-package-manager", - include_dependencies: false - }) + {:ok, resp} = + rpost(@oikos <> "/analysis/repository", %{ + repo_url: "https://github.com/hyperpolymath/odds-and-sods-package-manager", + include_dependencies: false + }) + assert resp.status == 200 scores = resp.body["scores"] assert is_map(scores) + for {_key, val} <- scores do assert is_float(val) or is_integer(val) assert val >= 0 and val <= 100 @@ -163,10 +170,12 @@ defmodule Opsm.Integration.TrustPipelineLiveE2ETest do end test "POST /analysis/repository handles unknown repo gracefully" do - {:ok, resp} = rpost(@oikos <> "/analysis/repository", %{ - repo_url: "https://github.com/unknown-org-xyz/nonexistent-repo-12345", - include_dependencies: false - }) + {:ok, resp} = + rpost(@oikos <> "/analysis/repository", %{ + repo_url: "https://github.com/unknown-org-xyz/nonexistent-repo-12345", + include_dependencies: false + }) + assert resp.status in [200, 404, 422] end end diff --git a/opsm_ex/test/integration/trust_pipeline_test.exs b/opsm_ex/test/integration/trust_pipeline_test.exs index 09eb6f0e..86e789e4 100644 --- a/opsm_ex/test/integration/trust_pipeline_test.exs +++ b/opsm_ex/test/integration/trust_pipeline_test.exs @@ -4,6 +4,7 @@ defmodule Opsm.Integration.TrustPipelineTest do use ExUnit.Case, async: false alias Opsm.Clients.{CheckyMonkey, Palimpsest, Oikos} + alias Opsm.Types.{ ServiceConfig, HttpConfig, @@ -72,7 +73,7 @@ defmodule Opsm.Integration.TrustPipelineTest do client = CheckyMonkey.new(configs.checky_monkey, configs.http) request_id = "test-request-123" - case CheckyMonkey.get_verification_status(client,request_id) do + case CheckyMonkey.get_verification_status(client, request_id) do {:ok, response} -> assert response.status in [:queued, :running, :completed, :failed] @@ -90,7 +91,7 @@ defmodule Opsm.Integration.TrustPipelineTest do client = CheckyMonkey.new(configs.checky_monkey, %{configs.http | timeout_ms: 100}) # Poll for non-existent request - case CheckyMonkey.get_verification_status(client,"nonexistent") do + case CheckyMonkey.get_verification_status(client, "nonexistent") do {:error, _reason} -> # Expected - service unavailable or not found :ok @@ -239,19 +240,16 @@ defmodule Opsm.Integration.TrustPipelineTest do :attestation_invalid -> :hard_fail :checksum_mismatch -> :hard_fail :malware_detected -> :hard_fail - # SOFT_FAIL - Warn but allow with degraded trust :checky_monkey_timeout -> :soft_fail :oikos_unreachable -> :soft_fail :network_error -> :soft_fail :service_timeout -> :soft_fail - # WARNING - Inform user but proceed :low_sustainability_score -> :warning :dev_dependency_issue -> :warning :optional_dep_unavailable -> :warning :missing_documentation -> :warning - _ -> :unknown end end diff --git a/opsm_ex/test/opsm/config_test.exs b/opsm_ex/test/opsm/config_test.exs index a6514366..695e2e6b 100644 --- a/opsm_ex/test/opsm/config_test.exs +++ b/opsm_ex/test/opsm/config_test.exs @@ -64,8 +64,10 @@ defmodule Opsm.ConfigTest do # Should return error (unless config exists in default locations) case result do - {:ok, _} -> assert true # Config found in default location - {:error, _} -> assert true # No config found (expected) + # Config found in default location + {:ok, _} -> assert true + # No config found (expected) + {:error, _} -> assert true end end end diff --git a/opsm_ex/test/opsm/crypto/api_key_storage_test.exs b/opsm_ex/test/opsm/crypto/api_key_storage_test.exs index 0f9ad19d..e9febec6 100644 --- a/opsm_ex/test/opsm/crypto/api_key_storage_test.exs +++ b/opsm_ex/test/opsm/crypto/api_key_storage_test.exs @@ -18,7 +18,8 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do describe "generate_master_key/0" do test "generates 256-bit master key" do master_key = ApiKeyStorage.generate_master_key() - assert byte_size(master_key) == 32 # 256 bits + # 256 bits + assert byte_size(master_key) == 32 end test "generates different keys each time" do @@ -74,7 +75,8 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do {:ok, hash1} = ApiKeyStorage.hash_key(api_key) {:ok, hash2} = ApiKeyStorage.hash_key(api_key) - assert hash1 != hash2 # Different salts + # Different salts + assert hash1 != hash2 assert :ok = ApiKeyStorage.verify_key(api_key, hash1) assert :ok = ApiKeyStorage.verify_key(api_key, hash2) end @@ -85,14 +87,16 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do master_key = ApiKeyStorage.generate_master_key() api_key = "my-secret-api-key" - {:ok, key_id} = ApiKeyStorage.store_key( - api_key, - master_key, - service: "github", - storage_path: storage_path - ) + {:ok, key_id} = + ApiKeyStorage.store_key( + api_key, + master_key, + service: "github", + storage_path: storage_path + ) - {:ok, retrieved} = ApiKeyStorage.retrieve_key(key_id, master_key, storage_path: storage_path) + {:ok, retrieved} = + ApiKeyStorage.retrieve_key(key_id, master_key, storage_path: storage_path) assert retrieved == api_key end @@ -100,24 +104,29 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do test "stores multiple API keys", %{storage_path: storage_path} do master_key = ApiKeyStorage.generate_master_key() - {:ok, key_id1} = ApiKeyStorage.store_key( - "github-key", - master_key, - service: "github", - storage_path: storage_path - ) - - {:ok, key_id2} = ApiKeyStorage.store_key( - "gitlab-key", - master_key, - service: "gitlab", - storage_path: storage_path - ) + {:ok, key_id1} = + ApiKeyStorage.store_key( + "github-key", + master_key, + service: "github", + storage_path: storage_path + ) + + {:ok, key_id2} = + ApiKeyStorage.store_key( + "gitlab-key", + master_key, + service: "gitlab", + storage_path: storage_path + ) assert key_id1 != key_id2 - {:ok, retrieved1} = ApiKeyStorage.retrieve_key(key_id1, master_key, storage_path: storage_path) - {:ok, retrieved2} = ApiKeyStorage.retrieve_key(key_id2, master_key, storage_path: storage_path) + {:ok, retrieved1} = + ApiKeyStorage.retrieve_key(key_id1, master_key, storage_path: storage_path) + + {:ok, retrieved2} = + ApiKeyStorage.retrieve_key(key_id2, master_key, storage_path: storage_path) assert retrieved1 == "github-key" assert retrieved2 == "gitlab-key" @@ -127,23 +136,26 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do correct_key = ApiKeyStorage.generate_master_key() wrong_key = ApiKeyStorage.generate_master_key() - {:ok, key_id} = ApiKeyStorage.store_key( - "secret", - correct_key, - storage_path: storage_path - ) + {:ok, key_id} = + ApiKeyStorage.store_key( + "secret", + correct_key, + storage_path: storage_path + ) - assert {:error, _} = ApiKeyStorage.retrieve_key(key_id, wrong_key, storage_path: storage_path) + assert {:error, _} = + ApiKeyStorage.retrieve_key(key_id, wrong_key, storage_path: storage_path) end test "fails to retrieve non-existent key", %{storage_path: storage_path} do master_key = ApiKeyStorage.generate_master_key() - assert {:error, "API key not found"} = ApiKeyStorage.retrieve_key( - "nonexistent-id", - master_key, - storage_path: storage_path - ) + assert {:error, "API key not found"} = + ApiKeyStorage.retrieve_key( + "nonexistent-id", + master_key, + storage_path: storage_path + ) end end @@ -152,14 +164,17 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do master_key = ApiKeyStorage.generate_master_key() expires_at = DateTime.utc_now() |> DateTime.add(3600, :second) - {:ok, key_id} = ApiKeyStorage.store_key( - "expiring-key", - master_key, - expires_at: expires_at, - storage_path: storage_path - ) + {:ok, key_id} = + ApiKeyStorage.store_key( + "expiring-key", + master_key, + expires_at: expires_at, + storage_path: storage_path + ) + + {:ok, retrieved} = + ApiKeyStorage.retrieve_key(key_id, master_key, storage_path: storage_path) - {:ok, retrieved} = ApiKeyStorage.retrieve_key(key_id, master_key, storage_path: storage_path) assert retrieved == "expiring-key" end @@ -168,30 +183,35 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do # Expire 1 hour ago expires_at = DateTime.utc_now() |> DateTime.add(-3600, :second) - {:ok, key_id} = ApiKeyStorage.store_key( - "expired-key", - master_key, - expires_at: expires_at, - storage_path: storage_path - ) - - assert {:error, "API key has expired"} = ApiKeyStorage.retrieve_key( - key_id, - master_key, - storage_path: storage_path - ) + {:ok, key_id} = + ApiKeyStorage.store_key( + "expired-key", + master_key, + expires_at: expires_at, + storage_path: storage_path + ) + + assert {:error, "API key has expired"} = + ApiKeyStorage.retrieve_key( + key_id, + master_key, + storage_path: storage_path + ) end test "allows keys without expiration", %{storage_path: storage_path} do master_key = ApiKeyStorage.generate_master_key() - {:ok, key_id} = ApiKeyStorage.store_key( - "no-expiry", - master_key, - storage_path: storage_path - ) + {:ok, key_id} = + ApiKeyStorage.store_key( + "no-expiry", + master_key, + storage_path: storage_path + ) + + {:ok, retrieved} = + ApiKeyStorage.retrieve_key(key_id, master_key, storage_path: storage_path) - {:ok, retrieved} = ApiKeyStorage.retrieve_key(key_id, master_key, storage_path: storage_path) assert retrieved == "no-expiry" end end @@ -200,19 +220,21 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do test "deletes API key", %{storage_path: storage_path} do master_key = ApiKeyStorage.generate_master_key() - {:ok, key_id} = ApiKeyStorage.store_key( - "to-delete", - master_key, - storage_path: storage_path - ) + {:ok, key_id} = + ApiKeyStorage.store_key( + "to-delete", + master_key, + storage_path: storage_path + ) assert :ok = ApiKeyStorage.delete_key(key_id, storage_path: storage_path) - assert {:error, "API key not found"} = ApiKeyStorage.retrieve_key( - key_id, - master_key, - storage_path: storage_path - ) + assert {:error, "API key not found"} = + ApiKeyStorage.retrieve_key( + key_id, + master_key, + storage_path: storage_path + ) end test "deleting non-existent key succeeds", %{storage_path: storage_path} do @@ -227,8 +249,11 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do assert :ok = ApiKeyStorage.delete_key(key_id1, storage_path: storage_path) - assert {:error, _} = ApiKeyStorage.retrieve_key(key_id1, master_key, storage_path: storage_path) - assert {:ok, "key2"} = ApiKeyStorage.retrieve_key(key_id2, master_key, storage_path: storage_path) + assert {:error, _} = + ApiKeyStorage.retrieve_key(key_id1, master_key, storage_path: storage_path) + + assert {:ok, "key2"} = + ApiKeyStorage.retrieve_key(key_id2, master_key, storage_path: storage_path) end end @@ -236,19 +261,21 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do test "lists all stored keys metadata", %{storage_path: storage_path} do master_key = ApiKeyStorage.generate_master_key() - {:ok, _} = ApiKeyStorage.store_key( - "github-key", - master_key, - service: "github", - storage_path: storage_path - ) - - {:ok, _} = ApiKeyStorage.store_key( - "gitlab-key", - master_key, - service: "gitlab", - storage_path: storage_path - ) + {:ok, _} = + ApiKeyStorage.store_key( + "github-key", + master_key, + service: "github", + storage_path: storage_path + ) + + {:ok, _} = + ApiKeyStorage.store_key( + "gitlab-key", + master_key, + service: "gitlab", + storage_path: storage_path + ) keys = ApiKeyStorage.list_keys(storage_path: storage_path) @@ -274,12 +301,13 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do master_key = ApiKeyStorage.generate_master_key() expires_at = DateTime.utc_now() |> DateTime.add(-3600, :second) - {:ok, _} = ApiKeyStorage.store_key( - "expired", - master_key, - expires_at: expires_at, - storage_path: storage_path - ) + {:ok, _} = + ApiKeyStorage.store_key( + "expired", + master_key, + expires_at: expires_at, + storage_path: storage_path + ) keys = ApiKeyStorage.list_keys(storage_path: storage_path) assert length(keys) == 1 @@ -292,25 +320,28 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do master_key = ApiKeyStorage.generate_master_key() api_key = "super-secret-key" - {:ok, _key_id} = ApiKeyStorage.store_key( - api_key, - master_key, - storage_path: storage_path - ) + {:ok, _key_id} = + ApiKeyStorage.store_key( + api_key, + master_key, + storage_path: storage_path + ) # Read raw storage file {:ok, content} = File.read(storage_path) - refute String.contains?(content, api_key) # Plaintext not exposed + # Plaintext not exposed + refute String.contains?(content, api_key) end test "storage file has restrictive permissions", %{storage_path: storage_path} do master_key = ApiKeyStorage.generate_master_key() - {:ok, _} = ApiKeyStorage.store_key( - "test-key", - master_key, - storage_path: storage_path - ) + {:ok, _} = + ApiKeyStorage.store_key( + "test-key", + master_key, + storage_path: storage_path + ) # Check file permissions (0600 = owner read/write only) stat = File.stat!(storage_path) @@ -321,22 +352,26 @@ defmodule Opsm.Crypto.ApiKeyStorageTest do assert perms == 0o600 end - test "service context isolation (different encryption contexts)", %{storage_path: storage_path} do + test "service context isolation (different encryption contexts)", %{ + storage_path: storage_path + } do master_key = ApiKeyStorage.generate_master_key() - {:ok, key_id1} = ApiKeyStorage.store_key( - "same-key", - master_key, - service: "service1", - storage_path: storage_path - ) - - {:ok, key_id2} = ApiKeyStorage.store_key( - "same-key", - master_key, - service: "service2", - storage_path: storage_path - ) + {:ok, key_id1} = + ApiKeyStorage.store_key( + "same-key", + master_key, + service: "service1", + storage_path: storage_path + ) + + {:ok, key_id2} = + ApiKeyStorage.store_key( + "same-key", + master_key, + service: "service2", + storage_path: storage_path + ) # Read raw storage {:ok, content} = File.read(storage_path) diff --git a/opsm_ex/test/opsm/crypto/hash_test.exs b/opsm_ex/test/opsm/crypto/hash_test.exs index a224272a..3be86def 100644 --- a/opsm_ex/test/opsm/crypto/hash_test.exs +++ b/opsm_ex/test/opsm/crypto/hash_test.exs @@ -10,7 +10,8 @@ defmodule Opsm.Crypto.HashTest do hash = Hash.hash_hot(data) assert is_binary(hash) - assert String.length(hash) == 128 # 512 bits = 64 bytes = 128 hex chars + # 512 bits = 64 bytes = 128 hex chars + assert String.length(hash) == 128 end test "produces deterministic hashes" do @@ -47,7 +48,8 @@ defmodule Opsm.Crypto.HashTest do hash = Hash.hash_cold(data) assert is_binary(hash) - assert String.length(hash) == 128 # 512 bits = 64 bytes = 128 hex chars + # 512 bits = 64 bytes = 128 hex chars + assert String.length(hash) == 128 end test "produces deterministic hashes" do @@ -131,7 +133,8 @@ defmodule Opsm.Crypto.HashTest do test "avalanche effect (single bit change -> significant hash change)" do data1 = "test" - data2 = "tesa" # Single character different + # Single character different + data2 = "tesa" hash1_hot = Hash.hash_hot(data1) hash2_hot = Hash.hash_hot(data2) diff --git a/opsm_ex/test/opsm/crypto/password_test.exs b/opsm_ex/test/opsm/crypto/password_test.exs index 2a07bb2f..383ec8b5 100644 --- a/opsm_ex/test/opsm/crypto/password_test.exs +++ b/opsm_ex/test/opsm/crypto/password_test.exs @@ -16,7 +16,8 @@ defmodule Opsm.Crypto.PasswordTest do secret = "test-secret" {:ok, hash1} = Password.hash(secret) {:ok, hash2} = Password.hash(secret) - assert hash1 != hash2 # Different salts + # Different salts + assert hash1 != hash2 end test "uses Argon2id with correct parameters" do @@ -24,9 +25,12 @@ defmodule Opsm.Crypto.PasswordTest do # Verify hash format includes parameter info assert String.starts_with?(hash, "$argon2id$") - assert String.contains?(hash, "m=524288") # 512 MiB - assert String.contains?(hash, "t=8") # 8 iterations - assert String.contains?(hash, "p=4") # 4 lanes + # 512 MiB + assert String.contains?(hash, "m=524288") + # 8 iterations + assert String.contains?(hash, "t=8") + # 4 lanes + assert String.contains?(hash, "p=4") end end diff --git a/opsm_ex/test/opsm/crypto/post_quantum_test.exs b/opsm_ex/test/opsm/crypto/post_quantum_test.exs index 712add59..f61cc709 100644 --- a/opsm_ex/test/opsm/crypto/post_quantum_test.exs +++ b/opsm_ex/test/opsm/crypto/post_quantum_test.exs @@ -33,19 +33,19 @@ defmodule Opsm.Crypto.PostQuantumTest do end test "signature algorithms have correct key sizes" do - dilithium = Enum.find(PostQuantum.algorithms(), & &1.name == :dilithium5) + dilithium = Enum.find(PostQuantum.algorithms(), &(&1.name == :dilithium5)) assert dilithium.pk_bytes == 2592 assert dilithium.sk_bytes == 4896 assert dilithium.sig_bytes == 4627 - sphincs = Enum.find(PostQuantum.algorithms(), & &1.name == :sphincs_plus) + sphincs = Enum.find(PostQuantum.algorithms(), &(&1.name == :sphincs_plus)) assert sphincs.pk_bytes == 64 assert sphincs.sk_bytes == 128 assert sphincs.sig_bytes == 49_856 end test "KEM algorithm has correct sizes" do - kyber = Enum.find(PostQuantum.algorithms(), & &1.name == :kyber1024) + kyber = Enum.find(PostQuantum.algorithms(), &(&1.name == :kyber1024)) assert kyber.pk_bytes == 1568 assert kyber.sk_bytes == 3168 assert kyber.ct_bytes == 1568 diff --git a/opsm_ex/test/opsm/crypto/symmetric_test.exs b/opsm_ex/test/opsm/crypto/symmetric_test.exs index e273380e..086e8e7b 100644 --- a/opsm_ex/test/opsm/crypto/symmetric_test.exs +++ b/opsm_ex/test/opsm/crypto/symmetric_test.exs @@ -68,7 +68,8 @@ defmodule Opsm.Crypto.SymmetricTest do describe "generate_key/0" do test "generates 256-bit keys" do key = Symmetric.generate_key() - assert byte_size(key) == 32 # 256 bits + # 256 bits + assert byte_size(key) == 32 end test "generates different keys each time" do @@ -80,7 +81,8 @@ defmodule Opsm.Crypto.SymmetricTest do describe "validation" do test "rejects invalid key size for encryption" do - invalid_key = :crypto.strong_rand_bytes(16) # 128 bits, too small + # 128 bits, too small + invalid_key = :crypto.strong_rand_bytes(16) plaintext = "data" assert {:error, "Key must be 256 bits (32 bytes)"} = diff --git a/opsm_ex/test/opsm/federation/system_query_test.exs b/opsm_ex/test/opsm/federation/system_query_test.exs index 368085a8..87a15f0f 100644 --- a/opsm_ex/test/opsm/federation/system_query_test.exs +++ b/opsm_ex/test/opsm/federation/system_query_test.exs @@ -49,7 +49,11 @@ defmodule Opsm.Federation.SystemQueryTest do describe "query_version/2" do test "returns :not_installed for unknown package" do if System.find_executable("rpm") do - assert {:error, :not_installed} = SystemQuery.query_version(:rpm, "nonexistent-package-xyz-#{:rand.uniform(100_000)}") + assert {:error, :not_installed} = + SystemQuery.query_version( + :rpm, + "nonexistent-package-xyz-#{:rand.uniform(100_000)}" + ) end end end diff --git a/opsm_ex/test/opsm/fuzz_harness_test.exs b/opsm_ex/test/opsm/fuzz_harness_test.exs index ca00afaf..8f935af3 100644 --- a/opsm_ex/test/opsm/fuzz_harness_test.exs +++ b/opsm_ex/test/opsm/fuzz_harness_test.exs @@ -22,9 +22,11 @@ defmodule Opsm.FuzzHarnessTest do # --------------------------------------------------------------------------- defp gen_semver do - gen all major <- StreamData.integer(0..99), - minor <- StreamData.integer(0..99), - patch <- StreamData.integer(0..99) do + gen all( + major <- StreamData.integer(0..99), + minor <- StreamData.integer(0..99), + patch <- StreamData.integer(0..99) + ) do "#{major}.#{minor}.#{patch}" end end @@ -35,14 +37,17 @@ defmodule Opsm.FuzzHarnessTest do StreamData.string(:ascii, min_length: 1, max_length: 20), StreamData.constant(""), StreamData.constant("latest"), - StreamData.constant("*"), + StreamData.constant("*") ]) end defp gen_constraint_string do operators = ["^", "~>", ">=", "<=", ">", "<", "=", "!=", "~"] - gen all op <- StreamData.member_of(operators), - ver <- gen_semver() do + + gen all( + op <- StreamData.member_of(operators), + ver <- gen_semver() + ) do op <> ver end end @@ -53,7 +58,7 @@ defmodule Opsm.FuzzHarnessTest do StreamData.string(:ascii, min_length: 0, max_length: 30), StreamData.constant(""), StreamData.constant("!@#$%"), - StreamData.constant(">>>invalid<<<"), + StreamData.constant(">>>invalid<<<") ]) end @@ -63,16 +68,22 @@ defmodule Opsm.FuzzHarnessTest do StreamData.string(:ascii, min_length: 1, max_length: 30), StreamData.constant(""), StreamData.constant("a/b"), - StreamData.constant("pkg@scope"), + StreamData.constant("pkg@scope") ]) end # Offline-safe adapters — work without network (git-fallback or always-empty) defp gen_registry_atom_offline do StreamData.member_of([ - :betlang, :ephapax, :phronesis, :tangle, - :wokelang, :lithoglyph, :quandledb, :nqc, - :totally_unknown_xyz_999, + :betlang, + :ephapax, + :phronesis, + :tangle, + :wokelang, + :lithoglyph, + :quandledb, + :nqc, + :totally_unknown_xyz_999 ]) end @@ -80,7 +91,7 @@ defmodule Opsm.FuzzHarnessTest do defp gen_registry_atom do StreamData.one_of([ gen_registry_atom_offline(), - StreamData.member_of([:npm, :hex, :crates, :pypi, :hf]), + StreamData.member_of([:npm, :hex, :crates, :pypi, :hf]) ]) end @@ -90,24 +101,37 @@ defmodule Opsm.FuzzHarnessTest do StreamData.constant("[package]\nname = \"foo\"\nversion = \"1.0\"\n"), StreamData.constant("{invalid toml ==="), StreamData.string(:utf8, min_length: 0, max_length: 200), - gen all k <- StreamData.string(:alphanumeric, min_length: 1, max_length: 10), - v <- StreamData.string(:alphanumeric, min_length: 0, max_length: 20) do + gen all( + k <- StreamData.string(:alphanumeric, min_length: 1, max_length: 10), + v <- StreamData.string(:alphanumeric, min_length: 0, max_length: 20) + ) do "[section]\n#{k} = \"#{v}\"\n" - end, + end ]) end defp gen_platform do StreamData.member_of([ - :linux_amd64, :linux_arm64, :darwin_amd64, :darwin_arm64, - :windows_amd64, :freebsd_amd64, :unknown_platform, + :linux_amd64, + :linux_arm64, + :darwin_amd64, + :darwin_arm64, + :windows_amd64, + :freebsd_amd64, + :unknown_platform ]) end defp gen_tool_name do StreamData.member_of([ - "zig", "golang", "nodejs", "julia", "dart", "nim", "kubectl", - "totally-unknown-tool-xyz", + "zig", + "golang", + "nodejs", + "julia", + "dart", + "nim", + "kubectl", + "totally-unknown-tool-xyz" ]) end @@ -125,16 +149,19 @@ defmodule Opsm.FuzzHarnessTest do # --------------------------------------------------------------------------- property "VersionConstraint.parse/2 never crashes on arbitrary input" do - check all constraint <- gen_arbitrary_constraint(), - scheme <- StreamData.member_of([:semver, :hex, :npm]) do + check all( + constraint <- gen_arbitrary_constraint(), + scheme <- StreamData.member_of([:semver, :hex, :npm]) + ) do result = VersionConstraint.parse(constraint, scheme) + assert match?({:ok, _}, result) or match?({:error, _}, result), "expected ok/error tuple for #{inspect(constraint)}, got #{inspect(result)}" end end property "VersionConstraint.parse/2 succeeds on well-formed semver constraints" do - check all constraint <- gen_constraint_string() do + check all(constraint <- gen_constraint_string()) do result = VersionConstraint.parse(constraint, :semver) # Well-formed constraints should parse successfully (or return a structured error) assert match?({:ok, _}, result) or match?({:error, _}, result) @@ -146,13 +173,17 @@ defmodule Opsm.FuzzHarnessTest do # --------------------------------------------------------------------------- property "VersionConstraint.satisfies?/2 always returns boolean" do - check all version <- gen_version_string(), - constraint <- gen_constraint_string() do + check all( + version <- gen_version_string(), + constraint <- gen_constraint_string() + ) do case VersionConstraint.parse(constraint, :semver) do {:ok, parsed} -> result = VersionConstraint.satisfies?(version, parsed) + assert is_boolean(result), "expected boolean for satisfies?(#{inspect(version)}, parsed), got #{inspect(result)}" + {:error, _} -> # Unparseable constraint — satisfies? is not called, skip :ok @@ -161,15 +192,19 @@ defmodule Opsm.FuzzHarnessTest do end property "VersionConstraint.satisfies?/2 stable on repeated calls with same inputs" do - check all version <- gen_semver(), - constraint <- gen_constraint_string() do + check all( + version <- gen_semver(), + constraint <- gen_constraint_string() + ) do case VersionConstraint.parse(constraint, :semver) do {:ok, parsed} -> # Deterministic: same inputs must produce same output r1 = VersionConstraint.satisfies?(version, parsed) r2 = VersionConstraint.satisfies?(version, parsed) assert r1 == r2, "satisfies? is non-deterministic for #{inspect(version)}" - {:error, _} -> :ok + + {:error, _} -> + :ok end end end @@ -179,9 +214,11 @@ defmodule Opsm.FuzzHarnessTest do # --------------------------------------------------------------------------- property "Lockfile.add_package/2 never crashes on arbitrary input" do - check all name <- gen_package_name(), - version <- gen_version_string(), - forth <- gen_registry_atom() do + check all( + name <- gen_package_name(), + version <- gen_version_string(), + forth <- gen_registry_atom() + ) do lockfile = Lockfile.new() result = Lockfile.add_package(lockfile, %{name: name, version: version, forth: forth}) # Must return a lockfile struct — not crash @@ -191,9 +228,11 @@ defmodule Opsm.FuzzHarnessTest do end property "Lockfile has_package?/3 is consistent with add_package/2" do - check all name <- StreamData.string(:alphanumeric, min_length: 1, max_length: 20), - version <- gen_semver(), - forth <- StreamData.member_of([:npm, :hex, :crates]) do + check all( + name <- StreamData.string(:alphanumeric, min_length: 1, max_length: 20), + version <- gen_semver(), + forth <- StreamData.member_of([:npm, :hex, :crates]) + ) do lockfile = Lockfile.new() |> Lockfile.add_package(%{name: name, version: version, forth: forth}) @@ -204,10 +243,13 @@ defmodule Opsm.FuzzHarnessTest do end property "Lockfile is idempotent: adding same package twice keeps it present" do - check all name <- StreamData.string(:alphanumeric, min_length: 1, max_length: 20), - version <- gen_semver(), - forth <- StreamData.member_of([:npm, :hex, :crates]) do + check all( + name <- StreamData.string(:alphanumeric, min_length: 1, max_length: 20), + version <- gen_semver(), + forth <- StreamData.member_of([:npm, :hex, :crates]) + ) do pkg = %{name: name, version: version, forth: forth} + lockfile = Lockfile.new() |> Lockfile.add_package(pkg) @@ -222,16 +264,19 @@ defmodule Opsm.FuzzHarnessTest do # --------------------------------------------------------------------------- property "Toml.decode/1 never crashes on arbitrary input" do - check all input <- gen_toml_string() do + check all(input <- gen_toml_string()) do result = Toml.decode(input) + assert match?({:ok, _}, result) or match?({:error, _}, result), "Toml.decode crashed on #{inspect(input)}" end end property "Toml.decode/1 returns ok map for valid TOML section headers" do - check all key <- StreamData.string(:alphanumeric, min_length: 1, max_length: 10), - val <- StreamData.string(:alphanumeric, min_length: 0, max_length: 20) do + check all( + key <- StreamData.string(:alphanumeric, min_length: 1, max_length: 10), + val <- StreamData.string(:alphanumeric, min_length: 0, max_length: 20) + ) do toml = "[section]\n#{key} = \"#{val}\"\n" result = Toml.decode(toml) # Valid TOML must parse to ok @@ -246,9 +291,12 @@ defmodule Opsm.FuzzHarnessTest do # Offline-only: uses adapters that don't make network requests property "Registry.search/3 never crashes on offline adapters with arbitrary queries" do - check all forth <- gen_registry_atom_offline(), - query <- StreamData.string(:alphanumeric, min_length: 0, max_length: 50) do + check all( + forth <- gen_registry_atom_offline(), + query <- StreamData.string(:alphanumeric, min_length: 0, max_length: 50) + ) do result = Registry.search(forth, query, []) + assert match?({:ok, _}, result) or match?({:error, _}, result), "Registry.search crashed for #{inspect(forth)}/#{inspect(query)}" end @@ -256,18 +304,24 @@ defmodule Opsm.FuzzHarnessTest do @tag :external_api property "Registry.search/3 never crashes on all adapters including live-network ones" do - check all forth <- gen_registry_atom(), - query <- StreamData.string(:alphanumeric, min_length: 0, max_length: 50) do + check all( + forth <- gen_registry_atom(), + query <- StreamData.string(:alphanumeric, min_length: 0, max_length: 50) + ) do result = Registry.search(forth, query, []) + assert match?({:ok, _}, result) or match?({:error, _}, result), "Registry.search crashed for #{inspect(forth)}/#{inspect(query)}" end end property "Registry.exists?/2 always returns boolean (offline adapters)" do - check all forth <- gen_registry_atom_offline(), - name <- gen_package_name() do + check all( + forth <- gen_registry_atom_offline(), + name <- gen_package_name() + ) do result = Registry.exists?(forth, name) + assert is_boolean(result), "Registry.exists? returned non-boolean #{inspect(result)} for #{forth}/#{name}" end @@ -278,22 +332,28 @@ defmodule Opsm.FuzzHarnessTest do # --------------------------------------------------------------------------- property "UrlHandler.archive_url/4 never crashes on arbitrary tool/version/platform" do - check all tool <- gen_tool_name(), - version <- gen_version_string(), - platform <- gen_platform() do + check all( + tool <- gen_tool_name(), + version <- gen_version_string(), + platform <- gen_platform() + ) do result = UrlHandler.archive_url(tool, version, platform, gen_url_handler()) + assert match?({:ok, _}, result) or match?({:error, _}, result), "archive_url crashed for #{tool}/#{version}/#{platform}" end end property "UrlHandler.archive_url/4 returns url string on success" do - check all version <- gen_semver(), - platform <- StreamData.member_of([:linux_amd64, :linux_arm64, :darwin_amd64]) do + check all( + version <- gen_semver(), + platform <- StreamData.member_of([:linux_amd64, :linux_arm64, :darwin_amd64]) + ) do case UrlHandler.archive_url("zig", version, platform, gen_url_handler()) do {:ok, url} -> assert is_binary(url) and String.starts_with?(url, "https://"), "expected https:// url, got #{inspect(url)}" + {:error, _} -> :ok end diff --git a/opsm_ex/test/opsm/git/build_detector_test.exs b/opsm_ex/test/opsm/git/build_detector_test.exs index 469c7916..6c316c26 100644 --- a/opsm_ex/test/opsm/git/build_detector_test.exs +++ b/opsm_ex/test/opsm/git/build_detector_test.exs @@ -75,7 +75,9 @@ defmodule Opsm.Git.BuildDetectorTest do end test "returns error for non-existent directory" do - assert {:error, msg} = BuildDetector.detect("/tmp/nonexistent_dir_#{:rand.uniform(100_000)}") + assert {:error, msg} = + BuildDetector.detect("/tmp/nonexistent_dir_#{:rand.uniform(100_000)}") + assert msg =~ "Not a directory" end end diff --git a/opsm_ex/test/opsm/git/pipeline_test.exs b/opsm_ex/test/opsm/git/pipeline_test.exs index e029dde7..0c8dd515 100644 --- a/opsm_ex/test/opsm/git/pipeline_test.exs +++ b/opsm_ex/test/opsm/git/pipeline_test.exs @@ -7,7 +7,9 @@ defmodule Opsm.Git.PipelineTest do describe "from_local/2" do test "returns error for non-directory path" do - assert {:error, msg} = Pipeline.from_local("/tmp/nonexistent_path_#{:rand.uniform(100_000)}") + assert {:error, msg} = + Pipeline.from_local("/tmp/nonexistent_path_#{:rand.uniform(100_000)}") + assert msg =~ "Not a directory" end @@ -23,19 +25,23 @@ defmodule Opsm.Git.PipelineTest do test "detects build system in local checkout" do dir = Path.join(System.tmp_dir!(), "opsm_pipeline_mix_#{:rand.uniform(100_000)}") File.mkdir_p!(dir) + File.write!(Path.join(dir, "mix.exs"), """ defmodule Test.MixProject do use Mix.Project def project, do: [app: :test, version: "0.0.1"] end """) + on_exit(fn -> File.rm_rf!(dir) end) # This will detect mix but fail at deps.get (no hex config) — that's expected result = Pipeline.from_local(dir, skip_deps: true) case result do - {:ok, %{build_system: :mix}} -> assert true + {:ok, %{build_system: :mix}} -> + assert true + {:error, msg} -> # mix compile will fail in a minimal env, but detection should have worked assert msg =~ "mix" or msg =~ "failed" diff --git a/opsm_ex/test/opsm/har/web_scraper_test.exs b/opsm_ex/test/opsm/har/web_scraper_test.exs index 8abb2b1a..a418d127 100644 --- a/opsm_ex/test/opsm/har/web_scraper_test.exs +++ b/opsm_ex/test/opsm/har/web_scraper_test.exs @@ -48,7 +48,11 @@ defmodule Opsm.Har.WebScraperTest do test "builds task with all required fields" do task = %{ "task_id" => "test-003", - "package" => %{"name" => "nonexistent-pkg-zzz", "version" => "latest", "language" => "unknown"}, + "package" => %{ + "name" => "nonexistent-pkg-zzz", + "version" => "latest", + "language" => "unknown" + }, "hints" => %{} } diff --git a/opsm_ex/test/opsm/imp_property_test.exs b/opsm_ex/test/opsm/imp_property_test.exs index 601cb23b..b719e84d 100644 --- a/opsm_ex/test/opsm/imp_property_test.exs +++ b/opsm_ex/test/opsm/imp_property_test.exs @@ -8,7 +8,7 @@ defmodule Opsm.ImpPropertyTest do alias Opsm.Types.ManifestFormat property "IMP normalization succeeds for manifests with required fields" do - check all manifest <- manifest_with_dependencies() do + check all(manifest <- manifest_with_dependencies()) do digest = "sha256:" <> Base.encode16(:crypto.strong_rand_bytes(16), case: :lower) assert {:ok, normalized} = @@ -20,7 +20,7 @@ defmodule Opsm.ImpPropertyTest do end property "IMP normalization rejects missing required fields" do - check all manifest <- manifest_missing_license() do + check all(manifest <- manifest_missing_license()) do digest = "sha256:" <> Base.encode16(:crypto.strong_rand_bytes(8), case: :lower) assert {:error, message} = Imp.normalize(manifest, "/tmp/missing.json", digest) @@ -29,7 +29,7 @@ defmodule Opsm.ImpPropertyTest do end property "IMP normalization rejects manifests with zero dependencies" do - check all manifest <- manifest_without_dependencies() do + check all(manifest <- manifest_without_dependencies()) do digest = "sha256:" <> Base.encode16(:crypto.strong_rand_bytes(8), case: :lower) assert {:error, message} = Imp.normalize(manifest, "/tmp/nop.json", digest) @@ -38,10 +38,12 @@ defmodule Opsm.ImpPropertyTest do end defp manifest_with_dependencies do - gen all name <- string(:alphanumeric, min_length: 1), - version <- string(:alphanumeric, min_length: 1), - license <- string(:alphanumeric, min_length: 1), - dependency <- dependency_pair() do + gen all( + name <- string(:alphanumeric, min_length: 1), + version <- string(:alphanumeric, min_length: 1), + license <- string(:alphanumeric, min_length: 1), + dependency <- dependency_pair() + ) do %ManifestFormat{ name: name, version: version, @@ -52,8 +54,10 @@ defmodule Opsm.ImpPropertyTest do end defp manifest_missing_license do - gen all name <- string(:alphanumeric, min_length: 1), - version <- string(:alphanumeric, min_length: 1) do + gen all( + name <- string(:alphanumeric, min_length: 1), + version <- string(:alphanumeric, min_length: 1) + ) do %ManifestFormat{ name: name, version: version, @@ -64,9 +68,11 @@ defmodule Opsm.ImpPropertyTest do end defp manifest_without_dependencies do - gen all name <- string(:alphanumeric, min_length: 1), - version <- string(:alphanumeric, min_length: 1), - license <- string(:alphanumeric, min_length: 1) do + gen all( + name <- string(:alphanumeric, min_length: 1), + version <- string(:alphanumeric, min_length: 1), + license <- string(:alphanumeric, min_length: 1) + ) do %ManifestFormat{ name: name, version: version, @@ -77,8 +83,10 @@ defmodule Opsm.ImpPropertyTest do end defp dependency_pair do - gen all name <- string(:alphanumeric, min_length: 1), - version <- string(:alphanumeric, min_length: 1) do + gen all( + name <- string(:alphanumeric, min_length: 1), + version <- string(:alphanumeric, min_length: 1) + ) do %{name: name, version: version} end end diff --git a/opsm_ex/test/opsm/integration/manifest_roundtrip_test.exs b/opsm_ex/test/opsm/integration/manifest_roundtrip_test.exs index 7e258449..aa6eb922 100644 --- a/opsm_ex/test/opsm/integration/manifest_roundtrip_test.exs +++ b/opsm_ex/test/opsm/integration/manifest_roundtrip_test.exs @@ -68,13 +68,16 @@ defmodule Opsm.Integration.ManifestRoundtripTest do File.mkdir_p!(tmp) pkg_path = Path.join(tmp, "package.json") - File.write!(pkg_path, Jason.encode!(%{ - "name" => "test-pkg", - "version" => "2.0.0", - "description" => "A test package", - "license" => "MIT", - "dependencies" => %{"lodash" => "^4.17.0"} - })) + File.write!( + pkg_path, + Jason.encode!(%{ + "name" => "test-pkg", + "version" => "2.0.0", + "description" => "A test package", + "license" => "MIT", + "dependencies" => %{"lodash" => "^4.17.0"} + }) + ) result = Federation.convert_manifest(pkg_path) assert {:ok, manifest} = result @@ -93,12 +96,15 @@ defmodule Opsm.Integration.ManifestRoundtripTest do File.mkdir_p!(tmp) pkg_path = Path.join(tmp, "package.json") - File.write!(pkg_path, Jason.encode!(%{ - "name" => "dev-dep-pkg", - "version" => "1.0.0", - "dependencies" => %{"express" => "^4.18.0"}, - "devDependencies" => %{"jest" => "^29.0.0"} - })) + File.write!( + pkg_path, + Jason.encode!(%{ + "name" => "dev-dep-pkg", + "version" => "1.0.0", + "dependencies" => %{"express" => "^4.18.0"}, + "devDependencies" => %{"jest" => "^29.0.0"} + }) + ) result = Federation.convert_manifest(pkg_path) assert {:ok, manifest} = result @@ -113,14 +119,17 @@ defmodule Opsm.Integration.ManifestRoundtripTest do File.mkdir_p!(tmp) pkg_path = Path.join(tmp, "package.json") - File.write!(pkg_path, Jason.encode!(%{ - "name" => "repo-pkg", - "version" => "1.0.0", - "repository" => %{ - "type" => "git", - "url" => "https://github.com/example/repo-pkg.git" - } - })) + File.write!( + pkg_path, + Jason.encode!(%{ + "name" => "repo-pkg", + "version" => "1.0.0", + "repository" => %{ + "type" => "git", + "url" => "https://github.com/example/repo-pkg.git" + } + }) + ) result = Federation.convert_manifest(pkg_path) assert {:ok, manifest} = result @@ -177,6 +186,7 @@ defmodule Opsm.Integration.ManifestRoundtripTest do disabled_names = [:deb, :rpm, :winget, :choco, :scoop, :pacman, :homebrew, :nix, :guix] enabled_names = Enum.map(enabled, & &1.name) + Enum.each(disabled_names, fn name -> refute name in enabled_names, "#{name} should be disabled but was in enabled list" end) diff --git a/opsm_ex/test/opsm/integration/pq_trust_pipeline_test.exs b/opsm_ex/test/opsm/integration/pq_trust_pipeline_test.exs index 47e2e04b..03beaca6 100644 --- a/opsm_ex/test/opsm/integration/pq_trust_pipeline_test.exs +++ b/opsm_ex/test/opsm/integration/pq_trust_pipeline_test.exs @@ -188,11 +188,12 @@ defmodule Opsm.Integration.PqTrustPipelineTest do {:ok, sign_result} -> {:ok, decoded_pks} = HybridSignatures.decode_public_keys(sign_result.public_keys) - result = PqTrust.verify_package_signature( - "verify me", - sign_result.signature, - decoded_pks - ) + result = + PqTrust.verify_package_signature( + "verify me", + sign_result.signature, + decoded_pks + ) assert match?({:ok, %{status: status}} when status in [:verified, :partial], result) diff --git a/opsm_ex/test/opsm/integration/slsa_pipeline_test.exs b/opsm_ex/test/opsm/integration/slsa_pipeline_test.exs index 2817361b..ec24dcbf 100644 --- a/opsm_ex/test/opsm/integration/slsa_pipeline_test.exs +++ b/opsm_ex/test/opsm/integration/slsa_pipeline_test.exs @@ -91,14 +91,19 @@ defmodule Opsm.Integration.SlsaPipelineTest do test "provenance with custom build info" do pkg_info = %{name: "custom-build", version: "1.0.0", forth: :cargo, tarball_url: nil} + build_info = %{ builder_id: "https://custom-builder.example.com", started_at: "2026-01-01T00:00:00Z" } assert {:ok, statement} = Slsa.generate_provenance(pkg_info, build_info) - assert statement["predicate"]["runDetails"]["builder"]["id"] == "https://custom-builder.example.com" - assert statement["predicate"]["runDetails"]["metadata"]["startedOn"] == "2026-01-01T00:00:00Z" + + assert statement["predicate"]["runDetails"]["builder"]["id"] == + "https://custom-builder.example.com" + + assert statement["predicate"]["runDetails"]["metadata"]["startedOn"] == + "2026-01-01T00:00:00Z" end test "provenance includes resolved dependencies when provided" do @@ -169,10 +174,11 @@ defmodule Opsm.Integration.SlsaPipelineTest do pkg_info = %{name: "uri-pkg", version: "1.0.0", forth: :npm, tarball_url: nil} {:ok, statement} = Slsa.generate_provenance(pkg_info) - meta = Slsa.lockfile_metadata(%{ - statement: statement, - uri: "https://attestations.opsm.dev/abc123" - }) + meta = + Slsa.lockfile_metadata(%{ + statement: statement, + uri: "https://attestations.opsm.dev/abc123" + }) assert meta.slsa_provenance_uri == "https://attestations.opsm.dev/abc123" end diff --git a/opsm_ex/test/opsm/live/github_attestation_live_test.exs b/opsm_ex/test/opsm/live/github_attestation_live_test.exs index 963023a7..341a3a39 100644 --- a/opsm_ex/test/opsm/live/github_attestation_live_test.exs +++ b/opsm_ex/test/opsm/live/github_attestation_live_test.exs @@ -27,7 +27,9 @@ defmodule Opsm.Live.GithubAttestationLiveTest do defp attested_digest, do: System.get_env("OPSM_ATTESTED_DIGEST", @default_digest) test "attestations API returns records for an attested artifact digest" do - assert {:ok, records} = GithubAttestation.fetch_attestations(attested_repo(), attested_digest()) + assert {:ok, records} = + GithubAttestation.fetch_attestations(attested_repo(), attested_digest()) + assert records != [], "expected at least one attestation record" assert Enum.all?(records, &is_map/1) end @@ -41,7 +43,11 @@ defmodule Opsm.Live.GithubAttestationLiveTest do test "full chain: fetch, verify via gh, gate builder trust on verification" do # ~12MB download; needs `gh` on PATH with auth. Mirrors the manual # acceptance run for issue #56. - tmp = Path.join(System.tmp_dir!(), "opsm-attest-live-#{System.unique_integer([:positive])}.tar.gz") + tmp = + Path.join( + System.tmp_dir!(), + "opsm-attest-live-#{System.unique_integer([:positive])}.tar.gz" + ) try do url = "https://github.com/cli/cli/releases/download/v2.92.0/gh_2.92.0_linux_amd64.tar.gz" diff --git a/opsm_ex/test/opsm/lockfile_test.exs b/opsm_ex/test/opsm/lockfile_test.exs index 7647e017..8b2c7b6c 100644 --- a/opsm_ex/test/opsm/lockfile_test.exs +++ b/opsm_ex/test/opsm/lockfile_test.exs @@ -9,10 +9,12 @@ defmodule Opsm.LockfileTest do test "creates an empty lockfile" do lockfile = Lockfile.new() - assert lockfile.version == "2" # v2: Added crypto integration + # v2: Added crypto integration + assert lockfile.version == "2" assert lockfile.packages == %{} assert lockfile.generated_at != nil - assert lockfile.integrity_hash == nil # Not computed until write + # Not computed until write + assert lockfile.integrity_hash == nil assert lockfile.integrity_algo == "sha3-512" end end @@ -46,9 +48,10 @@ defmodule Opsm.LockfileTest do package1 = %{name: "pkg", version: "1.0.0", forth: :npm} package2 = %{name: "pkg", version: "2.0.0", forth: :npm} - updated = lockfile - |> Lockfile.add_package(package1) - |> Lockfile.add_package(package2) + updated = + lockfile + |> Lockfile.add_package(package1) + |> Lockfile.add_package(package2) entry = Lockfile.get_package(updated, "pkg", :npm) assert entry.version == "2.0.0" @@ -60,9 +63,10 @@ defmodule Opsm.LockfileTest do npm_pkg = %{name: "chalk", version: "5.0.0", forth: :npm} cargo_pkg = %{name: "serde", version: "1.0.0", forth: :cargo} - updated = lockfile - |> Lockfile.add_package(npm_pkg) - |> Lockfile.add_package(cargo_pkg) + updated = + lockfile + |> Lockfile.add_package(npm_pkg) + |> Lockfile.add_package(cargo_pkg) assert Lockfile.has_package?(updated, "chalk", :npm) assert Lockfile.has_package?(updated, "serde", :cargo) @@ -72,8 +76,9 @@ defmodule Opsm.LockfileTest do describe "remove_package/3" do test "removes a package from the lockfile" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) assert Lockfile.has_package?(lockfile, "pkg", :npm) @@ -92,10 +97,11 @@ defmodule Opsm.LockfileTest do describe "list_packages/1" do test "returns sorted list of packages" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "zebra", version: "1.0.0", forth: :npm}) - |> Lockfile.add_package(%{name: "alpha", version: "1.0.0", forth: :npm}) - |> Lockfile.add_package(%{name: "beta", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "zebra", version: "1.0.0", forth: :npm}) + |> Lockfile.add_package(%{name: "alpha", version: "1.0.0", forth: :npm}) + |> Lockfile.add_package(%{name: "beta", version: "1.0.0", forth: :npm}) packages = Lockfile.list_packages(lockfile) @@ -108,10 +114,11 @@ defmodule Opsm.LockfileTest do describe "packages_for_forth/2" do test "filters packages by forth" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "lodash", version: "4.0.0", forth: :npm}) - |> Lockfile.add_package(%{name: "serde", version: "1.0.0", forth: :cargo}) - |> Lockfile.add_package(%{name: "chalk", version: "5.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "lodash", version: "4.0.0", forth: :npm}) + |> Lockfile.add_package(%{name: "serde", version: "1.0.0", forth: :cargo}) + |> Lockfile.add_package(%{name: "chalk", version: "5.0.0", forth: :npm}) npm_packages = Lockfile.packages_for_forth(lockfile, :npm) @@ -122,15 +129,17 @@ defmodule Opsm.LockfileTest do describe "verify_package/4" do test "returns :ok when checksum matches" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc123"}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc123"}) assert :ok = Lockfile.verify_package(lockfile, "pkg", :npm, "abc123") end test "returns mismatch when checksum differs" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc123"}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc123"}) assert {:mismatch, details} = Lockfile.verify_package(lockfile, "pkg", :npm, "xyz789") assert details.expected == "abc123" @@ -138,8 +147,9 @@ defmodule Opsm.LockfileTest do end test "returns ok with warning when no checksum recorded" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) assert {:ok, :no_checksum_recorded} = Lockfile.verify_package(lockfile, "pkg", :npm, "any") end @@ -153,9 +163,10 @@ defmodule Opsm.LockfileTest do describe "check_sync/2" do test "reports in sync when matching" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) - |> Lockfile.add_package(%{name: "pkg2", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) + |> Lockfile.add_package(%{name: "pkg2", version: "1.0.0", forth: :npm}) installed = [ %{name: "pkg1", forth: :npm}, @@ -170,8 +181,9 @@ defmodule Opsm.LockfileTest do end test "reports packages missing from lockfile" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) installed = [ %{name: "pkg1", forth: :npm}, @@ -185,9 +197,10 @@ defmodule Opsm.LockfileTest do end test "reports packages not installed" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) - |> Lockfile.add_package(%{name: "pkg2", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) + |> Lockfile.add_package(%{name: "pkg2", version: "1.0.0", forth: :npm}) installed = [ %{name: "pkg1", forth: :npm} @@ -213,15 +226,16 @@ defmodule Opsm.LockfileTest do test "roundtrip write and read", %{tmp_dir: tmp_dir} do path = Path.join(tmp_dir, "opsm.lock") - original = Lockfile.new() - |> Lockfile.add_package(%{ - name: "lodash", - version: "4.17.21", - forth: :npm, - checksum: "abc123", - source_url: "https://example.com/lodash.tgz", - dependencies: ["dep1", "dep2"] - }) + original = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "lodash", + version: "4.17.21", + forth: :npm, + checksum: "abc123", + source_url: "https://example.com/lodash.tgz", + dependencies: ["dep1", "dep2"] + }) {:ok, ^path} = Lockfile.write(original, path) {:ok, loaded} = Lockfile.read(path) @@ -282,31 +296,36 @@ defmodule Opsm.LockfileTest do describe "compute_integrity_hash/1" do test "computes SHA3-512 integrity hash for lockfile" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc123"}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc123"}) lockfile_with_hash = Lockfile.compute_integrity_hash(lockfile) assert lockfile_with_hash.integrity_hash != nil - assert String.length(lockfile_with_hash.integrity_hash) == 128 # 512 bits = 128 hex chars + # 512 bits = 128 hex chars + assert String.length(lockfile_with_hash.integrity_hash) == 128 assert lockfile_with_hash.integrity_algo == "sha3-512" end test "produces different hashes for different lockfiles" do - lockfile1 = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) - |> Lockfile.compute_integrity_hash() + lockfile1 = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg1", version: "1.0.0", forth: :npm}) + |> Lockfile.compute_integrity_hash() - lockfile2 = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg2", version: "1.0.0", forth: :npm}) - |> Lockfile.compute_integrity_hash() + lockfile2 = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg2", version: "1.0.0", forth: :npm}) + |> Lockfile.compute_integrity_hash() assert lockfile1.integrity_hash != lockfile2.integrity_hash end test "produces same hash for same lockfile" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) hash1 = Lockfile.compute_integrity_hash(lockfile).integrity_hash hash2 = Lockfile.compute_integrity_hash(lockfile).integrity_hash @@ -317,17 +336,19 @@ defmodule Opsm.LockfileTest do describe "verify_integrity/1" do test "verifies valid integrity hash" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) - |> Lockfile.compute_integrity_hash() + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + |> Lockfile.compute_integrity_hash() assert :ok = Lockfile.verify_integrity(lockfile) end test "detects tampering (modified packages)" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) - |> Lockfile.compute_integrity_hash() + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + |> Lockfile.compute_integrity_hash() # Tamper with the lockfile (change version) tampered = Lockfile.add_package(lockfile, %{name: "pkg", version: "2.0.0", forth: :npm}) @@ -338,8 +359,10 @@ defmodule Opsm.LockfileTest do end test "allows lockfiles without integrity hash (backward compatibility)" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + # No integrity hash computed assert {:ok, :no_integrity_hash} = Lockfile.verify_integrity(lockfile) @@ -360,8 +383,9 @@ defmodule Opsm.LockfileTest do path = Path.join(tmp_dir, "opsm.lock") key = Opsm.Crypto.Symmetric.generate_key() - original = Lockfile.new() - |> Lockfile.add_package(%{name: "secret-pkg", version: "1.0.0", forth: :npm}) + original = + Lockfile.new() + |> Lockfile.add_package(%{name: "secret-pkg", version: "1.0.0", forth: :npm}) {:ok, ^path} = Lockfile.write(original, path, encrypt: true, key: key) {:ok, loaded} = Lockfile.read(path, decrypt: true, key: key, verify_integrity: false) @@ -375,8 +399,9 @@ defmodule Opsm.LockfileTest do path = Path.join(tmp_dir, "opsm.lock") key = Opsm.Crypto.Symmetric.generate_key() - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) {:ok, ^path} = Lockfile.write(lockfile, path, encrypt: true, key: key) @@ -389,8 +414,9 @@ defmodule Opsm.LockfileTest do correct_key = Opsm.Crypto.Symmetric.generate_key() wrong_key = Opsm.Crypto.Symmetric.generate_key() - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) {:ok, ^path} = Lockfile.write(lockfile, path, encrypt: true, key: correct_key) @@ -401,7 +427,9 @@ defmodule Opsm.LockfileTest do describe "write with integrity hash" do setup do - tmp_dir = Path.join(System.tmp_dir!(), "opsm_lockfile_integrity_#{:rand.uniform(1_000_000)}") + tmp_dir = + Path.join(System.tmp_dir!(), "opsm_lockfile_integrity_#{:rand.uniform(1_000_000)}") + File.mkdir_p!(tmp_dir) on_exit(fn -> File.rm_rf!(tmp_dir) end) @@ -412,8 +440,9 @@ defmodule Opsm.LockfileTest do test "write automatically computes integrity hash", %{tmp_dir: tmp_dir} do path = Path.join(tmp_dir, "opsm.lock") - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) {:ok, ^path} = Lockfile.write(lockfile, path) {:ok, loaded} = Lockfile.read(path) @@ -426,8 +455,9 @@ defmodule Opsm.LockfileTest do test "read detects tampered lockfile", %{tmp_dir: tmp_dir} do path = Path.join(tmp_dir, "opsm.lock") - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm}) {:ok, ^path} = Lockfile.write(lockfile, path) @@ -443,22 +473,25 @@ defmodule Opsm.LockfileTest do describe "BLAKE2b package checksums" do test "new packages default to BLAKE2b checksums" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc123"}) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{name: "pkg", version: "1.0.0", forth: :npm, checksum: "abc123"}) entry = Lockfile.get_package(lockfile, "pkg", :npm) - assert entry.checksum_algo == "blake2b" # v1.0.1: Default to BLAKE2b + # v1.0.1: Default to BLAKE2b + assert entry.checksum_algo == "blake2b" end test "can specify custom checksum algorithm" do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: "pkg", - version: "1.0.0", - forth: :npm, - checksum: "xyz789", - checksum_algo: "sha3-512" - }) + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: "pkg", + version: "1.0.0", + forth: :npm, + checksum: "xyz789", + checksum_algo: "sha3-512" + }) entry = Lockfile.get_package(lockfile, "pkg", :npm) assert entry.checksum_algo == "sha3-512" diff --git a/opsm_ex/test/opsm/maintenance_test.exs b/opsm_ex/test/opsm/maintenance_test.exs index 243626f5..d06007f5 100644 --- a/opsm_ex/test/opsm/maintenance_test.exs +++ b/opsm_ex/test/opsm/maintenance_test.exs @@ -59,7 +59,8 @@ defmodule Opsm.MaintenanceTest do id = Maintenance.record_history("install", %{"package" => "test-pkg", "version" => "1.0.0"}) assert is_binary(id) - assert String.length(id) == 16 # 8 bytes hex encoded + # 8 bytes hex encoded + assert String.length(id) == 16 end end @@ -175,8 +176,10 @@ defmodule Opsm.MaintenanceTest do case result do {:error, "No history to undo"} -> assert true - {:ok, _, _} -> assert true # Has history from other tests - {:error, _} -> assert true # Cannot undo some operations + # Has history from other tests + {:ok, _, _} -> assert true + # Cannot undo some operations + {:error, _} -> assert true end end end @@ -276,6 +279,7 @@ defmodule Opsm.MaintenanceTest do pin = Maintenance.get_pin(pkg) assert pin["version"] == "1.0.0" assert Maintenance.pinned?(pkg) + # upgrade_path itself would return {:error, :not_installed} for a package not in installed.json, # but the pin check logic is covered by pinned? + get_pin above end diff --git a/opsm_ex/test/opsm/manifest/writer_test.exs b/opsm_ex/test/opsm/manifest/writer_test.exs index 1e2729ec..58d5a6fc 100644 --- a/opsm_ex/test/opsm/manifest/writer_test.exs +++ b/opsm_ex/test/opsm/manifest/writer_test.exs @@ -133,7 +133,15 @@ defmodule Opsm.Manifest.WriterTest do describe "convert/2" do test "supports all documented targets" do - targets = [:package_json, :cargo_toml, :mix_exs, :pyproject_toml, :pubspec_yaml, :go_mod, :opsm_toml] + targets = [ + :package_json, + :cargo_toml, + :mix_exs, + :pyproject_toml, + :pubspec_yaml, + :go_mod, + :opsm_toml + ] for target <- targets do assert {:ok, result} = Writer.convert(@sample_manifest, target) diff --git a/opsm_ex/test/opsm/network/ipv6_test.exs b/opsm_ex/test/opsm/network/ipv6_test.exs index 54542a80..0e94cc10 100644 --- a/opsm_ex/test/opsm/network/ipv6_test.exs +++ b/opsm_ex/test/opsm/network/ipv6_test.exs @@ -21,7 +21,7 @@ defmodule Opsm.Network.Ipv6Test do describe "ipv6?/1" do test "identifies IPv6 tuples" do assert Ipv6.ipv6?({0, 0, 0, 0, 0, 0, 0, 1}) - assert Ipv6.ipv6?({0x2001, 0xdb8, 0, 0, 0, 0, 0, 1}) + assert Ipv6.ipv6?({0x2001, 0xDB8, 0, 0, 0, 0, 0, 1}) end test "rejects IPv4 tuples" do @@ -48,7 +48,7 @@ defmodule Opsm.Network.Ipv6Test do end test "passes through IPv6 addresses unchanged" do - addr = {0x2001, 0xdb8, 0, 0, 0, 0, 0, 1} + addr = {0x2001, 0xDB8, 0, 0, 0, 0, 0, 1} assert Ipv6.to_ipv6_mapped(addr) == addr end end diff --git a/opsm_ex/test/opsm/package/transaction_test.exs b/opsm_ex/test/opsm/package/transaction_test.exs index 5280bd8d..4aece23f 100644 --- a/opsm_ex/test/opsm/package/transaction_test.exs +++ b/opsm_ex/test/opsm/package/transaction_test.exs @@ -28,9 +28,10 @@ defmodule Opsm.Package.TransactionTest do end test "accumulates multiple directories" do - txn = Transaction.new("pkg") - |> Transaction.record_directory("/tmp/dir1") - |> Transaction.record_directory("/tmp/dir2") + txn = + Transaction.new("pkg") + |> Transaction.record_directory("/tmp/dir1") + |> Transaction.record_directory("/tmp/dir2") assert length(txn.directories) == 2 end @@ -56,8 +57,9 @@ defmodule Opsm.Package.TransactionTest do describe "mark_completed/1" do test "marks transaction as completed" do - txn = Transaction.new("pkg") - |> Transaction.complete() + txn = + Transaction.new("pkg") + |> Transaction.complete() assert txn.completed == true end @@ -130,8 +132,9 @@ defmodule Opsm.Package.TransactionTest do file_path = Path.join(tmp_dir, "created_file.txt") File.write!(file_path, "content") - txn = Transaction.new("pkg") - |> Transaction.record_file(file_path) + txn = + Transaction.new("pkg") + |> Transaction.record_file(file_path) assert File.exists?(file_path) @@ -147,8 +150,9 @@ defmodule Opsm.Package.TransactionTest do File.write!(source, "content") File.ln_s!(source, link) - txn = Transaction.new("pkg") - |> Transaction.record_symlink(link) + txn = + Transaction.new("pkg") + |> Transaction.record_symlink(link) assert File.exists?(link) @@ -163,8 +167,9 @@ defmodule Opsm.Package.TransactionTest do dir_path = Path.join(tmp_dir, "created_dir") File.mkdir_p!(dir_path) - txn = Transaction.new("pkg") - |> Transaction.record_directory(dir_path) + txn = + Transaction.new("pkg") + |> Transaction.record_directory(dir_path) assert File.dir?(dir_path) @@ -177,9 +182,10 @@ defmodule Opsm.Package.TransactionTest do file_path = Path.join(tmp_dir, "keep_file.txt") File.write!(file_path, "content") - txn = Transaction.new("pkg") - |> Transaction.record_file(file_path) - |> Transaction.complete() + txn = + Transaction.new("pkg") + |> Transaction.record_file(file_path) + |> Transaction.complete() Transaction.rollback(txn) diff --git a/opsm_ex/test/opsm/progress_test.exs b/opsm_ex/test/opsm/progress_test.exs index 5f557574..7ac60ddd 100644 --- a/opsm_ex/test/opsm/progress_test.exs +++ b/opsm_ex/test/opsm/progress_test.exs @@ -55,9 +55,10 @@ defmodule Opsm.ProgressTest do describe "complete_bar/1" do test "sets current to total" do - state = Progress.new_bar(100) - |> Progress.update_bar(50) - |> Progress.complete_bar() + state = + Progress.new_bar(100) + |> Progress.update_bar(50) + |> Progress.complete_bar() assert state.current == state.total end @@ -65,8 +66,9 @@ defmodule Opsm.ProgressTest do describe "render_bar/1" do test "returns string representation" do - state = Progress.new_bar(100) - |> Progress.update_bar(50) + state = + Progress.new_bar(100) + |> Progress.update_bar(50) output = Progress.render_bar(state) @@ -83,8 +85,9 @@ defmodule Opsm.ProgressTest do end test "shows 100% when complete" do - state = Progress.new_bar(100) - |> Progress.complete_bar() + state = + Progress.new_bar(100) + |> Progress.complete_bar() output = Progress.render_bar(state) @@ -157,10 +160,11 @@ defmodule Opsm.ProgressTest do end test "does not exceed total" do - state = Progress.new_steps(2) - |> Progress.next_step("Step 1") - |> Progress.next_step("Step 2") - |> Progress.next_step("Step 3") + state = + Progress.new_steps(2) + |> Progress.next_step("Step 1") + |> Progress.next_step("Step 2") + |> Progress.next_step("Step 3") assert state.current == 2 end diff --git a/opsm_ex/test/opsm/registries/hyperpolymath_forge_test.exs b/opsm_ex/test/opsm/registries/hyperpolymath_forge_test.exs index bf8a15c8..bf70f898 100644 --- a/opsm_ex/test/opsm/registries/hyperpolymath_forge_test.exs +++ b/opsm_ex/test/opsm/registries/hyperpolymath_forge_test.exs @@ -90,7 +90,9 @@ defmodule Opsm.Registries.HyperpPolymathForgeTest do describe "fetch_package/2" do test "returns error tuple for non-existent package" do - result = HyperpPolymathForge.fetch_package("xyz-definitely-not-a-hp-package-abc-999", "latest") + result = + HyperpPolymathForge.fetch_package("xyz-definitely-not-a-hp-package-abc-999", "latest") + assert match?({:error, _}, result) end @@ -100,9 +102,11 @@ defmodule Opsm.Registries.HyperpPolymathForgeTest do {:ok, pkg} -> assert is_map(pkg) assert pkg.forth == :hf + {:error, :not_found} -> # Acceptable if GitHub API unavailable in CI or cache miss :ok + {:error, _} -> :ok end @@ -137,48 +141,51 @@ defmodule Opsm.Registries.HyperpPolymathForgeTest do expiry = now + 60_000 entries = [ - {"opsm", %{ - type: :package, - pkg_name: "opsm", - repo_name: "odds-and-sods-package-manager", - default_branch: "main", - version: "2.0.0", - description: "Universal package manager", - license: "MPL-2.0", - authors: ["Jonathan D.A. Jewell "], - keywords: ["package", "manager"], - forth: :hyperpolymath, - repo_url: "https://github.com/hyperpolymath/odds-and-sods-package-manager", - raw_toml: %{} - }, expiry}, - {"affinescript-vite", %{ - type: :package, - pkg_name: "affinescript-vite", - repo_name: "affinescript-vite", - default_branch: "main", - version: "0.1.0", - description: "Vite plugin for AffineScript", - license: "MPL-2.0", - authors: ["Jonathan D.A. Jewell "], - keywords: ["vite", "affinescript", "bundler"], - forth: :affinescript, - repo_url: "https://github.com/hyperpolymath/affinescript-vite", - raw_toml: %{} - }, expiry}, - {"ephapax-core", %{ - type: :package, - pkg_name: "ephapax-core", - repo_name: "ephapax", - default_branch: "main", - version: "0.3.0", - description: "Linear type system for Elixir", - license: "MPL-2.0", - authors: ["Jonathan D.A. Jewell "], - keywords: ["types", "linear", "ephapax"], - forth: :ephapax, - repo_url: "https://github.com/hyperpolymath/ephapax", - raw_toml: %{} - }, expiry} + {"opsm", + %{ + type: :package, + pkg_name: "opsm", + repo_name: "odds-and-sods-package-manager", + default_branch: "main", + version: "2.0.0", + description: "Universal package manager", + license: "MPL-2.0", + authors: ["Jonathan D.A. Jewell "], + keywords: ["package", "manager"], + forth: :hyperpolymath, + repo_url: "https://github.com/hyperpolymath/odds-and-sods-package-manager", + raw_toml: %{} + }, expiry}, + {"affinescript-vite", + %{ + type: :package, + pkg_name: "affinescript-vite", + repo_name: "affinescript-vite", + default_branch: "main", + version: "0.1.0", + description: "Vite plugin for AffineScript", + license: "MPL-2.0", + authors: ["Jonathan D.A. Jewell "], + keywords: ["vite", "affinescript", "bundler"], + forth: :affinescript, + repo_url: "https://github.com/hyperpolymath/affinescript-vite", + raw_toml: %{} + }, expiry}, + {"ephapax-core", + %{ + type: :package, + pkg_name: "ephapax-core", + repo_name: "ephapax", + default_branch: "main", + version: "0.3.0", + description: "Linear type system for Elixir", + license: "MPL-2.0", + authors: ["Jonathan D.A. Jewell "], + keywords: ["types", "linear", "ephapax"], + forth: :ephapax, + repo_url: "https://github.com/hyperpolymath/ephapax", + raw_toml: %{} + }, expiry} ] Enum.each(entries, fn {name, entry, exp} -> @@ -244,6 +251,7 @@ defmodule Opsm.Registries.HyperpPolymathForgeTest do test "each result has required ResolvedPackage fields" do {:ok, results} = HyperpPolymathForge.search("", []) + for pkg <- results do assert is_binary(pkg.package) assert is_binary(pkg.version) @@ -260,13 +268,24 @@ defmodule Opsm.Registries.HyperpPolymathForgeTest do now = System.monotonic_time(:millisecond) expiry = now + 60_000 - :ets.insert(:hfr_cache, {"seed-pkg", %{ - type: :package, pkg_name: "seed-pkg", repo_name: "seed-repo", - default_branch: "main", version: "1.0.0", description: "seeded", - license: "MPL-2.0", authors: [], keywords: [], - forth: :hyperpolymath, repo_url: "https://github.com/hyperpolymath/seed-repo", - raw_toml: %{} - }, expiry}) + :ets.insert( + :hfr_cache, + {"seed-pkg", + %{ + type: :package, + pkg_name: "seed-pkg", + repo_name: "seed-repo", + default_branch: "main", + version: "1.0.0", + description: "seeded", + license: "MPL-2.0", + authors: [], + keywords: [], + forth: :hyperpolymath, + repo_url: "https://github.com/hyperpolymath/seed-repo", + raw_toml: %{} + }, expiry} + ) on_exit(fn -> if :ets.info(:hfr_cache) != :undefined do @@ -292,15 +311,24 @@ defmodule Opsm.Registries.HyperpPolymathForgeTest do now = System.monotonic_time(:millisecond) expiry = now + 60_000 - :ets.insert(:hfr_cache, {"my-seeded-pkg", %{ - type: :package, pkg_name: "my-seeded-pkg", repo_name: "my-seeded-repo", - default_branch: "main", version: "1.2.3", description: "a test package", - license: "MPL-2.0", - authors: ["Jonathan D.A. Jewell "], - keywords: ["test"], forth: :hyperpolymath, - repo_url: "https://github.com/hyperpolymath/my-seeded-repo", - raw_toml: %{"dependencies" => %{}, "dev-dependencies" => %{}} - }, expiry}) + :ets.insert( + :hfr_cache, + {"my-seeded-pkg", + %{ + type: :package, + pkg_name: "my-seeded-pkg", + repo_name: "my-seeded-repo", + default_branch: "main", + version: "1.2.3", + description: "a test package", + license: "MPL-2.0", + authors: ["Jonathan D.A. Jewell "], + keywords: ["test"], + forth: :hyperpolymath, + repo_url: "https://github.com/hyperpolymath/my-seeded-repo", + raw_toml: %{"dependencies" => %{}, "dev-dependencies" => %{}} + }, expiry} + ) on_exit(fn -> if :ets.info(:hfr_cache) != :undefined do @@ -320,7 +348,8 @@ defmodule Opsm.Registries.HyperpPolymathForgeTest do test "returns {:error, :not_found} for absent package (no network fallback)" do # Cache is non-empty (seed-pkg is there), so refresh_index won't fire - assert {:error, :not_found} = HyperpPolymathForge.fetch_package("xyz-not-seeded-abc-999", "latest") + assert {:error, :not_found} = + HyperpPolymathForge.fetch_package("xyz-not-seeded-abc-999", "latest") end test "resolved manifest has correct fields" do diff --git a/opsm_ex/test/opsm/registries/language_adapters_test.exs b/opsm_ex/test/opsm/registries/language_adapters_test.exs index b5c6662f..73a41866 100644 --- a/opsm_ex/test/opsm/registries/language_adapters_test.exs +++ b/opsm_ex/test/opsm/registries/language_adapters_test.exs @@ -12,14 +12,14 @@ defmodule Opsm.Registries.LanguageAdaptersTest do # All new adapters introduced in the first-class system sprint (2026-04-12) @adapters [ - {Opsm.Registries.Betlang, :betlang, "betlang-rt"}, - {Opsm.Registries.Ephapax, :ephapax, "ephapax-std"}, - {Opsm.Registries.Phronesis, :phronesis, "phronesis-core"}, - {Opsm.Registries.Tangle, :tangle, "tangle-std"}, - {Opsm.Registries.Wokelang, :wokelang, "wokelang-std"}, + {Opsm.Registries.Betlang, :betlang, "betlang-rt"}, + {Opsm.Registries.Ephapax, :ephapax, "ephapax-std"}, + {Opsm.Registries.Phronesis, :phronesis, "phronesis-core"}, + {Opsm.Registries.Tangle, :tangle, "tangle-std"}, + {Opsm.Registries.Wokelang, :wokelang, "wokelang-std"}, {Opsm.Registries.Lithoglyph, :lithoglyph, "lithoglyph-core"}, - {Opsm.Registries.QuandleDB, :quandledb, "quandledb-core"}, - {Opsm.Registries.Nqc, :nqc, "nqc-core"}, + {Opsm.Registries.QuandleDB, :quandledb, "quandledb-core"}, + {Opsm.Registries.Nqc, :nqc, "nqc-core"} ] # --------------------------------------------------------------------------- @@ -150,7 +150,10 @@ defmodule Opsm.Registries.LanguageAdaptersTest do @tag :external_api test "returns ok or error for a known package name" do - case unquote(mod).fetch_package(unquote(mod_name |> String.downcase() |> Kernel.<>("-core")), "latest") do + case unquote(mod).fetch_package( + unquote(mod_name |> String.downcase() |> Kernel.<>("-core")), + "latest" + ) do {:ok, pkg} -> assert is_map(pkg) or is_struct(pkg) {:error, _} -> :ok end @@ -194,7 +197,9 @@ defmodule Opsm.Registries.LanguageAdaptersTest do # atom was in the list. search_all is async so we filter to just these. @tag :skip test "search_all includes betlang, ephapax, phronesis, tangle, wokelang" do - result = Registry.search_all("test", forths: [:betlang, :ephapax, :phronesis, :tangle, :wokelang]) + result = + Registry.search_all("test", forths: [:betlang, :ephapax, :phronesis, :tangle, :wokelang]) + assert is_map(result) assert Map.has_key?(result, :betlang) assert Map.has_key?(result, :ephapax) diff --git a/opsm_ex/test/opsm/runtime/integration_test.exs b/opsm_ex/test/opsm/runtime/integration_test.exs index a7984687..c933b8a4 100644 --- a/opsm_ex/test/opsm/runtime/integration_test.exs +++ b/opsm_ex/test/opsm/runtime/integration_test.exs @@ -32,8 +32,7 @@ defmodule Opsm.Runtime.IntegrationTest do @golang_handler %{ "versions_url" => "https://go.dev/dl/?mode=json&include=all", "version_key_pattern" => "^go[0-9]+\\.[0-9]+", - "archive_url_template" => - "https://go.dev/dl/{{go_version}}.{{go_os}}-{{go_arch}}.tar.gz" + "archive_url_template" => "https://go.dev/dl/{{go_version}}.{{go_os}}-{{go_arch}}.tar.gz" } @nodejs_handler %{ @@ -44,7 +43,7 @@ defmodule Opsm.Runtime.IntegrationTest do } # Known stable versions used as anchors for URL validation tests - @zig_stable "0.13.0" + @zig_stable "0.13.0" @golang_stable "1.21.0" @nodejs_stable "v20.11.0" @@ -127,7 +126,9 @@ defmodule Opsm.Runtime.IntegrationTest do @tag :external_api test "constructed URL for golang stable has correct structure" do - assert {:ok, url} = UrlHandler.archive_url("golang", @golang_stable, :linux_amd64, @golang_handler) + assert {:ok, url} = + UrlHandler.archive_url("golang", @golang_stable, :linux_amd64, @golang_handler) + assert url =~ "go.dev/dl/go#{@golang_stable}.linux-amd64.tar.gz" end end @@ -167,6 +168,7 @@ defmodule Opsm.Runtime.IntegrationTest do {:ok, path} -> assert is_binary(path) assert String.contains?(path, "zig") + {:error, :not_installed} -> # Install may have been skipped if zig was already present — acceptable :ok @@ -215,18 +217,24 @@ defmodule Opsm.Runtime.IntegrationTest do describe "install_from_manifest/1 — live manifest install" do setup do - dir = System.tmp_dir!() |> Path.join("opsm_runtime_integration_#{System.unique_integer([:positive])}") + dir = + System.tmp_dir!() + |> Path.join("opsm_runtime_integration_#{System.unique_integer([:positive])}") + File.mkdir_p!(dir) + on_exit(fn -> File.rm_rf!(dir) Manager.remove("zig") end) + {:ok, dir: dir} end @tag :live_download test "installs tools declared in [runtime] section", %{dir: dir} do manifest = Path.join(dir, "opsm.toml") + File.write!(manifest, """ [package] name = "test-project" diff --git a/opsm_ex/test/opsm/runtime/manager_test.exs b/opsm_ex/test/opsm/runtime/manager_test.exs index f36c833a..024f3ec0 100644 --- a/opsm_ex/test/opsm/runtime/manager_test.exs +++ b/opsm_ex/test/opsm/runtime/manager_test.exs @@ -6,7 +6,8 @@ defmodule Opsm.Runtime.ManagerTest do alias Opsm.Runtime.Manager # Use a temp dir so tests don't touch ~/.opsm/runtimes - @runtimes_base System.tmp_dir!() |> Path.join("opsm_manager_test_#{System.unique_integer([:positive])}") + @runtimes_base System.tmp_dir!() + |> Path.join("opsm_manager_test_#{System.unique_integer([:positive])}") setup do # Ensure clean temp dir for each test @@ -23,6 +24,7 @@ defmodule Opsm.Runtime.ManagerTest do describe "install_from_manifest/1" do test "returns empty list when no [runtime] section" do manifest = Path.join(@runtimes_base, "opsm_no_runtime.toml") + File.write!(manifest, """ [package] name = "myapp" @@ -34,6 +36,7 @@ defmodule Opsm.Runtime.ManagerTest do test "parses [runtime] section into tool/version pairs" do manifest = Path.join(@runtimes_base, "opsm_with_runtime.toml") + File.write!(manifest, """ [package] name = "myapp" @@ -54,6 +57,7 @@ defmodule Opsm.Runtime.ManagerTest do test "handles quoted values" do manifest = Path.join(@runtimes_base, "opsm_quoted.toml") + File.write!(manifest, ~S""" [runtime] julia = "1.10.2" @@ -64,6 +68,7 @@ defmodule Opsm.Runtime.ManagerTest do test "stops reading [runtime] when next section starts" do manifest = Path.join(@runtimes_base, "opsm_sections.toml") + File.write!(manifest, """ [runtime] zig = "0.13.0" @@ -84,6 +89,7 @@ defmodule Opsm.Runtime.ManagerTest do test "ignores comment lines, including comments containing =" do manifest = Path.join(@runtimes_base, "opsm_comments.toml") + File.write!(manifest, """ [runtime] # canonical pins, synced via just toolchain-sync @@ -96,6 +102,7 @@ defmodule Opsm.Runtime.ManagerTest do test "strips inline comments from pin values" do manifest = Path.join(@runtimes_base, "opsm_inline.toml") + File.write!(manifest, """ [runtime] nickel = "1.16.0" # config language @@ -137,6 +144,7 @@ defmodule Opsm.Runtime.ManagerTest do test "each entry has required keys" do result = Manager.list_installed() + for entry <- result do assert Map.has_key?(entry, :name) assert Map.has_key?(entry, :version) @@ -156,6 +164,7 @@ defmodule Opsm.Runtime.ManagerTest do test "returns list of {tool, version} tuples" do result = Manager.list_active() assert is_list(result) + for {tool, version} <- result do assert is_binary(tool) assert is_binary(version) @@ -205,6 +214,7 @@ defmodule Opsm.Runtime.ManagerTest do test "each update entry has required keys" do {:ok, updates} = Manager.check_updates() + for update <- updates do assert Map.has_key?(update, :name) assert Map.has_key?(update, :current) diff --git a/opsm_ex/test/opsm/runtime/source_builder_test.exs b/opsm_ex/test/opsm/runtime/source_builder_test.exs index a1658bc8..3edecb96 100644 --- a/opsm_ex/test/opsm/runtime/source_builder_test.exs +++ b/opsm_ex/test/opsm/runtime/source_builder_test.exs @@ -23,9 +23,13 @@ defmodule Opsm.Runtime.SourceBuilderTest do test "returns missing dependencies for unavailable tools" do plugin = %{ "system_dependencies" => [ - %{"name" => "definitely-nonexistent-tool-xyz-999", "check_command" => "definitely-nonexistent-tool-xyz-999 --version"} + %{ + "name" => "definitely-nonexistent-tool-xyz-999", + "check_command" => "definitely-nonexistent-tool-xyz-999 --version" + } ] } + result = SourceBuilder.check_system_dependencies(plugin) assert {:error, {:missing_system_dependencies, missing}} = result assert "definitely-nonexistent-tool-xyz-999" in missing @@ -37,6 +41,7 @@ defmodule Opsm.Runtime.SourceBuilderTest do %{"name" => "bash", "check_command" => "bash --version"} ] } + # bash is available on any system running these tests assert :ok = SourceBuilder.check_system_dependencies(plugin) end @@ -53,10 +58,12 @@ defmodule Opsm.Runtime.SourceBuilderTest do "install" => %{ "strategy" => "DelegateToManager", "delegate_to" => "ghcup-definitely-not-installed-xyz", - "delegate_install_command" => "ghcup-definitely-not-installed-xyz install ghc {{version}} --set" + "delegate_install_command" => + "ghcup-definitely-not-installed-xyz install ghc {{version}} --set" }, "system_dependencies" => [] } + result = SourceBuilder.install(plugin, "9.6.4") # Should fail because the delegate tool doesn't exist assert match?({:error, _}, result) @@ -77,6 +84,7 @@ defmodule Opsm.Runtime.SourceBuilderTest do }, "system_dependencies" => [] } + result = SourceBuilder.install(plugin, "1.0.0") assert match?({:error, _}, result) end diff --git a/opsm_ex/test/opsm/runtime/url_handler_test.exs b/opsm_ex/test/opsm/runtime/url_handler_test.exs index 0f3e1add..8a16bb3d 100644 --- a/opsm_ex/test/opsm/runtime/url_handler_test.exs +++ b/opsm_ex/test/opsm/runtime/url_handler_test.exs @@ -52,8 +52,7 @@ defmodule Opsm.Runtime.UrlHandlerTest do @go_handler %{ "versions_url" => "https://go.dev/dl/?mode=json&include=all", "version_key_pattern" => "^go[0-9]+\\.[0-9]+", - "archive_url_template" => - "https://go.dev/dl/{{go_version}}.{{go_os}}-{{go_arch}}.tar.gz" + "archive_url_template" => "https://go.dev/dl/{{go_version}}.{{go_os}}-{{go_arch}}.tar.gz" } test "linux_amd64 builds correct URL" do @@ -91,14 +90,17 @@ defmodule Opsm.Runtime.UrlHandlerTest do end test "windows_amd64 → win-x64" do - assert {:ok, url} = UrlHandler.archive_url("nodejs", "20.0.0", :windows_amd64, @node_handler) + assert {:ok, url} = + UrlHandler.archive_url("nodejs", "20.0.0", :windows_amd64, @node_handler) + assert url == "https://nodejs.org/dist/v20.0.0/node-v20.0.0-win-x64.tar.gz" end end describe "archive_url/4 — dart" do @dart_handler %{ - "versions_url" => "https://storage.googleapis.com/dart-archive/channels/stable/release/latest/VERSION", + "versions_url" => + "https://storage.googleapis.com/dart-archive/channels/stable/release/latest/VERSION", "archive_url_template" => "https://storage.googleapis.com/dart-archive/channels/stable/release/{{version}}/sdk/dartsdk-{{dart_os}}-{{dart_arch}}-release.zip" } @@ -142,12 +144,16 @@ defmodule Opsm.Runtime.UrlHandlerTest do } test "linux_amd64 builds dl.k8s.io URL" do - assert {:ok, url} = UrlHandler.archive_url("kubectl", "1.32.0", :linux_amd64, @kubectl_handler) + assert {:ok, url} = + UrlHandler.archive_url("kubectl", "1.32.0", :linux_amd64, @kubectl_handler) + assert url == "https://dl.k8s.io/release/v1.32.0/bin/linux/amd64/kubectl" end test "darwin_arm64 builds dl.k8s.io URL" do - assert {:ok, url} = UrlHandler.archive_url("kubectl", "1.32.0", :darwin_arm64, @kubectl_handler) + assert {:ok, url} = + UrlHandler.archive_url("kubectl", "1.32.0", :darwin_arm64, @kubectl_handler) + assert url == "https://dl.k8s.io/release/v1.32.0/bin/darwin/arm64/kubectl" end end @@ -155,6 +161,7 @@ defmodule Opsm.Runtime.UrlHandlerTest do describe "archive_url/4 — unknown tool" do test "returns no_url_handler for unrecognised tool" do handler = %{"archive_url_template" => "https://example.com/{{version}}"} + assert {:error, :no_url_handler} = UrlHandler.archive_url("unknowntool999", "1.0.0", :linux_amd64, handler) end @@ -176,6 +183,7 @@ defmodule Opsm.Runtime.UrlHandlerTest do "0.11.0" => %{"x86_64-linux" => %{"tarball" => "..."}}, "master" => %{"x86_64-linux" => %{"tarball" => "..."}} } + pattern = "^[0-9]+\\.[0-9]+\\.[0-9]+$" result = UrlHandler.process_versions_body(body, pattern, "zig") @@ -215,6 +223,7 @@ defmodule Opsm.Runtime.UrlHandlerTest do %{"version" => "v18.0.0", "lts" => "Hydrogen"}, %{"version" => "v16.0.0", "lts" => "Gallium"} ] + pattern = "^v[0-9]+\\.[0-9]+\\.[0-9]+$" result = UrlHandler.process_versions_body(body, pattern, "nodejs") @@ -273,6 +282,7 @@ defmodule Opsm.Runtime.UrlHandlerTest do "1.1.0-beta.1" => %{}, "2.0.0" => %{} } + pattern = "^[0-9]+\\.[0-9]+\\.[0-9]+$" result = UrlHandler.process_versions_body(body, pattern, "anytool") diff --git a/opsm_ex/test/opsm/security/security_test.exs b/opsm_ex/test/opsm/security/security_test.exs index c7e1c83e..3f01d015 100644 --- a/opsm_ex/test/opsm/security/security_test.exs +++ b/opsm_ex/test/opsm/security/security_test.exs @@ -125,6 +125,7 @@ defmodule Opsm.Security.SecurityTest do url: "https://osv.dev/vulnerability/GHSA-abc-123", published: "2024-01-01T00:00:00Z" } + assert v.id == "GHSA-abc-123" assert v.severity == :high assert v.aliases == ["CVE-2024-0001"] @@ -146,6 +147,7 @@ defmodule Opsm.Security.SecurityTest do typosquat: {:clean}, scanned_at: DateTime.utc_now() } + assert Scanner.Report.clean?(report) end @@ -159,6 +161,7 @@ defmodule Opsm.Security.SecurityTest do url: "https://osv.dev/vulnerability/GHSA-test", published: nil } + report = %Scanner.Report{ package: "test-pkg", version: "1.0.0", @@ -167,6 +170,7 @@ defmodule Opsm.Security.SecurityTest do typosquat: {:clean}, scanned_at: DateTime.utc_now() } + refute Scanner.Report.clean?(report) end @@ -176,19 +180,55 @@ defmodule Opsm.Security.SecurityTest do version: nil, forth: :npm, vulnerabilities: [], - typosquat: {:suspicious, [%Typosquat.Match{package: "lodash", similarity: :edit_distance_1, flags: []}]}, + typosquat: + {:suspicious, + [%Typosquat.Match{package: "lodash", similarity: :edit_distance_1, flags: []}]}, scanned_at: DateTime.utc_now() } + refute Scanner.Report.clean?(report) end test "critical_count and high_count aggregate correctly" do vulns = [ - %Osv.Vulnerability{id: "A", summary: nil, severity: :critical, aliases: [], fixed_in: nil, url: "", published: nil}, - %Osv.Vulnerability{id: "B", summary: nil, severity: :high, aliases: [], fixed_in: nil, url: "", published: nil}, - %Osv.Vulnerability{id: "C", summary: nil, severity: :medium, aliases: [], fixed_in: nil, url: "", published: nil}, - %Osv.Vulnerability{id: "D", summary: nil, severity: :critical, aliases: [], fixed_in: nil, url: "", published: nil} + %Osv.Vulnerability{ + id: "A", + summary: nil, + severity: :critical, + aliases: [], + fixed_in: nil, + url: "", + published: nil + }, + %Osv.Vulnerability{ + id: "B", + summary: nil, + severity: :high, + aliases: [], + fixed_in: nil, + url: "", + published: nil + }, + %Osv.Vulnerability{ + id: "C", + summary: nil, + severity: :medium, + aliases: [], + fixed_in: nil, + url: "", + published: nil + }, + %Osv.Vulnerability{ + id: "D", + summary: nil, + severity: :critical, + aliases: [], + fixed_in: nil, + url: "", + published: nil + } ] + report = %Scanner.Report{ package: "pkg", version: "1.0.0", @@ -197,6 +237,7 @@ defmodule Opsm.Security.SecurityTest do typosquat: {:clean}, scanned_at: DateTime.utc_now() } + assert Scanner.Report.critical_count(report) == 2 assert Scanner.Report.high_count(report) == 1 assert Scanner.Report.has_critical_or_high?(report) @@ -204,9 +245,26 @@ defmodule Opsm.Security.SecurityTest do test "has_critical_or_high? is false for only medium/low vulns" do vulns = [ - %Osv.Vulnerability{id: "A", summary: nil, severity: :medium, aliases: [], fixed_in: nil, url: "", published: nil}, - %Osv.Vulnerability{id: "B", summary: nil, severity: :low, aliases: [], fixed_in: nil, url: "", published: nil} + %Osv.Vulnerability{ + id: "A", + summary: nil, + severity: :medium, + aliases: [], + fixed_in: nil, + url: "", + published: nil + }, + %Osv.Vulnerability{ + id: "B", + summary: nil, + severity: :low, + aliases: [], + fixed_in: nil, + url: "", + published: nil + } ] + report = %Scanner.Report{ package: "pkg", version: "1.0.0", @@ -215,6 +273,7 @@ defmodule Opsm.Security.SecurityTest do typosquat: {:clean}, scanned_at: DateTime.utc_now() } + refute Scanner.Report.has_critical_or_high?(report) end end @@ -259,6 +318,7 @@ defmodule Opsm.Security.SecurityTest do typosquat: {:clean}, scanned_at: DateTime.utc_now() } + assert :ok = Scanner.print_report(report) end end diff --git a/opsm_ex/test/opsm/slsa/github_attestation_test.exs b/opsm_ex/test/opsm/slsa/github_attestation_test.exs index 67b1e2b6..821a2ac7 100644 --- a/opsm_ex/test/opsm/slsa/github_attestation_test.exs +++ b/opsm_ex/test/opsm/slsa/github_attestation_test.exs @@ -54,8 +54,15 @@ defmodule Opsm.Slsa.GithubAttestationTest do test "rejects non-builder identities" do refute GithubAttestation.github_actions_builder?("https://github.com/o/r") - refute GithubAttestation.github_actions_builder?("https://evil.com/o/r/.github/workflows/w@x") - refute GithubAttestation.github_actions_builder?("https://github.com/o/r/.github/workflows/w.yml") + + refute GithubAttestation.github_actions_builder?( + "https://evil.com/o/r/.github/workflows/w@x" + ) + + refute GithubAttestation.github_actions_builder?( + "https://github.com/o/r/.github/workflows/w.yml" + ) + refute GithubAttestation.github_actions_builder?("https://opsm.dev/builders/elixir-mix") refute GithubAttestation.github_actions_builder?(nil) refute GithubAttestation.github_actions_builder?(42) @@ -77,7 +84,9 @@ defmodule Opsm.Slsa.GithubAttestationTest do test "returns :none for non-GitHub or missing repositories" do assert {:none, _} = - GithubAttestation.github_owner_repo(%{manifest: %{repository: "https://gitlab.com/a/b"}}) + GithubAttestation.github_owner_repo(%{ + manifest: %{repository: "https://gitlab.com/a/b"} + }) assert {:none, _} = GithubAttestation.github_owner_repo(%{manifest: %{repository: nil}}) @@ -97,21 +106,37 @@ defmodule Opsm.Slsa.GithubAttestationTest do end test "returns :none for missing or non-sha256 checksums" do - assert {:none, _} = GithubAttestation.artifact_digest(%{checksum: nil, checksum_algo: :sha256}) - assert {:none, _} = GithubAttestation.artifact_digest(%{checksum: "abc", checksum_algo: :sha512}) - assert {:none, _} = GithubAttestation.artifact_digest(%{checksum: "not-hex", checksum_algo: :sha256}) + assert {:none, _} = + GithubAttestation.artifact_digest(%{checksum: nil, checksum_algo: :sha256}) + + assert {:none, _} = + GithubAttestation.artifact_digest(%{checksum: "abc", checksum_algo: :sha512}) + + assert {:none, _} = + GithubAttestation.artifact_digest(%{checksum: "not-hex", checksum_algo: :sha256}) end end describe "fetch_attestations/3 input validation" do test "rejects malformed owner/repo without touching the network" do - assert {:error, _} = GithubAttestation.fetch_attestations("owner repo; rm", "sha256:" <> String.duplicate("a", 64)) - assert {:error, _} = GithubAttestation.fetch_attestations("noslash", "sha256:" <> String.duplicate("a", 64)) + assert {:error, _} = + GithubAttestation.fetch_attestations( + "owner repo; rm", + "sha256:" <> String.duplicate("a", 64) + ) + + assert {:error, _} = + GithubAttestation.fetch_attestations( + "noslash", + "sha256:" <> String.duplicate("a", 64) + ) end test "rejects malformed digests without touching the network" do assert {:error, _} = GithubAttestation.fetch_attestations("o/r", "sha256:short") - assert {:error, _} = GithubAttestation.fetch_attestations("o/r", "md5:" <> String.duplicate("a", 64)) + + assert {:error, _} = + GithubAttestation.fetch_attestations("o/r", "md5:" <> String.duplicate("a", 64)) end end @@ -138,7 +163,9 @@ defmodule Opsm.Slsa.GithubAttestationTest do test "rejection signatures are definitive verified: false" do # cert-identity / digest / signer-repo mismatches all print this assert {:ok, %GithubAttestationVerification{verified: false}} = - GithubAttestation.classify_gh_failure(~s(Error: verifying with issuer "sigstore.dev")) + GithubAttestation.classify_gh_failure( + ~s(Error: verifying with issuer "sigstore.dev") + ) assert {:ok, %GithubAttestationVerification{verified: false}} = GithubAttestation.classify_gh_failure("Error: no attestations found") @@ -182,8 +209,11 @@ defmodule Opsm.Slsa.GithubAttestationTest do end test "empty output is not a verification" do - assert %GithubAttestationVerification{verified: false} = GithubAttestation.decode_gh_output([]) - assert %GithubAttestationVerification{verified: false} = GithubAttestation.decode_gh_output(%{}) + assert %GithubAttestationVerification{verified: false} = + GithubAttestation.decode_gh_output([]) + + assert %GithubAttestationVerification{verified: false} = + GithubAttestation.decode_gh_output(%{}) end end @@ -247,7 +277,10 @@ defmodule Opsm.Slsa.GithubAttestationTest do test "statement subject digest binds the artifact when it matches the package checksum" do # subject digest in statement/1 is "abab...": tarball_url would fail the # materials check, but the matching subject digest governs instead - package = %{tarball_url: "https://example.com/not-in-materials.tgz", checksum: String.duplicate("ab", 32)} + package = %{ + tarball_url: "https://example.com/not-in-materials.tgz", + checksum: String.duplicate("ab", 32) + } assert {:ok, result} = Provenance.verify_github_attestation(statement(@gh_builder), @@ -301,7 +334,11 @@ defmodule Opsm.Slsa.GithubAttestationTest do registry_url: "https://example.invalid", tarball_url: nil, checksum: nil, - manifest: %{repository: "https://github.com/hyperpolymath/proven", license: "MPL-2.0", dependencies: %{}} + manifest: %{ + repository: "https://github.com/hyperpolymath/proven", + license: "MPL-2.0", + dependencies: %{} + } } {:ok, results} = diff --git a/opsm_ex/test/opsm/validation_test.exs b/opsm_ex/test/opsm/validation_test.exs index 2dbb6236..7a182593 100644 --- a/opsm_ex/test/opsm/validation_test.exs +++ b/opsm_ex/test/opsm/validation_test.exs @@ -113,7 +113,9 @@ defmodule Opsm.ValidationTest do end test "rejects data: URLs" do - assert {:error, reason} = Validation.validate_url("data:text/html,") + assert {:error, reason} = + Validation.validate_url("data:text/html,") + # data: URLs have no host, so we get "must have a host" error assert reason =~ "host" or reason =~ "not allowed" end diff --git a/opsm_ex/test/opsm/verified/json_property_test.exs b/opsm_ex/test/opsm/verified/json_property_test.exs index cac9f6e1..2172d37d 100644 --- a/opsm_ex/test/opsm/verified/json_property_test.exs +++ b/opsm_ex/test/opsm/verified/json_property_test.exs @@ -106,9 +106,7 @@ defmodule Opsm.Verified.JsonPropertyTest do end property "handles empty structures" do - check all( - empty <- member_of([%{}, [], nil, ""]) - ) do + check all(empty <- member_of([%{}, [], nil, ""])) do wrapped = %{"empty" => empty} case Json.encode(wrapped) do diff --git a/opsm_ex/test/opsm/verified/url_property_test.exs b/opsm_ex/test/opsm/verified/url_property_test.exs index 253225c2..11d1b57a 100644 --- a/opsm_ex/test/opsm/verified/url_property_test.exs +++ b/opsm_ex/test/opsm/verified/url_property_test.exs @@ -8,8 +8,9 @@ defmodule Opsm.Verified.UrlPropertyTest do describe "Url.validate/1 properties" do property "always rejects URLs without schemes" do - check all(host <- string(:alphanumeric, min_length: 1), - path <- string(:alphanumeric) + check all( + host <- string(:alphanumeric, min_length: 1), + path <- string(:alphanumeric) ) do url = "#{host}/#{path}" assert {:error, :missing_scheme} = Url.validate(url) @@ -23,9 +24,10 @@ defmodule Opsm.Verified.UrlPropertyTest do end property "always rejects localhost variants" do - check all(scheme <- member_of(["http", "https"]), - localhost <- member_of(["localhost", "127.0.0.1", "0.0.0.0", "::1"]), - path <- string(:alphanumeric) + check all( + scheme <- member_of(["http", "https"]), + localhost <- member_of(["localhost", "127.0.0.1", "0.0.0.0", "::1"]), + path <- string(:alphanumeric) ) do url = "#{scheme}://#{localhost}/#{path}" assert {:error, :blocked_host} = Url.validate(url) @@ -33,10 +35,11 @@ defmodule Opsm.Verified.UrlPropertyTest do end property "always rejects private IP ranges" do - check all(scheme <- member_of(["http", "https"]), - octet2 <- integer(0..255), - octet3 <- integer(0..255), - octet4 <- integer(0..255) + check all( + scheme <- member_of(["http", "https"]), + octet2 <- integer(0..255), + octet3 <- integer(0..255), + octet4 <- integer(0..255) ) do # Test 192.168.x.x range url = "#{scheme}://192.168.#{octet3}.#{octet4}/test" @@ -49,8 +52,9 @@ defmodule Opsm.Verified.UrlPropertyTest do end property "always rejects unsupported schemes" do - check all(scheme <- member_of(["ftp", "file", "javascript", "data", "ssh"]), - host <- string(:alphanumeric, min_length: 1) + check all( + scheme <- member_of(["ftp", "file", "javascript", "data", "ssh"]), + host <- string(:alphanumeric, min_length: 1) ) do url = "#{scheme}://#{host}/path" assert match?({:error, {:invalid_scheme, _}}, Url.validate(url)) @@ -58,10 +62,11 @@ defmodule Opsm.Verified.UrlPropertyTest do end property "accepts valid http/https URLs to public domains" do - check all(scheme <- member_of(["http", "https"]), - # Use known safe domains - host <- member_of(["example.com", "github.com", "registry.npmjs.org"]), - path <- string(:alphanumeric) + check all( + scheme <- member_of(["http", "https"]), + # Use known safe domains + host <- member_of(["example.com", "github.com", "registry.npmjs.org"]), + path <- string(:alphanumeric) ) do url = "#{scheme}://#{host}/#{path}" @@ -79,10 +84,11 @@ defmodule Opsm.Verified.UrlPropertyTest do end property "preserves original URL in validated struct" do - check all(scheme <- member_of(["http", "https"]), - host <- member_of(["example.com", "github.com"]), - port <- integer(1..65535), - path <- string(:alphanumeric) + check all( + scheme <- member_of(["http", "https"]), + host <- member_of(["example.com", "github.com"]), + port <- integer(1..65535), + path <- string(:alphanumeric) ) do url = "#{scheme}://#{host}:#{port}/#{path}" @@ -100,9 +106,10 @@ defmodule Opsm.Verified.UrlPropertyTest do describe "Url.to_string/1 properties" do property "roundtrip: validate -> to_string returns original" do - check all(scheme <- member_of(["http", "https"]), - host <- member_of(["example.com", "test.org"]), - path <- string(:alphanumeric, max_length: 20) + check all( + scheme <- member_of(["http", "https"]), + host <- member_of(["example.com", "test.org"]), + path <- string(:alphanumeric, max_length: 20) ) do url = "#{scheme}://#{host}/#{path}" diff --git a/opsm_ex/test/property/lockfile_property_test.exs b/opsm_ex/test/property/lockfile_property_test.exs index 4fb072fb..03b9c2de 100644 --- a/opsm_ex/test/property/lockfile_property_test.exs +++ b/opsm_ex/test/property/lockfile_property_test.exs @@ -15,17 +15,21 @@ defmodule Opsm.Property.LockfilePropertyTest do end defp version_gen do - gen all major <- integer(0..10), - minor <- integer(0..20), - patch <- integer(0..50) do + gen all( + major <- integer(0..10), + minor <- integer(0..20), + patch <- integer(0..50) + ) do "#{major}.#{minor}.#{patch}" end end defp semver_version_gen do - gen all major <- integer(0..10), - minor <- integer(0..20), - patch <- integer(0..50) do + gen all( + major <- integer(0..10), + minor <- integer(0..20), + patch <- integer(0..50) + ) do "#{major}.#{minor}.#{patch}" end end @@ -48,11 +52,13 @@ defmodule Opsm.Property.LockfilePropertyTest do end defp package_gen do - gen all name <- package_name_gen(), - version <- version_gen(), - forth <- forth_gen(), - checksum <- checksum_gen(), - algo <- one_of([constant("sha256"), constant("blake2b"), constant("sha3-512")]) do + gen all( + name <- package_name_gen(), + version <- version_gen(), + forth <- forth_gen(), + checksum <- checksum_gen(), + algo <- one_of([constant("sha256"), constant("blake2b"), constant("sha3-512")]) + ) do %{ name: name, version: version, @@ -65,9 +71,10 @@ defmodule Opsm.Property.LockfilePropertyTest do describe "Lockfile Determinism" do property "package list is consistent after add/get operations" do - check all packages <- list_of(package_gen(), min_length: 1, max_length: 10) do - lockfile = packages - |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) + check all(packages <- list_of(package_gen(), min_length: 1, max_length: 10)) do + lockfile = + packages + |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) # List packages and count listed = Lockfile.list_packages(lockfile) @@ -82,9 +89,10 @@ defmodule Opsm.Property.LockfilePropertyTest do end property "lockfile list count matches added package count" do - check all packages <- list_of(package_gen(), min_length: 1, max_length: 15) do - lockfile = packages - |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) + check all(packages <- list_of(package_gen(), min_length: 1, max_length: 15)) do + lockfile = + packages + |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) listed = Lockfile.list_packages(lockfile) @@ -94,25 +102,34 @@ defmodule Opsm.Property.LockfilePropertyTest do end property "adding and removing same package returns to previous count" do - check all name <- package_name_gen(), - forth <- forth_gen(), - initial_count <- integer(0..5) do + check all( + name <- package_name_gen(), + forth <- forth_gen(), + initial_count <- integer(0..5) + ) do # Build initial lockfile - original = Enum.reduce(0..initial_count, Lockfile.new(), fn i, acc -> - Lockfile.add_package(acc, %{ - name: "pkg-#{i}", - version: "1.0.0", - forth: forth, - checksum: "hash-#{i}" - }) - end) + original = + Enum.reduce(0..initial_count, Lockfile.new(), fn i, acc -> + Lockfile.add_package(acc, %{ + name: "pkg-#{i}", + version: "1.0.0", + forth: forth, + checksum: "hash-#{i}" + }) + end) original_count = Lockfile.list_packages(original) |> length() # Add and remove new package - modified = original - |> Lockfile.add_package(%{name: name, version: "2.0.0", forth: forth, checksum: "new-hash"}) - |> Lockfile.remove_package(name, forth) + modified = + original + |> Lockfile.add_package(%{ + name: name, + version: "2.0.0", + forth: forth, + checksum: "new-hash" + }) + |> Lockfile.remove_package(name, forth) # Should be back to original count modified_count = Lockfile.list_packages(modified) |> length() @@ -123,24 +140,32 @@ defmodule Opsm.Property.LockfilePropertyTest do describe "Version Constraint Intersection" do property "intersection of compatible constraints is valid" do - check all constraint1 <- string(:alphanumeric, min_length: 1, max_length: 10), - constraint2 <- string(:alphanumeric, min_length: 1, max_length: 10) do - case {VersionConstraint.parse(constraint1, :semver), VersionConstraint.parse(constraint2, :semver)} do + check all( + constraint1 <- string(:alphanumeric, min_length: 1, max_length: 10), + constraint2 <- string(:alphanumeric, min_length: 1, max_length: 10) + ) do + case {VersionConstraint.parse(constraint1, :semver), + VersionConstraint.parse(constraint2, :semver)} do {{:ok, c1}, {:ok, c2}} -> # Both should parse successfully assert c1 != nil assert c2 != nil # Parsing failures are acceptable - {{:error, _}, _} -> :ok - {_, {:error, _}} -> :ok + {{:error, _}, _} -> + :ok + + {_, {:error, _}} -> + :ok end end end property "version satisfying constraint satisfies intersection" do - check all version <- semver_version_gen(), - version_constraint <- string(:alphanumeric, min_length: 1, max_length: 20) do + check all( + version <- semver_version_gen(), + version_constraint <- string(:alphanumeric, min_length: 1, max_length: 20) + ) do case VersionConstraint.parse(version_constraint, :semver) do {:ok, constraint} -> # Either it satisfies or it doesn't - both are valid outcomes @@ -157,9 +182,11 @@ defmodule Opsm.Property.LockfilePropertyTest do describe "Manifest Roundtrip" do property "parse -> serialize -> parse yields equivalent manifest" do - check all name <- package_name_gen(), - version <- version_gen(), - forth <- forth_gen() do + check all( + name <- package_name_gen(), + version <- version_gen(), + forth <- forth_gen() + ) do original = %{ name: name, version: version, @@ -172,7 +199,8 @@ defmodule Opsm.Property.LockfilePropertyTest do {:ok, deserialized} = Jason.decode(serialized) # Convert keys to atoms for comparison - deserialized_atoms = Map.new(deserialized, fn {k, v} -> {String.to_existing_atom(k), v} end) + deserialized_atoms = + Map.new(deserialized, fn {k, v} -> {String.to_existing_atom(k), v} end) # Core fields should match assert deserialized_atoms.name == original.name @@ -182,9 +210,10 @@ defmodule Opsm.Property.LockfilePropertyTest do end property "lockfile write -> read preserves package data" do - check all packages <- list_of(package_gen(), min_length: 1, max_length: 10) do - lockfile = packages - |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) + check all(packages <- list_of(package_gen(), min_length: 1, max_length: 10)) do + lockfile = + packages + |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) # Verify all packages are present and unchanged written_packages = Lockfile.list_packages(lockfile) @@ -203,10 +232,13 @@ defmodule Opsm.Property.LockfilePropertyTest do describe "Dependency Tree Consistency" do property "packages_for_forth returns only packages from specified forth" do - check all packages <- list_of(package_gen(), min_length: 1, max_length: 20), - target_forth <- forth_gen() do - lockfile = packages - |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) + check all( + packages <- list_of(package_gen(), min_length: 1, max_length: 20), + target_forth <- forth_gen() + ) do + lockfile = + packages + |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) filtered = Lockfile.packages_for_forth(lockfile, target_forth) @@ -220,9 +252,10 @@ defmodule Opsm.Property.LockfilePropertyTest do end property "list_packages maintains consistent ordering" do - check all packages <- list_of(package_gen(), min_length: 1, max_length: 15) do - lockfile = packages - |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) + check all(packages <- list_of(package_gen(), min_length: 1, max_length: 15)) do + lockfile = + packages + |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) listed1 = Lockfile.list_packages(lockfile) listed2 = Lockfile.list_packages(lockfile) @@ -235,18 +268,21 @@ defmodule Opsm.Property.LockfilePropertyTest do describe "Checksum Consistency" do property "checksum mismatch is consistently detected" do - check all pkg_name <- package_name_gen(), - version <- version_gen(), - forth <- forth_gen(), - stored_checksum <- checksum_gen(), - actual_checksum <- checksum_gen() do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: pkg_name, - version: version, - forth: forth, - checksum: stored_checksum - }) + check all( + pkg_name <- package_name_gen(), + version <- version_gen(), + forth <- forth_gen(), + stored_checksum <- checksum_gen(), + actual_checksum <- checksum_gen() + ) do + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: pkg_name, + version: version, + forth: forth, + checksum: stored_checksum + }) result1 = Lockfile.verify_package(lockfile, pkg_name, forth, actual_checksum) result2 = Lockfile.verify_package(lockfile, pkg_name, forth, actual_checksum) @@ -264,17 +300,20 @@ defmodule Opsm.Property.LockfilePropertyTest do end property "package is retrievable after adding" do - check all pkg_name <- package_name_gen(), - version <- version_gen(), - forth <- forth_gen(), - checksum <- checksum_gen() do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: pkg_name, - version: version, - forth: forth, - checksum: checksum - }) + check all( + pkg_name <- package_name_gen(), + version <- version_gen(), + forth <- forth_gen(), + checksum <- checksum_gen() + ) do + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: pkg_name, + version: version, + forth: forth, + checksum: checksum + }) pkg = Lockfile.get_package(lockfile, pkg_name, forth) @@ -288,19 +327,21 @@ defmodule Opsm.Property.LockfilePropertyTest do describe "Integrity Hash Properties" do property "integrity hash is recomputable for same packages" do - check all count <- integer(1..8) do - packages = Enum.map(0..count, fn i -> - %{ - name: "pkg-#{i}", - version: "1.0.#{i}", - forth: :npm, - checksum: "hash-#{i}" - } - end) + check all(count <- integer(1..8)) do + packages = + Enum.map(0..count, fn i -> + %{ + name: "pkg-#{i}", + version: "1.0.#{i}", + forth: :npm, + checksum: "hash-#{i}" + } + end) # Create lockfile - lockfile = packages - |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) + lockfile = + packages + |> Enum.reduce(Lockfile.new(), &Lockfile.add_package(&2, &1)) # Compute hash twice on same lockfile - should be identical with_hash1 = Lockfile.compute_integrity_hash(lockfile) @@ -312,15 +353,16 @@ defmodule Opsm.Property.LockfilePropertyTest do end property "integrity hash is never nil after computation" do - check all name <- package_name_gen() do - lockfile = Lockfile.new() - |> Lockfile.add_package(%{ - name: name, - version: "1.0.0", - forth: :npm, - checksum: "test-hash" - }) - |> Lockfile.compute_integrity_hash() + check all(name <- package_name_gen()) do + lockfile = + Lockfile.new() + |> Lockfile.add_package(%{ + name: name, + version: "1.0.0", + forth: :npm, + checksum: "test-hash" + }) + |> Lockfile.compute_integrity_hash() assert lockfile.integrity_hash != nil assert String.length(lockfile.integrity_hash) > 0 @@ -328,16 +370,17 @@ defmodule Opsm.Property.LockfilePropertyTest do end property "integrity hash uses correct algorithm" do - check all count <- integer(1..5) do - lockfile = Enum.reduce(0..count, Lockfile.new(), fn i, acc -> - Lockfile.add_package(acc, %{ - name: "pkg-#{i}", - version: "1.0.0", - forth: :npm, - checksum: "hash-#{i}" - }) - end) - |> Lockfile.compute_integrity_hash() + check all(count <- integer(1..5)) do + lockfile = + Enum.reduce(0..count, Lockfile.new(), fn i, acc -> + Lockfile.add_package(acc, %{ + name: "pkg-#{i}", + version: "1.0.0", + forth: :npm, + checksum: "hash-#{i}" + }) + end) + |> Lockfile.compute_integrity_hash() assert lockfile.integrity_algo == "sha3-512" assert String.length(lockfile.integrity_hash) == 128 diff --git a/opsm_ex/test/stress_test.exs b/opsm_ex/test/stress_test.exs index a162deeb..4f848a75 100644 --- a/opsm_ex/test/stress_test.exs +++ b/opsm_ex/test/stress_test.exs @@ -125,7 +125,7 @@ test_packages = [ {:maven, "org.projectlombok:lombok", "latest"}, {:maven, "com.google.code.gson:gson", "latest"}, {:maven, "org.mockito:mockito-core", "latest"}, - {:maven, "ch.qos.logback:logback-classic", "latest"}, + {:maven, "ch.qos.logback:logback-classic", "latest"} ] IO.puts("=" |> String.duplicate(70)) @@ -152,7 +152,11 @@ errors = [] elapsed = System.monotonic_time(:millisecond) - start checksum_status = if pkg.checksum, do: "✓ #{pkg.checksum_algo}", else: "✗ none" dep_count = map_size(pkg.manifest.dependencies || %{}) - IO.puts(" ✓ #{name}@#{pkg.version} (#{elapsed}ms, deps: #{dep_count}, checksum: #{checksum_status})") + + IO.puts( + " ✓ #{name}@#{pkg.version} (#{elapsed}ms, deps: #{dep_count}, checksum: #{checksum_status})" + ) + {%{r | ok: r.ok + 1}, e} {:error, reason} -> @@ -173,6 +177,7 @@ IO.puts("=" |> String.duplicate(70)) if errors != [] do IO.puts("") IO.puts("FAILURES:") + for {forth, name, reason} <- Enum.reverse(errors) do IO.puts(" @#{forth}/#{name}: #{inspect(reason)}") end diff --git a/runtime/contract/runtime-plugin.ncl b/runtime/contract/runtime-plugin.ncl index 7d28fcc2..8fd6b2f4 100644 --- a/runtime/contract/runtime-plugin.ncl +++ b/runtime/contract/runtime-plugin.ncl @@ -154,8 +154,8 @@ let RuntimePlugin = { version_sort | [| 'Semver, 'Lexicographic, 'Numeric |] - | default = 'Semver - | doc "How to sort versions for 'latest' resolution", + | doc "How to sort versions for 'latest' resolution" + | default = 'Semver, # === Installation (required) === install @@ -256,13 +256,13 @@ let RuntimePlugin = { # === Metadata === license | String - | default = "MPL-2.0" - | doc "License for this plugin definition (not the tool itself)", + | doc "License for this plugin definition (not the tool itself)" + | default = "MPL-2.0", schema_version | String - | default = "1.0.0" - | doc "Contract version this definition was written against", + | doc "Contract version this definition was written against" + | default = "1.0.0", } in RuntimePlugin diff --git a/runtime/core/age.ncl b/runtime/core/age.ncl index 36aa466d..48cd31cd 100644 --- a/runtime/core/age.ncl +++ b/runtime/core/age.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "age", diff --git a/runtime/core/apko.ncl b/runtime/core/apko.ncl index 3e7dd3e0..1a7b5263 100644 --- a/runtime/core/apko.ncl +++ b/runtime/core/apko.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "apko", diff --git a/runtime/core/arangodb.ncl b/runtime/core/arangodb.ncl index 79b9dd58..005e100d 100644 --- a/runtime/core/arangodb.ncl +++ b/runtime/core/arangodb.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "arangodb", diff --git a/runtime/core/asdf-configurator.ncl b/runtime/core/asdf-configurator.ncl index c101b951..2db1884e 100644 --- a/runtime/core/asdf-configurator.ncl +++ b/runtime/core/asdf-configurator.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "asdf-configurator", diff --git a/runtime/core/asdf-metaiconic.ncl b/runtime/core/asdf-metaiconic.ncl index b8ed3a86..aec6f71d 100644 --- a/runtime/core/asdf-metaiconic.ncl +++ b/runtime/core/asdf-metaiconic.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "asdf-metaiconic", diff --git a/runtime/core/asdf-security.ncl b/runtime/core/asdf-security.ncl index ef8a450b..d2335c16 100644 --- a/runtime/core/asdf-security.ncl +++ b/runtime/core/asdf-security.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "asdf-security", diff --git a/runtime/core/asdf-ui.ncl b/runtime/core/asdf-ui.ncl index 592d325f..b0f5cdbc 100644 --- a/runtime/core/asdf-ui.ncl +++ b/runtime/core/asdf-ui.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "asdf-ui", diff --git a/runtime/core/bebop.ncl b/runtime/core/bebop.ncl index 9e8079d0..442dd311 100644 --- a/runtime/core/bebop.ncl +++ b/runtime/core/bebop.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "bebop", diff --git a/runtime/core/borg.ncl b/runtime/core/borg.ncl index 82f25c67..c4c6515c 100644 --- a/runtime/core/borg.ncl +++ b/runtime/core/borg.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "borg", diff --git a/runtime/core/casket-ssg.ncl b/runtime/core/casket-ssg.ncl index 7342356c..5cc8cc70 100644 --- a/runtime/core/casket-ssg.ncl +++ b/runtime/core/casket-ssg.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "casket-ssg", diff --git a/runtime/core/cassandra.ncl b/runtime/core/cassandra.ncl index ac6764a3..d1e7517b 100644 --- a/runtime/core/cassandra.ncl +++ b/runtime/core/cassandra.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "cassandra", diff --git a/runtime/core/cfssl.ncl b/runtime/core/cfssl.ncl index d1adef6e..08fee91f 100644 --- a/runtime/core/cfssl.ncl +++ b/runtime/core/cfssl.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "cfssl", diff --git a/runtime/core/cobalt.ncl b/runtime/core/cobalt.ncl index 81650042..c19f1a15 100644 --- a/runtime/core/cobalt.ncl +++ b/runtime/core/cobalt.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "cobalt", diff --git a/runtime/core/coredns.ncl b/runtime/core/coredns.ncl index 911d7284..74f1824d 100644 --- a/runtime/core/coredns.ncl +++ b/runtime/core/coredns.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "coredns", diff --git a/runtime/core/cosign.ncl b/runtime/core/cosign.ncl index 44935500..0b139f25 100644 --- a/runtime/core/cosign.ncl +++ b/runtime/core/cosign.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "cosign", diff --git a/runtime/core/couchdb.ncl b/runtime/core/couchdb.ncl index a0d086dd..d8df07aa 100644 --- a/runtime/core/couchdb.ncl +++ b/runtime/core/couchdb.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "couchdb", diff --git a/runtime/core/cue.ncl b/runtime/core/cue.ncl index 91b9695d..cda3246b 100644 --- a/runtime/core/cue.ncl +++ b/runtime/core/cue.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "cue", diff --git a/runtime/core/dart.ncl b/runtime/core/dart.ncl index ff560822..eeeb8b6f 100644 --- a/runtime/core/dart.ncl +++ b/runtime/core/dart.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "dart", description = "dart — Client-optimised language for fast apps", diff --git a/runtime/core/deno.ncl b/runtime/core/deno.ncl index f995ca2a..4c1e8b6a 100644 --- a/runtime/core/deno.ncl +++ b/runtime/core/deno.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "deno", diff --git a/runtime/core/dhall.ncl b/runtime/core/dhall.ncl index 22e89828..d5c23a65 100644 --- a/runtime/core/dhall.ncl +++ b/runtime/core/dhall.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "dhall", diff --git a/runtime/core/doctl.ncl b/runtime/core/doctl.ncl index feb8a206..3b2da21a 100644 --- a/runtime/core/doctl.ncl +++ b/runtime/core/doctl.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "doctl", diff --git a/runtime/core/dragonfly.ncl b/runtime/core/dragonfly.ncl index 157bdf80..6f6d5ad9 100644 --- a/runtime/core/dragonfly.ncl +++ b/runtime/core/dragonfly.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "dragonfly", diff --git a/runtime/core/envoy.ncl b/runtime/core/envoy.ncl index e5ee0f2e..cf165c14 100644 --- a/runtime/core/envoy.ncl +++ b/runtime/core/envoy.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "envoy", diff --git a/runtime/core/ephapax.ncl b/runtime/core/ephapax.ncl index 10a36064..36ed858a 100644 --- a/runtime/core/ephapax.ncl +++ b/runtime/core/ephapax.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "ephapax", diff --git a/runtime/core/fornax.ncl b/runtime/core/fornax.ncl index 45c251ad..d7594072 100644 --- a/runtime/core/fornax.ncl +++ b/runtime/core/fornax.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "fornax", diff --git a/runtime/core/fortran.ncl b/runtime/core/fortran.ncl index 75b17fc7..808873ac 100644 --- a/runtime/core/fortran.ncl +++ b/runtime/core/fortran.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "fortran", diff --git a/runtime/core/franklin.ncl b/runtime/core/franklin.ncl index af3a5810..2ffff2ac 100644 --- a/runtime/core/franklin.ncl +++ b/runtime/core/franklin.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "franklin", diff --git a/runtime/core/fulcio.ncl b/runtime/core/fulcio.ncl index 53c93b39..08425941 100644 --- a/runtime/core/fulcio.ncl +++ b/runtime/core/fulcio.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "fulcio", diff --git a/runtime/core/ghjk.ncl b/runtime/core/ghjk.ncl index 7337ff68..58c1fdb4 100644 --- a/runtime/core/ghjk.ncl +++ b/runtime/core/ghjk.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "ghjk", diff --git a/runtime/core/git-crypt.ncl b/runtime/core/git-crypt.ncl index 689a3b3b..047041fd 100644 --- a/runtime/core/git-crypt.ncl +++ b/runtime/core/git-crypt.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "git-crypt", diff --git a/runtime/core/gitleaks.ncl b/runtime/core/gitleaks.ncl index 1094c77e..56b98188 100644 --- a/runtime/core/gitleaks.ncl +++ b/runtime/core/gitleaks.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "gitleaks", diff --git a/runtime/core/gleam.ncl b/runtime/core/gleam.ncl index 3219dc74..3330624d 100644 --- a/runtime/core/gleam.ncl +++ b/runtime/core/gleam.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "gleam", description = "gleam — A friendly language for building type-safe systems", diff --git a/runtime/core/gnat.ncl b/runtime/core/gnat.ncl index 8334f176..67a1bf4f 100644 --- a/runtime/core/gnat.ncl +++ b/runtime/core/gnat.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "gnat", diff --git a/runtime/core/gnucobol.ncl b/runtime/core/gnucobol.ncl index dce0ce32..c2992295 100644 --- a/runtime/core/gnucobol.ncl +++ b/runtime/core/gnucobol.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "gnucobol", diff --git a/runtime/core/golang.ncl b/runtime/core/golang.ncl index 03821a8c..e5fbae7b 100644 --- a/runtime/core/golang.ncl +++ b/runtime/core/golang.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "golang", description = "golang — The Go programming language", diff --git a/runtime/core/grype.ncl b/runtime/core/grype.ncl index 0b26dfbf..07a08a36 100644 --- a/runtime/core/grype.ncl +++ b/runtime/core/grype.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "grype", diff --git a/runtime/core/haproxy.ncl b/runtime/core/haproxy.ncl index ccd54a7c..ab503f62 100644 --- a/runtime/core/haproxy.ncl +++ b/runtime/core/haproxy.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "haproxy", diff --git a/runtime/core/httpd.ncl b/runtime/core/httpd.ncl index fd9b32ec..5c2eb00d 100644 --- a/runtime/core/httpd.ncl +++ b/runtime/core/httpd.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "httpd", diff --git a/runtime/core/influxdb.ncl b/runtime/core/influxdb.ncl index 61069550..a977eb81 100644 --- a/runtime/core/influxdb.ncl +++ b/runtime/core/influxdb.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "influxdb", diff --git a/runtime/core/julia.ncl b/runtime/core/julia.ncl index 8640e6ea..5aea902d 100644 --- a/runtime/core/julia.ncl +++ b/runtime/core/julia.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "julia", description = "julia — High-performance dynamic programming language", diff --git a/runtime/core/just.ncl b/runtime/core/just.ncl index c156140c..07f310c0 100644 --- a/runtime/core/just.ncl +++ b/runtime/core/just.ncl @@ -4,7 +4,7 @@ # OPSM Runtime Plugin: just # A command runner — handy way to save and run project-specific commands. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "just", diff --git a/runtime/core/kdl-fmt.ncl b/runtime/core/kdl-fmt.ncl index 6b6e5f3d..dcfacb34 100644 --- a/runtime/core/kdl-fmt.ncl +++ b/runtime/core/kdl-fmt.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "kdl-fmt", diff --git a/runtime/core/kubectl.ncl b/runtime/core/kubectl.ncl index 4b0c6276..69e26d30 100644 --- a/runtime/core/kubectl.ncl +++ b/runtime/core/kubectl.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "kubectl", description = "kubectl — Kubernetes command-line tool", diff --git a/runtime/core/lefthook.ncl b/runtime/core/lefthook.ncl index 2d9f05b0..05660c1d 100644 --- a/runtime/core/lefthook.ncl +++ b/runtime/core/lefthook.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "lefthook", description = "lefthook — Fast and powerful Git hooks manager", diff --git a/runtime/core/lego.ncl b/runtime/core/lego.ncl index 8c59e1ab..d5f94793 100644 --- a/runtime/core/lego.ncl +++ b/runtime/core/lego.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "lego", diff --git a/runtime/core/linkerd.ncl b/runtime/core/linkerd.ncl index b213cdb6..5657836c 100644 --- a/runtime/core/linkerd.ncl +++ b/runtime/core/linkerd.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "linkerd", diff --git a/runtime/core/mariadb.ncl b/runtime/core/mariadb.ncl index 60d37b47..69f33322 100644 --- a/runtime/core/mariadb.ncl +++ b/runtime/core/mariadb.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "mariadb", diff --git a/runtime/core/mdbook.ncl b/runtime/core/mdbook.ncl index db99d32b..ee36bd13 100644 --- a/runtime/core/mdbook.ncl +++ b/runtime/core/mdbook.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "mdbook", diff --git a/runtime/core/melange.ncl b/runtime/core/melange.ncl index 2255546f..05cfee82 100644 --- a/runtime/core/melange.ncl +++ b/runtime/core/melange.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "melange", diff --git a/runtime/core/mysql.ncl b/runtime/core/mysql.ncl index d657ebd3..958f72f5 100644 --- a/runtime/core/mysql.ncl +++ b/runtime/core/mysql.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "mysql", diff --git a/runtime/core/neo4j.ncl b/runtime/core/neo4j.ncl index cf19b717..5118df7e 100644 --- a/runtime/core/neo4j.ncl +++ b/runtime/core/neo4j.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "neo4j", diff --git a/runtime/core/nickel.ncl b/runtime/core/nickel.ncl index 82011d3f..1f0eb5df 100644 --- a/runtime/core/nickel.ncl +++ b/runtime/core/nickel.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "nickel", diff --git a/runtime/core/nim.ncl b/runtime/core/nim.ncl index 181b0f54..79d98cd6 100644 --- a/runtime/core/nim.ncl +++ b/runtime/core/nim.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "nim", description = "nim — Efficient, expressive, elegant systems language", diff --git a/runtime/core/nodejs.ncl b/runtime/core/nodejs.ncl index fc882adc..8fd5c042 100644 --- a/runtime/core/nodejs.ncl +++ b/runtime/core/nodejs.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "nodejs", description = "nodejs — JavaScript runtime built on V8", diff --git a/runtime/core/ocaml.ncl b/runtime/core/ocaml.ncl index d18b11b3..67f3ce9d 100644 --- a/runtime/core/ocaml.ncl +++ b/runtime/core/ocaml.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "ocaml", diff --git a/runtime/core/opa.ncl b/runtime/core/opa.ncl index d35913c1..875dea25 100644 --- a/runtime/core/opa.ncl +++ b/runtime/core/opa.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "opa", diff --git a/runtime/core/openlitespeed.ncl b/runtime/core/openlitespeed.ncl index bea3a503..b25d2cea 100644 --- a/runtime/core/openlitespeed.ncl +++ b/runtime/core/openlitespeed.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "openlitespeed", diff --git a/runtime/core/openssh.ncl b/runtime/core/openssh.ncl index 24cfe0fd..2fbea683 100644 --- a/runtime/core/openssh.ncl +++ b/runtime/core/openssh.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "openssh", diff --git a/runtime/core/openssl.ncl b/runtime/core/openssl.ncl index d7fba1e2..169b02ee 100644 --- a/runtime/core/openssl.ncl +++ b/runtime/core/openssl.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "openssl", diff --git a/runtime/core/orchid.ncl b/runtime/core/orchid.ncl index 23563ee8..95185339 100644 --- a/runtime/core/orchid.ncl +++ b/runtime/core/orchid.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "orchid", diff --git a/runtime/core/pollen.ncl b/runtime/core/pollen.ncl index 814f52c4..01ef0603 100644 --- a/runtime/core/pollen.ncl +++ b/runtime/core/pollen.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "pollen", diff --git a/runtime/core/pomerium.ncl b/runtime/core/pomerium.ncl index 18c84ead..3a767a58 100644 --- a/runtime/core/pomerium.ncl +++ b/runtime/core/pomerium.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "pomerium", diff --git a/runtime/core/rekor.ncl b/runtime/core/rekor.ncl index fb809527..40d80d96 100644 --- a/runtime/core/rekor.ncl +++ b/runtime/core/rekor.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "rekor", diff --git a/runtime/core/rescript.ncl b/runtime/core/rescript.ncl index 96dec572..93a5d2f5 100644 --- a/runtime/core/rescript.ncl +++ b/runtime/core/rescript.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "rescript", diff --git a/runtime/core/restic.ncl b/runtime/core/restic.ncl index c66c835b..ec2178f7 100644 --- a/runtime/core/restic.ncl +++ b/runtime/core/restic.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "restic", diff --git a/runtime/core/rethinkdb.ncl b/runtime/core/rethinkdb.ncl index 16effeb8..cd406dd0 100644 --- a/runtime/core/rethinkdb.ncl +++ b/runtime/core/rethinkdb.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "rethinkdb", diff --git a/runtime/core/serum.ncl b/runtime/core/serum.ncl index 4ac5d4d7..52ecd2d1 100644 --- a/runtime/core/serum.ncl +++ b/runtime/core/serum.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "serum", diff --git a/runtime/core/sops.ncl b/runtime/core/sops.ncl index ac257c5e..e19bfded 100644 --- a/runtime/core/sops.ncl +++ b/runtime/core/sops.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "sops", diff --git a/runtime/core/step-ca.ncl b/runtime/core/step-ca.ncl index a0174c47..ec1e4cee 100644 --- a/runtime/core/step-ca.ncl +++ b/runtime/core/step-ca.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "step-ca", diff --git a/runtime/core/surrealdb.ncl b/runtime/core/surrealdb.ncl index 780035f2..fe0e4b82 100644 --- a/runtime/core/surrealdb.ncl +++ b/runtime/core/surrealdb.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "surrealdb", diff --git a/runtime/core/syft.ncl b/runtime/core/syft.ncl index dbe053f1..d59101b5 100644 --- a/runtime/core/syft.ncl +++ b/runtime/core/syft.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "syft", diff --git a/runtime/core/taplo.ncl b/runtime/core/taplo.ncl index 3096ea8b..896c7c6b 100644 --- a/runtime/core/taplo.ncl +++ b/runtime/core/taplo.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "taplo", diff --git a/runtime/core/trivy.ncl b/runtime/core/trivy.ncl index e59a8f22..7c453ac8 100644 --- a/runtime/core/trivy.ncl +++ b/runtime/core/trivy.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "trivy", diff --git a/runtime/core/typst.ncl b/runtime/core/typst.ncl index f2d48105..ff98867b 100644 --- a/runtime/core/typst.ncl +++ b/runtime/core/typst.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "typst", description = "typst — A new markup-based typesetting system", diff --git a/runtime/core/varnish.ncl b/runtime/core/varnish.ncl index ad407127..3b9c4fbf 100644 --- a/runtime/core/varnish.ncl +++ b/runtime/core/varnish.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "varnish", diff --git a/runtime/core/virtuoso.ncl b/runtime/core/virtuoso.ncl index 2323ceab..288b19ce 100644 --- a/runtime/core/virtuoso.ncl +++ b/runtime/core/virtuoso.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "virtuoso", diff --git a/runtime/core/vlang.ncl b/runtime/core/vlang.ncl index 144e0e8c..2a41bff6 100644 --- a/runtime/core/vlang.ncl +++ b/runtime/core/vlang.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "vlang", diff --git a/runtime/core/wasmer.ncl b/runtime/core/wasmer.ncl index 37b5fa93..e0aad46b 100644 --- a/runtime/core/wasmer.ncl +++ b/runtime/core/wasmer.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "wasmer", description = "wasmer — Universal WebAssembly Runtime", diff --git a/runtime/core/wasmtime.ncl b/runtime/core/wasmtime.ncl index 225f4f7b..4114aa67 100644 --- a/runtime/core/wasmtime.ncl +++ b/runtime/core/wasmtime.ncl @@ -1,5 +1,5 @@ # SPDX-License-Identifier: MPL-2.0 -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "wasmtime", description = "wasmtime — Fast and secure WebAssembly runtime", diff --git a/runtime/core/yj.ncl b/runtime/core/yj.ncl index 919b3ff8..a6f79c86 100644 --- a/runtime/core/yj.ncl +++ b/runtime/core/yj.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "yj", diff --git a/runtime/core/yq.ncl b/runtime/core/yq.ncl index 9eea25e3..a794737f 100644 --- a/runtime/core/yq.ncl +++ b/runtime/core/yq.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "yq", diff --git a/runtime/core/zig.ncl b/runtime/core/zig.ncl index 7675a8e1..06e70a38 100644 --- a/runtime/core/zig.ncl +++ b/runtime/core/zig.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "zig", diff --git a/runtime/core/zola.ncl b/runtime/core/zola.ncl index aa9865df..6daaebc6 100644 --- a/runtime/core/zola.ncl +++ b/runtime/core/zola.ncl @@ -9,7 +9,7 @@ # archive format) in OPSM's Nickel contract format. The asdf plugin # ecosystem's pioneering work is gratefully acknowledged. -let { RuntimePlugin, .. } = import "../contract/runtime-plugin.ncl" in +let RuntimePlugin = import "../contract/runtime-plugin.ncl" in { name = "zola",