Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
851 changes: 851 additions & 0 deletions docs/superpowers/plans/2026-04-30-plan-3-templates-and-recipes.md

Large diffs are not rendered by default.

57 changes: 55 additions & 2 deletions plugin/agents/scaffolder.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,61 @@
---
name: scaffolder
description: FrinkLoop scaffolder — runs giget against the chosen template and applies recipes. One-shot subagent. Implemented in Plan 3.
description: FrinkLoop scaffolder — bootstraps a new project from the templates registry and applies a list of recipes. One-shot subagent. Reads config.yaml for template + recipes, calls giget, then applies each recipe atomically.
---

# scaffolder

Placeholder. Will be implemented in Plan 3.
## Inputs
- `<project>/.frinkloop/config.yaml` — `template`, `platform`, optional `recipes` list
- `<project>/.frinkloop/spec.md` for context (rarely needed)
- `plugin/templates/registry.yaml` — template registry

## Output
- A populated `$PROJECT_DIR` containing the scaffolded template + applied recipes
- One git commit per recipe applied (`recipe(<id>): apply`) plus the initial scaffold commit
- Marks the corresponding `kind=scaffold` task done in `tasks.json` (the orchestrator does this — you just return)

## Job

1. Read `template` from config.yaml. Resolve via:

```bash
source plugin/lib/scaffolder.sh
resolve_template "<template_id>"
```

If unknown, return BLOCKED.

2. Run scaffold:

```bash
scaffold "<template_id>" "$PROJECT_DIR"
```

This calls `giget` under the hood. Fails if the template isn't reachable (e.g. offline).

3. `cd "$PROJECT_DIR"` and `git init` if not already.

4. Stage everything and make an initial commit:

```bash
git add -A
git -c commit.gpgsign=false commit -m "scaffold: <template_id>"
```

5. For each recipe id listed in config.yaml's `recipes:` (optional field):

```bash
source plugin/lib/recipes.sh
apply_recipe "plugin/recipes/<recipe_id>"
```

Each apply is atomic — failure rolls back to pre-recipe state. If a recipe fails, return BLOCKED with the recipe id and stderr.

6. Return DONE with a list of: template id, applied recipes, final HEAD sha.

## Constraints
- Run only inside `$PROJECT_DIR`. Don't edit the plugin.
- Don't push. Don't deploy. That's Plan 8.
- Don't add new dependencies beyond what the template + recipes specify.
- One-shot: this subagent runs once per project at scaffold time, not per task.
37 changes: 37 additions & 0 deletions plugin/lib/recipes.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/usr/bin/env bash
# FrinkLoop recipe runner — atomic apply with rollback.
# Recipe folder structure: <recipe>/recipe.yaml + <recipe>/apply.sh
# Caller's cwd is PROJECT_DIR (a git repo).

set -euo pipefail

apply_recipe() {
local recipe_dir="$1"
local recipe_id
recipe_id=$(yq -o=json "$recipe_dir/recipe.yaml" | jq -r '.id')

if [ ! -x "$recipe_dir/apply.sh" ]; then
echo "recipes: $recipe_id has no executable apply.sh" >&2
return 1
fi

# Snapshot via git stash (if working tree dirty) so we can roll back.
local pre_sha
pre_sha=$(git rev-parse HEAD)

# Run apply.sh; on failure, hard-reset to pre_sha and clean.
if "$recipe_dir/apply.sh"; then
# If nothing changed, no-op (idempotent recipe) — return success without committing.
if [ -z "$(git status --porcelain)" ]; then
return 0
fi
git add -A
git -c commit.gpgsign=false commit -q -m "recipe($recipe_id): apply"
return 0
else
local rc=$?
git reset --hard "$pre_sha" >/dev/null
git clean -fd >/dev/null
return $rc
fi
}
41 changes: 41 additions & 0 deletions plugin/lib/scaffolder.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#!/usr/bin/env bash
# FrinkLoop scaffolder — wraps giget against the templates/registry.yaml.
# Caller may set GIGET_BIN (default: "npx --yes giget") and REGISTRY_FILE.

set -euo pipefail

: "${GIGET_BIN:=npx --yes giget}"
: "${REGISTRY_FILE:=$(dirname "${BASH_SOURCE[0]}")/../templates/registry.yaml}"

resolve_template() {
local id="$1"
local out
out=$(yq -o=json "$REGISTRY_FILE" | jq -r --arg id "$id" '.templates[] | select(.id == $id) | .giget' 2>/dev/null || true)
if [ -z "$out" ] || [ "$out" = "null" ]; then
return 1
fi
echo "$out"
}

default_template_for_platform() {
local platform="$1"
local out
out=$(yq -o=json "$REGISTRY_FILE" | jq -r --arg p "$platform" '
.templates[] | select(.platform == $p and .default == true) | .id
' | head -1)
if [ -z "$out" ]; then
return 1
fi
echo "$out"
}

scaffold() {
local template_id="$1"
local target="$2"
local source
source=$(resolve_template "$template_id") || {
echo "scaffolder: unknown template '$template_id'" >&2
return 1
}
$GIGET_BIN "$source" "$target"
}
28 changes: 28 additions & 0 deletions plugin/lib/schemas/recipe.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "FrinkLoop recipe",
"type": "object",
"required": ["schema_version", "id", "name", "applies_to"],
"additionalProperties": false,
"properties": {
"schema_version": { "type": "integer", "const": 1 },
"id": { "type": "string", "minLength": 1 },
"name": { "type": "string", "minLength": 1 },
"description": { "type": "string" },
"applies_to": {
"type": "array",
"minItems": 1,
"items": {
"type": "string",
"enum": [
"web-fullstack", "spa-static", "marketing-landing",
"node-api", "python-api", "node-cli", "python-cli",
"mobile-expo", "browser-extension", "discord-bot", "slack-bot",
"*"
]
}
},
"depends_on": { "type": "array", "items": { "type": "string" } },
"post_apply_check": { "type": "string" }
}
}
34 changes: 34 additions & 0 deletions plugin/lib/schemas/registry.schema.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "FrinkLoop templates registry",
"type": "object",
"required": ["schema_version", "templates"],
"additionalProperties": false,
"properties": {
"schema_version": { "type": "integer", "const": 1 },
"templates": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"required": ["id", "platform", "name", "giget", "default"],
"additionalProperties": false,
"properties": {
"id": { "type": "string", "minLength": 1 },
"platform": {
"type": "string",
"enum": [
"web-fullstack", "spa-static", "marketing-landing",
"node-api", "python-api", "node-cli", "python-cli",
"mobile-expo", "browser-extension", "discord-bot", "slack-bot"
]
},
"name": { "type": "string" },
"giget": { "type": "string", "minLength": 1 },
"default": { "type": "boolean" },
"notes": { "type": "string" }
}
}
}
}
}
5 changes: 5 additions & 0 deletions plugin/recipes/_template/apply.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env bash
# Recipe template. Replace this with your actual setup commands.
# Receives PROJECT_DIR as cwd. Exits non-zero on failure to trigger rollback.
set -euo pipefail
echo "_template recipe: no-op"
5 changes: 5 additions & 0 deletions plugin/recipes/_template/recipe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
schema_version: 1
id: _template
name: Recipe template — copy this for new recipes
description: Empty starter — duplicate this folder, rename, fill in apply.sh
applies_to: ["*"]
13 changes: 13 additions & 0 deletions plugin/recipes/deploy-vercel/apply.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# deploy-vercel recipe — drops a minimal vercel.json.
set -euo pipefail

if [ -f vercel.json ]; then
exit 0
fi

cat > vercel.json <<'EOF'
{
"$schema": "https://openapi.vercel.sh/vercel.json"
}
EOF
6 changes: 6 additions & 0 deletions plugin/recipes/deploy-vercel/recipe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
schema_version: 1
id: deploy-vercel
name: Deploy to Vercel
description: Add a vercel.json with framework auto-detection. Does not create a Vercel project — the human runs `vercel` first time.
applies_to: [web-fullstack, spa-static, marketing-landing]
post_apply_check: "test -f vercel.json"
24 changes: 24 additions & 0 deletions plugin/recipes/playwright/apply.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash
# Playwright recipe — installs @playwright/test + chromium browser.
set -euo pipefail

if [ ! -f package.json ]; then
echo "playwright recipe: no package.json found" >&2
exit 1
fi

npm install -D @playwright/test >/dev/null 2>&1
npx --yes playwright install --with-deps chromium >/dev/null 2>&1 || true

if ! [ -f playwright.config.ts ] && ! [ -f playwright.config.js ]; then
cat > playwright.config.ts <<'EOF'
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './tests/e2e',
use: {
baseURL: 'http://localhost:3000',
trace: 'on-first-retry',
},
});
EOF
fi
6 changes: 6 additions & 0 deletions plugin/recipes/playwright/recipe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
schema_version: 1
id: playwright
name: Playwright (headless browser, used by FrinkLoop screenshot pipeline)
description: Install playwright + chromium. Add a basic config so screenshot-capturer can run.
applies_to: [web-fullstack, spa-static, marketing-landing]
post_apply_check: "test -f playwright.config.ts -o -f playwright.config.js"
20 changes: 20 additions & 0 deletions plugin/recipes/tailwind/apply.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
# Tailwind recipe — installs tailwind, postcss, autoprefixer; inits config.
set -euo pipefail

if [ ! -f package.json ]; then
echo "tailwind recipe: no package.json found" >&2
exit 1
fi

npm install -D tailwindcss@^3 postcss@^8 autoprefixer@^10 >/dev/null 2>&1

if ! [ -f tailwind.config.js ] && ! [ -f tailwind.config.ts ]; then
npx --yes tailwindcss init -p >/dev/null 2>&1 || cat > tailwind.config.js <<'EOF'
module.exports = {
content: ["./index.html", "./src/**/*.{js,ts,jsx,tsx}"],
theme: { extend: {} },
plugins: [],
};
EOF
fi
6 changes: 6 additions & 0 deletions plugin/recipes/tailwind/recipe.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
schema_version: 1
id: tailwind
name: Tailwind CSS
description: Install Tailwind CSS, init config, add directives to main CSS entrypoint.
applies_to: [web-fullstack, spa-static, marketing-landing]
post_apply_check: "test -f tailwind.config.js -o -f tailwind.config.ts"
69 changes: 69 additions & 0 deletions plugin/templates/registry.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
schema_version: 1
templates:
- id: next-saas-starter
platform: web-fullstack
name: "Next.js SaaS Starter (Vercel)"
giget: "gh:nextjs/saas-starter"
default: true
notes: "Drizzle + Tailwind + shadcn baked in. Mark Stripe paths as Phase 2."

- id: vite-shadcn
platform: spa-static
name: "Vite + React + shadcn/ui"
giget: "gh:shadcn-ui/vite-template"
default: true

- id: astroship
platform: marketing-landing
name: "Astroship (Astro + Tailwind)"
giget: "gh:surjithctly/astroship"
default: true

- id: hono-openapi
platform: node-api
name: "Hono + OpenAPI starter"
giget: "gh:w3cj/hono-open-api-starter"
default: true

- id: fastapi-ai-prod
platform: python-api
name: "FastAPI AI Production Template"
giget: "gh:wahyudesu/Fastapi-AI-Production-Template"
default: true

- id: citty-playground
platform: node-cli
name: "Citty (UnJS) playground"
giget: "gh:unjs/citty/playground"
default: true

- id: uvinit
platform: python-cli
name: "uvinit (Typer + uv)"
giget: "gh:jlevy/uvinit"
default: true
notes: "Invoke with --data flags to skip interactive prompts."

- id: expo-obytes
platform: mobile-expo
name: "Expo Obytes Starter"
giget: "gh:obytes/react-native-template-obytes"
default: true

- id: wxt-extension
platform: browser-extension
name: "WXT browser extension starter"
giget: "gh:poweroutlet2/browser-extension-starter"
default: true

- id: discord-bot-ts
platform: discord-bot
name: "Discord Bot TypeScript Template"
giget: "gh:KevinNovak/Discord-Bot-TypeScript-Template"
default: true

- id: slack-bolt-ts
platform: slack-bot
name: "Slack Bolt TypeScript starter"
giget: "gh:slack-samples/bolt-ts-starter-template"
default: true
Loading