@mestreyoda/fabrica 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/ARCHITECTURE.md +87 -0
  2. package/LICENSE +21 -0
  3. package/README.md +289 -0
  4. package/defaults/AGENTS.md +150 -0
  5. package/defaults/HEARTBEAT.md +3 -0
  6. package/defaults/IDENTITY.md +6 -0
  7. package/defaults/SOUL.md +39 -0
  8. package/defaults/TOOLS.md +15 -0
  9. package/defaults/fabrica/prompts/architect.md +147 -0
  10. package/defaults/fabrica/prompts/developer.md +211 -0
  11. package/defaults/fabrica/prompts/reviewer.md +114 -0
  12. package/defaults/fabrica/prompts/security-checklist.md +58 -0
  13. package/defaults/fabrica/prompts/tester.md +150 -0
  14. package/defaults/fabrica/workflow.yaml +184 -0
  15. package/dist/index.js +143075 -0
  16. package/dist/index.js.map +7 -0
  17. package/dist/lib/worker.cjs +214 -0
  18. package/dist/worker.cjs +4754 -0
  19. package/fabrica.manifest.json +24 -0
  20. package/genesis/configs/classification-rules.json +32 -0
  21. package/genesis/configs/interview-templates.json +73 -0
  22. package/genesis/configs/labels.json +202 -0
  23. package/genesis/configs/triage-matrix.json +39 -0
  24. package/genesis/scripts/classify-idea.sh +161 -0
  25. package/genesis/scripts/conduct-interview.sh +199 -0
  26. package/genesis/scripts/create-task.sh +797 -0
  27. package/genesis/scripts/delivery-target-lib.sh +88 -0
  28. package/genesis/scripts/generate-qa-contract.sh +188 -0
  29. package/genesis/scripts/generate-spec.sh +171 -0
  30. package/genesis/scripts/genesis-telemetry.sh +97 -0
  31. package/genesis/scripts/genesis-utils.sh +617 -0
  32. package/genesis/scripts/impact-analysis.sh +135 -0
  33. package/genesis/scripts/interview.sh +98 -0
  34. package/genesis/scripts/map-project.sh +309 -0
  35. package/genesis/scripts/receive-idea.sh +69 -0
  36. package/genesis/scripts/register-project.sh +520 -0
  37. package/genesis/scripts/research-idea.sh +84 -0
  38. package/genesis/scripts/scaffold-project.sh +1396 -0
  39. package/genesis/scripts/security-review.sh +141 -0
  40. package/genesis/scripts/sideband-lib.sh +243 -0
  41. package/genesis/scripts/stack-detection-lib.sh +130 -0
  42. package/genesis/scripts/triage.sh +598 -0
  43. package/genesis/scripts/validate-step.sh +81 -0
  44. package/openclaw.plugin.json +45 -0
  45. package/package.json +60 -0
@@ -0,0 +1,1396 @@
1
+ #!/usr/bin/env bash
2
+
3
+ if [ -z "${BASH_VERSION:-}" ]; then
4
+ exec bash "$0" "$@"
5
+ fi
6
+
7
+ set -euo pipefail
8
+
9
+ # Step: Scaffold greenfield project
10
+ # Input: stdin JSON (from impact.stdout)
11
+ # Output: JSON with scaffold data + sideband file
12
+ # Requires: gh CLI authenticated, GENESIS_STACK env (optional)
13
+ # Repo creation/clone stays here because the current DevClaw/OpenClaw surface
14
+ # does not expose a deterministic repo lifecycle API equivalent to gh+git.
15
+
16
+ GENESIS_LOG="${GENESIS_LOG:-$HOME/.openclaw/workspace/logs/genesis.log}"
17
+ mkdir -p "$(dirname "$GENESIS_LOG")"
18
+ exec 2> >(tee -a "$GENESIS_LOG" >&2)
19
+
20
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
21
+ source "$SCRIPT_DIR/sideband-lib.sh"
22
+ source "$SCRIPT_DIR/delivery-target-lib.sh"
23
+ source "$SCRIPT_DIR/stack-detection-lib.sh"
24
+ source "$SCRIPT_DIR/genesis-telemetry.sh"
25
+ genesis_load_env_file "$HOME/.openclaw/.env"
26
+
27
+ MAX_REPO_NAME_LEN=80
28
+ MAX_REPO_DESC_LEN=340
29
+
30
+ sanitize_repo_name() {
31
+ local raw cleaned
32
+ raw="${1:-}"
33
+ cleaned="$(printf '%s' "$raw" | iconv -f utf-8 -t ascii//TRANSLIT 2>/dev/null || printf '%s' "$raw")"
34
+ cleaned="$(printf '%s' "$cleaned" \
35
+ | tr '[:upper:]' '[:lower:]' \
36
+ | tr '[:space:]_' '-' \
37
+ | sed -E 's/[^a-z0-9._-]+/-/g; s/[._-]+/-/g; s/^-+//; s/-+$//')"
38
+ cleaned="$(printf '%s' "$cleaned" | cut -c1-"$MAX_REPO_NAME_LEN" | sed -E 's/-+$//')"
39
+ printf '%s' "$cleaned"
40
+ }
41
+
42
+ GENERIC_NAMES_BLOCKLIST="novo|new|app|api|web|test|temp|tmp|projeto|project|demo|sample|example|my-app|my-project|untitled|criar|create|build"
43
+
44
+ validate_repo_name() {
45
+ local name="$1"
46
+ [[ -z "$name" ]] && return 1
47
+ echo "$name" | grep -qxE "$GENERIC_NAMES_BLOCKLIST" && {
48
+ echo "WARNING: Generic repo name '$name' rejected by blocklist" >&2
49
+ return 1
50
+ }
51
+ [[ "${#name}" -lt 3 ]] && {
52
+ echo "WARNING: Repo name '$name' too short" >&2
53
+ return 1
54
+ }
55
+ return 0
56
+ }
57
+
58
+ sanitize_repo_description() {
59
+ local raw cleaned
60
+ raw="${1:-}"
61
+ cleaned="$(printf '%s' "$raw" \
62
+ | sed -E $'s/[\\x00-\\x1F\\x7F]/ /g; s/[[:space:]]+/ /g; s/^ //; s/ $//')"
63
+ cleaned="$(printf '%s' "$cleaned" | cut -c1-"$MAX_REPO_DESC_LEN" | sed -E 's/[[:space:]]+$//')"
64
+ printf '%s' "$cleaned"
65
+ }
66
+
67
+ if [[ -n "${1:-}" && -f "${1:-}" ]]; then
68
+ INPUT="$(cat "$1")"
69
+ else
70
+ INPUT="$(cat)"
71
+ fi
72
+ SESSION_ID="$(echo "$INPUT" | jq -r '.session_id')"
73
+ genesis_metric_start "scaffold-project" "$SESSION_ID"
74
+ echo "=== $(date -Iseconds) | scaffold-project.sh | session=$SESSION_ID ===" >&2
75
+
76
+ IS_GREENFIELD="$(echo "$INPUT" | jq -r '.impact.is_greenfield // false')"
77
+ DRY_RUN="${GENESIS_DRY_RUN:-$(echo "$INPUT" | jq -r '.dry_run // false')}"
78
+
79
+ # Non-greenfield: passthrough
80
+ if [[ "$IS_GREENFIELD" != "true" ]]; then
81
+ echo "Not greenfield — skipping scaffold" >&2
82
+ echo "$INPUT" | jq '. + {scaffold: {created: false}}'
83
+ exit 0
84
+ fi
85
+
86
+ if [[ "$DRY_RUN" == "true" ]]; then
87
+ echo "Dry-run enabled — skipping scaffold and repo lifecycle actions" >&2
88
+ echo "$INPUT" | jq '. + {scaffold: {created: false, reason: "dry_run"}}'
89
+ exit 0
90
+ fi
91
+
92
+ echo "Greenfield project detected — running scaffold" >&2
93
+
94
+ GH_OWNER="${GENESIS_GH_OWNER:-${GENESIS_GH_ORG:-${GENESIS_GITHUB_ORG:-${GITHUB_OWNER:-${GITHUB_ORG:-}}}}}"
95
+
96
+ SPEC="$(echo "$INPUT" | jq '.spec // {}')"
97
+ TITLE="$(echo "$SPEC" | jq -r '.title // empty')"
98
+ DELIVERY_TARGET_RAW="$(echo "$INPUT" | jq -r '.spec.delivery_target // .classification.delivery_target // .metadata.delivery_target // empty')"
99
+ if [[ -z "$DELIVERY_TARGET_RAW" || "$DELIVERY_TARGET_RAW" == "null" ]]; then
100
+ DELIVERY_TARGET="$(genesis_detect_delivery_target_from_text "${GENESIS_IDEA:-$TITLE}")"
101
+ else
102
+ DELIVERY_TARGET_NORMALIZED="$(genesis_normalize_delivery_target "$DELIVERY_TARGET_RAW")"
103
+ DELIVERY_TARGET="$(genesis_cross_validate_delivery_target "$DELIVERY_TARGET_NORMALIZED" "${GENESIS_IDEA:-$TITLE}")"
104
+ fi
105
+ INPUT_REPO_URL="$(echo "$INPUT" | jq -r '.metadata.repo_url // .repo_url // .repo // .scaffold.repo_url // empty')"
106
+ INPUT_PROJECT_NAME="$(echo "$INPUT" | jq -r '.metadata.project_name // .project_name // .name // .repo_name // empty')"
107
+ EXPLICIT_OWNER_REPO="$(genesis_parse_owner_repo "$INPUT_REPO_URL" || true)"
108
+
109
+ if [[ -n "$INPUT_REPO_URL" ]] && [[ -z "$EXPLICIT_OWNER_REPO" ]]; then
110
+ if [[ "$INPUT_REPO_URL" == *"/"* ]] || [[ "$INPUT_REPO_URL" == http* ]] || [[ "$INPUT_REPO_URL" == git@* ]] || [[ "$INPUT_REPO_URL" == ssh://* ]]; then
111
+ echo "ERROR: Invalid explicit repository hint '$INPUT_REPO_URL'. Use owner/repo or a valid GitHub URL." >&2
112
+ exit 1
113
+ fi
114
+ fi
115
+
116
+ # Fallback: use idea text from env when spec is not available (branch B has no spec)
117
+ if [[ -z "$TITLE" ]]; then
118
+ TITLE="${GENESIS_IDEA:-untitled}"
119
+ fi
120
+
121
+ # --- Stack detection (3-tier): env > keyword > default ---
122
+ # Uses shared stack-detection-lib.sh for consistency with generate-qa-contract.sh
123
+ STACK="${GENESIS_STACK:-}"
124
+
125
+ # Normalize: "python" is not a valid stack. Clear unknown hints for auto-detect.
126
+ STACK="$(genesis_normalize_stack_hint "$STACK")"
127
+ [[ -n "${GENESIS_STACK:-}" && -z "$STACK" ]] && echo "Normalized unknown stack '${GENESIS_STACK}' → auto-detect" >&2
128
+
129
+ if [[ -z "$STACK" ]]; then
130
+ # Build SPEC_TEXT from all available sources: spec fields + raw idea.
131
+ # Always include GENESIS_IDEA to preserve user keywords the LLM may have
132
+ # "translated away" (e.g., user says "python" but LLM spec says "aplicação").
133
+ SPEC_TITLE="$(echo "$SPEC" | jq -r '.title // empty')"
134
+ IDEA_LOWER="$(echo "${GENESIS_IDEA:-}" | tr '[:upper:]' '[:lower:]')"
135
+ if [[ -z "$SPEC_TITLE" ]]; then
136
+ SPEC_TEXT="$IDEA_LOWER"
137
+ else
138
+ SPEC_FIELDS="$(echo "$SPEC" | jq -r '[.title, .objective, (.scope_v1 // [] | .[]), (.acceptance_criteria // [] | .[])] | map(select(. != null)) | join(" ")' | tr '[:upper:]' '[:lower:]')"
139
+ SPEC_TEXT="$IDEA_LOWER $SPEC_FIELDS"
140
+ fi
141
+
142
+ STACK="$(genesis_detect_stack_from_text "$SPEC_TEXT")"
143
+
144
+ if [[ -z "$STACK" ]]; then
145
+ STACK="$(genesis_detect_stack_from_delivery_target "$DELIVERY_TARGET")"
146
+ fi
147
+
148
+ echo "Auto-detected stack: $STACK (delivery_target=$DELIVERY_TARGET)" >&2
149
+ else
150
+ echo "Stack from env: $STACK" >&2
151
+ fi
152
+
153
+ # --- Resolve repo target from explicit input first; fallback to slugified title ---
154
+ REPO_NAME_SOURCE="$TITLE"
155
+ if [[ -n "$INPUT_PROJECT_NAME" ]]; then
156
+ REPO_NAME_SOURCE="$INPUT_PROJECT_NAME"
157
+ fi
158
+ if [[ -n "$EXPLICIT_OWNER_REPO" ]]; then
159
+ GH_OWNER="${EXPLICIT_OWNER_REPO%%/*}"
160
+ REPO_NAME_SOURCE="${EXPLICIT_OWNER_REPO##*/}"
161
+ echo "Using explicit repo target from metadata.repo_url: $GH_OWNER/$REPO_NAME_SOURCE" >&2
162
+ elif [[ -n "$INPUT_REPO_URL" ]]; then
163
+ REPO_NAME_SOURCE="$INPUT_REPO_URL"
164
+ echo "Using explicit repo name hint: $INPUT_REPO_URL" >&2
165
+ elif [[ -n "$INPUT_PROJECT_NAME" ]]; then
166
+ echo "Using explicit repo name from metadata.project_name: $INPUT_PROJECT_NAME" >&2
167
+ fi
168
+
169
+ if [[ -z "$GH_OWNER" ]]; then
170
+ GH_OWNER="$(gh api user -q '.login' 2>/dev/null || true)"
171
+ fi
172
+ if [[ -z "$GH_OWNER" ]]; then
173
+ echo "ERROR: Could not resolve GitHub owner/org. Set GENESIS_GH_OWNER or GENESIS_GH_ORG." >&2
174
+ exit 1
175
+ fi
176
+ if [[ ! "$GH_OWNER" =~ ^[A-Za-z0-9._-]+$ ]]; then
177
+ echo "ERROR: Invalid GitHub owner/org value: '$GH_OWNER'" >&2
178
+ exit 1
179
+ fi
180
+
181
+ REPO_NAME="$(sanitize_repo_name "$REPO_NAME_SOURCE")"
182
+ if ! validate_repo_name "$REPO_NAME"; then
183
+ FALLBACK_SOURCE="$(echo "$SPEC" | jq -r '.objective // empty' | head -c 60)"
184
+ [[ -n "$FALLBACK_SOURCE" ]] && REPO_NAME="$(sanitize_repo_name "$FALLBACK_SOURCE")"
185
+ if ! validate_repo_name "$REPO_NAME"; then
186
+ REPO_NAME="genesis-project-$(date +%s)"
187
+ echo "WARNING: Could not derive meaningful name, using fallback: $REPO_NAME" >&2
188
+ fi
189
+ fi
190
+
191
+ REPO_URL="https://github.com/$GH_OWNER/$REPO_NAME"
192
+ REPO_LOCAL="$HOME/git/$REPO_NAME"
193
+
194
+ echo "Repo: $GH_OWNER/$REPO_NAME, Stack: $STACK" >&2
195
+
196
+ # --- Create GitHub repo (idempotent) ---
197
+ REPO_CREATED_NOW=false
198
+ if gh repo view "$GH_OWNER/$REPO_NAME" &>/dev/null; then
199
+ echo "Repo $GH_OWNER/$REPO_NAME already exists — reusing" >&2
200
+ else
201
+ echo "Creating repo $GH_OWNER/$REPO_NAME..." >&2
202
+ REPO_DESC_RAW="$(echo "$SPEC" | jq -r '.objective // empty')"
203
+ if [[ -z "$REPO_DESC_RAW" ]]; then
204
+ REPO_DESC_RAW="${GENESIS_IDEA:-Auto-scaffolded project}"
205
+ fi
206
+ REPO_DESC="$(sanitize_repo_description "$REPO_DESC_RAW")"
207
+ if [[ -z "$REPO_DESC" ]]; then
208
+ REPO_DESC="Auto-scaffolded project"
209
+ fi
210
+
211
+ if ! gh repo create "$GH_OWNER/$REPO_NAME" --private --description "$REPO_DESC" >&2; then
212
+ if gh repo view "$GH_OWNER/$REPO_NAME" &>/dev/null; then
213
+ echo "Repo $GH_OWNER/$REPO_NAME already exists after failed create call — continuing" >&2
214
+ else
215
+ echo "Repo creation with description failed — retrying without description" >&2
216
+ gh repo create "$GH_OWNER/$REPO_NAME" --private >&2 || {
217
+ echo "ERROR: Failed to create repo" >&2
218
+ exit 1
219
+ }
220
+ fi
221
+ fi
222
+ REPO_CREATED_NOW=true
223
+ sleep 2 # wait for GitHub to fully initialize
224
+ fi
225
+
226
+ # --- Clone ---
227
+ if [[ -d "$REPO_LOCAL" ]]; then
228
+ echo "Local clone exists at $REPO_LOCAL — pulling" >&2
229
+ cd "$REPO_LOCAL" && git pull origin main >&2 2>/dev/null || true
230
+ else
231
+ echo "Cloning to $REPO_LOCAL..." >&2
232
+ mkdir -p "$HOME/git"
233
+ gh repo clone "$GH_OWNER/$REPO_NAME" "$REPO_LOCAL" >&2 || {
234
+ if [[ "$REPO_CREATED_NOW" == "true" && ! -e "$REPO_LOCAL" ]]; then
235
+ # Fresh repos can fail to clone before the default branch exists; seed locally.
236
+ mkdir -p "$REPO_LOCAL"
237
+ cd "$REPO_LOCAL"
238
+ git init >&2
239
+ git remote add origin "https://github.com/$GH_OWNER/$REPO_NAME.git" >&2
240
+ else
241
+ echo "ERROR: Failed to clone $GH_OWNER/$REPO_NAME" >&2
242
+ exit 1
243
+ fi
244
+ }
245
+ fi
246
+
247
+ cd "$REPO_LOCAL"
248
+
249
+ FILES_CREATED=()
250
+
251
+ # ===================================================================
252
+ # Scaffold functions per stack
253
+ # ===================================================================
254
+
255
+ scaffold_gitignore_node() {
256
+ cat > .gitignore <<'GITEOF'
257
+ node_modules/
258
+ dist/
259
+ build/
260
+ .next/
261
+ .env
262
+ .env.local
263
+ *.log
264
+ coverage/
265
+ .coverage
266
+ .DS_Store
267
+ GITEOF
268
+ FILES_CREATED+=(".gitignore")
269
+ }
270
+
271
+ scaffold_gitignore_python() {
272
+ cat > .gitignore <<'GITEOF'
273
+ __pycache__/
274
+ *.pyc
275
+ .env
276
+ .venv/
277
+ venv/
278
+ dist/
279
+ build/
280
+ *.egg-info/
281
+ .mypy_cache/
282
+ .ruff_cache/
283
+ .pytest_cache/
284
+ coverage/
285
+ htmlcov/
286
+ .coverage
287
+ .coverage.*
288
+ *.log
289
+ .DS_Store
290
+ GITEOF
291
+ FILES_CREATED+=(".gitignore")
292
+ }
293
+
294
+ scaffold_env_example() {
295
+ local stack="$1"
296
+ case "$stack" in
297
+ nextjs|express)
298
+ cat > .env.example <<'EOF'
299
+ # Application
300
+ NODE_ENV=development
301
+ PORT=3000
302
+
303
+ # Database (if applicable)
304
+ # DATABASE_URL=postgresql://user:pass@localhost:5432/dbname
305
+
306
+ # Auth (if applicable)
307
+ # JWT_SECRET=change-me
308
+ # SESSION_SECRET=change-me
309
+ EOF
310
+ ;;
311
+ fastapi|flask|django)
312
+ cat > .env.example <<'EOF'
313
+ # Application
314
+ DEBUG=true
315
+ SECRET_KEY=change-me
316
+
317
+ # Database (if applicable)
318
+ # DATABASE_URL=postgresql://user:pass@localhost:5432/dbname
319
+
320
+ # Server
321
+ HOST=0.0.0.0
322
+ PORT=8000
323
+ EOF
324
+ ;;
325
+ python-cli)
326
+ cat > .env.example <<'EOF'
327
+ # Application
328
+ # LOG_LEVEL=info
329
+ EOF
330
+ ;;
331
+ esac
332
+ FILES_CREATED+=(".env.example")
333
+ }
334
+
335
+ scaffold_readme() {
336
+ local name="$1" stack="$2" objective="$3"
337
+ cat > README.md <<EOF
338
+ # $name
339
+
340
+ $objective
341
+
342
+ ## Stack
343
+
344
+ - **Framework**: $stack
345
+
346
+ ## Setup
347
+
348
+ \`\`\`bash
349
+ $(case "$stack" in
350
+ nextjs|express) echo "npm install" ;;
351
+ fastapi|flask|django) echo "python -m venv .venv && source .venv/bin/activate && pip install -r requirements.txt" ;;
352
+ python-cli) echo "python -m venv .venv && source .venv/bin/activate && pip install -e '.[dev]'" ;;
353
+ esac)
354
+ \`\`\`
355
+
356
+ ## Run
357
+
358
+ \`\`\`bash
359
+ $(case "$stack" in
360
+ nextjs) echo "npm run dev" ;;
361
+ express) echo "npm run dev" ;;
362
+ fastapi) echo "uvicorn app.main:app --reload" ;;
363
+ flask) echo "flask run --debug" ;;
364
+ django) echo "python manage.py runserver" ;;
365
+ python-cli) echo "$name --help" ;;
366
+ esac)
367
+ \`\`\`
368
+
369
+ ## QA
370
+
371
+ \`\`\`bash
372
+ bash scripts/qa.sh
373
+ \`\`\`
374
+
375
+ ---
376
+ _Scaffolded by Genesis Flow_
377
+ EOF
378
+ FILES_CREATED+=("README.md")
379
+ }
380
+
381
+ scaffold_ci_workflow() {
382
+ mkdir -p .github/workflows
383
+ cat > .github/workflows/qa.yml <<'EOF'
384
+ name: QA
385
+
386
+ on:
387
+ push:
388
+ branches:
389
+ - main
390
+ pull_request:
391
+
392
+ jobs:
393
+ qa:
394
+ runs-on: ubuntu-latest
395
+ steps:
396
+ - name: Checkout
397
+ uses: actions/checkout@v4
398
+
399
+ - name: Setup Node (cached)
400
+ if: ${{ hashFiles('package.json') != '' && hashFiles('package-lock.json') != '' }}
401
+ uses: actions/setup-node@v4
402
+ with:
403
+ node-version: '20'
404
+ cache: npm
405
+
406
+ - name: Setup Node (no cache)
407
+ if: ${{ hashFiles('package.json') != '' && hashFiles('package-lock.json') == '' }}
408
+ uses: actions/setup-node@v4
409
+ with:
410
+ node-version: '20'
411
+
412
+ - name: Setup Python
413
+ if: ${{ hashFiles('requirements.txt') != '' || hashFiles('pyproject.toml') != '' }}
414
+ uses: actions/setup-python@v5
415
+ with:
416
+ python-version: '3.11'
417
+
418
+ - name: Install Node deps
419
+ if: ${{ hashFiles('package.json') != '' }}
420
+ run: |
421
+ if [ -f package-lock.json ]; then npm ci; else npm install; fi
422
+
423
+ - name: Install Python deps
424
+ if: ${{ hashFiles('requirements.txt') != '' || hashFiles('pyproject.toml') != '' }}
425
+ run: |
426
+ python -m pip install --upgrade pip
427
+ if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
428
+ if [ -f pyproject.toml ]; then pip install -e '.[dev]'; fi
429
+
430
+ - name: Run QA
431
+ run: bash scripts/qa.sh
432
+ EOF
433
+ FILES_CREATED+=(".github/workflows/qa.yml")
434
+ }
435
+
436
+ generate_node_lockfile() {
437
+ if command -v npm >/dev/null 2>&1; then
438
+ npm install --package-lock-only --ignore-scripts --no-audit --no-fund >/dev/null 2>&1 || true
439
+ fi
440
+ if [[ -f package-lock.json ]]; then
441
+ FILES_CREATED+=("package-lock.json")
442
+ fi
443
+ }
444
+
445
+ # --- Next.js ---
446
+ scaffold_nextjs() {
447
+ scaffold_gitignore_node
448
+ scaffold_env_example "nextjs"
449
+
450
+ cat > package.json <<'EOF'
451
+ {
452
+ "name": "REPO_NAME_PLACEHOLDER",
453
+ "version": "0.1.0",
454
+ "private": true,
455
+ "scripts": {
456
+ "dev": "next dev",
457
+ "build": "next build",
458
+ "start": "next start",
459
+ "lint": "next lint",
460
+ "test": "vitest run",
461
+ "test:watch": "vitest"
462
+ },
463
+ "dependencies": {
464
+ "next": "^15.0.0",
465
+ "react": "^19.0.0",
466
+ "react-dom": "^19.0.0"
467
+ },
468
+ "devDependencies": {
469
+ "@types/node": "^22.0.0",
470
+ "@types/react": "^19.0.0",
471
+ "eslint": "^9.0.0",
472
+ "eslint-config-next": "^15.0.0",
473
+ "typescript": "^5.7.0",
474
+ "vitest": "^3.0.0",
475
+ "@vitest/coverage-v8": "^3.0.0",
476
+ "@vitejs/plugin-react": "^4.0.0"
477
+ }
478
+ }
479
+ EOF
480
+ jq --arg name "$REPO_NAME" '.name = $name' package.json > package.json.tmp && mv package.json.tmp package.json
481
+ FILES_CREATED+=("package.json")
482
+ generate_node_lockfile
483
+
484
+ cat > tsconfig.json <<'EOF'
485
+ {
486
+ "compilerOptions": {
487
+ "target": "ES2017",
488
+ "lib": ["dom", "dom.iterable", "esnext"],
489
+ "allowJs": true,
490
+ "skipLibCheck": true,
491
+ "strict": true,
492
+ "noEmit": true,
493
+ "esModuleInterop": true,
494
+ "module": "esnext",
495
+ "moduleResolution": "bundler",
496
+ "resolveJsonModule": true,
497
+ "isolatedModules": true,
498
+ "jsx": "preserve",
499
+ "incremental": true,
500
+ "paths": { "@/*": ["./src/*"] }
501
+ },
502
+ "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],
503
+ "exclude": ["node_modules"]
504
+ }
505
+ EOF
506
+ FILES_CREATED+=("tsconfig.json")
507
+
508
+ mkdir -p src/app
509
+ cat > src/app/layout.tsx <<'EOF'
510
+ export const metadata = {
511
+ title: 'App',
512
+ description: 'Auto-scaffolded project',
513
+ };
514
+
515
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
516
+ return (
517
+ <html lang="en">
518
+ <body>{children}</body>
519
+ </html>
520
+ );
521
+ }
522
+ EOF
523
+ FILES_CREATED+=("src/app/layout.tsx")
524
+
525
+ cat > src/app/page.tsx <<'EOF'
526
+ export default function Home() {
527
+ return (
528
+ <main>
529
+ <h1>Welcome</h1>
530
+ <p>Project scaffolded by Genesis Flow.</p>
531
+ </main>
532
+ );
533
+ }
534
+ EOF
535
+ FILES_CREATED+=("src/app/page.tsx")
536
+
537
+ mkdir -p src/app/api/health
538
+ cat > src/app/api/health/route.ts <<'EOF'
539
+ import { NextResponse } from 'next/server';
540
+
541
+ export function GET() {
542
+ return NextResponse.json({ status: 'ok', timestamp: new Date().toISOString() });
543
+ }
544
+ EOF
545
+ FILES_CREATED+=("src/app/api/health/route.ts")
546
+
547
+ mkdir -p tests
548
+ cat > tests/health.test.ts <<'EOF'
549
+ import { describe, it, expect } from 'vitest';
550
+
551
+ describe('Health check', () => {
552
+ it('should return ok status', async () => {
553
+ const { GET } = await import('../src/app/api/health/route');
554
+ const response = GET();
555
+ const data = await response.json();
556
+ expect(data.status).toBe('ok');
557
+ });
558
+ });
559
+ EOF
560
+ FILES_CREATED+=("tests/health.test.ts")
561
+
562
+ mkdir -p scripts
563
+ cat > scripts/qa.sh <<'QAEOF'
564
+ #!/usr/bin/env bash
565
+ set -euo pipefail
566
+ echo "=== QA Gate ==="
567
+ FAIL=0
568
+
569
+ echo "--- Lint ---"
570
+ npx next lint . 2>&1 || { echo "LINT FAILED"; FAIL=1; }
571
+
572
+ echo "--- TypeScript ---"
573
+ npx tsc --noEmit 2>&1 || { echo "TSC FAILED"; FAIL=1; }
574
+
575
+ echo "--- Tests ---"
576
+ npx vitest run 2>&1 || { echo "TESTS FAILED"; FAIL=1; }
577
+
578
+ echo "--- Coverage (>=80%) ---"
579
+ npx vitest run --coverage --coverage.thresholds.lines=80 2>&1 || { echo "COVERAGE FAILED"; FAIL=1; }
580
+
581
+ echo "--- Secrets scan ---"
582
+ if grep -rn 'password\s*=\s*"[^"]\+"\|api_key\s*=\s*"[^"]\+"\|secret\s*=\s*"[^"]\+"' --include="*.ts" --include="*.tsx" --include="*.js" src/ 2>/dev/null; then
583
+ echo "SECRETS FOUND — FAIL"; FAIL=1
584
+ else
585
+ echo "No hardcoded secrets found"
586
+ fi
587
+
588
+ exit $FAIL
589
+ QAEOF
590
+ chmod +x scripts/qa.sh
591
+ FILES_CREATED+=("scripts/qa.sh")
592
+ }
593
+
594
+ # --- Express ---
595
+ scaffold_express() {
596
+ scaffold_gitignore_node
597
+ scaffold_env_example "express"
598
+
599
+ cat > package.json <<'EOF'
600
+ {
601
+ "name": "REPO_NAME_PLACEHOLDER",
602
+ "version": "0.1.0",
603
+ "private": true,
604
+ "type": "module",
605
+ "scripts": {
606
+ "dev": "tsx watch src/index.ts",
607
+ "build": "tsc",
608
+ "start": "node dist/index.js",
609
+ "lint": "eslint src/",
610
+ "test": "vitest run",
611
+ "test:watch": "vitest"
612
+ },
613
+ "dependencies": {
614
+ "express": "^5.0.0",
615
+ "dotenv": "^16.4.0"
616
+ },
617
+ "devDependencies": {
618
+ "@types/express": "^5.0.0",
619
+ "@types/node": "^22.0.0",
620
+ "eslint": "^9.0.0",
621
+ "typescript": "^5.7.0",
622
+ "tsx": "^4.0.0",
623
+ "vitest": "^3.0.0",
624
+ "@vitest/coverage-v8": "^3.0.0",
625
+ "supertest": "^7.0.0",
626
+ "@types/supertest": "^6.0.0"
627
+ }
628
+ }
629
+ EOF
630
+ jq --arg name "$REPO_NAME" '.name = $name' package.json > package.json.tmp && mv package.json.tmp package.json
631
+ FILES_CREATED+=("package.json")
632
+ generate_node_lockfile
633
+
634
+ cat > tsconfig.json <<'EOF'
635
+ {
636
+ "compilerOptions": {
637
+ "target": "ES2022",
638
+ "module": "ESNext",
639
+ "moduleResolution": "bundler",
640
+ "outDir": "dist",
641
+ "rootDir": "src",
642
+ "strict": true,
643
+ "esModuleInterop": true,
644
+ "skipLibCheck": true,
645
+ "resolveJsonModule": true,
646
+ "declaration": true
647
+ },
648
+ "include": ["src"],
649
+ "exclude": ["node_modules", "dist", "tests"]
650
+ }
651
+ EOF
652
+ FILES_CREATED+=("tsconfig.json")
653
+
654
+ mkdir -p src
655
+ cat > src/index.ts <<'EOF'
656
+ import express from 'express';
657
+ import dotenv from 'dotenv';
658
+
659
+ dotenv.config();
660
+
661
+ const app = express();
662
+ const PORT = process.env.PORT || 3000;
663
+
664
+ app.use(express.json());
665
+
666
+ app.get('/health', (_req, res) => {
667
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
668
+ });
669
+
670
+ app.use((err: Error, _req: express.Request, res: express.Response, _next: express.NextFunction) => {
671
+ console.error(err.message);
672
+ res.status(500).json({ error: 'Internal server error' });
673
+ });
674
+
675
+ if (process.env.NODE_ENV !== 'test') {
676
+ app.listen(PORT, () => {
677
+ console.log(`Server running on port ${PORT}`);
678
+ });
679
+ }
680
+
681
+ export default app;
682
+ EOF
683
+ FILES_CREATED+=("src/index.ts")
684
+
685
+ mkdir -p tests
686
+ cat > tests/health.test.ts <<'EOF'
687
+ import { describe, it, expect } from 'vitest';
688
+ import request from 'supertest';
689
+ import app from '../src/index.js';
690
+
691
+ describe('GET /health', () => {
692
+ it('should return ok status', async () => {
693
+ const res = await request(app).get('/health');
694
+ expect(res.status).toBe(200);
695
+ expect(res.body.status).toBe('ok');
696
+ });
697
+ });
698
+ EOF
699
+ FILES_CREATED+=("tests/health.test.ts")
700
+
701
+ mkdir -p scripts
702
+ cat > scripts/qa.sh <<'QAEOF'
703
+ #!/usr/bin/env bash
704
+ set -euo pipefail
705
+ echo "=== QA Gate ==="
706
+ FAIL=0
707
+
708
+ echo "--- Lint ---"
709
+ npx eslint src/ 2>&1 || { echo "LINT FAILED"; FAIL=1; }
710
+
711
+ echo "--- TypeScript ---"
712
+ npx tsc --noEmit 2>&1 || { echo "TSC FAILED"; FAIL=1; }
713
+
714
+ echo "--- Tests ---"
715
+ npx vitest run 2>&1 || { echo "TESTS FAILED"; FAIL=1; }
716
+
717
+ echo "--- Coverage (>=80%) ---"
718
+ npx vitest run --coverage --coverage.thresholds.lines=80 2>&1 || { echo "COVERAGE FAILED"; FAIL=1; }
719
+
720
+ echo "--- Secrets scan ---"
721
+ if grep -rn 'password\s*=\s*"[^"]\+"\|api_key\s*=\s*"[^"]\+"\|secret\s*=\s*"[^"]\+"' --include="*.ts" --include="*.js" src/ 2>/dev/null; then
722
+ echo "SECRETS FOUND — FAIL"; FAIL=1
723
+ else
724
+ echo "No hardcoded secrets found"
725
+ fi
726
+
727
+ exit $FAIL
728
+ QAEOF
729
+ chmod +x scripts/qa.sh
730
+ FILES_CREATED+=("scripts/qa.sh")
731
+ }
732
+
733
+ # --- FastAPI ---
734
+ scaffold_fastapi() {
735
+ scaffold_gitignore_python
736
+ scaffold_env_example "fastapi"
737
+
738
+ cat > pyproject.toml <<EOF
739
+ [build-system]
740
+ requires = ["setuptools>=75.0.0"]
741
+ build-backend = "setuptools.build_meta"
742
+
743
+ [project]
744
+ name = "$REPO_NAME"
745
+ version = "0.1.0"
746
+ requires-python = ">=3.11"
747
+ dependencies = [
748
+ "fastapi>=0.115.0",
749
+ "uvicorn[standard]>=0.32.0",
750
+ "pydantic>=2.0.0",
751
+ "pydantic-settings>=2.0.0",
752
+ "python-dotenv>=1.0.0",
753
+ ]
754
+
755
+ [project.optional-dependencies]
756
+ dev = [
757
+ "pytest>=8.0.0",
758
+ "pytest-asyncio>=0.24.0",
759
+ "pytest-cov>=5.0.0",
760
+ "httpx>=0.27.0",
761
+ "ruff>=0.8.0",
762
+ "mypy>=1.13.0",
763
+ ]
764
+
765
+ [tool.pytest.ini_options]
766
+ testpaths = ["tests"]
767
+ pythonpath = ["."]
768
+
769
+ [tool.ruff]
770
+ target-version = "py311"
771
+ line-length = 120
772
+
773
+ [tool.mypy]
774
+ python_version = "3.11"
775
+ warn_return_any = true
776
+ warn_unused_configs = true
777
+ EOF
778
+ FILES_CREATED+=("pyproject.toml")
779
+
780
+ cat > requirements.txt <<'EOF'
781
+ fastapi>=0.115.0
782
+ uvicorn[standard]>=0.32.0
783
+ pydantic>=2.0.0
784
+ pydantic-settings>=2.0.0
785
+ python-dotenv>=1.0.0
786
+ EOF
787
+ FILES_CREATED+=("requirements.txt")
788
+
789
+ mkdir -p app
790
+ cat > app/__init__.py <<'EOF'
791
+ EOF
792
+ cat > app/main.py <<'EOF'
793
+ from fastapi import FastAPI, Request
794
+ from fastapi.responses import JSONResponse
795
+ import os
796
+ from datetime import datetime, timezone
797
+
798
+ app = FastAPI(title=os.getenv("APP_NAME", "App"))
799
+
800
+
801
+ @app.get("/health")
802
+ def health():
803
+ return {"status": "ok", "timestamp": datetime.now(timezone.utc).isoformat()}
804
+
805
+
806
+ @app.exception_handler(Exception)
807
+ async def global_exception_handler(request: Request, exc: Exception):
808
+ return JSONResponse(status_code=500, content={"error": "Internal server error"})
809
+ EOF
810
+ FILES_CREATED+=("app/__init__.py" "app/main.py")
811
+
812
+ mkdir -p tests
813
+ cat > tests/__init__.py <<'EOF'
814
+ EOF
815
+ cat > tests/test_health.py <<'EOF'
816
+ from fastapi.testclient import TestClient
817
+ from app.main import app
818
+
819
+ client = TestClient(app)
820
+
821
+
822
+ def test_health():
823
+ response = client.get("/health")
824
+ assert response.status_code == 200
825
+ data = response.json()
826
+ assert data["status"] == "ok"
827
+ assert "timestamp" in data
828
+ EOF
829
+ FILES_CREATED+=("tests/__init__.py" "tests/test_health.py")
830
+
831
+ mkdir -p scripts
832
+ cat > scripts/qa.sh <<'QAEOF'
833
+ #!/usr/bin/env bash
834
+ set -euo pipefail
835
+
836
+ # Activate QA venv if available
837
+ if [[ -d "$HOME/.openclaw/qa-venv" ]]; then
838
+ export PATH="$HOME/.openclaw/qa-venv/bin:$PATH"
839
+ fi
840
+
841
+ echo "=== QA Gate ==="
842
+ FAIL=0
843
+
844
+ echo "--- Ruff lint ---"
845
+ ruff check app/ tests/ 2>&1 || { echo "RUFF FAILED"; FAIL=1; }
846
+
847
+ echo "--- Mypy ---"
848
+ mypy app/ 2>&1 || { echo "MYPY FAILED"; FAIL=1; }
849
+
850
+ echo "--- Tests ---"
851
+ python -m pytest tests/ -v 2>&1 || { echo "TESTS FAILED"; FAIL=1; }
852
+
853
+ echo "--- Coverage (>=80%) ---"
854
+ python -m pytest tests/ -q --cov=app --cov-report=term-missing --cov-fail-under=80 2>&1 || { echo "COVERAGE FAILED"; FAIL=1; }
855
+
856
+ echo "--- Secrets scan ---"
857
+ if grep -rn 'password\s*=\s*"[^"]\+"\|api_key\s*=\s*"[^"]\+"\|secret\s*=\s*"[^"]\+"' --include="*.py" app/ 2>/dev/null; then
858
+ echo "SECRETS FOUND — FAIL"; FAIL=1
859
+ else
860
+ echo "No hardcoded secrets found"
861
+ fi
862
+
863
+ exit $FAIL
864
+ QAEOF
865
+ chmod +x scripts/qa.sh
866
+ FILES_CREATED+=("scripts/qa.sh")
867
+ }
868
+
869
+ # --- Flask ---
870
+ scaffold_flask() {
871
+ scaffold_gitignore_python
872
+ scaffold_env_example "flask"
873
+
874
+ cat > pyproject.toml <<EOF
875
+ [build-system]
876
+ requires = ["setuptools>=75.0.0"]
877
+ build-backend = "setuptools.build_meta"
878
+
879
+ [project]
880
+ name = "$REPO_NAME"
881
+ version = "0.1.0"
882
+ requires-python = ">=3.11"
883
+ dependencies = [
884
+ "flask>=3.1.0",
885
+ "python-dotenv>=1.0.0",
886
+ ]
887
+
888
+ [project.optional-dependencies]
889
+ dev = [
890
+ "pytest>=8.0.0",
891
+ "pytest-cov>=5.0.0",
892
+ "ruff>=0.8.0",
893
+ "mypy>=1.13.0",
894
+ ]
895
+
896
+ [tool.pytest.ini_options]
897
+ testpaths = ["tests"]
898
+ pythonpath = ["."]
899
+
900
+ [tool.ruff]
901
+ target-version = "py311"
902
+ line-length = 120
903
+
904
+ [tool.mypy]
905
+ python_version = "3.11"
906
+ warn_return_any = true
907
+ warn_unused_configs = true
908
+ EOF
909
+ FILES_CREATED+=("pyproject.toml")
910
+
911
+ cat > requirements.txt <<'EOF'
912
+ flask>=3.1.0
913
+ python-dotenv>=1.0.0
914
+ EOF
915
+ FILES_CREATED+=("requirements.txt")
916
+
917
+ mkdir -p app
918
+ cat > app/__init__.py <<'EOF'
919
+ from flask import Flask, jsonify
920
+ import os
921
+
922
+
923
+ def create_app():
924
+ app = Flask(__name__)
925
+ app.config["SECRET_KEY"] = os.environ.get("SECRET_KEY", "change-me")
926
+
927
+ @app.route("/health")
928
+ def health():
929
+ from datetime import datetime, timezone
930
+ return jsonify(status="ok", timestamp=datetime.now(timezone.utc).isoformat())
931
+
932
+ @app.errorhandler(Exception)
933
+ def handle_exception(e):
934
+ return jsonify(error="Internal server error"), 500
935
+
936
+ return app
937
+ EOF
938
+ FILES_CREATED+=("app/__init__.py")
939
+
940
+ mkdir -p tests
941
+ cat > tests/__init__.py <<'EOF'
942
+ EOF
943
+ cat > tests/test_health.py <<'EOF'
944
+ from app import create_app
945
+
946
+
947
+ def test_health():
948
+ app = create_app()
949
+ client = app.test_client()
950
+ response = client.get("/health")
951
+ assert response.status_code == 200
952
+ data = response.get_json()
953
+ assert data["status"] == "ok"
954
+ EOF
955
+ FILES_CREATED+=("tests/__init__.py" "tests/test_health.py")
956
+
957
+ mkdir -p scripts
958
+ cat > scripts/qa.sh <<'QAEOF'
959
+ #!/usr/bin/env bash
960
+ set -euo pipefail
961
+
962
+ if [[ -d "$HOME/.openclaw/qa-venv" ]]; then
963
+ export PATH="$HOME/.openclaw/qa-venv/bin:$PATH"
964
+ fi
965
+
966
+ echo "=== QA Gate ==="
967
+ FAIL=0
968
+
969
+ echo "--- Ruff lint ---"
970
+ ruff check app/ tests/ 2>&1 || { echo "RUFF FAILED"; FAIL=1; }
971
+
972
+ echo "--- Mypy ---"
973
+ mypy app/ 2>&1 || { echo "MYPY FAILED"; FAIL=1; }
974
+
975
+ echo "--- Tests ---"
976
+ python -m pytest tests/ -v 2>&1 || { echo "TESTS FAILED"; FAIL=1; }
977
+
978
+ echo "--- Coverage (>=80%) ---"
979
+ python -m pytest tests/ -q --cov=app --cov-report=term-missing --cov-fail-under=80 2>&1 || { echo "COVERAGE FAILED"; FAIL=1; }
980
+
981
+ echo "--- Secrets scan ---"
982
+ if grep -rn 'password\s*=\s*"[^"]\+"\|api_key\s*=\s*"[^"]\+"\|secret\s*=\s*"[^"]\+"' --include="*.py" app/ 2>/dev/null; then
983
+ echo "SECRETS FOUND — FAIL"; FAIL=1
984
+ else
985
+ echo "No hardcoded secrets found"
986
+ fi
987
+
988
+ exit $FAIL
989
+ QAEOF
990
+ chmod +x scripts/qa.sh
991
+ FILES_CREATED+=("scripts/qa.sh")
992
+ }
993
+
994
+ # --- Django ---
995
+ scaffold_django() {
996
+ scaffold_gitignore_python
997
+ scaffold_env_example "django"
998
+
999
+ cat > pyproject.toml <<EOF
1000
+ [build-system]
1001
+ requires = ["setuptools>=75.0.0"]
1002
+ build-backend = "setuptools.build_meta"
1003
+
1004
+ [project]
1005
+ name = "$REPO_NAME"
1006
+ version = "0.1.0"
1007
+ requires-python = ">=3.11"
1008
+ dependencies = [
1009
+ "django>=5.1.0",
1010
+ "django-environ>=0.11.0",
1011
+ ]
1012
+
1013
+ [project.optional-dependencies]
1014
+ dev = [
1015
+ "pytest>=8.0.0",
1016
+ "pytest-django>=4.9.0",
1017
+ "pytest-cov>=5.0.0",
1018
+ "ruff>=0.8.0",
1019
+ "mypy>=1.13.0",
1020
+ ]
1021
+
1022
+ [tool.pytest.ini_options]
1023
+ testpaths = ["tests"]
1024
+ pythonpath = ["."]
1025
+ DJANGO_SETTINGS_MODULE = "app.settings"
1026
+
1027
+ [tool.ruff]
1028
+ target-version = "py311"
1029
+ line-length = 120
1030
+
1031
+ [tool.mypy]
1032
+ python_version = "3.11"
1033
+ warn_return_any = true
1034
+ warn_unused_configs = true
1035
+ EOF
1036
+ FILES_CREATED+=("pyproject.toml")
1037
+
1038
+ cat > requirements.txt <<'EOF'
1039
+ django>=5.1.0
1040
+ django-environ>=0.11.0
1041
+ EOF
1042
+ FILES_CREATED+=("requirements.txt")
1043
+
1044
+ # Use django-admin to create the project structure
1045
+ mkdir -p app
1046
+ cat > app/__init__.py <<'EOF'
1047
+ EOF
1048
+ cat > app/settings.py <<'EOF'
1049
+ import os
1050
+ import environ
1051
+
1052
+ env = environ.Env(DEBUG=(bool, False))
1053
+ environ.Env.read_env(os.path.join(os.path.dirname(os.path.dirname(__file__)), ".env"))
1054
+
1055
+ SECRET_KEY = env("SECRET_KEY", default="change-me")
1056
+ DEBUG = env("DEBUG")
1057
+ ALLOWED_HOSTS = ["*"]
1058
+ INSTALLED_APPS = [
1059
+ "django.contrib.contenttypes",
1060
+ "django.contrib.auth",
1061
+ ]
1062
+ ROOT_URLCONF = "app.urls"
1063
+ MIDDLEWARE = [
1064
+ "django.middleware.security.SecurityMiddleware",
1065
+ "django.middleware.common.CommonMiddleware",
1066
+ ]
1067
+ DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
1068
+ DATABASES = {
1069
+ "default": {
1070
+ "ENGINE": "django.db.backends.sqlite3",
1071
+ "NAME": os.path.join(os.path.dirname(os.path.dirname(__file__)), "db.sqlite3"),
1072
+ }
1073
+ }
1074
+ EOF
1075
+ FILES_CREATED+=("app/__init__.py" "app/settings.py")
1076
+
1077
+ cat > app/urls.py <<'EOF'
1078
+ from django.urls import path
1079
+ from app.views import health
1080
+
1081
+ urlpatterns = [
1082
+ path("health", health),
1083
+ ]
1084
+ EOF
1085
+ FILES_CREATED+=("app/urls.py")
1086
+
1087
+ cat > app/views.py <<'EOF'
1088
+ from django.http import JsonResponse
1089
+ from datetime import datetime, timezone
1090
+
1091
+
1092
+ def health(request):
1093
+ return JsonResponse({"status": "ok", "timestamp": datetime.now(timezone.utc).isoformat()})
1094
+ EOF
1095
+ FILES_CREATED+=("app/views.py")
1096
+
1097
+ cat > app/wsgi.py <<'EOF'
1098
+ import os
1099
+ from django.core.wsgi import get_wsgi_application
1100
+
1101
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
1102
+ application = get_wsgi_application()
1103
+ EOF
1104
+ FILES_CREATED+=("app/wsgi.py")
1105
+
1106
+ cat > manage.py <<'EOF'
1107
+ #!/usr/bin/env python
1108
+ import os
1109
+ import sys
1110
+
1111
+ def main():
1112
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "app.settings")
1113
+ from django.core.management import execute_from_command_line
1114
+ execute_from_command_line(sys.argv)
1115
+
1116
+ if __name__ == "__main__":
1117
+ main()
1118
+ EOF
1119
+ chmod +x manage.py
1120
+ FILES_CREATED+=("manage.py")
1121
+
1122
+ cat > pytest.ini <<'EOF'
1123
+ [pytest]
1124
+ DJANGO_SETTINGS_MODULE = app.settings
1125
+ EOF
1126
+ FILES_CREATED+=("pytest.ini")
1127
+
1128
+ mkdir -p tests
1129
+ cat > tests/__init__.py <<'EOF'
1130
+ EOF
1131
+ cat > tests/test_health.py <<'EOF'
1132
+ import pytest
1133
+ from django.test import Client
1134
+
1135
+
1136
+ @pytest.mark.django_db
1137
+ def test_health():
1138
+ client = Client()
1139
+ response = client.get("/health")
1140
+ assert response.status_code == 200
1141
+ data = response.json()
1142
+ assert data["status"] == "ok"
1143
+ EOF
1144
+ FILES_CREATED+=("tests/__init__.py" "tests/test_health.py")
1145
+
1146
+ mkdir -p scripts
1147
+ cat > scripts/qa.sh <<'QAEOF'
1148
+ #!/usr/bin/env bash
1149
+ set -euo pipefail
1150
+
1151
+ if [[ -d "$HOME/.openclaw/qa-venv" ]]; then
1152
+ export PATH="$HOME/.openclaw/qa-venv/bin:$PATH"
1153
+ fi
1154
+
1155
+ echo "=== QA Gate ==="
1156
+ FAIL=0
1157
+
1158
+ echo "--- Ruff lint ---"
1159
+ ruff check app/ tests/ 2>&1 || { echo "RUFF FAILED"; FAIL=1; }
1160
+
1161
+ echo "--- Mypy ---"
1162
+ mypy app/ 2>&1 || { echo "MYPY FAILED"; FAIL=1; }
1163
+
1164
+ echo "--- Tests ---"
1165
+ python -m pytest tests/ -v 2>&1 || { echo "TESTS FAILED"; FAIL=1; }
1166
+
1167
+ echo "--- Coverage (>=80%) ---"
1168
+ python -m pytest tests/ -q --cov=app --cov-report=term-missing --cov-fail-under=80 2>&1 || { echo "COVERAGE FAILED"; FAIL=1; }
1169
+
1170
+ echo "--- Secrets scan ---"
1171
+ if grep -rn 'password\s*=\s*"[^"]\+"\|api_key\s*=\s*"[^"]\+"\|secret\s*=\s*"[^"]\+"' --include="*.py" app/ 2>/dev/null; then
1172
+ echo "SECRETS FOUND — FAIL"; FAIL=1
1173
+ else
1174
+ echo "No hardcoded secrets found"
1175
+ fi
1176
+
1177
+ exit $FAIL
1178
+ QAEOF
1179
+ chmod +x scripts/qa.sh
1180
+ FILES_CREATED+=("scripts/qa.sh")
1181
+ }
1182
+
1183
+ # --- Python CLI ---
1184
+ scaffold_python_cli() {
1185
+ scaffold_gitignore_python
1186
+ scaffold_env_example "python-cli"
1187
+
1188
+ local pkg_name
1189
+ pkg_name="$(echo "$REPO_NAME" | tr '-' '_')"
1190
+
1191
+ cat > pyproject.toml <<EOF
1192
+ [build-system]
1193
+ requires = ["setuptools>=75.0.0"]
1194
+ build-backend = "setuptools.build_meta"
1195
+
1196
+ [project]
1197
+ name = "$REPO_NAME"
1198
+ version = "0.1.0"
1199
+ requires-python = ">=3.11"
1200
+ dependencies = []
1201
+
1202
+ [project.optional-dependencies]
1203
+ dev = [
1204
+ "pytest>=8.0.0",
1205
+ "pytest-cov>=5.0.0",
1206
+ "ruff>=0.8.0",
1207
+ "mypy>=1.13.0",
1208
+ ]
1209
+
1210
+ [project.scripts]
1211
+ $REPO_NAME = "${pkg_name}.main:main"
1212
+
1213
+ [tool.setuptools.packages.find]
1214
+ where = ["src"]
1215
+
1216
+ [tool.pytest.ini_options]
1217
+ testpaths = ["tests"]
1218
+ pythonpath = ["."]
1219
+
1220
+ [tool.ruff]
1221
+ target-version = "py311"
1222
+ line-length = 120
1223
+
1224
+ [tool.mypy]
1225
+ python_version = "3.11"
1226
+ warn_return_any = true
1227
+ warn_unused_configs = true
1228
+ EOF
1229
+ FILES_CREATED+=("pyproject.toml")
1230
+
1231
+ mkdir -p "src/${pkg_name}"
1232
+ cat > "src/${pkg_name}/__init__.py" <<'EOF'
1233
+ EOF
1234
+ cat > "src/${pkg_name}/main.py" <<'PYEOF'
1235
+ import argparse
1236
+
1237
+
1238
+ def main() -> None:
1239
+ parser = argparse.ArgumentParser(description="CLI tool")
1240
+ parser.add_argument("--version", action="version", version="%(prog)s 0.1.0")
1241
+ parser.parse_args()
1242
+
1243
+ # TODO: implement CLI logic
1244
+ print("Hello from CLI")
1245
+
1246
+
1247
+ if __name__ == "__main__":
1248
+ main()
1249
+ PYEOF
1250
+ FILES_CREATED+=("src/${pkg_name}/__init__.py" "src/${pkg_name}/main.py")
1251
+
1252
+ mkdir -p tests
1253
+ cat > tests/__init__.py <<'EOF'
1254
+ EOF
1255
+ cat > "tests/test_main.py" <<PYEOF
1256
+ from unittest.mock import patch
1257
+
1258
+ from src.${pkg_name}.main import main
1259
+
1260
+
1261
+ def test_cli_runs(capsys):
1262
+ with patch("sys.argv", ["prog"]):
1263
+ main()
1264
+ captured = capsys.readouterr()
1265
+ assert "Hello from CLI" in captured.out
1266
+
1267
+
1268
+ def test_cli_help(capsys):
1269
+ with patch("sys.argv", ["prog", "--help"]):
1270
+ try:
1271
+ main()
1272
+ except SystemExit as e:
1273
+ assert e.code == 0
1274
+ captured = capsys.readouterr()
1275
+ assert "usage" in captured.out.lower() or "help" in captured.out.lower()
1276
+ PYEOF
1277
+ FILES_CREATED+=("tests/__init__.py" "tests/test_main.py")
1278
+
1279
+ mkdir -p scripts
1280
+ cat > scripts/qa.sh <<'QAEOF'
1281
+ #!/usr/bin/env bash
1282
+ set -euo pipefail
1283
+
1284
+ if [[ -d "$HOME/.openclaw/qa-venv" ]]; then
1285
+ export PATH="$HOME/.openclaw/qa-venv/bin:$PATH"
1286
+ fi
1287
+
1288
+ echo "=== QA Gate ==="
1289
+ FAIL=0
1290
+
1291
+ echo "--- Ruff lint ---"
1292
+ ruff check src/ tests/ 2>&1 || { echo "RUFF FAILED"; FAIL=1; }
1293
+
1294
+ echo "--- Mypy ---"
1295
+ mypy src/ 2>&1 || { echo "MYPY FAILED"; FAIL=1; }
1296
+
1297
+ echo "--- Tests ---"
1298
+ python -m pytest tests/ -v 2>&1 || { echo "TESTS FAILED"; FAIL=1; }
1299
+
1300
+ echo "--- Coverage (>=80%) ---"
1301
+ python -m pytest tests/ -q --cov=src --cov-report=term-missing --cov-fail-under=80 2>&1 || { echo "COVERAGE FAILED"; FAIL=1; }
1302
+
1303
+ echo "--- Secrets scan ---"
1304
+ if grep -rn 'password\s*=\s*"[^"]\+"\|api_key\s*=\s*"[^"]\+"\|secret\s*=\s*"[^"]\+"' --include="*.py" src/ 2>/dev/null; then
1305
+ echo "SECRETS FOUND — FAIL"; FAIL=1
1306
+ else
1307
+ echo "No hardcoded secrets found"
1308
+ fi
1309
+
1310
+ exit $FAIL
1311
+ QAEOF
1312
+ chmod +x scripts/qa.sh
1313
+ FILES_CREATED+=("scripts/qa.sh")
1314
+ }
1315
+
1316
+ # ===================================================================
1317
+ # Run scaffold for detected stack
1318
+ # ===================================================================
1319
+
1320
+ OBJECTIVE="$(echo "$SPEC" | jq -r '.objective // empty')"
1321
+ if [[ -z "$OBJECTIVE" ]]; then
1322
+ OBJECTIVE="${GENESIS_IDEA:-Auto-scaffolded project}"
1323
+ fi
1324
+
1325
+ case "$STACK" in
1326
+ nextjs) scaffold_nextjs ;;
1327
+ express) scaffold_express ;;
1328
+ fastapi) scaffold_fastapi ;;
1329
+ flask) scaffold_flask ;;
1330
+ django) scaffold_django ;;
1331
+ python-cli) scaffold_python_cli ;;
1332
+ *)
1333
+ echo "ERROR: Unknown stack '$STACK'" >&2
1334
+ exit 1
1335
+ ;;
1336
+ esac
1337
+
1338
+ # Common files
1339
+ scaffold_readme "$REPO_NAME" "$STACK" "$OBJECTIVE"
1340
+ scaffold_ci_workflow
1341
+
1342
+ # --- Commit and push ---
1343
+ echo "Committing initial scaffold..." >&2
1344
+ git add -A >&2
1345
+ git commit -m "chore: initial scaffold ($STACK) — Genesis Flow" --allow-empty >&2 || true
1346
+
1347
+ # Ensure main branch
1348
+ git branch -M main 2>/dev/null || true
1349
+
1350
+ echo "Pushing to origin..." >&2
1351
+ if ! git push -u origin main >&2; then
1352
+ echo "ERROR: Push failed — remote repository is not ready for registration" >&2
1353
+ exit 1
1354
+ fi
1355
+
1356
+ # --- Write sideband file ---
1357
+ SCAFFOLD_PAYLOAD="$(jq -n \
1358
+ --arg stack "$STACK" \
1359
+ --arg repo_url "$REPO_URL" \
1360
+ --arg repo_local "$REPO_LOCAL" \
1361
+ --arg slug "$REPO_NAME" \
1362
+ --argjson files "$(printf '%s\n' "${FILES_CREATED[@]}" | jq -R . | jq -s .)" \
1363
+ '{
1364
+ scaffold: {
1365
+ created: true,
1366
+ stack: $stack,
1367
+ repo_url: $repo_url,
1368
+ repo_local: $repo_local,
1369
+ project_slug: $slug,
1370
+ files_created: $files
1371
+ }
1372
+ }')"
1373
+ SIDEBAND="$(genesis_sideband_write "scaffold" "$SESSION_ID" "$SCAFFOLD_PAYLOAD")"
1374
+
1375
+ echo "Sideband written to $SIDEBAND" >&2
1376
+ echo "Scaffold complete: $STACK project at $REPO_LOCAL" >&2
1377
+
1378
+ # --- Output ---
1379
+ echo "$INPUT" | jq \
1380
+ --arg stack "$STACK" \
1381
+ --arg repo_url "$REPO_URL" \
1382
+ --arg repo_local "$REPO_LOCAL" \
1383
+ --arg slug "$REPO_NAME" \
1384
+ --argjson files "$(printf '%s\n' "${FILES_CREATED[@]}" | jq -R . | jq -s .)" \
1385
+ '. + {
1386
+ scaffold: {
1387
+ created: true,
1388
+ stack: $stack,
1389
+ repo_url: $repo_url,
1390
+ repo_local: $repo_local,
1391
+ project_slug: $slug,
1392
+ files_created: $files
1393
+ }
1394
+ }'
1395
+
1396
+ genesis_metric_end "ok"