@1agh/maude 0.21.0 → 0.22.2
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/cli/commands/doctor.mjs +87 -76
- package/cli/commands/hub.mjs +17 -3
- package/package.json +9 -9
- package/plugins/design/dev-server/bin/check-runtime-bundles.sh +125 -0
- package/plugins/design/dev-server/bin/runtime-health.sh +47 -6
- package/plugins/design/dev-server/build.ts +19 -5
- package/plugins/design/dev-server/dist/client.bundle.js +3 -3
- package/plugins/design/dev-server/dist/runtime/.min-sizes.json +16 -0
- package/plugins/design/dev-server/dist/runtime/lib0_decoding.js +620 -0
- package/plugins/design/dev-server/dist/runtime/lib0_encoding.js +604 -0
- package/plugins/design/dev-server/dist/runtime/motion.js +4787 -8
- package/plugins/design/dev-server/dist/runtime/motion_react.js +9654 -7
- package/plugins/design/dev-server/dist/runtime/pixi-js.js +5865 -5469
- package/plugins/design/dev-server/dist/runtime/react-dom.js +4 -22
- package/plugins/design/dev-server/dist/runtime/react-dom_client.js +5 -23
- package/plugins/design/dev-server/dist/runtime/react.js +4 -22
- package/plugins/design/dev-server/dist/runtime/react_jsx-dev-runtime.js +4 -22
- package/plugins/design/dev-server/dist/runtime/react_jsx-runtime.js +4 -22
- package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +37 -248
- package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +13 -170
- package/plugins/design/dev-server/dist/runtime/yjs.js +300 -720
- package/plugins/design/dev-server/runtime-bundle.ts +11 -0
- package/plugins/design/templates/_shell.html +3 -1
package/cli/commands/doctor.mjs
CHANGED
|
@@ -252,96 +252,107 @@ function printReport({ depsByPlugin, configReport, summary }) {
|
|
|
252
252
|
async function applyFixes({ repoRoot, configFsPath, depsByPlugin, configReport }) {
|
|
253
253
|
let configChanged = false;
|
|
254
254
|
|
|
255
|
-
// ──
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (!cmd) {
|
|
273
|
-
process.stdout.write(` skip ${r.id} — no install command declared\n`);
|
|
274
|
-
continue;
|
|
275
|
-
}
|
|
276
|
-
// eslint-disable-next-line no-await-in-loop
|
|
277
|
-
const ans = await rl.question(` Install ${r.id} via \`${cmd}\`? [y/N] `);
|
|
278
|
-
if (ans.trim().toLowerCase() === 'y' || ans.trim().toLowerCase() === 'yes') {
|
|
279
|
-
process.stdout.write(` running: ${cmd}\n`);
|
|
280
|
-
const res = spawnSync('bash', ['-c', cmd], { stdio: 'inherit' });
|
|
281
|
-
if (res.status !== 0) process.stdout.write(` ✗ install failed (exit ${res.status})\n`);
|
|
282
|
-
} else {
|
|
283
|
-
process.stdout.write(` skipped ${r.id}\n`);
|
|
255
|
+
// ── Config edits ───────────────────────────────────────────────────────
|
|
256
|
+
// Run FIRST — deterministic (filesystem only) and must not be blocked by
|
|
257
|
+
// an interactive prompt that may never resolve on a non-TTY stdin (CI,
|
|
258
|
+
// scripted spawns with empty input). Dep prompts come AFTER.
|
|
259
|
+
if (configReport.exists) {
|
|
260
|
+
const config = JSON.parse(readFileSync(configFsPath, 'utf8'));
|
|
261
|
+
|
|
262
|
+
// Drop unknown additionalProperties errors.
|
|
263
|
+
for (const e of configReport.schema.errors) {
|
|
264
|
+
if (e.message.startsWith('unknown property')) {
|
|
265
|
+
const parts = e.path.split('/').filter(Boolean);
|
|
266
|
+
let cur = config;
|
|
267
|
+
for (let i = 0; i < parts.length - 1; i++) cur = cur?.[parts[i]];
|
|
268
|
+
if (cur && parts.length > 0) {
|
|
269
|
+
delete cur[parts[parts.length - 1]];
|
|
270
|
+
configChanged = true;
|
|
271
|
+
}
|
|
284
272
|
}
|
|
285
273
|
}
|
|
286
|
-
rl.close();
|
|
287
|
-
}
|
|
288
274
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if (e.message.startsWith('unknown property')) {
|
|
296
|
-
const parts = e.path.split('/').filter(Boolean);
|
|
297
|
-
let cur = config;
|
|
298
|
-
for (let i = 0; i < parts.length - 1; i++) cur = cur?.[parts[i]];
|
|
299
|
-
if (cur && parts.length > 0) {
|
|
300
|
-
delete cur[parts[parts.length - 1]];
|
|
275
|
+
// Apply drift — silent (detector returned a concrete value, so we know
|
|
276
|
+
// declared is wrong or stale).
|
|
277
|
+
if (configReport.drift.length > 0) {
|
|
278
|
+
if (!config.stack) config.stack = {};
|
|
279
|
+
for (const d of configReport.drift) {
|
|
280
|
+
config.stack[d.key] = d.detected;
|
|
301
281
|
configChanged = true;
|
|
302
282
|
}
|
|
303
283
|
}
|
|
304
|
-
}
|
|
305
284
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
285
|
+
// Add missing quality gates — silent additive merge.
|
|
286
|
+
if (configReport.qualityAdditions.length > 0) {
|
|
287
|
+
if (!config.quality) config.quality = {};
|
|
288
|
+
for (const a of configReport.qualityAdditions) {
|
|
289
|
+
if (!config.quality[a.gate]) {
|
|
290
|
+
config.quality[a.gate] = a.command;
|
|
291
|
+
configChanged = true;
|
|
292
|
+
}
|
|
293
|
+
}
|
|
313
294
|
}
|
|
314
|
-
}
|
|
315
295
|
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
296
|
+
if (configChanged) {
|
|
297
|
+
writeFileSync(configFsPath, `${JSON.stringify(config, null, 2)}\n`);
|
|
298
|
+
process.stdout.write(`\n ✓ wrote ${CONFIG_PATH}\n`);
|
|
299
|
+
} else {
|
|
300
|
+
process.stdout.write('\n (no config edits applied)\n');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// Note: invalid-enum errors (e.g. tests: "node-test") are NEVER auto-
|
|
304
|
+
// fixed — the user picks the migration target. Surface the leftover.
|
|
305
|
+
const remaining = configReport.schema.errors.filter(
|
|
306
|
+
(e) => !e.message.startsWith('unknown property')
|
|
307
|
+
);
|
|
308
|
+
if (remaining.length > 0) {
|
|
309
|
+
process.stdout.write(
|
|
310
|
+
`\n ⚠ ${remaining.length} schema error${remaining.length === 1 ? '' : 's'} not auto-fixed — user decision required:\n`
|
|
311
|
+
);
|
|
312
|
+
for (const e of remaining) {
|
|
313
|
+
process.stdout.write(` ${e.path}: ${e.message}\n`);
|
|
323
314
|
}
|
|
324
315
|
}
|
|
325
316
|
}
|
|
326
317
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
318
|
+
// ── Dependency installs ────────────────────────────────────────────────
|
|
319
|
+
const installable = [];
|
|
320
|
+
for (const env of Object.values(depsByPlugin)) {
|
|
321
|
+
if (!env.results) continue;
|
|
322
|
+
for (const r of env.results) {
|
|
323
|
+
if (r.status !== 'missing') continue;
|
|
324
|
+
installable.push(r);
|
|
325
|
+
}
|
|
332
326
|
}
|
|
333
|
-
|
|
334
|
-
//
|
|
335
|
-
//
|
|
336
|
-
|
|
337
|
-
(
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
327
|
+
// Per-dep prompt — no silent installs. Skip when stdin is not a TTY
|
|
328
|
+
// (CI runners, scripted spawns) so config edits above are never bypassed
|
|
329
|
+
// by a never-resolving readline prompt against closed stdin.
|
|
330
|
+
if (installable.length > 0) {
|
|
331
|
+
if (!stdin.isTTY) {
|
|
332
|
+
process.stdout.write(
|
|
333
|
+
`\n--- Dependency installs skipped (non-interactive stdin; ${installable.length} missing) ---\n`
|
|
334
|
+
);
|
|
335
|
+
} else {
|
|
336
|
+
process.stdout.write('\n--- Dependency installs (per-dep prompt) ---\n');
|
|
337
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
338
|
+
for (const r of installable) {
|
|
339
|
+
const cmd = (r.install && (r.install[process.platform] || r.install.preferred)) || null;
|
|
340
|
+
if (!cmd) {
|
|
341
|
+
process.stdout.write(` skip ${r.id} — no install command declared\n`);
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
// eslint-disable-next-line no-await-in-loop
|
|
345
|
+
const ans = await rl.question(` Install ${r.id} via \`${cmd}\`? [y/N] `);
|
|
346
|
+
const norm = (ans || '').trim().toLowerCase();
|
|
347
|
+
if (norm === 'y' || norm === 'yes') {
|
|
348
|
+
process.stdout.write(` running: ${cmd}\n`);
|
|
349
|
+
const res = spawnSync('bash', ['-c', cmd], { stdio: 'inherit' });
|
|
350
|
+
if (res.status !== 0) process.stdout.write(` ✗ install failed (exit ${res.status})\n`);
|
|
351
|
+
} else {
|
|
352
|
+
process.stdout.write(` skipped ${r.id}\n`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
rl.close();
|
|
345
356
|
}
|
|
346
357
|
}
|
|
347
358
|
|
package/cli/commands/hub.mjs
CHANGED
|
@@ -37,13 +37,18 @@ function usage() {
|
|
|
37
37
|
Start the self-hostable Yjs sync hub in the current process tree.
|
|
38
38
|
--port listen port (default 1234, env PORT)
|
|
39
39
|
--data hub.db + tokens.json dir (default ./data, env DATA_DIR)
|
|
40
|
-
--secret HUB_SECRET escape-hatch token (env HUB_SECRET)
|
|
40
|
+
--secret HUB_SECRET escape-hatch token (env HUB_SECRET).
|
|
41
|
+
If unset on an empty hub, a one-time bootstrap link is
|
|
42
|
+
printed to logs — open it in a browser to claim admin.
|
|
41
43
|
--insecure-http cosmetic log-only flag for non-TLS dev
|
|
42
44
|
--dev generate a one-shot mau_dev_<hex> token, print the
|
|
43
45
|
connect command, then run the hub. Convenience for
|
|
44
46
|
contributor onboarding — drops tokens.json on exit
|
|
45
47
|
is NOT performed (tokens persist; clean by hand).
|
|
46
48
|
|
|
49
|
+
On boot the hub prints its /admin URL. The admin UI generates invite
|
|
50
|
+
tokens, lists peers, and rotates tokens without shelling into the host.
|
|
51
|
+
|
|
47
52
|
token generate --label NAME [--data PATH] [--dev]
|
|
48
53
|
Generate a new mau_<32hex> token and append it to
|
|
49
54
|
<data>/tokens.json with the given label. Prints the raw token ONCE,
|
|
@@ -51,9 +56,12 @@ function usage() {
|
|
|
51
56
|
|
|
52
57
|
--dev produces a mau_dev_<hex> token (convention only — same auth).
|
|
53
58
|
|
|
59
|
+
Equivalent to the "Generate invite" button in the /admin UI — use
|
|
60
|
+
whichever is more convenient for the deploy.
|
|
61
|
+
|
|
54
62
|
status [URL] [--json]
|
|
55
|
-
HTTP GET <url>/health, print uptime/version/token-count. URL
|
|
56
|
-
to http://localhost:1234. --json emits the raw response.
|
|
63
|
+
HTTP GET <url>/health, print uptime/version/token-count/peers. URL
|
|
64
|
+
defaults to http://localhost:1234. --json emits the raw response.
|
|
57
65
|
|
|
58
66
|
NOTES
|
|
59
67
|
Local dev only in v1.1 Task 2 slice — production-install packaging
|
|
@@ -64,6 +72,12 @@ EXAMPLES
|
|
|
64
72
|
maude hub serve --port 4400
|
|
65
73
|
maude hub token generate --label alice
|
|
66
74
|
maude hub status http://localhost:4400
|
|
75
|
+
|
|
76
|
+
# Recommended flow on a fresh deploy:
|
|
77
|
+
# 1. maude hub serve → copy bootstrap link from logs
|
|
78
|
+
# 2. open the link in browser → first-run wizard, mint admin secret
|
|
79
|
+
# 3. click "Generate invite" → copy 'maude design link …' command
|
|
80
|
+
# 4. paste on the peer machine → linked
|
|
67
81
|
`;
|
|
68
82
|
}
|
|
69
83
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1agh/maude",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.2",
|
|
4
4
|
"description": "Marketplace of Claude Code plugins by Michal Dovrtěl: `design` (canvas-first design iteration) + `flow` (generic agentic workflow loop with .ai second brain). Ships the `maude` CLI (with `mdcc` legacy alias) to scaffold workspace, run the design dev server, and manage configs.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -38,16 +38,16 @@
|
|
|
38
38
|
"video:render": "cd scripts/video/final && pnpm run render",
|
|
39
39
|
"video:studio": "cd scripts/video/final && pnpm run studio",
|
|
40
40
|
"postinstall": "node cli/install.cjs",
|
|
41
|
-
"prepublishOnly": "bash scripts/check-version-parity.sh"
|
|
41
|
+
"prepublishOnly": "bash scripts/check-version-parity.sh && bash plugins/design/dev-server/bin/check-runtime-bundles.sh"
|
|
42
42
|
},
|
|
43
43
|
"optionalDependencies": {
|
|
44
|
-
"@1agh/maude-darwin-arm64": "0.
|
|
45
|
-
"@1agh/maude-darwin-x64": "0.
|
|
46
|
-
"@1agh/maude-linux-arm64": "0.
|
|
47
|
-
"@1agh/maude-linux-arm64-musl": "0.
|
|
48
|
-
"@1agh/maude-linux-x64": "0.
|
|
49
|
-
"@1agh/maude-linux-x64-musl": "0.
|
|
50
|
-
"@1agh/maude-win32-x64": "0.
|
|
44
|
+
"@1agh/maude-darwin-arm64": "0.22.2",
|
|
45
|
+
"@1agh/maude-darwin-x64": "0.22.2",
|
|
46
|
+
"@1agh/maude-linux-arm64": "0.22.2",
|
|
47
|
+
"@1agh/maude-linux-arm64-musl": "0.22.2",
|
|
48
|
+
"@1agh/maude-linux-x64": "0.22.2",
|
|
49
|
+
"@1agh/maude-linux-x64-musl": "0.22.2",
|
|
50
|
+
"@1agh/maude-win32-x64": "0.22.2"
|
|
51
51
|
},
|
|
52
52
|
"files": [
|
|
53
53
|
"cli",
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# check-runtime-bundles.sh — pre-publish guard against shipping defective
|
|
3
|
+
# /_canvas-runtime/<slug>.js bundles.
|
|
4
|
+
#
|
|
5
|
+
# Why: Bun.build's output for `motion` + `motion/react` is environment-sensitive
|
|
6
|
+
# (Bun version, OS, transitive dep resolution). v0.22.0 shipped a 13 kB
|
|
7
|
+
# motion_react.js where the working bundle is 155 kB+ — the smaller artifact
|
|
8
|
+
# parses cleanly + serves HTTP 200 but throws `ReferenceError: AcceleratedAnimation
|
|
9
|
+
# is not defined` at module-eval time, breaking every canvas that uses the
|
|
10
|
+
# motion lib. CI build was green; the regression slipped because nothing
|
|
11
|
+
# asserted bundle SIZE.
|
|
12
|
+
#
|
|
13
|
+
# This guard reads dist/runtime/.min-sizes.json and asserts each on-disk
|
|
14
|
+
# bundle ≥ its declared floor. Run from CI before `npm publish`. Hard-fails
|
|
15
|
+
# the publish job → the bad tarball never reaches npm.
|
|
16
|
+
#
|
|
17
|
+
# Floors are at ~70% of release-minified size (see manifest comment). Any
|
|
18
|
+
# minifier improvement that drops a bundle below the floor needs an explicit
|
|
19
|
+
# manifest bump + investigation — that's the point.
|
|
20
|
+
#
|
|
21
|
+
# Usage:
|
|
22
|
+
# check-runtime-bundles.sh [--runtime-dir <path>] [--manifest <path>]
|
|
23
|
+
#
|
|
24
|
+
# Defaults:
|
|
25
|
+
# --runtime-dir = <plugin>/dev-server/dist/runtime/
|
|
26
|
+
# --manifest = <runtime-dir>/.min-sizes.json
|
|
27
|
+
#
|
|
28
|
+
# Exit codes:
|
|
29
|
+
# 0 all bundles meet floor
|
|
30
|
+
# 1 manifest or runtime dir missing
|
|
31
|
+
# 2 bad args
|
|
32
|
+
# 3 one or more bundles below floor
|
|
33
|
+
|
|
34
|
+
set -euo pipefail
|
|
35
|
+
|
|
36
|
+
RUNTIME_DIR=""
|
|
37
|
+
MANIFEST=""
|
|
38
|
+
|
|
39
|
+
while [ $# -gt 0 ]; do
|
|
40
|
+
case "$1" in
|
|
41
|
+
--runtime-dir) RUNTIME_DIR="$2"; shift 2 ;;
|
|
42
|
+
--manifest) MANIFEST="$2"; shift 2 ;;
|
|
43
|
+
--help|-h)
|
|
44
|
+
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
|
45
|
+
exit 0
|
|
46
|
+
;;
|
|
47
|
+
*)
|
|
48
|
+
echo "check-runtime-bundles.sh: unknown arg '$1' (try --help)" >&2
|
|
49
|
+
exit 2
|
|
50
|
+
;;
|
|
51
|
+
esac
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
55
|
+
if [ -z "$RUNTIME_DIR" ]; then
|
|
56
|
+
RUNTIME_DIR="$SCRIPT_DIR/../dist/runtime"
|
|
57
|
+
fi
|
|
58
|
+
if [ -z "$MANIFEST" ]; then
|
|
59
|
+
MANIFEST="$RUNTIME_DIR/.min-sizes.json"
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
if [ ! -d "$RUNTIME_DIR" ]; then
|
|
63
|
+
echo "check-runtime-bundles.sh: runtime dir not found at $RUNTIME_DIR" >&2
|
|
64
|
+
exit 1
|
|
65
|
+
fi
|
|
66
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
67
|
+
echo "check-runtime-bundles.sh: manifest not found at $MANIFEST" >&2
|
|
68
|
+
exit 1
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
# Read manifest as <slug> <floor> pairs, one per line. Skip $-prefixed
|
|
72
|
+
# metadata keys ($comment etc.). Prefer jq when available; fall back to
|
|
73
|
+
# python3 (always present in CI runners + macOS).
|
|
74
|
+
read_pairs() {
|
|
75
|
+
if command -v jq >/dev/null 2>&1; then
|
|
76
|
+
jq -r 'to_entries | map(select(.key | startswith("$") | not)) | .[] | "\(.key) \(.value)"' "$MANIFEST"
|
|
77
|
+
else
|
|
78
|
+
python3 -c '
|
|
79
|
+
import json, sys
|
|
80
|
+
with open("'"$MANIFEST"'") as f:
|
|
81
|
+
data = json.load(f)
|
|
82
|
+
for k, v in data.items():
|
|
83
|
+
if k.startswith("$"): continue
|
|
84
|
+
print(k, v)
|
|
85
|
+
'
|
|
86
|
+
fi
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
FAIL_COUNT=0
|
|
90
|
+
CHECK_COUNT=0
|
|
91
|
+
MISSING_COUNT=0
|
|
92
|
+
|
|
93
|
+
while read -r slug floor; do
|
|
94
|
+
[ -z "$slug" ] && continue
|
|
95
|
+
CHECK_COUNT=$((CHECK_COUNT + 1))
|
|
96
|
+
PATH_JS="$RUNTIME_DIR/$slug"
|
|
97
|
+
if [ ! -f "$PATH_JS" ]; then
|
|
98
|
+
echo "✗ $slug — missing on disk (expected at $PATH_JS)" >&2
|
|
99
|
+
MISSING_COUNT=$((MISSING_COUNT + 1))
|
|
100
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
101
|
+
continue
|
|
102
|
+
fi
|
|
103
|
+
SIZE=$(wc -c < "$PATH_JS" | tr -d ' ')
|
|
104
|
+
if [ "$SIZE" -lt "$floor" ]; then
|
|
105
|
+
echo "✗ $slug — $SIZE B < floor $floor B (likely defective bundle)" >&2
|
|
106
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
107
|
+
else
|
|
108
|
+
echo "✓ $slug — $SIZE B ≥ floor $floor B"
|
|
109
|
+
fi
|
|
110
|
+
done < <(read_pairs)
|
|
111
|
+
|
|
112
|
+
if [ "$FAIL_COUNT" -eq 0 ]; then
|
|
113
|
+
echo ""
|
|
114
|
+
echo "✓ check-runtime-bundles OK — $CHECK_COUNT bundles, all above floor"
|
|
115
|
+
exit 0
|
|
116
|
+
fi
|
|
117
|
+
|
|
118
|
+
echo "" >&2
|
|
119
|
+
echo "✗ check-runtime-bundles FAIL — $FAIL_COUNT/$CHECK_COUNT bundle(s) below floor" >&2
|
|
120
|
+
if [ "$MISSING_COUNT" -gt 0 ]; then
|
|
121
|
+
echo " ($MISSING_COUNT missing from disk — was buildRuntimeBundles() skipped?)" >&2
|
|
122
|
+
fi
|
|
123
|
+
echo " Refusing to ship a tarball with defective /_canvas-runtime artifacts." >&2
|
|
124
|
+
echo " Investigate the Bun.build output for the offending package(s) before publishing." >&2
|
|
125
|
+
exit 3
|
|
@@ -8,9 +8,14 @@
|
|
|
8
8
|
# server returns HTTP 200, but the iframe throws at module-eval time with
|
|
9
9
|
# `ReferenceError: AcceleratedAnimation is not defined` (or similar).
|
|
10
10
|
#
|
|
11
|
-
# Parse-clean ≠ run-clean. This helper closes that gap
|
|
12
|
-
#
|
|
13
|
-
#
|
|
11
|
+
# Parse-clean ≠ run-clean. This helper closes that gap with TWO checks:
|
|
12
|
+
# 1. Per-bundle absolute floor — served body MUST clear the size declared
|
|
13
|
+
# in dist/runtime/.min-sizes.json. This catches the v0.22.0 regression
|
|
14
|
+
# class where the installed bundle on disk is ITSELF defective (so
|
|
15
|
+
# compare-to-disk would trivially pass).
|
|
16
|
+
# 2. Compare-to-disk ratio — served body MUST be ≥ threshold × on-disk
|
|
17
|
+
# size. Catches the original case where disk is good but the running
|
|
18
|
+
# dev-server returned a defective dynamic Bun.build cached in memory.
|
|
14
19
|
#
|
|
15
20
|
# Usage:
|
|
16
21
|
# runtime-health.sh [--port N] [--root <repo>] [--threshold 0.5]
|
|
@@ -67,6 +72,26 @@ if [ ! -d "$PREBUILT_DIR" ]; then
|
|
|
67
72
|
exit 1
|
|
68
73
|
fi
|
|
69
74
|
|
|
75
|
+
# Absolute-floor manifest. Optional — if missing we fall back to the
|
|
76
|
+
# disk-ratio check alone (older installs from before .min-sizes.json shipped).
|
|
77
|
+
MIN_SIZES_MANIFEST="$PREBUILT_DIR/.min-sizes.json"
|
|
78
|
+
floor_for() {
|
|
79
|
+
local slug="$1"
|
|
80
|
+
[ -f "$MIN_SIZES_MANIFEST" ] || { echo ""; return; }
|
|
81
|
+
if command -v jq >/dev/null 2>&1; then
|
|
82
|
+
jq -r --arg k "$slug" '.[$k] // empty' "$MIN_SIZES_MANIFEST" 2>/dev/null
|
|
83
|
+
else
|
|
84
|
+
python3 -c '
|
|
85
|
+
import json, sys
|
|
86
|
+
try:
|
|
87
|
+
with open("'"$MIN_SIZES_MANIFEST"'") as f: data = json.load(f)
|
|
88
|
+
v = data.get("'"$slug"'")
|
|
89
|
+
print(v if v is not None else "")
|
|
90
|
+
except Exception: print("")
|
|
91
|
+
' 2>/dev/null
|
|
92
|
+
fi
|
|
93
|
+
}
|
|
94
|
+
|
|
70
95
|
# ---------- resolve port from _server.json if not given ----------
|
|
71
96
|
DESIGN_ROOT="$REPO/.design"
|
|
72
97
|
STATE="$DESIGN_ROOT/_server.json"
|
|
@@ -101,11 +126,19 @@ probe_one() {
|
|
|
101
126
|
echo "✗ $slug — served HTTP error (curl failed for $url)" >&2
|
|
102
127
|
return 1
|
|
103
128
|
}
|
|
104
|
-
#
|
|
129
|
+
# Hard floor at 256 bytes — any working ESM bundle is bigger than that.
|
|
105
130
|
if [ "$served" -lt 256 ]; then
|
|
106
131
|
echo "✗ $slug — served body $served B < 256 B floor (server returned empty)" >&2
|
|
107
132
|
return 1
|
|
108
133
|
fi
|
|
134
|
+
# Absolute floor from .min-sizes.json (independent of disk — catches the
|
|
135
|
+
# case where the SHIPPED bundle is itself defective, like v0.22.0 motion_react).
|
|
136
|
+
local abs_floor
|
|
137
|
+
abs_floor=$(floor_for "$slug")
|
|
138
|
+
if [ -n "$abs_floor" ] && [ "$served" -lt "$abs_floor" ]; then
|
|
139
|
+
echo "✗ $slug — served $served B < absolute floor $abs_floor B (declared in .min-sizes.json — defective bundle in install)" >&2
|
|
140
|
+
return 1
|
|
141
|
+
fi
|
|
109
142
|
# Threshold ratio: served must be ≥ threshold × disk.
|
|
110
143
|
# awk handles the fractional math without bc dependency.
|
|
111
144
|
local ratio
|
|
@@ -113,7 +146,7 @@ probe_one() {
|
|
|
113
146
|
local ok
|
|
114
147
|
ok=$(awk -v r="$ratio" -v t="$THRESHOLD" 'BEGIN{print (r >= t) ? 1 : 0}')
|
|
115
148
|
if [ "$ok" = "1" ]; then
|
|
116
|
-
[ $QUIET -eq 0 ] && echo "✓ $slug — $served B / $disk_size B disk (ratio $ratio)" >&2
|
|
149
|
+
[ $QUIET -eq 0 ] && echo "✓ $slug — $served B / $disk_size B disk (ratio $ratio${abs_floor:+, floor ${abs_floor} B})" >&2
|
|
117
150
|
return 0
|
|
118
151
|
fi
|
|
119
152
|
echo "✗ $slug — served $served B / $disk_size B disk (ratio $ratio < $THRESHOLD) — defective dynamic build" >&2
|
|
@@ -139,8 +172,16 @@ fi
|
|
|
139
172
|
|
|
140
173
|
echo "" >&2
|
|
141
174
|
echo "✗ runtime-health FAIL — $FAIL_COUNT bundle(s) below threshold:$FAIL_LIST" >&2
|
|
142
|
-
echo " These bundles look
|
|
175
|
+
echo " These bundles look defective." >&2
|
|
143
176
|
echo " The canvas TSX will parse + serve cleanly, but the iframe will throw at runtime." >&2
|
|
177
|
+
echo "" >&2
|
|
178
|
+
echo " If the failure was from the 'absolute floor' check (see lines above):" >&2
|
|
179
|
+
echo " → the SHIPPED bundle on disk is itself defective (rare release-time regression)." >&2
|
|
180
|
+
echo " → --restart will NOT help; upgrade the package: \`npm i -g @1agh/maude@latest\`" >&2
|
|
181
|
+
echo " (or for marketplace installs: \`/plugin marketplace update maude\`)." >&2
|
|
182
|
+
echo " If the failure was from the 'ratio < threshold' check:" >&2
|
|
183
|
+
echo " → the running server cached a defective dynamic Bun.build output." >&2
|
|
184
|
+
echo " → --restart will respawn the server and load the (good) disk pre-built." >&2
|
|
144
185
|
|
|
145
186
|
if [ "$RESTART" -eq 1 ]; then
|
|
146
187
|
echo "→ --restart given; killing server and respawning via server-up.sh" >&2
|
|
@@ -405,11 +405,25 @@ async function main() {
|
|
|
405
405
|
// Pre-built runtime bundles — ship to disk so /_canvas-runtime/* never
|
|
406
406
|
// needs a runtime Bun.build (which would need disk node_modules/react,
|
|
407
407
|
// absent in compiled binaries + npm installs). Phase 19.1 / v0.18.1.
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
408
|
+
//
|
|
409
|
+
// MAUDE_SKIP_RUNTIME_BUILD=1 — skip the regen and trust the committed
|
|
410
|
+
// dist/runtime/*.js. Set in CI publish-main so the npm tarball ships
|
|
411
|
+
// exactly what was committed in git (platform-agnostic). v0.22.0 shipped
|
|
412
|
+
// a broken Ubuntu-CI motion_react.js because the on-disk authoritative
|
|
413
|
+
// bundle got overwritten by `pnpm build`; this flag prevents that class
|
|
414
|
+
// of regression. The check-runtime-bundles.sh step still validates the
|
|
415
|
+
// on-disk artifacts against .min-sizes.json after the build.
|
|
416
|
+
if (process.env.MAUDE_SKIP_RUNTIME_BUILD === '1') {
|
|
417
|
+
console.log(
|
|
418
|
+
`[build] dist/runtime/*.js SKIPPED (MAUDE_SKIP_RUNTIME_BUILD=1 — using committed pre-built)`
|
|
419
|
+
);
|
|
420
|
+
} else {
|
|
421
|
+
const runtime = await buildRuntimeBundles();
|
|
422
|
+
const t2b = performance.now();
|
|
423
|
+
console.log(
|
|
424
|
+
`[build] dist/runtime/*.js ${runtime.bytes.toLocaleString()} B in ${runtime.count} files (${(t2b - t2).toFixed(0)} ms)`
|
|
425
|
+
);
|
|
426
|
+
}
|
|
413
427
|
|
|
414
428
|
if (MODE === 'release') {
|
|
415
429
|
const targets: PlatformTarget[] = FLAG_TARGET
|