From 5521ef42f327f7000d64c2c2a5cd5e8952123d12 Mon Sep 17 00:00:00 2001 From: Igor Brandao Date: Wed, 29 Apr 2026 00:13:22 -0300 Subject: [PATCH] docs: add ADR-0004 go project structure Add Architecture Decision Record for the Go project directory layout: cmd/ + internal/ + pkg/ with compiler-enforced public/private boundaries. Design doc includes complete file tree, package dependency flow diagram, and cobra registration. Co-Authored-By: Claude Sonnet --- docs/adr/0004-go-project-structure.md | 91 +++++++++++++ docs/dev-guide/design/go-project-structure.md | 125 ++++++++++++++++++ 2 files changed, 216 insertions(+) create mode 100644 docs/adr/0004-go-project-structure.md create mode 100644 docs/dev-guide/design/go-project-structure.md diff --git a/docs/adr/0004-go-project-structure.md b/docs/adr/0004-go-project-structure.md new file mode 100644 index 0000000..e19fdcb --- /dev/null +++ b/docs/adr/0004-go-project-structure.md @@ -0,0 +1,91 @@ +# ADR-0004: Go Project Structure + +**Status**: Proposed +**Date**: 2026-04-29 +**Authors**: Igor Brandao +**Reviewers**: + +## Context + +The Go migration (ADR-0002) and extension architecture (ADR-0003) require a well-defined project layout. The directory structure must clearly separate public packages (importable by extension developers) from private implementation (CLI commands, internal logic), following established Go community conventions. + +Lola has two distinct audiences for its Go packages: +1. **Extension developers** who need to import the extension SDK interfaces and model types +2. **Core contributors** who work on the CLI commands, extension lifecycle, and internal logic + +These audiences require different access levels, which Go's `internal/` package convention enforces at the compiler level. + +## Decision + +Adopt a three-directory layout: `cmd/`, `internal/`, `pkg/`. + +``` +cmd/ # Binary entry points + lola/ + main.go + +internal/ # PRIVATE — compiler-enforced, not importable + cli/ # Cobra commands (one file per subcommand) + extensions/ # Extension discovery, registration, lifecycle + config/ # Viper configuration and LOLA_HOME paths + sync/ # Install/uninstall/update orchestration + frontmatter/ # YAML frontmatter parser + repo/ # Repository management + serve/ # API server (future) + +pkg/ # PUBLIC — importable by extension developers + sdk/ # Extension SDK: interfaces and manifest types + builtin/ # Built-in extension implementations + models/ # Shared model types (Module, Skill, etc.) +``` + +**`cmd/lola/main.go`**: Thin entry point that calls `internal/cli`. + +**`internal/`**: All private implementation. Go compiler prevents any code outside this module from importing these packages. + +**`pkg/`**: All public packages. Extension developers import `pkg/sdk/` for interfaces and `pkg/models/` for shared types. `pkg/builtin/` contains built-in implementations — public so they serve as reference for extension authors. + +## Rationale + +- Go's `internal/` package convention is the idiomatic way to enforce public/private boundaries in Go projects +- Three top-level directories is the minimum needed to separate concerns (entry point, private, public) +- This layout matches the pattern used by other Go CLI tools with extension systems + +## Consequences + +### Positive Consequences + +- Extension developers have a clear, stable import path (`pkg/sdk/` and `pkg/models/`) +- Core contributors can freely refactor everything in `internal/` without breaking extension code +- One file per command in `internal/cli/` makes the command tree easy to navigate for new contributors +- Built-in implementations in `pkg/builtin/` serve as development reference for extension authors +- Three-directory root keeps project navigation simple + +### Negative Consequences + +- Every new public type or interface must be consciously placed in `pkg/` — adding friction to API decisions +- Moving a package between `internal/` and `pkg/` is a breaking change requiring a semver bump +- Developers unfamiliar with Go conventions may not immediately understand the `internal/` restriction + +## Alternatives Considered + +### Alternative 1: Everything under pkg/ +- Description: No `internal/` directory — all packages under `pkg/` +- Pros: Minimal root, everything importable +- Cons: Exposes private CLI internals to importers; no compiler-enforced boundary +- Reason for rejection: Extension developers should not depend on CLI handler internals + +### Alternative 2: Flat root with many top-level directories +- Description: Each domain at root level (`sdk/`, `builtin/`, `cli/`, `config/`, etc.) +- Pros: Maximum visibility for each domain +- Cons: Too many top-level directories; loses the public/private distinction +- Reason for rejection: Cluttered root, no clear import guidance for extension developers + +## Implementation Notes + +See paired design document: `docs/dev-guide/design/go-project-structure.md` + +## References + +- [ADR-0002: Go Migration](0002-go-migration.md) +- [ADR-0003: Extension Architecture](0003-extension-architecture.md) diff --git a/docs/dev-guide/design/go-project-structure.md b/docs/dev-guide/design/go-project-structure.md new file mode 100644 index 0000000..c1f08a5 --- /dev/null +++ b/docs/dev-guide/design/go-project-structure.md @@ -0,0 +1,125 @@ +# Go Project Structure — Implementation Design + +Paired with [ADR-0004: Go Project Structure](../../adr/0004-go-project-structure.md). + +## Complete File Tree + +``` +cmd/ + lola/ + main.go # Thin entry, calls internal/cli + +internal/ + cli/ # Cobra commands — one file per subcommand + root.go # Root command, version, shell completion + mod.go # lola mod add|rm|ls|update|info|search + skill.go # lola skill add|rm|ls|info|search + plugin.go # lola plugin add|rm|ls|info|search + group.go # lola group add|rm|ls|info|install + repo.go # lola repo add|rm|ls|update|set + ext.go # lola ext add|rm|ls|info|search + install.go # lola install -a + update.go # lola update + search.go # lola search [--type mod|skill|plugin|ext] + serve.go # lola serve (future) + + extensions/ # Extension discovery and lifecycle + registry.go # Factory maps for built-in extensions + discovery.go # Scan extension dir + PATH for externals + runner.go # Execute external extensions via stdin/stdout + + config/ # Viper configuration + config.go # LOLA_HOME, MODULES_DIR, INSTALLED_FILE, etc. + + sync/ # Install/uninstall/update orchestration + install.go # install_to_target(), copy_module_to_local() + update.go # update_module(), compute orphans + uninstall.go # remove from target + registry + + frontmatter/ # Hand-rolled YAML frontmatter parser + parse.go # ParseFrontmatter(content, v) (body, err) + + repo/ # Repository/marketplace management + manager.go # RepoRegistry: add, update, search, resolve + search.go # Cross-repo module search + + serve/ # API server (future) + server.go + +pkg/ + sdk/ # PUBLIC extension SDK + extension.go # Base Extension interface, Kind type + manifest.go # ExtensionManifest struct (YAML schema) + target.go # TargetExtension interface + source.go # SourceExtension interface + repo.go # RepoExtension interface + runtime.go # RuntimeExtension interface + scan.go # ScanExtension interface + + builtin/ # PUBLIC built-in extension implementations + targets/ + claude_code.go # Separate files, .claude/ paths + cursor.go # Separate files, .cursor/ paths + gemini.go # Managed section in GEMINI.md + openclaw.go # Workspace-based + opencode.go # Managed section in AGENTS.md + sources/ + git.go # go-git/v5 shallow clone + zip.go # stdlib archive/zip + tar.go # stdlib archive/tar + folder.go # os.CopyFS + oci.go # imports skillimage pkg/oci + repos/ + yaml.go # Standard YAML catalog handler + oci.go # OCI registry catalog + + models/ # PUBLIC shared model types + module.go # Module, Skill, Command, Agent + installation.go # Installation, InstallationRegistry + repo.go # Repo (was Marketplace) + group.go # Group definition +``` + +## Package Dependency Flow + +```mermaid +graph TD + CMD["cmd/lola"] --> CLI["internal/cli"] + CLI --> EXT["internal/extensions"] + CLI --> SYNC["internal/sync"] + CLI --> REPO["internal/repo"] + CLI --> CFG["internal/config"] + + EXT --> SDK["pkg/sdk"] + EXT --> BUILTIN["pkg/builtin"] + SYNC --> SDK + SYNC --> MODELS["pkg/models"] + REPO --> MODELS + + BUILTIN --> SDK + BUILTIN --> MODELS + + style SDK fill:#90ee90,stroke:#333 + style BUILTIN fill:#90ee90,stroke:#333 + style MODELS fill:#90ee90,stroke:#333 +``` + +Green = public (`pkg/`), white = private (`internal/`). + +## Cobra Command Registration + +Each command file in `internal/cli/` exports a `NewXxxCmd()` function. The root command registers all subcommands explicitly — no magic discovery: + +``` +root.go: NewRootCmd() + ├── mod.go: NewModCmd() + ├── skill.go: NewSkillCmd() + ├── plugin.go: NewPluginCmd() + ├── group.go: NewGroupCmd() + ├── repo.go: NewRepoCmd() + ├── ext.go: NewExtCmd() + ├── install.go: NewInstallCmd() + ├── update.go: NewUpdateCmd() + ├── search.go: NewSearchCmd() + └── serve.go: NewServeCmd() +```