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

14 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 02 execute 2
05-01
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
true
truths artifacts key_links
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
path provides exports
cli/src/commands/ThemeCommands.scala Theme scaffolding commands
command
path provides exports
cli/src/commands/ComponentCommands.scala Component scaffolding commands
command
path provides exports
cli/src/scaffold/templates/ThemeTemplates.scala Theme file templates (blank and starter)
blankThemeFiles
starterThemeFiles
path provides exports
cli/src/scaffold/templates/ComponentTemplates.scala Component file templates
componentClass
componentPartial
componentYaml
from to via pattern
cli/src/Main.scala cli/src/commands/ThemeCommands.scala subcommands composition ThemeCommands.command
from to via pattern
cli/src/commands/ComponentCommands.scala cli/src/scaffold/ScaffoldService.scala plugin existence check exists.*Plugin.scala
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.

<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 @.planning/phases/05-cli-scaffolding/05-01-SUMMARY.md @build.mill @cli/src/Main.scala @cli/src/scaffold/ScaffoldService.scala Task 1: Theme Scaffolding Command cli/src/Main.scala cli/src/commands/ThemeCommands.scala cli/src/scaffold/templates/ThemeTemplates.scala cli/src/output/Progress.scala 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}}`
```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
```
`summer theme create Author.Theme` creates blank theme. `--template=starter` adds Vite/Tailwind setup. Progress bar shows during creation. Invalid templates rejected. Task 2: Component Scaffolding Command cli/src/Main.scala cli/src/commands/ComponentCommands.scala cli/src/scaffold/templates/ComponentTemplates.scala 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)")
```
```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...
```
`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. Full plan verification:
  1. Theme scaffolding works:

    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:

    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:

    ./mill cli.run component create Missing.Plugin Test 2>&1 | grep -q "not found"
    echo "Plugin check works: $?"
    
  4. Component creates correct structure:

    # 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:

    ./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:

    ./mill cli.run theme create Verify.Progress --template=starter 2>&1 | head -5
    # Should show progress output
    

<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>
After completion, create `.planning/phases/05-cli-scaffolding/05-02-SUMMARY.md`