feat(platform): add cmsctl and document newsletter and shell updates

Generated with Codebuff 🤖
Co-Authored-By: Codebuff <noreply@codebuff.com>
This commit is contained in:
mats committed 2026-08-20 18:13:49 +02:00
1 parent 0f8ed97878
commit fea6eb51b1
7 files changed
+2408

No files matched your search

+140
View File
@@ -0,0 +1,140 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
"time"
)
const (
defaultBaseURL = "http://localhost:8080"
configEnv = "CMSCTL_CONFIG"
)
// Config is deliberately small and JSON-compatible so operators can inspect
// or move it between machines. The file is written with mode 0600.
type Config struct {
BaseURL string `json:"base_url,omitempty"`
FinderURL string `json:"finder_url,omitempty"`
AccessToken string `json:"access_token,omitempty"`
TokenType string `json:"token_type,omitempty"`
ExpiresAt time.Time `json:"expires_at,omitempty"`
}
func defaultConfig() Config {
return Config{BaseURL: defaultBaseURL, TokenType: "Bearer"}
}
func configPath(override string) (string, error) {
if override != "" {
return override, nil
}
if p := os.Getenv(configEnv); p != "" {
return p, nil
}
// UserConfigDir follows the platform convention: $XDG_CONFIG_HOME or
// ~/.config on Unix, %AppData% on Windows, and ~/Library/Application
// Support on macOS. Keep the explicit XDG check for callers that set it
// in environments where UserConfigDir cannot resolve it yet.
if dir := os.Getenv("XDG_CONFIG_HOME"); dir != "" {
return filepath.Join(dir, "cmsctl", "config.json"), nil
}
if dir, err := os.UserConfigDir(); err == nil && dir != "" {
return filepath.Join(dir, "cmsctl", "config.json"), nil
}
// Last-resort Unix-compatible fallback for minimal environments without a
// discoverable platform config directory.
home, err := os.UserHomeDir()
if err != nil {
return "", fmt.Errorf("find user config directory: %w", err)
}
return filepath.Join(home, ".config", "cmsctl", "config.json"), nil
}
func loadConfig(path string) (Config, error) {
cfg := defaultConfig()
body, err := os.ReadFile(path)
if errors.Is(err, os.ErrNotExist) {
return cfg, nil
}
if err != nil {
return Config{}, fmt.Errorf("read config %s: %w", path, err)
}
if err := json.Unmarshal(body, &cfg); err != nil {
return Config{}, fmt.Errorf("parse config %s: %w", path, err)
}
if cfg.BaseURL == "" {
cfg.BaseURL = defaultBaseURL
}
if cfg.TokenType == "" {
cfg.TokenType = "Bearer"
}
return cfg, nil
}
func saveConfig(path string, cfg Config) error {
if cfg.BaseURL == "" {
cfg.BaseURL = defaultBaseURL
}
if cfg.TokenType == "" {
cfg.TokenType = "Bearer"
}
body, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
return fmt.Errorf("encode config: %w", err)
}
body = append(body, '\n')
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
return fmt.Errorf("create config directory: %w", err)
}
tmp, err := os.CreateTemp(filepath.Dir(path), ".config-*")
if err != nil {
return fmt.Errorf("create config temp file: %w", err)
}
tmpName := tmp.Name()
defer os.Remove(tmpName)
if err := tmp.Chmod(0o600); err != nil {
tmp.Close()
return fmt.Errorf("protect config temp file: %w", err)
}
if _, err := tmp.Write(body); err != nil {
tmp.Close()
return fmt.Errorf("write config: %w", err)
}
if err := tmp.Sync(); err != nil {
tmp.Close()
return fmt.Errorf("sync config: %w", err)
}
if err := tmp.Close(); err != nil {
return fmt.Errorf("close config: %w", err)
}
if err := os.Rename(tmpName, path); err != nil {
return fmt.Errorf("replace config: %w", err)
}
return nil
}
func clearToken(cfg *Config) {
cfg.AccessToken = ""
cfg.TokenType = "Bearer"
cfg.ExpiresAt = time.Time{}
}
func effectiveBaseURL(cfg Config) string {
base := strings.TrimRight(cfg.BaseURL, "/")
if base == "" {
return defaultBaseURL
}
return base
}
func effectiveFinderURL(cfg Config) string {
if cfg.FinderURL != "" {
return strings.TrimRight(cfg.FinderURL, "/")
}
return effectiveBaseURL(cfg) + "/find"
}