@1agh/maude 0.19.1 → 0.21.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 +7 -0
- package/cli/bin/maude.mjs +5 -1
- package/cli/commands/design-link.test.mjs +207 -0
- package/cli/commands/design.mjs +42 -12
- package/cli/commands/doctor.mjs +350 -0
- package/cli/commands/doctor.test.mjs +185 -0
- package/cli/commands/help.mjs +24 -0
- package/cli/commands/hub.mjs +231 -0
- package/cli/commands/hub.test.mjs +87 -0
- package/cli/lib/config-lint.mjs +141 -0
- package/cli/lib/config-lint.test.mjs +117 -0
- package/cli/lib/design-link.mjs +216 -0
- package/cli/lib/hubs-config.mjs +123 -0
- package/cli/lib/hubs-config.test.mjs +100 -0
- package/cli/lib/preflight.mjs +232 -0
- package/cli/lib/stack-detect.mjs +344 -0
- package/cli/lib/stack-detect.test.mjs +121 -0
- package/package.json +17 -9
- package/plugins/design/dependencies.json +147 -0
- package/plugins/design/dependencies.schema.json +107 -0
- package/plugins/design/dev-server/ai-banner.tsx +188 -0
- package/plugins/design/dev-server/annotations-context-toolbar.tsx +115 -41
- package/plugins/design/dev-server/annotations-layer.tsx +256 -107
- package/plugins/design/dev-server/api.ts +17 -1
- package/plugins/design/dev-server/artboard-marquee.tsx +10 -6
- package/plugins/design/dev-server/bin/asset-sweep.sh +180 -0
- package/plugins/design/dev-server/bin/preflight.sh +32 -0
- package/plugins/design/dev-server/bin/runtime-health.sh +169 -0
- package/plugins/design/dev-server/bin/visual-sanity.sh +221 -0
- package/plugins/design/dev-server/canvas-lib.tsx +534 -32
- package/plugins/design/dev-server/canvas-shell.tsx +479 -29
- package/plugins/design/dev-server/client/app.jsx +72 -0
- package/plugins/design/dev-server/client/hmr.mjs +28 -0
- package/plugins/design/dev-server/collab/ai-activity.ts +127 -0
- package/plugins/design/dev-server/collab/git-lifecycle.ts +124 -0
- package/plugins/design/dev-server/collab/index.ts +47 -0
- package/plugins/design/dev-server/collab/persistence.ts +123 -0
- package/plugins/design/dev-server/collab/protocol.ts +108 -0
- package/plugins/design/dev-server/collab/registry.ts +110 -0
- package/plugins/design/dev-server/collab/room.ts +215 -0
- package/plugins/design/dev-server/commands/annotation-strokes-command.ts +127 -0
- package/plugins/design/dev-server/commands/equal-spacing-command.ts +28 -0
- package/plugins/design/dev-server/commands/move-artboards-command.ts +158 -0
- package/plugins/design/dev-server/comments-overlay.tsx +41 -4
- package/plugins/design/dev-server/contextual-toolbar.tsx +241 -0
- package/plugins/design/dev-server/cursors-overlay.tsx +296 -0
- package/plugins/design/dev-server/dist/client.bundle.js +75 -3
- package/plugins/design/dev-server/dist/runtime/motion.js +165 -0
- package/plugins/design/dev-server/dist/runtime/motion_react.js +409 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_awareness.js +587 -0
- package/plugins/design/dev-server/dist/runtime/y-protocols_sync.js +257 -0
- package/plugins/design/dev-server/dist/runtime/yjs.js +7107 -0
- package/plugins/design/dev-server/equal-spacing-detector.ts +98 -0
- package/plugins/design/dev-server/equal-spacing-handles.tsx +289 -0
- package/plugins/design/dev-server/export-dialog.tsx +1 -1
- package/plugins/design/dev-server/handoff.ts +24 -0
- package/plugins/design/dev-server/hmr-broadcast.ts +15 -2
- package/plugins/design/dev-server/http.ts +91 -1
- package/plugins/design/dev-server/input-router.tsx +52 -2
- package/plugins/design/dev-server/marquee-overlay.tsx +282 -0
- package/plugins/design/dev-server/participants-chrome.tsx +261 -0
- package/plugins/design/dev-server/runtime-bundle.ts +15 -0
- package/plugins/design/dev-server/server.ts +78 -11
- package/plugins/design/dev-server/test/ai-activity.test.ts +113 -0
- package/plugins/design/dev-server/test/annotation-strokes-command.test.ts +149 -0
- package/plugins/design/dev-server/test/annotations-layer.test.ts +44 -0
- package/plugins/design/dev-server/test/canvas-lib-motion.test.ts +131 -0
- package/plugins/design/dev-server/test/collab-annotations-bridge.test.ts +55 -0
- package/plugins/design/dev-server/test/collab-bridge.test.ts +81 -0
- package/plugins/design/dev-server/test/collab-loopback.test.ts +63 -0
- package/plugins/design/dev-server/test/collab-protocol.test.ts +146 -0
- package/plugins/design/dev-server/test/collab-room.test.ts +182 -0
- package/plugins/design/dev-server/test/collab-stress.test.ts +121 -0
- package/plugins/design/dev-server/test/equal-spacing-detector.test.ts +93 -0
- package/plugins/design/dev-server/test/git-lifecycle.test.ts +101 -0
- package/plugins/design/dev-server/test/input-router.test.ts +87 -1
- package/plugins/design/dev-server/test/marquee-overlay.test.ts +94 -0
- package/plugins/design/dev-server/test/move-artboards-command.test.ts +108 -0
- package/plugins/design/dev-server/test/participants-chrome.test.ts +30 -0
- package/plugins/design/dev-server/test/undo-stack.test.ts +211 -0
- package/plugins/design/dev-server/test/use-collab.test.ts +71 -0
- package/plugins/design/dev-server/test/use-cursor-modifiers.test.ts +59 -0
- package/plugins/design/dev-server/test/use-keyboard-discipline.test.ts +27 -0
- package/plugins/design/dev-server/test/use-undo-stack.test.tsx +193 -0
- package/plugins/design/dev-server/tool-palette.tsx +7 -7
- package/plugins/design/dev-server/undo-hud.tsx +95 -0
- package/plugins/design/dev-server/undo-stack.ts +240 -0
- package/plugins/design/dev-server/use-annotation-resize.tsx +1 -1
- package/plugins/design/dev-server/use-artboard-drag.tsx +6 -3
- package/plugins/design/dev-server/use-collab.tsx +478 -0
- package/plugins/design/dev-server/use-cursor-modifiers.tsx +122 -0
- package/plugins/design/dev-server/use-keyboard-discipline.tsx +125 -0
- package/plugins/design/dev-server/use-undo-stack.tsx +355 -0
- package/plugins/design/dev-server/ws.ts +123 -7
- package/plugins/design/templates/_shell.html +51 -6
- package/plugins/design/templates/design-system-inspiration/SUB-AGENT-PROMPTS.md +245 -0
- package/plugins/design/templates/design-system-inspiration/_MAPPING.md +2 -2
- package/plugins/design/templates/design-system-inspiration/core/preview/_components.css.tpl +129 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/_motion-readme.md.tpl +63 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.css.tpl +106 -0
- package/plugins/design/templates/design-system-inspiration/core/preview/motion.tsx.tpl +208 -0
- package/plugins/flow/.claude-plugin/config.schema.json +12 -0
- package/plugins/flow/dependencies.json +143 -0
- package/plugins/flow/dependencies.schema.json +107 -0
- /package/plugins/design/templates/design-system-inspiration/core/preview/{motion.html → .archive/motion.html} +0 -0
|
@@ -18,18 +18,17 @@
|
|
|
18
18
|
import { useEffect, useRef } from 'react';
|
|
19
19
|
|
|
20
20
|
import { useArtboardsContext } from './canvas-lib.tsx';
|
|
21
|
+
import { DRAG_THRESHOLD_PX } from './input-router.tsx';
|
|
21
22
|
import { type Selection, useSelectionSet } from './use-selection-set.tsx';
|
|
22
23
|
import { useToolMode } from './use-tool-mode.tsx';
|
|
23
24
|
|
|
24
|
-
const DRAG_THRESHOLD_PX = 4;
|
|
25
|
-
|
|
26
25
|
const MARQUEE_CSS = `
|
|
27
26
|
.dc-cv-marquee {
|
|
28
27
|
position: fixed;
|
|
29
28
|
pointer-events: none;
|
|
30
29
|
z-index: 5;
|
|
31
|
-
border: 1px solid var(--accent, #0d99ff);
|
|
32
|
-
background: color-mix(in oklab, var(--accent, #0d99ff) 8%, transparent);
|
|
30
|
+
border: 1px solid var(--maude-hud-accent, #0d99ff);
|
|
31
|
+
background: color-mix(in oklab, var(--maude-hud-accent, #0d99ff) 8%, transparent);
|
|
33
32
|
display: none;
|
|
34
33
|
}
|
|
35
34
|
`.trim();
|
|
@@ -52,7 +51,7 @@ function shouldIgnoreTarget(t: EventTarget | null): boolean {
|
|
|
52
51
|
// Skip floating chrome and existing overlays (their click handlers run).
|
|
53
52
|
if (
|
|
54
53
|
el.closest(
|
|
55
|
-
'.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu, .dc-cv-halo, .dc-cv-group-bbox, .cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, .dc-annot-svg, .dc-annot-ctx, .dc-tp-popover, .dc-annot-resize-handle'
|
|
54
|
+
'.dc-mm, .dc-zoom-tb, .dc-tool-palette, .dc-context-menu, .dc-cv-halo, .dc-cv-group-bbox, .cm-composer, .cm-thread, .cm-mention-popup, .cm-pin, .dc-annot-svg, .dc-annot-ctx, .dc-tp-popover, .dc-annot-resize-handle, .dc-multi-artboard-tb, .dc-elem-ctx-tb, .dc-cv-eq-spacing-layer'
|
|
56
55
|
)
|
|
57
56
|
) {
|
|
58
57
|
return true;
|
|
@@ -125,7 +124,12 @@ export function ArtboardMarqueeOverlay() {
|
|
|
125
124
|
stateRef.current = null;
|
|
126
125
|
const div = overlayRef.current;
|
|
127
126
|
if (div) div.style.display = 'none';
|
|
128
|
-
if (!s.crossed)
|
|
127
|
+
if (!s.crossed) {
|
|
128
|
+
// Bare click on empty world without drag → clear selection
|
|
129
|
+
// (post-Wave-3 user feedback). Shift-click preserves it.
|
|
130
|
+
if (!s.shift) selSet.clear();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
129
133
|
if (!artboardsCtx) return;
|
|
130
134
|
|
|
131
135
|
// Intersect the marquee AABB with each artboard's screen-coord bbox.
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# asset-sweep.sh — pre-scaffold real-asset sweep for `/design:setup-ds`.
|
|
3
|
+
# Greps a target repo for production sources of logo/mark/wordmark/mascot/glyph/
|
|
4
|
+
# illustration BEFORE Batch A authors any placeholder. Single hit at a
|
|
5
|
+
# conventional path → copy 1:1. Multiple hits → caller asks user via
|
|
6
|
+
# AskUserQuestion. Zero hits → THEN placeholder authorship is permitted.
|
|
7
|
+
#
|
|
8
|
+
# Closes D-2 in the studyfi imprint-bootstrap retro (placeholder bleed).
|
|
9
|
+
# Spec: `.ai/plans/phase-3.7-setup-ds-hardening-and-motion-subsystem.md` Task 1.
|
|
10
|
+
#
|
|
11
|
+
# Usage:
|
|
12
|
+
# asset-sweep.sh --root <repo> [--query "logo,mark,wordmark,mascot,glyph,illustration"]
|
|
13
|
+
# [--depth 6] [--ext "svg,png"]
|
|
14
|
+
#
|
|
15
|
+
# Defaults:
|
|
16
|
+
# --query "logo,mark,wordmark,mascot,glyph,illustration"
|
|
17
|
+
# --depth 6 (`find -maxdepth`; capped to keep monorepo sweeps < 2s)
|
|
18
|
+
# --ext "svg" (svg-only by default — production marks are vector)
|
|
19
|
+
#
|
|
20
|
+
# Output: one JSON object on stdout, e.g.
|
|
21
|
+
# { "logo": ["packages/ui/src/components/ui/logo/logo.svg"],
|
|
22
|
+
# "mark": [],
|
|
23
|
+
# "wordmark": [],
|
|
24
|
+
# "mascot": [],
|
|
25
|
+
# "glyph": ["packages/ui/.../glyphs/sparkle.svg",
|
|
26
|
+
# "packages/ui/.../glyphs/check.svg"],
|
|
27
|
+
# "illustration": [] }
|
|
28
|
+
#
|
|
29
|
+
# Paths are relative to --root. Sorted lexicographically. Duplicates removed.
|
|
30
|
+
# Excludes: node_modules/, .git/, .design/_history/, .next/, dist/, build/.
|
|
31
|
+
#
|
|
32
|
+
# Exit: 0 always (zero hits is a valid result, not an error). 2 = bad args.
|
|
33
|
+
|
|
34
|
+
ROOT=""
|
|
35
|
+
QUERY="logo,mark,wordmark,mascot,glyph,illustration"
|
|
36
|
+
DEPTH=6
|
|
37
|
+
EXT="svg"
|
|
38
|
+
|
|
39
|
+
while [ $# -gt 0 ]; do
|
|
40
|
+
case "$1" in
|
|
41
|
+
--root) ROOT="$2"; shift 2 ;;
|
|
42
|
+
--query) QUERY="$2"; shift 2 ;;
|
|
43
|
+
--depth) DEPTH="$2"; shift 2 ;;
|
|
44
|
+
--ext) EXT="$2"; shift 2 ;;
|
|
45
|
+
--help|-h)
|
|
46
|
+
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
|
47
|
+
exit 0
|
|
48
|
+
;;
|
|
49
|
+
*)
|
|
50
|
+
echo "asset-sweep.sh: unknown arg '$1' (try --help)" >&2
|
|
51
|
+
exit 2
|
|
52
|
+
;;
|
|
53
|
+
esac
|
|
54
|
+
done
|
|
55
|
+
|
|
56
|
+
if [ -z "$ROOT" ]; then
|
|
57
|
+
echo "asset-sweep.sh: --root <repo> required" >&2
|
|
58
|
+
exit 2
|
|
59
|
+
fi
|
|
60
|
+
if [ ! -d "$ROOT" ]; then
|
|
61
|
+
echo "asset-sweep.sh: --root '$ROOT' is not a directory" >&2
|
|
62
|
+
exit 2
|
|
63
|
+
fi
|
|
64
|
+
|
|
65
|
+
# Resolve --root to an absolute path so the relative-path strip below is stable
|
|
66
|
+
# regardless of caller cwd.
|
|
67
|
+
ROOT_ABS=$(cd "$ROOT" && pwd)
|
|
68
|
+
|
|
69
|
+
# Build the find -name predicate for the extension list.
|
|
70
|
+
# EXT="svg,png" → \( -name "*.svg" -o -name "*.png" \)
|
|
71
|
+
ext_predicate=()
|
|
72
|
+
IFS=',' read -r -a EXT_ARR <<< "$EXT"
|
|
73
|
+
ext_predicate+=("(")
|
|
74
|
+
first=1
|
|
75
|
+
for e in "${EXT_ARR[@]}"; do
|
|
76
|
+
e_trim=$(echo "$e" | tr -d '[:space:]')
|
|
77
|
+
[ -z "$e_trim" ] && continue
|
|
78
|
+
if [ $first -eq 1 ]; then
|
|
79
|
+
ext_predicate+=("-name" "*.$e_trim")
|
|
80
|
+
first=0
|
|
81
|
+
else
|
|
82
|
+
ext_predicate+=("-o" "-name" "*.$e_trim")
|
|
83
|
+
fi
|
|
84
|
+
done
|
|
85
|
+
ext_predicate+=(")")
|
|
86
|
+
|
|
87
|
+
# Single find pass; bucket by noun in-memory. Earlier per-noun-find walked the
|
|
88
|
+
# tree N times (6× for default query) — ~4s on a 20k-file repo. Single pass
|
|
89
|
+
# pushes us under the 2s/50k budget from the phase plan.
|
|
90
|
+
#
|
|
91
|
+
# Match policy (case-insensitive): a file is bucketed under noun N if N appears
|
|
92
|
+
# as a substring of the basename OR as a path segment in the parent chain
|
|
93
|
+
# (catches `packages/ui/.../logo/logo.svg` AND `assets/glyphs/foo.svg`).
|
|
94
|
+
|
|
95
|
+
# Normalize nouns once.
|
|
96
|
+
IFS=',' read -r -a NOUNS <<< "$QUERY"
|
|
97
|
+
declare -a NOUNS_LC=()
|
|
98
|
+
for n in "${NOUNS[@]}"; do
|
|
99
|
+
n_trim=$(echo "$n" | tr -d '[:space:]')
|
|
100
|
+
[ -z "$n_trim" ] && continue
|
|
101
|
+
NOUNS_LC+=("$(printf '%s' "$n_trim" | tr '[:upper:]' '[:lower:]')")
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
# Buckets keyed by noun. Bash 4+ supports declare -A; macOS ships bash 3.2
|
|
105
|
+
# by default — fall back to a per-noun delimiter-joined string in flat vars.
|
|
106
|
+
declare -a BUCKETS=()
|
|
107
|
+
for _ in "${NOUNS_LC[@]}"; do BUCKETS+=(""); done
|
|
108
|
+
|
|
109
|
+
# Walk once. Prune vendor/build dirs to keep the tree small.
|
|
110
|
+
while IFS= read -r f; do
|
|
111
|
+
[ -z "$f" ] && continue
|
|
112
|
+
rel="${f#"$ROOT_ABS"/}"
|
|
113
|
+
base="${f##*/}"
|
|
114
|
+
base_lc=$(printf '%s' "$base" | tr '[:upper:]' '[:lower:]')
|
|
115
|
+
parent="${f%/*}"
|
|
116
|
+
parent_lc=$(printf '%s' "$parent" | tr '[:upper:]' '[:lower:]')
|
|
117
|
+
i=0
|
|
118
|
+
for noun in "${NOUNS_LC[@]}"; do
|
|
119
|
+
matched=0
|
|
120
|
+
case "$base_lc" in *"$noun"*) matched=1 ;; esac
|
|
121
|
+
if [ $matched -eq 0 ]; then
|
|
122
|
+
case "$parent_lc" in
|
|
123
|
+
*"/$noun"*|*"/$noun"|*"/${noun}s"*|*"/${noun}s") matched=1 ;;
|
|
124
|
+
esac
|
|
125
|
+
fi
|
|
126
|
+
if [ $matched -eq 1 ]; then
|
|
127
|
+
if [ -z "${BUCKETS[$i]}" ]; then
|
|
128
|
+
BUCKETS[$i]="$rel"
|
|
129
|
+
else
|
|
130
|
+
BUCKETS[$i]="${BUCKETS[$i]}"$'\n'"$rel"
|
|
131
|
+
fi
|
|
132
|
+
fi
|
|
133
|
+
i=$((i + 1))
|
|
134
|
+
done
|
|
135
|
+
done < <(
|
|
136
|
+
find "$ROOT_ABS" \
|
|
137
|
+
-maxdepth "$DEPTH" \
|
|
138
|
+
\( \
|
|
139
|
+
-path "*/node_modules" -o \
|
|
140
|
+
-path "*/.git" -o \
|
|
141
|
+
-path "*/.design/_history" -o \
|
|
142
|
+
-path "*/.next" -o \
|
|
143
|
+
-path "*/dist" -o \
|
|
144
|
+
-path "*/build" -o \
|
|
145
|
+
-path "*/coverage" -o \
|
|
146
|
+
-path "*/.turbo" -o \
|
|
147
|
+
-path "*/.cache" \
|
|
148
|
+
\) -prune -o \
|
|
149
|
+
-type f \
|
|
150
|
+
"${ext_predicate[@]}" \
|
|
151
|
+
-print 2>/dev/null
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
# JSON emission. Avoid jq as a hard dep (matches server-up.sh's defensive
|
|
155
|
+
# posture) — the JSON shape is small + predictable.
|
|
156
|
+
printf '{'
|
|
157
|
+
first=1
|
|
158
|
+
i=0
|
|
159
|
+
for noun in "${NOUNS_LC[@]}"; do
|
|
160
|
+
if [ $first -eq 0 ]; then printf ','; fi
|
|
161
|
+
first=0
|
|
162
|
+
printf '\n "%s": [' "$noun"
|
|
163
|
+
if [ -n "${BUCKETS[$i]}" ]; then
|
|
164
|
+
# Sort + dedupe, then JSON-emit.
|
|
165
|
+
printf '\n'
|
|
166
|
+
ln_first=1
|
|
167
|
+
while IFS= read -r hit; do
|
|
168
|
+
[ -z "$hit" ] && continue
|
|
169
|
+
esc=$(printf '%s' "$hit" | sed -e 's/\\/\\\\/g' -e 's/"/\\"/g')
|
|
170
|
+
if [ $ln_first -eq 0 ]; then printf ',\n'; fi
|
|
171
|
+
ln_first=0
|
|
172
|
+
printf ' "%s"' "$esc"
|
|
173
|
+
done < <(printf '%s\n' "${BUCKETS[$i]}" | sort -u)
|
|
174
|
+
printf '\n ]'
|
|
175
|
+
else
|
|
176
|
+
printf ']'
|
|
177
|
+
fi
|
|
178
|
+
i=$((i + 1))
|
|
179
|
+
done
|
|
180
|
+
printf '\n}\n'
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# preflight.sh — design plugin dependency health check.
|
|
3
|
+
# Thin shell wrapper over `node cli/lib/preflight.mjs --plugin design`.
|
|
4
|
+
# Keeps the detection logic in one place (the .mjs lib) while exposing the
|
|
5
|
+
# bash-friendly modes (--shell-export, --warn-only) that init.md and the
|
|
6
|
+
# SessionStart hook need.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# preflight.sh # text table (humans)
|
|
10
|
+
# preflight.sh --json # machine-readable envelope
|
|
11
|
+
# preflight.sh --shell-export # `export DEPS_OK=... DEPS_MISSING=...`
|
|
12
|
+
# preflight.sh --quiet # silent on success, one-liner on miss
|
|
13
|
+
# preflight.sh --warn-only # same as --quiet (SessionStart hook)
|
|
14
|
+
#
|
|
15
|
+
# Resolution (DDR-045): never compute paths from `dirname $0` inside a
|
|
16
|
+
# `bun --compile` standalone binary — that maps to /$bunfs/root. The CLI
|
|
17
|
+
# entry stays Node, so we resolve via $CLAUDE_PLUGIN_ROOT first, with the
|
|
18
|
+
# script-dir fallback only used when running uncompiled from the repo.
|
|
19
|
+
#
|
|
20
|
+
# Exit: 0 if all hard deps pass; 1 otherwise.
|
|
21
|
+
|
|
22
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
23
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
24
|
+
PKG_ROOT="$(cd "$PLUGIN_ROOT/../.." && pwd)"
|
|
25
|
+
|
|
26
|
+
# The Node lib needs cwd to be the package root (it resolves dependencies.json
|
|
27
|
+
# via `plugins/<plugin>/dependencies.json`). Subshell so we don't mutate the
|
|
28
|
+
# caller's cwd.
|
|
29
|
+
(
|
|
30
|
+
cd "$PKG_ROOT" || exit 1
|
|
31
|
+
exec node "$PKG_ROOT/cli/lib/preflight.mjs" --plugin design "$@"
|
|
32
|
+
)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# runtime-health.sh — verify the dev-server is serving the on-disk pre-built
|
|
3
|
+
# runtime bundles, not a defective dynamic Bun.build output.
|
|
4
|
+
#
|
|
5
|
+
# Why: a stale dev-server process can cache a broken dynamic build of
|
|
6
|
+
# /_canvas-runtime/<slug>.js (e.g. a 409-line motion_react.js with a hoisting
|
|
7
|
+
# bug instead of the 10056-line pre-built). The canvas TSX parses cleanly,
|
|
8
|
+
# server returns HTTP 200, but the iframe throws at module-eval time with
|
|
9
|
+
# `ReferenceError: AcceleratedAnimation is not defined` (or similar).
|
|
10
|
+
#
|
|
11
|
+
# Parse-clean ≠ run-clean. This helper closes that gap: probe every URL the
|
|
12
|
+
# canvas-lib pulls in, compare byte-count to the disk pre-built, fail loud
|
|
13
|
+
# when the served body is suspiciously small.
|
|
14
|
+
#
|
|
15
|
+
# Usage:
|
|
16
|
+
# runtime-health.sh [--port N] [--root <repo>] [--threshold 0.5]
|
|
17
|
+
# [--restart] [--quiet]
|
|
18
|
+
#
|
|
19
|
+
# Behavior:
|
|
20
|
+
# --restart → on detected defect, kill the server PID from _server.json
|
|
21
|
+
# and respawn via server-up.sh. Exit 0 if the restart heals.
|
|
22
|
+
# --quiet → silence per-bundle PASS lines; only print failures + summary.
|
|
23
|
+
#
|
|
24
|
+
# Exit codes:
|
|
25
|
+
# 0 all bundles healthy (or --restart healed the defect)
|
|
26
|
+
# 1 server unreachable / no _server.json
|
|
27
|
+
# 2 bad args
|
|
28
|
+
# 3 defective bundle detected (and either no --restart, or restart did not heal)
|
|
29
|
+
|
|
30
|
+
PORT=""
|
|
31
|
+
REPO=""
|
|
32
|
+
THRESHOLD="0.5"
|
|
33
|
+
RESTART=0
|
|
34
|
+
QUIET=0
|
|
35
|
+
|
|
36
|
+
while [ $# -gt 0 ]; do
|
|
37
|
+
case "$1" in
|
|
38
|
+
--port) PORT="$2"; shift 2 ;;
|
|
39
|
+
--root) REPO="$2"; shift 2 ;;
|
|
40
|
+
--threshold) THRESHOLD="$2"; shift 2 ;;
|
|
41
|
+
--restart) RESTART=1; shift ;;
|
|
42
|
+
--quiet) QUIET=1; shift ;;
|
|
43
|
+
--help|-h)
|
|
44
|
+
sed -n '2,30p' "$0" | sed 's/^# \?//'
|
|
45
|
+
exit 0
|
|
46
|
+
;;
|
|
47
|
+
*)
|
|
48
|
+
echo "runtime-health.sh: unknown arg '$1' (try --help)" >&2
|
|
49
|
+
exit 2
|
|
50
|
+
;;
|
|
51
|
+
esac
|
|
52
|
+
done
|
|
53
|
+
|
|
54
|
+
# ---------- repo + plugin root resolution ----------
|
|
55
|
+
if [ -z "$REPO" ]; then
|
|
56
|
+
REPO="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
57
|
+
fi
|
|
58
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
59
|
+
PLUGIN_ROOT="${CLAUDE_PLUGIN_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
|
|
60
|
+
PREBUILT_DIR="$PLUGIN_ROOT/dev-server/dist/runtime"
|
|
61
|
+
if [ ! -d "$PREBUILT_DIR" ]; then
|
|
62
|
+
PREBUILT_DIR="$SCRIPT_DIR/../dist/runtime"
|
|
63
|
+
fi
|
|
64
|
+
if [ ! -d "$PREBUILT_DIR" ]; then
|
|
65
|
+
echo "runtime-health.sh: pre-built runtime dir not found at $PREBUILT_DIR" >&2
|
|
66
|
+
echo " (canvas runtime bundles ship in <plugin>/dev-server/dist/runtime/)" >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# ---------- resolve port from _server.json if not given ----------
|
|
71
|
+
DESIGN_ROOT="$REPO/.design"
|
|
72
|
+
STATE="$DESIGN_ROOT/_server.json"
|
|
73
|
+
if [ -z "$PORT" ]; then
|
|
74
|
+
if [ ! -f "$STATE" ]; then
|
|
75
|
+
echo "runtime-health.sh: no --port given and $STATE not found (run server-up.sh first)" >&2
|
|
76
|
+
exit 1
|
|
77
|
+
fi
|
|
78
|
+
if command -v jq >/dev/null 2>&1; then
|
|
79
|
+
PORT=$(jq -r .port "$STATE" 2>/dev/null)
|
|
80
|
+
else
|
|
81
|
+
PORT=$(sed -nE 's/.*"port"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
|
|
82
|
+
fi
|
|
83
|
+
fi
|
|
84
|
+
[ -z "$PORT" ] && { echo "runtime-health.sh: could not resolve port" >&2; exit 1; }
|
|
85
|
+
|
|
86
|
+
BASE="http://localhost:$PORT"
|
|
87
|
+
|
|
88
|
+
# Confirm server alive before probing individual bundles.
|
|
89
|
+
if ! curl -fs "$BASE/_health" >/dev/null 2>&1; then
|
|
90
|
+
echo "runtime-health.sh: server not responding at $BASE/_health" >&2
|
|
91
|
+
exit 1
|
|
92
|
+
fi
|
|
93
|
+
|
|
94
|
+
# ---------- per-bundle probe ----------
|
|
95
|
+
probe_one() {
|
|
96
|
+
local slug="$1"
|
|
97
|
+
local disk_size="$2"
|
|
98
|
+
local url="$BASE/_canvas-runtime/$slug"
|
|
99
|
+
local served
|
|
100
|
+
served=$(curl -fs -o /dev/null -w '%{size_download}' "$url" 2>/dev/null) || {
|
|
101
|
+
echo "✗ $slug — served HTTP error (curl failed for $url)" >&2
|
|
102
|
+
return 1
|
|
103
|
+
}
|
|
104
|
+
# Floor at 256 bytes — any working ESM bundle is bigger than that.
|
|
105
|
+
if [ "$served" -lt 256 ]; then
|
|
106
|
+
echo "✗ $slug — served body $served B < 256 B floor (server returned empty)" >&2
|
|
107
|
+
return 1
|
|
108
|
+
fi
|
|
109
|
+
# Threshold ratio: served must be ≥ threshold × disk.
|
|
110
|
+
# awk handles the fractional math without bc dependency.
|
|
111
|
+
local ratio
|
|
112
|
+
ratio=$(awk -v s="$served" -v d="$disk_size" 'BEGIN{ if(d==0){print 0}else{printf "%.3f", s/d} }')
|
|
113
|
+
local ok
|
|
114
|
+
ok=$(awk -v r="$ratio" -v t="$THRESHOLD" 'BEGIN{print (r >= t) ? 1 : 0}')
|
|
115
|
+
if [ "$ok" = "1" ]; then
|
|
116
|
+
[ $QUIET -eq 0 ] && echo "✓ $slug — $served B / $disk_size B disk (ratio $ratio)" >&2
|
|
117
|
+
return 0
|
|
118
|
+
fi
|
|
119
|
+
echo "✗ $slug — served $served B / $disk_size B disk (ratio $ratio < $THRESHOLD) — defective dynamic build" >&2
|
|
120
|
+
return 1
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
FAIL_COUNT=0
|
|
124
|
+
FAIL_LIST=""
|
|
125
|
+
for f in "$PREBUILT_DIR"/*.js; do
|
|
126
|
+
[ -f "$f" ] || continue
|
|
127
|
+
SLUG=$(basename "$f")
|
|
128
|
+
DISK_SIZE=$(wc -c < "$f" | tr -d ' ')
|
|
129
|
+
if ! probe_one "$SLUG" "$DISK_SIZE"; then
|
|
130
|
+
FAIL_COUNT=$((FAIL_COUNT + 1))
|
|
131
|
+
FAIL_LIST="$FAIL_LIST $SLUG"
|
|
132
|
+
fi
|
|
133
|
+
done
|
|
134
|
+
|
|
135
|
+
if [ "$FAIL_COUNT" -eq 0 ]; then
|
|
136
|
+
[ $QUIET -eq 0 ] && echo "✓ runtime-health OK — all bundles match disk pre-built (threshold $THRESHOLD)" >&2
|
|
137
|
+
exit 0
|
|
138
|
+
fi
|
|
139
|
+
|
|
140
|
+
echo "" >&2
|
|
141
|
+
echo "✗ runtime-health FAIL — $FAIL_COUNT bundle(s) below threshold:$FAIL_LIST" >&2
|
|
142
|
+
echo " These bundles look like defective dynamic Bun.build output." >&2
|
|
143
|
+
echo " The canvas TSX will parse + serve cleanly, but the iframe will throw at runtime." >&2
|
|
144
|
+
|
|
145
|
+
if [ "$RESTART" -eq 1 ]; then
|
|
146
|
+
echo "→ --restart given; killing server and respawning via server-up.sh" >&2
|
|
147
|
+
if [ -f "$STATE" ]; then
|
|
148
|
+
if command -v jq >/dev/null 2>&1; then
|
|
149
|
+
PID=$(jq -r .pid "$STATE" 2>/dev/null)
|
|
150
|
+
else
|
|
151
|
+
PID=$(sed -nE 's/.*"pid"[[:space:]]*:[[:space:]]*([0-9]+).*/\1/p' "$STATE" | head -n1)
|
|
152
|
+
fi
|
|
153
|
+
if [ -n "$PID" ] && kill -0 "$PID" 2>/dev/null; then
|
|
154
|
+
kill "$PID" 2>/dev/null
|
|
155
|
+
sleep 1
|
|
156
|
+
kill -0 "$PID" 2>/dev/null && kill -9 "$PID" 2>/dev/null
|
|
157
|
+
fi
|
|
158
|
+
rm -f "$STATE"
|
|
159
|
+
fi
|
|
160
|
+
NEW_PORT=$(bash "$SCRIPT_DIR/server-up.sh" --root "$REPO")
|
|
161
|
+
[ -z "$NEW_PORT" ] && { echo "✗ server-up.sh did not return a port after restart" >&2; exit 3; }
|
|
162
|
+
echo "→ server restarted port=$NEW_PORT; re-probing once" >&2
|
|
163
|
+
# Re-probe via tail-recursive invocation with no --restart (avoid infinite loop).
|
|
164
|
+
bash "$0" --port "$NEW_PORT" --root "$REPO" --threshold "$THRESHOLD" $( [ $QUIET -eq 1 ] && echo --quiet )
|
|
165
|
+
exit $?
|
|
166
|
+
fi
|
|
167
|
+
|
|
168
|
+
echo " Remediation: rerun with --restart, OR manually 'kill $(cat $STATE 2>/dev/null | sed -nE 's/.*pid.*:.*([0-9]+).*/\1/p') && bash $SCRIPT_DIR/server-up.sh'" >&2
|
|
169
|
+
exit 3
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# visual-sanity.sh — mandatory post-scaffold visual sanity gate for
|
|
3
|
+
# `/design:setup-ds`. Boots the dev-server (via server-up.sh), screenshots
|
|
4
|
+
# N specimens (via screenshot.sh) into `_history/_system/<ds>-visual-sanity-
|
|
5
|
+
# <ISO>/`, and exits non-zero on any failure so the caller can surface an
|
|
6
|
+
# `AskUserQuestion` ("dev-server boot failed: <reason> — skip visual sanity
|
|
7
|
+
# or fix and retry?"). Never silently elides the gate.
|
|
8
|
+
#
|
|
9
|
+
# Closes D-3 + D-4 in the imprint-bootstrap retro (sparkle overflow + broken
|
|
10
|
+
# relative-URL asset path — both would have been caught by one screenshot read).
|
|
11
|
+
# Spec: `.ai/plans/phase-3.7-setup-ds-hardening-and-motion-subsystem.md` Task 2.
|
|
12
|
+
#
|
|
13
|
+
# Usage:
|
|
14
|
+
# visual-sanity.sh --ds <ds-name>
|
|
15
|
+
# [--specimens "colors-accent,motion,logo"]
|
|
16
|
+
# [--root <repo>] [--out-dir <dir>]
|
|
17
|
+
# [--boot-timeout 15] [--shot-timeout 5]
|
|
18
|
+
# [--engine auto|agent-browser|playwright]
|
|
19
|
+
#
|
|
20
|
+
# Defaults:
|
|
21
|
+
# --root "$CLAUDE_PROJECT_DIR" → "$(git rev-parse --show-toplevel)" → cwd
|
|
22
|
+
# --specimens "colors-accent,motion,ui_kits-desktop-showcase"
|
|
23
|
+
# (signature trio — accent / motion / DS-in-use)
|
|
24
|
+
# caller may override with the roster-derived list of actually-
|
|
25
|
+
# written specimens to avoid screenshotting files that don't
|
|
26
|
+
# exist on the disk yet.
|
|
27
|
+
# --out-dir "<root>/.design/_history/_system/<ds>-visual-sanity-<ISO>"
|
|
28
|
+
# --boot-timeout 15 (server-up.sh polling window, in seconds)
|
|
29
|
+
# --shot-timeout 5 (screenshot.sh DC-mount poll window per specimen; bare
|
|
30
|
+
# DS-preview specimens have no [data-dc-screen] so the
|
|
31
|
+
# poll always exhausts — 5s is plenty for the page-load
|
|
32
|
+
# settle without burning 15s × N on every gate run)
|
|
33
|
+
#
|
|
34
|
+
# Output: one PNG path per line on stdout (composable in for-loops); the caller
|
|
35
|
+
# is expected to `Read` each PNG into context so the agent ACTUALLY SEES
|
|
36
|
+
# what shipped. JSON manifest written alongside as `_manifest.json`.
|
|
37
|
+
# Stderr: diagnostic, engine choice, timing, failure reasons.
|
|
38
|
+
# Exit:
|
|
39
|
+
# 0 success — all specimens screenshot successfully
|
|
40
|
+
# 1 dev-server boot failed (caller MUST surface AskUserQuestion)
|
|
41
|
+
# 2 bad args
|
|
42
|
+
# 3 one or more specimen screenshots failed
|
|
43
|
+
# 4 no specimens existed on disk (DS not scaffolded? typo?)
|
|
44
|
+
|
|
45
|
+
DS=""
|
|
46
|
+
SPECIMENS=""
|
|
47
|
+
ROOT=""
|
|
48
|
+
OUT_DIR=""
|
|
49
|
+
BOOT_TIMEOUT=15
|
|
50
|
+
SHOT_TIMEOUT=5
|
|
51
|
+
ENGINE="auto"
|
|
52
|
+
|
|
53
|
+
while [ $# -gt 0 ]; do
|
|
54
|
+
case "$1" in
|
|
55
|
+
--ds) DS="$2"; shift 2 ;;
|
|
56
|
+
--specimens) SPECIMENS="$2"; shift 2 ;;
|
|
57
|
+
--root) ROOT="$2"; shift 2 ;;
|
|
58
|
+
--out-dir) OUT_DIR="$2"; shift 2 ;;
|
|
59
|
+
--boot-timeout) BOOT_TIMEOUT="$2"; shift 2 ;;
|
|
60
|
+
--shot-timeout) SHOT_TIMEOUT="$2"; shift 2 ;;
|
|
61
|
+
--timeout) BOOT_TIMEOUT="$2"; SHOT_TIMEOUT="$2"; shift 2 ;; # legacy
|
|
62
|
+
--engine) ENGINE="$2"; shift 2 ;;
|
|
63
|
+
--help|-h)
|
|
64
|
+
sed -n '2,33p' "$0" | sed 's/^# \?//'
|
|
65
|
+
exit 0
|
|
66
|
+
;;
|
|
67
|
+
*)
|
|
68
|
+
echo "visual-sanity.sh: unknown arg '$1' (try --help)" >&2
|
|
69
|
+
exit 2
|
|
70
|
+
;;
|
|
71
|
+
esac
|
|
72
|
+
done
|
|
73
|
+
|
|
74
|
+
if [ -z "$DS" ]; then
|
|
75
|
+
echo "visual-sanity.sh: --ds <ds-name> required" >&2
|
|
76
|
+
exit 2
|
|
77
|
+
fi
|
|
78
|
+
|
|
79
|
+
# Resolve repo root.
|
|
80
|
+
if [ -z "$ROOT" ]; then
|
|
81
|
+
ROOT="${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}"
|
|
82
|
+
fi
|
|
83
|
+
if [ ! -d "$ROOT" ]; then
|
|
84
|
+
echo "visual-sanity.sh: --root '$ROOT' is not a directory" >&2
|
|
85
|
+
exit 2
|
|
86
|
+
fi
|
|
87
|
+
ROOT_ABS=$(cd "$ROOT" && pwd)
|
|
88
|
+
|
|
89
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
90
|
+
|
|
91
|
+
DESIGN_ROOT="$ROOT_ABS/.design"
|
|
92
|
+
DS_PREVIEW="$DESIGN_ROOT/system/$DS/preview"
|
|
93
|
+
if [ ! -d "$DS_PREVIEW" ]; then
|
|
94
|
+
echo "visual-sanity.sh: preview directory not found at $DS_PREVIEW" >&2
|
|
95
|
+
echo " (is the DS scaffolded? did you mean a different --ds value?)" >&2
|
|
96
|
+
exit 4
|
|
97
|
+
fi
|
|
98
|
+
|
|
99
|
+
# Default specimen list: the signature trio (accent, motion, DS-in-use).
|
|
100
|
+
# Caller can pass a roster-derived list to override.
|
|
101
|
+
if [ -z "$SPECIMENS" ]; then
|
|
102
|
+
SPECIMENS="colors-accent,motion,ui_kits-desktop-showcase"
|
|
103
|
+
fi
|
|
104
|
+
|
|
105
|
+
# Filter specimens to those that actually exist on disk — the trio is a
|
|
106
|
+
# default that not every DS will have written (e.g. an early scaffold may
|
|
107
|
+
# not have ui_kits-* yet). Silently skipping a non-existent specimen is OK;
|
|
108
|
+
# the caller logs which ones we attempted vs. captured.
|
|
109
|
+
declare -a EXISTING=()
|
|
110
|
+
declare -a MISSING=()
|
|
111
|
+
IFS=',' read -r -a SPEC_ARR <<< "$SPECIMENS"
|
|
112
|
+
for s in "${SPEC_ARR[@]}"; do
|
|
113
|
+
s_trim=$(echo "$s" | tr -d '[:space:]')
|
|
114
|
+
[ -z "$s_trim" ] && continue
|
|
115
|
+
if [ -f "$DS_PREVIEW/${s_trim}.tsx" ]; then
|
|
116
|
+
EXISTING+=("$s_trim")
|
|
117
|
+
else
|
|
118
|
+
MISSING+=("$s_trim")
|
|
119
|
+
fi
|
|
120
|
+
done
|
|
121
|
+
if [ ${#EXISTING[@]} -eq 0 ]; then
|
|
122
|
+
echo "visual-sanity.sh: none of the requested specimens exist on disk under $DS_PREVIEW" >&2
|
|
123
|
+
printf ' missing: %s\n' "${MISSING[@]}" >&2
|
|
124
|
+
exit 4
|
|
125
|
+
fi
|
|
126
|
+
if [ ${#MISSING[@]} -gt 0 ]; then
|
|
127
|
+
echo "visual-sanity.sh: skipping ${#MISSING[@]} non-existent specimen(s): ${MISSING[*]}" >&2
|
|
128
|
+
fi
|
|
129
|
+
|
|
130
|
+
# ISO-like timestamp safe for filenames (no colons / spaces).
|
|
131
|
+
TS=$(date -u +"%Y%m%dT%H%M%SZ")
|
|
132
|
+
|
|
133
|
+
if [ -z "$OUT_DIR" ]; then
|
|
134
|
+
OUT_DIR="$DESIGN_ROOT/_history/_system/${DS}-visual-sanity-${TS}"
|
|
135
|
+
fi
|
|
136
|
+
mkdir -p "$OUT_DIR"
|
|
137
|
+
|
|
138
|
+
# Step 1 — boot the dev-server. Failure here is fatal: the caller MUST surface
|
|
139
|
+
# AskUserQuestion ("dev-server boot failed: <reason>; skip visual sanity or
|
|
140
|
+
# fix and retry?") rather than silently skipping the gate.
|
|
141
|
+
echo "→ booting dev-server (timeout ${BOOT_TIMEOUT}s)" >&2
|
|
142
|
+
PORT=$(bash "$SCRIPT_DIR/server-up.sh" --root "$ROOT_ABS" --timeout "$BOOT_TIMEOUT")
|
|
143
|
+
SERVER_RC=$?
|
|
144
|
+
if [ $SERVER_RC -ne 0 ] || [ -z "$PORT" ]; then
|
|
145
|
+
echo "✗ dev-server boot failed (server-up.sh exit $SERVER_RC); see $DESIGN_ROOT/_server.log" >&2
|
|
146
|
+
cat > "$OUT_DIR/_manifest.json" <<EOF
|
|
147
|
+
{
|
|
148
|
+
"ds": "$DS",
|
|
149
|
+
"ts": "$TS",
|
|
150
|
+
"status": "server-boot-failed",
|
|
151
|
+
"server_up_rc": $SERVER_RC,
|
|
152
|
+
"specimens_requested": $(printf '%s\n' "${SPEC_ARR[@]}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//' | sed '/^$/d' | sed 's/.*/"&"/' | paste -sd, -),
|
|
153
|
+
"specimens_captured": []
|
|
154
|
+
}
|
|
155
|
+
EOF
|
|
156
|
+
exit 1
|
|
157
|
+
fi
|
|
158
|
+
echo "✓ dev-server on port $PORT" >&2
|
|
159
|
+
|
|
160
|
+
# Step 2 — screenshot each existing specimen. Track captures vs failures.
|
|
161
|
+
declare -a CAPTURED=()
|
|
162
|
+
declare -a FAILED=()
|
|
163
|
+
for s in "${EXISTING[@]}"; do
|
|
164
|
+
REL="system/$DS/preview/${s}.tsx"
|
|
165
|
+
REL_ENC=$(printf '%s' "$REL" | sed 's/ /%20/g')
|
|
166
|
+
URL="http://localhost:${PORT}/_canvas-shell.html?canvas=${REL_ENC}"
|
|
167
|
+
OUT_PNG="$OUT_DIR/${s}.png"
|
|
168
|
+
echo "→ screenshot: $s" >&2
|
|
169
|
+
if bash "$SCRIPT_DIR/screenshot.sh" \
|
|
170
|
+
--url "$URL" \
|
|
171
|
+
--full \
|
|
172
|
+
--out "$OUT_PNG" \
|
|
173
|
+
--engine "$ENGINE" \
|
|
174
|
+
--timeout "$SHOT_TIMEOUT" >&2; then
|
|
175
|
+
echo "$OUT_PNG"
|
|
176
|
+
CAPTURED+=("$s")
|
|
177
|
+
else
|
|
178
|
+
echo "✗ screenshot failed: $s" >&2
|
|
179
|
+
FAILED+=("$s")
|
|
180
|
+
fi
|
|
181
|
+
done
|
|
182
|
+
|
|
183
|
+
# Step 3 — write manifest. Caller can parse this to know what was captured
|
|
184
|
+
# without re-listing the directory.
|
|
185
|
+
json_arr() {
|
|
186
|
+
# Print a JSON array literal from positional args. Empty → "[]".
|
|
187
|
+
if [ $# -eq 0 ]; then printf '[]'; return; fi
|
|
188
|
+
printf '['
|
|
189
|
+
i=0
|
|
190
|
+
for a in "$@"; do
|
|
191
|
+
if [ $i -gt 0 ]; then printf ', '; fi
|
|
192
|
+
printf '"%s"' "$a"
|
|
193
|
+
i=$((i + 1))
|
|
194
|
+
done
|
|
195
|
+
printf ']'
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
STATUS="ok"
|
|
199
|
+
[ ${#FAILED[@]} -gt 0 ] && STATUS="partial-failure"
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
printf '{\n'
|
|
203
|
+
printf ' "ds": "%s",\n' "$DS"
|
|
204
|
+
printf ' "ts": "%s",\n' "$TS"
|
|
205
|
+
printf ' "status": "%s",\n' "$STATUS"
|
|
206
|
+
printf ' "port": %s,\n' "$PORT"
|
|
207
|
+
printf ' "out_dir": "%s",\n' "$OUT_DIR"
|
|
208
|
+
printf ' "specimens_requested": %s,\n' "$(json_arr "${SPEC_ARR[@]}")"
|
|
209
|
+
printf ' "specimens_captured": %s,\n' "$(json_arr "${CAPTURED[@]}")"
|
|
210
|
+
printf ' "specimens_missing": %s,\n' "$(json_arr "${MISSING[@]}")"
|
|
211
|
+
printf ' "specimens_failed": %s\n' "$(json_arr "${FAILED[@]}")"
|
|
212
|
+
printf '}\n'
|
|
213
|
+
} > "$OUT_DIR/_manifest.json"
|
|
214
|
+
|
|
215
|
+
echo "→ manifest: $OUT_DIR/_manifest.json" >&2
|
|
216
|
+
|
|
217
|
+
if [ ${#FAILED[@]} -gt 0 ]; then
|
|
218
|
+
echo "✗ ${#FAILED[@]}/${#EXISTING[@]} specimen(s) failed to capture: ${FAILED[*]}" >&2
|
|
219
|
+
exit 3
|
|
220
|
+
fi
|
|
221
|
+
exit 0
|