Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
54 commits
Select commit Hold shift + click to select a range
1b8f2a9
switch to other account on logout
miloschwartz Jan 9, 2026
d6f8870
fix perist org switch
miloschwartz Jan 9, 2026
822b17a
set user agent
miloschwartz Jan 13, 2026
882ea7a
add check if device is blocked
miloschwartz Jan 13, 2026
9847022
feat(fingerprint): init internal/fingerprint package structure
water-sucks Jan 12, 2026
ffa354e
feat(fingerprint): add macos-specific fingerprint/posture query impls
water-sucks Jan 12, 2026
d565650
feat(fingerprint): add linux-specific fingerprint/posture query impls
water-sucks Jan 12, 2026
4308264
feat(fingerprint): add windows-specific fingerprint/posture query impls
water-sucks Jan 13, 2026
f536e6d
chore(deps): use new olm version, update package references
water-sucks Jan 13, 2026
6001139
feat(fingerprint): start fingerprinting routine when client comes up
water-sucks Jan 13, 2026
8afeb9a
fix(fingerprint): use sudo user when possible on linux
water-sucks Jan 14, 2026
fc58b8e
feat(fingerprint): add linux platform fingerprint hash
water-sucks Jan 14, 2026
3d806f5
feat(fingerprint): add windows platform fingerprint hash
water-sucks Jan 15, 2026
25714cf
feat(fingerprint): add macos platform fingerprint hash
water-sucks Jan 15, 2026
88f476e
chore(deps): pin olm version to remote
water-sucks Jan 15, 2026
a50674e
feat: add prettier device names for linux and macos devices
water-sucks Jan 16, 2026
a2f3ac0
revert: use previous account save/delete mechanism with login/logout
water-sucks Jan 14, 2026
0d5ce5a
feat(login): recover olm device when possible
water-sucks Jan 14, 2026
4e5f708
fix(fingerprint): resolve import cycles, standardize output
water-sucks Jan 16, 2026
7ec8e1f
fix(fingerprint): cache platform fingerprint hash when running as root
water-sucks Jan 16, 2026
69df31a
add bubble up error from olm
miloschwartz Jan 17, 2026
7fa7737
feat(fingerprint): init internal/fingerprint package structure
water-sucks Jan 12, 2026
ac14da7
feat(fingerprint): add macos-specific fingerprint/posture query impls
water-sucks Jan 12, 2026
3a804da
feat(fingerprint): add linux-specific fingerprint/posture query impls
water-sucks Jan 12, 2026
b95dec5
feat(fingerprint): add windows-specific fingerprint/posture query impls
water-sucks Jan 13, 2026
3853fd2
chore(deps): use new olm version, update package references
water-sucks Jan 13, 2026
858e1f6
feat(fingerprint): start fingerprinting routine when client comes up
water-sucks Jan 13, 2026
753c240
fix(fingerprint): use sudo user when possible on linux
water-sucks Jan 14, 2026
394dc3e
feat(fingerprint): add linux platform fingerprint hash
water-sucks Jan 14, 2026
a03e901
feat(fingerprint): add windows platform fingerprint hash
water-sucks Jan 15, 2026
44eb270
feat(fingerprint): add macos platform fingerprint hash
water-sucks Jan 15, 2026
53014ef
chore(deps): pin olm version to remote
water-sucks Jan 15, 2026
8ea474d
feat: add prettier device names for linux and macos devices
water-sucks Jan 16, 2026
cddb706
revert: use previous account save/delete mechanism with login/logout
water-sucks Jan 14, 2026
e3f0d05
feat(login): recover olm device when possible
water-sucks Jan 14, 2026
5aaae68
fix(fingerprint): resolve import cycles, standardize output
water-sucks Jan 16, 2026
9c11413
fix(fingerprint): cache platform fingerprint hash when running as root
water-sucks Jan 16, 2026
0f1d4ab
Merge pull request #15 from water-sucks/add-fingerprint-and-posture-c…
water-sucks Jan 19, 2026
a4f3026
Merge branch 'add-fingerprint-and-posture-check-info' into dev
miloschwartz Jan 20, 2026
f4de2d1
add bubble up error, server health check, server info check, and bett…
miloschwartz Jan 20, 2026
f7132bb
feat(blueprint): add apply blueprint cmd
water-sucks Jan 21, 2026
ca728f9
fix darwin posture checks and add test script
miloschwartz Jan 21, 2026
6418290
Implement apply blueprint
oschwartz10612 Jan 21, 2026
c4cb7fa
Handle the fingerprint correctly
oschwartz10612 Jan 21, 2026
57a50e8
Dont print the error
oschwartz10612 Jan 21, 2026
dacdc3c
Allow running up command as sudo
oschwartz10612 Jan 21, 2026
95e4ce9
fix auto update check and biometrics check
miloschwartz Jan 21, 2026
ac247d4
Keep the olm id when logging out again
oschwartz10612 Jan 22, 2026
e914f70
move apply to apply blueprint, add file flag, and update docs
miloschwartz Jan 22, 2026
6a4911a
Handle blueprint response correctly
oschwartz10612 Jan 22, 2026
ccc7e90
bump olm and newt
miloschwartz Jan 23, 2026
8913bae
bump version
miloschwartz Jan 23, 2026
da49477
Merge branch 'main' into dev
miloschwartz Jan 23, 2026
66498c3
make docs
miloschwartz Jan 23, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions cmd/apply/apply.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package apply

import (
"github.com/fosrl/cli/cmd/apply/blueprint"
"github.com/spf13/cobra"
)

func ApplyCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "apply",
Short: "Apply commands",
Long: "Apply resources to the Pangolin server",
}

cmd.AddCommand(blueprint.BlueprintCmd())

return cmd
}
98 changes: 98 additions & 0 deletions cmd/apply/blueprint/blueprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package blueprint

import (
"errors"
"os"
"path/filepath"
"strings"

"github.com/fosrl/cli/internal/api"
"github.com/fosrl/cli/internal/config"
"github.com/fosrl/newt/logger"
"github.com/spf13/cobra"
)

type BlueprintCmdOpts struct {
Name string
Path string
}

func BlueprintCmd() *cobra.Command {
opts := BlueprintCmdOpts{}

cmd := &cobra.Command{
Use: "blueprint",
Short: "Apply a blueprint",
Long: "Apply a YAML blueprint to the Pangolin server",
PreRunE: func(cmd *cobra.Command, args []string) error {
if opts.Path == "" {
return errors.New("--file is required")
}

if _, err := os.Stat(opts.Path); err != nil {
return err
}

// Strip file extension and use file basename path as name
if opts.Name == "" {
filename := filepath.Base(opts.Path)
if before, ok := strings.CutSuffix(filename, ".yaml"); ok {
opts.Name = before
} else if before, ok := strings.CutSuffix(filename, ".yml"); ok {
opts.Name = before
} else {
opts.Name = filename
}
}

if len(opts.Name) < 1 || len(opts.Name) > 255 {
return errors.New("name must be between 1-255 characters")
}

return nil
},
Run: func(cmd *cobra.Command, args []string) {
if err := applyBlueprintMain(cmd, opts); err != nil {
os.Exit(1)
}
},
}

cmd.Flags().StringVarP(&opts.Path, "file", "f", "", "Path to blueprint file (required)")
cmd.Flags().StringVarP(&opts.Name, "name", "n", "", "Name of blueprint (default: filename, without extension)")
cmd.MarkFlagRequired("file")

return cmd
}

func applyBlueprintMain(cmd *cobra.Command, opts BlueprintCmdOpts) error {
api := api.FromContext(cmd.Context())
accountStore := config.AccountStoreFromContext(cmd.Context())

account, err := accountStore.ActiveAccount()
if err != nil {
logger.Error("Error: %v", err)
return err
}

if account.OrgID == "" {
logger.Error("Error: no organization selected. Run 'pangolin select org' first.")
return errors.New("no organization selected")
}

blueprintContents, err := os.ReadFile(opts.Path)
if err != nil {
logger.Error("Error: failed to read blueprint file: %v", err)
return err
}

_, err = api.ApplyBlueprint(account.OrgID, opts.Name, string(blueprintContents))
if err != nil {
logger.Error("Error: failed to apply blueprint: %v", err)
return err
}

logger.Info("Successfully applied blueprint!")

return nil
}
49 changes: 40 additions & 9 deletions cmd/auth/login/login.go
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,8 @@ func loginMain(cmd *cobra.Command, opts *LoginCmdOpts) error {

var newAccount config.Account

// Re-use the current account entry in case
// Re-use the current account entry in case it exists
// This preserves OLM credentials across logout/login cycles
if account, exists := accountStore.Accounts[user.UserID]; exists {
newAccount = account
}
Expand All @@ -274,6 +275,14 @@ func loginMain(cmd *cobra.Command, opts *LoginCmdOpts) error {
newAccount.Host = hostname
newAccount.SessionToken = sessionToken

// Update account with username and name from user data
if user.Username != nil {
newAccount.Username = user.Username
}
if user.Name != nil {
newAccount.Name = user.Name
}

// Ensure new user has an organization selected
if newAccount.OrgID == "" {
orgID, err := utils.SelectOrgForm(apiClient, userID)
Expand All @@ -287,7 +296,7 @@ func loginMain(cmd *cobra.Command, opts *LoginCmdOpts) error {

// Ensure OLM credentials exist
if newAccount.OlmCredentials == nil {
newOlmCreds, err := apiClient.CreateOlm(userID, utils.GetDeviceName())
newOlmCreds, err := apiClient.CreateOlm(userID, getDeviceName())
if err != nil {
logger.Error("Failed to obtain olm credentials: %v", err)
return err
Expand All @@ -298,7 +307,7 @@ func loginMain(cmd *cobra.Command, opts *LoginCmdOpts) error {
Secret: newOlmCreds.Secret,
}
} else {
// logger.Info("Olm credentials already exist for this account, skipping generation")
logger.Info("Olm credentials already exist for this account, skipping generation")
}

accountStore.Accounts[user.UserID] = newAccount
Expand All @@ -311,13 +320,35 @@ func loginMain(cmd *cobra.Command, opts *LoginCmdOpts) error {
return err
}

// Print logged in message after all setup is complete
displayName := user.Email
if displayName == "" && user.Username != nil && *user.Username != "" {
displayName = *user.Username
// Fetch server info after successful authentication
apiServerInfo, err := apiClient.GetServerInfo()
if err != nil {
// Log warning but don't fail login if server info fetch fails
logger.Debug("Failed to fetch server info: %v", err)
} else if apiServerInfo != nil {
// Convert api.ServerInfo to config.ServerInfo
serverInfo := &config.ServerInfo{
Version: apiServerInfo.Version,
SupporterStatusValid: apiServerInfo.SupporterStatusValid,
Build: apiServerInfo.Build,
EnterpriseLicenseValid: apiServerInfo.EnterpriseLicenseValid,
EnterpriseLicenseType: apiServerInfo.EnterpriseLicenseType,
}
// Update account with server info
account := accountStore.Accounts[user.UserID]
account.ServerInfo = serverInfo
accountStore.Accounts[user.UserID] = account
if err := accountStore.Save(); err != nil {
logger.Debug("Failed to save server info: %v", err)
}
}
if displayName != "" {
logger.Success("Logged in as %s", displayName)

// Print logged in message after all setup is complete
if user != nil {
displayName := utils.UserDisplayName(user)
if displayName != "" {
logger.Success("Logged in as %s", displayName)
}
}

return nil
Expand Down
14 changes: 8 additions & 6 deletions cmd/auth/logout/logout.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,18 +108,20 @@ func logoutMain(cmd *cobra.Command) error {
return err
}

// Deactivate clears session token and org ID but keeps OLM credentials
if err := accountStore.Deactivate(accountStore.ActiveUserID); err != nil {
logger.Error("Failed to save account store: %v", err)
return err
}

if err := accountStore.Save(); err != nil {
logger.Error("Failed to save account store: %v", err)
return err
}

// Print logout message with account name
logger.Success("Logged out of Pangolin account %s", account.Email)
displayName := account.Email
if account.Name != nil && *account.Name != "" {
displayName = *account.Name
} else if account.Username != nil && *account.Username != "" {
displayName = *account.Username
}
logger.Success("Logged out of Pangolin account %s", displayName)

return nil
}
110 changes: 97 additions & 13 deletions cmd/auth/status/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@ package status
import (
"fmt"
"os"
"strings"

"github.com/fosrl/cli/internal/api"
"github.com/fosrl/cli/internal/config"
"github.com/fosrl/cli/internal/logger"
"github.com/fosrl/cli/internal/utils"
"github.com/spf13/cobra"
)

Expand Down Expand Up @@ -36,14 +38,60 @@ func statusMain(cmd *cobra.Command) error {
return err
}

// User info exists in config, try to get user from API
// Check health before fetching user data
healthOk, healthErr := apiClient.CheckHealth()
isServerDown := healthErr != nil || !healthOk

if isServerDown {
logger.Warning("The server appears to be down.")
fmt.Println()
// Still show account info from stored data
logger.Info("Status: logged in (using cached account data)")
logger.Info("@ %s", account.Host)
fmt.Println()

// Display account information from stored data
displayName := utils.AccountDisplayName(account)
if displayName != "" {
logger.Info("User: %s", displayName)
}
if account.UserID != "" {
logger.Info("User ID: %s", account.UserID)
}

// Display organization information if available
if account.OrgID != "" {
logger.Info("Org ID: %s", account.OrgID)
}

return nil
}

// Health check passed, try to get user from API
user, err := apiClient.GetUser()
if err != nil {
// Unable to get user - consider logged out (previously logged in but now not)
logger.Info("Status: logged out: %v", err)
logger.Info("Your session has expired or is invalid")
logger.Info("Run 'pangolin login' to authenticate again")
return err
// Unable to get user - show error but still display account info
logger.Warning("Failed to fetch user data: %v", err)
fmt.Println()
logger.Info("Status: logged in (using cached account data)")
logger.Info("@ %s", account.Host)
fmt.Println()

// Display account information from stored data
displayName := utils.AccountDisplayName(account)
if displayName != "" {
logger.Info("User: %s", displayName)
}
if account.UserID != "" {
logger.Info("User ID: %s", account.UserID)
}

// Display organization information if available
if account.OrgID != "" {
logger.Info("Org ID: %s", account.OrgID)
}

return nil
}

// Successfully got user - logged in
Expand All @@ -53,12 +101,7 @@ func statusMain(cmd *cobra.Command) error {
fmt.Println()

// Display user information
displayName := user.Email
if user.Username != nil && *user.Username != "" {
displayName = *user.Username
} else if user.Name != nil && *user.Name != "" {
displayName = *user.Name
}
displayName := utils.UserDisplayName(user)
if displayName != "" {
logger.Info("User: %s", displayName)
}
Expand All @@ -67,7 +110,48 @@ func statusMain(cmd *cobra.Command) error {
}

// Display organization information
logger.Info("Org ID: %s", account.OrgID)
if account.OrgID != "" {
logger.Info("Org ID: %s", account.OrgID)
}

// Show watermark messages if server info is available
if account.ServerInfo != nil {
watermark := getWatermarkMessage(account.ServerInfo)
if watermark != "" {
fmt.Println()
logger.Info(watermark)
}
}

return nil
}

// getWatermarkMessage returns the appropriate watermark message based on server info
func getWatermarkMessage(serverInfo *config.ServerInfo) string {
if serverInfo == nil {
return ""
}

build := strings.ToLower(serverInfo.Build)
licenseType := ""
if serverInfo.EnterpriseLicenseType != nil {
licenseType = strings.ToLower(*serverInfo.EnterpriseLicenseType)
}

// Enterprise + Personal License
if build == "enterprise" && licenseType == "personal" {
return "Licensed for personal use only."
}

// Enterprise + Unlicensed
if build == "enterprise" && !serverInfo.EnterpriseLicenseValid {
return "This server is unlicensed."
}

// OSS + No Supporter Key
if build == "oss" && !serverInfo.SupporterStatusValid {
return "Community Edition. Consider supporting."
}

return ""
}
2 changes: 1 addition & 1 deletion cmd/down/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ func clientDownMain(cmd *cobra.Command) error {
}

// Show log preview until process stops
completed, err := tui.NewLogPreview(tui.LogPreviewConfig{
completed, _, err := tui.NewLogPreview(tui.LogPreviewConfig{
LogFile: cfg.LogFile,
Header: "Shutting down client...",
ExitCondition: func(client *olm.Client, status *olm.StatusResponse) (bool, bool) {
Expand Down
2 changes: 2 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"os"
"path/filepath"

"github.com/fosrl/cli/cmd/apply"
"github.com/fosrl/cli/cmd/auth"
"github.com/fosrl/cli/cmd/auth/login"
"github.com/fosrl/cli/cmd/auth/logout"
Expand Down Expand Up @@ -41,6 +42,7 @@ func RootCommand(initResources bool) (*cobra.Command, error) {
}

cmd.AddCommand(auth.AuthCommand())
cmd.AddCommand(apply.ApplyCommand())
cmd.AddCommand(selectcmd.SelectCmd())
cmd.AddCommand(up.UpCmd())
cmd.AddCommand(down.DownCmd())
Expand Down
Loading