@samahlstrom/forge-cli 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/README.md +175 -0
- package/bin/forge.js +2 -0
- package/dist/addons/index.d.ts +25 -0
- package/dist/addons/index.js +139 -0
- package/dist/addons/index.js.map +1 -0
- package/dist/commands/add.d.ts +1 -0
- package/dist/commands/add.js +61 -0
- package/dist/commands/add.js.map +1 -0
- package/dist/commands/doctor.d.ts +1 -0
- package/dist/commands/doctor.js +177 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/ingest.d.ts +24 -0
- package/dist/commands/ingest.js +316 -0
- package/dist/commands/ingest.js.map +1 -0
- package/dist/commands/init.d.ts +8 -0
- package/dist/commands/init.js +557 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/remove.d.ts +1 -0
- package/dist/commands/remove.js +42 -0
- package/dist/commands/remove.js.map +1 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +48 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/commands/upgrade.d.ts +5 -0
- package/dist/commands/upgrade.js +190 -0
- package/dist/commands/upgrade.js.map +1 -0
- package/dist/detect/features.d.ts +10 -0
- package/dist/detect/features.js +33 -0
- package/dist/detect/features.js.map +1 -0
- package/dist/detect/go.d.ts +3 -0
- package/dist/detect/go.js +38 -0
- package/dist/detect/go.js.map +1 -0
- package/dist/detect/index.d.ts +25 -0
- package/dist/detect/index.js +32 -0
- package/dist/detect/index.js.map +1 -0
- package/dist/detect/node.d.ts +3 -0
- package/dist/detect/node.js +99 -0
- package/dist/detect/node.js.map +1 -0
- package/dist/detect/python.d.ts +3 -0
- package/dist/detect/python.js +86 -0
- package/dist/detect/python.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +51 -0
- package/dist/index.js.map +1 -0
- package/dist/render/engine.d.ts +8 -0
- package/dist/render/engine.js +71 -0
- package/dist/render/engine.js.map +1 -0
- package/dist/render/merge.d.ts +5 -0
- package/dist/render/merge.js +33 -0
- package/dist/render/merge.js.map +1 -0
- package/dist/utils/fs.d.ts +8 -0
- package/dist/utils/fs.js +42 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/git.d.ts +3 -0
- package/dist/utils/git.js +31 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/hash.d.ts +8 -0
- package/dist/utils/hash.js +22 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/yaml.d.ts +3 -0
- package/dist/utils/yaml.js +12 -0
- package/dist/utils/yaml.js.map +1 -0
- package/package.json +53 -0
- package/templates/addons/beads-dolt-backend/files/dolt-setup.sh +267 -0
- package/templates/addons/beads-dolt-backend/manifest.yaml +13 -0
- package/templates/addons/browser-testing/files/browser-smoke.sh +196 -0
- package/templates/addons/browser-testing/files/visual-qa.md +103 -0
- package/templates/addons/browser-testing/manifest.yaml +20 -0
- package/templates/addons/compliance-hipaa/files/hipaa-checks.sh +184 -0
- package/templates/addons/compliance-hipaa/files/hipaa-context.md +91 -0
- package/templates/addons/compliance-hipaa/manifest.yaml +15 -0
- package/templates/addons/compliance-soc2/files/soc2-checks.sh +232 -0
- package/templates/addons/compliance-soc2/files/soc2-context.md +147 -0
- package/templates/addons/compliance-soc2/manifest.yaml +15 -0
- package/templates/core/CLAUDE.md.hbs +70 -0
- package/templates/core/agents/architect.md.hbs +68 -0
- package/templates/core/agents/backend.md.hbs +27 -0
- package/templates/core/agents/frontend.md.hbs +25 -0
- package/templates/core/agents/quality.md.hbs +40 -0
- package/templates/core/agents/security.md.hbs +53 -0
- package/templates/core/context/project.md.hbs +60 -0
- package/templates/core/forge.yaml.hbs +69 -0
- package/templates/core/hooks/post-edit.sh.hbs +8 -0
- package/templates/core/hooks/pre-edit.sh.hbs +41 -0
- package/templates/core/hooks/session-start.sh.hbs +34 -0
- package/templates/core/pipeline/classify.sh.hbs +159 -0
- package/templates/core/pipeline/decompose.md.hbs +100 -0
- package/templates/core/pipeline/deliver.sh.hbs +171 -0
- package/templates/core/pipeline/execute.md.hbs +138 -0
- package/templates/core/pipeline/intake.sh.hbs +152 -0
- package/templates/core/pipeline/orchestrator.sh.hbs +361 -0
- package/templates/core/pipeline/verify.sh.hbs +160 -0
- package/templates/core/settings.json.hbs +55 -0
- package/templates/core/skill-creator.md.hbs +151 -0
- package/templates/core/skill-deliver.md.hbs +46 -0
- package/templates/core/skill-ingest.md.hbs +245 -0
- package/templates/presets/go/stack.md.hbs +133 -0
- package/templates/presets/python-fastapi/stack.md.hbs +101 -0
- package/templates/presets/react-next-ts/stack.md.hbs +77 -0
- package/templates/presets/sveltekit-ts/stack.md.hbs +116 -0
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# dolt-setup.sh — Initialize Dolt database for bead storage
|
|
5
|
+
# Part of the beads-dolt-backend forge addon
|
|
6
|
+
|
|
7
|
+
DOLT_DIR=".forge/beads/dolt"
|
|
8
|
+
BEAD_STATE_SCRIPT=".forge/bead-state.sh"
|
|
9
|
+
|
|
10
|
+
# --- Helpers ---
|
|
11
|
+
|
|
12
|
+
log() { echo "[dolt-setup] $*"; }
|
|
13
|
+
warn() { echo "[dolt-setup] WARN: $*" >&2; }
|
|
14
|
+
die() { echo "[dolt-setup] ERROR: $*" >&2; exit 1; }
|
|
15
|
+
|
|
16
|
+
# --- Check prerequisites ---
|
|
17
|
+
|
|
18
|
+
log "Checking for Dolt installation..."
|
|
19
|
+
|
|
20
|
+
if ! command -v dolt > /dev/null 2>&1; then
|
|
21
|
+
log "Dolt not found. Attempting to install..."
|
|
22
|
+
|
|
23
|
+
if command -v brew > /dev/null 2>&1; then
|
|
24
|
+
log "Installing via Homebrew..."
|
|
25
|
+
brew install dolt
|
|
26
|
+
elif command -v curl > /dev/null 2>&1; then
|
|
27
|
+
log "Installing via install script..."
|
|
28
|
+
sudo bash -c 'curl -L https://github.com/dolthub/dolt/releases/latest/download/install.sh | bash'
|
|
29
|
+
else
|
|
30
|
+
die "Cannot install Dolt automatically. Install manually: https://docs.dolthub.com/introduction/installation"
|
|
31
|
+
fi
|
|
32
|
+
|
|
33
|
+
# Verify installation
|
|
34
|
+
if ! command -v dolt > /dev/null 2>&1; then
|
|
35
|
+
die "Dolt installation failed. Install manually: https://docs.dolthub.com/introduction/installation"
|
|
36
|
+
fi
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
DOLT_VERSION=$(dolt version 2>/dev/null | head -1)
|
|
40
|
+
log "Found: ${DOLT_VERSION}"
|
|
41
|
+
|
|
42
|
+
# --- Initialize Dolt database ---
|
|
43
|
+
|
|
44
|
+
if [ -d "${DOLT_DIR}/.dolt" ]; then
|
|
45
|
+
log "Dolt database already exists at ${DOLT_DIR}"
|
|
46
|
+
else
|
|
47
|
+
log "Initializing Dolt database at ${DOLT_DIR}..."
|
|
48
|
+
mkdir -p "${DOLT_DIR}"
|
|
49
|
+
|
|
50
|
+
# Configure Dolt user if not already set
|
|
51
|
+
if ! dolt config --global --get user.name > /dev/null 2>&1; then
|
|
52
|
+
GIT_USER=$(git config user.name 2>/dev/null || echo "forge-user")
|
|
53
|
+
GIT_EMAIL=$(git config user.email 2>/dev/null || echo "forge@local")
|
|
54
|
+
dolt config --global --add user.name "${GIT_USER}"
|
|
55
|
+
dolt config --global --add user.email "${GIT_EMAIL}"
|
|
56
|
+
log "Configured Dolt user: ${GIT_USER} <${GIT_EMAIL}>"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
cd "${DOLT_DIR}"
|
|
60
|
+
dolt init
|
|
61
|
+
cd - > /dev/null
|
|
62
|
+
|
|
63
|
+
log "Dolt database initialized"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# --- Create beads table schema ---
|
|
67
|
+
|
|
68
|
+
log "Creating beads table schema..."
|
|
69
|
+
|
|
70
|
+
cd "${DOLT_DIR}"
|
|
71
|
+
|
|
72
|
+
# Create the beads table if it doesn't exist
|
|
73
|
+
dolt sql -q "
|
|
74
|
+
CREATE TABLE IF NOT EXISTS beads (
|
|
75
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
76
|
+
title VARCHAR(255) NOT NULL,
|
|
77
|
+
status VARCHAR(32) NOT NULL DEFAULT 'open',
|
|
78
|
+
branch VARCHAR(128),
|
|
79
|
+
parent_id VARCHAR(64),
|
|
80
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
81
|
+
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
82
|
+
metadata JSON,
|
|
83
|
+
INDEX idx_status (status),
|
|
84
|
+
INDEX idx_branch (branch),
|
|
85
|
+
INDEX idx_parent (parent_id)
|
|
86
|
+
);
|
|
87
|
+
" 2>/dev/null || log " beads table already exists or schema unchanged"
|
|
88
|
+
|
|
89
|
+
# Create the bead_events table for history tracking
|
|
90
|
+
dolt sql -q "
|
|
91
|
+
CREATE TABLE IF NOT EXISTS bead_events (
|
|
92
|
+
id VARCHAR(64) PRIMARY KEY,
|
|
93
|
+
bead_id VARCHAR(64) NOT NULL,
|
|
94
|
+
event_type VARCHAR(32) NOT NULL,
|
|
95
|
+
actor VARCHAR(128),
|
|
96
|
+
timestamp TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
97
|
+
data JSON,
|
|
98
|
+
INDEX idx_bead_id (bead_id),
|
|
99
|
+
INDEX idx_timestamp (timestamp),
|
|
100
|
+
FOREIGN KEY (bead_id) REFERENCES beads(id)
|
|
101
|
+
);
|
|
102
|
+
" 2>/dev/null || log " bead_events table already exists or schema unchanged"
|
|
103
|
+
|
|
104
|
+
# Create the bead_links table for relationships
|
|
105
|
+
dolt sql -q "
|
|
106
|
+
CREATE TABLE IF NOT EXISTS bead_links (
|
|
107
|
+
source_id VARCHAR(64) NOT NULL,
|
|
108
|
+
target_id VARCHAR(64) NOT NULL,
|
|
109
|
+
link_type VARCHAR(32) NOT NULL DEFAULT 'related',
|
|
110
|
+
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
111
|
+
PRIMARY KEY (source_id, target_id, link_type),
|
|
112
|
+
FOREIGN KEY (source_id) REFERENCES beads(id),
|
|
113
|
+
FOREIGN KEY (target_id) REFERENCES beads(id)
|
|
114
|
+
);
|
|
115
|
+
" 2>/dev/null || log " bead_links table already exists or schema unchanged"
|
|
116
|
+
|
|
117
|
+
# Commit the schema
|
|
118
|
+
if dolt status | grep -q "Changes\|new table"; then
|
|
119
|
+
dolt add .
|
|
120
|
+
dolt commit -m "Initialize bead storage schema"
|
|
121
|
+
log "Schema committed to Dolt"
|
|
122
|
+
else
|
|
123
|
+
log "Schema already up to date"
|
|
124
|
+
fi
|
|
125
|
+
|
|
126
|
+
cd - > /dev/null
|
|
127
|
+
|
|
128
|
+
# --- Configure bead-state.sh for Dolt backend ---
|
|
129
|
+
|
|
130
|
+
log "Configuring bead-state.sh to use Dolt backend..."
|
|
131
|
+
|
|
132
|
+
if [ -f "${BEAD_STATE_SCRIPT}" ]; then
|
|
133
|
+
# Check if already configured for dolt
|
|
134
|
+
if grep -q 'BEAD_BACKEND=dolt\|BEAD_BACKEND="dolt"' "${BEAD_STATE_SCRIPT}" 2>/dev/null; then
|
|
135
|
+
log " bead-state.sh already configured for Dolt"
|
|
136
|
+
else
|
|
137
|
+
# Add Dolt backend configuration at the top of the script (after shebang)
|
|
138
|
+
TEMP_FILE=$(mktemp)
|
|
139
|
+
{
|
|
140
|
+
head -1 "${BEAD_STATE_SCRIPT}"
|
|
141
|
+
echo ""
|
|
142
|
+
echo "# --- Dolt backend configuration (added by beads-dolt-backend addon) ---"
|
|
143
|
+
echo "export BEAD_BACKEND=dolt"
|
|
144
|
+
echo "export BEAD_DOLT_DIR=\"${DOLT_DIR}\""
|
|
145
|
+
echo "# --- End Dolt configuration ---"
|
|
146
|
+
echo ""
|
|
147
|
+
tail -n +2 "${BEAD_STATE_SCRIPT}"
|
|
148
|
+
} > "${TEMP_FILE}"
|
|
149
|
+
mv "${TEMP_FILE}" "${BEAD_STATE_SCRIPT}"
|
|
150
|
+
chmod +x "${BEAD_STATE_SCRIPT}"
|
|
151
|
+
log " bead-state.sh updated with Dolt backend config"
|
|
152
|
+
fi
|
|
153
|
+
else
|
|
154
|
+
# Create a minimal bead-state.sh with Dolt backend
|
|
155
|
+
mkdir -p "$(dirname "${BEAD_STATE_SCRIPT}")"
|
|
156
|
+
cat > "${BEAD_STATE_SCRIPT}" <<'BEAD_SCRIPT'
|
|
157
|
+
#!/usr/bin/env bash
|
|
158
|
+
set -euo pipefail
|
|
159
|
+
|
|
160
|
+
# bead-state.sh — Bead state management (Dolt backend)
|
|
161
|
+
# Generated by beads-dolt-backend addon
|
|
162
|
+
|
|
163
|
+
export BEAD_BACKEND=dolt
|
|
164
|
+
BEAD_SCRIPT
|
|
165
|
+
|
|
166
|
+
# Append the DOLT_DIR (not using heredoc to expand variable)
|
|
167
|
+
echo "export BEAD_DOLT_DIR=\"${DOLT_DIR}\"" >> "${BEAD_STATE_SCRIPT}"
|
|
168
|
+
|
|
169
|
+
cat >> "${BEAD_STATE_SCRIPT}" <<'BEAD_SCRIPT'
|
|
170
|
+
|
|
171
|
+
COMMAND="${1:-help}"
|
|
172
|
+
shift || true
|
|
173
|
+
|
|
174
|
+
dolt_query() {
|
|
175
|
+
cd "${BEAD_DOLT_DIR}" && dolt sql -q "$1" -r json 2>/dev/null && cd - > /dev/null
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
dolt_exec() {
|
|
179
|
+
cd "${BEAD_DOLT_DIR}" && dolt sql -q "$1" 2>/dev/null && cd - > /dev/null
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
dolt_commit() {
|
|
183
|
+
cd "${BEAD_DOLT_DIR}" && dolt add . && dolt commit -m "$1" 2>/dev/null && cd - > /dev/null
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
case "$COMMAND" in
|
|
187
|
+
list)
|
|
188
|
+
dolt_query "SELECT id, title, status, branch, created_at FROM beads ORDER BY updated_at DESC"
|
|
189
|
+
;;
|
|
190
|
+
get)
|
|
191
|
+
[ -z "${1:-}" ] && echo "Usage: bead-state.sh get <id>" && exit 1
|
|
192
|
+
dolt_query "SELECT * FROM beads WHERE id = '$1'"
|
|
193
|
+
;;
|
|
194
|
+
create)
|
|
195
|
+
ID="${1:-$(uuidgen | tr '[:upper:]' '[:lower:]')}"
|
|
196
|
+
TITLE="${2:-Untitled bead}"
|
|
197
|
+
BRANCH="${3:-$(git branch --show-current 2>/dev/null || echo 'none')}"
|
|
198
|
+
dolt_exec "INSERT INTO beads (id, title, status, branch) VALUES ('${ID}', '${TITLE}', 'open', '${BRANCH}')"
|
|
199
|
+
dolt_exec "INSERT INTO bead_events (id, bead_id, event_type, actor) VALUES ('$(uuidgen | tr '[:upper:]' '[:lower:]')', '${ID}', 'created', '$(whoami)')"
|
|
200
|
+
dolt_commit "Create bead: ${TITLE}"
|
|
201
|
+
echo "${ID}"
|
|
202
|
+
;;
|
|
203
|
+
update)
|
|
204
|
+
[ -z "${1:-}" ] || [ -z "${2:-}" ] && echo "Usage: bead-state.sh update <id> <status>" && exit 1
|
|
205
|
+
dolt_exec "UPDATE beads SET status = '$2', updated_at = CURRENT_TIMESTAMP WHERE id = '$1'"
|
|
206
|
+
dolt_exec "INSERT INTO bead_events (id, bead_id, event_type, actor, data) VALUES ('$(uuidgen | tr '[:upper:]' '[:lower:]')', '$1', 'status_change', '$(whoami)', '{\"status\":\"$2\"}')"
|
|
207
|
+
dolt_commit "Update bead $1: status -> $2"
|
|
208
|
+
;;
|
|
209
|
+
history)
|
|
210
|
+
[ -z "${1:-}" ] && echo "Usage: bead-state.sh history <id>" && exit 1
|
|
211
|
+
dolt_query "SELECT * FROM bead_events WHERE bead_id = '$1' ORDER BY timestamp DESC"
|
|
212
|
+
;;
|
|
213
|
+
diff)
|
|
214
|
+
cd "${BEAD_DOLT_DIR}" && dolt diff "${1:-HEAD~1}" && cd - > /dev/null
|
|
215
|
+
;;
|
|
216
|
+
sync)
|
|
217
|
+
cd "${BEAD_DOLT_DIR}"
|
|
218
|
+
if dolt remote -v 2>/dev/null | grep -q origin; then
|
|
219
|
+
dolt push origin main 2>/dev/null || echo "Push failed — configure remote with: cd ${BEAD_DOLT_DIR} && dolt remote add origin <url>"
|
|
220
|
+
else
|
|
221
|
+
echo "No remote configured. Add one with: cd ${BEAD_DOLT_DIR} && dolt remote add origin <url>"
|
|
222
|
+
fi
|
|
223
|
+
cd - > /dev/null
|
|
224
|
+
;;
|
|
225
|
+
help|*)
|
|
226
|
+
echo "Usage: bead-state.sh <command> [args]"
|
|
227
|
+
echo ""
|
|
228
|
+
echo "Commands:"
|
|
229
|
+
echo " list List all beads"
|
|
230
|
+
echo " get <id> Get bead details"
|
|
231
|
+
echo " create [id] [title] [branch] Create a new bead"
|
|
232
|
+
echo " update <id> <status> Update bead status"
|
|
233
|
+
echo " history <id> Show bead event history"
|
|
234
|
+
echo " diff [ref] Show Dolt diff (versioned changes)"
|
|
235
|
+
echo " sync Push to remote Dolt (if configured)"
|
|
236
|
+
;;
|
|
237
|
+
esac
|
|
238
|
+
BEAD_SCRIPT
|
|
239
|
+
|
|
240
|
+
chmod +x "${BEAD_STATE_SCRIPT}"
|
|
241
|
+
log " Created ${BEAD_STATE_SCRIPT} with Dolt backend"
|
|
242
|
+
fi
|
|
243
|
+
|
|
244
|
+
# --- Add to .gitignore ---
|
|
245
|
+
|
|
246
|
+
GITIGNORE=".gitignore"
|
|
247
|
+
if [ -f "$GITIGNORE" ]; then
|
|
248
|
+
if ! grep -q '.forge/beads/dolt' "$GITIGNORE" 2>/dev/null; then
|
|
249
|
+
echo "" >> "$GITIGNORE"
|
|
250
|
+
echo "# Dolt bead database (local state)" >> "$GITIGNORE"
|
|
251
|
+
echo ".forge/beads/dolt/" >> "$GITIGNORE"
|
|
252
|
+
log "Added .forge/beads/dolt/ to .gitignore"
|
|
253
|
+
fi
|
|
254
|
+
fi
|
|
255
|
+
|
|
256
|
+
# --- Done ---
|
|
257
|
+
|
|
258
|
+
log "Setup complete. Dolt bead backend is ready."
|
|
259
|
+
log ""
|
|
260
|
+
log "Quick start:"
|
|
261
|
+
log " bash ${BEAD_STATE_SCRIPT} create '' 'My first bead'"
|
|
262
|
+
log " bash ${BEAD_STATE_SCRIPT} list"
|
|
263
|
+
log " bash ${BEAD_STATE_SCRIPT} history <id>"
|
|
264
|
+
log ""
|
|
265
|
+
log "For team sync, configure a DoltHub remote:"
|
|
266
|
+
log " cd ${DOLT_DIR} && dolt remote add origin <dolthub-url>"
|
|
267
|
+
log " bash ${BEAD_STATE_SCRIPT} sync"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
name: beads-dolt-backend
|
|
2
|
+
description: "Upgrade bead storage to Dolt DB for versioned state and team sync"
|
|
3
|
+
version: 1
|
|
4
|
+
patches:
|
|
5
|
+
forge_yaml:
|
|
6
|
+
beads.backend: dolt
|
|
7
|
+
addons:
|
|
8
|
+
- "+beads-dolt-backend"
|
|
9
|
+
files:
|
|
10
|
+
- source: dolt-setup.sh
|
|
11
|
+
target: .forge/addons/dolt-setup.sh
|
|
12
|
+
post_install:
|
|
13
|
+
- "bash .forge/addons/dolt-setup.sh"
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# browser-smoke.sh — Run Playwright visual smoke tests at mobile + desktop viewports
|
|
5
|
+
# Part of the browser-testing forge addon
|
|
6
|
+
|
|
7
|
+
SCREENSHOT_DIR=".forge/state/screenshots"
|
|
8
|
+
DEV_PORT="${DEV_PORT:-5173}"
|
|
9
|
+
DEV_URL="http://localhost:${DEV_PORT}"
|
|
10
|
+
TIMEOUT=30
|
|
11
|
+
STARTED_SERVER=false
|
|
12
|
+
PASS=0
|
|
13
|
+
FAIL=0
|
|
14
|
+
ERRORS=()
|
|
15
|
+
|
|
16
|
+
mkdir -p "${SCREENSHOT_DIR}/mobile" "${SCREENSHOT_DIR}/desktop"
|
|
17
|
+
|
|
18
|
+
# --- Helpers ---
|
|
19
|
+
|
|
20
|
+
log() { echo "[browser-smoke] $*"; }
|
|
21
|
+
warn() { echo "[browser-smoke] WARN: $*" >&2; }
|
|
22
|
+
fail() { echo "[browser-smoke] FAIL: $*" >&2; ERRORS+=("$*"); FAIL=$((FAIL + 1)); }
|
|
23
|
+
pass() { log "PASS: $*"; PASS=$((PASS + 1)); }
|
|
24
|
+
|
|
25
|
+
cleanup() {
|
|
26
|
+
if [ "$STARTED_SERVER" = true ]; then
|
|
27
|
+
log "Stopping dev server (PID ${DEV_PID:-unknown})..."
|
|
28
|
+
kill "$DEV_PID" 2>/dev/null || true
|
|
29
|
+
wait "$DEV_PID" 2>/dev/null || true
|
|
30
|
+
fi
|
|
31
|
+
}
|
|
32
|
+
trap cleanup EXIT
|
|
33
|
+
|
|
34
|
+
# --- Check Playwright ---
|
|
35
|
+
|
|
36
|
+
if ! npx playwright --version > /dev/null 2>&1; then
|
|
37
|
+
log "Playwright not found. Installing chromium..."
|
|
38
|
+
npx playwright install chromium || {
|
|
39
|
+
fail "Could not install Playwright. Aborting."
|
|
40
|
+
exit 1
|
|
41
|
+
}
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
# --- Dev Server ---
|
|
45
|
+
|
|
46
|
+
check_server() {
|
|
47
|
+
curl -sf --max-time 3 "${DEV_URL}" > /dev/null 2>&1
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if check_server; then
|
|
51
|
+
log "Dev server already running at ${DEV_URL}"
|
|
52
|
+
else
|
|
53
|
+
log "Starting dev server..."
|
|
54
|
+
npm run dev > /tmp/forge-dev-server.log 2>&1 &
|
|
55
|
+
DEV_PID=$!
|
|
56
|
+
STARTED_SERVER=true
|
|
57
|
+
|
|
58
|
+
# Wait for server to be ready
|
|
59
|
+
for i in $(seq 1 "$TIMEOUT"); do
|
|
60
|
+
if check_server; then
|
|
61
|
+
log "Dev server ready after ${i}s"
|
|
62
|
+
break
|
|
63
|
+
fi
|
|
64
|
+
if ! kill -0 "$DEV_PID" 2>/dev/null; then
|
|
65
|
+
fail "Dev server process exited unexpectedly. Check /tmp/forge-dev-server.log"
|
|
66
|
+
echo '{"pass":0,"fail":1,"errors":["Dev server failed to start"]}' > "${SCREENSHOT_DIR}/results.json"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
sleep 1
|
|
70
|
+
done
|
|
71
|
+
|
|
72
|
+
if ! check_server; then
|
|
73
|
+
fail "Dev server did not respond within ${TIMEOUT}s"
|
|
74
|
+
echo '{"pass":0,"fail":1,"errors":["Dev server timeout"]}' > "${SCREENSHOT_DIR}/results.json"
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# --- Determine pages to test ---
|
|
80
|
+
|
|
81
|
+
PAGES=("/")
|
|
82
|
+
|
|
83
|
+
# Add pages from changed route files if in a git repo
|
|
84
|
+
if git rev-parse --is-inside-work-tree > /dev/null 2>&1; then
|
|
85
|
+
while IFS= read -r file; do
|
|
86
|
+
# Extract route path from SvelteKit-style route files
|
|
87
|
+
if [[ "$file" =~ src/routes/(.+)/\+page ]]; then
|
|
88
|
+
route="/${BASH_REMATCH[1]}"
|
|
89
|
+
# Skip parameterized routes (contain [...] or [[...]])
|
|
90
|
+
if [[ ! "$route" =~ \[ ]]; then
|
|
91
|
+
PAGES+=("$route")
|
|
92
|
+
fi
|
|
93
|
+
fi
|
|
94
|
+
done < <(git diff --name-only HEAD~1 2>/dev/null || true)
|
|
95
|
+
fi
|
|
96
|
+
|
|
97
|
+
# Deduplicate
|
|
98
|
+
PAGES=($(printf '%s\n' "${PAGES[@]}" | sort -u))
|
|
99
|
+
|
|
100
|
+
log "Testing ${#PAGES[@]} page(s): ${PAGES[*]}"
|
|
101
|
+
|
|
102
|
+
# --- Run tests ---
|
|
103
|
+
|
|
104
|
+
run_viewport_test() {
|
|
105
|
+
local page="$1"
|
|
106
|
+
local label="$2"
|
|
107
|
+
local width="$3"
|
|
108
|
+
local height="$4"
|
|
109
|
+
|
|
110
|
+
local safe_name
|
|
111
|
+
safe_name=$(echo "$page" | sed 's|/|_|g; s|^_||')
|
|
112
|
+
[ -z "$safe_name" ] && safe_name="home"
|
|
113
|
+
|
|
114
|
+
local screenshot_path="${SCREENSHOT_DIR}/${label}/${safe_name}.png"
|
|
115
|
+
|
|
116
|
+
log "Testing ${page} at ${label} (${width}x${height})..."
|
|
117
|
+
|
|
118
|
+
local script="
|
|
119
|
+
const { chromium } = require('playwright');
|
|
120
|
+
(async () => {
|
|
121
|
+
const browser = await chromium.launch();
|
|
122
|
+
const context = await browser.newContext({
|
|
123
|
+
viewport: { width: ${width}, height: ${height} }
|
|
124
|
+
});
|
|
125
|
+
const page = await context.newPage();
|
|
126
|
+
try {
|
|
127
|
+
const response = await page.goto('${DEV_URL}${page}', {
|
|
128
|
+
waitUntil: 'networkidle',
|
|
129
|
+
timeout: ${TIMEOUT}000
|
|
130
|
+
});
|
|
131
|
+
const status = response ? response.status() : 0;
|
|
132
|
+
await page.screenshot({ path: '${screenshot_path}', fullPage: true });
|
|
133
|
+
if (status >= 400) {
|
|
134
|
+
console.error('HTTP ' + status);
|
|
135
|
+
process.exit(2);
|
|
136
|
+
}
|
|
137
|
+
// Check for horizontal overflow (mobile)
|
|
138
|
+
if (${width} <= 768) {
|
|
139
|
+
const hasOverflow = await page.evaluate(() => {
|
|
140
|
+
return document.documentElement.scrollWidth > document.documentElement.clientWidth;
|
|
141
|
+
});
|
|
142
|
+
if (hasOverflow) {
|
|
143
|
+
console.error('OVERFLOW');
|
|
144
|
+
process.exit(3);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
process.exit(0);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
console.error(err.message);
|
|
150
|
+
try { await page.screenshot({ path: '${screenshot_path}', fullPage: true }); } catch (_) {}
|
|
151
|
+
process.exit(1);
|
|
152
|
+
} finally {
|
|
153
|
+
await browser.close();
|
|
154
|
+
}
|
|
155
|
+
})();
|
|
156
|
+
"
|
|
157
|
+
|
|
158
|
+
local exit_code=0
|
|
159
|
+
node -e "$script" 2>/tmp/forge-browser-err.txt || exit_code=$?
|
|
160
|
+
|
|
161
|
+
case $exit_code in
|
|
162
|
+
0) pass "${page} @ ${label}" ;;
|
|
163
|
+
2)
|
|
164
|
+
local status
|
|
165
|
+
status=$(cat /tmp/forge-browser-err.txt)
|
|
166
|
+
fail "${page} @ ${label}: HTTP error (${status})"
|
|
167
|
+
;;
|
|
168
|
+
3) fail "${page} @ ${label}: horizontal overflow detected on mobile" ;;
|
|
169
|
+
*) fail "${page} @ ${label}: $(cat /tmp/forge-browser-err.txt)" ;;
|
|
170
|
+
esac
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
for page in "${PAGES[@]}"; do
|
|
174
|
+
run_viewport_test "$page" "mobile" 375 812
|
|
175
|
+
run_viewport_test "$page" "desktop" 1440 900
|
|
176
|
+
done
|
|
177
|
+
|
|
178
|
+
# --- Report ---
|
|
179
|
+
|
|
180
|
+
TOTAL=$((PASS + FAIL))
|
|
181
|
+
|
|
182
|
+
cat > "${SCREENSHOT_DIR}/results.json" <<EOF
|
|
183
|
+
{
|
|
184
|
+
"total": ${TOTAL},
|
|
185
|
+
"pass": ${PASS},
|
|
186
|
+
"fail": ${FAIL},
|
|
187
|
+
"pages": ${#PAGES[@]},
|
|
188
|
+
"errors": [$(printf '"%s",' "${ERRORS[@]}" 2>/dev/null | sed 's/,$//')]
|
|
189
|
+
}
|
|
190
|
+
EOF
|
|
191
|
+
|
|
192
|
+
log "Results: ${PASS}/${TOTAL} passed, ${FAIL} failed"
|
|
193
|
+
log "Screenshots saved to ${SCREENSHOT_DIR}/"
|
|
194
|
+
log "Results written to ${SCREENSHOT_DIR}/results.json"
|
|
195
|
+
|
|
196
|
+
[ "$FAIL" -eq 0 ] && exit 0 || exit 1
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Visual QA Agent — Browser Testing Addon
|
|
2
|
+
|
|
3
|
+
You are a visual QA agent. Your job is to verify UI changes at two viewport sizes using Playwright.
|
|
4
|
+
|
|
5
|
+
## Viewports
|
|
6
|
+
|
|
7
|
+
| Label | Width | Height |
|
|
8
|
+
|---------|--------|--------|
|
|
9
|
+
| mobile | 375px | 812px |
|
|
10
|
+
| desktop | 1440px | 900px |
|
|
11
|
+
|
|
12
|
+
## Workflow
|
|
13
|
+
|
|
14
|
+
### 1. Start the Dev Server
|
|
15
|
+
|
|
16
|
+
Check if the dev server is already running:
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
curl -sf http://localhost:5173 > /dev/null 2>&1 || npm run dev &
|
|
20
|
+
sleep 3
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
If the project uses a different port, check `forge.yaml` or `package.json` for the correct `dev` command and port.
|
|
24
|
+
|
|
25
|
+
### 2. Identify Pages to Test
|
|
26
|
+
|
|
27
|
+
Read the current changeset (`git diff --name-only HEAD~1`) and determine which routes/pages are affected. Map changed files to their corresponding URLs. At minimum, always test:
|
|
28
|
+
|
|
29
|
+
- `/` (home page)
|
|
30
|
+
- Any route whose component files were modified
|
|
31
|
+
|
|
32
|
+
### 3. Take Before Screenshots (Baseline)
|
|
33
|
+
|
|
34
|
+
If a baseline branch is available (e.g., `main`), check it out temporarily and capture screenshots:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
.forge/state/screenshots/before/<page>-mobile.png
|
|
38
|
+
.forge/state/screenshots/before/<page>-desktop.png
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Switch back to the working branch after capturing baselines.
|
|
42
|
+
|
|
43
|
+
### 4. Take After Screenshots
|
|
44
|
+
|
|
45
|
+
Capture the current state at both viewports:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
.forge/state/screenshots/after/<page>-mobile.png
|
|
49
|
+
.forge/state/screenshots/after/<page>-desktop.png
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
### 5. Visual Regression Check
|
|
53
|
+
|
|
54
|
+
Compare before/after screenshots. Flag issues:
|
|
55
|
+
|
|
56
|
+
- **Layout shifts**: Elements that moved unexpectedly between viewports
|
|
57
|
+
- **Overflow**: Content that bleeds outside its container, especially on mobile
|
|
58
|
+
- **Truncation**: Text cut off without ellipsis or scroll
|
|
59
|
+
- **Z-index conflicts**: Overlapping elements that shouldn't overlap
|
|
60
|
+
- **Missing responsive breakpoints**: Desktop layout appearing on mobile
|
|
61
|
+
- **Touch target size**: Interactive elements smaller than 44x44px on mobile
|
|
62
|
+
|
|
63
|
+
### 6. Responsive Layout Verification
|
|
64
|
+
|
|
65
|
+
For each page, verify at both viewports:
|
|
66
|
+
|
|
67
|
+
- Navigation is accessible (hamburger menu on mobile, full nav on desktop)
|
|
68
|
+
- Images scale proportionally without distortion
|
|
69
|
+
- Forms are usable (inputs are full-width on mobile, appropriately sized on desktop)
|
|
70
|
+
- Modals/dialogs fit within the viewport
|
|
71
|
+
- Scroll behavior is correct (no horizontal scroll on mobile)
|
|
72
|
+
- Font sizes are readable (minimum 14px on mobile)
|
|
73
|
+
|
|
74
|
+
### 7. Report Results
|
|
75
|
+
|
|
76
|
+
Write a summary to `.forge/state/screenshots/report.md` with:
|
|
77
|
+
|
|
78
|
+
- Pass/fail per page per viewport
|
|
79
|
+
- Screenshot paths for any failures
|
|
80
|
+
- Specific observations about visual issues
|
|
81
|
+
- Recommendations for fixes
|
|
82
|
+
|
|
83
|
+
## Screenshot Storage
|
|
84
|
+
|
|
85
|
+
All screenshots go in `.forge/state/screenshots/`:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
.forge/state/screenshots/
|
|
89
|
+
before/
|
|
90
|
+
home-mobile.png
|
|
91
|
+
home-desktop.png
|
|
92
|
+
after/
|
|
93
|
+
home-mobile.png
|
|
94
|
+
home-desktop.png
|
|
95
|
+
report.md
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## Error Handling
|
|
99
|
+
|
|
100
|
+
- If the dev server fails to start, report the error and skip browser tests
|
|
101
|
+
- If a page returns 404/500, capture the error page screenshot and flag it
|
|
102
|
+
- If Playwright is not installed, run `npx playwright install chromium` first
|
|
103
|
+
- Timeout after 30 seconds per page navigation
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
name: browser-testing
|
|
2
|
+
description: "Playwright visual QA at mobile + desktop viewports"
|
|
3
|
+
version: 1
|
|
4
|
+
requires:
|
|
5
|
+
commands:
|
|
6
|
+
dev: true
|
|
7
|
+
patches:
|
|
8
|
+
forge_yaml:
|
|
9
|
+
verification.browser.enabled: true
|
|
10
|
+
addons:
|
|
11
|
+
- "+browser-testing"
|
|
12
|
+
agents:
|
|
13
|
+
- "+qa-visual"
|
|
14
|
+
files:
|
|
15
|
+
- source: visual-qa.md
|
|
16
|
+
target: .forge/addons/visual-qa.md
|
|
17
|
+
- source: browser-smoke.sh
|
|
18
|
+
target: .forge/addons/browser-smoke.sh
|
|
19
|
+
post_install:
|
|
20
|
+
- "npx playwright install chromium"
|