Skip to content
Open
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
42 changes: 42 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,3 +46,45 @@ Behavior:
- argument auto-resolves against site ID/domain/server/user when possible
- stores only file path mappings in config (`sync.files.items`)
- database details are requested interactively for each run (not persisted)

### Context-aware defaults

The CLI resolves defaults using context detection and a fixed precedence.

Context detection order:
1. `--context` / `INTERCUBE_CONTEXT` (`pipeline`, `server`, `repository`, `global`)
2. `CI=true` -> pipeline mode
3. server config at `/etc/intercube.yaml` -> server mode
4. nearest repo containing `.intercube.yaml` -> repository mode
5. fallback -> global mode

Resolution precedence:
1. command flags
2. environment variables
3. active context config
4. user defaults

Supported default keys:

```yaml
context:
org_id: org_xxx
site_id: "58"
server_id: "42"

behavior:
non_interactive: false
```

Config scopes:
- user: `~/.intercube.yaml`
- repository: `<repo>/.intercube.yaml`
- server: `~/.intercube.yaml` (same user-level config, used when `--context server` is selected)

Environment overrides:
- `INTERCUBE_ORG_ID` (preferred) and `INTERCUBE_ORGANIZATION_ID` (legacy)
- `INTERCUBE_SITE_ID`
- `INTERCUBE_SERVER_ID`
- `INTERCUBE_NON_INTERACTIVE`

In non-interactive mode, commands fail instead of prompting when required values are missing.
2 changes: 1 addition & 1 deletion cmd/auth-org-select.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func runAuthOrgSelect(cmd *cobra.Command, args []string, forcePrompt bool) error
}

if forcePrompt || orgID == "" {
orgID, err = selectOrPromptOrgID(session.OrganizationID, appconfig.OrganizationID, session.KnownOrgIDs, organizations, forcePrompt)
orgID, err = selectOrPromptOrgID(session.OrganizationID, contextOrgDefault(), session.KnownOrgIDs, organizations, forcePrompt)
if err != nil {
return err
}
Expand Down
79 changes: 79 additions & 0 deletions cmd/context_runtime.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package cmd

import (
"fmt"
"os"
"strings"

"github.com/intercube/cli/util/appconfig"
authutil "github.com/intercube/cli/util/auth"
"github.com/intercube/cli/util/contextconfig"
"github.com/spf13/cobra"
)

var runtimeContext contextconfig.Runtime
var contextOverride string

func isNonInteractiveMode() bool {
if config.Behavior.NonInteractive {
return true
}

return runtimeContext.NonInteractive
}

func ensureInteractiveMode(action string) error {
if isNonInteractiveMode() {
return fmt.Errorf("%s requires interactive input; pass explicit flags or set defaults in context config", action)
}

if !stdinIsTerminal() {
return fmt.Errorf("%s requires a terminal", action)
}

return nil
}

func resolveOrganizationID(cmd *cobra.Command, organizationOverride string) (string, string, error) {
store, err := authutil.NewSessionStore("intercube-cli")
if err != nil {
return "", "", err
}

sessionOrg := ""
if session, sessionErr := store.Load(cmd.Context()); sessionErr == nil {
sessionOrg = strings.TrimSpace(session.OrganizationID)
}

resolved := contextconfig.ResolveValue(contextconfig.Inputs{
FlagValue: organizationOverride,
PreferredEnvValue: strings.TrimSpace(os.Getenv(appconfig.EnvOrganizationIDAlt)),
LegacyEnvValue: strings.TrimSpace(os.Getenv(appconfig.EnvOrganizationID)),
ContextConfigValue: strings.TrimSpace(config.Context.OrgID),
SessionDefaultValue: sessionOrg,
UserDefaultValue: strings.TrimSpace(appconfig.OrganizationID),
})

return resolved.Value, resolved.Source, nil
}

func resolveSiteID(siteOverride string) string {
resolved := contextconfig.ResolveValue(contextconfig.Inputs{
FlagValue: siteOverride,
PreferredEnvValue: strings.TrimSpace(os.Getenv(appconfig.EnvSiteID)),
ContextConfigValue: strings.TrimSpace(config.Context.SiteID),
})

return resolved.Value
}

func contextOrgDefault() string {
resolved := contextconfig.ResolveValue(contextconfig.Inputs{
PreferredEnvValue: strings.TrimSpace(os.Getenv(appconfig.EnvOrganizationIDAlt)),
LegacyEnvValue: strings.TrimSpace(os.Getenv(appconfig.EnvOrganizationID)),
ContextConfigValue: strings.TrimSpace(config.Context.OrgID),
UserDefaultValue: strings.TrimSpace(appconfig.OrganizationID),
})

return resolved.Value
}
13 changes: 3 additions & 10 deletions cmd/inventory-helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,16 +26,9 @@ func newInventoryClient(cmd *cobra.Command, organizationOverride string) (*inven
return nil, "", err
}

organizationID := strings.TrimSpace(organizationOverride)
if organizationID == "" {
session, sessionErr := store.Load(cmd.Context())
if sessionErr == nil {
organizationID = strings.TrimSpace(session.OrganizationID)
}
}

if organizationID == "" {
organizationID = strings.TrimSpace(appconfig.OrganizationID)
organizationID, _, err := resolveOrganizationID(cmd, organizationOverride)
if err != nil {
return nil, "", err
}

clerkClient := &authutil.ClerkClient{
Expand Down
4 changes: 4 additions & 0 deletions cmd/map.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ var mapCmd = &cobra.Command{
Use: "map",
Short: "Maps files based on config yaml file",
RunE: func(cmd *cobra.Command, args []string) error {
if interactiveMapSetup && isNonInteractiveMode() {
return fmt.Errorf("--interactive is not allowed in non-interactive context")
}

if len(config.Mappings) == 0 {
if !interactiveMapSetup {
return fmt.Errorf("no mappings configured. set `mappings` in config or run `intercube onboarding` interactively")
Expand Down
45 changes: 41 additions & 4 deletions cmd/onboarding.go
Original file line number Diff line number Diff line change
Expand Up @@ -152,12 +152,53 @@ func runOnboarding() {
}
}

configureContextDefaults, err := chooseYesNo("Configure context defaults too?")
if err != nil {
fmt.Printf("Onboarding cancelled: %v\n", err)
return
}

contextOrgID := strings.TrimSpace(config.Context.OrgID)
contextSiteID := strings.TrimSpace(config.Context.SiteID)
contextServerID := strings.TrimSpace(config.Context.ServerID)
contextNonInteractive := config.Behavior.NonInteractive

if configureContextDefaults {
contextOrgID, err = promptText("Default organization ID (optional)", contextOrgID, optionalValue, 0)
if err != nil {
fmt.Printf("Onboarding cancelled: %v\n", err)
return
}

contextSiteID, err = promptText("Default site ID (optional)", contextSiteID, optionalValue, 0)
if err != nil {
fmt.Printf("Onboarding cancelled: %v\n", err)
return
}

contextServerID, err = promptText("Default server ID (optional)", contextServerID, optionalValue, 0)
if err != nil {
fmt.Printf("Onboarding cancelled: %v\n", err)
return
}

contextNonInteractive, err = chooseYesNo("Force non-interactive mode by default?")
if err != nil {
fmt.Printf("Onboarding cancelled: %v\n", err)
return
}
}

viper.Set("login.username", username)
viper.Set("login.password", password)
viper.Set("login.scope", scope)
viper.Set("login.auth_method", authMethod)
viper.Set("login.instance_url", instanceURL)
viper.Set("sync.files.items", toMapSlice(syncItems))
viper.Set("context.org_id", contextOrgID)
viper.Set("context.site_id", contextSiteID)
viper.Set("context.server_id", contextServerID)
viper.Set("behavior.non_interactive", contextNonInteractive)

if err := writeOnboardingConfig(configPath); err != nil {
panic(err)
Expand Down Expand Up @@ -414,10 +455,6 @@ func resolveOnboardingConfigPath() (string, error) {
return cfgFile, nil
}

if used := viper.ConfigFileUsed(); used != "" {
return used, nil
}

home, err := homedir.Dir()
if err != nil {
return "", err
Expand Down
48 changes: 30 additions & 18 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,11 @@ package cmd
import (
"fmt"
"github.com/intercube/cli/util"
"github.com/intercube/cli/util/contextconfig"
"github.com/spf13/cobra"
"os"
"strings"

"github.com/mitchellh/go-homedir"
"github.com/spf13/viper"
)

Expand All @@ -48,41 +49,52 @@ func Execute() {
func init() {
rootCmd.PersistentFlags().BoolVarP(&Verbose, "verbose", "v", false, "verbose output")
rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.intercube.yaml)")
rootCmd.PersistentFlags().StringVar(&contextOverride, "context", "", "execution context override (pipeline,server,repository,global)")

cobra.OnInitialize(initConfig)
}

var config util.Configuration

func initConfig() {
viper.SetConfigName("config")
if cfgFile != "" {
viper.SetConfigFile(cfgFile)
} else {
home, err := homedir.Dir()
if err != nil {
fmt.Println(err)
os.Exit(1)
}
viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
viper.BindEnv("context", "INTERCUBE_CONTEXT")
viper.BindEnv("behavior.non_interactive", "INTERCUBE_NON_INTERACTIVE")
viper.BindEnv("context.org_id", "INTERCUBE_ORG_ID", "INTERCUBE_ORGANIZATION_ID")
viper.BindEnv("context.site_id", "INTERCUBE_SITE_ID")
viper.BindEnv("context.server_id", "INTERCUBE_SERVER_ID")
viper.AutomaticEnv()

viper.AddConfigPath(home)
viper.SetConfigName(".intercube")
workingDir, _ := os.Getwd()
explicitContext := strings.TrimSpace(contextOverride)
if explicitContext == "" {
explicitContext = strings.TrimSpace(viper.GetString("context"))
}
runtimeContext = contextconfig.DetectRuntime(explicitContext, workingDir)

viper.AutomaticEnv()

viper.SetDefault("behavior.non_interactive", false)
viper.SetDefault("sync", map[string]interface{}{})
loadResult, err := contextconfig.LoadLayeredConfig(viper.GetViper(), runtimeContext, cfgFile)
if err != nil {
fmt.Println(err)
os.Exit(1)
}

if err := viper.ReadInConfig(); err == nil {
if Verbose {
fmt.Println("Using config file:", viper.ConfigFileUsed())
if Verbose {
for _, path := range loadResult.LoadedPaths {
fmt.Println("Using config file:", path)
}
fmt.Printf("Using context: %s\n", runtimeContext.Kind)
}

err := viper.Unmarshal(&config)
err = viper.Unmarshal(&config)
if err != nil {
panic(fmt.Errorf("Unable to decode Config: %s \n", err))
}

if config.Behavior.NonInteractive {
runtimeContext.NonInteractive = true
}
}

func displaySubCommands(commands []*cobra.Command) {
Expand Down
Loading