Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 91 additions & 0 deletions docs/adr/0004-go-project-structure.md
Original file line number Diff line number Diff line change
@@ -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)
125 changes: 125 additions & 0 deletions docs/dev-guide/design/go-project-structure.md
Original file line number Diff line number Diff line change
@@ -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 <module> -a <target>
update.go # lola update
search.go # lola search <query> [--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()
```
Loading