# Phase 5: CLI Scaffolding - Research

**Researched:** 2026-02-05
**Domain:** CLI parsing, file generation, interactive prompts, terminal output
**Confidence:** HIGH

## Summary

This phase implements the `summer` CLI tool for scaffolding plugins, themes, components, controllers, models, and migrations. The research covers CLI parsing libraries for Scala 3/ZIO, file system operations, template-based code generation, interactive prompts, and terminal UI (colors, progress bars).

The recommended stack uses: **ZIO CLI 0.7.4** for command parsing (native ZIO integration, built-in wizard mode, shell completion), **os-lib 0.11.7** for file operations (bundled with Mill, simple API, path handling), **fansi 0.5.1** for colored terminal output, and **string interpolation** for template generation (simple scaffolds don't need template engine). Interactive prompts use ZIO CLI's built-in wizard mode with fallback to JLine for advanced cases.

**Primary recommendation:** Use ZIO CLI for all command parsing with noun-verb subcommand structure, os-lib for file generation with `os.makeDir.all` and `os.write`, string interpolation for templates with placeholder replacement, and fansi for colored output with `--no-color` fallback.

## Standard Stack

The established libraries/tools for this domain:

### Core
| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| ZIO CLI | 0.7.4 | Command parsing | Native ZIO, wizard mode, shell completion, type-safe |
| os-lib | 0.11.7 | File operations | Bundled with Mill, simple API, cross-platform paths |
| fansi | 0.5.1 | Colored output | Zero-dependency, Scala 3 native, efficient |

### Supporting
| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| JLine | 4.0.0 | Advanced prompts | Complex interactive input beyond wizard mode |
| circe-yaml | 0.16.1 | YAML generation | Generating plugin.yaml manifests |

### Alternatives Considered
| Instead of | Could Use | Tradeoff |
|------------|-----------|----------|
| ZIO CLI | MainArgs | MainArgs simpler but no wizard mode, shell completion |
| ZIO CLI | Decline | Decline uses cats, needs interop-cats for ZIO |
| os-lib | java.nio.file | nio verbose, os-lib has better ergonomics |
| fansi | scala-rainbow | scala-rainbow less maintained, fansi more features |
| String interpolation | Pebble | Overkill for scaffolds, adds dependency |

**Installation (Mill build.mill):**
```scala
def mvnDeps = Seq(
  // Existing deps from Phase 1-4...

  // CLI parsing
  mvn"dev.zio::zio-cli:0.7.4",

  // File operations (bundled with Mill, but explicit for CLI module)
  mvn"com.lihaoyi::os-lib:0.11.7",

  // Colored terminal output
  mvn"com.lihaoyi::fansi:0.5.1"
)
```

## Architecture Patterns

### Recommended CLI Module Structure
```
cli/
├── src/
│   ├── Main.scala              # CLI entry point, CliApp
│   ├── commands/
│   │   ├── PluginCommands.scala    # summer plugin create/list
│   │   ├── ThemeCommands.scala     # summer theme create/list
│   │   ├── ComponentCommands.scala # summer component create
│   │   ├── ControllerCommands.scala # summer controller create
│   │   ├── ModelCommands.scala     # summer model create
│   │   ├── MigrationCommands.scala # summer migration create/run/status
│   │   └── VersionCommand.scala    # summer version
│   ├── scaffold/
│   │   ├── ScaffoldService.scala   # File generation service
│   │   ├── TemplateRenderer.scala  # Variable substitution
│   │   └── templates/
│   │       ├── plugin/
│   │       ├── theme/
│   │       ├── component/
│   │       ├── controller/
│   │       └── model/
│   ├── config/
│   │   └── ConfigDetector.scala    # Find project root, load config
│   └── output/
│       ├── Console.scala           # Colored output service
│       └── Progress.scala          # Progress bar service
```

### Pattern 1: ZIO CLI Command Structure (Noun-Verb)
**What:** Commands follow noun-verb pattern with subcommands
**When to use:** All CLI commands
**Example:**
```scala
// Source: ZIO CLI documentation + CONTEXT.md decisions
import zio.cli.*

// Command models
sealed trait CliCommand
object CliCommand:
  case class PluginCreate(name: String, dryRun: Boolean) extends CliCommand
  case class ThemeCreate(name: String, template: String, dryRun: Boolean) extends CliCommand
  case class ComponentCreate(plugin: String, name: String, dryRun: Boolean) extends CliCommand
  case class ModelCreate(plugin: String, name: String, noMigration: Boolean, dryRun: Boolean) extends CliCommand
  case class MigrationCreate(plugin: String, name: Option[String], dryRun: Boolean) extends CliCommand
  case class Version() extends CliCommand

// Options
val dryRunOption: Options[Boolean] =
  Options.boolean("dry-run").alias("n")
    .withHelp("Show what would be created without creating")

val templateOption: Options[String] =
  Options.text("template").withDefault("starter")
    .withHelp("Theme template: blank or starter")

// Arguments
val pluginNameArg: Args[String] =
  Args.text("name")
    .withHelp("Plugin name in Author.Name format (e.g., Golem15.Blog)")

// Subcommands
val pluginCreate: Command[CliCommand.PluginCreate] =
  Command("create", dryRunOption, pluginNameArg)
    .withHelp("Create a new plugin scaffold")
    .map((dryRun, name) => CliCommand.PluginCreate(name, dryRun))

val pluginCommand: Command[CliCommand] =
  Command("plugin")
    .withHelp("Plugin management commands")
    .subcommands(pluginCreate)

// Top-level command combining all subcommands
val summerCommand: Command[CliCommand] =
  Command("summer")
    .subcommands(
      pluginCommand,
      themeCommand,
      componentCommand,
      controllerCommand,
      modelCommand,
      migrationCommand,
      versionCommand
    )
```

### Pattern 2: CLI Application Entry Point
**What:** Main entry using ZIOCliDefault with CliApp.make
**When to use:** CLI main class
**Example:**
```scala
// Source: ZIO CLI documentation
import zio.*
import zio.cli.*

object Main extends ZIOCliDefault:
  val cliApp = CliApp.make(
    name = "summer",
    version = BuildInfo.version,
    summary = HelpDoc.p("SummerCMS scaffolding CLI"),
    command = summerCommand
  ) { command =>
    command match
      case CliCommand.PluginCreate(name, dryRun) =>
        PluginScaffold.create(name, dryRun)
      case CliCommand.ThemeCreate(name, template, dryRun) =>
        ThemeScaffold.create(name, template, dryRun)
      case CliCommand.Version() =>
        printVersion
      // ... other commands
  }.provideSome[ZIOAppArgs](
    ScaffoldService.live,
    ConsoleOutput.live,
    ConfigDetector.live
  )

  def printVersion: ZIO[Any, Nothing, Unit] =
    Console.printLine(s"SummerCMS ${BuildInfo.version} (Scala ${BuildInfo.scalaVersion}, JVM ${System.getProperty("java.version")})")
      .orDie
```

### Pattern 3: Scaffold Service with os-lib
**What:** Service for file generation using os-lib operations
**When to use:** All file creation operations
**Example:**
```scala
// Source: os-lib documentation
import zio.*

trait ScaffoldService:
  def createDirectory(path: os.Path): IO[ScaffoldError, Unit]
  def writeFile(path: os.Path, content: String): IO[ScaffoldError, Unit]
  def exists(path: os.Path): UIO[Boolean]
  def listFiles(path: os.Path): IO[ScaffoldError, List[os.Path]]

object ScaffoldService:
  val live: ZLayer[Any, Nothing, ScaffoldService] =
    ZLayer.succeed {
      new ScaffoldService:
        def createDirectory(path: os.Path): IO[ScaffoldError, Unit] =
          ZIO.attemptBlocking {
            os.makeDir.all(path)
          }.mapError(e => ScaffoldError.FileSystemError(path.toString, e))

        def writeFile(path: os.Path, content: String): IO[ScaffoldError, Unit] =
          ZIO.attemptBlocking {
            // Create parent directories if needed
            os.makeDir.all(path / os.up)
            os.write(path, content)
          }.mapError(e => ScaffoldError.FileSystemError(path.toString, e))

        def exists(path: os.Path): UIO[Boolean] =
          ZIO.succeed(os.exists(path))

        def listFiles(path: os.Path): IO[ScaffoldError, List[os.Path]] =
          ZIO.attemptBlocking {
            os.list(path).toList
          }.mapError(e => ScaffoldError.FileSystemError(path.toString, e))
    }
```

### Pattern 4: Template Rendering with String Interpolation
**What:** Simple variable substitution in template strings
**When to use:** Scaffold file generation
**Example:**
```scala
// Source: WinterCMS BaseScaffoldCommand pattern adapted to Scala
case class TemplateVars(
  name: String,
  author: String,
  plugin: String
):
  // Derived variables
  val lowerName: String = name.toLowerCase
  val lowerAuthor: String = author.toLowerCase
  val lowerPlugin: String = plugin.toLowerCase
  val studlyName: String = name.split("[_\\-\\s]").map(_.capitalize).mkString
  val studlyAuthor: String = author.capitalize
  val studlyPlugin: String = plugin.capitalize
  val snakeName: String = name.replaceAll("([A-Z])", "_$1").toLowerCase.stripPrefix("_")
  val snakePluralName: String = pluralize(snakeName)
  val titleName: String = name.split("[_\\-\\s]").map(_.capitalize).mkString(" ")
  val pluginId: String = s"$lowerAuthor.$lowerPlugin"
  val pluginCode: String = s"$studlyAuthor.$studlyPlugin"
  val pluginNamespace: String = s"$studlyAuthor.$studlyPlugin"
  val tableName: String = s"${lowerAuthor}_${lowerPlugin}_${snakePluralName}"
  val timestamp: String = java.time.LocalDateTime.now()
    .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))

trait TemplateRenderer:
  def render(template: String, vars: TemplateVars): String

object TemplateRenderer:
  val live: ZLayer[Any, Nothing, TemplateRenderer] =
    ZLayer.succeed {
      new TemplateRenderer:
        def render(template: String, vars: TemplateVars): String =
          template
            .replace("{{name}}", vars.name)
            .replace("{{lower_name}}", vars.lowerName)
            .replace("{{studly_name}}", vars.studlyName)
            .replace("{{snake_name}}", vars.snakeName)
            .replace("{{author}}", vars.author)
            .replace("{{lower_author}}", vars.lowerAuthor)
            .replace("{{studly_author}}", vars.studlyAuthor)
            .replace("{{plugin}}", vars.plugin)
            .replace("{{lower_plugin}}", vars.lowerPlugin)
            .replace("{{studly_plugin}}", vars.studlyPlugin)
            .replace("{{plugin_id}}", vars.pluginId)
            .replace("{{plugin_namespace}}", vars.pluginNamespace)
            .replace("{{table_name}}", vars.tableName)
            .replace("{{timestamp}}", vars.timestamp)
    }
```

### Pattern 5: Plugin Scaffold Generation
**What:** Complete plugin scaffold with proper structure
**When to use:** `summer plugin create Author.Name`
**Example:**
```scala
// Source: WinterCMS CreatePlugin pattern + CONTEXT.md decisions
object PluginScaffold:
  def create(nameArg: String, dryRun: Boolean): ZIO[ScaffoldService & ConsoleOutput, ScaffoldError, Unit] =
    for
      // 1. Parse Author.Name format
      (author, name) <- ZIO.fromEither(parsePluginName(nameArg))
        .mapError(ScaffoldError.InvalidName(_))

      // 2. Build paths
      pluginsDir = os.pwd / "plugins"
      pluginDir = pluginsDir / author.toLowerCase / name.toLowerCase

      // 3. Check if exists
      scaffold <- ZIO.service[ScaffoldService]
      console  <- ZIO.service[ConsoleOutput]
      exists   <- scaffold.exists(pluginDir)
      _        <- ZIO.when(exists) {
        ZIO.fail(ScaffoldError.AlreadyExists(pluginDir.toString))
      }

      // 4. Build template vars
      vars = TemplateVars(name, author, name)

      // 5. Define files to create
      files = List(
        (pluginDir / "Plugin.scala", pluginTemplate),
        (pluginDir / "plugin.yaml", manifestTemplate),
        (pluginDir / "routes.scala", routesTemplate),
        (pluginDir / "models" / ".gitkeep", ""),
        (pluginDir / "controllers" / ".gitkeep", ""),
        (pluginDir / "components" / ".gitkeep", ""),
        (pluginDir / "resources" / "db" / "migration" / ".gitkeep", ""),
        (pluginDir / "resources" / "lang" / "en" / "lang.yaml", langTemplate),
        (pluginDir / "resources" / "views" / ".gitkeep", "")
      )

      // 6. Render and create (or show dry-run)
      renderer = TemplateRenderer.live
      _ <- ZIO.foreach(files) { case (path, template) =>
        val content = renderer.render(template, vars)
        if dryRun then
          console.info(s"Would create: $path")
        else
          scaffold.writeFile(path, content) *>
          console.success(s"Created: $path")
      }

      // 7. Show next steps
      _ <- console.nextSteps(List(
        s"Edit Plugin.scala to configure your plugin",
        s"Add models with: summer model create ${vars.pluginCode} ModelName",
        s"Add components with: summer component create ${vars.pluginCode} ComponentName",
        s"Run migrations with: summer migration run"
      ))
    yield ()

  private def parsePluginName(name: String): Either[String, (String, String)] =
    name.split("\\.").toList match
      case List(author, plugin) if author.nonEmpty && plugin.nonEmpty =>
        Right((author, plugin))
      case _ =>
        Left(s"Invalid plugin name '$name'. Use Author.Name format (e.g., Golem15.Blog)")

  private val pluginTemplate = """
    |package {{plugin_namespace}}
    |
    |import com.summercms.plugin.*
    |import zio.*
    |
    |// TODO: Configure your plugin here
    |class Plugin extends SummerPlugin:
    |  def id: PluginId = PluginId("{{lower_author}}", "{{lower_plugin}}")
    |
    |  def register(ctx: PluginContext): PluginRegistration =
    |    PluginRegistration(
    |      components = List.empty,
    |      permissions = List.empty,
    |      navigation = List.empty,
    |      settings = List.empty,
    |      events = List.empty,
    |      extensions = List.empty
    |    )
    |
    |  def boot: ZIO[PluginEnv, PluginError, Unit] =
    |    ZIO.unit
    |
    |  def shutdown: ZIO[Any, Nothing, Unit] =
    |    ZIO.unit
    |""".stripMargin

  private val manifestTemplate = """
    |vendor: {{lower_author}}
    |name: {{lower_plugin}}
    |version: 1.0.0
    |description: "TODO: Add description"
    |author: {{studly_author}}
    |
    |dependencies: {}
    |
    |enabled: true
    |""".stripMargin
```

### Pattern 6: Colored Console Output with fansi
**What:** Terminal output service with colors and styling
**When to use:** All CLI output
**Example:**
```scala
// Source: fansi documentation
import fansi.*

trait ConsoleOutput:
  def info(message: String): UIO[Unit]
  def success(message: String): UIO[Unit]
  def warning(message: String): UIO[Unit]
  def error(message: String): UIO[Unit]
  def nextSteps(steps: List[String]): UIO[Unit]

object ConsoleOutput:
  def live(useColors: Boolean = true): ZLayer[Any, Nothing, ConsoleOutput] =
    ZLayer.succeed {
      new ConsoleOutput:
        private def colorize(str: Str): String =
          if useColors then str.render else str.plainText

        def info(message: String): UIO[Unit] =
          Console.printLine(colorize(Color.Cyan(message))).orDie

        def success(message: String): UIO[Unit] =
          Console.printLine(colorize(Color.Green("[OK] ") ++ Str(message))).orDie

        def warning(message: String): UIO[Unit] =
          Console.printLine(colorize(Color.Yellow("[WARN] ") ++ Str(message))).orDie

        def error(message: String): UIO[Unit] =
          Console.printLine(colorize(Color.Red("[ERROR] ") ++ Str(message))).orDie

        def nextSteps(steps: List[String]): UIO[Unit] =
          val header = colorize(Bold.On("Next steps:"))
          val bulletPoints = steps.map(s => s"  - $s").mkString("\n")
          Console.printLine(s"\n$header\n$bulletPoints\n").orDie
    }
```

### Pattern 7: Migration Timestamp Naming
**What:** Migrations named with timestamp prefix for ordering
**When to use:** `summer migration create` command
**Example:**
```scala
// Source: CONTEXT.md decisions
object MigrationScaffold:
  def generateMigrationName(description: String): String =
    val timestamp = java.time.LocalDateTime.now()
      .format(java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd_HHmmss"))
    val snakeName = description
      .replaceAll("([A-Z])", "_$1")
      .toLowerCase
      .stripPrefix("_")
      .replaceAll("\\s+", "_")
    s"${timestamp}_$snakeName"

  // e.g., "Create Posts Table" -> "20260205_143022_create_posts_table.scala"

  def create(plugin: String, nameOpt: Option[String], dryRun: Boolean): ZIO[ScaffoldEnv, ScaffoldError, Unit] =
    for
      (author, pluginName) <- ZIO.fromEither(parsePluginName(plugin))
      name = nameOpt.getOrElse("migration")
      migrationName = generateMigrationName(name)
      migrationDir = os.pwd / "plugins" / author.toLowerCase / pluginName.toLowerCase / "resources" / "db" / "migration"

      vars = TemplateVars(name, author, pluginName)
      content = migrationTemplate.render(vars)

      scaffold <- ZIO.service[ScaffoldService]
      console  <- ZIO.service[ConsoleOutput]

      _ <- if dryRun then
        console.info(s"Would create: $migrationDir/${migrationName}.scala")
      else
        scaffold.writeFile(migrationDir / s"${migrationName}.scala", content) *>
        console.success(s"Created: $migrationDir/${migrationName}.scala")
    yield ()
```

### Pattern 8: Interactive Wizard Mode
**What:** ZIO CLI's built-in wizard prompts for missing arguments
**When to use:** Interactive mode when args missing or `--wizard` flag
**Example:**
```scala
// Source: ZIO CLI built-in commands documentation
// Wizard mode is automatic in ZIO CLI with --wizard flag

// If user runs: summer plugin create --wizard
// ZIO CLI will prompt:
//   > Select command: [plugin, theme, component, ...]
//   > Enter plugin name:
//   > Enable dry-run? [y/N]

// For custom prompts beyond wizard (e.g., validation loops):
object InteractivePrompt:
  def promptWithValidation(
    prompt: String,
    validate: String => Either[String, String]
  ): ZIO[Any, Nothing, String] =
    def loop: ZIO[Any, Nothing, String] =
      Console.print(prompt).orDie *>
      Console.readLine.orDie.flatMap { input =>
        validate(input) match
          case Right(valid) => ZIO.succeed(valid)
          case Left(error) =>
            Console.printLine(fansi.Color.Red(error).render).orDie *>
            loop
      }
    loop

  // Usage for plugin name validation:
  def promptPluginName: ZIO[Any, Nothing, String] =
    promptWithValidation(
      "Plugin name (Author.Name): ",
      input => parsePluginName(input).map(_._1 + "." + _._2)
    )
```

### Pattern 9: Config File Detection
**What:** Find SummerCMS project root by looking for marker files
**When to use:** CLI startup to determine paths
**Example:**
```scala
// Source: Common CLI pattern (CONTEXT.md Claude's discretion)
trait ConfigDetector:
  def findProjectRoot: IO[ScaffoldError, os.Path]
  def loadConfig: IO[ScaffoldError, ProjectConfig]

object ConfigDetector:
  // Marker files that indicate SummerCMS root
  private val markers = List(
    "build.mill",
    "build.mill.yaml",
    ".summercms"
  )

  val live: ZLayer[Any, Nothing, ConfigDetector] =
    ZLayer.succeed {
      new ConfigDetector:
        def findProjectRoot: IO[ScaffoldError, os.Path] =
          ZIO.attemptBlocking {
            var current = os.pwd
            while (current != os.root) {
              if (markers.exists(m => os.exists(current / m)))
                return ZIO.succeed(current)
              current = current / os.up
            }
            throw new Exception("Not in a SummerCMS project")
          }.flatMap(identity)
           .mapError(e => ScaffoldError.NotInProject(e.getMessage))

        def loadConfig: IO[ScaffoldError, ProjectConfig] =
          for
            root <- findProjectRoot
            configPath = root / ".summercms" / "config.yaml"
            exists <- ZIO.succeed(os.exists(configPath))
            config <- if exists then
              ZIO.attemptBlocking(os.read(configPath))
                .flatMap(parseConfig)
                .mapError(e => ScaffoldError.ConfigError(e.getMessage))
            else
              ZIO.succeed(ProjectConfig.default)
          yield config
    }
```

### Pattern 10: Progress Bar for Larger Operations
**What:** Simple progress indication for multi-file scaffolds
**When to use:** Theme scaffold with Tailwind, any operation with multiple steps
**Example:**
```scala
// Source: Common CLI pattern (CONTEXT.md Claude's discretion)
trait Progress:
  def start(total: Int, description: String): UIO[ProgressHandle]
  def update(handle: ProgressHandle, current: Int): UIO[Unit]
  def complete(handle: ProgressHandle): UIO[Unit]

case class ProgressHandle(id: Long, total: Int, description: String)

object Progress:
  val live: ZLayer[Any, Nothing, Progress] =
    ZLayer.succeed {
      new Progress:
        private val counter = new java.util.concurrent.atomic.AtomicLong(0)

        def start(total: Int, description: String): UIO[ProgressHandle] =
          ZIO.succeed {
            val handle = ProgressHandle(counter.incrementAndGet(), total, description)
            print(s"$description [${progressBar(0, total)}] 0/$total\r")
            handle
          }

        def update(handle: ProgressHandle, current: Int): UIO[Unit] =
          ZIO.succeed {
            print(s"${handle.description} [${progressBar(current, handle.total)}] $current/${handle.total}\r")
          }

        def complete(handle: ProgressHandle): UIO[Unit] =
          ZIO.succeed {
            println(s"${handle.description} [${progressBar(handle.total, handle.total)}] ${handle.total}/${handle.total}")
          }

        private def progressBar(current: Int, total: Int): String =
          val width = 30
          val filled = (current.toDouble / total * width).toInt
          val empty = width - filled
          "=" * filled + " " * empty
    }
```

### Anti-Patterns to Avoid
- **Direct System.out.println:** Use ConsoleOutput service for testability and color support
- **Blocking file I/O without ZIO:** Wrap all os-lib calls in `ZIO.attemptBlocking`
- **Hard-coded paths:** Use ConfigDetector to find project root
- **Mutable state in scaffolds:** Use ZIO Ref if state needed between steps
- **Ignoring `--dry-run`:** Every file operation must check dry-run flag
- **Silent failures:** Always report errors with clear messages and suggestions
- **Requiring interactive input in CI:** Check `--no-interaction` flag

## Don't Hand-Roll

Problems that look simple but have existing solutions:

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Command parsing | Regex on args | ZIO CLI | Type safety, help gen, completion, wizard |
| File system ops | java.io.File | os-lib | Cross-platform, ergonomic, path safety |
| Terminal colors | ANSI escape codes | fansi | Handles terminal detection, efficient |
| Progress bars | Custom spinners | Simple print loop | Sufficient for scaffolding use case |
| YAML generation | String concat | circe-yaml | Proper escaping, structure |
| Template rendering | Custom parser | String interpolation | Scaffolds are simple, no need for Pebble |
| Input validation | Manual loops | ZIO CLI validators | Composable, type-safe |

**Key insight:** CLI scaffolding is IO-heavy but logic-light. Use proven libraries for parsing and file operations; keep template generation simple with string interpolation.

## Common Pitfalls

### Pitfall 1: Path Separator Issues on Windows
**What goes wrong:** Paths broken on Windows due to hardcoded `/`
**Why it happens:** Using string concatenation instead of path combinators
**How to avoid:** Always use `os.Path` with `/` operator: `root / "plugins" / author`
**Warning signs:** "File not found" errors only on Windows

### Pitfall 2: Existing File Overwrite
**What goes wrong:** User's existing code overwritten silently
**Why it happens:** No existence check before write
**How to avoid:** Check `os.exists()` before write, fail with clear message (no `--force` per CONTEXT.md)
**Warning signs:** Lost user code, angry developers

### Pitfall 3: Invalid Plugin Name Format
**What goes wrong:** Directory structure wrong, plugin won't load
**Why it happens:** Name not validated (missing dot, special characters)
**How to avoid:** Validate format upfront: `^[A-Z][a-zA-Z0-9]*\\.[A-Z][a-zA-Z0-9]*$`
**Warning signs:** Plugin discovery failures, namespace errors

### Pitfall 4: Missing Parent Directories
**What goes wrong:** File write fails with "directory not found"
**Why it happens:** Trying to write file before creating parent dirs
**How to avoid:** Use `os.makeDir.all(path / os.up)` before `os.write`
**Warning signs:** "No such file or directory" errors

### Pitfall 5: CI Environment Interactive Prompts
**What goes wrong:** Build hangs waiting for input
**Why it happens:** Interactive prompt in non-interactive environment
**How to avoid:** Check `--no-interaction` flag, fail with clear error on missing required args
**Warning signs:** CI timeouts, hanging builds

### Pitfall 6: Wizard Mode Not Triggering
**What goes wrong:** User expects prompts but gets error
**Why it happens:** ZIO CLI wizard requires explicit `--wizard` flag
**How to avoid:** Document wizard mode clearly, consider making it default for missing args
**Warning signs:** Confused users, support requests

### Pitfall 7: Color Output in Non-TTY
**What goes wrong:** ANSI codes visible as garbage characters
**Why it happens:** Colors output to redirected stdout or file
**How to avoid:** fansi handles this, also support `--no-color` flag
**Warning signs:** Garbled output in logs, CI output

### Pitfall 8: Template Variable Collision
**What goes wrong:** Template contains literal `{{name}}` that shouldn't be replaced
**Why it happens:** Content and variables use same syntax
**How to avoid:** Use distinctive delimiters or escape sequence for literal braces
**Warning signs:** Unexpected content replacement

## Code Examples

Verified patterns from official sources:

### Complete CLI Main Entry
```scala
// Source: ZIO CLI documentation
package com.summercms.cli

import zio.*
import zio.cli.*

object Main extends ZIOCliDefault:
  // Global options
  val noColorOption: Options[Boolean] =
    Options.boolean("no-color")
      .withHelp("Disable colored output")

  val verboseOption: Options[Boolean] =
    Options.boolean("verbose").alias("v")
      .withHelp("Enable verbose output")

  val quietOption: Options[Boolean] =
    Options.boolean("quiet").alias("q")
      .withHelp("Suppress non-essential output")

  val noInteractionOption: Options[Boolean] =
    Options.boolean("no-interaction")
      .withHelp("Disable interactive prompts (for CI)")

  // Build complete command tree
  val command: Command[CliCommand] =
    Command("summer", noColorOption ++ verboseOption ++ quietOption ++ noInteractionOption)
      .subcommands(
        PluginCommands.command,
        ThemeCommands.command,
        ComponentCommands.command,
        ControllerCommands.command,
        ModelCommands.command,
        MigrationCommands.command,
        VersionCommand.command
      )

  val cliApp: CliApp[CliCommand] = CliApp.make(
    name = "summer",
    version = BuildInfo.version,
    summary = HelpDoc.p("SummerCMS scaffolding and development CLI"),
    command = command
  ) { case ((noColor, verbose, quiet, noInteraction), cmd) =>
    val consoleLayer = ConsoleOutput.live(!noColor)
    val configLayer = ConfigDetector.live
    val scaffoldLayer = ScaffoldService.live

    cmd match
      case CliCommand.PluginCreate(name, dryRun) =>
        PluginScaffold.create(name, dryRun)
          .provide(consoleLayer, configLayer, scaffoldLayer)

      case CliCommand.Version() =>
        Console.printLine(
          s"SummerCMS ${BuildInfo.version} (Scala ${BuildInfo.scalaVersion}, JVM ${System.getProperty("java.version")})"
        )

      // ... other commands
  }
```

### Theme Scaffold with Template Choice
```scala
// Source: WinterCMS CreateTheme pattern + CONTEXT.md decisions
object ThemeScaffold:
  sealed trait ThemeTemplate
  case object Blank extends ThemeTemplate
  case object Starter extends ThemeTemplate

  def create(
    name: String,
    templateArg: String,
    dryRun: Boolean
  ): ZIO[ScaffoldEnv, ScaffoldError, Unit] =
    for
      // 1. Parse template choice
      template <- ZIO.fromEither(
        templateArg.toLowerCase match
          case "blank" => Right(Blank)
          case "starter" => Right(Starter)
          case other => Left(ScaffoldError.InvalidTemplate(other))
      )

      // 2. Parse theme name (Author.ThemeName format)
      (author, themeName) <- ZIO.fromEither(parseThemeName(name))

      // 3. Build paths
      config   <- ZIO.service[ConfigDetector].flatMap(_.loadConfig)
      themeDir = config.themesDir / author.toLowerCase / themeName.toLowerCase

      // 4. Check existence
      scaffold <- ZIO.service[ScaffoldService]
      exists   <- scaffold.exists(themeDir)
      _        <- ZIO.when(exists) {
        ZIO.fail(ScaffoldError.AlreadyExists(themeDir.toString))
      }

      // 5. Generate files based on template
      vars = TemplateVars(themeName, author, themeName)
      files = template match
        case Blank => blankThemeFiles(themeDir, vars)
        case Starter => starterThemeFiles(themeDir, vars)

      // 6. Create with progress
      console  <- ZIO.service[ConsoleOutput]
      progress <- ZIO.service[Progress]

      _ <- if dryRun then
        ZIO.foreach(files) { case (path, _) =>
          console.info(s"Would create: $path")
        }
      else
        for
          handle <- progress.start(files.length, "Creating theme")
          _ <- ZIO.foreachDiscard(files.zipWithIndex) { case ((path, content), idx) =>
            scaffold.writeFile(path, content) *>
            progress.update(handle, idx + 1)
          }
          _ <- progress.complete(handle)
        yield ()

      // 7. Next steps
      _ <- console.nextSteps(List(
        s"Edit theme.yaml to configure your theme",
        template match
          case Starter =>
            s"Run 'cd themes/${author.toLowerCase}/${themeName.toLowerCase} && npm install' to install dependencies"
          case Blank =>
            "Add layouts, pages, and partials to your theme"
      ))
    yield ()

  private def blankThemeFiles(dir: os.Path, vars: TemplateVars): List[(os.Path, String)] =
    List(
      (dir / "theme.yaml", themeManifest(vars)),
      (dir / "layouts" / "default.htm", blankLayout),
      (dir / "pages" / "home.htm", blankHomePage),
      (dir / "pages" / "404.htm", blank404Page),
      (dir / "partials" / ".gitkeep", ""),
      (dir / "assets" / "css" / ".gitkeep", ""),
      (dir / "assets" / "js" / ".gitkeep", "")
    )

  private def starterThemeFiles(dir: os.Path, vars: TemplateVars): List[(os.Path, String)] =
    blankThemeFiles(dir, vars) ++ List(
      (dir / "package.json", packageJson(vars)),
      (dir / "vite.config.js", viteConfig),
      (dir / ".gitignore", themeGitignore),
      // Tailwind starter files
      (dir / "assets" / "css" / "app.css", tailwindCss),
      (dir / "assets" / "js" / "app.js", starterJs)
    )
```

### Component Scaffold (Plugin Required)
```scala
// Source: WinterCMS CreateComponent pattern + CONTEXT.md decisions
object ComponentScaffold:
  def create(
    pluginArg: String,
    componentName: String,
    dryRun: Boolean
  ): ZIO[ScaffoldEnv, ScaffoldError, Unit] =
    for
      // Component requires explicit plugin
      (author, plugin) <- ZIO.fromEither(parsePluginName(pluginArg))

      // Verify plugin exists
      config    <- ZIO.service[ConfigDetector].flatMap(_.loadConfig)
      pluginDir = config.pluginsDir / author.toLowerCase / plugin.toLowerCase
      scaffold  <- ZIO.service[ScaffoldService]
      exists    <- scaffold.exists(pluginDir / "Plugin.scala")
      _         <- ZIO.unless(exists) {
        ZIO.fail(ScaffoldError.PluginNotFound(pluginArg))
      }

      // Build paths and vars
      componentDir = pluginDir / "components"
      vars = TemplateVars(componentName, author, plugin)

      files = List(
        (componentDir / s"${vars.studlyName}.scala", componentClass(vars)),
        (componentDir / vars.lowerName / "default.htm", componentPartial(vars)),
        (componentDir / vars.lowerName / "component.yaml", componentConfig(vars))
      )

      console <- ZIO.service[ConsoleOutput]
      _ <- ZIO.foreach(files) { case (path, content) =>
        val rendered = TemplateRenderer.live.render(content, vars)
        if dryRun then
          console.info(s"Would create: $path")
        else
          scaffold.writeFile(path, rendered) *>
          console.success(s"Created: $path")
      }

      _ <- console.nextSteps(List(
        s"Register component in Plugin.scala register() method",
        s"Edit ${vars.studlyName}.scala to add component logic",
        s"Edit default.htm partial for component output"
      ))
    yield ()

  private def componentClass(vars: TemplateVars): String = s"""
    |package ${vars.pluginNamespace}.components
    |
    |import com.summercms.component.*
    |import zio.*
    |
    |// TODO: Implement your component
    |class ${vars.studlyName} extends SummerComponent:
    |  def componentDetails: ComponentDetails =
    |    ComponentDetails(
    |      name = "${vars.titleName}",
    |      description = "TODO: Add description"
    |    )
    |
    |  def defineProperties: List[PropertyDef] =
    |    List.empty
    |
    |  def onRun: ZIO[ComponentEnv, ComponentError, Map[String, Any]] =
    |    ZIO.succeed(Map.empty)
    |""".stripMargin
```

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Scopt for CLI parsing | ZIO CLI | 2023+ | ZIO-native, wizard mode, shell completion |
| java.io.File | os-lib | 2020+ | Cross-platform, better API, safer paths |
| Manual ANSI codes | fansi | 2018+ | Terminal detection, efficient, composable |
| Scalate for templates | String interpolation | Always for simple | No dependency, fast, debuggable |
| Interactive readline | JLine/ZIO CLI wizard | JLine 4.x | Menu selection, history, completion |

**Deprecated/outdated:**
- scopt: Less maintained, no wizard mode
- scallop: Macro-based, compile-time issues with Scala 3
- java.io.File: Not cross-platform, verbose API
- Manual ANSI escape codes: Brittle, terminal-specific

## Open Questions

Things that couldn't be fully resolved:

1. **Shell Completion Installation**
   - What we know: ZIO CLI generates completion scripts via `--shell-completion-script`
   - What's unclear: Best UX for installing completions (manual copy vs auto-install command)
   - Recommendation: Add `summer completion install` command that writes to appropriate shell config

2. **Native Binary Distribution**
   - What we know: Mill can create fat JARs; GraalVM native-image possible but complex
   - What's unclear: Whether startup time warrants native compilation
   - Recommendation: Start with fat JAR via Mill assembly; evaluate native-image later based on user feedback

3. **Plugin Templates Beyond Basic**
   - What we know: WinterCMS has options like `--controller`, `--all` on model create
   - What's unclear: Full set of template combinations needed
   - Recommendation: Start with basic scaffolds, add templates based on common patterns observed

4. **BuildInfo Generation**
   - What we know: Need version/Scala info in CLI output
   - What's unclear: Best Mill plugin for BuildInfo generation
   - Recommendation: Use mill-buildinfo or hand-roll with Mill's T.ctx() for simple cases

5. **Wizard Mode Default Behavior**
   - What we know: ZIO CLI wizard is opt-in with `--wizard` flag
   - What's unclear: Should wizard be default when required args missing vs error?
   - Recommendation: Per CONTEXT.md, prompt interactively when required args missing (make wizard default)

## Sources

### Primary (HIGH confidence)
- [ZIO CLI Documentation](https://zio.dev/zio-cli/) - Commands, options, wizard mode, shell completion
- [ZIO CLI GitHub](https://github.com/zio/zio-cli) - Version 0.7.4, API reference
- [os-lib GitHub](https://github.com/com-lihaoyi/os-lib) - Version 0.11.7, file operations
- [fansi GitHub](https://github.com/com-lihaoyi/fansi) - Version 0.5.1, terminal colors
- [Mill Bundled Libraries](https://mill-build.org/mill/fundamentals/bundled-libraries.html) - os-lib, MainArgs integration
- [WinterCMS Scaffolding Commands](https://wintercms.com/docs/v1.2/docs/console/scaffolding) - Reference patterns

### Secondary (MEDIUM confidence)
- [MainArgs GitHub](https://github.com/com-lihaoyi/mainargs) - Version 0.7.7, alternative CLI parser
- [JLine 3 GitHub](https://github.com/jline/jline3) - Version 4.0.0, advanced terminal input
- [ZIO CLI Built-in Commands](https://zio.dev/zio-cli/built-in-commands/) - Wizard and help features

### Tertiary (LOW confidence)
- Community blog posts on ZIO CLI usage
- Stack Overflow answers on CLI patterns

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH - ZIO CLI, os-lib, fansi are well-documented with official sources
- Command structure: HIGH - Based on ZIO CLI docs and CONTEXT.md locked decisions
- File operations: HIGH - os-lib API is well-documented, Mill integration verified
- Template rendering: HIGH - Simple string interpolation, WinterCMS patterns verified
- Interactive prompts: MEDIUM - ZIO CLI wizard documented, JLine for advanced cases
- Progress bar: MEDIUM - Custom implementation, simple approach sufficient

**Research date:** 2026-02-05
**Valid until:** 2026-03-05 (30 days - CLI libraries are stable)
