molecule-cli/internal/cmd/config.go
claude-ceo-assistant 76f37d928f
All checks were successful
Release Go binaries / test (pull_request) Successful in 1m37s
Release Go binaries / release (pull_request) Has been skipped
fix(post-suspension): vanity import path go.moleculesai.app/cli (closes molecule-ai/internal#71 phase 2)
Migrates go.mod + 22 Go imports + README + comments + generated config
templates off the dead github.com/Molecule-AI/ identity onto the vanity
host go.moleculesai.app, owned by us.

Surfaces touched:
- go.mod module declaration: github.com/Molecule-AI/molecule-cli ->
  go.moleculesai.app/cli
- Every Go import statement under cmd/ + internal/
- README install section: rewritten to lead with the vanity install
  command (the previous text was migration-in-progress hedging)
- Comment URLs in internal/backends/backend.go + internal/cmd/connect.go
  (https://github.com/Molecule-AI/molecule-cli/issues/10) -> point at
  git.moleculesai.app/molecule-ai/molecule-cli
- Generated config templates in internal/cmd/init.go +
  internal/cmd/config.go: header URL updated so new users land on the
  live SCM
- Adds internal/lint/import_path_lint_test.go — structural test that
  walks every *.go / *.mod / Dockerfile / *.md / *.sh / *.yml in the
  module and rejects future references to github.com/Molecule-AI/ or
  Molecule-AI/molecule-monorepo. Mutation-tested before commit.

Test plan
- go build ./... clean
- go test ./... green (cmd/molecule + 5 internal packages + new lint
  gate, all pass)
- TestNoLegacyGitHubImportPaths fails on injected canary, passes on
  clean tree (no tautology)

Open dependency
- go.moleculesai.app responder must be deployed before
  'go install go.moleculesai.app/cli/cmd/molecule@latest' works
  externally. Internal builds + 'go build ./cmd/molecule' from a fresh
  clone work today (self-referential module path).
- Responder code prepared (worker.js, vendor-portable for CF Workers /
  Vercel Edge); deploy tracked separately under internal#71 phase 1.

Pairs with parallel migrations of plugin-gh-identity (#3) +
molecule-controlplane + molecule-core under the same internal#71 sweep.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 22:26:45 +00:00

175 lines
5.6 KiB
Go

// Package cmd implements the CLI command tree.
package cmd
import (
"fmt"
"os"
"path/filepath"
"strings"
"text/tabwriter"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
// ---------------------------------------------------------------------------
// Config command group
// ---------------------------------------------------------------------------
var configCmd = &cobra.Command{
Use: "config",
Short: "View and manage CLI and workspace configuration",
Long: `molecule config list — list all config keys (from file + env)
molecule config get <key> — print a single config value
molecule config set <key> <value> — write a key to the config file
molecule config init — scaffold a default molecule.yaml in the current directory
molecule config view — print the current config file with sources annotated`,
}
func init() {
configCmd.AddCommand(
configListCmd, configGetCmd, configSetCmd, configInitCmd, configViewCmd,
)
}
// ===========================================================================
// mol config list
// ===========================================================================
var configListCmd = &cobra.Command{
Use: "list",
Short: "List all known config keys and their effective values",
RunE: runConfigList,
}
func runConfigList(cmd *cobra.Command, _ []string) error {
settings := viper.AllSettings()
if len(settings) == 0 {
fmt.Println("No config keys set. Use `molecule config set <key> <value>` or set env vars.")
return nil
}
w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0)
fmt.Fprintln(w, "KEY\tVALUE\tSOURCE")
for k, v := range settings {
source := "default"
if viper.InConfig(k) {
source = "file"
}
if strings.HasPrefix(k, "MOLECULE_") || strings.HasPrefix(k, "MOL_") {
source = "env"
}
fmt.Fprintf(w, "%s\t%v\t%s\n", k, v, source)
}
return w.Flush()
}
// ===========================================================================
// mol config get
// ===========================================================================
var configGetCmd = &cobra.Command{
Use: "get <key>",
Short: "Print the effective value of a config key",
Args: cobra.ExactArgs(1),
RunE: runConfigGet,
}
func runConfigGet(cmd *cobra.Command, args []string) error {
if !viper.IsSet(args[0]) {
return fmt.Errorf("config get: key %q not set (check env var MOLECULE_%s)", args[0], args[0])
}
fmt.Println(viper.GetString(args[0]))
return nil
}
// ===========================================================================
// mol config set
// ===========================================================================
var configSetCmd = &cobra.Command{
Use: "set <key> <value>",
Short: "Write a config key to the config file (~/.config/molecule.yaml)",
Args: cobra.ExactArgs(2),
RunE: runConfigSet,
}
func runConfigSet(cmd *cobra.Command, args []string) error {
key, value := args[0], args[1]
configDir, err := os.UserConfigDir()
if err != nil {
configDir = "."
}
configFile := filepath.Join(configDir, "molecule.yaml")
v := viper.New()
v.SetConfigFile(configFile)
_ = v.ReadInConfig() // ignore not-found
v.Set(key, value)
if err := v.WriteConfig(); err != nil {
if err2 := v.SafeWriteConfig(); err2 != nil {
return fmt.Errorf("config set: write %s: %w (tried WriteConfig then SafeWriteConfig)", configFile, err)
}
}
fmt.Printf("Set %s=%q in %s\n", key, value, v.ConfigFileUsed())
return nil
}
// ===========================================================================
// mol config init
// ===========================================================================
var configInitCmd = &cobra.Command{
Use: "init",
Short: "Scaffold a default molecule.yaml in the current directory",
RunE: runConfigInit,
}
func runConfigInit(cmd *cobra.Command, _ []string) error {
const defaultConfig = `# molecule CLI config — https://git.moleculesai.app/molecule-ai/molecule-cli
#
# All values can be overridden by environment variables:
# MOLECULE_API_URL, MOLECULE_RUNTIME_URL, MOL_OUTPUT, MOL_VERBOSE, etc.
# Platform API base URL (env: MOLECULE_API_URL)
# api_url: http://localhost:8080
# Output format: table | json | yaml (env: MOL_OUTPUT)
# output: table
# Verbose logging: true | false (env: MOL_VERBOSE)
# verbose: false
`
if _, err := os.Stat("molecule.yaml"); err == nil {
return fmt.Errorf("config init: molecule.yaml already exists (not overwriting)")
}
if err := os.WriteFile("molecule.yaml", []byte(defaultConfig), 0o644); err != nil {
return fmt.Errorf("config init: write molecule.yaml: %w", err)
}
fmt.Println("Scaffolded molecule.yaml — edit it and run molecule --config molecule.yaml, or move it to ~/.config/molecule.yaml")
return nil
}
// ===========================================================================
// mol config view
// ===========================================================================
var configViewCmd = &cobra.Command{
Use: "view",
Short: "Print the current config file with sources annotated",
RunE: runConfigView,
}
func runConfigView(cmd *cobra.Command, _ []string) error {
if viper.ConfigFileUsed() == "" {
fmt.Println("No config file in use. Set one with --config or molecule config init.")
fmt.Println("\nActive env vars starting with MOLECULE_ or MOL_:")
for _, env := range os.Environ() {
if strings.HasPrefix(env, "MOLECULE_") || strings.HasPrefix(env, "MOL_") {
fmt.Println(" ", env)
}
}
return nil
}
data, err := os.ReadFile(viper.ConfigFileUsed())
if err != nil {
return fmt.Errorf("config view: read %s: %w", viper.ConfigFileUsed(), err)
}
fmt.Printf("# Config file: %s\n\n", viper.ConfigFileUsed())
fmt.Print(string(data))
return nil
}