@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.
- package/ARCHITECTURE.md +87 -0
- package/LICENSE +21 -0
- package/README.md +289 -0
- package/defaults/AGENTS.md +150 -0
- package/defaults/HEARTBEAT.md +3 -0
- package/defaults/IDENTITY.md +6 -0
- package/defaults/SOUL.md +39 -0
- package/defaults/TOOLS.md +15 -0
- package/defaults/fabrica/prompts/architect.md +147 -0
- package/defaults/fabrica/prompts/developer.md +211 -0
- package/defaults/fabrica/prompts/reviewer.md +114 -0
- package/defaults/fabrica/prompts/security-checklist.md +58 -0
- package/defaults/fabrica/prompts/tester.md +150 -0
- package/defaults/fabrica/workflow.yaml +184 -0
- package/dist/index.js +143075 -0
- package/dist/index.js.map +7 -0
- package/dist/lib/worker.cjs +214 -0
- package/dist/worker.cjs +4754 -0
- package/fabrica.manifest.json +24 -0
- package/genesis/configs/classification-rules.json +32 -0
- package/genesis/configs/interview-templates.json +73 -0
- package/genesis/configs/labels.json +202 -0
- package/genesis/configs/triage-matrix.json +39 -0
- package/genesis/scripts/classify-idea.sh +161 -0
- package/genesis/scripts/conduct-interview.sh +199 -0
- package/genesis/scripts/create-task.sh +797 -0
- package/genesis/scripts/delivery-target-lib.sh +88 -0
- package/genesis/scripts/generate-qa-contract.sh +188 -0
- package/genesis/scripts/generate-spec.sh +171 -0
- package/genesis/scripts/genesis-telemetry.sh +97 -0
- package/genesis/scripts/genesis-utils.sh +617 -0
- package/genesis/scripts/impact-analysis.sh +135 -0
- package/genesis/scripts/interview.sh +98 -0
- package/genesis/scripts/map-project.sh +309 -0
- package/genesis/scripts/receive-idea.sh +69 -0
- package/genesis/scripts/register-project.sh +520 -0
- package/genesis/scripts/research-idea.sh +84 -0
- package/genesis/scripts/scaffold-project.sh +1396 -0
- package/genesis/scripts/security-review.sh +141 -0
- package/genesis/scripts/sideband-lib.sh +243 -0
- package/genesis/scripts/stack-detection-lib.sh +130 -0
- package/genesis/scripts/triage.sh +598 -0
- package/genesis/scripts/validate-step.sh +81 -0
- package/openclaw.plugin.json +45 -0
- 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"
|