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:
358
.planning/phases/05-cli-scaffolding/05-02-PLAN.md
Normal file
358
.planning/phases/05-cli-scaffolding/05-02-PLAN.md
Normal file
@@ -0,0 +1,358 @@
|
||||
---
|
||||
phase: 05-cli-scaffolding
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on: ["05-01"]
|
||||
files_modified:
|
||||
- cli/src/Main.scala
|
||||
- cli/src/commands/ThemeCommands.scala
|
||||
- cli/src/commands/ComponentCommands.scala
|
||||
- cli/src/scaffold/templates/ThemeTemplates.scala
|
||||
- cli/src/scaffold/templates/ComponentTemplates.scala
|
||||
- cli/src/output/Progress.scala
|
||||
autonomous: true
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "Developer can run `summer theme create Author.Theme` and get a blank theme scaffold"
|
||||
- "Developer can run `summer theme create Author.Theme --template=starter` and get a starter theme with Vite/Tailwind"
|
||||
- "Developer can run `summer component create Author.Plugin MyComponent` and get a component scaffold"
|
||||
- "Component scaffolding fails with clear error if plugin doesn't exist"
|
||||
- "Theme scaffolding shows progress bar during file creation"
|
||||
artifacts:
|
||||
- path: "cli/src/commands/ThemeCommands.scala"
|
||||
provides: "Theme scaffolding commands"
|
||||
exports: ["command"]
|
||||
- path: "cli/src/commands/ComponentCommands.scala"
|
||||
provides: "Component scaffolding commands"
|
||||
exports: ["command"]
|
||||
- path: "cli/src/scaffold/templates/ThemeTemplates.scala"
|
||||
provides: "Theme file templates (blank and starter)"
|
||||
exports: ["blankThemeFiles", "starterThemeFiles"]
|
||||
- path: "cli/src/scaffold/templates/ComponentTemplates.scala"
|
||||
provides: "Component file templates"
|
||||
exports: ["componentClass", "componentPartial", "componentYaml"]
|
||||
key_links:
|
||||
- from: "cli/src/Main.scala"
|
||||
to: "cli/src/commands/ThemeCommands.scala"
|
||||
via: "subcommands composition"
|
||||
pattern: "ThemeCommands\\.command"
|
||||
- from: "cli/src/commands/ComponentCommands.scala"
|
||||
to: "cli/src/scaffold/ScaffoldService.scala"
|
||||
via: "plugin existence check"
|
||||
pattern: "exists.*Plugin\\.scala"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Implement theme and component scaffolding commands for the `summer` CLI.
|
||||
|
||||
Purpose: Complete the core scaffolding capabilities for developer productivity (CORE-08, CORE-09).
|
||||
Output: Working `summer theme create` and `summer component create` commands with templates.
|
||||
</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
|
||||
@.planning/phases/05-cli-scaffolding/05-01-SUMMARY.md
|
||||
@build.mill
|
||||
@cli/src/Main.scala
|
||||
@cli/src/scaffold/ScaffoldService.scala
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Theme Scaffolding Command</name>
|
||||
<files>
|
||||
cli/src/Main.scala
|
||||
cli/src/commands/ThemeCommands.scala
|
||||
cli/src/scaffold/templates/ThemeTemplates.scala
|
||||
cli/src/output/Progress.scala
|
||||
</files>
|
||||
<action>
|
||||
Implement `summer theme create Author.Theme [--template=blank|starter]` command.
|
||||
|
||||
1. **Create cli/src/output/Progress.scala:**
|
||||
- ZIO service trait with methods:
|
||||
- `start(total: Int, description: String): UIO[ProgressHandle]`
|
||||
- `update(handle: ProgressHandle, current: Int): UIO[Unit]`
|
||||
- `complete(handle: ProgressHandle): UIO[Unit]`
|
||||
- Case class `ProgressHandle(id: Long, total: Int, description: String)`
|
||||
- Live layer implementation:
|
||||
- Use `print` with `\r` for in-place updates
|
||||
- Progress bar format: `[==== ] 4/10`
|
||||
- Width: 30 characters for the bar
|
||||
- On complete: print newline to preserve final state
|
||||
|
||||
2. **Create cli/src/scaffold/templates/ThemeTemplates.scala:**
|
||||
- Object with template string vals for BLANK theme:
|
||||
- `themeYaml`: theme.yaml manifest (name, author, description, version)
|
||||
- `defaultLayout`: layouts/default.htm with HTML5 boilerplate, {{page}} placeholder
|
||||
- `homePage`: pages/home.htm with basic content, url="/", title="Home"
|
||||
- `notFoundPage`: pages/404.htm with 404 content, url="/404"
|
||||
- Additional templates for STARTER theme:
|
||||
- `packageJson`: package.json with Vite, Tailwind dependencies
|
||||
- `viteConfig`: vite.config.js with build output to assets/
|
||||
- `tailwindCss`: assets/css/app.css with @tailwind directives
|
||||
- `starterJs`: assets/js/app.js with basic imports
|
||||
- `gitignore`: .gitignore for node_modules, dist
|
||||
- Helper function `blankThemeFiles(dir: os.Path, vars: TemplateVars): List[(os.Path, String)]`
|
||||
- Helper function `starterThemeFiles(dir: os.Path, vars: TemplateVars): List[(os.Path, String)]`
|
||||
- Starter includes all blank files PLUS the npm/Vite files
|
||||
|
||||
3. **Create cli/src/commands/ThemeCommands.scala:**
|
||||
- Sealed trait `ThemeCommand` with case class `Create(name: String, template: String, dryRun: Boolean)`
|
||||
- Define options:
|
||||
- `--template`: String with default "blank", help "Theme template: blank or starter"
|
||||
- `--dry-run` / `-n`: Show what would be created
|
||||
- Define args:
|
||||
- `name`: Theme name in Author.Name format
|
||||
- Validate template option: must be "blank" or "starter"
|
||||
- Create `createCommand: Command[ThemeCommand.Create]`
|
||||
- Create `command: Command[ThemeCommand]` with "theme" parent, "create" subcommand
|
||||
- Implement handler:
|
||||
1. Validate name format (same regex as plugin: `^[A-Z][a-zA-Z0-9]*\\.[A-Z][a-zA-Z0-9]*$`)
|
||||
2. Parse into (author, theme) tuple
|
||||
3. Build theme directory path: `themes/{lower_author}/{lower_theme}/`
|
||||
4. Check if directory exists -> fail with `AlreadyExists`
|
||||
5. Get file list based on template choice (blank or starter)
|
||||
6. If --dry-run: print file list
|
||||
7. Else:
|
||||
- Start progress bar
|
||||
- Create each file, update progress
|
||||
- Complete progress bar
|
||||
8. Print next steps:
|
||||
- Blank: "Add layouts, pages, and partials to your theme"
|
||||
- Starter: "Run 'npm install' in theme directory, then 'npm run build'"
|
||||
|
||||
4. **Update cli/src/Main.scala:**
|
||||
- Add ThemeCommands.command to subcommands
|
||||
- Wire Progress.live layer
|
||||
|
||||
**Template placeholder format (use {{var}} consistently):**
|
||||
- `{{name}}`, `{{lower_name}}`, `{{studly_name}}`
|
||||
- `{{author}}`, `{{lower_author}}`, `{{studly_author}}`
|
||||
</action>
|
||||
<verify>
|
||||
```bash
|
||||
# Test blank theme
|
||||
./mill cli.run theme create Acme.Corporate --dry-run
|
||||
# Should list: theme.yaml, layouts/default.htm, pages/home.htm, pages/404.htm
|
||||
|
||||
./mill cli.run theme create Acme.Corporate
|
||||
# Should create themes/acme/corporate/ with blank structure
|
||||
|
||||
ls themes/acme/corporate/
|
||||
# theme.yaml, layouts/, pages/, partials/, assets/
|
||||
|
||||
cat themes/acme/corporate/theme.yaml
|
||||
# Valid YAML with name, author, version
|
||||
|
||||
# Test starter theme
|
||||
./mill cli.run theme create Acme.Modern --template=starter --dry-run
|
||||
# Should list additional: package.json, vite.config.js, etc.
|
||||
|
||||
./mill cli.run theme create Acme.Modern --template=starter
|
||||
# Should create with npm files
|
||||
|
||||
ls themes/acme/modern/
|
||||
# Includes package.json, vite.config.js, etc.
|
||||
|
||||
# Test invalid template
|
||||
./mill cli.run theme create Test.Theme --template=invalid
|
||||
# Should error with valid options
|
||||
|
||||
# Test already exists
|
||||
./mill cli.run theme create Acme.Corporate
|
||||
# Should error
|
||||
```
|
||||
</verify>
|
||||
<done>
|
||||
`summer theme create Author.Theme` creates blank theme. `--template=starter` adds Vite/Tailwind setup. Progress bar shows during creation. Invalid templates rejected.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Component Scaffolding Command</name>
|
||||
<files>
|
||||
cli/src/Main.scala
|
||||
cli/src/commands/ComponentCommands.scala
|
||||
cli/src/scaffold/templates/ComponentTemplates.scala
|
||||
</files>
|
||||
<action>
|
||||
Implement `summer component create Author.Plugin ComponentName` command.
|
||||
|
||||
1. **Create cli/src/scaffold/templates/ComponentTemplates.scala:**
|
||||
- Object with template string vals:
|
||||
- `componentScala`: Component class extending SummerComponent
|
||||
- `componentDetails`: name and description
|
||||
- `defineProperties`: returns List.empty (placeholder)
|
||||
- `onRun`: returns Map.empty (placeholder)
|
||||
- Include TODO comments for each section
|
||||
- `componentPartial`: default.htm partial with basic HTML
|
||||
- Include example of accessing component properties
|
||||
- `componentYaml`: component.yaml with name, description, properties section
|
||||
- Template variables used: `{{name}}`, `{{studly_name}}`, `{{lower_name}}`, `{{plugin_namespace}}`
|
||||
|
||||
2. **Create cli/src/commands/ComponentCommands.scala:**
|
||||
- Sealed trait `ComponentCommand` with case class `Create(plugin: String, name: String, dryRun: Boolean)`
|
||||
- Define args:
|
||||
- `plugin`: Plugin identifier in Author.Name format (required)
|
||||
- `name`: Component name (single word, PascalCase)
|
||||
- Define options:
|
||||
- `--dry-run` / `-n`: Show what would be created
|
||||
- Create `createCommand: Command[ComponentCommand.Create]`
|
||||
- Create `command: Command[ComponentCommand]` with "component" parent, "create" subcommand
|
||||
- Implement handler:
|
||||
1. Validate plugin name format (Author.Name)
|
||||
2. Parse plugin into (author, pluginName) tuple
|
||||
3. Validate component name (PascalCase: `^[A-Z][a-zA-Z0-9]*$`)
|
||||
4. Build plugin directory path: `plugins/{lower_author}/{lower_plugin}/`
|
||||
5. **Check if plugin exists** by verifying `Plugin.scala` exists in plugin dir
|
||||
- If not: fail with `PluginNotFound` error (add to ScaffoldError if needed)
|
||||
6. Build component paths:
|
||||
- `components/{StudlyName}.scala` - component class
|
||||
- `components/{lower_name}/default.htm` - default partial
|
||||
- `components/{lower_name}/component.yaml` - component config
|
||||
7. Check if component already exists -> fail with `AlreadyExists`
|
||||
8. If --dry-run: print file list
|
||||
9. Else: create files via ScaffoldService
|
||||
10. Print next steps:
|
||||
- "Register component in Plugin.scala register() method"
|
||||
- "Edit {StudlyName}.scala to add component logic"
|
||||
- "Edit default.htm partial for component output"
|
||||
|
||||
3. **Update cli/src/Main.scala:**
|
||||
- Add ComponentCommands.command to subcommands
|
||||
|
||||
4. **Add PluginNotFound to ScaffoldError if not present:**
|
||||
- `case class PluginNotFound(pluginId: String) extends ScaffoldError`
|
||||
- Message: "Plugin '{pluginId}' not found. Create it first with: summer plugin create {pluginId}"
|
||||
|
||||
**Component name validation pattern:**
|
||||
```scala
|
||||
private val componentNamePattern = "^[A-Z][a-zA-Z0-9]*$".r
|
||||
|
||||
def validateComponentName(name: String): Either[String, String] =
|
||||
if componentNamePattern.matches(name) then Right(name)
|
||||
else Left(s"Invalid component name '$name'. Use PascalCase (e.g., BlogPost, FeaturedItems)")
|
||||
```
|
||||
</action>
|
||||
<verify>
|
||||
```bash
|
||||
# Ensure test plugin exists first
|
||||
./mill cli.run plugin create Test.Blog 2>/dev/null || true
|
||||
|
||||
# Test component creation
|
||||
./mill cli.run component create Test.Blog FeaturedPosts --dry-run
|
||||
# Should list: FeaturedPosts.scala, featuredposts/default.htm, featuredposts/component.yaml
|
||||
|
||||
./mill cli.run component create Test.Blog FeaturedPosts
|
||||
# Should create component files
|
||||
|
||||
ls plugins/test/blog/components/
|
||||
# FeaturedPosts.scala, featuredposts/
|
||||
|
||||
ls plugins/test/blog/components/featuredposts/
|
||||
# default.htm, component.yaml
|
||||
|
||||
cat plugins/test/blog/components/FeaturedPosts.scala
|
||||
# Valid Scala extending SummerComponent
|
||||
|
||||
# Test plugin not found
|
||||
./mill cli.run component create Nonexistent.Plugin MyComponent
|
||||
# Error: Plugin 'Nonexistent.Plugin' not found...
|
||||
|
||||
# Test invalid component name
|
||||
./mill cli.run component create Test.Blog invalid_name
|
||||
# Error: Invalid component name...
|
||||
|
||||
# Test component already exists
|
||||
./mill cli.run component create Test.Blog FeaturedPosts
|
||||
# Error: Already exists...
|
||||
```
|
||||
</verify>
|
||||
<done>
|
||||
`summer component create Author.Plugin ComponentName` creates component scaffold inside existing plugin. Plugin existence is verified. Invalid names produce clear errors. Generated files include TODO comments.
|
||||
</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
Full plan verification:
|
||||
|
||||
1. Theme scaffolding works:
|
||||
```bash
|
||||
rm -rf themes/verify # Cleanup
|
||||
./mill cli.run theme create Verify.Blank
|
||||
ls themes/verify/blank/
|
||||
# theme.yaml, layouts/, pages/, partials/, assets/
|
||||
```
|
||||
|
||||
2. Starter theme includes npm files:
|
||||
```bash
|
||||
rm -rf themes/verify/starter
|
||||
./mill cli.run theme create Verify.Starter --template=starter
|
||||
ls themes/verify/starter/
|
||||
# Includes package.json, vite.config.js
|
||||
cat themes/verify/starter/package.json
|
||||
# Valid JSON with Vite, Tailwind deps
|
||||
```
|
||||
|
||||
3. Component scaffolding requires existing plugin:
|
||||
```bash
|
||||
./mill cli.run component create Missing.Plugin Test 2>&1 | grep -q "not found"
|
||||
echo "Plugin check works: $?"
|
||||
```
|
||||
|
||||
4. Component creates correct structure:
|
||||
```bash
|
||||
# Use existing test plugin from above
|
||||
./mill cli.run component create Test.Blog LatestPosts
|
||||
test -f plugins/test/blog/components/LatestPosts.scala && echo "Component class created"
|
||||
test -f plugins/test/blog/components/latestposts/default.htm && echo "Partial created"
|
||||
test -f plugins/test/blog/components/latestposts/component.yaml && echo "Config created"
|
||||
```
|
||||
|
||||
5. All help commands work:
|
||||
```bash
|
||||
./mill cli.run theme --help
|
||||
./mill cli.run theme create --help
|
||||
./mill cli.run component --help
|
||||
./mill cli.run component create --help
|
||||
```
|
||||
|
||||
6. Progress bar displays during theme creation:
|
||||
```bash
|
||||
./mill cli.run theme create Verify.Progress --template=starter 2>&1 | head -5
|
||||
# Should show progress output
|
||||
```
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- [ ] `summer theme create Author.Theme` creates valid blank theme
|
||||
- [ ] `--template=starter` adds Vite/Tailwind configuration files
|
||||
- [ ] `--template=invalid` produces error listing valid options
|
||||
- [ ] Progress bar displays during theme file creation
|
||||
- [ ] `summer component create Plugin.Name ComponentName` creates component scaffold
|
||||
- [ ] Component command validates plugin exists before creating
|
||||
- [ ] Component command validates component name is PascalCase
|
||||
- [ ] Generated component includes .scala, .htm partial, .yaml config
|
||||
- [ ] All generated files contain appropriate TODO comments
|
||||
- [ ] Next steps printed after each successful scaffold
|
||||
- [ ] Dry-run mode works for both commands
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/05-cli-scaffolding/05-02-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user