Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ The CLI is built with `urfave/cli/v2` and organized hierarchically:
| Command | Description |
| --- | --- |
| `eigenx app create [name] [language]` | Create new app project from template |
| `eigenx app name <app-id\|name> [new-name]` | Set, change, or remove a friendly name for your app |
| `eigenx app profile set <app-id\|name>` | Set app profile (name, website, description, X URL, image) |
| `eigenx app deploy [image_ref]` | Build, push, deploy to TEE |
| `eigenx app upgrade <app-id\|name> <image_ref>` | Upgrade existing deployment |
| `eigenx app start [app-id\|name]` | Start stopped app (start GCP instance) |
Expand All @@ -86,7 +86,7 @@ Optional parameters are requested interactively when not provided:

Commands auto-detect project context when run in directory containing `Dockerfile`. Makes `name` parameter optional for: `deploy`.

Commands also support app name resolution - you can use either the full app ID (0x123...) or a friendly name you've set with `eigenx app name`.
Commands also support app name resolution - you can use either the full app ID (0x123...) or a friendly name you've set with `eigenx app profile set`.

### Configuration System
Global configuration with XDG Base Directory compliance:
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ ACME_FORCE_ISSUE=true # Only if staging cert exists
| --- | --- |
| `eigenx app create [name] [language]` | Create new project from template |
| `eigenx app configure tls` | Add TLS configuration to your project |
| `eigenx app name <app-id\|name> <new-name>` | Set a friendly name for your app |
| `eigenx app profile set <app-id\|name>` | Set app profile (name, website, description, social links, icon) |

### Deployment & Updates

Expand Down
4 changes: 2 additions & 2 deletions config/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,9 @@ eigenx app upgrade [app-name] [image] # Update deployment
eigenx app configure tls # Configure TLS
```

### App Naming
### App Profile
```bash
eigenx app name [app-id] [new-name] # Update friendly name
eigenx app profile set [app-id] # Set app name, website, description, social links, and icon
```

## TLS Configuration (Optional)
Expand Down
2 changes: 1 addition & 1 deletion pkg/commands/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ var AppCommand = &cli.Command{
app.ListCommand,
app.InfoCommand,
app.LogsCommand,
app.NameCommand,
app.ProfileCommand,
app.ConfigureTLSCommand,
},
}
61 changes: 41 additions & 20 deletions pkg/commands/app/deploy.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,13 @@ var DeployCommand = &cli.Command{
common.PrivateKeyFlag,
common.EnvFlag,
common.FileFlag,
common.NameFlag,
common.LogVisibilityFlag,
common.InstanceTypeFlag,
common.NameFlag,
common.WebsiteFlag,
common.DescriptionFlag,
common.XURLFlag,
common.ImageFlag,
}...),
Action: deployAction,
}
Expand Down Expand Up @@ -60,39 +64,32 @@ func deployAction(cCtx *cli.Context) error {
return fmt.Errorf("failed to get image reference: %w", err)
}

// 6. Get app name upfront (before any expensive operations)
environment := preflightCtx.EnvironmentConfig.Name
name, err := utils.GetOrPromptAppName(cCtx, environment, imageRef)
if err != nil {
return fmt.Errorf("failed to get app name: %w", err)
}

// 7. Get environment file configuration
// 6. Get environment file configuration
envFilePath, err := utils.GetEnvFileInteractive(cCtx)
if err != nil {
return fmt.Errorf("failed to get env file path: %w", err)
}

// 8. Get instance type selection (uses first from backend as default for new apps)
// 7. Get instance type selection (uses first from backend as default for new apps)
instanceType, err := utils.GetInstanceTypeInteractive(cCtx, "")
if err != nil {
return fmt.Errorf("failed to get instance: %w", err)
}

// 9. Get log settings from flags or interactive prompt
// 8. Get log settings from flags or interactive prompt
logRedirect, publicLogs, err := utils.GetLogSettingsInteractive(cCtx)
if err != nil {
return fmt.Errorf("failed to get log settings: %w", err)
}

// 10. Generate random salt
// 9. Generate random salt
salt := [32]byte{}
_, err = rand.Read(salt[:])
if err != nil {
return fmt.Errorf("failed to generate random salt: %w", err)
}

// 11. Get app ID
// 10. Get app ID
_, appController, err := utils.GetAppControllerBinding(cCtx)
if err != nil {
return fmt.Errorf("failed to get app controller binding: %w", err)
Expand All @@ -102,23 +99,47 @@ func deployAction(cCtx *cli.Context) error {
return fmt.Errorf("failed to get app id: %w", err)
}

// 12. Prepare the release (includes build/push if needed, with automatic retry on permission errors)
// 11. Prepare the release (includes build/push if needed, with automatic retry on permission errors)
release, imageRef, err := utils.PrepareReleaseFromContext(cCtx, preflightCtx.EnvironmentConfig, appIDToBeDeployed, dockerfilePath, imageRef, envFilePath, logRedirect, instanceType, 3)
if err != nil {
return err
}

// 13. Deploy the app
// 12. Deploy the app
appID, err := preflightCtx.Caller.DeployApp(cCtx.Context, salt, release, publicLogs, imageRef)
if err != nil {
return fmt.Errorf("failed to deploy app: %w", err)
}

// 14. Save the app name mapping
if err := common.SetAppName(environment, appID.Hex(), name); err != nil {
logger.Warn("Failed to save app name: %s", err.Error())
} else {
logger.Info("App saved with name: %s", name)
// 13. Collect app profile while deployment is in progress (optional)
environment := preflightCtx.EnvironmentConfig.Name
suggestedName, err := utils.ExtractAndFindAvailableName(environment, imageRef)
if err != nil {
logger.Warn("Failed to extract suggested name: %s", err.Error())
suggestedName = ""
}

logger.Info("Deployment confirmed onchain. While your instance provisions, set up a public profile")
profile, err := utils.GetAppProfileInteractive(cCtx, suggestedName, true)
if err != nil {
logger.Warn("Failed to collect profile: %s", err.Error())
profile = nil
}

// 14. Upload profile if provided (non-blocking - warn on failure but don't fail deployment)
if profile != nil {
logger.Info("Uploading app profile...")
userApiClient, err := utils.NewUserApiClient(cCtx)
if err != nil {
logger.Warn("Failed to create API client for profile upload: %s", err.Error())
} else {
_, err := userApiClient.UploadAppProfile(cCtx, appID.Hex(), profile.Name, profile.Website, profile.Description, profile.XURL, profile.ImagePath)
if err != nil {
logger.Warn("Failed to upload profile: %s", err.Error())
} else {
logger.Info("✓ Profile uploaded successfully")
}
}
}

// 15. Watch until deployment completes
Expand Down
3 changes: 2 additions & 1 deletion pkg/commands/app/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,8 @@ func logsAction(cCtx *cli.Context) error {
return fmt.Errorf("failed to create API client: %w", err)
}

formattedApp := common.FormatAppDisplay(environmentConfig.Name, appID)
profileName := utils.GetAppProfileName(cCtx, appID)
formattedApp := common.FormatAppDisplay(environmentConfig.Name, appID, profileName)

logs, err := userApiClient.GetLogs(cCtx, appID)
watchMode := cCtx.Bool(common.WatchFlag.Name)
Expand Down
9 changes: 6 additions & 3 deletions pkg/commands/app/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ func startAction(cCtx *cli.Context) error {
return fmt.Errorf("failed to get app address: %w", err)
}

formattedApp := common.FormatAppDisplay(preflightCtx.EnvironmentConfig.Name, appID)
profileName := utils.GetAppProfileName(cCtx, appID)
formattedApp := common.FormatAppDisplay(preflightCtx.EnvironmentConfig.Name, appID, profileName)

// Call AppController.StartApp
err = preflightCtx.Caller.StartApp(ctx, appID)
Expand Down Expand Up @@ -94,7 +95,8 @@ func stopAction(cCtx *cli.Context) error {
return fmt.Errorf("failed to get app address: %w", err)
}

formattedApp := common.FormatAppDisplay(preflightCtx.EnvironmentConfig.Name, appID)
profileName := utils.GetAppProfileName(cCtx, appID)
formattedApp := common.FormatAppDisplay(preflightCtx.EnvironmentConfig.Name, appID, profileName)

// Call AppController.StopApp
err = preflightCtx.Caller.StopApp(ctx, appID)
Expand Down Expand Up @@ -137,7 +139,8 @@ func terminateAction(cCtx *cli.Context) error {
return err
}

logger.Info("App %s terminated successfully", common.FormatAppDisplay(preflightCtx.EnvironmentConfig.Name, appID))
profileName := utils.GetAppProfileName(cCtx, appID)
logger.Info("App %s terminated successfully", common.FormatAppDisplay(preflightCtx.EnvironmentConfig.Name, appID, profileName))

return utils.GetAndPrintAppInfo(cCtx, appID, common.AppStatusTerminating)
}
84 changes: 84 additions & 0 deletions pkg/commands/app/profile.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package app

import (
"fmt"

"github.com/Layr-Labs/eigenx-cli/pkg/commands/utils"
"github.com/Layr-Labs/eigenx-cli/pkg/common"
"github.com/urfave/cli/v2"
)

var ProfileCommand = &cli.Command{
Name: "profile",
Usage: "Manage public app profile",
ArgsUsage: "<app-id|name>",
Subcommands: []*cli.Command{
{
Name: "set",
Usage: "Set public profile information for an app",
ArgsUsage: "<app-id|name>",
Flags: append(common.GlobalFlags, []cli.Flag{
common.EnvironmentFlag,
common.RpcUrlFlag,
common.NameFlag,
common.WebsiteFlag,
common.DescriptionFlag,
common.XURLFlag,
common.ImageFlag,
}...),
Action: profileSetAction,
},
},
}

func profileSetAction(cCtx *cli.Context) error {
logger := common.LoggerFromContext(cCtx)

// Get app ID
appID, err := utils.GetAppIDInteractive(cCtx, 0, "set profile for")
if err != nil {
return err
}

logger.Info("Setting profile for app: %s", appID.Hex())

// Collect profile fields using shared function
profile, err := utils.GetAppProfileInteractive(cCtx, "", false)
if err != nil {
return err
}

// Upload profile via API
logger.Info("Uploading app profile...")

userApiClient, err := utils.NewUserApiClient(cCtx)
if err != nil {
return fmt.Errorf("failed to create API client: %w", err)
}

response, err := userApiClient.UploadAppProfile(cCtx, appID.Hex(), profile.Name, profile.Website, profile.Description, profile.XURL, profile.ImagePath)
if err != nil {
return fmt.Errorf("failed to upload profile: %w", err)
}

// Display success message with returned data
logger.Info("✓ Profile updated successfully for app '%s'", response.Name)

// Show uploaded profile data
fmt.Println("\nUploaded Profile:")
fmt.Printf(" Name: %s\n", response.Name)
if response.Website != nil {
fmt.Printf(" Website: %s\n", *response.Website)
}
if response.Description != nil {
fmt.Printf(" Description: %s\n", *response.Description)
}
if response.XURL != nil {
fmt.Printf(" X URL: %s\n", *response.XURL)
}
if response.ImageURL != nil {
fmt.Printf(" Image URL: %s\n", *response.ImageURL)
}

return nil
}
37 changes: 35 additions & 2 deletions pkg/commands/utils/contract_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,21 @@ func CalculateAndSignApiPermissionDigest(
return signature, nil
}

// GetAppProfileName fetches the profile name for an app from the API
// Returns empty string if profile doesn't exist or API call fails
func GetAppProfileName(cCtx *cli.Context, appID ethcommon.Address) string {
userApiClient, err := NewUserApiClient(cCtx)
if err != nil {
return ""
}

info, err := userApiClient.GetInfos(cCtx, []ethcommon.Address{appID}, 1)
if err == nil && len(info.Apps) > 0 && info.Apps[0].Profile != nil {
return info.Apps[0].Profile.Name
}
return ""
}

func GetAndPrintAppInfo(cCtx *cli.Context, appID ethcommon.Address, statusOverride ...string) error {
logger := common.LoggerFromContext(cCtx)

Expand Down Expand Up @@ -254,8 +269,10 @@ func PrintAppInfoWithStatus(ctx context.Context, logger iface.Logger, client *et
}
fmt.Println()

// Show app name if available
if name := common.GetAppName(environmentName, appID.Hex()); name != "" {
// Show app name - prioritize profile name, fall back to local registry
if info.Profile != nil && info.Profile.Name != "" {
logger.Info("App Name: %s", info.Profile.Name)
} else if name := common.GetAppName(environmentName, appID.Hex()); name != "" {
logger.Info("App Name: %s", name)
}

Expand All @@ -268,6 +285,22 @@ func PrintAppInfoWithStatus(ctx context.Context, logger iface.Logger, client *et
logger.Info("Instance: %s", info.MachineType)
logger.Info("IP: %s", info.Ip)

// Display app profile if available
if info.Profile != nil {
if info.Profile.Website != nil {
logger.Info("Website: %s", *info.Profile.Website)
}
if info.Profile.Description != nil {
logger.Info("Description: %s", *info.Profile.Description)
}
if info.Profile.XURL != nil {
logger.Info("X URL: %s", *info.Profile.XURL)
}
if info.Profile.ImageURL != nil {
logger.Info("Image URL: %s", *info.Profile.ImageURL)
}
}

// Display addresses if available
if len(info.EVMAddresses) > 0 {
printEVMAddresses(logger, info.EVMAddresses)
Expand Down
Loading
Loading