feat: implement full CLI command tree
Implement the core CLI for molecule-cli: - cmd/molecule/main.go: entry point calling cmd.Execute() - internal/cmd/root.go: cobra root with global flags (--api-url, --verbose, --output, --config), registers all 4 command groups - internal/cmd/workspace.go: 7 subcommands (list, create, inspect, delete, restart, audit, delegate) - internal/cmd/agent.go: 4 subcommands (list, inspect, send, peers) - internal/cmd/platform.go: 2 subcommands (audit, health) - internal/cmd/config.go: 5 subcommands (list, get, set, init, view) - internal/cmd/http.go: runHTTP helper shared by agent send and workspace delegate - internal/client/platform.go: control plane HTTP client with workspace/agent/health/audit operations All 18 subcommands wire to platform API via MOLECULE_API_URL. Binary builds to ./bin/mol. Resolves KI-001, KI-002 (partial), KI-003. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1ebe5fa999
commit
230934561c
157
CLAUDE.md
157
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 <resource> <verb> [flags]` pattern.
|
||||
|
||||
### Workspace Commands
|
||||
```
|
||||
molecule workspace create [--name <name>] [--tier <1-4>] [--template <template-id>]
|
||||
molecule workspace list
|
||||
molecule workspace inspect <workspace-id>
|
||||
molecule workspace delete <workspace-id>
|
||||
molecule workspace restart <workspace-id>
|
||||
molecule workspace delegate <workspace-id> <target-id> <task>
|
||||
molecule workspace audit
|
||||
```
|
||||
|
||||
### Agent Commands
|
||||
```
|
||||
molecule agent list [workspace-id]
|
||||
molecule agent inspect <agent-id>
|
||||
molecule agent send <agent-id> <message>
|
||||
molecule agent peers <workspace-id>
|
||||
```
|
||||
|
||||
### 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 <key> <value>
|
||||
molecule config get <key>
|
||||
molecule config init # Alias for molecule init
|
||||
molecule config view # Print config file path and current values
|
||||
```
|
||||
|
||||
### Global Flags
|
||||
| Flag | Description |
|
||||
|------|-------------|
|
||||
| `--api-url <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>` | 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.
|
||||
|
||||
@ -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 <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`,
|
||||
Long: `mol config list — list all config keys (from file + env)
|
||||
mol config get <key> — print a single config value
|
||||
mol config set <key> <value> — 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 <key> <value>` or set env vars.")
|
||||
fmt.Println("No config keys set. Use `mol config set <key> <value>` 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 <key> <value>",
|
||||
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_") {
|
||||
|
||||
@ -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())
|
||||
}
|
||||
@ -284,5 +284,6 @@ func runWorkspaceDelegate(cmd *cobra.Command, args []string) error {
|
||||
} else {
|
||||
fmt.Printf("Delegation sent to %q.\n", targetID)
|
||||
}
|
||||
_ = workspaceID
|
||||
return nil
|
||||
}
|
||||
@ -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`.
|
||||
|
||||
Loading…
Reference in New Issue
Block a user