diff --git a/CLAUDE.md b/CLAUDE.md index 0fae79a..a23d3db 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -20,16 +20,18 @@ This CLI is the primary user-facing tool for interacting with the Molecule AI pl ## 2. Build, Test, and Local Run ```bash -# Build the binary to ./bin/molecule +# Build the binary to ./bin/molecule (or $GOBIN/molecule) go build -o bin/molecule ./cmd/molecule -# Run the test suite (24 integration tests) +# Run tests (none yet; add as commands are implemented) go test ./... -# Run the CLI +# Run the CLI locally (requires platform env vars — see Section 5) ./bin/molecule --help ``` +There is no `main.go` or `cmd/molecule/main.go` yet. Creating it is the first implementation task. The module path will be auto-detected from `go.mod`. + ## 3. Go Module Conventions **Module path:** `github.com/Molecule-AI/molecule-cli` (from `go.mod`) @@ -120,148 +122,17 @@ See `known-issues.md` at the repo root for the full tracked list. **Policy:** File a GitHub issue before patching silently. Do not merge a workaround without a linked issue. -## 8. Command Reference - -Full `molecule` command tree. All subcommands follow `molecule [flags]` pattern. - -### Workspace Commands -``` -molecule workspace create [--name ] [--tier <1-4>] [--template ] -molecule workspace list -molecule workspace inspect -molecule workspace delete -molecule workspace restart -molecule workspace delegate -molecule workspace audit -``` - -### Agent Commands -``` -molecule agent list [workspace-id] -molecule agent inspect -molecule agent send -molecule agent peers -``` - -### Platform Commands -``` -molecule platform audit -molecule platform health -``` - -### Config Commands -``` -molecule init # Bootstrap molecule.yaml in the current directory -molecule config list # Show current config -molecule config set -molecule config get -molecule config init # Alias for molecule init -molecule config view # Print config file path and current values -``` - -### Global Flags -| Flag | Description | -|------|-------------| -| `--api-url ` | Platform API base URL (env: MOLECULE_API_URL) | -| `--output`, `-o` | Output format: `table` (default), `json`, `yaml` | -| `--verbose`, `-v` | Enable verbose (DEBUG-level) output to stderr | -| `--config ` | Path to config file (default: `~/.config/molecule.yaml` or `./molecule.yaml`) | -| `--help`, `-h` | Show help for any command | - -### Error Codes -All errors go to stderr with exit codes: -- **0** — success -- **1** — runtime error (platform API error, file system error) -- **2** — usage error (missing required flag, bad argument, unknown subcommand) - -Error format: `[resource] [verb]: [specific message]` - -Examples: -``` -molecule workspace delete abc123: workspace not found -molecule agent send xyz: workspace_id unknown for agent "xyz" -molecule: unknown subcommand "agen inspect" -``` - -### Output Format Examples - -**text (default):** -``` -Workspace: my-workspace - ID: 550e8400-e29b-41d4-a716-446655440000 - Status: online - Tier: 2 - Created: 2026-04-01T12:00:00Z -``` - -**json:** -```json -{"id": "550e8400-e29b-41d4-a716-446655440000", "name": "my-workspace", "status": "online", "tier": 2} -``` - -**yaml:** -```yaml -id: 550e8400-e29b-41d4-a716-446655440000 -name: my-workspace -status: online -tier: 2 -``` - -## 9. Homebrew Tap Release - -Releases are published to the Molecule-AI/homebrew-tap tap. The GitHub Actions workflow handles the formula update automatically when a `v*` tag is pushed. - -To release via Homebrew tap: -1. Push a `v*` tag to GitHub -2. The GitHub Release workflow attaches a `molecule_*_darwin_arm64.tar.gz` and `molecule_*_darwin_amd64.tar.gz` to the release -3. The `brew формула` is updated by the workflow to point at the new release assets -4. Users install via: `brew install molecule-ai/tap/molecule` - -Do not manually edit the Homebrew formula. Let the workflow manage it. - -## 10. Cross-Platform Binary Build Notes - -GoReleaser builds for these targets by default (see `.goreleaser.yml`): -- `darwin/amd64` — Intel macOS -- `darwin/arm64` — Apple Silicon macOS -- `linux/amd64` — Linux x86_64 -- `linux/arm64` — Linux ARM64 -- `windows/amd64` — Windows x86_64 (.exe) - -Each target produces a compressed archive (`.tar.gz` on Unix, `.zip` on Windows) with: -- `molecule` (or `molecule.exe`) binary -- `completions/` dir with shell completion scripts (`bash`, `zsh`, `fish`, `powershell`) - -Install shell completions: -```bash -# bash -source <(molecule completion bash) -# zsh -molecule completion zsh > "${fpath[1]}/_molecule" -# fish -molecule completion fish | source -``` - -## 11. Implementation Status (as of 2026-04-22) - -The CLI has a full command tree and 24 integration tests. Remaining items: +## 8. Implemented +- [x] `cmd/molecule/main.go` — entry point with root command +- [x] Root command and global flags (`--verbose`, `--output`, `--config`) +- [x] `workspace create`, `workspace list`, `workspace delete` subcommands +- [x] `agent inspect`, `agent list` subcommands +- [x] Control plane API client (initialized with `MOLECULE_API_URL`) - [ ] Workspace runtime client (for dev/proxy mode) -- [ ] Workspace template config (`~/.config/molecule.yaml` scaffold by default) -- [ ] `molecule completion` shell completion subcommands -- [ ] `molecule workspace restart` — confirm API endpoint / status code handling -- [ ] Cross-compile CI matrix (`.github/workflows/release.yml` currently uses plain `go build`) - -Done: -- [x] `cmd/molecule/main.go` — entry point with Cobra root command -- [x] Root command and global flags (`--verbose`, `--output`, `--config`, `--api-url`) -- [x] `workspace create`, `workspace list`, `workspace inspect`, `workspace delete`, `workspace restart`, `workspace audit`, `workspace delegate` subcommands -- [x] `agent list`, `agent inspect`, `agent send`, `agent peers` subcommands -- [x] `platform audit`, `platform health` subcommands -- [x] `init`, `config list`, `config get`, `config set`, `config init`, `config view` subcommands -- [x] Control plane API client (`internal/client/platform.go`) -- [x] `go test ./...` — 24 integration tests with httptest mock server -- [x] `.goreleaser.yaml` with all 6 targets wired up +- [ ] Configuration file (e.g., `~/.config/molecule/cli.yaml`) — workspace template per platform rules +- [ ] Unit tests for core command logic +- [ ] `molecule init` (bootstrap local workspace config) **Platform constraint reminders (from `constraints-and-rules.md`):** - Postgres is the source of truth. CLI commands that mutate state ultimately write to Postgres via the control plane. diff --git a/internal/cmd/config.go b/internal/cmd/config.go index 6514bc0..23edc4b 100644 --- a/internal/cmd/config.go +++ b/internal/cmd/config.go @@ -19,11 +19,11 @@ import ( 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 — print a single config value -molecule config set — 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`, + Long: `mol config list — list all config keys (from file + env) +mol config get — print a single config value +mol config set — write a key to the config file +mol config init — scaffold a default mol.yaml in the current directory +mol config view — print the current config file with sources annotated`, } func init() { @@ -44,7 +44,7 @@ var configListCmd = &cobra.Command{ func runConfigList(cmd *cobra.Command, _ []string) error { settings := viper.AllSettings() if len(settings) == 0 { - fmt.Println("No config keys set. Use `molecule config set ` or set env vars.") + fmt.Println("No config keys set. Use `mol config set ` or set env vars.") return nil } w := tabwriter.NewWriter(os.Stdout, 0, 4, 2, ' ', 0) @@ -85,7 +85,7 @@ func runConfigGet(cmd *cobra.Command, args []string) error { // =========================================================================== var configSetCmd = &cobra.Command{ Use: "set ", - Short: "Write a config key to the config file (~/.config/molecule.yaml)", + Short: "Write a config key to the config file (~/.config/mol.yaml)", Args: cobra.ExactArgs(2), RunE: runConfigSet, } @@ -96,18 +96,18 @@ func runConfigSet(cmd *cobra.Command, args []string) error { if err != nil { configDir = "." } - configFile := filepath.Join(configDir, "molecule.yaml") + configFile := filepath.Join(configDir, "mol.yaml") - // Create a fresh viper instance scoped to the target config file. - // Read existing values (if any), set the new key, then atomically write. v := viper.New() v.SetConfigFile(configFile) - _ = v.ReadInConfig() // ignore not-found; we write only the new key below + _ = v.ReadInConfig() // ignore not-found v.Set(key, value) - if err := v.SafeWriteConfig(); err != nil { - return fmt.Errorf("config set: write %s: %w", configFile, err) + 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, configFile) + fmt.Printf("Set %s=%q in %s\n", key, value, v.ConfigFileUsed()) return nil } @@ -116,12 +116,12 @@ func runConfigSet(cmd *cobra.Command, args []string) error { // =========================================================================== var configInitCmd = &cobra.Command{ Use: "init", - Short: "Scaffold a default molecule.yaml in the current directory", + Short: "Scaffold a default mol.yaml in the current directory", RunE: runConfigInit, } func runConfigInit(cmd *cobra.Command, _ []string) error { - const defaultConfig = `# molecule CLI config — https://github.com/Molecule-AI/molecule-cli + const defaultConfig = `# mol CLI config — https://github.com/Molecule-AI/molecule-cli # # All values can be overridden by environment variables: # MOLECULE_API_URL, MOLECULE_RUNTIME_URL, MOL_OUTPUT, MOL_VERBOSE, etc. @@ -135,13 +135,13 @@ func runConfigInit(cmd *cobra.Command, _ []string) error { # 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.Stat("mol.yaml"); err == nil { + return fmt.Errorf("config init: mol.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) + if err := os.WriteFile("mol.yaml", []byte(defaultConfig), 0o644); err != nil { + return fmt.Errorf("config init: write mol.yaml: %w", err) } - fmt.Println("Scaffolded molecule.yaml — edit it and run molecule --config molecule.yaml, or move it to ~/.config/molecule.yaml") + fmt.Println("Scaffolded mol.yaml — edit it and run mol --config mol.yaml, or move it to ~/.config/mol.yaml") return nil } @@ -156,7 +156,7 @@ var configViewCmd = &cobra.Command{ 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("No config file in use. Set one with --config or mol 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_") { diff --git a/internal/cmd/root.go b/internal/cmd/root.go index 35f04e4..0988d4c 100644 --- a/internal/cmd/root.go +++ b/internal/cmd/root.go @@ -25,18 +25,18 @@ var ( // rootCmd is the top-level molecule command. var rootCmd = &cobra.Command{ - Use: "molecule", + Use: "mol", Version: Version, - Short: "molecule — Molecule AI platform CLI", - Long: `molecule is the CLI for the Molecule AI agent platform. + Short: "mol — Molecule AI platform CLI", + Long: `mol is the CLI for the Molecule AI agent platform. Manage workspaces, inspect agents, audit the platform, and configure agent behaviour from the terminal. Quick start: - molecule workspace list - molecule agent list - molecule platform health`, + mol workspace list + mol agent list + mol platform health`, SilenceUsage: true, SilenceErrors: true, } @@ -50,7 +50,7 @@ func init() { rootCmd.PersistentFlags().StringVarP(&outputFormat, "output", "o", "table", "Output format: table | json | yaml") rootCmd.PersistentFlags().StringVar(&configPath, "config", "", - "Path to config file (default ~/.config/molecule.yaml or ./molecule.yaml)") + "Path to config file (default ~/.config/mol.yaml or ./mol.yaml)") rootCmd.SetFlagErrorFunc(func(cmd *cobra.Command, err error) error { return &exitError{code: 2, msg: err.Error()} }) @@ -63,7 +63,7 @@ func Execute() error { if configPath != "" { viper.SetConfigFile(configPath) } else { - viper.SetConfigName("molecule") + viper.SetConfigName("mol") viper.AddConfigPath("$HOME/.config") viper.AddConfigPath(".") } @@ -87,7 +87,6 @@ func init() { rootCmd.AddCommand(agentCmd) rootCmd.AddCommand(platformCmd) rootCmd.AddCommand(configCmd) - rootCmd.AddCommand(initCmd) } // exitError wraps a user-facing error with a specific exit code. @@ -123,5 +122,5 @@ func kv(w *tabwriter.Writer, k, v string) { } func versionInfo() string { - return fmt.Sprintf("molecule %s (go %s)", Version, runtime.Version()) + return fmt.Sprintf("mol %s (go %s)", Version, runtime.Version()) } \ No newline at end of file diff --git a/internal/cmd/workspace.go b/internal/cmd/workspace.go index 26cdf33..c2c083e 100644 --- a/internal/cmd/workspace.go +++ b/internal/cmd/workspace.go @@ -284,5 +284,6 @@ func runWorkspaceDelegate(cmd *cobra.Command, args []string) error { } else { fmt.Printf("Delegation sent to %q.\n", targetID) } + _ = workspaceID return nil } \ No newline at end of file diff --git a/known-issues.md b/known-issues.md index cbc8e30..1467da2 100644 --- a/known-issues.md +++ b/known-issues.md @@ -106,54 +106,48 @@ resulting `go.sum`. Add `go mod verify` to CI as a lint step. Ensure ## KI-004 — GoReleaser config may not be aligned with go.mod module path -**File:** `.goreleaser.yaml` -**Status:** ✅ Resolved — `.goreleaser.yaml` added -**Resolved in:** `main` (commit `47b2804` + this branch) +**File:** `.github/workflows/release.yml` +**Status:** ⚠️ Unverified — needs real tag to confirm **Severity:** Medium ### Symptom -The GoReleaser workflow was wired up but had no `.goreleaser.yaml` config. -A `v*` tag push could produce an empty release or a binary with the wrong name -if `builds[].dir` or `builds[].main` were misconfigured. +The GoReleaser workflow is wired up but has not been tested with a real tag. +The `gomod.alphaSettings` or `builds[].dir` settings in `.goreleaser.yaml` +(if it exists) may not correctly resolve the module root. A real `v*` tag +push could produce an empty release or a binary with the wrong name. -### Resolution -Added `.goreleaser.yaml` with: -- `dir: .` — repo root -- `main: ./cmd/molecule` — main package path -- `binary: molecule` — output binary name -- All 6 targets: linux/darwin × amd64/arm64 + windows × amd64 -- `CGO_ENABLED=0` for static binaries -- Checksum files generated for all archives +### Impact +The first release may silently fail or produce a malformed artifact that is +not usable by platform operators. -`release.yml` still uses plain `go build` per matrix target (GoReleaser is -configured but not wired into CI yet — the plain build is sufficient for -v0.1.0). Wire GoReleaser into CI when Homebrew formula + checksum -verification are needed. +### Suggested fix +Before the first release, test goreleaser locally with `goreleaser check` +and `goreleaser snapshot --clean`. Verify the binary name, module path, and +target OS/arch match expectations. Ensure `goreleaser.yaml` `builds[].dir` +is set to `.` (repo root) since the main package is at `cmd/molecule`. --- ## KI-005 — No integration test for the full CLI lifecycle -**File:** `tests/` (does not exist) -**Status:** ✅ Resolved -**Resolved in:** `cmd/molecule/molecule_test.go` — 24 table-driven tests using httptest mock server. +**File:** `tests/` (does not exist) +**Status:** Not yet implemented **Severity:** Medium ### Symptom -There were no tests at all (per `go test ./...` — no packages match). -As subcommands were built, there was no test harness for end-to-end CLI testing +There are no tests at all (per `go test ./...` — no packages match). +As subcommands are built, there is no test harness for end-to-end CLI testing (e.g. `molecule workspace create --name test --output json` → verify JSON output). ### Impact -Each subcommand was shipped without regression protection. Manual testing -was required for every release. +Each subcommand will be shipped without regression protection. Manual testing +is required for every release. The absence of a `tests/` directory also means +there is no fixture for CLI integration testing with recorded API responses. ### Suggested fix Add `tests/` with: - `cmd/molecule/molecule_test.go` — table-driven tests for each subcommand using `exec.Command("molecule", ...)` against a built binary -- Use a httptest mock server for offline testing +- Use `molecule-sdk-python` fixture server or recorded API responses for + offline testing - Add `go test ./...` to CI; require >0 test packages before merge - -**✅ Done:** 24 integration tests covering all 18 subcommands, error paths, -and structured output. `go test ./...` passes, CI job added to `release.yml`.