--- phase: 05-cli-scaffolding plan: 01 type: execute wave: 1 depends_on: [] files_modified: - 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 autonomous: true must_haves: truths: - "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" artifacts: - path: "cli/src/Main.scala" provides: "CLI entry point with ZIO CLI" contains: "ZIOCliDefault" - path: "cli/src/commands/PluginCommands.scala" provides: "Plugin subcommands" exports: ["command"] - path: "cli/src/scaffold/ScaffoldService.scala" provides: "File generation service" exports: ["ScaffoldService"] - path: "cli/src/output/ConsoleOutput.scala" provides: "Colored console output" exports: ["ConsoleOutput"] key_links: - from: "cli/src/Main.scala" to: "cli/src/commands/PluginCommands.scala" via: "subcommands composition" pattern: "PluginCommands\\.command" - from: "cli/src/commands/PluginCommands.scala" to: "cli/src/scaffold/ScaffoldService.scala" via: "ZIO service dependency" pattern: "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. @/home/jin/.claude/get-shit-done/workflows/execute-plan.md @/home/jin/.claude/get-shit-done/templates/summary.md @.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: ```bash ./mill cli.compile ``` 2. Version command works: ```bash ./mill cli.run version ``` 3. Help shows all commands: ```bash ./mill cli.run --help ./mill cli.run plugin --help ./mill cli.run plugin create --help ``` 4. Plugin scaffolding works end-to-end: ```bash 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: ```bash ./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: ```bash ./mill cli.run plugin create badname # Error: Invalid plugin name 'badname'. Use Author.Name format ``` - [ ] 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 After completion, create `.planning/phases/05-cli-scaffolding/05-01-SUMMARY.md`