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
This commit is contained in:
323
.planning/phases/05-cli-scaffolding/05-01-PLAN.md
Normal file
323
.planning/phases/05-cli-scaffolding/05-01-PLAN.md
Normal file
@@ -0,0 +1,323 @@
|
||||
---
|
||||
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\\]"
|
||||
---
|
||||
|
||||
<objective>
|
||||
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.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@/home/jin/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@/home/jin/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<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
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: CLI Module Setup with ZIO CLI</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<action>
|
||||
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.
|
||||
</action>
|
||||
<verify>
|
||||
```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
|
||||
```
|
||||
</verify>
|
||||
<done>
|
||||
CLI module compiles. `summer version` outputs version info. `summer --help` shows command structure.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Plugin Scaffolding Command</name>
|
||||
<files>
|
||||
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
|
||||
</files>
|
||||
<action>
|
||||
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)")
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
```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"
|
||||
```
|
||||
</verify>
|
||||
<done>
|
||||
`summer plugin create Author.Name` creates complete plugin scaffold. `--dry-run` previews changes. Invalid names produce helpful errors. Existing plugins are not overwritten.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
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
|
||||
```
|
||||
</verification>
|
||||
|
||||
<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>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/05-cli-scaffolding/05-01-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user