@massu/core 1.2.1 → 1.4.0-soak.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 +40 -0
- package/commands/README.md +137 -0
- package/commands/massu-deploy.python-docker.md +170 -0
- package/commands/massu-deploy.python-fly.md +189 -0
- package/commands/massu-deploy.python-launchd.md +144 -0
- package/commands/massu-deploy.python-systemd.md +163 -0
- package/commands/massu-deploy.python.md +200 -0
- package/commands/massu-scaffold-page.md +172 -59
- package/commands/massu-scaffold-page.swift.md +121 -0
- package/commands/massu-scaffold-router.python-django.md +153 -0
- package/commands/massu-scaffold-router.python-fastapi.md +145 -0
- package/commands/massu-scaffold-router.python.md +143 -0
- package/dist/cli.js +10170 -4138
- package/dist/hooks/auto-learning-pipeline.js +44 -6
- package/dist/hooks/classify-failure.js +44 -6
- package/dist/hooks/cost-tracker.js +44 -6
- package/dist/hooks/fix-detector.js +44 -6
- package/dist/hooks/incident-pipeline.js +44 -6
- package/dist/hooks/post-edit-context.js +44 -6
- package/dist/hooks/post-tool-use.js +44 -6
- package/dist/hooks/pre-compact.js +44 -6
- package/dist/hooks/pre-delete-check.js +44 -6
- package/dist/hooks/quality-event.js +44 -6
- package/dist/hooks/rule-enforcement-pipeline.js +44 -6
- package/dist/hooks/session-end.js +44 -6
- package/dist/hooks/session-start.js +4789 -410
- package/dist/hooks/user-prompt.js +44 -6
- package/package.json +10 -4
- package/src/cli.ts +28 -2
- package/src/commands/config-refresh.ts +88 -20
- package/src/commands/init.ts +130 -23
- package/src/commands/install-commands.ts +482 -42
- package/src/commands/refresh-log.ts +37 -0
- package/src/commands/show-template.ts +65 -0
- package/src/commands/template-engine.ts +262 -0
- package/src/commands/watch.ts +430 -0
- package/src/config.ts +69 -3
- package/src/detect/adapters/nextjs-trpc.ts +166 -0
- package/src/detect/adapters/parse-guard.ts +133 -0
- package/src/detect/adapters/python-django.ts +208 -0
- package/src/detect/adapters/python-fastapi.ts +223 -0
- package/src/detect/adapters/query-helpers.ts +170 -0
- package/src/detect/adapters/runner.ts +252 -0
- package/src/detect/adapters/swift-swiftui.ts +171 -0
- package/src/detect/adapters/tree-sitter-loader.ts +348 -0
- package/src/detect/adapters/types.ts +174 -0
- package/src/detect/codebase-introspector.ts +190 -0
- package/src/detect/index.ts +28 -2
- package/src/detect/regex-fallback.ts +449 -0
- package/src/hooks/session-start.ts +94 -3
- package/src/lib/gitToplevel.ts +22 -0
- package/src/lib/installLock.ts +179 -0
- package/src/lib/pidLiveness.ts +67 -0
- package/src/lsp/auto-detect.ts +89 -0
- package/src/lsp/client.ts +590 -0
- package/src/lsp/enrich.ts +127 -0
- package/src/lsp/types.ts +221 -0
- package/src/watch/daemon.ts +385 -0
- package/src/watch/lockfile-detector.ts +65 -0
- package/src/watch/paths.ts +279 -0
- package/src/watch/state.ts +178 -0
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a Python service supervised by systemd (Linux) — restart the user unit, poll health, tail journalctl, diff before/after, rollback if unhealthy"
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Massu Deploy: Python Service — systemd (Linux)
|
|
8
|
+
|
|
9
|
+
Restarts a Python service running under a `systemd --user` unit on Linux. Use this variant when your `massu.config.yaml` declares `config.python.service_label` and your host is Linux.
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-push -> /massu-deploy (systemd variant)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
This command restarts a production service. **If the service handles financial, transactional, or otherwise consequential state, real data is at risk** — pre-flight checks are mandatory.
|
|
18
|
+
|
|
19
|
+
---
|
|
20
|
+
|
|
21
|
+
## NON-NEGOTIABLE RULES
|
|
22
|
+
|
|
23
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
24
|
+
- **Never deploy with failing tests** — test suite must be green before this runs
|
|
25
|
+
- **Always restart-and-probe** — file-saved ≠ process-running-the-fix
|
|
26
|
+
- **Never kill processes without identifying them first** — confirm the unit name before sending any signal
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## Pre-flight
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# 1. Branch + working tree clean
|
|
34
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
35
|
+
|
|
36
|
+
# 2. Tests green
|
|
37
|
+
pytest -x 2>&1 | tail -10
|
|
38
|
+
|
|
39
|
+
# 3. Confirm the unit is active
|
|
40
|
+
systemctl --user status {{config.python.service_label | default("<service-label>")}} --no-pager | head -20
|
|
41
|
+
|
|
42
|
+
# 4. Capture current health for diff
|
|
43
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
44
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-before.json
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Approval Gate
|
|
50
|
+
|
|
51
|
+
```
|
|
52
|
+
===============================================================================
|
|
53
|
+
APPROVAL REQUIRED — SYSTEMD RESTART
|
|
54
|
+
===============================================================================
|
|
55
|
+
|
|
56
|
+
Service unit : {{config.python.service_label | default("<service-label>")}}
|
|
57
|
+
Supervisor : systemd --user (Linux)
|
|
58
|
+
Pre-flight : PASS
|
|
59
|
+
|
|
60
|
+
This will:
|
|
61
|
+
1. systemctl --user restart {{config.python.service_label | default("<service-label>")}}
|
|
62
|
+
2. Poll /health every 2s (max 60s) until 200
|
|
63
|
+
3. Smoke /health + any critical endpoint
|
|
64
|
+
4. Diff before/after health JSON
|
|
65
|
+
5. On failure: print rollback command
|
|
66
|
+
|
|
67
|
+
Reply "approve" or "abort".
|
|
68
|
+
===============================================================================
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## Restart
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
systemctl --user restart {{config.python.service_label | default("<service-label>")}}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
If the unit is system-level (not user-level), drop `--user` and run with `sudo`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## Poll Health
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
for i in $(seq 1 30); do
|
|
87
|
+
sleep 2
|
|
88
|
+
status=$(curl -sS -o /dev/null -w "%{http_code}" http://localhost:8000/health || echo "000")
|
|
89
|
+
[ "$status" = "200" ] && { echo "READY after ${i} polls"; break; }
|
|
90
|
+
echo "poll ${i}: ${status}"
|
|
91
|
+
done
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Smoke + Diff
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
curl -sS http://localhost:8000/health | python3 -m json.tool \
|
|
100
|
+
> /tmp/{{config.python.service_label | default("service")}}-health-after.json
|
|
101
|
+
|
|
102
|
+
diff \
|
|
103
|
+
/tmp/{{config.python.service_label | default("service")}}-health-before.json \
|
|
104
|
+
/tmp/{{config.python.service_label | default("service")}}-health-after.json \
|
|
105
|
+
| head -50
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Tail Startup Logs
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
# Recent errors from the unit (2-minute window)
|
|
114
|
+
journalctl --user -u {{config.python.service_label | default("<service-label>")}} \
|
|
115
|
+
--since "2 minutes ago" --no-pager | grep -iE "error|warn" | head -20
|
|
116
|
+
|
|
117
|
+
# Follow live (Ctrl-C to stop)
|
|
118
|
+
journalctl --user -u {{config.python.service_label | default("<service-label>")}} -f --no-pager
|
|
119
|
+
|
|
120
|
+
# Full boot for this unit
|
|
121
|
+
journalctl --user -u {{config.python.service_label | default("<service-label>")}} \
|
|
122
|
+
-b --no-pager | tail -100
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Rollback
|
|
128
|
+
|
|
129
|
+
If `/health` does not return 200 within 60s, or any smoke check fails:
|
|
130
|
+
|
|
131
|
+
```bash
|
|
132
|
+
git revert HEAD --no-edit
|
|
133
|
+
systemctl --user restart {{config.python.service_label | default("<service-label>")}}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Then page yourself or your on-call — an unhealthy production service is an incident.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## Useful systemd Diagnostics
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# Check if the unit file needs a daemon-reload after editing the .service file
|
|
144
|
+
systemctl --user daemon-reload
|
|
145
|
+
|
|
146
|
+
# Show the resolved unit definition (useful to verify ExecStart path)
|
|
147
|
+
systemctl --user cat {{config.python.service_label | default("<service-label>")}}
|
|
148
|
+
|
|
149
|
+
# List recent restarts / crash history
|
|
150
|
+
systemctl --user show {{config.python.service_label | default("<service-label>")}} \
|
|
151
|
+
--property=NRestarts,ActiveState,SubState
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Audit Log
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
echo "$(date -u +%FT%TZ) deploy surface=systemd sha=$(git rev-parse HEAD) unit={{config.python.service_label | default("<service-label>")}} actor=$(whoami)" \
|
|
160
|
+
>> data/audit/deploys.log
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Done. Report: unit name, sha, restart time, health status, any warnings.
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: massu-deploy
|
|
3
|
+
description: "Deploy a Python service (FastAPI / asgi) to production. Default target is a long-running process supervised by launchd, systemd, pm2, or docker — NOT Vercel. Asks which surface before acting."
|
|
4
|
+
allowed-tools: Bash(*), Read(*), Grep(*), Glob(*)
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
> **Shared rules apply.** Read `${paths.commands}/_shared-preamble.md` before proceeding.
|
|
8
|
+
|
|
9
|
+
# Massu Deploy: Python Service Production Deploy
|
|
10
|
+
|
|
11
|
+
## Workflow Position
|
|
12
|
+
|
|
13
|
+
```
|
|
14
|
+
/massu-create-plan -> /massu-plan -> /massu-loop -> /massu-commit -> /massu-push -> /massu-deploy
|
|
15
|
+
(CREATE) (AUDIT) (IMPLEMENT) (COMMIT) (PUSH) (DEPLOY)
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
This command deploys to production. **If the service is doing anything financial, transactional, or otherwise consequential, real money / data integrity is at risk** — pre-flight checks are mandatory.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## CRITICAL — Deployment Surfaces
|
|
23
|
+
|
|
24
|
+
A non-Vercel Python project usually has multiple deploy surfaces. Ask the user which one(s) before proceeding:
|
|
25
|
+
|
|
26
|
+
| Surface | What "deploy" means | Authority |
|
|
27
|
+
|---------|---------------------|-----------|
|
|
28
|
+
| **Service** (default) | Restart the supervised process — `launchctl kickstart`, `systemctl restart`, `pm2 restart`, or `docker compose up -d` | The actual production brain |
|
|
29
|
+
| **Static / docs** | rsync / S3 sync (if applicable) | Static site only |
|
|
30
|
+
| **Worker(s)** | Restart any background workers (celery, rq, custom asyncio task runners) | Background processing |
|
|
31
|
+
|
|
32
|
+
**There is NO assumption of Vercel.** Do NOT run `vercel --prod` from this template. If the user wants a Vercel deploy of a separate sub-project, route them to a different surface or scaffold a dedicated script.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## NON-NEGOTIABLE RULES
|
|
37
|
+
|
|
38
|
+
- **Never deploy with uncommitted changes** — push first via `/massu-push`
|
|
39
|
+
- **Never deploy with failing tests** — `pytest` (or your project's test runner) must be green
|
|
40
|
+
- **Always restart-and-probe** — file-saved ≠ process-running-the-fix. After restart, hit a health endpoint and diff before/after
|
|
41
|
+
- **Never kill processes without identifying them first** — get the PID, log line, or supervisor label before sending any signal
|
|
42
|
+
- **Live-trading or live-billing flag flips are a separate concern** — this command does not flip `auto_*_mode`, `*_live`, `production_*` flags. Use the dedicated approval flow
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## START NOW
|
|
47
|
+
|
|
48
|
+
### Step 0: Ask the user
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
===============================================================================
|
|
52
|
+
PYTHON DEPLOY — Which surface?
|
|
53
|
+
===============================================================================
|
|
54
|
+
|
|
55
|
+
service Restart the supervised Python service (default)
|
|
56
|
+
workers Restart background workers (celery / rq / custom)
|
|
57
|
+
all service + workers
|
|
58
|
+
static rsync / S3 sync (only if applicable)
|
|
59
|
+
|
|
60
|
+
Which? [service / workers / all / static]
|
|
61
|
+
===============================================================================
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Path A: Service (default)
|
|
67
|
+
|
|
68
|
+
Substitute `${supervisor}` with whatever your project actually uses (`launchd`, `systemd`, `pm2`, `docker`). Substitute `${service_label}` with the actual label / unit name from `massu.config.yaml` (`deploy.python.service_label`) or from your project's process manager.
|
|
69
|
+
|
|
70
|
+
### Pre-flight
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# 1. Branch + working tree clean
|
|
74
|
+
test -z "$(git status --porcelain)" || { echo "DIRTY — commit/stash first"; exit 1; }
|
|
75
|
+
|
|
76
|
+
# 2. Tests green (impacted area only — full suite is for /massu-push)
|
|
77
|
+
${paths.python_test_command:-pytest} -x 2>&1 | tail -10
|
|
78
|
+
|
|
79
|
+
# 3. Identify the running process (NEVER blind-kill)
|
|
80
|
+
# launchd: launchctl list | grep ${service_label}
|
|
81
|
+
# systemd: systemctl status ${service_label}
|
|
82
|
+
# pm2: pm2 jlist | jq '.[] | select(.name=="${service_label}")'
|
|
83
|
+
# docker: docker compose ps ${service_label}
|
|
84
|
+
|
|
85
|
+
# 4. Capture current health for diff
|
|
86
|
+
curl -sS ${health_url:-http://localhost:8000/health} | python3 -m json.tool > /tmp/${service_label}-health-before.json
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Approval gate
|
|
90
|
+
|
|
91
|
+
```
|
|
92
|
+
===============================================================================
|
|
93
|
+
APPROVAL REQUIRED — RESTART ${service_label}
|
|
94
|
+
===============================================================================
|
|
95
|
+
|
|
96
|
+
Pre-flight: PASS
|
|
97
|
+
Branch: <current>
|
|
98
|
+
HEAD: <sha>
|
|
99
|
+
Last service uptime: <etime>
|
|
100
|
+
|
|
101
|
+
This will:
|
|
102
|
+
1. Restart ${service_label} via ${supervisor}
|
|
103
|
+
2. Wait 5s, poll /health until 200 (max 60s)
|
|
104
|
+
3. Smoke: /health, /api/feature-flags (if applicable), one critical endpoint
|
|
105
|
+
4. Diff before/after health responses
|
|
106
|
+
5. If any check fails: print rollback (`git revert HEAD` + restart)
|
|
107
|
+
|
|
108
|
+
Reply "approve" or "abort".
|
|
109
|
+
===============================================================================
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Restart + verify
|
|
113
|
+
|
|
114
|
+
Pick the line that matches your supervisor:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
# launchd
|
|
118
|
+
launchctl kickstart -k gui/$(id -u)/${service_label}
|
|
119
|
+
|
|
120
|
+
# systemd (user)
|
|
121
|
+
systemctl --user restart ${service_label}
|
|
122
|
+
|
|
123
|
+
# pm2
|
|
124
|
+
pm2 restart ${service_label}
|
|
125
|
+
|
|
126
|
+
# docker compose
|
|
127
|
+
docker compose up -d --force-recreate ${service_label}
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
Then probe:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Poll until ready (60s budget)
|
|
134
|
+
for i in $(seq 1 30); do
|
|
135
|
+
sleep 2
|
|
136
|
+
status=$(curl -sS -o /dev/null -w "%{http_code}" ${health_url:-http://localhost:8000/health} || echo "000")
|
|
137
|
+
[ "$status" = "200" ] && { echo "READY after ${i} polls"; break; }
|
|
138
|
+
done
|
|
139
|
+
|
|
140
|
+
# Smoke + diff
|
|
141
|
+
curl -sS ${health_url:-http://localhost:8000/health} | python3 -m json.tool > /tmp/${service_label}-health-after.json
|
|
142
|
+
diff /tmp/${service_label}-health-before.json /tmp/${service_label}-health-after.json | head -50
|
|
143
|
+
|
|
144
|
+
# Tail the supervisor log for startup errors
|
|
145
|
+
# launchd:
|
|
146
|
+
log show --predicate 'subsystem == "${service_label}"' --last 2m --info 2>/dev/null | grep -iE "error|warn" | head -20
|
|
147
|
+
# systemd:
|
|
148
|
+
# journalctl --user -u ${service_label} --since "2 minutes ago" | grep -iE "error|warn" | head -20
|
|
149
|
+
# pm2:
|
|
150
|
+
# pm2 logs ${service_label} --lines 100 --nostream | grep -iE "error|warn" | head -20
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Rollback
|
|
154
|
+
|
|
155
|
+
If `/health` does not return 200 within 60s, or smoke fails:
|
|
156
|
+
|
|
157
|
+
```bash
|
|
158
|
+
git revert HEAD --no-edit && <restart-command-from-above>
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Then page yourself or your on-call: an unhealthy production service is an incident.
|
|
162
|
+
|
|
163
|
+
---
|
|
164
|
+
|
|
165
|
+
## Path B: Workers
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
# Restart background workers (celery / rq / custom asyncio runners)
|
|
169
|
+
# Substitute the supervisor label list for your project's worker names.
|
|
170
|
+
for label in ${worker_labels}; do
|
|
171
|
+
echo "restarting $label..."
|
|
172
|
+
# launchd: launchctl kickstart -k gui/$(id -u)/$label
|
|
173
|
+
# systemd: systemctl --user restart $label
|
|
174
|
+
# pm2: pm2 restart $label
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
# Verify each worker is processing again — push a no-op job if you have one,
|
|
178
|
+
# or read the queue depth before/after.
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## Path C: Static (only if applicable)
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Build static site, rsync, or aws s3 sync — fill in your project's pipeline.
|
|
187
|
+
# This is intentionally NOT a default; only run if your project has a static deploy target.
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
---
|
|
191
|
+
|
|
192
|
+
## After ANY surface deploys
|
|
193
|
+
|
|
194
|
+
Save the deployment sha and timestamp to your project's audit log so the boundary is visible:
|
|
195
|
+
|
|
196
|
+
```bash
|
|
197
|
+
echo "$(date -u +%FT%TZ) deploy surface=<surface> sha=$(git rev-parse HEAD) actor=$(whoami)" >> ${paths.audit_log:-data/audit/deploys.log}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Done. Report: surface, sha, time, smoke results, any warnings.
|
|
@@ -1,24 +1,40 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: massu-scaffold-page
|
|
3
|
-
description: "When user wants to create a new page,
|
|
3
|
+
description: "When user wants to create a new page, screen, or view — asks which framework target (Next.js, SwiftUI, FastAPI templates, Rust web), then scaffolds component / view / handler with project conventions"
|
|
4
4
|
allowed-tools: Bash(*), Read(*), Write(*), Edit(*), Grep(*), Glob(*)
|
|
5
5
|
---
|
|
6
6
|
|
|
7
|
-
# Scaffold New Page
|
|
7
|
+
# Scaffold New Page / View
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
This default template is **framework-agnostic**. It asks the user (or, when invoked by an agent, infers from `massu.config.yaml`) which target to scaffold, then dispatches to one of the embedded patterns below.
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
> **If your project ships a stack-specific variant** of this template (e.g., `massu-scaffold-page.swift.md`), the variant is preferred and this default is not installed. See `${paths.commands}/README.md` for the variant-resolution rules.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|------|---------|
|
|
15
|
-
| `page.tsx` | Main page component with Suspense boundary |
|
|
16
|
-
| `loading.tsx` | Skeleton loading state |
|
|
17
|
-
| `error.tsx` | Error boundary with retry |
|
|
13
|
+
## Step 1 — Pick a target
|
|
18
14
|
|
|
19
|
-
|
|
15
|
+
Ask the user (or read `framework.primary` / `framework.languages` from the consumer's `massu.config.yaml`):
|
|
16
|
+
|
|
17
|
+
| Stack | Path A: web | Path B: native / mobile | Path C: backend rendered |
|
|
18
|
+
|-------|-------------|-------------------------|--------------------------|
|
|
19
|
+
| TypeScript / Next.js | `app/<route>/page.tsx` + loading + error | — | — |
|
|
20
|
+
| Swift / SwiftUI | — | `Features/<feature>/Views/<Name>View.swift` (+ ViewModel + Response) | — |
|
|
21
|
+
| Python / FastAPI | — | — | Jinja template + handler in `routers/<name>.py` |
|
|
22
|
+
| Rust / Axum | — | — | `src/handlers/<name>.rs` returning `Html` or JSON |
|
|
23
|
+
|
|
24
|
+
If the user is unsure, default to whatever `framework.primary` points at.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Pattern 1 — Next.js App Router page
|
|
29
|
+
|
|
30
|
+
### What gets created
|
|
31
|
+
|
|
32
|
+
- `${paths.web_source}/app/<route>/page.tsx`
|
|
33
|
+
- `${paths.web_source}/app/<route>/loading.tsx`
|
|
34
|
+
- `${paths.web_source}/app/<route>/error.tsx`
|
|
35
|
+
|
|
36
|
+
### `page.tsx`
|
|
20
37
|
|
|
21
|
-
### page.tsx
|
|
22
38
|
```tsx
|
|
23
39
|
import { Suspense } from 'react';
|
|
24
40
|
import { PageContent } from './page-content';
|
|
@@ -26,82 +42,179 @@ import Loading from './loading';
|
|
|
26
42
|
|
|
27
43
|
export default function Page() {
|
|
28
44
|
return (
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
</Suspense>
|
|
33
|
-
</div>
|
|
45
|
+
<Suspense fallback={<Loading />}>
|
|
46
|
+
<PageContent />
|
|
47
|
+
</Suspense>
|
|
34
48
|
);
|
|
35
49
|
}
|
|
36
50
|
```
|
|
37
51
|
|
|
38
|
-
### page-content.tsx
|
|
52
|
+
### `page-content.tsx`
|
|
53
|
+
|
|
39
54
|
```tsx
|
|
40
55
|
'use client';
|
|
41
56
|
|
|
42
|
-
import {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
+
import { useEffect, useState } from 'react';
|
|
58
|
+
|
|
59
|
+
export function PageContent() {
|
|
60
|
+
const [data, setData] = useState<unknown>(null);
|
|
61
|
+
const [error, setError] = useState<string | null>(null);
|
|
62
|
+
|
|
63
|
+
useEffect(() => {
|
|
64
|
+
// Substitute the URL with your project's API base — tRPC client, REST URL, etc.
|
|
65
|
+
fetch(`${process.env.NEXT_PUBLIC_API_BASE ?? '/api'}/<endpoint>`, {
|
|
66
|
+
credentials: 'include',
|
|
67
|
+
})
|
|
68
|
+
.then(async (r) => {
|
|
69
|
+
if (!r.ok) throw new Error(`HTTP ${r.status}`);
|
|
70
|
+
setData(await r.json());
|
|
71
|
+
})
|
|
72
|
+
.catch((e: Error) => setError(e.message));
|
|
73
|
+
}, []);
|
|
74
|
+
|
|
75
|
+
if (error) throw new Error(error);
|
|
76
|
+
if (!data) return null;
|
|
77
|
+
return <pre>{JSON.stringify(data, null, 2)}</pre>;
|
|
57
78
|
}
|
|
58
79
|
```
|
|
59
80
|
|
|
60
|
-
### loading.tsx
|
|
61
|
-
```tsx
|
|
62
|
-
import { Skeleton } from '@/components/ui/skeleton';
|
|
81
|
+
### `loading.tsx` / `error.tsx`
|
|
63
82
|
|
|
83
|
+
```tsx
|
|
84
|
+
// loading.tsx — show a skeleton, NOT a flash of empty state.
|
|
64
85
|
export default function Loading() {
|
|
65
|
-
return
|
|
66
|
-
<div className="page-container">
|
|
67
|
-
<Skeleton className="h-8 w-48 mb-6" />
|
|
68
|
-
<Skeleton className="h-64 w-full" />
|
|
69
|
-
</div>
|
|
70
|
-
);
|
|
86
|
+
return <div className="page-skeleton" />;
|
|
71
87
|
}
|
|
72
88
|
```
|
|
73
89
|
|
|
74
|
-
### error.tsx
|
|
75
90
|
```tsx
|
|
91
|
+
// error.tsx — must be a client component. Provides a retry handle to the user.
|
|
76
92
|
'use client';
|
|
77
|
-
|
|
78
93
|
export default function Error({ error, reset }: { error: Error; reset: () => void }) {
|
|
79
94
|
return (
|
|
80
|
-
<div
|
|
81
|
-
<
|
|
82
|
-
<p className="text-muted-foreground">{error.message}</p>
|
|
95
|
+
<div role="alert">
|
|
96
|
+
<p>Something went wrong: {error.message}</p>
|
|
83
97
|
<button onClick={reset}>Try again</button>
|
|
84
98
|
</div>
|
|
85
99
|
);
|
|
86
100
|
}
|
|
87
101
|
```
|
|
88
102
|
|
|
89
|
-
|
|
103
|
+
### Verification
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm run build 2>&1 | tail -20 # or pnpm/yarn equivalent
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Pattern 2 — SwiftUI iOS / visionOS view
|
|
112
|
+
|
|
113
|
+
(Full version available as the `.swift.md` variant of this template.)
|
|
114
|
+
|
|
115
|
+
### What gets created
|
|
116
|
+
|
|
117
|
+
- `${paths.swift_source}/Features/<feature>/Views/<Name>View.swift`
|
|
118
|
+
- `${paths.swift_source}/Features/<feature>/ViewModels/<Name>ViewModel.swift`
|
|
119
|
+
- `${paths.swift_source}/Features/<feature>/Models/<Name>Response.swift`
|
|
120
|
+
|
|
121
|
+
### Sketch
|
|
122
|
+
|
|
123
|
+
```swift
|
|
124
|
+
struct <Name>View: View {
|
|
125
|
+
@StateObject private var viewModel = <Name>ViewModel()
|
|
126
|
+
var body: some View {
|
|
127
|
+
Group {
|
|
128
|
+
if viewModel.isLoading { ProgressView() }
|
|
129
|
+
else if let err = viewModel.error { ErrorState(message: err) { Task { await viewModel.load() } } }
|
|
130
|
+
else { content }
|
|
131
|
+
}
|
|
132
|
+
.task { await viewModel.load() }
|
|
133
|
+
}
|
|
134
|
+
private var content: some View { /* real content */ EmptyView() }
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@MainActor
|
|
138
|
+
final class <Name>ViewModel: ObservableObject {
|
|
139
|
+
@Published var data: <Name>Response?
|
|
140
|
+
@Published var isLoading = false
|
|
141
|
+
@Published var error: String?
|
|
142
|
+
func load() async { /* fetch and decode */ }
|
|
143
|
+
}
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
**Critical**: With `JSONDecoder.keyDecodingStrategy = .convertFromSnakeCase`, mismatched property names decode to nil silently. Hand-verify every Decodable property against a real API response.
|
|
147
|
+
|
|
148
|
+
### Verification
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
xcodebuild -scheme <Target>_iOS -destination 'generic/platform=iOS Simulator' build | tail -20
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
## Pattern 3 — FastAPI Jinja-rendered page
|
|
90
157
|
|
|
91
|
-
|
|
92
|
-
- **Suspense boundaries** are REQUIRED for pages using `use(params)` or `useSearchParams()`
|
|
93
|
-
- **protectedProcedure** for ALL tRPC queries that need auth
|
|
94
|
-
- **No `sm:page-container`** — only exception is mobile chat layouts
|
|
95
|
-
- **Breadcrumbs**: Add to PageHeader if page is nested (e.g., `/crm/contacts/[id]`)
|
|
158
|
+
### What gets created
|
|
96
159
|
|
|
97
|
-
|
|
160
|
+
- `${paths.python_source}/routers/<name>.py` (Jinja-rendering handler)
|
|
161
|
+
- `${paths.python_templates}/<name>.html`
|
|
98
162
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
163
|
+
### Sketch
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
from fastapi import APIRouter, Depends, Request
|
|
167
|
+
from fastapi.responses import HTMLResponse
|
|
168
|
+
from fastapi.templating import Jinja2Templates
|
|
169
|
+
|
|
170
|
+
from ._shared import get_current_user
|
|
171
|
+
|
|
172
|
+
router = APIRouter()
|
|
173
|
+
templates = Jinja2Templates(directory="${paths.python_templates}")
|
|
174
|
+
|
|
175
|
+
@router.get("/<route>", response_class=HTMLResponse)
|
|
176
|
+
async def render(request: Request, user: dict = Depends(get_current_user)):
|
|
177
|
+
return templates.TemplateResponse("<name>.html", {"request": request, "user": user})
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Critical**: this pattern serves HTML from FastAPI directly; for pure-API endpoints, use `/massu-scaffold-router` instead.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Pattern 4 — Axum (Rust) HTML handler
|
|
185
|
+
|
|
186
|
+
### What gets created
|
|
187
|
+
|
|
188
|
+
- `${paths.rust_source}/handlers/<name>.rs`
|
|
189
|
+
- Wire-up in `src/main.rs` or `src/router.rs` via `.route("/<path>", get(<name>::handler))`.
|
|
190
|
+
|
|
191
|
+
### Sketch
|
|
192
|
+
|
|
193
|
+
```rust
|
|
194
|
+
use axum::{response::Html, routing::get, Router};
|
|
195
|
+
|
|
196
|
+
pub fn router() -> Router {
|
|
197
|
+
Router::new().route("/<path>", get(handler))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
async fn handler() -> Html<&'static str> {
|
|
201
|
+
Html("<!doctype html><meta charset='utf-8'><title>Page</title><h1>Hello</h1>")
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Verification
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
cargo build 2>&1 | tail -10 && cargo test --quiet
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
---
|
|
104
212
|
|
|
105
213
|
## START NOW
|
|
106
214
|
|
|
107
|
-
Ask the user
|
|
215
|
+
Ask the user (in this order):
|
|
216
|
+
|
|
217
|
+
1. **Which target?** TS / Next.js · Swift / SwiftUI · Python / FastAPI · Rust / Axum. Default to `framework.primary` from `massu.config.yaml` if the user is unsure.
|
|
218
|
+
2. What's the URL path / feature name?
|
|
219
|
+
3. What does the page render, and which API endpoint feeds it?
|
|
220
|
+
4. Any auth requirements? (logged-in only · role-gated · biometric-gated for sensitive actions)
|