cmsctl: persist a scoped API key for script auth

Add `auth api-key {set|clear}` and prefer a stored key over the device
token, so scripts authenticate with a permanent minted API key
(X-API-Key) instead of re-running the 12h device flow. Explicit flags
and env still win over persisted credentials. Also treat a zero-byte
config file as defaults (previously every command failed to parse), and
correct AGENTS.md, which claimed device tokens auto-refresh.

🤖 Generated with Codebuff
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
mats committed 2026-08-29 14:52:50 +02:00
1 parent cb7967e419
commit a5d00c9d7e
3 files changed
+111 -6

No files matched your search

+49 -5
View File
@@ -21,6 +21,7 @@ const usageText = `cmsctl — CMS and finder API CLI
Usage:
cmsctl auth login [--scope read,write] Device-login and persist token
cmsctl auth api-key {set|clear} [<key>] Store/remove a minted API key (no expiry)
cmsctl auth status Show current credential status
cmsctl auth token [--raw] Print the persisted token
cmsctl auth logout Remove the persisted token
@@ -140,8 +141,14 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) erro
if global.apiKey == "" {
global.apiKey = os.Getenv("CMSCTL_API_KEY")
}
// Fall back to persisted credentials only when nothing was supplied on
// the command line or via env for either mechanism. The persisted API
// key (no expiry) is preferred over the persisted device token.
if global.token == "" && global.apiKey == "" {
global.token = cfg.AccessToken
global.apiKey = cfg.APIKey
if global.apiKey == "" {
global.token = cfg.AccessToken
}
}
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
@@ -211,10 +218,46 @@ func parseGlobal(args []string) (globalOptions, []string, error) {
func runAuth(ctx context.Context, cfgPath string, cfg *Config, args []string, stdout, stderr io.Writer, timeout time.Duration) error {
if len(args) == 0 || args[0] == "help" || args[0] == "--help" {
_, _ = io.WriteString(stdout, "Usage: cmsctl auth login [--scope read,write] [--no-poll]\n cmsctl auth status\n cmsctl auth token [--raw]\n cmsctl auth logout\n")
_, _ = io.WriteString(stdout, "Usage: cmsctl auth login [--scope read,write] [--no-poll]\n cmsctl auth api-key [set <key> | clear]\n cmsctl auth status\n cmsctl auth token [--raw]\n cmsctl auth logout\n")
return nil
}
switch args[0] {
case "api-key":
fs := flag.NewFlagSet("auth api-key", flag.ContinueOnError)
fs.SetOutput(stderr)
if err := fs.Parse(args[1:]); err != nil {
return err
}
rest := fs.Args()
if len(rest) == 0 {
if cfg.APIKey == "" {
fmt.Fprintln(stdout, "no API key stored")
} else {
fmt.Fprintf(stdout, "API key stored in %s\n", cfgPath)
}
return nil
}
switch rest[0] {
case "set":
if len(rest) < 2 || rest[1] == "" {
return errors.New("usage: cmsctl auth api-key set <key>")
}
cfg.APIKey = rest[1]
if err := saveConfig(cfgPath, *cfg); err != nil {
return err
}
fmt.Fprintf(stdout, "API key stored in %s\n", cfgPath)
return nil
case "clear":
cfg.APIKey = ""
if err := saveConfig(cfgPath, *cfg); err != nil {
return err
}
fmt.Fprintf(stdout, "API key removed from %s\n", cfgPath)
return nil
default:
return fmt.Errorf("unknown api-key subcommand %q (use set|clear)", rest[0])
}
case "login":
fs := flag.NewFlagSet("auth login", flag.ContinueOnError)
fs.SetOutput(stderr)
@@ -308,7 +351,8 @@ func runAuth(ctx context.Context, cfgPath string, cfg *Config, args []string, st
"config": cfgPath,
"base_url": effectiveBaseURL(*cfg),
"finder_url": effectiveFinderURL(*cfg),
"authenticated": cfg.AccessToken != "" && (cfg.ExpiresAt.IsZero() || time.Now().Before(cfg.ExpiresAt)),
"api_key": cfg.APIKey != "",
"authenticated": cfg.APIKey != "" || (cfg.AccessToken != "" && (cfg.ExpiresAt.IsZero() || time.Now().Before(cfg.ExpiresAt))),
}
if !cfg.ExpiresAt.IsZero() {
status["expires_at"] = cfg.ExpiresAt
@@ -333,11 +377,11 @@ func runAuth(ctx context.Context, cfgPath string, cfg *Config, args []string, st
}
return writeJSON(stdout, map[string]any{"access_token": cfg.AccessToken, "token_type": cfg.TokenType, "expires_at": cfg.ExpiresAt})
case "logout":
clearToken(cfg)
clearCredentials(cfg)
if err := saveConfig(cfgPath, *cfg); err != nil {
return err
}
fmt.Fprintf(stdout, "Logged out; token removed from %s\n", cfgPath)
fmt.Fprintf(stdout, "Logged out; credentials removed from %s\n", cfgPath)
return nil
default:
return fmt.Errorf("unknown auth command %q", args[0])