Files
Jakub Zych 5cdf78b93d docs(05): create phase plan
Phase 05: CLI Scaffolding
- 2 plans in 2 waves
- Wave 1: CLI framework + plugin scaffolding (05-01)
- Wave 2: Theme + component scaffolding (05-02)
- Ready for execution
2026-02-05 14:20:40 +01:00

12 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, must_haves
phase plan type wave depends_on files_modified autonomous must_haves
05-cli-scaffolding 01 execute 1
build.mill
cli/src/Main.scala
cli/src/commands/PluginCommands.scala
cli/src/commands/VersionCommand.scala
cli/src/scaffold/ScaffoldService.scala
cli/src/scaffold/TemplateRenderer.scala
cli/src/scaffold/templates/PluginTemplates.scala
cli/src/output/ConsoleOutput.scala
cli/src/config/ConfigDetector.scala
cli/src/errors/ScaffoldError.scala
true
truths artifacts key_links
Developer can run `./mill cli.run version` and see version info with Scala/JVM versions
Developer can run `./mill cli.run plugin create Author.Name` and get a complete plugin scaffold
Developer can run `./mill cli.run plugin create Author.Name --dry-run` to preview without creating
Generated plugin scaffold has correct directory structure (Plugin.scala, plugin.yaml, empty dirs)
Invalid plugin name format produces clear error message with suggestion
path provides contains
cli/src/Main.scala CLI entry point with ZIO CLI ZIOCliDefault
path provides exports
cli/src/commands/PluginCommands.scala Plugin subcommands
command
path provides exports
cli/src/scaffold/ScaffoldService.scala File generation service
ScaffoldService
path provides exports
cli/src/output/ConsoleOutput.scala Colored console output
ConsoleOutput
from to via pattern
cli/src/Main.scala cli/src/commands/PluginCommands.scala subcommands composition PluginCommands.command
from to via pattern
cli/src/commands/PluginCommands.scala cli/src/scaffold/ScaffoldService.scala ZIO service dependency ZIO.service[ScaffoldService]
Create the `summer` CLI tool with ZIO CLI framework and implement plugin scaffolding command.

Purpose: Establish CLI foundation for all developer tooling and deliver the first scaffolding capability (plugins). Output: Working summer CLI with version and plugin create commands.

<execution_context> @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md </execution_context>

@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/05-cli-scaffolding/05-CONTEXT.md @.planning/phases/05-cli-scaffolding/05-RESEARCH.md @build.mill Task 1: CLI Module Setup with ZIO CLI build.mill cli/src/Main.scala cli/src/commands/VersionCommand.scala cli/src/output/ConsoleOutput.scala cli/src/config/ConfigDetector.scala cli/src/errors/ScaffoldError.scala Create a new `cli` Mill module alongside the existing `summercms` module.
1. **Update build.mill:**
   - Add `cli` module extending ScalaModule
   - Same scalaVersion as summercms (3.3.4)
   - Dependencies:
     - `mvn"dev.zio::zio-cli:0.7.4"` (command parsing)
     - `mvn"com.lihaoyi::os-lib:0.11.7"` (file operations)
     - `mvn"com.lihaoyi::fansi:0.5.1"` (colored output)
   - No dependency on summercms module (CLI is standalone)

2. **Create cli/src/errors/ScaffoldError.scala:**
   - Sealed trait `ScaffoldError` extending Exception
   - Case classes: `InvalidName`, `AlreadyExists`, `FileSystemError`, `NotInProject`, `ConfigError`
   - Each has clear message field

3. **Create cli/src/output/ConsoleOutput.scala:**
   - ZIO service trait with methods: `info`, `success`, `warning`, `error`, `nextSteps`
   - Live layer using fansi for colors
   - Accept `useColors: Boolean` parameter (for --no-color support)
   - Use fansi.Color.Cyan for info, Green for success, Yellow for warning, Red for error
   - `nextSteps` renders bold header with bulleted list

4. **Create cli/src/config/ConfigDetector.scala:**
   - ZIO service trait with `findProjectRoot` method
   - Look for marker files: `build.mill`, `.summercms`
   - Walk up directory tree from cwd until found or reach root
   - Return `os.Path` of project root or fail with `NotInProject` error

5. **Create cli/src/commands/VersionCommand.scala:**
   - Define `VersionCommand` case object
   - Create `command: Command[VersionCommand]` for "version" subcommand
   - Handler prints: "SummerCMS 0.1.0 (Scala 3.3.4, JVM {java.version})"
   - Note: Version hardcoded for now; BuildInfo generation deferred

6. **Create cli/src/Main.scala:**
   - Extend ZIOCliDefault
   - Define global options:
     - `--no-color` (Boolean, disables colors)
     - `--verbose` / `-v` (Boolean)
     - `--quiet` / `-q` (Boolean)
     - `--no-interaction` (Boolean, for CI)
   - Create top-level `summer` command with VersionCommand as subcommand
   - Use CliApp.make with name="summer", version="0.1.0"
   - Wire ConsoleOutput layer based on --no-color flag

**Do NOT use:** Complex dependency injection beyond ZIO layers. Keep it simple.
```bash ./mill cli.compile ./mill cli.run version # Should output: SummerCMS 0.1.0 (Scala 3.3.4, JVM 21) ./mill cli.run --help # Should show summer command with version subcommand ``` CLI module compiles. `summer version` outputs version info. `summer --help` shows command structure. Task 2: Plugin Scaffolding Command cli/src/Main.scala cli/src/commands/PluginCommands.scala cli/src/scaffold/ScaffoldService.scala cli/src/scaffold/TemplateRenderer.scala cli/src/scaffold/templates/PluginTemplates.scala Implement `summer plugin create Author.Name` command.
1. **Create cli/src/scaffold/ScaffoldService.scala:**
   - ZIO service trait with methods:
     - `createDirectory(path: os.Path): IO[ScaffoldError, Unit]`
     - `writeFile(path: os.Path, content: String): IO[ScaffoldError, Unit]`
     - `exists(path: os.Path): UIO[Boolean]`
   - Live layer using os-lib:
     - `createDirectory`: `os.makeDir.all(path)`
     - `writeFile`: `os.makeDir.all(path / os.up)` then `os.write(path, content)`
     - `exists`: `os.exists(path)`
   - Wrap all os-lib calls in `ZIO.attemptBlocking` with error mapping

2. **Create cli/src/scaffold/TemplateRenderer.scala:**
   - Case class `TemplateVars` with fields:
     - `name`, `author`, `plugin` (raw inputs)
     - Derived: `lowerName`, `lowerAuthor`, `lowerPlugin`
     - Derived: `studlyName`, `studlyAuthor`, `studlyPlugin`
     - Derived: `snakeName` (e.g., "BlogPost" -> "blog_post")
     - Derived: `pluginId` (e.g., "acme.blog")
     - Derived: `pluginNamespace` (e.g., "Acme.Blog")
     - Derived: `timestamp` (yyyyMMdd_HHmmss format)
   - Method `render(template: String, vars: TemplateVars): String`
   - Replace placeholders: `{{name}}`, `{{lower_name}}`, `{{studly_name}}`, etc.
   - Use distinctive delimiters to avoid collision with Scala code

3. **Create cli/src/scaffold/templates/PluginTemplates.scala:**
   - Object with template string vals:
     - `pluginScala`: Plugin.scala with SummerPlugin trait stub
     - `manifestYaml`: plugin.yaml with vendor, name, version, dependencies
     - `langYaml`: Basic lang.yaml for en locale
   - Include TODO comments showing where to add code
   - Templates reference plugin types that will exist in Phase 2

4. **Create cli/src/commands/PluginCommands.scala:**
   - Sealed trait `PluginCommand` with case class `Create(name: String, dryRun: Boolean)`
   - Define options:
     - `--dry-run` / `-n`: Show what would be created
   - Define args:
     - `name`: Plugin name in Author.Name format
   - Create `createCommand: Command[PluginCommand.Create]`
   - Create `command: Command[PluginCommand]` with "plugin" as parent, "create" as subcommand
   - Implement handler:
     1. Validate name format (Author.Name with regex `^[A-Z][a-zA-Z0-9]*\\.[A-Z][a-zA-Z0-9]*$`)
     2. Parse into (author, plugin) tuple
     3. Build plugin directory path: `plugins/{lower_author}/{lower_plugin}/`
     4. Check if directory exists -> fail with `AlreadyExists`
     5. Build file list with template content:
        - `Plugin.scala`
        - `plugin.yaml`
        - `routes.scala` (empty routes placeholder)
        - `models/.gitkeep`
        - `controllers/.gitkeep`
        - `components/.gitkeep`
        - `resources/db/migration/.gitkeep`
        - `resources/lang/en/lang.yaml`
        - `resources/views/.gitkeep`
     6. If --dry-run: print what would be created
     7. Else: create files via ScaffoldService
     8. Print next steps (edit Plugin.scala, add models, add components)

5. **Update cli/src/Main.scala:**
   - Add PluginCommands.command to subcommands
   - Wire ScaffoldService.live and ConfigDetector.live layers

**Pattern for name validation:**
```scala
private val pluginNamePattern = "^([A-Z][a-zA-Z0-9]*)\\.([A-Z][a-zA-Z0-9]*)$".r

def parsePluginName(input: String): Either[String, (String, String)] =
  input match
    case pluginNamePattern(author, name) => Right((author, name))
    case _ => Left(s"Invalid plugin name '$input'. Use Author.Name format (e.g., Acme.Blog)")
```
```bash # Test plugin creation ./mill cli.run plugin create Acme.Blog --dry-run # Should list files that would be created
./mill cli.run plugin create Acme.Blog
# Should create plugins/acme/blog/ with all files

ls -la plugins/acme/blog/
# Should show Plugin.scala, plugin.yaml, directories

cat plugins/acme/blog/Plugin.scala
# Should show valid Scala code with TODO comments

cat plugins/acme/blog/plugin.yaml
# Should show valid YAML manifest

# Test error case
./mill cli.run plugin create invalidname
# Should error with suggestion

# Test already exists
./mill cli.run plugin create Acme.Blog
# Should error with "already exists"
```
`summer plugin create Author.Name` creates complete plugin scaffold. `--dry-run` previews changes. Invalid names produce helpful errors. Existing plugins are not overwritten. Full plan verification:
  1. CLI module builds independently:

    ./mill cli.compile
    
  2. Version command works:

    ./mill cli.run version
    
  3. Help shows all commands:

    ./mill cli.run --help
    ./mill cli.run plugin --help
    ./mill cli.run plugin create --help
    
  4. Plugin scaffolding works end-to-end:

    rm -rf plugins/test/example  # Cleanup if exists
    ./mill cli.run plugin create Test.Example
    ls plugins/test/example/
    # Plugin.scala, plugin.yaml, routes.scala, models/, controllers/, components/, resources/
    
  5. Dry-run mode works:

    ./mill cli.run plugin create Another.Plugin --dry-run
    # Shows what would be created, but doesn't create
    ls plugins/another/plugin 2>/dev/null || echo "Directory not created (correct)"
    
  6. Error handling:

    ./mill cli.run plugin create badname
    # Error: Invalid plugin name 'badname'. Use Author.Name format
    

<success_criteria>

  • CLI module exists as separate Mill module with ZIO CLI, os-lib, fansi deps
  • summer version shows version with Scala/JVM info
  • summer plugin create Author.Name creates valid plugin scaffold
  • --dry-run shows preview without file creation
  • Invalid plugin names produce clear error with format suggestion
  • Existing plugin directories prevent overwrite with error
  • Generated Plugin.scala compiles (references future types with stubs)
  • Generated plugin.yaml is valid YAML
  • Colored output works with --no-color fallback
  • Next steps printed after successful scaffold </success_criteria>
After completion, create `.planning/phases/05-cli-scaffolding/05-01-SUMMARY.md`