#!/bin/bash
# Manage the approved dev-dependency list for a project or, with --global,
# every project. Approvals live host-side (never mounted into the
# container), so a sandboxed session can ask for a package but can never
# grant itself one - the same split bin/dejima-allow uses for
# domains.
#
# Usage: dejima-dep <add|remove|list> [--global | --project [path]] [<spec>]
#   dejima-dep add --project ~/myproject cargo:cargo-nextest
#   dejima-dep add --project cargo:cargo-nextest   # the project in $PWD
#   dejima-dep add --global cargo:cargo-nextest
#   dejima-dep list
#
# A spec is <apt|npm|pip|cargo>:<name>, or tarball:<name>@<sha256>:<url> for a
# release archive no package ecosystem ships in a usable version (a current Go
# toolchain, say). The dependencies actually installed
# for a project are the union of its global and project approvals.
set -euo pipefail

# --help before anything else runs: no libraries sourced, no config read. The
# moment someone reaches for it is often the moment the config is wrong.
# stdout and exit 0, where usage() stays on stderr and exits nonzero.
case "${1:-}" in
    -h|--help)
        cat <<'DEJIMA_DEP_HELP'
dejima-dep - approve a package a project needs, so its image carries it

Usage: dejima-dep <add|remove|list> [--global | --project [path]] [<spec>]

  add <spec>       approve it; the image is rebuilt with it at the next launch
  remove <spec>    withdraw the approval
  list             show what is approved in the scope given

Scope:
  --project [path]   one project, every session in it. Path defaults to $PWD.
  --global           every project on this host.
  (none)             same as --project for the current directory.

A spec is <apt|npm|pip|cargo>:<name>, optionally @<version>, or a release
archive as tarball:<name>@<sha256>:<url>. The digest is required and checked at
build time, because a tarball is code that will run in every future session.

Approving is a separate act from declaring. A project declares what it needs in
.dejima/dependencies, which is inside the workspace and therefore writable by
the session; nothing is installed until an operator approves it here. Use
`dejima-project status` to see what a project is asking for.

Nothing is installed into a running container: a rebuild happens at the next
launch, so an approval made mid-session takes effect on the one after it.

See docs/configuration.md.
DEJIMA_DEP_HELP
        exit 0 ;;
esac

SCRIPT_DIR="$(cd "$(dirname "$(readlink -f "${BASH_SOURCE[0]}")")" && pwd)"
# shellcheck source=dejima-project-lib.sh
source "$SCRIPT_DIR/dejima-project-lib.sh"

action=""
spec=""
project=""
global=0

usage() {
    echo "Usage: dejima-dep <add|remove|list> [--global | --project [path]] [<spec>]" >&2
    exit 1
}

# The action is read first so the required trailing count is known before
# parsing the rest: add/remove need a spec after any --project path, list does
# not - and that count is exactly what decides whether a token after --project
# is the path or the spec.
case "${1:-}" in
    add|remove) action="$1"; shift; required=1 ;;
    list)       action="$1"; shift; required=0 ;;
    *)          usage ;;
esac

parse_scope_args "$required" "$@" || usage
case "${#dj_positional[@]}" in
    0) spec="" ;;
    1) spec="${dj_positional[0]}" ;;
    *) usage ;;
esac
global="$dj_global"
[ "$dj_project_set" -eq 0 ] || project="${dj_project:-$PWD}"

# The shared parser knows --container because dejima-allow needs it;
# accepting it silently here would give project scope - a persistent grant -
# to a caller who believes they scoped an approval to one session, with a
# zero exit telling them they were right.
if [ -n "$dj_container" ]; then
    echo "dejima-dep has no --container scope: a dependency needs an image" >&2
    echo "rebuild, so there is nothing to patch in a live session." >&2
    exit 1
fi

# An entry belongs to exactly one scope; silently picking one would misplace
# an approval.
if [ "$global" -eq 1 ] && [ "$dj_project_set" -eq 1 ]; then
    echo "--global and --project are mutually exclusive." >&2
    exit 1
fi

# With --global there is no project path to resolve; doing it anyway would
# put a misleading $PWD-derived path in error messages.
if [ "$global" -eq 0 ]; then
    project="$(cd "${project:-$PWD}" && pwd)"
    disclose_project_symlink "$project"
fi

case "$action" in
    list)
        if [ "$global" -eq 1 ]; then
            approved_entries "$(config_root)" dependencies
        else
            store="$(resolve_project_store "$project")"
            [ -n "$store" ] || exit 0
            approved_entries "$store" dependencies
        fi
        ;;
    add)
        [ -n "$spec" ] || usage
        valid_dep_spec "$spec" || { echo "Not a valid dependency spec: $spec" >&2; echo "Expected <apt|npm|pip|cargo>:<name>, or tarball:<name>@<sha256>:<url>" >&2; exit 1; }
        if [ "$global" -eq 1 ]; then
            store="$(ensure_global_store)"
            approve_entry "$store" dependencies "$spec"
            echo "Approved $spec globally (every project)."
        else
            store="$(ensure_project_store "$project")"
            approve_entry "$store" dependencies "$spec"
            echo "Approved $spec for $project."
            # Printed only after the approval above, and guarded rather than
            # left to set -e: the approval is what grants, and it is already
            # written by this point. Letting a declare_entry failure abort
            # here would exit before this echo ever ran, leaving the operator
            # thinking nothing happened when a spec had, in fact, just been
            # approved.
            if declare_entry "$project" dependencies "$spec"; then
                echo "Declared it in $project/.dejima/dependencies - commit that to share it."
            else
                echo "The approval above IS recorded; only the project's own .dejima" >&2
                echo "declaration could not be written." >&2
            fi
        fi
        # Unlike dejima-allow, this cannot touch a running session:
        # it needs an image rebuild. Say so, or an agent will keep insisting
        # the dependency is missing right after the user added it.
        echo "Takes effect on the next launch (bin/dejima rebuilds the project image)."
        ;;
    remove)
        [ -n "$spec" ] || usage
        if [ "$global" -eq 1 ]; then
            # Same rule as the project path below: with no store there is
            # nothing to remove from, and confirming a removal that never
            # happened is how a typo'd or premature remove looks like it
            # worked.
            store="$(config_root)"
            [ -d "$store" ] || { echo "No global approved config." >&2; exit 1; }
            unapprove_entry "$store" dependencies "$spec"
            echo "Removed $spec from the global dependencies (takes effect on the next launch)."
        else
            store="$(resolve_project_store "$project")"
            [ -n "$store" ] || { echo "No approved config for $project." >&2; exit 1; }
            unapprove_entry "$store" dependencies "$spec"
            echo "Removed $spec from $project (takes effect on the next launch)."
            # `add --project` writes a declaration; this deliberately does not
            # take one back, since withdrawing one machine's approval is not
            # the same decision as editing a file the whole team shares. That
            # asymmetry, left silent, is self-undoing: the entry goes
            # back to `pending`, the session briefing reports it missing, and
            # the next `dejima-project approve` re-approves it - so say where
            # the durable copy is rather than letting the removal look final.
            # -f before the grep, not a bare grep: this reads a path the
            # sandboxed session owns, and a FIFO planted there would hang the
            # command outright.
            if [ -f "$project/.dejima/dependencies" ] &&
                grep -qxF "$spec" "$project/.dejima/dependencies" 2>/dev/null; then
                echo "$project/.dejima/dependencies still declares it - this command does"
                echo "not edit the project's committed declaration, so dejima-project"
                echo "approve will offer it again. Edit that file to drop it for good."
            fi
        fi
        ;;
    *) usage ;;
esac
