188 lines
5 KiB
Markdown
188 lines
5 KiB
Markdown
|
|
# Helpers CLI
|
||
|
|
|
||
|
|
`cli` fournit des helpers simples pour les assistants interactifs :
|
||
|
|
|
||
|
|
```go
|
||
|
|
profileName := cli.ResolveProfileName(flagProfile, os.Getenv("MY_MCP_PROFILE"), cfg.CurrentProfile)
|
||
|
|
|
||
|
|
baseURL, err := cli.PromptLine(reader, os.Stdout, "Base URL", profile.BaseURL)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
if err := cli.ValidateBaseURL(baseURL); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
token, err := cli.PromptSecret(os.Stdin, os.Stdout, "API token", hasStoredSecret, storedToken)
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
_ = token
|
||
|
|
```
|
||
|
|
|
||
|
|
Pour décrire un setup complet sans réécrire la boucle interactive :
|
||
|
|
|
||
|
|
```go
|
||
|
|
result, err := cli.RunSetup(cli.SetupOptions{
|
||
|
|
Stdin: os.Stdin,
|
||
|
|
Stdout: os.Stdout,
|
||
|
|
Fields: []cli.SetupField{
|
||
|
|
{
|
||
|
|
Name: "base_url",
|
||
|
|
Label: "Base URL",
|
||
|
|
Type: cli.SetupFieldURL,
|
||
|
|
Required: true,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Name: "api_token",
|
||
|
|
Label: "API token",
|
||
|
|
Type: cli.SetupFieldSecret,
|
||
|
|
Required: true,
|
||
|
|
ExistingSecret: storedToken,
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Name: "enabled",
|
||
|
|
Label: "Enable integration",
|
||
|
|
Type: cli.SetupFieldBool,
|
||
|
|
Default: "true",
|
||
|
|
},
|
||
|
|
{
|
||
|
|
Name: "scopes",
|
||
|
|
Label: "Scopes",
|
||
|
|
Type: cli.SetupFieldList,
|
||
|
|
Default: "read,write",
|
||
|
|
Normalize: func(value string) string { return strings.TrimSpace(strings.ToLower(value)) },
|
||
|
|
},
|
||
|
|
},
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
baseURL, _ := result.Get("base_url")
|
||
|
|
apiToken, _ := result.Get("api_token")
|
||
|
|
enabled, _ := result.Get("enabled")
|
||
|
|
scopes, _ := result.Get("scopes")
|
||
|
|
|
||
|
|
if apiToken.KeptStoredSecret {
|
||
|
|
fmt.Println("Stored token kept.")
|
||
|
|
}
|
||
|
|
|
||
|
|
_ = baseURL
|
||
|
|
_ = enabled
|
||
|
|
_ = scopes
|
||
|
|
```
|
||
|
|
|
||
|
|
Chaque champ peut déclarer ses propres hooks de validation (`Validate`, `ValidateBool`, `ValidateList`).
|
||
|
|
Les validations sont appliquées de manière cohérente en TTY et en stdin non interactif.
|
||
|
|
|
||
|
|
Pour standardiser la résolution `flag > env > config > secret` avec provenance :
|
||
|
|
|
||
|
|
```go
|
||
|
|
store, err := secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
|
||
|
|
ServiceName: "my-mcp",
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
lookup := cli.ResolveLookup(cli.ResolveLookupOptions{
|
||
|
|
Flag: cli.MapLookup(flagValues),
|
||
|
|
Env: cli.EnvLookup(os.LookupEnv),
|
||
|
|
Config: cli.ConfigMap(configValues),
|
||
|
|
Secret: cli.SecretStore(store),
|
||
|
|
})
|
||
|
|
|
||
|
|
resolution, err := cli.ResolveFields(cli.ResolveOptions{
|
||
|
|
Fields: []cli.FieldSpec{
|
||
|
|
{Name: "base_url", Required: true, FlagKey: "base-url", EnvKey: "MY_MCP_BASE_URL"},
|
||
|
|
{Name: "api_token", Required: true, EnvKey: "MY_MCP_API_TOKEN", SecretKey: "my-mcp-api-token"},
|
||
|
|
{Name: "timeout", DefaultValue: "30s"},
|
||
|
|
},
|
||
|
|
Lookup: lookup,
|
||
|
|
})
|
||
|
|
if err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
if err := cli.RenderResolutionProvenance(os.Stdout, resolution); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
`ResolveOptions.Order` permet de changer la priorité globale si nécessaire, et `FieldSpec.Sources` permet de définir un ordre spécifique pour un champ.
|
||
|
|
|
||
|
|
Le package fournit aussi un socle réutilisable pour une commande `doctor`.
|
||
|
|
L'application cliente choisit comment exposer la commande (`flag`, `cobra`, etc.), mais peut réutiliser les checks communs et ajouter ses propres hooks :
|
||
|
|
|
||
|
|
```go
|
||
|
|
report := cli.RunDoctor(context.Background(), cli.DoctorOptions{
|
||
|
|
ConfigCheck: cli.NewConfigCheck(store),
|
||
|
|
SecretStoreCheck: cli.SecretStoreAvailabilityCheck(func() (secretstore.Store, error) {
|
||
|
|
return secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
|
||
|
|
ServiceName: "my-mcp",
|
||
|
|
})
|
||
|
|
}),
|
||
|
|
RequiredSecrets: []cli.DoctorSecret{
|
||
|
|
{Name: "api-token", Label: "API token"},
|
||
|
|
},
|
||
|
|
SecretStoreFactory: func() (secretstore.Store, error) {
|
||
|
|
return secretstore.OpenFromManifest(secretstore.OpenFromManifestOptions{
|
||
|
|
ServiceName: "my-mcp",
|
||
|
|
})
|
||
|
|
},
|
||
|
|
ManifestDir: ".",
|
||
|
|
ConnectivityCheck: func(context.Context) cli.DoctorResult {
|
||
|
|
if err := pingBackend(); err != nil {
|
||
|
|
return cli.DoctorResult{
|
||
|
|
Name: "connectivity",
|
||
|
|
Status: cli.DoctorStatusFail,
|
||
|
|
Summary: "backend is unreachable",
|
||
|
|
Detail: err.Error(),
|
||
|
|
}
|
||
|
|
}
|
||
|
|
return cli.DoctorResult{
|
||
|
|
Name: "connectivity",
|
||
|
|
Status: cli.DoctorStatusOK,
|
||
|
|
Summary: "backend is reachable",
|
||
|
|
}
|
||
|
|
},
|
||
|
|
})
|
||
|
|
|
||
|
|
if err := cli.RenderDoctorReport(os.Stdout, report); err != nil {
|
||
|
|
return err
|
||
|
|
}
|
||
|
|
|
||
|
|
if report.HasFailures() {
|
||
|
|
os.Exit(1)
|
||
|
|
}
|
||
|
|
```
|
||
|
|
|
||
|
|
Pour éviter des checks custom répétitifs sur les profils, `doctor` expose aussi un helper basé sur `FieldSpec` :
|
||
|
|
|
||
|
|
```go
|
||
|
|
report := cli.RunDoctor(context.Background(), cli.DoctorOptions{
|
||
|
|
ExtraChecks: []cli.DoctorCheck{
|
||
|
|
cli.RequiredResolvedFieldsCheck(cli.ResolveOptions{
|
||
|
|
Fields: []cli.FieldSpec{
|
||
|
|
{Name: "base_url", Required: true, EnvKey: "MY_MCP_BASE_URL"},
|
||
|
|
{
|
||
|
|
Name: "api_token",
|
||
|
|
Required: true,
|
||
|
|
EnvKey: "MY_MCP_API_TOKEN",
|
||
|
|
SecretKey: "my-mcp-api-token",
|
||
|
|
Sources: []cli.ValueSource{cli.SourceEnv, cli.SourceSecret},
|
||
|
|
},
|
||
|
|
},
|
||
|
|
Lookup: cli.ResolveLookup(cli.ResolveLookupOptions{
|
||
|
|
Env: cli.EnvLookup(os.LookupEnv),
|
||
|
|
Config: cli.ConfigMap(configValues),
|
||
|
|
Secret: cli.SecretStore(secretStore),
|
||
|
|
}),
|
||
|
|
}),
|
||
|
|
},
|
||
|
|
})
|
||
|
|
```
|
||
|
|
|
||
|
|
En cas d'échec de résolution, tu peux aussi réutiliser le formatteur `cli.FormatResolveFieldsError(err)` dans un check custom pour garder des messages homogènes.
|