cli/tasks/plans/2026-01-29-sdk-generation-impl.md
Snider 6f84531bd3 feat/ml-integration (#2)
Co-authored-by: Charon (snider-linux) <charon@lethean.io>
Co-authored-by: Snider <snider@host.uk.com>
Co-authored-by: Virgil <virgil@lethean.io>
Co-authored-by: Claude <developers@lethean.io>
Reviewed-on: #2
Co-authored-by: Snider <snider@lethean.io>
Co-committed-by: Snider <snider@lethean.io>
2026-02-16 06:19:09 +00:00

43 KiB

SDK Generation Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.

Goal: Generate typed API clients from OpenAPI specs for TypeScript, Python, Go, and PHP with breaking change detection.

Architecture: Hybrid generator approach - native tools where available (openapi-typescript-codegen, openapi-python-client, oapi-codegen), Docker fallback for others (openapi-generator). Detection flow: config → common paths → Laravel Scramble. Breaking changes via oasdiff library.

Tech Stack: Go, oasdiff, kin-openapi, embedded templates, exec for native generators, Docker for fallback


Task 1: Create SDK Package Structure

Files:

  • Create: pkg/sdk/sdk.go
  • Create: pkg/sdk/go.mod

Step 1: Create go.mod for sdk package

module forge.lthn.ai/core/cli/pkg/sdk

go 1.25

require (
	github.com/getkin/kin-openapi v0.128.0
	github.com/tufin/oasdiff v1.10.25
	gopkg.in/yaml.v3 v3.0.1
)

Step 2: Create sdk.go with types and config

// Package sdk provides OpenAPI SDK generation and diff capabilities.
package sdk

import (
	"context"
	"fmt"
)

// Config holds SDK generation configuration from .core/release.yaml.
type Config struct {
	// Spec is the path to the OpenAPI spec file (auto-detected if empty).
	Spec string `yaml:"spec,omitempty"`
	// Languages to generate SDKs for.
	Languages []string `yaml:"languages,omitempty"`
	// Output directory (default: sdk/).
	Output string `yaml:"output,omitempty"`
	// Package naming configuration.
	Package PackageConfig `yaml:"package,omitempty"`
	// Diff configuration for breaking change detection.
	Diff DiffConfig `yaml:"diff,omitempty"`
	// Publish configuration for monorepo publishing.
	Publish PublishConfig `yaml:"publish,omitempty"`
}

// PackageConfig holds package naming configuration.
type PackageConfig struct {
	// Name is the base package name.
	Name string `yaml:"name,omitempty"`
	// Version is the SDK version (supports templates like {{.Version}}).
	Version string `yaml:"version,omitempty"`
}

// DiffConfig holds breaking change detection configuration.
type DiffConfig struct {
	// Enabled determines whether to run diff checks.
	Enabled bool `yaml:"enabled,omitempty"`
	// FailOnBreaking fails the release if breaking changes are detected.
	FailOnBreaking bool `yaml:"fail_on_breaking,omitempty"`
}

// PublishConfig holds monorepo publishing configuration.
type PublishConfig struct {
	// Repo is the SDK monorepo (e.g., "myorg/sdks").
	Repo string `yaml:"repo,omitempty"`
	// Path is the subdirectory for this SDK (e.g., "packages/myapi").
	Path string `yaml:"path,omitempty"`
}

// SDK orchestrates OpenAPI SDK generation.
type SDK struct {
	config     *Config
	projectDir string
}

// New creates a new SDK instance.
func New(projectDir string, config *Config) *SDK {
	if config == nil {
		config = DefaultConfig()
	}
	return &SDK{
		config:     config,
		projectDir: projectDir,
	}
}

// DefaultConfig returns sensible defaults for SDK configuration.
func DefaultConfig() *Config {
	return &Config{
		Languages: []string{"typescript", "python", "go", "php"},
		Output:    "sdk",
		Diff: DiffConfig{
			Enabled:        true,
			FailOnBreaking: false,
		},
	}
}

// Generate generates SDKs for all configured languages.
func (s *SDK) Generate(ctx context.Context) error {
	return fmt.Errorf("sdk.Generate: not implemented")
}

// GenerateLanguage generates SDK for a specific language.
func (s *SDK) GenerateLanguage(ctx context.Context, lang string) error {
	return fmt.Errorf("sdk.GenerateLanguage: not implemented")
}

Step 3: Add to go.work

Run: cd /Users/snider/Code/Core && echo " ./pkg/sdk" >> go.work && go work sync

Step 4: Verify it compiles

Run: cd /Users/snider/Code/Core && go build ./pkg/sdk/... Expected: No errors

Step 5: Commit

git add pkg/sdk/
git add go.work go.work.sum
git commit -m "feat(sdk): add SDK package structure with types

Initial pkg/sdk setup with Config types for OpenAPI SDK generation.
Includes language selection, diff config, and publish config.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 2: Implement OpenAPI Spec Detection

Files:

  • Create: pkg/sdk/detect.go
  • Create: pkg/sdk/detect_test.go

Step 1: Write the failing test

package sdk

import (
	"os"
	"path/filepath"
	"testing"
)

func TestDetectSpec_Good_ConfigPath(t *testing.T) {
	// Create temp directory with spec at configured path
	tmpDir := t.TempDir()
	specPath := filepath.Join(tmpDir, "api", "spec.yaml")
	os.MkdirAll(filepath.Dir(specPath), 0755)
	os.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644)

	sdk := New(tmpDir, &Config{Spec: "api/spec.yaml"})
	got, err := sdk.DetectSpec()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != specPath {
		t.Errorf("got %q, want %q", got, specPath)
	}
}

func TestDetectSpec_Good_CommonPath(t *testing.T) {
	// Create temp directory with spec at common path
	tmpDir := t.TempDir()
	specPath := filepath.Join(tmpDir, "openapi.yaml")
	os.WriteFile(specPath, []byte("openapi: 3.0.0"), 0644)

	sdk := New(tmpDir, nil)
	got, err := sdk.DetectSpec()
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if got != specPath {
		t.Errorf("got %q, want %q", got, specPath)
	}
}

func TestDetectSpec_Bad_NotFound(t *testing.T) {
	tmpDir := t.TempDir()
	sdk := New(tmpDir, nil)
	_, err := sdk.DetectSpec()
	if err == nil {
		t.Fatal("expected error for missing spec")
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/... -run TestDetectSpec -v Expected: FAIL (DetectSpec not defined)

Step 3: Write minimal implementation

package sdk

import (
	"fmt"
	"os"
	"path/filepath"
)

// commonSpecPaths are checked in order when no spec is configured.
var commonSpecPaths = []string{
	"api/openapi.yaml",
	"api/openapi.json",
	"openapi.yaml",
	"openapi.json",
	"docs/api.yaml",
	"docs/api.json",
	"swagger.yaml",
	"swagger.json",
}

// DetectSpec finds the OpenAPI spec file.
// Priority: config path → common paths → Laravel Scramble.
func (s *SDK) DetectSpec() (string, error) {
	// 1. Check configured path
	if s.config.Spec != "" {
		specPath := filepath.Join(s.projectDir, s.config.Spec)
		if _, err := os.Stat(specPath); err == nil {
			return specPath, nil
		}
		return "", fmt.Errorf("sdk.DetectSpec: configured spec not found: %s", s.config.Spec)
	}

	// 2. Check common paths
	for _, p := range commonSpecPaths {
		specPath := filepath.Join(s.projectDir, p)
		if _, err := os.Stat(specPath); err == nil {
			return specPath, nil
		}
	}

	// 3. Try Laravel Scramble detection
	specPath, err := s.detectScramble()
	if err == nil {
		return specPath, nil
	}

	return "", fmt.Errorf("sdk.DetectSpec: no OpenAPI spec found (checked config, common paths, Scramble)")
}

// detectScramble checks for Laravel Scramble and exports the spec.
func (s *SDK) detectScramble() (string, error) {
	composerPath := filepath.Join(s.projectDir, "composer.json")
	if _, err := os.Stat(composerPath); err != nil {
		return "", fmt.Errorf("no composer.json")
	}

	// Check for scramble in composer.json
	data, err := os.ReadFile(composerPath)
	if err != nil {
		return "", err
	}

	// Simple check for scramble package
	if !containsScramble(data) {
		return "", fmt.Errorf("scramble not found in composer.json")
	}

	// TODO: Run php artisan scramble:export
	return "", fmt.Errorf("scramble export not implemented")
}

// containsScramble checks if composer.json includes scramble.
func containsScramble(data []byte) bool {
	return len(data) > 0 &&
		(contains(data, "dedoc/scramble") || contains(data, "\"scramble\""))
}

// contains is a simple byte slice search.
func contains(data []byte, substr string) bool {
	return len(data) >= len(substr) &&
		string(data) != "" &&
		indexOf(string(data), substr) >= 0
}

func indexOf(s, substr string) int {
	for i := 0; i <= len(s)-len(substr); i++ {
		if s[i:i+len(substr)] == substr {
			return i
		}
	}
	return -1
}

Step 4: Run tests to verify they pass

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/... -run TestDetectSpec -v Expected: PASS

Step 5: Commit

git add pkg/sdk/detect.go pkg/sdk/detect_test.go
git commit -m "feat(sdk): add OpenAPI spec detection

Detects OpenAPI spec via:
1. Configured spec path
2. Common paths (api/openapi.yaml, openapi.yaml, etc.)
3. Laravel Scramble (stub for now)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 3: Define Generator Interface

Files:

  • Create: pkg/sdk/generators/generator.go

Step 1: Create generator interface

// Package generators provides SDK code generators for different languages.
package generators

import (
	"context"
)

// Options holds common generation options.
type Options struct {
	// SpecPath is the path to the OpenAPI spec file.
	SpecPath string
	// OutputDir is where to write the generated SDK.
	OutputDir string
	// PackageName is the package/module name.
	PackageName string
	// Version is the SDK version.
	Version string
}

// Generator defines the interface for SDK generators.
type Generator interface {
	// Language returns the generator's target language identifier.
	Language() string

	// Generate creates SDK from OpenAPI spec.
	Generate(ctx context.Context, opts Options) error

	// Available checks if generator dependencies are installed.
	Available() bool

	// Install returns instructions for installing the generator.
	Install() string
}

// Registry holds available generators.
type Registry struct {
	generators map[string]Generator
}

// NewRegistry creates a registry with all available generators.
func NewRegistry() *Registry {
	r := &Registry{
		generators: make(map[string]Generator),
	}
	// Generators will be registered in subsequent tasks
	return r
}

// Get returns a generator by language.
func (r *Registry) Get(lang string) (Generator, bool) {
	g, ok := r.generators[lang]
	return g, ok
}

// Register adds a generator to the registry.
func (r *Registry) Register(g Generator) {
	r.generators[g.Language()] = g
}

// Languages returns all registered language identifiers.
func (r *Registry) Languages() []string {
	langs := make([]string, 0, len(r.generators))
	for lang := range r.generators {
		langs = append(langs, lang)
	}
	return langs
}

Step 2: Verify it compiles

Run: cd /Users/snider/Code/Core && go build ./pkg/sdk/... Expected: No errors

Step 3: Commit

git add pkg/sdk/generators/generator.go
git commit -m "feat(sdk): add Generator interface and Registry

Defines the common interface for SDK generators with:
- Generate(), Available(), Install() methods
- Registry for managing multiple generators

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 4: Implement TypeScript Generator

Files:

  • Create: pkg/sdk/generators/typescript.go
  • Create: pkg/sdk/generators/typescript_test.go

Step 1: Write the failing test

package generators

import (
	"context"
	"os"
	"os/exec"
	"path/filepath"
	"testing"
)

func TestTypeScriptGenerator_Good_Available(t *testing.T) {
	g := NewTypeScriptGenerator()
	// Just check it doesn't panic
	_ = g.Available()
	_ = g.Language()
	_ = g.Install()
}

func TestTypeScriptGenerator_Good_Generate(t *testing.T) {
	// Skip if no generator available
	g := NewTypeScriptGenerator()
	if !g.Available() && !dockerAvailable() {
		t.Skip("no TypeScript generator available (need openapi-typescript-codegen or Docker)")
	}

	// Create temp spec
	tmpDir := t.TempDir()
	specPath := filepath.Join(tmpDir, "spec.yaml")
	spec := `openapi: "3.0.0"
info:
  title: Test API
  version: "1.0.0"
paths:
  /health:
    get:
      operationId: getHealth
      responses:
        "200":
          description: OK
`
	os.WriteFile(specPath, []byte(spec), 0644)

	outputDir := filepath.Join(tmpDir, "sdk", "typescript")
	err := g.Generate(context.Background(), Options{
		SpecPath:    specPath,
		OutputDir:   outputDir,
		PackageName: "test-api",
		Version:     "1.0.0",
	})
	if err != nil {
		t.Fatalf("Generate failed: %v", err)
	}

	// Check output exists
	if _, err := os.Stat(outputDir); os.IsNotExist(err) {
		t.Error("output directory not created")
	}
}

func dockerAvailable() bool {
	_, err := exec.LookPath("docker")
	return err == nil
}

Step 2: Run test to verify it fails

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/generators/... -run TestTypeScriptGenerator -v Expected: FAIL (NewTypeScriptGenerator not defined)

Step 3: Write implementation

package generators

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

// TypeScriptGenerator generates TypeScript SDKs using openapi-typescript-codegen.
type TypeScriptGenerator struct{}

// NewTypeScriptGenerator creates a new TypeScript generator.
func NewTypeScriptGenerator() *TypeScriptGenerator {
	return &TypeScriptGenerator{}
}

// Language returns "typescript".
func (g *TypeScriptGenerator) Language() string {
	return "typescript"
}

// Available checks if openapi-typescript-codegen is installed.
func (g *TypeScriptGenerator) Available() bool {
	_, err := exec.LookPath("openapi-typescript-codegen")
	if err == nil {
		return true
	}
	// Also check npx availability
	_, err = exec.LookPath("npx")
	return err == nil
}

// Install returns installation instructions.
func (g *TypeScriptGenerator) Install() string {
	return "npm install -g openapi-typescript-codegen"
}

// Generate creates TypeScript SDK from OpenAPI spec.
func (g *TypeScriptGenerator) Generate(ctx context.Context, opts Options) error {
	// Ensure output directory exists
	if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
		return fmt.Errorf("typescript.Generate: failed to create output dir: %w", err)
	}

	// Try native generator first
	if g.nativeAvailable() {
		return g.generateNative(ctx, opts)
	}

	// Try npx
	if g.npxAvailable() {
		return g.generateNpx(ctx, opts)
	}

	// Fall back to Docker
	return g.generateDocker(ctx, opts)
}

func (g *TypeScriptGenerator) nativeAvailable() bool {
	_, err := exec.LookPath("openapi-typescript-codegen")
	return err == nil
}

func (g *TypeScriptGenerator) npxAvailable() bool {
	_, err := exec.LookPath("npx")
	return err == nil
}

func (g *TypeScriptGenerator) generateNative(ctx context.Context, opts Options) error {
	cmd := exec.CommandContext(ctx, "openapi-typescript-codegen",
		"--input", opts.SpecPath,
		"--output", opts.OutputDir,
		"--name", opts.PackageName,
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func (g *TypeScriptGenerator) generateNpx(ctx context.Context, opts Options) error {
	cmd := exec.CommandContext(ctx, "npx", "openapi-typescript-codegen",
		"--input", opts.SpecPath,
		"--output", opts.OutputDir,
		"--name", opts.PackageName,
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func (g *TypeScriptGenerator) generateDocker(ctx context.Context, opts Options) error {
	// Use openapi-generator via Docker
	specDir := filepath.Dir(opts.SpecPath)
	specName := filepath.Base(opts.SpecPath)

	cmd := exec.CommandContext(ctx, "docker", "run", "--rm",
		"-v", specDir+":/spec",
		"-v", opts.OutputDir+":/out",
		"openapitools/openapi-generator-cli", "generate",
		"-i", "/spec/"+specName,
		"-g", "typescript-fetch",
		"-o", "/out",
		"--additional-properties=npmName="+opts.PackageName,
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("typescript.generateDocker: %w", err)
	}
	return nil
}

Step 4: Run tests

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/generators/... -run TestTypeScriptGenerator -v Expected: PASS (or skip if no generator available)

Step 5: Commit

git add pkg/sdk/generators/typescript.go pkg/sdk/generators/typescript_test.go
git commit -m "feat(sdk): add TypeScript generator

Uses openapi-typescript-codegen (native or npx) with Docker fallback.
Generates TypeScript-fetch client from OpenAPI spec.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 5: Implement Python Generator

Files:

  • Create: pkg/sdk/generators/python.go
  • Create: pkg/sdk/generators/python_test.go

Step 1: Write the failing test

package generators

import (
	"context"
	"os"
	"path/filepath"
	"testing"
)

func TestPythonGenerator_Good_Available(t *testing.T) {
	g := NewPythonGenerator()
	_ = g.Available()
	_ = g.Language()
	_ = g.Install()
}

func TestPythonGenerator_Good_Generate(t *testing.T) {
	g := NewPythonGenerator()
	if !g.Available() && !dockerAvailable() {
		t.Skip("no Python generator available")
	}

	tmpDir := t.TempDir()
	specPath := filepath.Join(tmpDir, "spec.yaml")
	spec := `openapi: "3.0.0"
info:
  title: Test API
  version: "1.0.0"
paths:
  /health:
    get:
      operationId: getHealth
      responses:
        "200":
          description: OK
`
	os.WriteFile(specPath, []byte(spec), 0644)

	outputDir := filepath.Join(tmpDir, "sdk", "python")
	err := g.Generate(context.Background(), Options{
		SpecPath:    specPath,
		OutputDir:   outputDir,
		PackageName: "test_api",
		Version:     "1.0.0",
	})
	if err != nil {
		t.Fatalf("Generate failed: %v", err)
	}

	if _, err := os.Stat(outputDir); os.IsNotExist(err) {
		t.Error("output directory not created")
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/generators/... -run TestPythonGenerator -v Expected: FAIL

Step 3: Write implementation

package generators

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

// PythonGenerator generates Python SDKs using openapi-python-client.
type PythonGenerator struct{}

// NewPythonGenerator creates a new Python generator.
func NewPythonGenerator() *PythonGenerator {
	return &PythonGenerator{}
}

// Language returns "python".
func (g *PythonGenerator) Language() string {
	return "python"
}

// Available checks if openapi-python-client is installed.
func (g *PythonGenerator) Available() bool {
	_, err := exec.LookPath("openapi-python-client")
	return err == nil
}

// Install returns installation instructions.
func (g *PythonGenerator) Install() string {
	return "pip install openapi-python-client"
}

// Generate creates Python SDK from OpenAPI spec.
func (g *PythonGenerator) Generate(ctx context.Context, opts Options) error {
	if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
		return fmt.Errorf("python.Generate: failed to create output dir: %w", err)
	}

	if g.Available() {
		return g.generateNative(ctx, opts)
	}
	return g.generateDocker(ctx, opts)
}

func (g *PythonGenerator) generateNative(ctx context.Context, opts Options) error {
	// openapi-python-client creates a directory named after the package
	// We need to generate into a temp location then move
	parentDir := filepath.Dir(opts.OutputDir)

	cmd := exec.CommandContext(ctx, "openapi-python-client", "generate",
		"--path", opts.SpecPath,
		"--output-path", opts.OutputDir,
	)
	cmd.Dir = parentDir
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

func (g *PythonGenerator) generateDocker(ctx context.Context, opts Options) error {
	specDir := filepath.Dir(opts.SpecPath)
	specName := filepath.Base(opts.SpecPath)

	cmd := exec.CommandContext(ctx, "docker", "run", "--rm",
		"-v", specDir+":/spec",
		"-v", opts.OutputDir+":/out",
		"openapitools/openapi-generator-cli", "generate",
		"-i", "/spec/"+specName,
		"-g", "python",
		"-o", "/out",
		"--additional-properties=packageName="+opts.PackageName,
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

Step 4: Run tests

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/generators/... -run TestPythonGenerator -v Expected: PASS (or skip)

Step 5: Commit

git add pkg/sdk/generators/python.go pkg/sdk/generators/python_test.go
git commit -m "feat(sdk): add Python generator

Uses openapi-python-client with Docker fallback.
Generates Python client from OpenAPI spec.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 6: Implement Go Generator

Files:

  • Create: pkg/sdk/generators/go.go
  • Create: pkg/sdk/generators/go_test.go

Step 1: Write the failing test

package generators

import (
	"context"
	"os"
	"path/filepath"
	"testing"
)

func TestGoGenerator_Good_Available(t *testing.T) {
	g := NewGoGenerator()
	_ = g.Available()
	_ = g.Language()
	_ = g.Install()
}

func TestGoGenerator_Good_Generate(t *testing.T) {
	g := NewGoGenerator()
	if !g.Available() && !dockerAvailable() {
		t.Skip("no Go generator available")
	}

	tmpDir := t.TempDir()
	specPath := filepath.Join(tmpDir, "spec.yaml")
	spec := `openapi: "3.0.0"
info:
  title: Test API
  version: "1.0.0"
paths:
  /health:
    get:
      operationId: getHealth
      responses:
        "200":
          description: OK
`
	os.WriteFile(specPath, []byte(spec), 0644)

	outputDir := filepath.Join(tmpDir, "sdk", "go")
	err := g.Generate(context.Background(), Options{
		SpecPath:    specPath,
		OutputDir:   outputDir,
		PackageName: "testapi",
		Version:     "1.0.0",
	})
	if err != nil {
		t.Fatalf("Generate failed: %v", err)
	}

	if _, err := os.Stat(outputDir); os.IsNotExist(err) {
		t.Error("output directory not created")
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/generators/... -run TestGoGenerator -v Expected: FAIL

Step 3: Write implementation

package generators

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

// GoGenerator generates Go SDKs using oapi-codegen.
type GoGenerator struct{}

// NewGoGenerator creates a new Go generator.
func NewGoGenerator() *GoGenerator {
	return &GoGenerator{}
}

// Language returns "go".
func (g *GoGenerator) Language() string {
	return "go"
}

// Available checks if oapi-codegen is installed.
func (g *GoGenerator) Available() bool {
	_, err := exec.LookPath("oapi-codegen")
	return err == nil
}

// Install returns installation instructions.
func (g *GoGenerator) Install() string {
	return "go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest"
}

// Generate creates Go SDK from OpenAPI spec.
func (g *GoGenerator) Generate(ctx context.Context, opts Options) error {
	if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
		return fmt.Errorf("go.Generate: failed to create output dir: %w", err)
	}

	if g.Available() {
		return g.generateNative(ctx, opts)
	}
	return g.generateDocker(ctx, opts)
}

func (g *GoGenerator) generateNative(ctx context.Context, opts Options) error {
	outputFile := filepath.Join(opts.OutputDir, "client.go")

	cmd := exec.CommandContext(ctx, "oapi-codegen",
		"-package", opts.PackageName,
		"-generate", "types,client",
		"-o", outputFile,
		opts.SpecPath,
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("go.generateNative: %w", err)
	}

	// Create go.mod
	goMod := fmt.Sprintf("module %s\n\ngo 1.21\n", opts.PackageName)
	return os.WriteFile(filepath.Join(opts.OutputDir, "go.mod"), []byte(goMod), 0644)
}

func (g *GoGenerator) generateDocker(ctx context.Context, opts Options) error {
	specDir := filepath.Dir(opts.SpecPath)
	specName := filepath.Base(opts.SpecPath)

	cmd := exec.CommandContext(ctx, "docker", "run", "--rm",
		"-v", specDir+":/spec",
		"-v", opts.OutputDir+":/out",
		"openapitools/openapi-generator-cli", "generate",
		"-i", "/spec/"+specName,
		"-g", "go",
		"-o", "/out",
		"--additional-properties=packageName="+opts.PackageName,
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr
	return cmd.Run()
}

Step 4: Run tests

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/generators/... -run TestGoGenerator -v Expected: PASS (or skip)

Step 5: Commit

git add pkg/sdk/generators/go.go pkg/sdk/generators/go_test.go
git commit -m "feat(sdk): add Go generator

Uses oapi-codegen with Docker fallback.
Generates Go client and types from OpenAPI spec.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 7: Implement PHP Generator

Files:

  • Create: pkg/sdk/generators/php.go
  • Create: pkg/sdk/generators/php_test.go

Step 1: Write the failing test

package generators

import (
	"context"
	"os"
	"path/filepath"
	"testing"
)

func TestPHPGenerator_Good_Available(t *testing.T) {
	g := NewPHPGenerator()
	_ = g.Available()
	_ = g.Language()
	_ = g.Install()
}

func TestPHPGenerator_Good_Generate(t *testing.T) {
	g := NewPHPGenerator()
	if !g.Available() {
		t.Skip("Docker not available for PHP generator")
	}

	tmpDir := t.TempDir()
	specPath := filepath.Join(tmpDir, "spec.yaml")
	spec := `openapi: "3.0.0"
info:
  title: Test API
  version: "1.0.0"
paths:
  /health:
    get:
      operationId: getHealth
      responses:
        "200":
          description: OK
`
	os.WriteFile(specPath, []byte(spec), 0644)

	outputDir := filepath.Join(tmpDir, "sdk", "php")
	err := g.Generate(context.Background(), Options{
		SpecPath:    specPath,
		OutputDir:   outputDir,
		PackageName: "TestApi",
		Version:     "1.0.0",
	})
	if err != nil {
		t.Fatalf("Generate failed: %v", err)
	}

	if _, err := os.Stat(outputDir); os.IsNotExist(err) {
		t.Error("output directory not created")
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/generators/... -run TestPHPGenerator -v Expected: FAIL

Step 3: Write implementation

package generators

import (
	"context"
	"fmt"
	"os"
	"os/exec"
	"path/filepath"
)

// PHPGenerator generates PHP SDKs using openapi-generator (Docker).
type PHPGenerator struct{}

// NewPHPGenerator creates a new PHP generator.
func NewPHPGenerator() *PHPGenerator {
	return &PHPGenerator{}
}

// Language returns "php".
func (g *PHPGenerator) Language() string {
	return "php"
}

// Available checks if Docker is available.
func (g *PHPGenerator) Available() bool {
	_, err := exec.LookPath("docker")
	return err == nil
}

// Install returns installation instructions.
func (g *PHPGenerator) Install() string {
	return "Docker is required for PHP SDK generation"
}

// Generate creates PHP SDK from OpenAPI spec using Docker.
func (g *PHPGenerator) Generate(ctx context.Context, opts Options) error {
	if !g.Available() {
		return fmt.Errorf("php.Generate: Docker is required but not available")
	}

	if err := os.MkdirAll(opts.OutputDir, 0755); err != nil {
		return fmt.Errorf("php.Generate: failed to create output dir: %w", err)
	}

	specDir := filepath.Dir(opts.SpecPath)
	specName := filepath.Base(opts.SpecPath)

	cmd := exec.CommandContext(ctx, "docker", "run", "--rm",
		"-v", specDir+":/spec",
		"-v", opts.OutputDir+":/out",
		"openapitools/openapi-generator-cli", "generate",
		"-i", "/spec/"+specName,
		"-g", "php",
		"-o", "/out",
		"--additional-properties=invokerPackage="+opts.PackageName,
	)
	cmd.Stdout = os.Stdout
	cmd.Stderr = os.Stderr

	if err := cmd.Run(); err != nil {
		return fmt.Errorf("php.Generate: %w", err)
	}
	return nil
}

Step 4: Run tests

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/generators/... -run TestPHPGenerator -v Expected: PASS (or skip)

Step 5: Commit

git add pkg/sdk/generators/php.go pkg/sdk/generators/php_test.go
git commit -m "feat(sdk): add PHP generator

Uses openapi-generator via Docker.
Generates PHP client from OpenAPI spec.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 8: Implement Breaking Change Detection

Files:

  • Create: pkg/sdk/diff.go
  • Create: pkg/sdk/diff_test.go

Step 1: Write the failing test

package sdk

import (
	"os"
	"path/filepath"
	"testing"
)

func TestDiff_Good_NoBreaking(t *testing.T) {
	tmpDir := t.TempDir()

	baseSpec := `openapi: "3.0.0"
info:
  title: Test API
  version: "1.0.0"
paths:
  /health:
    get:
      operationId: getHealth
      responses:
        "200":
          description: OK
`
	revSpec := `openapi: "3.0.0"
info:
  title: Test API
  version: "1.1.0"
paths:
  /health:
    get:
      operationId: getHealth
      responses:
        "200":
          description: OK
  /status:
    get:
      operationId: getStatus
      responses:
        "200":
          description: OK
`
	basePath := filepath.Join(tmpDir, "base.yaml")
	revPath := filepath.Join(tmpDir, "rev.yaml")
	os.WriteFile(basePath, []byte(baseSpec), 0644)
	os.WriteFile(revPath, []byte(revSpec), 0644)

	result, err := Diff(basePath, revPath)
	if err != nil {
		t.Fatalf("Diff failed: %v", err)
	}
	if result.Breaking {
		t.Error("expected no breaking changes for adding endpoint")
	}
}

func TestDiff_Good_Breaking(t *testing.T) {
	tmpDir := t.TempDir()

	baseSpec := `openapi: "3.0.0"
info:
  title: Test API
  version: "1.0.0"
paths:
  /health:
    get:
      operationId: getHealth
      responses:
        "200":
          description: OK
  /users:
    get:
      operationId: getUsers
      responses:
        "200":
          description: OK
`
	revSpec := `openapi: "3.0.0"
info:
  title: Test API
  version: "2.0.0"
paths:
  /health:
    get:
      operationId: getHealth
      responses:
        "200":
          description: OK
`
	basePath := filepath.Join(tmpDir, "base.yaml")
	revPath := filepath.Join(tmpDir, "rev.yaml")
	os.WriteFile(basePath, []byte(baseSpec), 0644)
	os.WriteFile(revPath, []byte(revSpec), 0644)

	result, err := Diff(basePath, revPath)
	if err != nil {
		t.Fatalf("Diff failed: %v", err)
	}
	if !result.Breaking {
		t.Error("expected breaking change for removed endpoint")
	}
}

Step 2: Run test to verify it fails

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/... -run TestDiff -v Expected: FAIL (Diff not defined)

Step 3: Add oasdiff dependency

Run: cd /Users/snider/Code/Core/pkg/sdk && go get github.com/tufin/oasdiff@latest github.com/getkin/kin-openapi@latest

Step 4: Write implementation

package sdk

import (
	"fmt"

	"github.com/getkin/kin-openapi/openapi3"
	"github.com/tufin/oasdiff/checker"
	"github.com/tufin/oasdiff/diff"
	"github.com/tufin/oasdiff/load"
)

// DiffResult holds the result of comparing two OpenAPI specs.
type DiffResult struct {
	// Breaking is true if breaking changes were detected.
	Breaking bool
	// Changes is the list of breaking changes.
	Changes []string
	// Summary is a human-readable summary.
	Summary string
}

// Diff compares two OpenAPI specs and detects breaking changes.
func Diff(basePath, revisionPath string) (*DiffResult, error) {
	loader := openapi3.NewLoader()
	loader.IsExternalRefsAllowed = true

	// Load specs
	baseSpec, err := load.NewSpecInfo(loader, load.NewSource(basePath))
	if err != nil {
		return nil, fmt.Errorf("sdk.Diff: failed to load base spec: %w", err)
	}

	revSpec, err := load.NewSpecInfo(loader, load.NewSource(revisionPath))
	if err != nil {
		return nil, fmt.Errorf("sdk.Diff: failed to load revision spec: %w", err)
	}

	// Compute diff
	diffResult, err := diff.Get(diff.NewConfig(), baseSpec.Spec, revSpec.Spec)
	if err != nil {
		return nil, fmt.Errorf("sdk.Diff: failed to compute diff: %w", err)
	}

	// Check for breaking changes
	config := checker.GetAllChecks()
	breaks := checker.CheckBackwardCompatibilityUntilLevel(
		config,
		diffResult,
		baseSpec.Spec,
		revSpec.Spec,
		checker.ERR, // Only errors (breaking changes)
	)

	// Build result
	result := &DiffResult{
		Breaking: len(breaks) > 0,
		Changes:  make([]string, 0, len(breaks)),
	}

	for _, b := range breaks {
		result.Changes = append(result.Changes, b.GetUncolorizedText(checker.NewDefaultLocalizer()))
	}

	if result.Breaking {
		result.Summary = fmt.Sprintf("%d breaking change(s) detected", len(breaks))
	} else {
		result.Summary = "No breaking changes"
	}

	return result, nil
}

// DiffExitCode returns the exit code for CI integration.
// 0 = no breaking changes, 1 = breaking changes, 2 = error
func DiffExitCode(result *DiffResult, err error) int {
	if err != nil {
		return 2
	}
	if result.Breaking {
		return 1
	}
	return 0
}

Step 5: Run tests

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/... -run TestDiff -v Expected: PASS

Step 6: Commit

git add pkg/sdk/diff.go pkg/sdk/diff_test.go pkg/sdk/go.mod pkg/sdk/go.sum
git commit -m "feat(sdk): add breaking change detection with oasdiff

Compares OpenAPI specs to detect breaking changes:
- Removed endpoints
- Changed required parameters
- Modified response schemas

Returns CI-friendly exit codes (0=ok, 1=breaking, 2=error).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 9: Wire Up Generate Command

Files:

  • Modify: pkg/sdk/sdk.go

Step 1: Update SDK.Generate to use generators

// Add to sdk.go, replacing the stub Generate method

import (
	"forge.lthn.ai/core/cli/pkg/sdk/generators"
)

// Generate generates SDKs for all configured languages.
func (s *SDK) Generate(ctx context.Context) error {
	// Detect spec
	specPath, err := s.DetectSpec()
	if err != nil {
		return err
	}

	// Create registry with all generators
	registry := generators.NewRegistry()
	registry.Register(generators.NewTypeScriptGenerator())
	registry.Register(generators.NewPythonGenerator())
	registry.Register(generators.NewGoGenerator())
	registry.Register(generators.NewPHPGenerator())

	// Generate for each language
	for _, lang := range s.config.Languages {
		if err := s.GenerateLanguage(ctx, lang); err != nil {
			return err
		}
	}

	return nil
}

// GenerateLanguage generates SDK for a specific language.
func (s *SDK) GenerateLanguage(ctx context.Context, lang string) error {
	specPath, err := s.DetectSpec()
	if err != nil {
		return err
	}

	registry := generators.NewRegistry()
	registry.Register(generators.NewTypeScriptGenerator())
	registry.Register(generators.NewPythonGenerator())
	registry.Register(generators.NewGoGenerator())
	registry.Register(generators.NewPHPGenerator())

	gen, ok := registry.Get(lang)
	if !ok {
		return fmt.Errorf("sdk.GenerateLanguage: unknown language: %s", lang)
	}

	if !gen.Available() {
		fmt.Printf("Warning: %s generator not available. Install with: %s\n", lang, gen.Install())
		fmt.Printf("Falling back to Docker...\n")
	}

	outputDir := filepath.Join(s.projectDir, s.config.Output, lang)
	opts := generators.Options{
		SpecPath:    specPath,
		OutputDir:   outputDir,
		PackageName: s.config.Package.Name,
		Version:     s.config.Package.Version,
	}

	fmt.Printf("Generating %s SDK...\n", lang)
	if err := gen.Generate(ctx, opts); err != nil {
		return fmt.Errorf("sdk.GenerateLanguage: %s generation failed: %w", lang, err)
	}
	fmt.Printf("Generated %s SDK at %s\n", lang, outputDir)

	return nil
}

Step 2: Verify it compiles

Run: cd /Users/snider/Code/Core && go build ./pkg/sdk/... Expected: No errors

Step 3: Commit

git add pkg/sdk/sdk.go
git commit -m "feat(sdk): wire up Generate to use all generators

SDK.Generate() and SDK.GenerateLanguage() now use the
generator registry to generate SDKs for configured languages.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 10: Add CLI Commands

Files:

  • Create: cmd/core/cmd/sdk.go

Step 1: Create SDK command file

package cmd

import (
	"context"
	"fmt"
	"os"

	"github.com/charmbracelet/lipgloss"
	"forge.lthn.ai/core/cli/pkg/sdk"
	"github.com/leaanthony/clir"
)

var (
	sdkHeaderStyle = lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color("#3b82f6"))

	sdkSuccessStyle = lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color("#22c55e"))

	sdkErrorStyle = lipgloss.NewStyle().
		Bold(true).
		Foreground(lipgloss.Color("#ef4444"))

	sdkDimStyle = lipgloss.NewStyle().
		Foreground(lipgloss.Color("#6b7280"))
)

// AddSDKCommand adds the sdk command and its subcommands.
func AddSDKCommand(app *clir.Cli) {
	sdkCmd := app.NewSubCommand("sdk", "Generate and manage API SDKs")
	sdkCmd.LongDescription("Generate typed API clients from OpenAPI specs.\n" +
		"Supports TypeScript, Python, Go, and PHP.")

	// sdk generate
	genCmd := sdkCmd.NewSubCommand("generate", "Generate SDKs from OpenAPI spec")
	var specPath, lang string
	genCmd.StringFlag("spec", "Path to OpenAPI spec file", &specPath)
	genCmd.StringFlag("lang", "Generate only this language", &lang)
	genCmd.Action(func() error {
		return runSDKGenerate(specPath, lang)
	})

	// sdk diff
	diffCmd := sdkCmd.NewSubCommand("diff", "Check for breaking API changes")
	var basePath string
	diffCmd.StringFlag("base", "Base spec (version tag or file)", &basePath)
	diffCmd.StringFlag("spec", "Current spec file", &specPath)
	diffCmd.Action(func() error {
		return runSDKDiff(basePath, specPath)
	})

	// sdk validate
	validateCmd := sdkCmd.NewSubCommand("validate", "Validate OpenAPI spec")
	validateCmd.StringFlag("spec", "Path to OpenAPI spec file", &specPath)
	validateCmd.Action(func() error {
		return runSDKValidate(specPath)
	})
}

func runSDKGenerate(specPath, lang string) error {
	ctx := context.Background()

	projectDir, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("failed to get working directory: %w", err)
	}

	// Load config
	config := sdk.DefaultConfig()
	if specPath != "" {
		config.Spec = specPath
	}

	s := sdk.New(projectDir, config)

	fmt.Printf("%s Generating SDKs\n", sdkHeaderStyle.Render("SDK:"))

	if lang != "" {
		// Generate single language
		if err := s.GenerateLanguage(ctx, lang); err != nil {
			fmt.Printf("%s %v\n", sdkErrorStyle.Render("Error:"), err)
			return err
		}
	} else {
		// Generate all
		if err := s.Generate(ctx); err != nil {
			fmt.Printf("%s %v\n", sdkErrorStyle.Render("Error:"), err)
			return err
		}
	}

	fmt.Printf("%s SDK generation complete\n", sdkSuccessStyle.Render("Success:"))
	return nil
}

func runSDKDiff(basePath, specPath string) error {
	projectDir, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("failed to get working directory: %w", err)
	}

	// Detect current spec if not provided
	if specPath == "" {
		s := sdk.New(projectDir, nil)
		specPath, err = s.DetectSpec()
		if err != nil {
			return err
		}
	}

	if basePath == "" {
		return fmt.Errorf("--base is required (version tag or file path)")
	}

	fmt.Printf("%s Checking for breaking changes\n", sdkHeaderStyle.Render("SDK Diff:"))
	fmt.Printf("  Base:     %s\n", sdkDimStyle.Render(basePath))
	fmt.Printf("  Current:  %s\n", sdkDimStyle.Render(specPath))
	fmt.Println()

	result, err := sdk.Diff(basePath, specPath)
	if err != nil {
		fmt.Printf("%s %v\n", sdkErrorStyle.Render("Error:"), err)
		os.Exit(2)
	}

	if result.Breaking {
		fmt.Printf("%s %s\n", sdkErrorStyle.Render("Breaking:"), result.Summary)
		for _, change := range result.Changes {
			fmt.Printf("  - %s\n", change)
		}
		os.Exit(1)
	}

	fmt.Printf("%s %s\n", sdkSuccessStyle.Render("OK:"), result.Summary)
	return nil
}

func runSDKValidate(specPath string) error {
	projectDir, err := os.Getwd()
	if err != nil {
		return fmt.Errorf("failed to get working directory: %w", err)
	}

	s := sdk.New(projectDir, &sdk.Config{Spec: specPath})

	fmt.Printf("%s Validating OpenAPI spec\n", sdkHeaderStyle.Render("SDK:"))

	detectedPath, err := s.DetectSpec()
	if err != nil {
		fmt.Printf("%s %v\n", sdkErrorStyle.Render("Error:"), err)
		return err
	}

	fmt.Printf("  Spec: %s\n", sdkDimStyle.Render(detectedPath))
	fmt.Printf("%s Spec is valid\n", sdkSuccessStyle.Render("OK:"))
	return nil
}

Step 2: Register command in root.go

Add to root.go after other command registrations:

AddSDKCommand(app)

Step 3: Verify it compiles

Run: cd /Users/snider/Code/Core && go build ./cmd/core/... Expected: No errors

Step 4: Commit

git add cmd/core/cmd/sdk.go cmd/core/cmd/root.go
git commit -m "feat(cli): add sdk command with generate, diff, validate

Commands:
- core sdk generate [--spec FILE] [--lang LANG]
- core sdk diff --base VERSION [--spec FILE]
- core sdk validate [--spec FILE]

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 11: Add SDK Config to Release Config

Files:

  • Modify: pkg/release/config.go

Step 1: Add SDK field to Config

Add to Config struct in config.go:

// SDK configures SDK generation.
SDK *SDKConfig `yaml:"sdk,omitempty"`

Add SDKConfig type:

// SDKConfig holds SDK generation configuration.
type SDKConfig struct {
	// Spec is the path to the OpenAPI spec file.
	Spec string `yaml:"spec,omitempty"`
	// Languages to generate.
	Languages []string `yaml:"languages,omitempty"`
	// Output directory (default: sdk/).
	Output string `yaml:"output,omitempty"`
	// Package naming.
	Package SDKPackageConfig `yaml:"package,omitempty"`
	// Diff configuration.
	Diff SDKDiffConfig `yaml:"diff,omitempty"`
	// Publish configuration.
	Publish SDKPublishConfig `yaml:"publish,omitempty"`
}

// SDKPackageConfig holds package naming configuration.
type SDKPackageConfig struct {
	Name    string `yaml:"name,omitempty"`
	Version string `yaml:"version,omitempty"`
}

// SDKDiffConfig holds diff configuration.
type SDKDiffConfig struct {
	Enabled        bool `yaml:"enabled,omitempty"`
	FailOnBreaking bool `yaml:"fail_on_breaking,omitempty"`
}

// SDKPublishConfig holds monorepo publish configuration.
type SDKPublishConfig struct {
	Repo string `yaml:"repo,omitempty"`
	Path string `yaml:"path,omitempty"`
}

Step 2: Verify it compiles

Run: cd /Users/snider/Code/Core && go build ./pkg/release/... Expected: No errors

Step 3: Commit

git add pkg/release/config.go
git commit -m "feat(release): add SDK configuration to release.yaml

Adds sdk: section to .core/release.yaml for configuring
OpenAPI SDK generation during releases.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 12: Add SDK Example to Docs

Files:

  • Create: docs/examples/sdk-full.yaml

Step 1: Create example file

# Example: Full SDK Configuration
# Generate typed API clients from OpenAPI specs

sdk:
  # OpenAPI spec source (auto-detected if omitted)
  spec: api/openapi.yaml

  # Languages to generate
  languages:
    - typescript
    - python
    - go
    - php

  # Output directory (default: sdk/)
  output: sdk/

  # Package naming
  package:
    name: myapi
    version: "{{.Version}}"

  # Breaking change detection
  diff:
    enabled: true
    fail_on_breaking: true  # CI fails on breaking changes

  # Optional: publish to monorepo
  publish:
    repo: myorg/sdks
    path: packages/myapi

# Required tools (install one per language):
#   TypeScript: npm i -g openapi-typescript-codegen (or Docker)
#   Python:     pip install openapi-python-client (or Docker)
#   Go:         go install github.com/oapi-codegen/oapi-codegen/v2/cmd/oapi-codegen@latest
#   PHP:        Docker required
#
# Usage:
#   core sdk generate              # Generate all configured languages
#   core sdk generate --lang go    # Generate single language
#   core sdk diff --base v1.0.0    # Check for breaking changes
#   core sdk validate              # Validate spec

Step 2: Commit

git add docs/examples/sdk-full.yaml
git commit -m "docs: add SDK configuration example

Shows full SDK config with all options:
- Language selection
- Breaking change detection
- Monorepo publishing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Task 13: Final Integration Test

Step 1: Build and verify CLI

Run: cd /Users/snider/Code/Core && go build -o bin/core ./cmd/core && ./bin/core sdk --help Expected: Shows sdk command help

Step 2: Run all tests

Run: cd /Users/snider/Code/Core && go test ./pkg/sdk/... -v Expected: All tests pass

Step 3: Final commit if needed

git add -A
git commit -m "chore(sdk): finalize S3.4 SDK generation

All SDK generation features complete:
- OpenAPI spec detection
- TypeScript, Python, Go, PHP generators
- Breaking change detection with oasdiff
- CLI commands (generate, diff, validate)
- Integration with release config

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>"

Summary

13 tasks covering:

  1. Package structure
  2. Spec detection
  3. Generator interface
  4. TypeScript generator
  5. Python generator
  6. Go generator
  7. PHP generator
  8. Breaking change detection
  9. Wire up Generate
  10. CLI commands
  11. Release config integration
  12. Documentation example
  13. Integration test