@atlassian/chippy 0.0.0-stage → 0.0.1-beta.1

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.
Files changed (126) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +188 -2
  3. package/bin/chippy.mjs +134 -0
  4. package/bin/pack.mjs +184 -0
  5. package/lib/avalanche.js +2570 -0
  6. package/lib/avalanche.js.map +7 -0
  7. package/lib/bootstrap.d.ts +1 -0
  8. package/lib/bootstrap.js +51 -0
  9. package/lib/bootstrap.js.map +7 -0
  10. package/lib/browser.js +15 -0
  11. package/lib/browser.js.map +7 -0
  12. package/lib/extension/avalanche.es.js +5396 -0
  13. package/lib/extension/avalanche.es.js.map +1 -0
  14. package/lib/extension/chippy-page-bridge.js +1833 -0
  15. package/lib/extension/inspect-react.js +216 -0
  16. package/lib/extension/layout-lab.js +1453 -0
  17. package/lib/extension/panel-dom.js +90 -0
  18. package/lib/extension/resolve-probes.js +342 -0
  19. package/lib/mcp-skill/LICENSE +201 -0
  20. package/lib/mcp-skill/README.md +150 -0
  21. package/lib/mcp-skill/SKILL.md +330 -0
  22. package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
  23. package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
  24. package/lib/mcp-skill/extension/avalanche.es.js +5396 -0
  25. package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
  26. package/lib/mcp-skill/extension/avalanche.iife.js +5397 -0
  27. package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
  28. package/lib/mcp-skill/extension/background.js +323 -0
  29. package/lib/mcp-skill/extension/chippy-page-bridge.js +1833 -0
  30. package/lib/mcp-skill/extension/content-script.js +167 -0
  31. package/lib/mcp-skill/extension/devtools-hook-shim.js +68 -0
  32. package/lib/mcp-skill/extension/early-bootstrap.js +47 -0
  33. package/lib/mcp-skill/extension/icon-128.png +0 -0
  34. package/lib/mcp-skill/extension/icon-16.png +0 -0
  35. package/lib/mcp-skill/extension/icon-32.png +0 -0
  36. package/lib/mcp-skill/extension/icon-48.png +0 -0
  37. package/lib/mcp-skill/extension/inspect-react.js +216 -0
  38. package/lib/mcp-skill/extension/layout-lab.js +1453 -0
  39. package/lib/mcp-skill/extension/logo.svg +14 -0
  40. package/lib/mcp-skill/extension/manifest.json +69 -0
  41. package/lib/mcp-skill/extension/panel-dom.js +90 -0
  42. package/lib/mcp-skill/extension/panel-settings.js +191 -0
  43. package/lib/mcp-skill/extension/popup.html +41 -0
  44. package/lib/mcp-skill/extension/popup.js +80 -0
  45. package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
  46. package/lib/mcp-skill/extension/resolve-probes.js +342 -0
  47. package/lib/mcp-skill/package.json +1 -0
  48. package/lib/mcp-skill/references/README.md +56 -0
  49. package/lib/mcp-skill/references/active-stimulus.md +85 -0
  50. package/lib/mcp-skill/references/avalanche-detector.md +731 -0
  51. package/lib/mcp-skill/references/bdd-router.md +310 -0
  52. package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
  53. package/lib/mcp-skill/references/command-output-formats.md +1075 -0
  54. package/lib/mcp-skill/references/command-surface.md +183 -0
  55. package/lib/mcp-skill/references/cynefin.md +99 -0
  56. package/lib/mcp-skill/references/debug-hook.md +161 -0
  57. package/lib/mcp-skill/references/fixing-performance.md +490 -0
  58. package/lib/mcp-skill/references/hook-metrics.md +199 -0
  59. package/lib/mcp-skill/references/hooks-inspection.md +61 -0
  60. package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
  61. package/lib/mcp-skill/references/mcp.md +122 -0
  62. package/lib/mcp-skill/references/mode-a-injection.md +68 -0
  63. package/lib/mcp-skill/references/operating-model.md +128 -0
  64. package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
  65. package/lib/mcp-skill/references/playwright-probes.md +205 -0
  66. package/lib/mcp-skill/references/procedures.md +545 -0
  67. package/lib/mcp-skill/references/recipes.txt +114 -0
  68. package/lib/mcp-skill/references/stability-render.md +428 -0
  69. package/lib/mcp-skill/references/test-mode-playbook.md +193 -0
  70. package/lib/mcp-skill/references/troubleshooting.md +54 -0
  71. package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
  72. package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
  73. package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
  74. package/lib/mcp-skill/scripts/chippy-helper-server.js +1710 -0
  75. package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
  76. package/lib/mcp-skill/scripts/chippy.sh +1218 -0
  77. package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
  78. package/lib/mcp-skill/scripts/helper-access.js +144 -0
  79. package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
  80. package/lib/mcp-skill/scripts/search-recipes.js +248 -0
  81. package/lib/mcp-skill/snippets/README.md +89 -0
  82. package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
  83. package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
  84. package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
  85. package/lib/mcp-skill/snippets/find-prop.js +83 -0
  86. package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
  87. package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
  88. package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
  89. package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
  90. package/lib/package.json +1 -0
  91. package/lib/plugin.js +119 -0
  92. package/lib/plugin.js.map +7 -0
  93. package/lib/storybook/avalanche-panel-model.js +79 -0
  94. package/lib/storybook/brand.jsx +6 -0
  95. package/lib/storybook/connection-fallback.jsx +47 -0
  96. package/lib/storybook/constants.js +15 -0
  97. package/lib/storybook/context-card.jsx +171 -0
  98. package/lib/storybook/manager.jsx +403 -0
  99. package/lib/storybook/preset.js +7 -0
  100. package/lib/storybook/preview.js +263 -0
  101. package/lib/storybook/probe-annotation-editor.jsx +52 -0
  102. package/lib/storybook/probe-intent.js +23 -0
  103. package/lib/storybook/setup.js +93 -0
  104. package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
  105. package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
  106. package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
  107. package/lib/types/avalanche/detector.d.ts +183 -0
  108. package/lib/types/avalanche/interaction-capture.d.ts +25 -0
  109. package/lib/types/avalanche/internals.d.ts +189 -0
  110. package/lib/types/avalanche/report-serializer.d.ts +283 -0
  111. package/lib/types/avalanche/reporter.d.ts +610 -0
  112. package/lib/types/chippy-bridge/fiber-source.d.ts +37 -0
  113. package/lib/types/chippy-bridge/react-renderer.d.ts +49 -0
  114. package/lib/types/chippy-bridge/source-map.d.ts +30 -0
  115. package/lib/types/debug-render/core.d.ts +72 -0
  116. package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
  117. package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
  118. package/lib/types/layout-lab/marker-context.d.ts +94 -0
  119. package/lib/types/package/avalanche.d.ts +6 -0
  120. package/lib/types/package/browser.d.ts +10 -0
  121. package/lib/types/package/plugin.d.ts +15 -0
  122. package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
  123. package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
  124. package/lib/types/plugins/index.d.ts +49 -0
  125. package/lib/types/plugins/jira/context-provider.d.ts +36 -0
  126. package/package.json +104 -4
@@ -0,0 +1,1218 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ BASE_URL="http://127.0.0.1:7823/__chippy"
5
+ SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
6
+ HELPER_ENTRY="$SCRIPT_DIR/chippy-helper-server.js"
7
+ HELPER_TOKEN_FILE="${CHIPPY_STATE_DIR:-$HOME/.chippy}/helper-7823.token"
8
+ AUTH_HEADER=()
9
+ CURL_OPTS=(-fsS)
10
+
11
+ AUTH_HEADER_FILE=""
12
+
13
+ # The helper writes its access token after it starts listening. Reload it after
14
+ # starting the helper; CHIPPY_HELPER_TOKEN overrides the file. curl reads the
15
+ # header from a private file so the token does not appear in the process list.
16
+ load_helper_token() {
17
+ local token="${CHIPPY_HELPER_TOKEN:-}"
18
+ if [[ -z "$token" && -r "$HELPER_TOKEN_FILE" ]]; then
19
+ token=$(tr -d '[:space:]' <"$HELPER_TOKEN_FILE")
20
+ fi
21
+ AUTH_HEADER=()
22
+ if [[ -n "$token" ]]; then
23
+ if [[ -z "$AUTH_HEADER_FILE" ]]; then
24
+ AUTH_HEADER_FILE=$(mktemp "${TMPDIR:-/tmp}/chippy-auth.XXXXXX")
25
+ trap 'rm -f "$AUTH_HEADER_FILE"' EXIT
26
+ fi
27
+ printf 'X-Chippy-Token: %s\n' "$token" >"$AUTH_HEADER_FILE"
28
+ AUTH_HEADER=(-H "@$AUTH_HEADER_FILE")
29
+ fi
30
+ CURL_OPTS=(-fsS ${AUTH_HEADER[@]+"${AUTH_HEADER[@]}"})
31
+ }
32
+ load_helper_token
33
+
34
+ usage() {
35
+ cat <<'EOF'
36
+ Usage:
37
+ ./scripts/chippy.sh start [--host <address>]
38
+ ./scripts/chippy.sh stop
39
+ ./scripts/chippy.sh health
40
+ ./scripts/chippy.sh inject-url
41
+ ./scripts/chippy.sh recipes <query...> [--limit N]
42
+ ./scripts/chippy.sh pages [slice]
43
+ ./scripts/chippy.sh recent-pages
44
+ ./scripts/chippy.sh page <clientId> [slice]
45
+ ./scripts/chippy.sh snapshot <clientId> [slice]
46
+ ./scripts/chippy.sh request-snapshot <clientId>
47
+ ./scripts/chippy.sh wait <clientId> [timeoutSeconds]
48
+ ./scripts/chippy.sh activate <clientId>
49
+ ./scripts/chippy.sh deactivate <clientId>
50
+ ./scripts/chippy.sh context-ui <clientId> <on|off>
51
+ ./scripts/chippy.sh ensure-probes <clientId>
52
+ ./scripts/chippy.sh clear-probes <clientId>
53
+ ./scripts/chippy.sh show-status <clientId> <text>
54
+ ./scripts/chippy.sh hide-status <clientId>
55
+ ./scripts/chippy.sh command <type> [clientId]
56
+ ./scripts/chippy.sh get-computed-style <clientId> <probeId> [parentLevel] [selector]
57
+ ./scripts/chippy.sh probe-dom <clientId> <probeId> [parentLevel]
58
+ ./scripts/chippy.sh probe-parents <clientId> <probeId> [max]
59
+ ./scripts/chippy.sh probe-boundaries <clientId> <probeId> [max]
60
+ ./scripts/chippy.sh probe-owners <clientId> <probeId>
61
+ ./scripts/chippy.sh probe-owner <clientId> <probeId> <ownerIndex>
62
+ ./scripts/chippy.sh probe-props <clientId> <probeId> [ownerIndex]
63
+ ./scripts/chippy.sh probe-prop-value <clientId> <probeId> <propName> [ownerIndex]
64
+ ./scripts/chippy.sh probe-hooks <clientId> <probeId>
65
+ ./scripts/chippy.sh probe-hooks-summary <clientId> <probeId>
66
+ ./scripts/chippy.sh page-click <clientId> <selector|probeId|fiberId>
67
+ ./scripts/chippy.sh fiber-prop-contract <clientId> <fiberId> <propName>
68
+ ./scripts/chippy.sh fiber-invoke <clientId> <fiberId> <propName> [--args JSON]
69
+ ./scripts/chippy.sh fiber-schedule-update <clientId> <fiberId> [--ownerIndex N]
70
+ ./scripts/chippy.sh find-fibers <clientId> [--name regex] [--file regex] [--prop name] [--testid regex] [--text regex] [--max N]
71
+ ./scripts/chippy.sh fiber-state-capture <clientId> [label] [maxFibers]
72
+ ./scripts/chippy.sh fiber-state-list
73
+ ./scripts/chippy.sh fiber-state-compare <beforeCaptureId> <afterCaptureId> [maxChanges]
74
+ ./scripts/chippy.sh page-boundaries <clientId> [max]
75
+ ./scripts/chippy.sh inspect-fiber-hooks <clientId> <fiberId>
76
+ ./scripts/chippy.sh inspect-fiber-hooks-summary <clientId> <fiberId>
77
+ ./scripts/chippy.sh inspect-fiber-props <clientId> <fiberId>
78
+ ./scripts/chippy.sh inspect-fiber-owners <clientId> <fiberId>
79
+ ./scripts/chippy.sh inspect-fiber-parents <clientId> <fiberId> [max]
80
+ ./scripts/chippy.sh inspect-fiber-boundaries <clientId> <fiberId> [max]
81
+ ./scripts/chippy.sh inspect-fiber-structure <clientId> <fiberId> <hookIndex> [windowSize]
82
+ ./scripts/chippy.sh describe-component <clientId> <fiberIdOrName> [--text|--json]
83
+ ./scripts/chippy.sh explain-component <clientId> <probeId|fiberId|name> [--text|--json|--complexity]
84
+ ./scripts/chippy.sh avalanche-clear <clientId>
85
+ ./scripts/chippy.sh avalanche-status <clientId>
86
+ ./scripts/chippy.sh avalanche-export <clientId> [lastN]
87
+ ./scripts/chippy.sh avalanche-hot-sources <clientId> [limit]
88
+ ./scripts/chippy.sh avalanche-commit-list <clientId> [top] [by]
89
+ ./scripts/chippy.sh avalanche-commit-peek <clientId> <commitId> [include] [depth] [maxItems]
90
+ ./scripts/chippy.sh avalanche-describe-action <clientId> <actionId>
91
+ ./scripts/chippy.sh avalanche-install <clientId> [detectionMode]
92
+ ./scripts/chippy.sh avalanche-recapture <clientId> [detectionMode]
93
+ ./scripts/chippy.sh avalanche-debug-hook <clientId> <sourceId> <hookIndex> [window]
94
+ ./scripts/chippy.sh avalanche-save <clientId> <file> [count]
95
+ ./scripts/chippy.sh avalanche-offline <file> <summary|commit-list|commit-peek|hot-sources|describe-action|source-search> [args...]
96
+ ./scripts/chippy.sh eval-page <clientId> <snippet-file-or-name> [--key value ...]
97
+ ./scripts/chippy.sh eval-snippets
98
+ ./scripts/chippy.sh avalanche-arm <clientId> <origin> [maxCommits] [captureFibers=true|false] [detectionMode=sources|full|stats|amplifiers]
99
+ ./scripts/chippy.sh avalanche-disarm <clientId> <origin>
100
+ ./scripts/chippy.sh page-reload <clientId>
101
+ ./scripts/chippy.sh page-wait-reload <clientId> [timeoutMs]
102
+ ./scripts/chippy.sh reload-with-avalanche <clientId> <origin> [maxCommits] [timeoutMs] [settleMs] [captureFibers=true|false] [detectionMode=sources|full|stats|amplifiers]
103
+ ./scripts/chippy.sh handoff-to-playwright <clientId> [url] [probeIdsCsv] [browserName] [headless]
104
+ ./scripts/chippy.sh proxy-state
105
+ ./scripts/chippy.sh proxy-start <targetPort> [originalUrl]
106
+ ./scripts/chippy.sh proxy-stop
107
+
108
+ Notes:
109
+ - Base URL is fixed to http://127.0.0.1:7823/__chippy.
110
+ - recipes searches the local task corpus and does not contact the helper.
111
+ - slice is one of: minimal, standard, full.
112
+ - wait performs one cancellable long poll; it does not install or modify host hooks.
113
+ - Prefer the explicit subcommands over the generic 'command' form.
114
+ EOF
115
+ }
116
+
117
+ post_avalanche() {
118
+ local client_id="$1"
119
+ local body="$2"
120
+ post_json "/api/pages/$client_id/avalanche" "$body"
121
+ }
122
+
123
+ json_escape() {
124
+ node -e 'process.stdout.write(JSON.stringify(process.argv[1]))' "$1"
125
+ }
126
+
127
+ helper_is_up() {
128
+ curl "${CURL_OPTS[@]}" "$BASE_URL/health" >/dev/null 2>&1
129
+ }
130
+
131
+ # Binds 127.0.0.1 unless --host or CHIPPY_HELPER_HOST asks for a wider address.
132
+ start_helper() {
133
+ local bind_host="${CHIPPY_HELPER_HOST:-127.0.0.1}"
134
+ if [[ "${1:-}" == "--host" ]]; then
135
+ bind_host="${2:?--host needs an address, for example 0.0.0.0}"
136
+ fi
137
+ if helper_is_up; then
138
+ echo "Chippy helper already up at $BASE_URL; stop it first to change the bind address" >&2
139
+ curl "${CURL_OPTS[@]}" "$BASE_URL/health"
140
+ return 0
141
+ fi
142
+
143
+ mkdir -p .agents
144
+ nohup node "$HELPER_ENTRY" --host "$bind_host" --port 7823 >/dev/null 2>&1 &
145
+ local pid=$!
146
+
147
+ for _ in $(seq 1 40); do
148
+ if helper_is_up; then
149
+ load_helper_token
150
+ echo "Chippy helper started (pid $pid)" >&2
151
+ curl "${CURL_OPTS[@]}" "$BASE_URL/health"
152
+ return 0
153
+ fi
154
+ sleep 0.25
155
+ done
156
+
157
+ echo "Failed to start Chippy helper." >&2
158
+ return 1
159
+ }
160
+
161
+ stop_helper() {
162
+ local helper_pattern="chippy-helper-server.js .*--port 7823"
163
+ if ! pgrep -f "$helper_pattern" >/dev/null 2>&1; then
164
+ echo "Chippy helper is not running" >&2
165
+ return 0
166
+ fi
167
+
168
+ pkill -f "$helper_pattern" >/dev/null 2>&1 || true
169
+
170
+ for _ in $(seq 1 20); do
171
+ if ! helper_is_up; then
172
+ echo "Chippy helper stopped" >&2
173
+ return 0
174
+ fi
175
+ sleep 0.25
176
+ done
177
+
178
+ echo "Timed out waiting for Chippy helper to stop" >&2
179
+ return 1
180
+ }
181
+
182
+ run_curl() {
183
+ local method="$1"
184
+ local url="$2"
185
+ local body="${3:-}"
186
+ local response_file stderr_file http_code code stderr_text
187
+ response_file=$(mktemp)
188
+ stderr_file=$(mktemp)
189
+ set +e
190
+ if [[ "$method" == "POST" ]]; then
191
+ http_code=$(curl -sS -o "$response_file" -w '%{http_code}' ${AUTH_HEADER[@]+"${AUTH_HEADER[@]}"} -X POST "$url" -H 'Content-Type: application/json' -d "$body" 2>"$stderr_file")
192
+ else
193
+ http_code=$(curl -sS -o "$response_file" -w '%{http_code}' ${AUTH_HEADER[@]+"${AUTH_HEADER[@]}"} "$url" 2>"$stderr_file")
194
+ fi
195
+ code=$?
196
+ set -e
197
+ stderr_text=$(cat "$stderr_file" 2>/dev/null || true)
198
+
199
+ if [[ $code -eq 0 && "$http_code" -lt 400 ]]; then
200
+ cat "$response_file"
201
+ rm -f "$response_file" "$stderr_file"
202
+ return 0
203
+ fi
204
+
205
+ case "$code" in
206
+ 7)
207
+ echo "Chippy helper request failed." >&2
208
+ echo " observed: curl_exit=7 request='$method $url'" >&2
209
+ echo " meaning: could not connect to helper at $BASE_URL" >&2
210
+ echo " next: start helper with ./.agents/skills/chippy/scripts/chippy.sh start" >&2
211
+ ;;
212
+ 28)
213
+ echo "Chippy helper request failed." >&2
214
+ echo " observed: curl_exit=28 request='$method $url'" >&2
215
+ echo " meaning: HTTP request itself timed out before a response completed" >&2
216
+ echo " next: retry, then check helper and page responsiveness with pages / request-snapshot" >&2
217
+ ;;
218
+ *)
219
+ if [[ "$http_code" == "504" ]]; then
220
+ echo "Chippy helper request failed." >&2
221
+ echo " observed: http_status=504 request='$method $url'" >&2
222
+ echo " meaning: helper answered the HTTP request, but timed out while waiting for an upstream result" >&2
223
+ echo " upstream in this setup is usually the connected page/runtime" >&2
224
+ echo " next: verify lighter round-trips first (pages, request-snapshot, avalanche-status) before retrying this command" >&2
225
+ elif [[ "$http_code" =~ ^5 ]]; then
226
+ echo "Chippy helper request failed." >&2
227
+ echo " observed: http_status=$http_code request='$method $url'" >&2
228
+ echo " meaning: helper returned a server error" >&2
229
+ elif [[ "$http_code" == "401" ]]; then
230
+ echo "Chippy helper request failed." >&2
231
+ echo " observed: http_status=401 request='$method $url'" >&2
232
+ echo " meaning: helper requires its access token and none matched" >&2
233
+ echo " next: the running helper writes its token to $HELPER_TOKEN_FILE; restart it with stop/start if that file is missing or stale, or set CHIPPY_HELPER_TOKEN" >&2
234
+ elif [[ "$http_code" =~ ^4 ]]; then
235
+ echo "Chippy helper request failed." >&2
236
+ echo " observed: http_status=$http_code request='$method $url'" >&2
237
+ echo " meaning: helper rejected the request" >&2
238
+ else
239
+ echo "Chippy helper request failed." >&2
240
+ echo " observed: curl_exit=$code request='$method $url'" >&2
241
+ echo " meaning: curl failed before a successful helper response was received" >&2
242
+ fi
243
+ ;;
244
+ esac
245
+ if [[ -n "$stderr_text" ]]; then
246
+ echo " → curl: $stderr_text" >&2
247
+ fi
248
+ rm -f "$response_file" "$stderr_file"
249
+ if [[ "$http_code" =~ ^[0-9]+$ && "$http_code" -ge 400 ]]; then
250
+ return 22
251
+ fi
252
+ return "$code"
253
+ }
254
+
255
+ post_json() {
256
+ local path="$1"
257
+ local body="$2"
258
+ run_curl POST "$BASE_URL$path" "$body"
259
+ }
260
+
261
+ print_probe_result_text() {
262
+ node "$SCRIPT_DIR/format-probe-result.js" "$1"
263
+ }
264
+
265
+ get_json() {
266
+ local path="$1"
267
+ run_curl GET "$BASE_URL$path"
268
+ }
269
+
270
+ send_command() {
271
+ local type="$1"
272
+ local client_id="${2:-}"
273
+ local extra_json="${3:-}"
274
+ local body
275
+
276
+ body="{"
277
+ if [[ -n "$client_id" ]]; then
278
+ body+="\"clientId\":$(json_escape "$client_id"),"
279
+ fi
280
+ body+="\"command\":{\"type\":$(json_escape "$type")"
281
+
282
+ if [[ -n "$extra_json" ]]; then
283
+ body+="${extra_json}"
284
+ fi
285
+
286
+ body+="}}"
287
+ post_json "/api/command" "$body"
288
+ }
289
+
290
+ cmd="${1:-}"
291
+ case "$cmd" in
292
+ ""|-h|--help|help)
293
+ usage
294
+ ;;
295
+ start)
296
+ start_helper "${@:2}"
297
+ ;;
298
+ stop)
299
+ stop_helper
300
+ ;;
301
+ health)
302
+ curl "${CURL_OPTS[@]}" "$BASE_URL/health"
303
+ ;;
304
+ inject-url)
305
+ echo "Import integration is document-scoped: a full reload/navigation removes Chippy and requires the user or automation to inject it again." >&2
306
+ printf '%s/inject.js\n' "$BASE_URL"
307
+ ;;
308
+ recipes)
309
+ shift
310
+ node "$SCRIPT_DIR/search-recipes.js" "$SCRIPT_DIR/../references/recipes.txt" "$@"
311
+ ;;
312
+ pages)
313
+ slice="${2:-minimal}"
314
+ curl "${CURL_OPTS[@]}" "$BASE_URL/api/pages?slice=$slice"
315
+ ;;
316
+ recent-pages)
317
+ curl "${CURL_OPTS[@]}" "$BASE_URL/api/recent-pages"
318
+ ;;
319
+ page)
320
+ client_id="${2:-}"
321
+ slice="${3:-standard}"
322
+ if [[ -z "$client_id" ]]; then
323
+ echo "page requires <clientId>" >&2
324
+ exit 1
325
+ fi
326
+ curl "${CURL_OPTS[@]}" "$BASE_URL/api/pages/$client_id?slice=$slice"
327
+ ;;
328
+ snapshot)
329
+ client_id="${2:-}"
330
+ slice="${3:-standard}"
331
+ if [[ -z "$client_id" ]]; then
332
+ echo "snapshot requires <clientId> [slice]" >&2
333
+ exit 1
334
+ fi
335
+ get_json "/api/pages/$client_id/snapshot?slice=$slice"
336
+ ;;
337
+ request-snapshot)
338
+ client_id="${2:-}"
339
+ if [[ -z "$client_id" ]]; then
340
+ echo "request-snapshot requires <clientId>" >&2
341
+ exit 1
342
+ fi
343
+ post_json "/api/pages/$client_id/snapshot" '{}'
344
+ ;;
345
+ wait)
346
+ client_id="${2:-}"
347
+ timeout_seconds="${3:-300}"
348
+ if [[ -z "$client_id" ]]; then
349
+ echo "wait requires <clientId> [timeoutSeconds]" >&2
350
+ exit 1
351
+ fi
352
+ if [[ ! "$timeout_seconds" =~ ^[1-9][0-9]*$ ]] || (( timeout_seconds > 1800 )); then
353
+ echo "timeoutSeconds must be an integer from 1 through 1800" >&2
354
+ exit 1
355
+ fi
356
+ get_json "/api/pages/$client_id/probe-annotations/wait?timeoutMs=$((timeout_seconds * 1000))"
357
+ ;;
358
+ activate)
359
+ client_id="${2:-}"
360
+ if [[ -z "$client_id" ]]; then
361
+ echo "activate requires <clientId>" >&2
362
+ exit 1
363
+ fi
364
+ send_command "activate" "$client_id"
365
+ ;;
366
+ deactivate)
367
+ client_id="${2:-}"
368
+ if [[ -z "$client_id" ]]; then
369
+ echo "deactivate requires <clientId>" >&2
370
+ exit 1
371
+ fi
372
+ send_command "deactivate" "$client_id"
373
+ ;;
374
+ context-ui)
375
+ command_name="$1"
376
+ client_id="${2:-}"
377
+ mode="${3:-}"
378
+ if [[ -z "$client_id" || -z "$mode" ]]; then
379
+ echo "$command_name requires <clientId> <on|off>" >&2
380
+ exit 1
381
+ fi
382
+ case "$mode" in
383
+ on|true|1) enabled=true ;;
384
+ off|false|0) enabled=false ;;
385
+ *)
386
+ echo "$command_name mode must be on or off" >&2
387
+ exit 1
388
+ ;;
389
+ esac
390
+ send_command "set-context-ui" "$client_id" ",\"enabled\":$enabled"
391
+ ;;
392
+ ensure-probes)
393
+ client_id="${2:-}"
394
+ if [[ -z "$client_id" ]]; then
395
+ echo "ensure-probes requires <clientId>" >&2
396
+ exit 1
397
+ fi
398
+ send_command "ensure-probes" "$client_id"
399
+ ;;
400
+ clear-probes)
401
+ client_id="${2:-}"
402
+ if [[ -z "$client_id" ]]; then
403
+ echo "clear-probes requires <clientId>" >&2
404
+ exit 1
405
+ fi
406
+ send_command "clear-probes" "$client_id"
407
+ ;;
408
+ show-status)
409
+ client_id="${2:-}"
410
+ text="${3:-}"
411
+ if [[ -z "$client_id" || -z "$text" ]]; then
412
+ echo "show-status requires <clientId> <text>" >&2
413
+ exit 1
414
+ fi
415
+ send_command "show-status" "$client_id" ",\"text\":$(json_escape "$text")"
416
+ ;;
417
+ hide-status)
418
+ client_id="${2:-}"
419
+ if [[ -z "$client_id" ]]; then
420
+ echo "hide-status requires <clientId>" >&2
421
+ exit 1
422
+ fi
423
+ send_command "hide-status" "$client_id"
424
+ ;;
425
+ command)
426
+ type="${2:-}"
427
+ client_id="${3:-}"
428
+ if [[ -z "$type" ]]; then
429
+ echo "command requires <type> [clientId]" >&2
430
+ exit 1
431
+ fi
432
+ send_command "$type" "$client_id"
433
+ ;;
434
+ get-computed-style)
435
+ client_id="${2:-}"
436
+ probe_id="${3:-}"
437
+ parent_level="${4:-0}"
438
+ selector="${5:-}"
439
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
440
+ echo "get-computed-style requires <clientId> <probeId> [parentLevel] [selector]" >&2
441
+ exit 1
442
+ fi
443
+ if [[ -n "$selector" ]]; then
444
+ body="{\"probeId\":$(json_escape "$probe_id"),\"parentLevel\":$parent_level,\"selector\":$(json_escape "$selector")}"
445
+ else
446
+ body="{\"probeId\":$(json_escape "$probe_id"),\"parentLevel\":$parent_level}"
447
+ fi
448
+ post_json "/api/pages/$client_id/computed-style" "$body"
449
+ ;;
450
+ probe-dom)
451
+ # Usage: probe-dom <clientId> <probeId> [parentLevel]
452
+ client_id="${2:-}"
453
+ probe_id="${3:-}"
454
+ parent_level="${4:-0}"
455
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
456
+ echo "probe-dom requires <clientId> <probeId> [parentLevel]" >&2
457
+ exit 1
458
+ fi
459
+ body="{\"command\":\"eval-probe-dom\",\"probeId\":$(json_escape "$probe_id"),\"parentLevel\":$parent_level}"
460
+ post_json "/api/pages/$client_id/eval-probe" "$body"
461
+ ;;
462
+ probe-parents)
463
+ client_id="${2:-}"
464
+ probe_id="${3:-}"
465
+ max="${4:-50}"
466
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
467
+ echo "probe-parents requires <clientId> <probeId> [max]" >&2
468
+ exit 1
469
+ fi
470
+ if [[ ! "$max" =~ ^[0-9]+$ ]]; then
471
+ echo "max must be a non-negative integer" >&2
472
+ exit 1
473
+ fi
474
+ body="{\"command\":\"eval-probe-parents\",\"probeId\":$(json_escape "$probe_id"),\"max\":$max}"
475
+ post_json "/api/pages/$client_id/eval-probe" "$body"
476
+ ;;
477
+ probe-boundaries)
478
+ client_id="${2:-}"
479
+ probe_id="${3:-}"
480
+ max="${4:-100}"
481
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
482
+ echo "probe-boundaries requires <clientId> <probeId> [max]" >&2
483
+ exit 1
484
+ fi
485
+ if [[ ! "$max" =~ ^[1-9][0-9]*$ ]]; then
486
+ echo "max must be a positive integer" >&2
487
+ exit 1
488
+ fi
489
+ body="{\"command\":\"eval-probe-boundaries\",\"probeId\":$(json_escape "$probe_id"),\"max\":$max}"
490
+ post_json "/api/pages/$client_id/eval-probe" "$body"
491
+ ;;
492
+ probe-owners)
493
+ client_id="${2:-}"
494
+ probe_id="${3:-}"
495
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
496
+ echo "probe-owners requires <clientId> <probeId>" >&2
497
+ exit 1
498
+ fi
499
+ get_json "/api/pages/$client_id/probe/$probe_id/owners"
500
+ ;;
501
+ probe-owner)
502
+ client_id="${2:-}"
503
+ probe_id="${3:-}"
504
+ owner_index="${4:-}"
505
+ if [[ -z "$client_id" || -z "$probe_id" || -z "$owner_index" ]]; then
506
+ echo "probe-owner requires <clientId> <probeId> <ownerIndex>" >&2
507
+ exit 1
508
+ fi
509
+ get_json "/api/pages/$client_id/probe/$probe_id/owners/$owner_index"
510
+ ;;
511
+ probe-props)
512
+ # Usage: probe-props <clientId> <probeId> [ownerIndex]
513
+ # ownerIndex: 0 = immediate owner, 1 = next owner up, etc.
514
+ client_id="${2:-}"
515
+ probe_id="${3:-}"
516
+ owner_index="${4:-}"
517
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
518
+ echo "probe-props requires <clientId> <probeId> [ownerIndex]" >&2
519
+ exit 1
520
+ fi
521
+ if [[ -n "$owner_index" && ! "$owner_index" =~ ^-?[0-9]+$ ]]; then
522
+ echo "ownerIndex must be an integer" >&2
523
+ exit 1
524
+ fi
525
+ if [[ -n "$owner_index" ]]; then
526
+ body="{\"command\":\"eval-probe-props\",\"probeId\":$(json_escape "$probe_id"),\"ownerIndex\":$owner_index}"
527
+ else
528
+ body="{\"command\":\"eval-probe-props\",\"probeId\":$(json_escape "$probe_id")}"
529
+ fi
530
+ post_json "/api/pages/$client_id/eval-probe" "$body"
531
+ ;;
532
+ probe-prop-value)
533
+ # Usage: probe-prop-value <clientId> <probeId> <propName> [ownerIndex]
534
+ # ownerIndex: 0 = immediate owner, 1 = next owner up, etc.
535
+ client_id="${2:-}"
536
+ probe_id="${3:-}"
537
+ prop_name="${4:-}"
538
+ owner_index="${5:-}"
539
+ if [[ -z "$client_id" || -z "$probe_id" || -z "$prop_name" ]]; then
540
+ echo "probe-prop-value requires <clientId> <probeId> <propName> [ownerIndex]" >&2
541
+ exit 1
542
+ fi
543
+ if [[ -n "$owner_index" && ! "$owner_index" =~ ^-?[0-9]+$ ]]; then
544
+ echo "ownerIndex must be an integer" >&2
545
+ exit 1
546
+ fi
547
+ if [[ -n "$owner_index" ]]; then
548
+ body="{\"command\":\"eval-prop-value\",\"probeId\":$(json_escape "$probe_id"),\"prop\":$(json_escape "$prop_name"),\"ownerIndex\":$owner_index}"
549
+ else
550
+ body="{\"command\":\"eval-prop-value\",\"probeId\":$(json_escape "$probe_id"),\"prop\":$(json_escape "$prop_name")}"
551
+ fi
552
+ post_json "/api/pages/$client_id/eval-probe" "$body"
553
+ ;;
554
+ probe-hooks)
555
+ client_id="${2:-}"
556
+ probe_id="${3:-}"
557
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
558
+ echo "probe-hooks requires <clientId> <probeId>" >&2
559
+ exit 1
560
+ fi
561
+ body="{\"command\":\"eval-probe-hooks\",\"probeId\":$(json_escape "$probe_id")}"
562
+ post_json "/api/pages/$client_id/eval-probe" "$body"
563
+ ;;
564
+ probe-hooks-summary)
565
+ client_id="${2:-}"
566
+ probe_id="${3:-}"
567
+ if [[ -z "$client_id" || -z "$probe_id" ]]; then
568
+ echo "probe-hooks-summary requires <clientId> <probeId>" >&2
569
+ exit 1
570
+ fi
571
+ body="{\"command\":\"eval-probe-hooks-summary\",\"probeId\":$(json_escape "$probe_id")}"
572
+ post_json "/api/pages/$client_id/eval-probe" "$body"
573
+ ;;
574
+ page-click)
575
+ client_id="${2:-}"
576
+ target_raw="${3:-}"
577
+ shift 3 || true
578
+ if [[ -z "$client_id" || -z "$target_raw" ]]; then
579
+ echo "page-click requires <clientId> <selector|probeId|fiberId>" >&2
580
+ exit 1
581
+ fi
582
+ if [[ $# -gt 0 ]]; then
583
+ echo "page-click does not support extra options; selector must resolve to exactly one target" >&2
584
+ exit 1
585
+ fi
586
+ if [[ "$target_raw" =~ ^[Pp]?[123]$ ]]; then
587
+ probe_id="${target_raw#[Pp]}"
588
+ body='{"probeId":'"$(json_escape "$probe_id")"'}'
589
+ elif [[ "$target_raw" =~ ^fiber:[0-9]+$ ]]; then
590
+ body='{"fiberId":'"${target_raw#fiber:}"'}'
591
+ elif [[ "$target_raw" =~ ^[0-9]+$ ]]; then
592
+ body='{"fiberId":'"$target_raw"'}'
593
+ else
594
+ body='{"selector":'"$(json_escape "$target_raw")"'}'
595
+ fi
596
+ post_json "/api/pages/$client_id/page-click" "$body"
597
+ ;;
598
+ fiber-prop-contract)
599
+ client_id="${2:-}"
600
+ fiber_id="${3:-}"
601
+ fiber_id="${fiber_id#fiber:}"
602
+ prop_name="${4:-}"
603
+ shift 4 || true
604
+ if [[ -z "$client_id" || -z "$fiber_id" || -z "$prop_name" ]]; then
605
+ echo "fiber-prop-contract requires <clientId> <fiberId> <propName>" >&2
606
+ exit 1
607
+ fi
608
+ if [[ $# -gt 0 ]]; then
609
+ echo "fiber-prop-contract does not support extra options" >&2
610
+ exit 1
611
+ fi
612
+ post_json "/api/pages/$client_id/fiber-prop-contract" '{"fiberId":'"$fiber_id"',"propName":'"$(json_escape "$prop_name")"'}'
613
+ ;;
614
+ fiber-invoke)
615
+ client_id="${2:-}"
616
+ fiber_id="${3:-}"
617
+ fiber_id="${fiber_id#fiber:}"
618
+ prop_name="${4:-}"
619
+ shift 4 || true
620
+ if [[ -z "$client_id" || -z "$fiber_id" || -z "$prop_name" ]]; then
621
+ echo "fiber-invoke requires <clientId> <fiberId> <propName> [--args JSON]" >&2
622
+ exit 1
623
+ fi
624
+ args='[]'
625
+ while [[ $# -gt 0 ]]; do
626
+ case "$1" in
627
+ --args)
628
+ if [[ $# -lt 2 ]]; then
629
+ echo "missing value for --args" >&2
630
+ exit 1
631
+ fi
632
+ args="$2"
633
+ shift
634
+ ;;
635
+ *) echo "unknown option for fiber-invoke: $1" >&2; exit 1 ;;
636
+ esac
637
+ shift
638
+ done
639
+ post_json "/api/pages/$client_id/fiber-invoke" '{"fiberId":'"$fiber_id"',"propName":'"$(json_escape "$prop_name")"',"args":'"$args"'}'
640
+ ;;
641
+ fiber-schedule-update)
642
+ client_id="${2:-}"
643
+ fiber_id="${3:-}"
644
+ fiber_id="${fiber_id#fiber:}"
645
+ shift 3 || true
646
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
647
+ echo "fiber-schedule-update requires <clientId> <fiberId> [--ownerIndex N]" >&2
648
+ exit 1
649
+ fi
650
+ owner_index=0
651
+ while [[ $# -gt 0 ]]; do
652
+ case "$1" in
653
+ --ownerIndex)
654
+ if [[ $# -lt 2 ]]; then
655
+ echo "missing value for --ownerIndex" >&2
656
+ exit 1
657
+ fi
658
+ owner_index="$2"
659
+ shift
660
+ ;;
661
+ *) echo "unknown option for fiber-schedule-update: $1" >&2; exit 1 ;;
662
+ esac
663
+ shift
664
+ done
665
+ post_json "/api/pages/$client_id/fiber-schedule-update" '{"fiberId":'"$(json_escape "$fiber_id")"',"ownerIndex":'"$owner_index"'}'
666
+ ;;
667
+ find-fibers)
668
+ client_id="${2:-}"
669
+ shift 2 || true
670
+ if [[ -z "$client_id" ]]; then
671
+ echo "find-fibers requires <clientId> [--name regex] [--file regex] [--prop name] [--testid regex] [--text regex] [--max N]" >&2
672
+ exit 1
673
+ fi
674
+ filters='{}'
675
+ while [[ $# -gt 0 ]]; do
676
+ if [[ "$1" == --* ]]; then
677
+ k="${1#--}"; v="${2:-}"; shift 2
678
+ filters=$(node -e 'const o=JSON.parse(process.argv[1]);o[process.argv[2]]=process.argv[3];process.stdout.write(JSON.stringify(o));' "$filters" "$k" "$v") || exit 1
679
+ else shift; fi
680
+ done
681
+ body=$(node -e 'process.stdout.write(JSON.stringify({command:"find-fibers",filters:JSON.parse(process.argv[1])}))' "$filters") || exit 1
682
+ post_json "/api/pages/$client_id/eval-probe" "$body"
683
+ ;;
684
+ fiber-state-capture)
685
+ client_id="${2:-}"
686
+ label="${3:-}"
687
+ max_fibers="${4:-100000}"
688
+ if [[ -z "$client_id" ]]; then
689
+ echo "fiber-state-capture requires <clientId> [label] [maxFibers]" >&2
690
+ exit 1
691
+ fi
692
+ if [[ ! "$max_fibers" =~ ^[1-9][0-9]*$ ]]; then
693
+ echo "maxFibers must be a positive integer" >&2
694
+ exit 1
695
+ fi
696
+ body="{\"label\":$(json_escape "$label"),\"maxFibers\":$max_fibers}"
697
+ post_json "/api/pages/$client_id/fiber-state-captures" "$body"
698
+ ;;
699
+ fiber-state-list)
700
+ get_json "/api/fiber-state-captures"
701
+ ;;
702
+ fiber-state-compare)
703
+ before_capture_id="${2:-}"
704
+ after_capture_id="${3:-}"
705
+ max_changes="${4:-200}"
706
+ if [[ -z "$before_capture_id" || -z "$after_capture_id" ]]; then
707
+ echo "fiber-state-compare requires <beforeCaptureId> <afterCaptureId> [maxChanges]" >&2
708
+ exit 1
709
+ fi
710
+ if [[ ! "$max_changes" =~ ^[1-9][0-9]*$ ]]; then
711
+ echo "maxChanges must be a positive integer" >&2
712
+ exit 1
713
+ fi
714
+ body="{\"before\":$(json_escape "$before_capture_id"),\"after\":$(json_escape "$after_capture_id"),\"maxChanges\":$max_changes}"
715
+ post_json "/api/fiber-state-comparisons" "$body"
716
+ ;;
717
+ page-boundaries)
718
+ client_id="${2:-}"
719
+ max="${3:-1000}"
720
+ if [[ -z "$client_id" ]]; then
721
+ echo "page-boundaries requires <clientId> [max]" >&2
722
+ exit 1
723
+ fi
724
+ if [[ ! "$max" =~ ^[1-9][0-9]*$ ]]; then
725
+ echo "max must be a positive integer" >&2
726
+ exit 1
727
+ fi
728
+ body="{\"command\":\"page-boundaries\",\"max\":$max}"
729
+ post_json "/api/pages/$client_id/eval-probe" "$body"
730
+ ;;
731
+ inspect-fiber-hooks)
732
+ client_id="${2:-}"
733
+ fiber_id="${3:-}"
734
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
735
+ echo "inspect-fiber-hooks requires <clientId> <fiberId>" >&2
736
+ exit 1
737
+ fi
738
+ body="{\"command\":\"inspect-fiber-hooks\",\"fiberId\":$(json_escape "$fiber_id")}"
739
+ post_json "/api/pages/$client_id/eval-probe" "$body"
740
+ ;;
741
+ inspect-fiber-hooks-summary)
742
+ client_id="${2:-}"
743
+ fiber_id="${3:-}"
744
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
745
+ echo "inspect-fiber-hooks-summary requires <clientId> <fiberId>" >&2
746
+ exit 1
747
+ fi
748
+ body="{\"command\":\"inspect-fiber-hooks-summary\",\"fiberId\":$(json_escape "$fiber_id")}"
749
+ post_json "/api/pages/$client_id/eval-probe" "$body"
750
+ ;;
751
+ inspect-fiber-props)
752
+ client_id="${2:-}"
753
+ fiber_id="${3:-}"
754
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
755
+ echo "inspect-fiber-props requires <clientId> <fiberId>" >&2
756
+ exit 1
757
+ fi
758
+ body="{\"command\":\"inspect-fiber-props\",\"fiberId\":$(json_escape "$fiber_id")}"
759
+ post_json "/api/pages/$client_id/eval-probe" "$body"
760
+ ;;
761
+ inspect-fiber-owners)
762
+ client_id="${2:-}"
763
+ fiber_id="${3:-}"
764
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
765
+ echo "inspect-fiber-owners requires <clientId> <fiberId>" >&2
766
+ exit 1
767
+ fi
768
+ body="{\"command\":\"inspect-fiber-owners\",\"fiberId\":$(json_escape "$fiber_id")}"
769
+ post_json "/api/pages/$client_id/eval-probe" "$body"
770
+ ;;
771
+ inspect-fiber-parents)
772
+ client_id="${2:-}"
773
+ fiber_id="${3:-}"
774
+ max="${4:-50}"
775
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
776
+ echo "inspect-fiber-parents requires <clientId> <fiberId> [max]" >&2
777
+ exit 1
778
+ fi
779
+ if [[ ! "$max" =~ ^[0-9]+$ ]]; then
780
+ echo "max must be a non-negative integer" >&2
781
+ exit 1
782
+ fi
783
+ body="{\"command\":\"inspect-fiber-parents\",\"fiberId\":$(json_escape "$fiber_id"),\"max\":$max}"
784
+ post_json "/api/pages/$client_id/eval-probe" "$body"
785
+ ;;
786
+ inspect-fiber-boundaries)
787
+ client_id="${2:-}"
788
+ fiber_id="${3:-}"
789
+ max="${4:-100}"
790
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
791
+ echo "inspect-fiber-boundaries requires <clientId> <fiberId> [max]" >&2
792
+ exit 1
793
+ fi
794
+ if [[ ! "$max" =~ ^[1-9][0-9]*$ ]]; then
795
+ echo "max must be a positive integer" >&2
796
+ exit 1
797
+ fi
798
+ body="{\"command\":\"inspect-fiber-boundaries\",\"fiberId\":$(json_escape "$fiber_id"),\"max\":$max}"
799
+ post_json "/api/pages/$client_id/eval-probe" "$body"
800
+ ;;
801
+ inspect-fiber-structure)
802
+ client_id="${2:-}"
803
+ fiber_id="${3:-}"
804
+ hook_index="${4:-0}"
805
+ window_size="${5:-3}"
806
+ if [[ -z "$client_id" || -z "$fiber_id" ]]; then
807
+ echo "inspect-fiber-structure requires <clientId> <fiberId> <hookIndex> [windowSize]" >&2
808
+ exit 1
809
+ fi
810
+ body="{\"command\":\"inspect-fiber-structure\",\"fiberId\":$(json_escape "$fiber_id"),\"hookIndex\":$hook_index,\"windowSize\":$window_size}"
811
+ post_json "/api/pages/$client_id/eval-probe" "$body"
812
+ ;;
813
+ describe-component)
814
+ # Usage: describe-component <clientId> <fiberIdOrName> [--text|--json]
815
+ # If arg starts with "fiber:" → resolve from registry; else treat as component name.
816
+ client_id="${2:-}"
817
+ target="${3:-}"
818
+ view="${4:---text}"
819
+ if [[ -z "$client_id" || -z "$target" ]]; then
820
+ echo "describe-component requires <clientId> <fiberIdOrName> [--text|--json]" >&2
821
+ exit 1
822
+ fi
823
+ if [[ "$view" != "--text" && "$view" != "--json" ]]; then
824
+ echo "describe-component view must be --text or --json" >&2
825
+ exit 1
826
+ fi
827
+ if [[ "$target" == fiber:* ]]; then
828
+ body="{\"command\":\"describe-component\",\"fiberId\":$(json_escape "$target")}"
829
+ else
830
+ body="{\"command\":\"describe-component\",\"name\":$(json_escape "$target")}"
831
+ fi
832
+ response=$(post_json "/api/pages/$client_id/eval-probe" "$body")
833
+ print_probe_result_text "${view#--}" <<<"$response"
834
+ ;;
835
+ explain-component)
836
+ # Usage: explain-component <clientId> <probeId|fiberId|name> [--text|--json|--complexity]
837
+ # Default --text prints semantic sections as plain text; --json preserves the full raw payload.
838
+ client_id="${2:-}"
839
+ target="${3:-}"
840
+ view="${4:---text}"
841
+ if [[ -z "$client_id" || -z "$target" ]]; then
842
+ echo "explain-component requires <clientId> <probeId|fiberId|name> [--text|--json|--complexity]" >&2
843
+ exit 1
844
+ fi
845
+ if [[ "$view" != "--text" && "$view" != "--json" && "$view" != "--complexity" ]]; then
846
+ echo "explain-component view must be --text, --json, or --complexity" >&2
847
+ exit 1
848
+ fi
849
+ if [[ "$target" =~ ^[0-9]+$ ]]; then
850
+ body="{\"command\":\"explain-component\",\"probeId\":$(json_escape "$target")}"
851
+ elif [[ "$target" == fiber:* ]]; then
852
+ body="{\"command\":\"explain-component\",\"fiberId\":$(json_escape "$target")}"
853
+ else
854
+ body="{\"command\":\"explain-component\",\"name\":$(json_escape "$target")}"
855
+ fi
856
+ response=$(post_json "/api/pages/$client_id/eval-probe" "$body")
857
+ print_probe_result_text "${view#--}" <<<"$response"
858
+ ;;
859
+ # NOTE: avalanche-start / avalanche-stop are removed in the upstream-shape bridge.
860
+ # The detector auto-installs on bundle eval; there is no pause/resume API. Use
861
+ # avalanche-clear (alias to upstream reset) to drop history.
862
+ avalanche-clear)
863
+ client_id="${2:-}"
864
+ if [[ -z "$client_id" ]]; then
865
+ echo "avalanche-clear requires <clientId>" >&2
866
+ exit 1
867
+ fi
868
+ post_avalanche "$client_id" '{"action":"clear"}'
869
+ ;;
870
+ avalanche-status)
871
+ client_id="${2:-}"
872
+ if [[ -z "$client_id" ]]; then
873
+ echo "avalanche-status requires <clientId>" >&2
874
+ exit 1
875
+ fi
876
+ post_avalanche "$client_id" '{"action":"status"}'
877
+ ;;
878
+ avalanche-export)
879
+ client_id="${2:-}"
880
+ last_n="${3:-}"
881
+ out_file="${4:-}"
882
+ if [[ -z "$client_id" ]]; then
883
+ echo "avalanche-export requires <clientId> [lastN] [outFile]" >&2
884
+ exit 1
885
+ fi
886
+ if [[ -n "$last_n" ]]; then
887
+ payload=$(post_avalanche "$client_id" "{\"action\":\"export-last\",\"count\":$last_n}")
888
+ else
889
+ payload=$(post_avalanche "$client_id" '{"action":"export"}')
890
+ fi
891
+ if [[ -n "$out_file" ]]; then
892
+ printf '%s' "$payload" > "$out_file"
893
+ echo "avalanche export written to $out_file" >&2
894
+ else
895
+ printf '%s' "$payload"
896
+ fi
897
+ ;;
898
+ # NOTE: avalanche-metrics / avalanche-help are removed. Upstream surface does
899
+ # not have these. Use avalanche-status for installed/commitCount overview.
900
+ avalanche-hot-sources)
901
+ client_id="${2:-}"
902
+ limit="${3:-}"
903
+ if [[ -z "$client_id" ]]; then
904
+ echo "avalanche-hot-sources requires <clientId> [limit]" >&2
905
+ exit 1
906
+ fi
907
+ if [[ -n "$limit" ]]; then
908
+ post_avalanche "$client_id" "{\"action\":\"hot-sources\",\"limit\":$limit}"
909
+ else
910
+ post_avalanche "$client_id" '{"action":"hot-sources"}'
911
+ fi
912
+ ;;
913
+ avalanche-commit-list)
914
+ client_id="${2:-}"
915
+ top="${3:-20}"
916
+ by="${4:-collateral}"
917
+ if [[ -z "$client_id" ]]; then
918
+ echo "avalanche-commit-list requires <clientId> [top] [by]" >&2
919
+ echo " live by ∈ collateral | sources | restarts | performedWork | effects | memos | callbacks | contextUpdates" >&2
920
+ echo " offline by additionally supports restartProxy | amplifiers | componentRenders | externalStoreRestarts | priorityInterruptions | fiberInterruptions | repeats" >&2
921
+ exit 1
922
+ fi
923
+ post_avalanche "$client_id" "{\"action\":\"commit-list\",\"top\":$top,\"by\":\"$by\"}"
924
+ ;;
925
+ avalanche-commit-peek)
926
+ client_id="${2:-}"
927
+ commit_id="${3:-}"
928
+ include="${4:-stats,sources,timing,metrics}"
929
+ depth="${5:-3}"
930
+ max_items="${6:-20}"
931
+ if [[ -z "$client_id" || -z "$commit_id" ]]; then
932
+ echo "avalanche-commit-peek requires <clientId> <commitId> [include] [depth] [maxItems]" >&2
933
+ echo " live include csv ⊆ stats,sources,timing,metrics,unmounts,actions,rendered (atl: alias of metrics)" >&2
934
+ echo " note: \"rendered\" requires arming with detectionMode=full" >&2
935
+ echo " offline include csv ⊆ stats,sources,collaterals,context,amplifiers,metrics" >&2
936
+ exit 1
937
+ fi
938
+ include_json="["
939
+ first=1
940
+ IFS=',' read -ra parts <<< "$include"
941
+ for p in "${parts[@]}"; do
942
+ p_trim=$(echo "$p" | tr -d ' ')
943
+ [[ -z "$p_trim" ]] && continue
944
+ if [[ $first -eq 0 ]]; then include_json+=","; fi
945
+ include_json+="\"$p_trim\""
946
+ first=0
947
+ done
948
+ include_json+="]"
949
+ post_avalanche "$client_id" "{\"action\":\"commit-peek\",\"commitId\":$commit_id,\"include\":$include_json,\"depth\":$depth,\"maxItems\":$max_items}"
950
+ ;;
951
+ avalanche-describe-action)
952
+ client_id="${2:-}"
953
+ action_id="${3:-}"
954
+ if [[ -z "$client_id" || -z "$action_id" ]]; then
955
+ echo "avalanche-describe-action requires <clientId> <actionId>" >&2
956
+ exit 1
957
+ fi
958
+ post_avalanche "$client_id" "{\"action\":\"describe-action\",\"actionId\":$action_id}"
959
+ ;;
960
+ avalanche-install)
961
+ client_id="${2:-}"
962
+ detection_mode="${3:-sources}"
963
+ if [[ -z "$client_id" ]]; then
964
+ echo "avalanche-install requires <clientId> [detectionMode]" >&2
965
+ exit 1
966
+ fi
967
+ body="{\"action\":\"install\",\"options\":{\"captureFibers\":true,\"detectionMode\":\"$detection_mode\",\"historySize\":1000}}"
968
+ post_avalanche "$client_id" "$body"
969
+ ;;
970
+ avalanche-recapture)
971
+ client_id="${2:-}"
972
+ detection_mode="${3:-sources}"
973
+ if [[ -z "$client_id" ]]; then
974
+ echo "avalanche-recapture requires <clientId> [detectionMode]" >&2
975
+ exit 1
976
+ fi
977
+ body="{\"action\":\"install\",\"options\":{\"captureFibers\":true,\"detectionMode\":\"$detection_mode\",\"historySize\":1000}}"
978
+ post_avalanche "$client_id" "$body" >/dev/null
979
+ post_avalanche "$client_id" '{"action":"clear"}' >/dev/null
980
+ post_avalanche "$client_id" '{"action":"status"}'
981
+ ;;
982
+ avalanche-debug-hook)
983
+ client_id="${2:-}"
984
+ source_id="${3:-}"
985
+ hook_index="${4:-}"
986
+ window_size="${5:-3}"
987
+ if [[ -z "$client_id" || -z "$source_id" || -z "$hook_index" ]]; then
988
+ echo "avalanche-debug-hook requires <clientId> <sourceId> <hookIndex> [window]" >&2
989
+ exit 1
990
+ fi
991
+ source_json=$(json_escape "$source_id")
992
+ post_avalanche "$client_id" "{\"action\":\"debug-hook\",\"sourceId\":$source_json,\"hookIndex\":$hook_index,\"window\":$window_size}"
993
+ ;;
994
+ avalanche-save)
995
+ client_id="${2:-}"
996
+ out_file="${3:-}"
997
+ count="${4:-0}"
998
+ if [[ -z "$client_id" || -z "$out_file" ]]; then
999
+ echo "avalanche-save requires <clientId> <file> [count]" >&2
1000
+ echo " count=0 → full export, count=N → last N commits" >&2
1001
+ exit 1
1002
+ fi
1003
+ if [[ "$count" -gt 0 ]]; then
1004
+ body="{\"action\":\"export-last\",\"count\":$count}"
1005
+ else
1006
+ body='{"action":"export"}'
1007
+ fi
1008
+ response=$(post_avalanche "$client_id" "$body")
1009
+ if [[ -z "$response" ]]; then
1010
+ echo "avalanche-save: empty response from helper" >&2
1011
+ exit 1
1012
+ fi
1013
+ payload=$(node "$SCRIPT_DIR/avalanche-analyze.cjs" normalize "$count" <<<"$response") || exit 1
1014
+ printf '%s' "$payload" > "$out_file"
1015
+ bytes=$(wc -c < "$out_file" | tr -d ' ')
1016
+ commits=$(node "$SCRIPT_DIR/avalanche-analyze.cjs" "$out_file" summary | node -e 'process.stdout.write(String(JSON.parse(require("fs").readFileSync(0,"utf8")).commits))')
1017
+ echo "{\"ok\":true,\"file\":\"$out_file\",\"bytes\":$bytes,\"commits\":$commits}"
1018
+ ;;
1019
+ avalanche-offline)
1020
+ shift
1021
+ if [[ $# -lt 2 ]]; then
1022
+ echo "avalanche-offline requires <file> <summary|commit-list|commit-peek|hot-sources|describe-action|source-search> [args...]" >&2
1023
+ exit 1
1024
+ fi
1025
+ node "$SCRIPT_DIR/avalanche-analyze.cjs" "$@"
1026
+ ;;
1027
+ eval-snippets)
1028
+ snip_dir="$SCRIPT_DIR/../snippets"
1029
+ if [[ ! -d "$snip_dir" ]]; then echo "no snippets dir at $snip_dir" >&2; exit 1; fi
1030
+ ( cd "$snip_dir" && ls -1 *.js 2>/dev/null )
1031
+ ;;
1032
+ eval-page)
1033
+ client_id="${2:-}"
1034
+ snippet="${3:-}"
1035
+ shift 3 || true
1036
+ if [[ -z "$client_id" || -z "$snippet" ]]; then
1037
+ echo "eval-page requires <clientId> <snippet-file-or-name> [--key value ...]" >&2
1038
+ echo " snippet may be an absolute path, a path relative to cwd, or a name resolved against $SCRIPT_DIR/../snippets/<name>.js" >&2
1039
+ exit 1
1040
+ fi
1041
+ snip_path=""
1042
+ if [[ -f "$snippet" ]]; then snip_path="$snippet"; fi
1043
+ if [[ -z "$snip_path" && -f "$SCRIPT_DIR/../snippets/$snippet" ]]; then snip_path="$SCRIPT_DIR/../snippets/$snippet"; fi
1044
+ if [[ -z "$snip_path" && -f "$SCRIPT_DIR/../snippets/$snippet.js" ]]; then snip_path="$SCRIPT_DIR/../snippets/$snippet.js"; fi
1045
+ if [[ -z "$snip_path" ]]; then echo "snippet not found: $snippet" >&2; exit 1; fi
1046
+ # Build args object from --key value pairs.
1047
+ args_json='{}'
1048
+ while [[ $# -gt 0 ]]; do
1049
+ if [[ "$1" == --* ]]; then
1050
+ k="${1#--}"; v="${2:-}"; shift 2
1051
+ args_json=$(node -e '
1052
+ const o = JSON.parse(process.argv[1]);
1053
+ const k = process.argv[2];
1054
+ let v = process.argv[3];
1055
+ if (v === "true") v = true;
1056
+ else if (v === "false") v = false;
1057
+ else if (!Number.isNaN(Number(v)) && v !== "") v = Number(v);
1058
+ o[k] = v;
1059
+ process.stdout.write(JSON.stringify(o));
1060
+ ' "$args_json" "$k" "$v") || exit 1
1061
+ else shift; fi
1062
+ done
1063
+ src=$(cat "$snip_path")
1064
+ body=$(node -e '
1065
+ const src = require("fs").readFileSync(process.argv[1], "utf8");
1066
+ const args = JSON.parse(process.argv[2]);
1067
+ process.stdout.write(JSON.stringify({ source: src, args }));
1068
+ ' "$snip_path" "$args_json") || exit 1
1069
+ run_curl POST "$BASE_URL/api/pages/$client_id/eval-script" "$body"
1070
+ ;;
1071
+ avalanche-arm)
1072
+ client_id="${2:-}"
1073
+ origin="${3:-}"
1074
+ max_commits="${4:-}"
1075
+ capture_fibers="${5:-true}"
1076
+ detection_mode="${6:-sources}"
1077
+ if [[ -z "$client_id" || -z "$origin" ]]; then
1078
+ echo "avalanche-arm requires <clientId> <origin> [maxCommits] [captureFibers=true|false] [detectionMode=sources|full|stats|amplifiers]" >&2
1079
+ exit 1
1080
+ fi
1081
+ max_commits="${max_commits:-0}"
1082
+ if [[ "$capture_fibers" != "true" && "$capture_fibers" != "false" ]]; then
1083
+ echo "avalanche-arm captureFibers must be true or false" >&2
1084
+ exit 1
1085
+ fi
1086
+ if [[ "$detection_mode" != "sources" && "$detection_mode" != "full" && "$detection_mode" != "stats" && "$detection_mode" != "amplifiers" ]]; then
1087
+ echo "avalanche-arm detectionMode must be sources, full, stats, or amplifiers" >&2
1088
+ exit 1
1089
+ fi
1090
+ body="{\"clientId\":$(json_escape "$client_id"),\"origin\":$(json_escape "$origin"),\"autoStart\":true,\"max\":$max_commits,\"options\":{\"captureFibers\":$capture_fibers,\"detectionMode\":\"$detection_mode\"}}"
1091
+ post_json "/api/avalanche-arm" "$body" >/dev/null
1092
+ # Reset first (clear prior capture), then install detector — no reload needed
1093
+ reset_body="{\"action\":\"reset\"}"
1094
+ post_json "/api/pages/$client_id/avalanche" "$reset_body" >/dev/null
1095
+ install_body="{\"action\":\"install\",\"options\":{\"captureFibers\":$capture_fibers,\"detectionMode\":\"$detection_mode\"}}"
1096
+ post_json "/api/pages/$client_id/avalanche" "$install_body"
1097
+ ;;
1098
+ avalanche-disarm)
1099
+ client_id="${2:-}"
1100
+ origin="${3:-}"
1101
+ if [[ -z "$client_id" || -z "$origin" ]]; then
1102
+ echo "avalanche-disarm requires <clientId> <origin>" >&2
1103
+ exit 1
1104
+ fi
1105
+ body="{\"clientId\":$(json_escape "$client_id"),\"origin\":$(json_escape "$origin"),\"autoStart\":false}"
1106
+ post_json "/api/avalanche-arm" "$body"
1107
+ ;;
1108
+ page-reload)
1109
+ client_id="${2:-}"
1110
+ if [[ -z "$client_id" ]]; then
1111
+ echo "page-reload requires <clientId>" >&2
1112
+ exit 1
1113
+ fi
1114
+ post_json "/api/pages/$client_id/reload" '{}'
1115
+ ;;
1116
+ page-wait-reload)
1117
+ client_id="${2:-}"
1118
+ timeout_ms="${3:-5000}"
1119
+ if [[ -z "$client_id" ]]; then
1120
+ echo "page-wait-reload requires <clientId> [timeoutMs]" >&2
1121
+ exit 1
1122
+ fi
1123
+ body="{\"timeoutMs\":$timeout_ms}"
1124
+ post_json "/api/pages/$client_id/wait-reload" "$body"
1125
+ ;;
1126
+ reload-with-avalanche)
1127
+ client_id="${2:-}"
1128
+ origin="${3:-}"
1129
+ max_commits="${4:-0}"
1130
+ timeout_ms="${5:-5000}"
1131
+ settle_ms="${6:-3000}"
1132
+ capture_fibers="${7:-true}"
1133
+ detection_mode="${8:-sources}"
1134
+ if [[ -z "$client_id" || -z "$origin" ]]; then
1135
+ echo "reload-with-avalanche requires <clientId> <origin> [maxCommits] [timeoutMs] [settleMs] [captureFibers=true|false] [detectionMode=sources|full|stats|amplifiers]" >&2
1136
+ exit 1
1137
+ fi
1138
+ echo "== arm ==" >&2
1139
+ "$0" avalanche-arm "$client_id" "$origin" "$max_commits" "$capture_fibers" "$detection_mode"
1140
+ echo >&2
1141
+ echo "== reload ==" >&2
1142
+ "$0" page-reload "$client_id"
1143
+ echo >&2
1144
+ echo "== wait ==" >&2
1145
+ "$0" page-wait-reload "$client_id" "$timeout_ms"
1146
+ if [[ "$settle_ms" -gt 0 ]]; then
1147
+ echo >&2
1148
+ echo "== settle (${settle_ms}ms) ==" >&2
1149
+ sleep "$(awk "BEGIN { printf \"%.3f\", ${settle_ms}/1000 }")"
1150
+ fi
1151
+ # Upstream IIFE has no stop/pause; capture continues. We just sample status
1152
+ # and hot-sources after the settle window.
1153
+ echo >&2
1154
+ echo "== status ==" >&2
1155
+ "$0" avalanche-status "$client_id"
1156
+ echo >&2
1157
+ echo "== hot sources ==" >&2
1158
+ "$0" avalanche-hot-sources "$client_id" 12
1159
+ ;;
1160
+ handoff-to-playwright)
1161
+ client_id="${2:-}"
1162
+ url="${3:-}"
1163
+ probe_ids_csv="${4:-1,2}"
1164
+ browser_name="${5:-chromium}"
1165
+ headless="${6:-true}"
1166
+ if [[ -z "$client_id" ]]; then
1167
+ echo "handoff-to-playwright requires <clientId> [url] [probeIdsCsv] [browserName] [headless]" >&2
1168
+ exit 1
1169
+ fi
1170
+ IFS=',' read -ra probe_parts <<< "$probe_ids_csv"
1171
+ probe_json="["
1172
+ first=1
1173
+ for probe_id in "${probe_parts[@]}"; do
1174
+ probe_trim=$(echo "$probe_id" | tr -d ' ')
1175
+ [[ -z "$probe_trim" ]] && continue
1176
+ if [[ $first -eq 0 ]]; then probe_json+=","; fi
1177
+ probe_json+="\"$probe_trim\""
1178
+ first=0
1179
+ done
1180
+ probe_json+="]"
1181
+ body=$(node -e '
1182
+ const body = {
1183
+ url: process.argv[1] || undefined,
1184
+ probeIds: JSON.parse(process.argv[2]),
1185
+ browserName: process.argv[3] || "chromium",
1186
+ headless: process.argv[4] !== "false",
1187
+ };
1188
+ process.stdout.write(JSON.stringify(body));
1189
+ ' "$url" "$probe_json" "$browser_name" "$headless") || exit 1
1190
+ curl -fsSL ${AUTH_HEADER[@]+"${AUTH_HEADER[@]}"} -X POST -H 'content-type: application/json' \
1191
+ "$BASE_URL/api/pages/$client_id/handoff-to-playwright" --data-binary "$body"
1192
+ ;;
1193
+ proxy-state)
1194
+ curl "${CURL_OPTS[@]}" "$BASE_URL/api/proxy/state"
1195
+ ;;
1196
+ proxy-start)
1197
+ target_port="${2:-}"
1198
+ original_url="${3:-}"
1199
+ if [[ -z "$target_port" ]]; then
1200
+ echo "proxy-start requires <targetPort> [originalUrl]" >&2
1201
+ exit 1
1202
+ fi
1203
+ if [[ -n "$original_url" ]]; then
1204
+ body="{\"targetPort\":$target_port,\"originalUrl\":$(json_escape "$original_url")}"
1205
+ else
1206
+ body="{\"targetPort\":$target_port}"
1207
+ fi
1208
+ post_json "/api/proxy/start" "$body"
1209
+ ;;
1210
+ proxy-stop)
1211
+ post_json "/api/proxy/stop" '{}'
1212
+ ;;
1213
+ *)
1214
+ echo "Unknown command: $cmd" >&2
1215
+ usage >&2
1216
+ exit 1
1217
+ ;;
1218
+ esac