@ishlabs/cli 0.26.0 → 0.26.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.
- package/dist/commands/doctor.d.ts +16 -0
- package/dist/commands/doctor.js +34 -9
- package/dist/commands/iteration.js +23 -5
- package/dist/commands/study-participant.js +1 -1
- package/dist/commands/study-run.js +26 -1
- package/dist/commands/study-screenshots.js +38 -5
- package/dist/lib/api-client.d.ts +3 -0
- package/dist/lib/api-client.js +6 -1
- package/dist/lib/docs.js +15 -3
- package/dist/lib/local-sim/actions.d.ts +18 -0
- package/dist/lib/local-sim/actions.js +30 -0
- package/dist/lib/local-sim/adb.d.ts +23 -0
- package/dist/lib/local-sim/adb.js +106 -16
- package/dist/lib/local-sim/android.d.ts +7 -1
- package/dist/lib/local-sim/android.js +16 -1
- package/dist/lib/local-sim/device.d.ts +19 -0
- package/dist/lib/local-sim/ios.d.ts +7 -1
- package/dist/lib/local-sim/ios.js +16 -1
- package/dist/lib/local-sim/loop.js +133 -26
- package/dist/lib/local-sim/simctl.d.ts +15 -0
- package/dist/lib/local-sim/simctl.js +41 -1
- package/dist/lib/local-sim/types.d.ts +11 -2
- package/dist/lib/paths.d.ts +6 -0
- package/dist/lib/paths.js +9 -0
- package/dist/lib/report-readiness.d.ts +44 -0
- package/dist/lib/report-readiness.js +74 -0
- package/dist/lib/skill-content.js +2 -0
- package/package.json +1 -1
|
@@ -23,4 +23,20 @@
|
|
|
23
23
|
* build.
|
|
24
24
|
*/
|
|
25
25
|
import type { Command } from "commander";
|
|
26
|
+
export type CheckStatus = "pass" | "warn" | "fail" | "skip";
|
|
27
|
+
export interface Check {
|
|
28
|
+
/** Stable machine key (for --json). */
|
|
29
|
+
key: string;
|
|
30
|
+
/** Human label. */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Coarse grouping for the human checklist. */
|
|
33
|
+
group: "iOS" | "Android" | "Web" | "Account";
|
|
34
|
+
status: CheckStatus;
|
|
35
|
+
message?: string;
|
|
36
|
+
/** One-line remediation hint shown on warn/fail. */
|
|
37
|
+
fix?: string;
|
|
38
|
+
}
|
|
39
|
+
export declare function runChecks(): Promise<Check[]>;
|
|
40
|
+
export declare function scopeChecks(checks: Check[], platform?: string): Check[];
|
|
41
|
+
export declare function overall(checks: Check[]): CheckStatus;
|
|
26
42
|
export declare function registerDoctorCommands(program: Command): void;
|
package/dist/commands/doctor.js
CHANGED
|
@@ -28,9 +28,10 @@ import { existsSync, readdirSync } from "node:fs";
|
|
|
28
28
|
import * as path from "node:path";
|
|
29
29
|
import { runInline } from "../lib/command-helpers.js";
|
|
30
30
|
import { output } from "../lib/output.js";
|
|
31
|
+
import { reportReadiness } from "../lib/report-readiness.js";
|
|
31
32
|
import { loadConfig } from "../config.js";
|
|
32
33
|
import { decodeJwtClaims, isTokenExpired } from "../auth.js";
|
|
33
|
-
import { wdaDir } from "../lib/paths.js";
|
|
34
|
+
import { wdaDir, adbBin } from "../lib/paths.js";
|
|
34
35
|
import pkg from "../../package.json" with { type: "json" };
|
|
35
36
|
const execFileAsync = promisify(execFile);
|
|
36
37
|
const XCRUN = "/usr/bin/xcrun";
|
|
@@ -52,7 +53,7 @@ async function tryExec(cmd, args, timeoutMs = 8000) {
|
|
|
52
53
|
};
|
|
53
54
|
}
|
|
54
55
|
}
|
|
55
|
-
/** adb path, mirroring the device layer's resolution (ISH_ADB →
|
|
56
|
+
/** adb path, mirroring the device layer's resolution (ISH_ADB → SDK → Homebrew → our download cache → PATH). */
|
|
56
57
|
function resolveAdb() {
|
|
57
58
|
for (const v of [process.env.ISH_ADB, process.env.ADB]) {
|
|
58
59
|
if (v && existsSync(v))
|
|
@@ -65,6 +66,10 @@ function resolveAdb() {
|
|
|
65
66
|
return p;
|
|
66
67
|
}
|
|
67
68
|
}
|
|
69
|
+
if (existsSync("/opt/homebrew/bin/adb"))
|
|
70
|
+
return "/opt/homebrew/bin/adb";
|
|
71
|
+
if (existsSync(adbBin()))
|
|
72
|
+
return adbBin();
|
|
68
73
|
return "adb";
|
|
69
74
|
}
|
|
70
75
|
// ── Individual checks ──────────────────────────────────────────────────────
|
|
@@ -156,7 +161,7 @@ async function checkAdb() {
|
|
|
156
161
|
const adbPath = resolveAdb();
|
|
157
162
|
const r = await tryExec(adbPath, ["devices"]);
|
|
158
163
|
if (!r.ok) {
|
|
159
|
-
const adb = { key: "adb", name: "Android adb", group: "Android", status: "warn", message: "not found (only needed for Android)", fix: "
|
|
164
|
+
const adb = { key: "adb", name: "Android adb", group: "Android", status: "warn", message: "not found (only needed for Android)", fix: "ish setup (fetches adb), or install Android platform-tools" };
|
|
160
165
|
const emulator = { key: "android_emulator", name: "Android emulator", group: "Android", status: "skip" };
|
|
161
166
|
return { adb, emulator };
|
|
162
167
|
}
|
|
@@ -188,7 +193,7 @@ async function checkChromium() {
|
|
|
188
193
|
function checkCli() {
|
|
189
194
|
return { key: "cli", name: "ish CLI", group: "Account", status: "pass", message: `v${pkg.version}` };
|
|
190
195
|
}
|
|
191
|
-
async function runChecks() {
|
|
196
|
+
export async function runChecks() {
|
|
192
197
|
const { adb, emulator } = await checkAdb();
|
|
193
198
|
return [
|
|
194
199
|
await checkXcode(),
|
|
@@ -218,13 +223,13 @@ const PLATFORM_GROUPS = {
|
|
|
218
223
|
android: ["Android", "Account"],
|
|
219
224
|
web: ["Web", "Account"],
|
|
220
225
|
};
|
|
221
|
-
function scopeChecks(checks, platform) {
|
|
226
|
+
export function scopeChecks(checks, platform) {
|
|
222
227
|
if (!platform)
|
|
223
228
|
return checks;
|
|
224
229
|
const groups = PLATFORM_GROUPS[platform];
|
|
225
230
|
return groups ? checks.filter((c) => groups.includes(c.group)) : checks;
|
|
226
231
|
}
|
|
227
|
-
function overall(checks) {
|
|
232
|
+
export function overall(checks) {
|
|
228
233
|
if (checks.some((c) => c.status === "fail"))
|
|
229
234
|
return "fail";
|
|
230
235
|
if (checks.some((c) => c.status === "warn"))
|
|
@@ -274,17 +279,28 @@ export function registerDoctorCommands(program) {
|
|
|
274
279
|
throw new Error(`Unknown platform "${platform}". Use one of: ios, android, web.`);
|
|
275
280
|
}
|
|
276
281
|
const checks = scopeChecks(await runChecks(), scope);
|
|
282
|
+
const aggregate = overall(checks);
|
|
277
283
|
if (globals.json) {
|
|
278
|
-
output({ checks, overall:
|
|
284
|
+
output({ checks, overall: aggregate }, true);
|
|
279
285
|
}
|
|
280
286
|
else {
|
|
281
287
|
renderHuman(checks);
|
|
282
288
|
}
|
|
289
|
+
// Best-effort: when scoped to a native platform, report readiness to
|
|
290
|
+
// the backend so the web app can render a live native-readiness panel
|
|
291
|
+
// (the native analog of `ish connect` registering a tunnel). Never for
|
|
292
|
+
// the no-arg `ish check` (no single platform) or `web`. Awaited so a
|
|
293
|
+
// short-lived `ish check` doesn't drop the in-flight request before
|
|
294
|
+
// exit — `reportReadiness` is fully best-effort and never throws or
|
|
295
|
+
// blocks meaningfully (5s ceiling).
|
|
296
|
+
if (scope === "ios" || scope === "android") {
|
|
297
|
+
await reportReadiness(scope, checks, aggregate, globals);
|
|
298
|
+
}
|
|
283
299
|
});
|
|
284
300
|
});
|
|
285
301
|
program
|
|
286
302
|
.command("setup")
|
|
287
|
-
.description("Fetch/install local-simulation dependencies (Chromium, iOS runner) and show what's left to do")
|
|
303
|
+
.description("Fetch/install local-simulation dependencies (Chromium, iOS runner, adb) and show what's left to do")
|
|
288
304
|
.action(async (_opts, cmd) => {
|
|
289
305
|
await runInline(cmd, async (globals) => {
|
|
290
306
|
const quiet = globals.json;
|
|
@@ -320,7 +336,16 @@ export function registerDoctorCommands(program) {
|
|
|
320
336
|
log(`iOS runner: could not fetch (${e instanceof Error ? e.message : String(e)}).`);
|
|
321
337
|
}
|
|
322
338
|
}
|
|
323
|
-
// 3.
|
|
339
|
+
// 3. Android adb — auto-fetchable (Google's standalone platform-tools).
|
|
340
|
+
try {
|
|
341
|
+
const { ensureAdb } = await import("../lib/local-sim/adb.js");
|
|
342
|
+
await ensureAdb();
|
|
343
|
+
log("Android adb: ready.");
|
|
344
|
+
}
|
|
345
|
+
catch (e) {
|
|
346
|
+
log(`Android adb: could not fetch (${e instanceof Error ? e.message : String(e)}).`);
|
|
347
|
+
}
|
|
348
|
+
// 4. Re-run checks so the user sees the resulting state, and hand back
|
|
324
349
|
// the structured result on --json.
|
|
325
350
|
const checks = await runChecks();
|
|
326
351
|
if (globals.json) {
|
|
@@ -23,6 +23,15 @@ function readTextOrAtFile(value) {
|
|
|
23
23
|
}
|
|
24
24
|
return value;
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Native (ios/android) interactive platforms. For these the real target app is
|
|
28
|
+
* supplied at run time via `ish study run --app <bundle>`, so the iteration's
|
|
29
|
+
* `url` is just a placeholder — we don't force the user to invent one.
|
|
30
|
+
* Mirrors `isNativePlatform` in `lib/local-sim/loop.ts`.
|
|
31
|
+
*/
|
|
32
|
+
function isNativePlatform(platform) {
|
|
33
|
+
return platform === "ios" || platform === "android";
|
|
34
|
+
}
|
|
26
35
|
/**
|
|
27
36
|
* Parse a JSON-blob flag that also supports `@filepath` (read from disk).
|
|
28
37
|
* Used for participant_pair `--role-criteria-a/-b` and any future blob inputs.
|
|
@@ -282,7 +291,7 @@ function buildIterationDetails(modality, opts) {
|
|
|
282
291
|
};
|
|
283
292
|
}
|
|
284
293
|
default:
|
|
285
|
-
if (!opts.url) {
|
|
294
|
+
if (!opts.url && !isNativePlatform(opts.platform)) {
|
|
286
295
|
throw new Error("Interactive iterations require --url. Provide the URL to test.");
|
|
287
296
|
}
|
|
288
297
|
if (opts.platform === "figma" && (!opts.fileKey || !opts.startNodeId)) {
|
|
@@ -296,10 +305,18 @@ function buildIterationDetails(modality, opts) {
|
|
|
296
305
|
}
|
|
297
306
|
screenFormat = normalized;
|
|
298
307
|
}
|
|
308
|
+
// Native (ios/android) names its target via app_artifact (a bundle id /
|
|
309
|
+
// package name, or a local .app/.apk path) and carries no url. We accept
|
|
310
|
+
// the target from --app (preferred) or a --url passed out of habit. When
|
|
311
|
+
// neither is given, app_artifact is omitted = "chosen at run time".
|
|
312
|
+
const isNative = isNativePlatform(opts.platform);
|
|
313
|
+
const nativeTarget = isNative ? (opts.app ?? opts.url)?.trim() : undefined;
|
|
299
314
|
return {
|
|
300
315
|
type: "interactive",
|
|
301
316
|
platform: opts.platform || "browser",
|
|
302
|
-
|
|
317
|
+
...(isNative
|
|
318
|
+
? (nativeTarget ? { app_artifact: nativeTarget } : {})
|
|
319
|
+
: { url: opts.url }),
|
|
303
320
|
screen_format: screenFormat,
|
|
304
321
|
...(opts.locale && { locale: opts.locale }),
|
|
305
322
|
...(opts.fileKey && { file_key: opts.fileKey }),
|
|
@@ -365,8 +382,9 @@ Concept pages: ish docs get-page concepts/iteration
|
|
|
365
382
|
.option("--name <name>", "Iteration name (defaults to the next position letter A/B/C… if omitted)")
|
|
366
383
|
.option("--description <description>", "Iteration description")
|
|
367
384
|
// Interactive
|
|
368
|
-
.option("--platform <platform>", "Platform (browser, android, figma, code) — interactive only")
|
|
369
|
-
.option("--url <url>", "URL to test — interactive only")
|
|
385
|
+
.option("--platform <platform>", "Platform (browser, android, ios, figma, code) — interactive only")
|
|
386
|
+
.option("--url <url>", "URL to test — interactive only (optional for ios/android native apps)")
|
|
387
|
+
.option("--app <id>", "Native app bundle id (or .app/.apk path) — ios/android; supplies the iteration target so --url isn't required")
|
|
370
388
|
.option("--screen-format <format>", "Screen format (mobile_portrait, desktop) — interactive only; hyphen/underscore variants accepted")
|
|
371
389
|
.option("--locale <locale>", "Locale code (e.g. en-US) — interactive only")
|
|
372
390
|
.option("--file-key <key>", "Figma file key — required when --platform=figma")
|
|
@@ -596,7 +614,7 @@ Next: \`ish study run\` to dispatch simulations against this iteration.`)
|
|
|
596
614
|
break;
|
|
597
615
|
}
|
|
598
616
|
default:
|
|
599
|
-
if (!opts.url) {
|
|
617
|
+
if (!opts.url && !isNativePlatform(opts.platform)) {
|
|
600
618
|
throw new Error("Interactive iterations require --url. Provide the URL to test.");
|
|
601
619
|
}
|
|
602
620
|
}
|
|
@@ -109,7 +109,7 @@ Tips:
|
|
|
109
109
|
.option("--study <id>", "Study ID; resolves to the latest iteration on that study (alternative to --iteration)")
|
|
110
110
|
.requiredOption("--person <id>", "Participant profile ID")
|
|
111
111
|
.option("--language <lang>", "Language code (e.g. en, sv)")
|
|
112
|
-
.option("--platform <platform>", "Platform (browser, android, figma, code)")
|
|
112
|
+
.option("--platform <platform>", "Platform (browser, android, ios, figma, code)")
|
|
113
113
|
.option("--participant-type <type>", "Participant type (ai, human)", "ai")
|
|
114
114
|
.addHelpText("after", "\nExamples:\n $ ish study participant create --iteration <id> --person <id>\n $ ish study participant create --study s-XXX --person p-XXX\n $ ish study participant create --iteration <id> --person <id> --platform android --json")
|
|
115
115
|
.action(async (opts, cmd) => {
|
|
@@ -24,6 +24,8 @@ import { isMediaModality, isChatModality, iterationHasContent, describeRequiredC
|
|
|
24
24
|
// just `study run --local`. The bun-compiled binary bundles the deep
|
|
25
25
|
// path so it doesn't hit Node's resolver; only the npm path is sensitive.
|
|
26
26
|
import { estimateChatPair, estimateChatSolo, estimateMediaRun } from "../lib/billing.js";
|
|
27
|
+
import { reportReadiness } from "../lib/report-readiness.js";
|
|
28
|
+
import { runChecks, scopeChecks, overall } from "./doctor.js";
|
|
27
29
|
function parseMaxInteractions(value) {
|
|
28
30
|
const n = parseInt(value, 10);
|
|
29
31
|
if (isNaN(n) || n < 1)
|
|
@@ -262,6 +264,7 @@ function readIterationDetails(details) {
|
|
|
262
264
|
...(screenFormat && { screenFormat }),
|
|
263
265
|
...(typeof details.locale === "string" && { locale: details.locale }),
|
|
264
266
|
...(typeof details.title === "string" && { title: details.title }),
|
|
267
|
+
...(typeof details.app_artifact === "string" && { appArtifact: details.app_artifact }),
|
|
265
268
|
};
|
|
266
269
|
}
|
|
267
270
|
/**
|
|
@@ -760,6 +763,24 @@ Examples:
|
|
|
760
763
|
?? platformFromApp
|
|
761
764
|
?? detailsView.platform
|
|
762
765
|
?? "browser";
|
|
766
|
+
// Best-effort native-readiness report. When this is a LOCAL native run
|
|
767
|
+
// (iOS/Android driven on this developer's machine), fire-and-forget a
|
|
768
|
+
// fresh, platform-scoped `runChecks()` to the backend so the web app
|
|
769
|
+
// can render a live native-readiness panel — the native analog of how
|
|
770
|
+
// `ish connect` registers a tunnel. Fully decoupled from the run:
|
|
771
|
+
// never awaited (must not delay dispatch), never blocks, never throws.
|
|
772
|
+
// Browser local runs and all remote runs are skipped (remote runs use
|
|
773
|
+
// Browserbase, so this machine's device readiness is irrelevant there).
|
|
774
|
+
if (opts.local && (resolvedPlatform === "ios" || resolvedPlatform === "android")) {
|
|
775
|
+
const readinessPlatform = resolvedPlatform;
|
|
776
|
+
void (async () => {
|
|
777
|
+
const scoped = scopeChecks(await runChecks(), readinessPlatform);
|
|
778
|
+
await reportReadiness(readinessPlatform, scoped, overall(scoped), globals, { debug: !!opts.debug });
|
|
779
|
+
})().catch(() => {
|
|
780
|
+
// Belt-and-suspenders: reportReadiness already swallows everything,
|
|
781
|
+
// but runChecks() / scopeChecks() must not bubble either.
|
|
782
|
+
});
|
|
783
|
+
}
|
|
763
784
|
if (isPair && pairConfig) {
|
|
764
785
|
// Pair-mode flow mirrors the MCP (`ish-mcp` `_run_pair_mode`):
|
|
765
786
|
// 1. If the iteration already carries `conversations[]` from a
|
|
@@ -884,7 +905,11 @@ Examples:
|
|
|
884
905
|
debug: opts.debug,
|
|
885
906
|
parallel: opts.parallel ? parseInt(opts.parallel, 10) : undefined,
|
|
886
907
|
platform: resolvedPlatform,
|
|
887
|
-
|
|
908
|
+
// Native target: --app overrides; otherwise default from the
|
|
909
|
+
// iteration's remembered app_artifact so reruns need no --app.
|
|
910
|
+
...((opts.app ?? detailsView.appArtifact) && {
|
|
911
|
+
appPath: opts.app ?? detailsView.appArtifact,
|
|
912
|
+
}),
|
|
888
913
|
quiet: globals.quiet,
|
|
889
914
|
json: globals.json,
|
|
890
915
|
});
|
|
@@ -21,6 +21,36 @@ import { dirname, extname, join } from "node:path";
|
|
|
21
21
|
import { withClient, resolveStudy } from "../lib/command-helpers.js";
|
|
22
22
|
import { resolveId } from "../lib/alias-store.js";
|
|
23
23
|
import { output, printTable } from "../lib/output.js";
|
|
24
|
+
import { ApiError } from "../lib/api-client.js";
|
|
25
|
+
/**
|
|
26
|
+
* Server-side screenshots are produced by remote interactive runs only. A
|
|
27
|
+
* study whose only runs were local (`ish study run --local`) has none — and the
|
|
28
|
+
* grouped endpoint currently 500s instead of returning an empty index. Tag this
|
|
29
|
+
* hint onto the error so the bare 500 points the user at the local debug report.
|
|
30
|
+
*/
|
|
31
|
+
const LOCAL_RUN_SCREENSHOT_HINT = [
|
|
32
|
+
"Screenshots are produced by remote runs only.",
|
|
33
|
+
"Ran this study locally (--local)? The per-step screenshots are in the HTML debug report under ~/.ish/debug/ (path printed at the end of each local run).",
|
|
34
|
+
];
|
|
35
|
+
/**
|
|
36
|
+
* GET the frame-grouped screenshot index, tagging the local-run hint onto any
|
|
37
|
+
* ApiError before it propagates to `outputError` (which merges `.suggestions`).
|
|
38
|
+
*/
|
|
39
|
+
async function getGroupedScreenshots(client, studyId) {
|
|
40
|
+
try {
|
|
41
|
+
return await client.get(`/studies/${studyId}/screenshots/grouped`);
|
|
42
|
+
}
|
|
43
|
+
catch (err) {
|
|
44
|
+
if (err instanceof ApiError) {
|
|
45
|
+
const tagged = err;
|
|
46
|
+
tagged.suggestions = [
|
|
47
|
+
...(Array.isArray(tagged.suggestions) ? tagged.suggestions : []),
|
|
48
|
+
...LOCAL_RUN_SCREENSHOT_HINT,
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
throw err;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
24
54
|
function projectScreenshot(s) {
|
|
25
55
|
return {
|
|
26
56
|
id: s.id,
|
|
@@ -106,9 +136,12 @@ Examples:
|
|
|
106
136
|
$ ish study screenshots download <study-id> --id <scid> --out shot.png
|
|
107
137
|
$ ish study screenshots download <study-id> --all --out ./shots/
|
|
108
138
|
|
|
109
|
-
Screenshots are produced server-side by interactive runs only — chat /
|
|
110
|
-
text studies don't have them
|
|
111
|
-
|
|
139
|
+
Screenshots are produced server-side by remote interactive runs only — chat /
|
|
140
|
+
video / text studies don't have them, and neither do local runs
|
|
141
|
+
(\`ish study run --local\`), which instead write a per-step HTML debug report to
|
|
142
|
+
~/.ish/debug/ (the path is printed at the end of each local run). Each row's
|
|
143
|
+
storage URL is self-credentialed, so the CLI fetches bytes without forwarding
|
|
144
|
+
your bearer.`);
|
|
112
145
|
screenshots
|
|
113
146
|
.command("list", { isDefault: true })
|
|
114
147
|
.description("List screenshots for a study (frame-grouped).")
|
|
@@ -117,7 +150,7 @@ so the CLI fetches bytes without forwarding your bearer.`);
|
|
|
117
150
|
.action(async (id, _opts, cmd) => {
|
|
118
151
|
await withClient(cmd, async (client, globals) => {
|
|
119
152
|
const studyId = resolveStudy(id);
|
|
120
|
-
const raw = await client
|
|
153
|
+
const raw = await getGroupedScreenshots(client, studyId);
|
|
121
154
|
const listing = projectListing(studyId, raw);
|
|
122
155
|
if (globals.json) {
|
|
123
156
|
output(listing, true, { preProjected: true });
|
|
@@ -172,7 +205,7 @@ so the CLI fetches bytes without forwarding your bearer.`);
|
|
|
172
205
|
}
|
|
173
206
|
// --all: walk the index, fetch each row, save under --out dir.
|
|
174
207
|
const outDir = opts.out ?? "./screenshots";
|
|
175
|
-
const grouped = await client
|
|
208
|
+
const grouped = await getGroupedScreenshots(client, studyId);
|
|
176
209
|
const all = [
|
|
177
210
|
...(grouped.groups ?? []).flatMap((g) => g.screenshots ?? []),
|
|
178
211
|
...(grouped.uncategorized ?? []),
|
package/dist/lib/api-client.d.ts
CHANGED
|
@@ -40,6 +40,7 @@ export declare class ApiClient {
|
|
|
40
40
|
iteration_id: string;
|
|
41
41
|
}): Promise<import("./local-sim/types.js").LocalSimInitResponse>;
|
|
42
42
|
localSimStep(body: import("./local-sim/types.js").LocalSimStepRequest): Promise<import("./local-sim/types.js").LocalSimStepResponseRaw>;
|
|
43
|
+
localSimRecordInteraction(body: import("./local-sim/types.js").LocalSimInteractionRequest): Promise<import("./local-sim/types.js").LocalSimInteractionResponse>;
|
|
43
44
|
localSimRecord(body: import("./local-sim/types.js").LocalSimRecordRequest): Promise<import("./local-sim/types.js").LocalSimRecordResponse>;
|
|
44
45
|
localSimMatchFrame(body: {
|
|
45
46
|
product_id: string;
|
|
@@ -50,6 +51,8 @@ export declare class ApiClient {
|
|
|
50
51
|
screen_format?: string;
|
|
51
52
|
full_page_screenshot_base64?: string;
|
|
52
53
|
platform?: string;
|
|
54
|
+
previous_frame_version_id?: string;
|
|
55
|
+
same_screen_continuation?: boolean;
|
|
53
56
|
}): Promise<{
|
|
54
57
|
frame_version_id: string;
|
|
55
58
|
}>;
|
package/dist/lib/api-client.js
CHANGED
|
@@ -260,8 +260,13 @@ export class ApiClient {
|
|
|
260
260
|
async localSimStep(body) {
|
|
261
261
|
return this.post("/simulation/local/step", body, { timeout: 60_000 });
|
|
262
262
|
}
|
|
263
|
+
async localSimRecordInteraction(body) {
|
|
264
|
+
return this.post("/simulation/local/interaction", body, { timeout: 30_000 });
|
|
265
|
+
}
|
|
263
266
|
async localSimRecord(body) {
|
|
264
|
-
|
|
267
|
+
// Finalize now runs the pre/post survey + participant summary inline
|
|
268
|
+
// server-side, so it can take materially longer than a plain DB write.
|
|
269
|
+
return this.post("/simulation/local/record", body, { timeout: 180_000 });
|
|
265
270
|
}
|
|
266
271
|
async localSimMatchFrame(body) {
|
|
267
272
|
return this.post("/simulation/local/match-frame", body, { timeout: 30_000 });
|
package/dist/lib/docs.js
CHANGED
|
@@ -380,6 +380,14 @@ ish iteration create --platform figma --url https://figma.com/proto \\
|
|
|
380
380
|
--screen-format mobile_portrait --file-key abc123 --start-node-id 0:1 \\
|
|
381
381
|
--flow-name "Onboarding A"
|
|
382
382
|
|
|
383
|
+
# Native app (ios / android): --app names the target, stored as app_artifact (no URL).
|
|
384
|
+
ish iteration create --platform ios --app com.example.app
|
|
385
|
+
ish iteration create --platform ios # --app optional; "chosen at run time"
|
|
386
|
+
# drive it locally against a booted simulator / emulator — the iteration
|
|
387
|
+
# remembers the app, so no --app needed on reruns:
|
|
388
|
+
ish study run --local
|
|
389
|
+
ish study run --local --app ./Build.app # override with a fresh local build
|
|
390
|
+
|
|
383
391
|
# Text/email content from a file:
|
|
384
392
|
ish iteration create --content-text @./email.html --title "Newsletter"
|
|
385
393
|
|
|
@@ -1992,10 +2000,14 @@ Interactive study runs produce per-frame screenshots server-side. They
|
|
|
1992
2000
|
let you (or an agent) see what participants actually saw alongside the
|
|
1993
2001
|
sentiment summary.
|
|
1994
2002
|
|
|
1995
|
-
## Screenshots — interactive studies only
|
|
2003
|
+
## Screenshots — remote interactive studies only
|
|
1996
2004
|
|
|
1997
|
-
Screenshots are produced by interactive runs only — chat / video /
|
|
1998
|
-
studies don't have them.
|
|
2005
|
+
Screenshots are produced by remote interactive runs only — chat / video /
|
|
2006
|
+
text studies don't have them. **Local runs** (\`ish study run --local\`,
|
|
2007
|
+
including ios/android) don't push screenshots to the server either; they
|
|
2008
|
+
write a per-step HTML debug report to \`~/.ish/debug/sim-*.html\` (the path is
|
|
2009
|
+
printed at the end of the run). \`ish study screenshots list\` on a local-only
|
|
2010
|
+
study therefore returns nothing useful — open the debug report instead.
|
|
1999
2011
|
|
|
2000
2012
|
### CLI
|
|
2001
2013
|
|
|
@@ -25,6 +25,24 @@ export declare function resolveTextValue(action: LocalStepAction, contextValues:
|
|
|
25
25
|
* Compare two base64 screenshots to detect visible change.
|
|
26
26
|
*/
|
|
27
27
|
export declare function detectNoVisibleChange(before: string, after: string): boolean;
|
|
28
|
+
/**
|
|
29
|
+
* Logical classification of what a completed step's batch of actions did to the
|
|
30
|
+
* screen, used to tell the backend's frame matcher whether the NEXT observation
|
|
31
|
+
* is the same logical screen (so it reuses the previous frame instead of minting
|
|
32
|
+
* a new one off shifted pixels).
|
|
33
|
+
*
|
|
34
|
+
* - "scroll": a pure vertical scroll (all actions succeeded) — same screen.
|
|
35
|
+
* - "keyboard": raised the keyboard without submitting/navigating — same screen.
|
|
36
|
+
* - "ui_change": anything that (likely) changed the logical screen — NOT same.
|
|
37
|
+
* - "none": nothing actionable happened (empty batch / think-only).
|
|
38
|
+
*/
|
|
39
|
+
export type StepKind = "scroll" | "keyboard" | "ui_change" | "none";
|
|
40
|
+
/**
|
|
41
|
+
* Classify a step's batch of actions into a StepKind. Pure — `successByIndex[i]`
|
|
42
|
+
* is the executor's `result.success` for `actions[i]`. `think` actions are
|
|
43
|
+
* ignored entirely (they neither move the screen nor count toward a verdict).
|
|
44
|
+
*/
|
|
45
|
+
export declare function classifyStepKind(actions: LocalStepAction[], successByIndex: boolean[]): StepKind;
|
|
28
46
|
/**
|
|
29
47
|
* Build a human-readable action description matching backend's format_action_detail().
|
|
30
48
|
*/
|
|
@@ -403,6 +403,36 @@ function isRecoverableError(err) {
|
|
|
403
403
|
export function detectNoVisibleChange(before, after) {
|
|
404
404
|
return before === after;
|
|
405
405
|
}
|
|
406
|
+
// Scroll directions that keep us on the same logical screen (vertical pan).
|
|
407
|
+
// undefined/null defaults to a downward scroll in executeScroll(), so it counts.
|
|
408
|
+
const VERTICAL_SCROLL_DIRECTIONS = new Set([
|
|
409
|
+
"up", "down", "to_top", "to_bottom", "to_element",
|
|
410
|
+
]);
|
|
411
|
+
/**
|
|
412
|
+
* Classify a step's batch of actions into a StepKind. Pure — `successByIndex[i]`
|
|
413
|
+
* is the executor's `result.success` for `actions[i]`. `think` actions are
|
|
414
|
+
* ignored entirely (they neither move the screen nor count toward a verdict).
|
|
415
|
+
*/
|
|
416
|
+
export function classifyStepKind(actions, successByIndex) {
|
|
417
|
+
// Keep the original index so success lines up after dropping `think`s.
|
|
418
|
+
const considered = actions
|
|
419
|
+
.map((action, i) => ({ action, success: successByIndex[i] }))
|
|
420
|
+
.filter(({ action }) => action.type !== "think");
|
|
421
|
+
if (considered.length === 0)
|
|
422
|
+
return "none";
|
|
423
|
+
// Pure vertical scroll: every action a successful vertical scroll.
|
|
424
|
+
const allScroll = considered.every(({ action, success }) => action.type === "scroll" &&
|
|
425
|
+
success === true &&
|
|
426
|
+
VERTICAL_SCROLL_DIRECTIONS.has(action.direction ?? "down"));
|
|
427
|
+
if (allScroll)
|
|
428
|
+
return "scroll";
|
|
429
|
+
// Keyboard-only: every action a text_input that raises the keyboard WITHOUT
|
|
430
|
+
// submitting/navigating. A submit makes it a ui_change (it can navigate away).
|
|
431
|
+
const allKeyboard = considered.every(({ action }) => action.type === "text_input" && !action.submit);
|
|
432
|
+
if (allKeyboard)
|
|
433
|
+
return "keyboard";
|
|
434
|
+
return "ui_change";
|
|
435
|
+
}
|
|
406
436
|
/**
|
|
407
437
|
* Build a human-readable action description matching backend's format_action_detail().
|
|
408
438
|
*/
|
|
@@ -10,6 +10,8 @@
|
|
|
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
|
+
/** Resolve adb, downloading Google's platform-tools on first use if not found. */
|
|
14
|
+
export declare function ensureAdb(): Promise<string>;
|
|
13
15
|
export declare class AdbError extends Error {
|
|
14
16
|
constructor(message: string);
|
|
15
17
|
}
|
|
@@ -17,6 +19,27 @@ export declare class AdbError extends Error {
|
|
|
17
19
|
export declare function adb(args: string[], timeoutMs?: number): Promise<string>;
|
|
18
20
|
/** Run `adb shell <args>` and return trimmed stdout. */
|
|
19
21
|
export declare function adbShell(args: string[], timeoutMs?: number): Promise<string>;
|
|
22
|
+
/**
|
|
23
|
+
* Pull versionName / versionCode out of `dumpsys package <pkg>` text. The
|
|
24
|
+
* relevant lines read `versionCode=42 minSdk=24 targetSdk=34` and
|
|
25
|
+
* `versionName=1.2.3`; `\d+` stops the build before the trailing tokens and
|
|
26
|
+
* `\S+` takes the version up to the next space. Returns null when neither is
|
|
27
|
+
* present (wrong/empty package).
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseDumpsysAppBuild(out: string): {
|
|
30
|
+
version: string | null;
|
|
31
|
+
build: string | null;
|
|
32
|
+
} | null;
|
|
33
|
+
/**
|
|
34
|
+
* Read an installed package's versionName / versionCode from
|
|
35
|
+
* `dumpsys package <pkg>`. Best-effort: returns null on any failure (the run
|
|
36
|
+
* never depends on it). Covers both freshly-installed apks and pre-installed
|
|
37
|
+
* packages — by call time the package name is already resolved.
|
|
38
|
+
*/
|
|
39
|
+
export declare function appBuildFromDevice(pkg: string): Promise<{
|
|
40
|
+
version: string | null;
|
|
41
|
+
build: string | null;
|
|
42
|
+
} | null>;
|
|
20
43
|
/**
|
|
21
44
|
* Capture the current screen as raw PNG bytes via `adb exec-out screencap -p`.
|
|
22
45
|
* `exec-out` (not `shell`) avoids the CRLF translation that corrupts binary
|