@ishlabs/cli 0.27.0 → 0.28.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 +4 -0
- package/dist/commands/doctor.js +21 -11
- package/dist/commands/feedback.d.ts +22 -0
- package/dist/commands/feedback.js +259 -0
- package/dist/commands/iteration.js +13 -4
- package/dist/commands/study-run.js +12 -12
- package/dist/commands/study-screenshots.js +15 -12
- package/dist/commands/study.js +22 -3
- package/dist/commands/workspace.js +1 -1
- package/dist/index.js +2 -0
- package/dist/lib/command-helpers.js +7 -3
- package/dist/lib/docs.js +238 -19
- package/dist/lib/local-sim/actions.d.ts +18 -2
- package/dist/lib/local-sim/actions.js +24 -1
- package/dist/lib/local-sim/adb.d.ts +19 -2
- package/dist/lib/local-sim/adb.js +71 -23
- package/dist/lib/local-sim/android.js +7 -2
- package/dist/lib/local-sim/device-pool.d.ts +85 -0
- package/dist/lib/local-sim/device-pool.js +316 -0
- package/dist/lib/local-sim/device.d.ts +4 -0
- package/dist/lib/local-sim/device.js +19 -1
- package/dist/lib/local-sim/emulator.d.ts +50 -0
- package/dist/lib/local-sim/emulator.js +189 -0
- package/dist/lib/local-sim/install.js +23 -3
- package/dist/lib/local-sim/ios.d.ts +26 -1
- package/dist/lib/local-sim/ios.js +61 -17
- package/dist/lib/local-sim/loop.js +117 -11
- package/dist/lib/local-sim/screen-signature.js +4 -0
- package/dist/lib/local-sim/simctl-provision.d.ts +49 -0
- package/dist/lib/local-sim/simctl-provision.js +89 -0
- package/dist/lib/local-sim/simctl.d.ts +6 -4
- package/dist/lib/local-sim/simctl.js +18 -5
- package/dist/lib/local-sim/types.d.ts +27 -1
- package/dist/lib/local-sim/xcuitest.d.ts +39 -1
- package/dist/lib/local-sim/xcuitest.js +70 -6
- package/dist/lib/output.d.ts +11 -1
- package/dist/lib/output.js +56 -2
- package/dist/lib/paths.d.ts +1 -0
- package/dist/lib/paths.js +3 -0
- package/dist/lib/skill-content.js +14 -2
- package/dist/lib/upload.d.ts +27 -0
- package/dist/lib/upload.js +108 -11
- package/package.json +2 -2
package/dist/lib/docs.js
CHANGED
|
@@ -381,12 +381,19 @@ ish iteration create --platform figma --url https://figma.com/proto \\
|
|
|
381
381
|
--flow-name "Onboarding A"
|
|
382
382
|
|
|
383
383
|
# Native app (ios / android): --app names the target, stored as app_artifact (no URL).
|
|
384
|
+
# screen_format defaults to mobile_portrait for native (vs desktop for browser).
|
|
384
385
|
ish iteration create --platform ios --app com.example.app
|
|
385
386
|
ish iteration create --platform ios # --app optional; "chosen at run time"
|
|
386
387
|
# drive it locally against a booted simulator / emulator — the iteration
|
|
387
388
|
# remembers the app, so no --app needed on reruns:
|
|
388
389
|
ish study run --local
|
|
389
390
|
ish study run --local --app ./Build.app # override with a fresh local build
|
|
391
|
+
# State reset between participants: with a local .app build the runner does a
|
|
392
|
+
# clean uninstall+reinstall before each participant, so state one participant
|
|
393
|
+
# creates (a reminder, a saved record) does NOT leak into the next. A bare
|
|
394
|
+
# bundle-id / system-app target can't be reinstalled — it relaunches and warns
|
|
395
|
+
# that state may persist; pass --app <.app> or run one participant per study for
|
|
396
|
+
# a guaranteed clean start. See guides/native-app.
|
|
390
397
|
|
|
391
398
|
# Text/email content from a file:
|
|
392
399
|
ish iteration create --content-text @./email.html --title "Newsletter"
|
|
@@ -2000,14 +2007,22 @@ Interactive study runs produce per-frame screenshots server-side. They
|
|
|
2000
2007
|
let you (or an agent) see what participants actually saw alongside the
|
|
2001
2008
|
sentiment summary.
|
|
2002
2009
|
|
|
2003
|
-
## Screenshots —
|
|
2010
|
+
## Screenshots — the grouped index vs. per-interaction frames
|
|
2004
2011
|
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2012
|
+
\`ish study screenshots list\` reads the **frame-grouped index**
|
|
2013
|
+
(\`/screenshots/grouped\`), a remote-interactive-run artifact keyed by frame —
|
|
2014
|
+
chat / video / text studies don't populate it, and neither do local runs. On a
|
|
2015
|
+
local-only study this endpoint currently 500s rather than returning an empty
|
|
2016
|
+
index, so \`screenshots list\` isn't the way to view a local run.
|
|
2017
|
+
|
|
2018
|
+
**Local runs DO still capture per-interaction screenshots** (\`ish study run
|
|
2019
|
+
--local\`, including ios/android). They live on the participant interaction rows,
|
|
2020
|
+
not in the grouped index — read them two ways:
|
|
2021
|
+
|
|
2022
|
+
- \`ish study get <id>\` — each interaction carries a \`screenshot_url\` (a public
|
|
2023
|
+
storage URL you can fetch directly).
|
|
2024
|
+
- the per-step HTML debug report at \`~/.ish/debug/sim-*.html\` (path printed at
|
|
2025
|
+
the end of each local run).
|
|
2011
2026
|
|
|
2012
2027
|
### CLI
|
|
2013
2028
|
|
|
@@ -2180,6 +2195,12 @@ The CLI guarantees these contracts so agents can chain safely:
|
|
|
2180
2195
|
\`--fields\` set, you can identify the affected resource. Default
|
|
2181
2196
|
write-path JSON is compact (\`{id, alias, name, updated_at,
|
|
2182
2197
|
...changed_fields}\`); pass \`--verbose\` for the full server payload.
|
|
2198
|
+
- **Active-context setters emit a capturable object on stdout.** The
|
|
2199
|
+
\`use\` commands (\`study use\`, \`workspace use\`, \`ask use\`) write their
|
|
2200
|
+
human "Active … set to …" confirmation to **stderr** and, under
|
|
2201
|
+
\`--json\`, an \`{id, alias, name, active}\` object to **stdout** — so
|
|
2202
|
+
\`ish study use s-b2c --json --get alias\` is capturable. (\`--clear\`
|
|
2203
|
+
is a stderr-only confirmation.)
|
|
2183
2204
|
- **Write-path echoes keep collection arrays even when empty.** On a
|
|
2184
2205
|
create/update echo (e.g. \`study create\`/\`study update\`), entity
|
|
2185
2206
|
collections like \`assignments\`, \`interview_questions\`, and
|
|
@@ -2528,6 +2549,15 @@ from envelopes that don't originate from an HTTP response (Commander
|
|
|
2528
2549
|
parse errors, local validation failures, alias-resolution errors). Do
|
|
2529
2550
|
not branch on \`status: 0\` — that value is never emitted as of 0.20.
|
|
2530
2551
|
|
|
2552
|
+
A **\`report\`** field appears on *genuine faults* — uncategorized
|
|
2553
|
+
client errors, \`server\`/5xx, network failures, and unknown throws —
|
|
2554
|
+
carrying the suggested \`ish feedback "…"\` invocation to report the
|
|
2555
|
+
bug. It is deliberately **absent** on user-actionable failures (usage /
|
|
2556
|
+
validation exit 2, auth exit 3, not-found exit 4, \`usage_limit_reached\`)
|
|
2557
|
+
so agents don't report their own input mistakes. In human mode the same
|
|
2558
|
+
nudge prints as a \`→ Looks like a bug? Report it: …\` line on stderr.
|
|
2559
|
+
See \`guides/feedback\`.
|
|
2560
|
+
|
|
2531
2561
|
## Conventions
|
|
2532
2562
|
|
|
2533
2563
|
- Successful commands exit 0 and print one JSON object/array on stdout.
|
|
@@ -3203,22 +3233,26 @@ request time, for any client, is the backend's \`TIER_LIMITS\` dict in
|
|
|
3203
3233
|
|
|
3204
3234
|
## Limits enforced
|
|
3205
3235
|
|
|
3206
|
-
| Limit | Free |
|
|
3207
|
-
|
|
3208
|
-
| \`maxProducts\` | 1 |
|
|
3209
|
-
| \`maxStudiesPerProduct\` | 3 |
|
|
3210
|
-
| \`
|
|
3211
|
-
| \`
|
|
3212
|
-
| \`
|
|
3213
|
-
| \`
|
|
3236
|
+
| Limit | Free | Starter | Pro | Enterprise |
|
|
3237
|
+
|-----------------------------|------|---------|-----|------------|
|
|
3238
|
+
| \`maxProducts\` | 1 | 3 | ∞ | ∞ |
|
|
3239
|
+
| \`maxStudiesPerProduct\` | 3 | 15 | ∞ | ∞ |
|
|
3240
|
+
| \`maxAsksPerProduct\` | 3 | 15 | ∞ | ∞ |
|
|
3241
|
+
| \`maxIterationsPerStudy\` | 2 | 5 | ∞ | ∞ |
|
|
3242
|
+
| \`maxCustomPersons\` | 3 | 10 | ∞ | ∞ |
|
|
3243
|
+
| \`maxConcurrentParticipants\` | 3 | 10 | 50 | ∞ |
|
|
3244
|
+
| \`maxSeats\` | 1 | 1 | 10 | ∞ |
|
|
3214
3245
|
|
|
3215
3246
|
Commands that may hit a limit: \`ish workspace create\`,
|
|
3216
|
-
\`ish study create\`, \`ish study generate\`, \`ish
|
|
3247
|
+
\`ish study create\`, \`ish study generate\`, \`ish ask create\`
|
|
3248
|
+
(and \`ish ask run --new\`), \`ish iteration create\`,
|
|
3217
3249
|
\`ish person create\`, \`ish person generate\`.
|
|
3218
3250
|
|
|
3219
|
-
\`maxConcurrentParticipants\`
|
|
3220
|
-
|
|
3221
|
-
|
|
3251
|
+
\`maxConcurrentParticipants\` caps how many participants a single run can
|
|
3252
|
+
include — cumulative per iteration for studies, per ask for asks — and is
|
|
3253
|
+
enforced when participants are created/added, not at dispatch. \`maxSeats\`
|
|
3254
|
+
gates workspace membership (active members + pending invitations). Both are
|
|
3255
|
+
enforced server-side.
|
|
3222
3256
|
|
|
3223
3257
|
## What you see when a limit is hit
|
|
3224
3258
|
|
|
@@ -3270,6 +3304,7 @@ upgrade or delete an existing resource to free up headroom.
|
|
|
3270
3304
|
that page is about *how many credits each run draws*).
|
|
3271
3305
|
- \`concepts/workspace\` — \`maxProducts\` is per-account.
|
|
3272
3306
|
- \`concepts/study\` — \`maxStudiesPerProduct\` gates study creation.
|
|
3307
|
+
- \`concepts/ask\` — \`maxAsksPerProduct\` gates ask creation.
|
|
3273
3308
|
- \`concepts/iteration\` — \`maxIterationsPerStudy\` gates iteration creation.
|
|
3274
3309
|
- \`concepts/person\` — \`maxCustomPersons\` gates person creation.
|
|
3275
3310
|
- \`reference/json-mode\` — full error envelope shape and exit codes.
|
|
@@ -4340,6 +4375,178 @@ The viewer is only as good as the run behind it. Before sharing, make sure:
|
|
|
4340
4375
|
- \`concepts/active-context\` — \`ish study share\` defaults to the active study.
|
|
4341
4376
|
- \`reference/json-mode\` — the \`{ token, share_url, … }\` envelope.
|
|
4342
4377
|
`;
|
|
4378
|
+
const GUIDE_NATIVE_APP = `# guide: native app studies (ios / android)
|
|
4379
|
+
|
|
4380
|
+
Run an interactive study against a **native iOS or Android app** on a local
|
|
4381
|
+
simulator/emulator, driven step-by-step by AI participants — the native
|
|
4382
|
+
counterpart of a browser (URL) interactive study.
|
|
4383
|
+
|
|
4384
|
+
## 1. Check the local toolchain
|
|
4385
|
+
|
|
4386
|
+
\`\`\`
|
|
4387
|
+
ish check ios # Xcode/xcrun, a booted simulator, the WDA runner, auth
|
|
4388
|
+
ish check android # adb, a running emulator
|
|
4389
|
+
ish setup # fetch/install whatever's missing (WDA runner, etc.)
|
|
4390
|
+
\`\`\`
|
|
4391
|
+
|
|
4392
|
+
\`check\` must be green before a run — it verifies the whole chain (simulator
|
|
4393
|
+
booted → automation runner installed → logged in), so you don't discover a
|
|
4394
|
+
missing piece mid-run.
|
|
4395
|
+
|
|
4396
|
+
## 2. Create the study, then a native iteration
|
|
4397
|
+
|
|
4398
|
+
The study (assignments + questionnaire) is platform-agnostic. The **iteration**
|
|
4399
|
+
names the platform and the app:
|
|
4400
|
+
|
|
4401
|
+
\`\`\`
|
|
4402
|
+
ish study create --name "Onboarding" --modality interactive \\
|
|
4403
|
+
--assignment "Explore:Open the app and look around" \\
|
|
4404
|
+
--question "How clear was it?"
|
|
4405
|
+
|
|
4406
|
+
# --app is a bundle id (already-installed / system app) OR a local .app/.apk path.
|
|
4407
|
+
ish iteration create --platform ios --app com.acme.app # installed bundle id
|
|
4408
|
+
ish iteration create --platform ios --app ./Build/MyApp.app # local build (installed for you)
|
|
4409
|
+
ish iteration create --platform android --app ./app-debug.apk
|
|
4410
|
+
\`\`\`
|
|
4411
|
+
|
|
4412
|
+
The target is stored as \`app_artifact\`; no \`--url\` is needed. \`screen_format\`
|
|
4413
|
+
defaults to **mobile_portrait** for native (vs desktop for browser).
|
|
4414
|
+
|
|
4415
|
+
## 3. Run locally
|
|
4416
|
+
|
|
4417
|
+
\`\`\`
|
|
4418
|
+
ish study run --local --platform ios --person p-913 --wait
|
|
4419
|
+
ish study run --local --platform ios --all --wait # whole matching cohort
|
|
4420
|
+
\`\`\`
|
|
4421
|
+
|
|
4422
|
+
The platform defaults to the iteration's; \`--app\` on \`study run\` overrides the
|
|
4423
|
+
stored target with a fresh local build. The WebDriverAgent runner cold-starts
|
|
4424
|
+
slowly the first time (~30-60s) and is then reused across participants.
|
|
4425
|
+
|
|
4426
|
+
## 3b. Parallel runs — \`--parallel N\` (iOS + Android)
|
|
4427
|
+
|
|
4428
|
+
\`\`\`
|
|
4429
|
+
ish study run --local --platform ios --all --parallel 5 --wait
|
|
4430
|
+
ish study run --local --platform android --all --parallel 5 --wait
|
|
4431
|
+
\`\`\`
|
|
4432
|
+
|
|
4433
|
+
Native runs can drive a **pool of N devices** at once, one participant per
|
|
4434
|
+
device:
|
|
4435
|
+
- **iOS** reuses any booted simulators and **auto-creates + boots** the
|
|
4436
|
+
shortfall, then deletes the simulators it created (reused ones are left alone).
|
|
4437
|
+
- **Android** reuses online emulators and **auto-launches headless emulators**
|
|
4438
|
+
(tuned low-RAM), then stops the ones it started. You only need **one AVD**: the
|
|
4439
|
+
pool **clones it** (a fast file-copy — no avdmanager/JDK needed) to as many as
|
|
4440
|
+
it needs, and deletes the clones afterward. Make one AVD in Android Studio ›
|
|
4441
|
+
Device Manager.
|
|
4442
|
+
|
|
4443
|
+
N is **auto-sized to the host's RAM** —
|
|
4444
|
+
default 1, capped at 5. A small machine runs fewer concurrently and queues the
|
|
4445
|
+
rest rather than erroring, so the same command works everywhere, scaled to the
|
|
4446
|
+
machine. Each participant still gets a clean device per the reset rules below.
|
|
4447
|
+
|
|
4448
|
+
## 4. State reset between participants (important)
|
|
4449
|
+
|
|
4450
|
+
Participants share one simulator, run **sequentially** for native. A
|
|
4451
|
+
terminate+relaunch alone does NOT clear app data, so:
|
|
4452
|
+
|
|
4453
|
+
- **Local \`.app\`/\`.apk\` build** → the runner does a clean **uninstall+reinstall**
|
|
4454
|
+
before each participant. State one participant creates (a saved record, a new
|
|
4455
|
+
reminder) does NOT leak into the next.
|
|
4456
|
+
- **Bare bundle-id / system app** (e.g. \`com.apple.reminders\`) → can't be
|
|
4457
|
+
reinstalled, so it relaunches and **warns once** that earlier-participant state
|
|
4458
|
+
may persist. For a guaranteed clean start, pass \`--app <.app>\` or run one
|
|
4459
|
+
participant per study.
|
|
4460
|
+
|
|
4461
|
+
## 5. Locale / keyboard
|
|
4462
|
+
|
|
4463
|
+
The simulator uses the host machine's keyboard locale. A non-English keyboard
|
|
4464
|
+
can derail text entry — pin a locale on the iteration (\`--locale en-US\`) for
|
|
4465
|
+
reproducible runs.
|
|
4466
|
+
|
|
4467
|
+
## 6. Results, screenshots, transcripts
|
|
4468
|
+
|
|
4469
|
+
- \`ish study results <id>\` — sentiment + interview answers, same as any study.
|
|
4470
|
+
- **Per-interaction screenshots** are captured even for local runs — read them
|
|
4471
|
+
via \`ish study get <id>\` (each interaction carries a \`screenshot_url\`) or the
|
|
4472
|
+
per-step HTML debug report at \`~/.ish/debug/sim-*.html\` (path printed at the
|
|
4473
|
+
end of the run). Note \`ish study screenshots list\` reads the *remote-run*
|
|
4474
|
+
frame index and won't show local frames — see reference/screenshots.
|
|
4475
|
+
|
|
4476
|
+
## Related
|
|
4477
|
+
|
|
4478
|
+
- \`concepts/iteration\` — \`app_artifact\`, screen_format, platforms.
|
|
4479
|
+
- \`reference/screenshots\` — grouped index vs per-interaction frames.
|
|
4480
|
+
- \`guides/first-study\` — the browser-URL version of this flow.
|
|
4481
|
+
`;
|
|
4482
|
+
const GUIDE_FEEDBACK = `# guide: report a bug or send feedback
|
|
4483
|
+
|
|
4484
|
+
\`ish feedback\` files a bug report, feature request, or general note
|
|
4485
|
+
straight from the terminal — to the same place the web app's "Send
|
|
4486
|
+
feedback" dialog goes. As the agent operating the CLI, you usually see
|
|
4487
|
+
a failure first; this is how you hand it to the team without leaving
|
|
4488
|
+
the shell.
|
|
4489
|
+
|
|
4490
|
+
\`\`\`bash
|
|
4491
|
+
ish feedback "the save button on the profile page does nothing"
|
|
4492
|
+
ish feedback --type feature "let me export interactions as CSV"
|
|
4493
|
+
ish feedback --type other "the guided setup was great"
|
|
4494
|
+
\`\`\`
|
|
4495
|
+
|
|
4496
|
+
## The message
|
|
4497
|
+
|
|
4498
|
+
Pass the body as the positional argument, or read it from elsewhere —
|
|
4499
|
+
the same idioms as other text fields:
|
|
4500
|
+
|
|
4501
|
+
- \`ish feedback @bug-report.md\` — read the body from a file.
|
|
4502
|
+
- \`… 2>err.txt; ish feedback @err.txt\` — attach a failing command's output.
|
|
4503
|
+
- \`some-cmd 2>&1 | ish feedback -\` — read the body from stdin.
|
|
4504
|
+
|
|
4505
|
+
The body must be at least 10 characters. \`--title\` is optional; by
|
|
4506
|
+
default the first line of the message becomes the title.
|
|
4507
|
+
|
|
4508
|
+
## Type
|
|
4509
|
+
|
|
4510
|
+
\`--type\` is one of \`bug\` (default), \`feature\`, or \`other\` — the same
|
|
4511
|
+
three the web dialog offers.
|
|
4512
|
+
|
|
4513
|
+
## Diagnostics (help whoever debugs it)
|
|
4514
|
+
|
|
4515
|
+
To give the debugger as much grounded signal as possible, the report
|
|
4516
|
+
auto-attaches an environment + account block: ish CLI version, OS,
|
|
4517
|
+
node, the logged-in account, and your active workspace/study/ask. Opt
|
|
4518
|
+
out with \`--no-diagnostics\`.
|
|
4519
|
+
|
|
4520
|
+
\`--health\` adds more for simulation/setup bugs: it runs the \`ish check\`
|
|
4521
|
+
setup checklist (Xcode, simulators, adb, Chromium, account) and
|
|
4522
|
+
attaches the tail of \`~/.ish/local-sim.log\` when present. Use it
|
|
4523
|
+
whenever the bug involves \`study run --local\` or native device setup.
|
|
4524
|
+
|
|
4525
|
+
\`--dry-run\` prints exactly what would be sent (message + diagnostics)
|
|
4526
|
+
without submitting — preview it first if you're unsure.
|
|
4527
|
+
|
|
4528
|
+
## When the CLI nudges you to report
|
|
4529
|
+
|
|
4530
|
+
On a *genuine fault* (uncategorized client error, 5xx/\`server\`, network
|
|
4531
|
+
failure, unknown throw) the CLI appends a hint: a \`report\` field in the
|
|
4532
|
+
\`--json\` error envelope, and a \`→ Looks like a bug? Report it: …\` line
|
|
4533
|
+
on stderr in human mode. It is intentionally **silent** on
|
|
4534
|
+
user-actionable failures (usage, validation, auth, not-found,
|
|
4535
|
+
usage-limit) so the nudge stays high-signal. When you see it, that
|
|
4536
|
+
error is worth an \`ish feedback\` — paste what you were doing and add
|
|
4537
|
+
\`--health\` if it was a local/native run.
|
|
4538
|
+
|
|
4539
|
+
## Where it lands
|
|
4540
|
+
|
|
4541
|
+
Sends as the logged-in user (run \`ish login\` first) through the ish
|
|
4542
|
+
backend, so reports show up alongside web feedback in the team's review
|
|
4543
|
+
surface. Output is the created row \`{id, type, title, status}\`.
|
|
4544
|
+
|
|
4545
|
+
## Related
|
|
4546
|
+
|
|
4547
|
+
- \`reference/json-mode\` — the \`report\` field + error envelope.
|
|
4548
|
+
- \`guides/native-app\` — when to reach for \`--health\`.
|
|
4549
|
+
`;
|
|
4343
4550
|
const PAGES = [
|
|
4344
4551
|
{
|
|
4345
4552
|
slug: "overview",
|
|
@@ -4503,12 +4710,24 @@ const PAGES = [
|
|
|
4503
4710
|
description: "Iterative probe loop for one specific persona: person suggest-scenarios returns LLM probes; answer them locally; person evidence add persists answers; person evidence list reads them back.",
|
|
4504
4711
|
body: GUIDE_BUILD_SPECIFIC_PERSON,
|
|
4505
4712
|
},
|
|
4713
|
+
{
|
|
4714
|
+
slug: "guides/native-app",
|
|
4715
|
+
title: "guide: native app studies (ios / android)",
|
|
4716
|
+
description: "Run an interactive study against a native iOS or Android app on a local simulator/emulator: check ios/android, create a --platform ios/android iteration with --app (bundle id or .app/.apk), run --local, per-participant state reset, locale/keyboard, and where local screenshots live.",
|
|
4717
|
+
body: GUIDE_NATIVE_APP,
|
|
4718
|
+
},
|
|
4506
4719
|
{
|
|
4507
4720
|
slug: "guides/mcp-add",
|
|
4508
4721
|
title: "guide: wire ish into your AI clients (`ish mcp add`)",
|
|
4509
4722
|
description: "One command wires the hosted ish MCP server into Cursor, VS Code, Claude Code, Claude Desktop, and Windsurf. Idempotent, atomic, preserves unrelated keys, no tokens written.",
|
|
4510
4723
|
body: GUIDE_MCP_ADD,
|
|
4511
4724
|
},
|
|
4725
|
+
{
|
|
4726
|
+
slug: "guides/feedback",
|
|
4727
|
+
title: "guide: report a bug or send feedback (`ish feedback`)",
|
|
4728
|
+
description: "File a bug/feature/other report from the terminal: message via arg/@file/stdin, --type, auto-attached environment+account diagnostics (--no-diagnostics to opt out), --health for setup checks + local-sim logs, --dry-run preview. Also: the `report` hint the CLI appends to genuine-fault errors.",
|
|
4729
|
+
body: GUIDE_FEEDBACK,
|
|
4730
|
+
},
|
|
4512
4731
|
];
|
|
4513
4732
|
const PAGES_BY_SLUG = new Map(PAGES.map((p) => [p.slug, p]));
|
|
4514
4733
|
export function listPages() {
|
|
@@ -10,15 +10,31 @@
|
|
|
10
10
|
* lives there, not here.
|
|
11
11
|
*/
|
|
12
12
|
import type { Page } from "playwright-core";
|
|
13
|
-
import type { LocalStepAction, ActionResult, ContextValue, TreeData } from "./types.js";
|
|
13
|
+
import type { LocalStepAction, ActionResult, ContextValue, WireContextValue, TreeData } from "./types.js";
|
|
14
14
|
import type { TabManager } from "./tabs.js";
|
|
15
15
|
/**
|
|
16
16
|
* Execute a single action on the page.
|
|
17
17
|
*/
|
|
18
18
|
export declare function executeAction(page: Page, action: LocalStepAction, treeData: TreeData, contextValues: ContextValue[], tabs?: TabManager): Promise<ActionResult>;
|
|
19
|
+
/**
|
|
20
|
+
* Normalize the backend's wire context-value shape ({@link WireContextValue}:
|
|
21
|
+
* `{key, requires_resolution, …}`) into the internal {@link ContextValue}
|
|
22
|
+
* ({name, type, …}) the executors and {@link resolveTextValue} consume.
|
|
23
|
+
*
|
|
24
|
+
* This mapping is the fix for a long-standing silent break: the backend has
|
|
25
|
+
* always sent `key`/`requires_resolution`, the CLI has always read `name`/`type`,
|
|
26
|
+
* and nothing mapped between them — so var/secret lookups never matched and the
|
|
27
|
+
* agent typed the literal key (e.g. `LOGIN_USERNAME`) into login fields on web,
|
|
28
|
+
* iOS and Android alike. `requires_resolution` is true for secrets (resolved at
|
|
29
|
+
* runtime) and false for preloaded variables.
|
|
30
|
+
*/
|
|
31
|
+
export declare function normalizeContextValues(wire: WireContextValue[]): ContextValue[];
|
|
19
32
|
/**
|
|
20
33
|
* Resolve the actual text to type from an action, handling var/secret value types.
|
|
21
|
-
* Exported so the native (Android)
|
|
34
|
+
* Exported so the native (Android/iOS) executors can resolve values the same way.
|
|
35
|
+
*
|
|
36
|
+
* `contextValues` must already be normalized via {@link normalizeContextValues}
|
|
37
|
+
* — the lookup matches on `name`, which is the backend's `key` post-mapping.
|
|
22
38
|
*/
|
|
23
39
|
export declare function resolveTextValue(action: LocalStepAction, contextValues: ContextValue[]): string;
|
|
24
40
|
/**
|
|
@@ -376,9 +376,32 @@ async function executeKeyboardShortcut(page, action) {
|
|
|
376
376
|
await page.keyboard.press(combo);
|
|
377
377
|
}
|
|
378
378
|
// --- Helpers ---
|
|
379
|
+
/**
|
|
380
|
+
* Normalize the backend's wire context-value shape ({@link WireContextValue}:
|
|
381
|
+
* `{key, requires_resolution, …}`) into the internal {@link ContextValue}
|
|
382
|
+
* ({name, type, …}) the executors and {@link resolveTextValue} consume.
|
|
383
|
+
*
|
|
384
|
+
* This mapping is the fix for a long-standing silent break: the backend has
|
|
385
|
+
* always sent `key`/`requires_resolution`, the CLI has always read `name`/`type`,
|
|
386
|
+
* and nothing mapped between them — so var/secret lookups never matched and the
|
|
387
|
+
* agent typed the literal key (e.g. `LOGIN_USERNAME`) into login fields on web,
|
|
388
|
+
* iOS and Android alike. `requires_resolution` is true for secrets (resolved at
|
|
389
|
+
* runtime) and false for preloaded variables.
|
|
390
|
+
*/
|
|
391
|
+
export function normalizeContextValues(wire) {
|
|
392
|
+
return wire.map(cv => ({
|
|
393
|
+
name: cv.key,
|
|
394
|
+
type: cv.requires_resolution ? "secret" : "var",
|
|
395
|
+
value: cv.value,
|
|
396
|
+
...(cv.description != null && { description: cv.description }),
|
|
397
|
+
}));
|
|
398
|
+
}
|
|
379
399
|
/**
|
|
380
400
|
* Resolve the actual text to type from an action, handling var/secret value types.
|
|
381
|
-
* Exported so the native (Android)
|
|
401
|
+
* Exported so the native (Android/iOS) executors can resolve values the same way.
|
|
402
|
+
*
|
|
403
|
+
* `contextValues` must already be normalized via {@link normalizeContextValues}
|
|
404
|
+
* — the lookup matches on `name`, which is the backend's `key` post-mapping.
|
|
382
405
|
*/
|
|
383
406
|
export function resolveTextValue(action, contextValues) {
|
|
384
407
|
if (action.value_type === "var" || action.value_type === "secret") {
|
|
@@ -10,12 +10,16 @@
|
|
|
10
10
|
* backend's 0-1000 coordinates against the screencap pixel size and taps
|
|
11
11
|
* directly. (Verified by the Layer-1 driver smoke; see scripts/mobile-e2e.)
|
|
12
12
|
*/
|
|
13
|
+
/** Run `fn` with all its adb calls pinned to `serial` (parallel pool path). */
|
|
14
|
+
export declare function withAdbSerial<T>(serial: string | undefined, fn: () => Promise<T>): Promise<T>;
|
|
15
|
+
/** `["-s", serial]` or `[]` — the device-targeting prefix. Pure (tested). */
|
|
16
|
+
export declare function serialArgs(serial: string | undefined): string[];
|
|
13
17
|
/** Resolve adb, downloading Google's platform-tools on first use if not found. */
|
|
14
18
|
export declare function ensureAdb(): Promise<string>;
|
|
15
19
|
export declare class AdbError extends Error {
|
|
16
20
|
constructor(message: string);
|
|
17
21
|
}
|
|
18
|
-
/** Run `adb <args>` and return trimmed stdout. Throws AdbError on failure. */
|
|
22
|
+
/** Run `adb [-s serial] <args>` and return trimmed stdout. Throws AdbError on failure. */
|
|
19
23
|
export declare function adb(args: string[], timeoutMs?: number): Promise<string>;
|
|
20
24
|
/** Run `adb shell <args>` and return trimmed stdout. */
|
|
21
25
|
export declare function adbShell(args: string[], timeoutMs?: number): Promise<string>;
|
|
@@ -62,7 +66,20 @@ export declare function currentActivity(): Promise<string>;
|
|
|
62
66
|
* output. Returns the PNG buffer at full device resolution.
|
|
63
67
|
*/
|
|
64
68
|
export declare function screencapPng(): Promise<Buffer>;
|
|
65
|
-
/**
|
|
69
|
+
/**
|
|
70
|
+
* Parse `adb devices` output into {serial, state} rows. Pure (tested). Skips the
|
|
71
|
+
* "List of devices attached" header and blank lines.
|
|
72
|
+
*/
|
|
73
|
+
export declare function parseAdbDevices(out: string): Array<{
|
|
74
|
+
serial: string;
|
|
75
|
+
state: string;
|
|
76
|
+
}>;
|
|
77
|
+
/** List online (state==="device") serials. */
|
|
78
|
+
export declare function listOnlineSerials(): Promise<string[]>;
|
|
79
|
+
/**
|
|
80
|
+
* Assert the target device is online. With a serial in effect (pool path or
|
|
81
|
+
* ANDROID_SERIAL), confirm THAT serial is online. Otherwise require exactly one.
|
|
82
|
+
*/
|
|
66
83
|
export declare function requireOneDevice(): Promise<void>;
|
|
67
84
|
export declare function inputTap(x: number, y: number): Promise<void>;
|
|
68
85
|
export declare function inputSwipe(x1: number, y1: number, x2: number, y2: number, durationMs?: number): Promise<void>;
|
|
@@ -14,8 +14,32 @@ import { execFile, execFileSync } from "node:child_process";
|
|
|
14
14
|
import { existsSync, mkdirSync, writeFileSync, rmSync } from "node:fs";
|
|
15
15
|
import { join } from "node:path";
|
|
16
16
|
import { promisify } from "node:util";
|
|
17
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
17
18
|
import { binDir, adbBin } from "../paths.js";
|
|
18
19
|
const execFileAsync = promisify(execFile);
|
|
20
|
+
/**
|
|
21
|
+
* The adb serial to target for the current async call chain. A parallel run
|
|
22
|
+
* drives N emulators in ONE process; every adb call must hit the right device,
|
|
23
|
+
* but the CLI targets devices via the `adb -s <serial>` prefix, not a per-call
|
|
24
|
+
* argument threaded through ~25 functions. AsyncLocalStorage carries the serial
|
|
25
|
+
* implicitly through the call stack so `adb()` / `screencapPng()` pick it up,
|
|
26
|
+
* and two concurrent `withAdbSerial(A, …)` / `withAdbSerial(B, …)` chains stay
|
|
27
|
+
* isolated. Single-device runs leave the store empty and fall back to
|
|
28
|
+
* ANDROID_SERIAL / the one online device (unchanged behavior).
|
|
29
|
+
*/
|
|
30
|
+
const serialStore = new AsyncLocalStorage();
|
|
31
|
+
/** Run `fn` with all its adb calls pinned to `serial` (parallel pool path). */
|
|
32
|
+
export function withAdbSerial(serial, fn) {
|
|
33
|
+
return serialStore.run(serial?.trim() || undefined, fn);
|
|
34
|
+
}
|
|
35
|
+
/** The serial in effect for this call chain: store → ANDROID_SERIAL → none. */
|
|
36
|
+
function activeSerial() {
|
|
37
|
+
return serialStore.getStore() ?? (process.env.ANDROID_SERIAL?.trim() || undefined);
|
|
38
|
+
}
|
|
39
|
+
/** `["-s", serial]` or `[]` — the device-targeting prefix. Pure (tested). */
|
|
40
|
+
export function serialArgs(serial) {
|
|
41
|
+
return serial ? ["-s", serial] : [];
|
|
42
|
+
}
|
|
19
43
|
// Resolve adb without depending on the caller's PATH: ISH_ADB/ADB override → the
|
|
20
44
|
// Android SDK → Homebrew → our own download cache → PATH. If none is found,
|
|
21
45
|
// ensureAdb() fetches Google's standalone platform-tools (a small zip) into
|
|
@@ -105,11 +129,12 @@ export class AdbError extends Error {
|
|
|
105
129
|
this.name = "AdbError";
|
|
106
130
|
}
|
|
107
131
|
}
|
|
108
|
-
/** Run `adb <args>` and return trimmed stdout. Throws AdbError on failure. */
|
|
132
|
+
/** Run `adb [-s serial] <args>` and return trimmed stdout. Throws AdbError on failure. */
|
|
109
133
|
export async function adb(args, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
110
134
|
const bin = await ensureAdb();
|
|
135
|
+
const full = [...serialArgs(activeSerial()), ...args];
|
|
111
136
|
try {
|
|
112
|
-
const { stdout } = await execFileAsync(bin,
|
|
137
|
+
const { stdout } = await execFileAsync(bin, full, {
|
|
113
138
|
timeout: timeoutMs,
|
|
114
139
|
maxBuffer: 4 * 1024 * 1024,
|
|
115
140
|
});
|
|
@@ -117,7 +142,7 @@ export async function adb(args, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
|
117
142
|
}
|
|
118
143
|
catch (err) {
|
|
119
144
|
const msg = err instanceof Error ? err.message : String(err);
|
|
120
|
-
throw new AdbError(`adb ${
|
|
145
|
+
throw new AdbError(`adb ${full.join(" ")} failed: ${msg}`);
|
|
121
146
|
}
|
|
122
147
|
}
|
|
123
148
|
/** Run `adb shell <args>` and return trimmed stdout. */
|
|
@@ -191,8 +216,9 @@ export async function currentActivity() {
|
|
|
191
216
|
*/
|
|
192
217
|
export async function screencapPng() {
|
|
193
218
|
const bin = await ensureAdb();
|
|
219
|
+
const full = [...serialArgs(activeSerial()), "exec-out", "screencap", "-p"];
|
|
194
220
|
try {
|
|
195
|
-
const { stdout } = await execFileAsync(bin,
|
|
221
|
+
const { stdout } = await execFileAsync(bin, full, {
|
|
196
222
|
timeout: SCREENCAP_TIMEOUT_MS,
|
|
197
223
|
maxBuffer: SCREENCAP_MAX_BUFFER,
|
|
198
224
|
encoding: "buffer",
|
|
@@ -204,40 +230,62 @@ export async function screencapPng() {
|
|
|
204
230
|
throw new AdbError(`adb exec-out screencap failed: ${msg}`);
|
|
205
231
|
}
|
|
206
232
|
}
|
|
207
|
-
/**
|
|
233
|
+
/**
|
|
234
|
+
* Parse `adb devices` output into {serial, state} rows. Pure (tested). Skips the
|
|
235
|
+
* "List of devices attached" header and blank lines.
|
|
236
|
+
*/
|
|
237
|
+
export function parseAdbDevices(out) {
|
|
238
|
+
return out
|
|
239
|
+
.split("\n")
|
|
240
|
+
.slice(1)
|
|
241
|
+
.map((l) => l.trim())
|
|
242
|
+
.filter(Boolean)
|
|
243
|
+
.map((l) => {
|
|
244
|
+
const [serial, state] = l.split("\t");
|
|
245
|
+
return { serial: serial ?? "", state: state ?? "" };
|
|
246
|
+
})
|
|
247
|
+
.filter((d) => d.serial);
|
|
248
|
+
}
|
|
249
|
+
/** `adb devices` WITHOUT a serial prefix (the list is global, not per-device). */
|
|
250
|
+
async function devicesRaw() {
|
|
251
|
+
const bin = await ensureAdb();
|
|
252
|
+
const { stdout } = await execFileAsync(bin, ["devices"], {
|
|
253
|
+
timeout: DEFAULT_TIMEOUT_MS,
|
|
254
|
+
maxBuffer: 1024 * 1024,
|
|
255
|
+
});
|
|
256
|
+
return stdout.trim();
|
|
257
|
+
}
|
|
258
|
+
/** List online (state==="device") serials. */
|
|
259
|
+
export async function listOnlineSerials() {
|
|
260
|
+
return parseAdbDevices(await devicesRaw())
|
|
261
|
+
.filter((d) => d.state === "device")
|
|
262
|
+
.map((d) => d.serial);
|
|
263
|
+
}
|
|
264
|
+
/**
|
|
265
|
+
* Assert the target device is online. With a serial in effect (pool path or
|
|
266
|
+
* ANDROID_SERIAL), confirm THAT serial is online. Otherwise require exactly one.
|
|
267
|
+
*/
|
|
208
268
|
export async function requireOneDevice() {
|
|
209
|
-
let
|
|
269
|
+
let online;
|
|
210
270
|
try {
|
|
211
|
-
|
|
271
|
+
online = await listOnlineSerials();
|
|
212
272
|
}
|
|
213
273
|
catch (err) {
|
|
214
274
|
const msg = err instanceof Error ? err.message : String(err);
|
|
215
275
|
throw new AdbError(`Could not run adb (looked for "${findAdb() ?? "adb"}"). Run \`ish check android\` to check your setup. ${msg}`);
|
|
216
276
|
}
|
|
217
|
-
// Output: "List of devices attached\n<serial>\tdevice\n..."
|
|
218
|
-
const online = out
|
|
219
|
-
.split("\n")
|
|
220
|
-
.slice(1)
|
|
221
|
-
.map((l) => l.trim())
|
|
222
|
-
.filter((l) => l && l.endsWith("\tdevice"));
|
|
223
277
|
if (online.length === 0) {
|
|
224
278
|
throw new AdbError("No Android device/emulator online. Run `ish check android` to check your setup and how to boot one.");
|
|
225
279
|
}
|
|
226
|
-
|
|
227
|
-
// device, pin to it instead of failing on "more than one device". The adb
|
|
228
|
-
// wrapper inherits process.env, so every subsequent `adb` call already targets
|
|
229
|
-
// that serial — this lets multiple emulators run in parallel, each driven by a
|
|
230
|
-
// CLI invocation with its own ANDROID_SERIAL.
|
|
231
|
-
const pinned = process.env.ANDROID_SERIAL?.trim();
|
|
280
|
+
const pinned = activeSerial();
|
|
232
281
|
if (pinned) {
|
|
233
|
-
if (online.
|
|
282
|
+
if (online.includes(pinned))
|
|
234
283
|
return;
|
|
235
|
-
throw new AdbError(`
|
|
236
|
-
`Online: ${online.map((l) => l.split("\t")[0]).join(", ") || "none"}.`);
|
|
284
|
+
throw new AdbError(`Android device ${pinned} is not online. Online: ${online.join(", ") || "none"}.`);
|
|
237
285
|
}
|
|
238
286
|
if (online.length > 1) {
|
|
239
287
|
throw new AdbError(`Expected exactly one Android device, found ${online.length}. ` +
|
|
240
|
-
`Stop the extras, or
|
|
288
|
+
`Stop the extras, or run with --parallel to pool them.`);
|
|
241
289
|
}
|
|
242
290
|
}
|
|
243
291
|
// --- Input gestures (all in screencap pixel space) ---
|
|
@@ -403,10 +403,15 @@ export class AndroidDevice {
|
|
|
403
403
|
await settle(250);
|
|
404
404
|
}
|
|
405
405
|
const text = resolveTextValue(action, this.contextValues);
|
|
406
|
-
|
|
406
|
+
// Clear before typing by default — ADBKeyboard's type broadcast APPENDS, so
|
|
407
|
+
// a retried or app-pre-filled field would otherwise accumulate text. This is
|
|
408
|
+
// the replace-on-type semantic the browser (select-all+type) and iOS paths
|
|
409
|
+
// also use; previously only click_type cleared. Skip the clear+type entirely
|
|
410
|
+
// when there's nothing to type so an empty no-op can't wipe the field.
|
|
411
|
+
if (text) {
|
|
407
412
|
await adbKeyboardClear();
|
|
413
|
+
await adbKeyboardType(text);
|
|
408
414
|
}
|
|
409
|
-
await adbKeyboardType(text);
|
|
410
415
|
if (action.submit) {
|
|
411
416
|
await settle(150);
|
|
412
417
|
await pressKeyEvent("KEYCODE_ENTER");
|