mirror of
https://github.com/safedep/pmg.git
synced 2026-08-03 07:24:09 +02:00
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:
+46
-3
@@ -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 {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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=
|
||||||
|
|||||||
Executable
+138
@@ -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!"
|
||||||
Reference in New Issue
Block a user