feat: add macOS setup script for Jamf deployment (#258)

* feat: add macOS setup script for Jamf deployment

Adds scripts/pmg_setup_install.sh that installs/updates pmg (via
Homebrew or GitHub releases), runs pmg setup install, and optionally
enables cloud sync with credentials stored in macOS Keychain.

* feat: add --from-env flag to pmg cloud login

Allows non-interactive credential import from SAFEDEP_API_KEY and
SAFEDEP_TENANT_ID environment variables. Fails explicitly if either
is missing. Used by the setup script for Jamf deployments.

* refactor: use cloud.NewEnvCredentialResolver for --from-env

Use the dry library's env credential resolver instead of reading
env vars directly, keeping env var ownership in the shared library.

* update DRY

* update go to 1.25 back
This commit is contained in:
Sahil Bansal
2026-05-13 15:49:43 +00:00
committed by GitHub
parent 6249d0781d
commit 4de9f84c0c
4 changed files with 214 additions and 37 deletions
+46 -3
View File
@@ -8,16 +8,58 @@ import (
"github.com/spf13/cobra" "github.com/spf13/cobra"
) )
var loginFromEnv bool
func newLoginCommand() *cobra.Command { func newLoginCommand() *cobra.Command {
return &cobra.Command{ cmd := &cobra.Command{
Use: "login", Use: "login",
Short: "Store SafeDep Cloud credentials securely", Short: "Store SafeDep Cloud credentials securely",
RunE: runLogin, RunE: runLogin,
} }
cmd.Flags().BoolVar(&loginFromEnv, "from-env", false,
"Read credentials from SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables")
return cmd
} }
func runLogin(cmd *cobra.Command, args []string) error { func runLogin(cmd *cobra.Command, args []string) error {
tenantID, err := ui.PromptInput("Tenant ID: ") var tenantID, apiKey string
if loginFromEnv {
resolver, err := cloud.NewEnvCredentialResolver()
if err != nil {
ui.ErrorExit(usefulerror.Useful().
Wrap(err).
WithCode(usefulerror.ErrCodeLifecycle).
WithHumanError("Failed to create environment credential resolver"))
}
creds, err := resolver.Resolve()
if err != nil {
ui.ErrorExit(usefulerror.Useful().
Wrap(err).
WithCode(usefulerror.ErrCodeInvalidArgument).
WithHumanError("Failed to resolve credentials from environment").
WithHelp("Set SAFEDEP_API_KEY and SAFEDEP_TENANT_ID environment variables"))
}
apiKey, err = creds.GetAPIKey()
if err != nil || apiKey == "" {
ui.ErrorExit(usefulerror.Useful().
WithCode(usefulerror.ErrCodeInvalidArgument).
WithHumanError("SAFEDEP_API_KEY environment variable is not set"))
}
tenantID, err = creds.GetTenantDomain()
if err != nil || tenantID == "" {
ui.ErrorExit(usefulerror.Useful().
WithCode(usefulerror.ErrCodeInvalidArgument).
WithHumanError("SAFEDEP_TENANT_ID environment variable is not set"))
}
} else {
var err error
tenantID, err = ui.PromptInput("Tenant ID: ")
if err != nil { if err != nil {
ui.ErrorExit(usefulerror.Useful(). ui.ErrorExit(usefulerror.Useful().
Wrap(err). Wrap(err).
@@ -31,7 +73,7 @@ func runLogin(cmd *cobra.Command, args []string) error {
WithHumanError("Tenant ID cannot be empty")) WithHumanError("Tenant ID cannot be empty"))
} }
apiKey, err := ui.PromptSecret("API Key: ") apiKey, err = ui.PromptSecret("API Key: ")
if err != nil { if err != nil {
ui.ErrorExit(usefulerror.Useful(). ui.ErrorExit(usefulerror.Useful().
Wrap(err). Wrap(err).
@@ -44,6 +86,7 @@ func runLogin(cmd *cobra.Command, args []string) error {
WithCode(usefulerror.ErrCodeInvalidArgument). WithCode(usefulerror.ErrCodeInvalidArgument).
WithHumanError("API Key cannot be empty")) WithHumanError("API Key cannot be empty"))
} }
}
store, err := cloud.NewKeychainCredentialStore() store, err := cloud.NewKeychainCredentialStore()
if err != nil { if err != nil {
+3 -3
View File
@@ -10,10 +10,10 @@ require (
github.com/fatih/color v1.18.0 github.com/fatih/color v1.18.0
github.com/google/osv-scalibr v0.2.1 github.com/google/osv-scalibr v0.2.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/jedib0t/go-pretty/v6 v6.6.7 github.com/jedib0t/go-pretty/v6 v6.7.9
github.com/landlock-lsm/go-landlock v0.7.0 github.com/landlock-lsm/go-landlock v0.7.0
github.com/posthog/posthog-go v1.5.12 github.com/posthog/posthog-go v1.5.12
github.com/safedep/dry v0.0.0-20260411074023-b589e91de472 github.com/safedep/dry v0.0.0-20260513152148-f809919cc4ce
github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12 github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12
github.com/sony/gobreaker/v2 v2.4.0 github.com/sony/gobreaker/v2 v2.4.0
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
@@ -35,7 +35,7 @@ require (
github.com/Masterminds/semver/v3 v3.3.1 // indirect github.com/Masterminds/semver/v3 v3.3.1 // indirect
github.com/caarlos0/env/v11 v11.3.1 // indirect github.com/caarlos0/env/v11 v11.3.1 // indirect
github.com/clipperhouse/stringish v0.1.1 // indirect github.com/clipperhouse/stringish v0.1.1 // indirect
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect github.com/clipperhouse/uax29/v2 v2.5.0 // indirect
github.com/danieljoos/wincred v1.2.2 // indirect github.com/danieljoos/wincred v1.2.2 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/denisbrodbeck/machineid v1.0.1 // indirect github.com/denisbrodbeck/machineid v1.0.1 // indirect
+6 -10
View File
@@ -2,12 +2,8 @@ al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXy
al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240508200655-46a4cf4ba109.1 h1:NXwdBG3BiC6xWH4iG3csbT+JHF9u1jl5ThDhumCnKnk= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240508200655-46a4cf4ba109.1 h1:NXwdBG3BiC6xWH4iG3csbT+JHF9u1jl5ThDhumCnKnk=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240508200655-46a4cf4ba109.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM= buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20240508200655-46a4cf4ba109.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1 h1:OKKSMXf1k7NZ1qcp5gfpTw0F3QGVM/X6ghkRs9GtrIA=
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260409081445-73994c4e35a3.1/go.mod h1:k1tkPvr2SWI5hWfY/fF9XgIBh1sZOMPMPl/bUH3L0wE=
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260507092425-ac47f9a19339.1 h1:TgL0Xu+EFQhr68C/eGrflL+TaFtEUWkfWXQ+f+vWGdg= buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260507092425-ac47f9a19339.1 h1:TgL0Xu+EFQhr68C/eGrflL+TaFtEUWkfWXQ+f+vWGdg=
buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260507092425-ac47f9a19339.1/go.mod h1:AU7tshd3hSyemWEYn6mofmWPUJaWmGExFHAQSjp6N4o= buf.build/gen/go/safedep/api/grpc/go v1.6.1-20260507092425-ac47f9a19339.1/go.mod h1:AU7tshd3hSyemWEYn6mofmWPUJaWmGExFHAQSjp6N4o=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1 h1:x5a6h/YeT2cqgxqmBGOCKlifMEMb4VRJ3eOgQkNvJrs=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260409081445-73994c4e35a3.1/go.mod h1:I8E+sZXJNqzWBtSlRGCoiEorLSRiix50h2R/66aBzME=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260507092425-ac47f9a19339.1 h1:JRzHMhoJg1Mlae+PR+ZZ1I1aaqAJdyF4WY3CidJD/us= buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260507092425-ac47f9a19339.1 h1:JRzHMhoJg1Mlae+PR+ZZ1I1aaqAJdyF4WY3CidJD/us=
buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260507092425-ac47f9a19339.1/go.mod h1:I8E+sZXJNqzWBtSlRGCoiEorLSRiix50h2R/66aBzME= buf.build/gen/go/safedep/api/protocolbuffers/go v1.36.11-20260507092425-ac47f9a19339.1/go.mod h1:I8E+sZXJNqzWBtSlRGCoiEorLSRiix50h2R/66aBzME=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
@@ -27,8 +23,8 @@ github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XL
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= github.com/clipperhouse/uax29/v2 v2.5.0 h1:x7T0T4eTHDONxFJsL94uKNKPHrclyFI0lm7+w94cO8U=
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= github.com/clipperhouse/uax29/v2 v2.5.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5 h1:6xNmx7iTtyBRev0+D/Tv1FZd4SCg8axKApyNyRsAt/w=
github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI= github.com/cncf/xds/go v0.0.0-20251210132809-ee656c7534f5/go.mod h1:KdCmV+x/BuvyMxRnYBlmVaq4OLiKW6iRQfvC62cvdkI=
@@ -128,8 +124,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A=
github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo=
github.com/jedib0t/go-pretty/v6 v6.6.7 h1:m+LbHpm0aIAPLzLbMfn8dc3Ht8MW7lsSO4MPItz/Uuo= github.com/jedib0t/go-pretty/v6 v6.7.9 h1:frarzQWmkZd97syT81+TH8INKPpzoxQnk+Mk5EIHSrM=
github.com/jedib0t/go-pretty/v6 v6.6.7/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU= github.com/jedib0t/go-pretty/v6 v6.7.9/go.mod h1:YwC5CE4fJ1HFUDeivSV1r//AmANFHyqczZk+U6BDALU=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -176,8 +172,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/safedep/dry v0.0.0-20260411074023-b589e91de472 h1:8CztWcu+C7alCjAS5AVDRhxr/LL/9y6lHwpZKmxZjvE= github.com/safedep/dry v0.0.0-20260513152148-f809919cc4ce h1:0FxzXTmSrMWse6u0Wv2CbZOjXB1EFZE1qjhF4vB9NOg=
github.com/safedep/dry v0.0.0-20260411074023-b589e91de472/go.mod h1:JOiCF9w4plbcMS6XnLNpDaSoUEabdscP+eFk+kROHrs= github.com/safedep/dry v0.0.0-20260513152148-f809919cc4ce/go.mod h1:tKhOr0osgpefdBbxG8n4H5gYH1GLXidKS+hzNbRQ9LQ=
github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12 h1:NzARvPtncPbVI8a8Z0JKpJ7XJCSPpsRstV7wAgEtmOU= github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12 h1:NzARvPtncPbVI8a8Z0JKpJ7XJCSPpsRstV7wAgEtmOU=
github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12/go.mod h1:fyt+PACz6dtEoqsnE0BPPv/lHpuBG/8zkDqeIVcyRY4= github.com/safedep/ptyx v0.2.1-0.20260119085117-f667570c2d12/go.mod h1:fyt+PACz6dtEoqsnE0BPPv/lHpuBG/8zkDqeIVcyRY4=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc= github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
+138
View File
@@ -0,0 +1,138 @@
#!/bin/bash
# pmg_setup_install.sh — Install/update PMG, configure it, and enable cloud sync.
#
# This script is intended to be packaged and deployed via Jamf or similar MDM tools.
#
# Usage:
# ./pmg_setup_install.sh
# SAFEDEP_API_KEY=... SAFEDEP_TENANT_ID=... ./pmg_setup_install.sh
#
# Environment variables:
# SAFEDEP_API_KEY — SafeDep Cloud API key (enables cloud sync when set with tenant ID)
# SAFEDEP_TENANT_ID — SafeDep Cloud tenant ID
#
# What it does:
# 1. Installs or updates pmg (via Homebrew if available, otherwise from GitHub releases)
# 2. Runs `pmg setup install` to create config, shell aliases, and PATH shims
# 3. Enables cloud sync and stores credentials in macOS Keychain (if both env vars are set)
set -euo pipefail
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "Error: this script is for macOS only" >&2
exit 1
fi
REPO="safedep/pmg"
CLOUD_API_KEY="${SAFEDEP_API_KEY:-}"
CLOUD_TENANT_ID="${SAFEDEP_TENANT_ID:-}"
log() { echo "==> $*"; }
# ── Install or update pmg ───────────────────────────────────────────────────
install_via_brew() {
local brew_bin="$1"
log "Installing/updating pmg via Homebrew"
if "$brew_bin" ls --versions safedep/tap/pmg &>/dev/null; then
log "pmg is already installed, upgrading"
"$brew_bin" upgrade safedep/tap/pmg || true
else
"$brew_bin" install safedep/tap/pmg
fi
}
install_via_release() {
log "Homebrew not found, installing pmg from GitHub releases"
local install_dir="/usr/local/bin"
log "Fetching latest release..."
tag=$(curl -fsSI -o /dev/null -w '%{redirect_url}' "https://github.com/${REPO}/releases/latest" | sed 's|.*/||')
if [[ -z "$tag" ]]; then
echo "Error: could not determine latest release" >&2
exit 1
fi
log "Latest release: $tag"
asset="pmg_Darwin_all.tar.gz"
url="https://github.com/${REPO}/releases/download/${tag}/${asset}"
checksums_url="https://github.com/${REPO}/releases/download/${tag}/checksums.txt"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
log "Downloading $asset"
curl -fsSL -o "${tmpdir}/${asset}" "$url"
curl -fsSL -o "${tmpdir}/checksums.txt" "$checksums_url"
expected=$(grep " ${asset}$" "${tmpdir}/checksums.txt" | cut -d' ' -f1)
if [[ -z "$expected" ]]; then
echo "Error: no checksum entry found for ${asset}" >&2
exit 1
fi
actual=$(shasum -a 256 "${tmpdir}/${asset}" | cut -d' ' -f1)
if [[ "$actual" != "$expected" ]]; then
echo "Error: checksum mismatch for ${asset}" >&2
echo " expected: $expected" >&2
echo " actual: $actual" >&2
exit 1
fi
log "Checksum verified"
tar -xzf "${tmpdir}/${asset}" -C "${tmpdir}" pmg
if [[ -w "$install_dir" ]]; then
install -m 755 "${tmpdir}/pmg" "${install_dir}/pmg"
else
sudo install -m 755 "${tmpdir}/pmg" "${install_dir}/pmg"
fi
log "Installed pmg $tag to ${install_dir}/pmg"
}
BREW_BIN=""
for candidate in "/opt/homebrew/bin/brew" "/usr/local/bin/brew"; do
if [[ -x "$candidate" ]]; then
BREW_BIN="$candidate"
break
fi
done
if [[ -n "$BREW_BIN" ]]; then
install_via_brew "$BREW_BIN"
else
install_via_release
fi
if ! command -v pmg &>/dev/null; then
echo "Error: pmg not found in PATH after install" >&2
exit 1
fi
log "pmg installed: $(pmg version 2>/dev/null || echo 'unknown')"
# ── Run pmg setup ────────────────────────────────────────────────────────────
log "Running pmg setup install"
pmg setup install
# ── Enable cloud sync ───────────────────────────────────────────────────────
if [[ -n "$CLOUD_API_KEY" && -n "$CLOUD_TENANT_ID" ]]; then
log "Enabling cloud sync"
CONFIG_FILE="${HOME}/Library/Application Support/safedep/pmg/config.yml"
if [[ -f "$CONFIG_FILE" ]]; then
awk '
/^cloud:/ { in_cloud=1 }
in_cloud && /^ enabled: false/ { sub(/enabled: false/, "enabled: true"); in_cloud=0 }
/^[a-z]/ && !/^cloud:/ { in_cloud=0 }
{ print }
' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
log "Cloud sync enabled in config"
fi
SAFEDEP_API_KEY="$CLOUD_API_KEY" SAFEDEP_TENANT_ID="$CLOUD_TENANT_ID" pmg cloud login --from-env
log "Credentials stored securely"
fi
# ── Done ─────────────────────────────────────────────────────────────────────
log "pmg setup complete!"