@ishlabs/cli 0.26.0 → 0.27.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/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 +4 -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 +39 -0
- package/dist/lib/local-sim/adb.js +152 -17
- package/dist/lib/local-sim/android.d.ts +12 -4
- package/dist/lib/local-sim/android.js +44 -11
- package/dist/lib/local-sim/device.d.ts +44 -0
- package/dist/lib/local-sim/ios.d.ts +12 -5
- package/dist/lib/local-sim/ios.js +45 -11
- package/dist/lib/local-sim/loop.js +220 -26
- package/dist/lib/local-sim/native-a11y.d.ts +24 -0
- package/dist/lib/local-sim/native-a11y.js +76 -14
- package/dist/lib/local-sim/screen-signature.d.ts +77 -0
- package/dist/lib/local-sim/screen-signature.js +166 -0
- 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/local-sim/xcuitest.d.ts +7 -0
- package/dist/lib/local-sim/xcuitest.js +16 -0
- package/dist/lib/modality.js +7 -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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native "screen signature" v2 — a SCROLL-INVARIANT structural identity for a
|
|
3
|
+
* logical native screen, derived from the accessibility tree, sent to the
|
|
4
|
+
* backend as an entry/cross-run frame anchor (Phase 2 of native frame
|
|
5
|
+
* continuity; Phase 1 reuses the prior frame on pure scroll/keyboard steps).
|
|
6
|
+
*
|
|
7
|
+
* FCIS: this module is PURE (NativeNode[] + coarse inputs in, signature out) —
|
|
8
|
+
* no device access. The device gathers the coarse inputs (foreground activity /
|
|
9
|
+
* bundle id) and the parsed tree; this turns them into `{value, usable}`.
|
|
10
|
+
*
|
|
11
|
+
* The signature has two parts:
|
|
12
|
+
* coarse — a cheap, almost-always-available anchor (android `package|activity`,
|
|
13
|
+
* ios `bundleId|navTitle`).
|
|
14
|
+
* tokens — the persistent CHROME tokens that are NOT scroll content. Each
|
|
15
|
+
* chrome node contributes its resource-id (`id:…`) AND its label
|
|
16
|
+
* (`tx:…`) when present. This is what makes the signature
|
|
17
|
+
* scroll-invariant AND lets two same-activity screens be told apart.
|
|
18
|
+
*
|
|
19
|
+
* WHY v2 (two verified gaps in the id-only v1):
|
|
20
|
+
* 1. LABELS close the shared-chrome OVER-MERGE. A single-Activity app — Jetpack
|
|
21
|
+
* Compose (exposes NO resource-ids beyond the framework `android:id/content`)
|
|
22
|
+
* or a View app with a fixed toolbar+container shared across fragments —
|
|
23
|
+
* gives two DISTINCT screens the SAME id-set → identical signature → SILENT
|
|
24
|
+
* over-merge (the cardinal failure). But those screens DO differ in chrome
|
|
25
|
+
* LABELS (a home screen vs a settings sub-screen show different toolbar /
|
|
26
|
+
* button text). Including labels makes distinct screens produce distinct
|
|
27
|
+
* signatures, and makes Compose usable at all (label-only tokens).
|
|
28
|
+
* 2. STRUCTURAL scroll-exclusion replaces v1's geometric `contains()`. v1
|
|
29
|
+
* excluded scroll content by bounds-containment, which (a) mis-flagged an
|
|
30
|
+
* overlay/FAB sitting inside a list's rect as content (→ could over-merge),
|
|
31
|
+
* and (b) on iOS the scroll CONTAINER is isAccessible=0 and pruned from the
|
|
32
|
+
* NativeNode[], so geometric exclusion never fired (scroll changed the
|
|
33
|
+
* signature → over-split, feature inert). v2 excludes by TREE STRUCTURE: a
|
|
34
|
+
* node is content iff `insideScrollable` (it has a scrollable ANCESTOR),
|
|
35
|
+
* computed during parsing — see native-a11y.ts. The scroll container's OWN
|
|
36
|
+
* tokens are kept (it's durable chrome; `insideScrollable` is about
|
|
37
|
+
* descendants).
|
|
38
|
+
*
|
|
39
|
+
* The remaining failure mode after v2 is SAFE: dynamic chrome labels (a live
|
|
40
|
+
* clock, an unread badge) cause OVER-SPLIT (a new frame), never over-merge — the
|
|
41
|
+
* backend just mints a fresh frame, which is the conservative direction.
|
|
42
|
+
*
|
|
43
|
+
* USABLE GUARD (load-bearing, unchanged in spirit): `usable` is true only with
|
|
44
|
+
* >= MIN_STABLE_TOKENS tokens. A signature derived from an empty/sparse token
|
|
45
|
+
* set must NEVER be sent — sha1("") (and any near-empty set) collides across
|
|
46
|
+
* distinct screens and would silently over-merge them. When unusable the caller
|
|
47
|
+
* omits the field entirely and the backend falls back to Phase-1 continuity.
|
|
48
|
+
* This is the SAFE default: Flutter (no a11y tree) and the sparsest screens
|
|
49
|
+
* degrade here; id-rich Android and label-rich Compose are the validated wins.
|
|
50
|
+
*/
|
|
51
|
+
import { createHash } from "node:crypto";
|
|
52
|
+
/** Minimum stable-chrome tokens for a signature to be usable (sent to the backend). */
|
|
53
|
+
export const MIN_STABLE_TOKENS = 2;
|
|
54
|
+
/**
|
|
55
|
+
* App-bar / title chrome resource-ids. A node whose resource-id matches carries
|
|
56
|
+
* the SCREEN TITLE (e.g. a CollapsingToolbar's title, an ActionBar/Toolbar title).
|
|
57
|
+
* On modern Android the title sits INSIDE the scrollable app-bar (CoordinatorLayout
|
|
58
|
+
* / NestedScrollView), so structural scroll-exclusion drops it — collapsing every
|
|
59
|
+
* sub-screen of a single host activity (e.g. Android Settings' `.SubSettings`) to
|
|
60
|
+
* the SAME generic outer-container ids and silently OVER-MERGING distinct screens
|
|
61
|
+
* (proven live: Display/Apps/Network all → one frame). We rescue the title LABEL
|
|
62
|
+
* even when `insideScrollable`: the title text is the screen's most reliable
|
|
63
|
+
* discriminator and is scroll-INVARIANT (it stays constant as the bar collapses).
|
|
64
|
+
*
|
|
65
|
+
* WHY no `_title$` here (removed): a trailing-`_title$` rescue was originally added
|
|
66
|
+
* to also catch Android `homepage_title`, but (a) `homepage_title` is the VOLATILE
|
|
67
|
+
* home big-title we deliberately do NOT want to rescue, and (b) `_title$`
|
|
68
|
+
* OVER-MATCHES iOS list-row section ids that are pure SCROLL CONTENT —
|
|
69
|
+
* `MOTION_TITLE`, `SPEECH_TITLE`, `LIVE_SPEECH_TITLE`, `VOCAL_SHORTCUTS_TITLE`
|
|
70
|
+
* (role=text, insideScrollable=true). Rescuing those re-includes scroll-content row
|
|
71
|
+
* labels in the signature, so scrolling reveals different rows and CHURNS the
|
|
72
|
+
* signature → over-fragment (iOS Settings/Accessibility split into two frames). The
|
|
73
|
+
* SubSettings over-merge discriminator rides on the explicitly-matched
|
|
74
|
+
* `collapsing_toolbar` / `action_bar` / `toolbar`, NOT on an arbitrary `_title$`.
|
|
75
|
+
*/
|
|
76
|
+
const TITLE_CHROME_ID = /(collapsing_toolbar|action_bar|toolbar)$/;
|
|
77
|
+
function isTitleChrome(resourceId) {
|
|
78
|
+
return resourceId !== "" && TITLE_CHROME_ID.test(resourceId.toLowerCase());
|
|
79
|
+
}
|
|
80
|
+
/** Max length of a label token's value — bounds the hashed string on chatty labels. */
|
|
81
|
+
const LABEL_TOKEN_MAX_LENGTH = 64;
|
|
82
|
+
/**
|
|
83
|
+
* Normalized role of the iOS navigation bar after `normalizeRole` in
|
|
84
|
+
* native-a11y.ts (`AXNavigationBar`/`NavigationBar` → "navigationbar"). The
|
|
85
|
+
* spike matched the raw "NavigationBar" role; the CLI's NativeNode carries the
|
|
86
|
+
* normalized role, so we match the normalized form here.
|
|
87
|
+
*/
|
|
88
|
+
const IOS_NAV_BAR_ROLE = "navigationbar";
|
|
89
|
+
function sha1(s) {
|
|
90
|
+
return createHash("sha1").update(s).digest("hex");
|
|
91
|
+
}
|
|
92
|
+
/** First nav-bar node's label (iOS), else "" — best-effort coarse signal. */
|
|
93
|
+
function iosNavTitle(nodes) {
|
|
94
|
+
const nav = nodes.find((n) => n.role === IOS_NAV_BAR_ROLE);
|
|
95
|
+
return nav ? nav.label.trim() : "";
|
|
96
|
+
}
|
|
97
|
+
/** The coarse, almost-always-available anchor token. */
|
|
98
|
+
function coarseToken(nodes, coarse) {
|
|
99
|
+
if (coarse.platform === "android") {
|
|
100
|
+
return `${coarse.package ?? ""}|${coarse.activity ?? ""}`;
|
|
101
|
+
}
|
|
102
|
+
return `${coarse.bundleId ?? ""}|${iosNavTitle(nodes)}`;
|
|
103
|
+
}
|
|
104
|
+
/** Collapse whitespace, lowercase, and cap length so a label token is stable. */
|
|
105
|
+
function normalizeLabelToken(label) {
|
|
106
|
+
const collapsed = label.replace(/\s+/g, " ").trim().toLowerCase();
|
|
107
|
+
return collapsed.length <= LABEL_TOKEN_MAX_LENGTH
|
|
108
|
+
? collapsed
|
|
109
|
+
: collapsed.slice(0, LABEL_TOKEN_MAX_LENGTH);
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Sorted, de-duped token set of the persistent CHROME — every node that is NOT
|
|
113
|
+
* scroll content (`!insideScrollable`). Each such node contributes:
|
|
114
|
+
* - `id:<resourceId>` when its resource-id is non-empty, and
|
|
115
|
+
* - `tx:<label>` when its label is non-empty (normalized).
|
|
116
|
+
* A scroll CONTAINER's own tokens ARE kept (the container is durable chrome;
|
|
117
|
+
* `insideScrollable` flags its descendants, not the container). Labels are the
|
|
118
|
+
* v2 win: they discriminate single-Activity screens that share a chrome id-set
|
|
119
|
+
* (and give Compose, which exposes no ids, a usable signature at all).
|
|
120
|
+
*/
|
|
121
|
+
function stableTokenSet(nodes) {
|
|
122
|
+
const out = new Set();
|
|
123
|
+
for (const n of nodes) {
|
|
124
|
+
const id = (n.resourceId ?? "").trim();
|
|
125
|
+
const label = n.label.trim();
|
|
126
|
+
// App-bar / title chrome carries the screen's name but sits inside the
|
|
127
|
+
// scrollable app-bar on modern Android — rescue its scroll-invariant LABEL
|
|
128
|
+
// even when insideScrollable (its generic container id is NOT added; the
|
|
129
|
+
// label is the discriminator). See TITLE_CHROME_ID.
|
|
130
|
+
if (label && isTitleChrome(id))
|
|
131
|
+
out.add(`tx:${normalizeLabelToken(label)}`);
|
|
132
|
+
if (n.insideScrollable)
|
|
133
|
+
continue; // scroll content — shifts under a scroll
|
|
134
|
+
// Suppress the `id:` token for a self-named / generic nav control. On iOS,
|
|
135
|
+
// parseXcuiHierarchy promotes the WDA `name` to resourceId, but WDA reports a
|
|
136
|
+
// NON-DETERMINISTIC name for the nav back button — "BackButton" when fresh,
|
|
137
|
+
// the parent screen's title (e.g. "Settings") once scrolled — while its
|
|
138
|
+
// visible label stays constant. That churn flips the `id:` token between
|
|
139
|
+
// scroll states and fragments the same screen. Drop the id when it's the
|
|
140
|
+
// literal "BackButton" OR is redundant with the node's own label (id ===
|
|
141
|
+
// normalized label): in both cases the `id:` carries no identity beyond the
|
|
142
|
+
// already-emitted `tx:` label. We KEEP the `tx:` token below.
|
|
143
|
+
const redundantNavId = id !== "" &&
|
|
144
|
+
(id === "BackButton" ||
|
|
145
|
+
(label !== "" && normalizeLabelToken(id) === normalizeLabelToken(label)));
|
|
146
|
+
if (id && !redundantNavId)
|
|
147
|
+
out.add(`id:${id}`);
|
|
148
|
+
if (label)
|
|
149
|
+
out.add(`tx:${normalizeLabelToken(label)}`);
|
|
150
|
+
}
|
|
151
|
+
return [...out].sort();
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Compute the screen signature from this step's parsed tree + coarse inputs.
|
|
155
|
+
* `value` is `platform|coarse|sha1(tokens)`; `usable` gates whether it's safe to
|
|
156
|
+
* send (>= MIN_STABLE_TOKENS distinct stable chrome tokens).
|
|
157
|
+
*/
|
|
158
|
+
export function computeScreenSignature(nodes, coarse) {
|
|
159
|
+
const tokens = stableTokenSet(nodes);
|
|
160
|
+
const value = `${coarse.platform}|${coarseToken(nodes, coarse)}|${sha1(tokens.join(","))}`;
|
|
161
|
+
return {
|
|
162
|
+
value,
|
|
163
|
+
usable: tokens.length >= MIN_STABLE_TOKENS,
|
|
164
|
+
tokenCount: tokens.length,
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -53,3 +53,18 @@ export declare function isAppInstalled(udid: string, bundleId: string): Promise<
|
|
|
53
53
|
* terminate+launch a just-installed app without diffing the app list.
|
|
54
54
|
*/
|
|
55
55
|
export declare function bundleIdFromApp(appPath: string): Promise<string | null>;
|
|
56
|
+
/**
|
|
57
|
+
* Read the installed app's marketing version + build number from the booted
|
|
58
|
+
* simulator. `simctl listapps` emits a (NeXTSTEP) plist of every installed
|
|
59
|
+
* bundle; we round-trip it through `plutil -convert json` and index by bundle
|
|
60
|
+
* id. JSON-not-keypath because a bundle id's dots (`com.apple.Preferences`)
|
|
61
|
+
* collide with plutil's `-extract` keypath separator.
|
|
62
|
+
*
|
|
63
|
+
* Best-effort: returns null on any failure (the run never depends on it). Works
|
|
64
|
+
* for both CLI-installed `.app`s and pre-installed system/app-store bundles —
|
|
65
|
+
* by call time the bundle id is already resolved.
|
|
66
|
+
*/
|
|
67
|
+
export declare function appBuildFromSimulator(udid: string, bundleId: string): Promise<{
|
|
68
|
+
version: string | null;
|
|
69
|
+
build: string | null;
|
|
70
|
+
} | null>;
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { execFile } from "node:child_process";
|
|
18
18
|
import { existsSync } from "node:fs";
|
|
19
|
-
import { mkdtemp, readFile, rm } from "node:fs/promises";
|
|
19
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
20
20
|
import { tmpdir } from "node:os";
|
|
21
21
|
import { join } from "node:path";
|
|
22
22
|
import { promisify } from "node:util";
|
|
@@ -142,3 +142,43 @@ export async function bundleIdFromApp(appPath) {
|
|
|
142
142
|
return null;
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Read the installed app's marketing version + build number from the booted
|
|
147
|
+
* simulator. `simctl listapps` emits a (NeXTSTEP) plist of every installed
|
|
148
|
+
* bundle; we round-trip it through `plutil -convert json` and index by bundle
|
|
149
|
+
* id. JSON-not-keypath because a bundle id's dots (`com.apple.Preferences`)
|
|
150
|
+
* collide with plutil's `-extract` keypath separator.
|
|
151
|
+
*
|
|
152
|
+
* Best-effort: returns null on any failure (the run never depends on it). Works
|
|
153
|
+
* for both CLI-installed `.app`s and pre-installed system/app-store bundles —
|
|
154
|
+
* by call time the bundle id is already resolved.
|
|
155
|
+
*/
|
|
156
|
+
export async function appBuildFromSimulator(udid, bundleId) {
|
|
157
|
+
const dir = await mkdtemp(join(tmpdir(), "ish-ios-apps-"));
|
|
158
|
+
const path = join(dir, "apps.plist");
|
|
159
|
+
try {
|
|
160
|
+
const { stdout } = await execFileAsync(XCRUN, ["simctl", "listapps", udid], {
|
|
161
|
+
timeout: 60_000,
|
|
162
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
163
|
+
});
|
|
164
|
+
await writeFile(path, stdout);
|
|
165
|
+
const { stdout: json } = await execFileAsync(PLUTIL, ["-convert", "json", "-o", "-", path], {
|
|
166
|
+
timeout: 10_000,
|
|
167
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
168
|
+
});
|
|
169
|
+
const apps = JSON.parse(json);
|
|
170
|
+
const app = apps[bundleId];
|
|
171
|
+
if (!app)
|
|
172
|
+
return null;
|
|
173
|
+
return {
|
|
174
|
+
version: app.CFBundleShortVersionString ?? null,
|
|
175
|
+
build: app.CFBundleVersion ?? null,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
catch {
|
|
179
|
+
return null;
|
|
180
|
+
}
|
|
181
|
+
finally {
|
|
182
|
+
await rm(dir, { recursive: true, force: true }).catch(() => { });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -12,10 +12,12 @@ export interface LocalSimInitRequest {
|
|
|
12
12
|
iteration_id: string;
|
|
13
13
|
}
|
|
14
14
|
export interface IterationDetails {
|
|
15
|
-
url
|
|
15
|
+
url?: string;
|
|
16
16
|
platform: string;
|
|
17
17
|
screen_format: "desktop" | "mobile_portrait";
|
|
18
18
|
locale?: string;
|
|
19
|
+
/** Native (ios/android): the app target to install/launch (bundle id / package / path). */
|
|
20
|
+
app_artifact?: string;
|
|
19
21
|
}
|
|
20
22
|
export interface LocalSimInitResponse {
|
|
21
23
|
participant_id: string;
|
|
@@ -221,6 +223,14 @@ export interface RecordInteraction {
|
|
|
221
223
|
assignment_status: AssignmentStatus;
|
|
222
224
|
tabs?: LocalTabInfo[];
|
|
223
225
|
}
|
|
226
|
+
export interface LocalSimInteractionRequest {
|
|
227
|
+
participant_id: string;
|
|
228
|
+
product_id: string;
|
|
229
|
+
interaction: RecordInteraction;
|
|
230
|
+
}
|
|
231
|
+
export interface LocalSimInteractionResponse {
|
|
232
|
+
interaction_id: string;
|
|
233
|
+
}
|
|
224
234
|
export interface AssignmentStatusUpdate {
|
|
225
235
|
assignment_id: string;
|
|
226
236
|
status: string;
|
|
@@ -229,7 +239,6 @@ export interface AssignmentStatusUpdate {
|
|
|
229
239
|
export interface LocalSimRecordRequest {
|
|
230
240
|
participant_id: string;
|
|
231
241
|
product_id: string;
|
|
232
|
-
interactions: RecordInteraction[];
|
|
233
242
|
final_status: string;
|
|
234
243
|
assignment_statuses: AssignmentStatusUpdate[];
|
|
235
244
|
}
|
|
@@ -45,6 +45,13 @@ export declare function closeWda(udid: string): Promise<void>;
|
|
|
45
45
|
export declare function describeScreen(udid: string): Promise<IosScreen>;
|
|
46
46
|
/** Raw WDA `/source?format=json` string — feed to `parseXcuiHierarchy`. */
|
|
47
47
|
export declare function describeAll(udid: string): Promise<string>;
|
|
48
|
+
/**
|
|
49
|
+
* The foreground app's bundle id via WDA `GET /wda/activeAppInfo`, a coarse
|
|
50
|
+
* input for the screen signature. Best-effort: returns "" on any failure (the
|
|
51
|
+
* signature degrades to a bundleId-less coarse token, and the run never depends
|
|
52
|
+
* on this read).
|
|
53
|
+
*/
|
|
54
|
+
export declare function activeBundleId(udid: string): Promise<string>;
|
|
48
55
|
export declare function uiTap(udid: string, x: number, y: number): Promise<void>;
|
|
49
56
|
export declare function uiLongPress(udid: string, x: number, y: number, durationMs?: number): Promise<void>;
|
|
50
57
|
export declare function uiSwipe(udid: string, x1: number, y1: number, x2: number, y2: number, durationMs?: number): Promise<void>;
|
|
@@ -250,6 +250,22 @@ export async function describeAll(udid) {
|
|
|
250
250
|
const json = await wdaCall(s.port, "GET", `/session/${s.sessionId}/source?format=json`);
|
|
251
251
|
return JSON.stringify(json);
|
|
252
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* The foreground app's bundle id via WDA `GET /wda/activeAppInfo`, a coarse
|
|
255
|
+
* input for the screen signature. Best-effort: returns "" on any failure (the
|
|
256
|
+
* signature degrades to a bundleId-less coarse token, and the run never depends
|
|
257
|
+
* on this read).
|
|
258
|
+
*/
|
|
259
|
+
export async function activeBundleId(udid) {
|
|
260
|
+
try {
|
|
261
|
+
const s = await getSession(udid);
|
|
262
|
+
const v = unwrap(await wdaCall(s.port, "GET", "/wda/activeAppInfo"));
|
|
263
|
+
return typeof v.bundleId === "string" ? v.bundleId : "";
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
return "";
|
|
267
|
+
}
|
|
268
|
+
}
|
|
253
269
|
// ── Gestures (W3C pointer actions; coordinates in POINTS) ────────────────────
|
|
254
270
|
function pointerAction(steps) {
|
|
255
271
|
return {
|
package/dist/lib/modality.js
CHANGED
|
@@ -97,8 +97,13 @@ export function iterationHasContent(details, modality) {
|
|
|
97
97
|
const ep = readExternalChatbotEndpoint(details);
|
|
98
98
|
return !!ep.endpoint || !!ep.chatbot_endpoint_id;
|
|
99
99
|
}
|
|
100
|
-
// interactive (default)
|
|
101
|
-
|
|
100
|
+
// interactive (default): browser/figma iterations carry a `url`; native
|
|
101
|
+
// (ios/android) iterations carry an `app_artifact` instead and need no URL
|
|
102
|
+
// (the backend made url optional for native — ish-backend 9708e06e). Either
|
|
103
|
+
// satisfies "has content".
|
|
104
|
+
const hasUrl = typeof d.url === "string" && d.url.length > 0;
|
|
105
|
+
const hasApp = typeof d.app_artifact === "string" && d.app_artifact.length > 0;
|
|
106
|
+
return hasUrl || hasApp;
|
|
102
107
|
}
|
|
103
108
|
/** The flag fragment a user should pass to populate content for a given modality. */
|
|
104
109
|
export function describeRequiredContentFlag(modality, chatMode) {
|
package/dist/lib/paths.d.ts
CHANGED
|
@@ -20,4 +20,10 @@ export declare function cloudflaredBin(): string;
|
|
|
20
20
|
export declare function wdaDir(): string;
|
|
21
21
|
/** Stamp file recording which CLI/runner version the cached WDA bundle is for. */
|
|
22
22
|
export declare function wdaVersionFile(): string;
|
|
23
|
+
/**
|
|
24
|
+
* Path to `adb` inside Google's standalone Android platform-tools, when we've
|
|
25
|
+
* fetched it into `binDir()` on demand (the zip unpacks a `platform-tools/`
|
|
26
|
+
* dir). Mirrors `cloudflaredBin()` / `wdaDir()`.
|
|
27
|
+
*/
|
|
28
|
+
export declare function adbBin(): string;
|
|
23
29
|
export declare function connectLockPath(): string;
|
package/dist/lib/paths.js
CHANGED
|
@@ -46,6 +46,15 @@ export function wdaDir() {
|
|
|
46
46
|
export function wdaVersionFile() {
|
|
47
47
|
return path.join(wdaDir(), "VERSION");
|
|
48
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* Path to `adb` inside Google's standalone Android platform-tools, when we've
|
|
51
|
+
* fetched it into `binDir()` on demand (the zip unpacks a `platform-tools/`
|
|
52
|
+
* dir). Mirrors `cloudflaredBin()` / `wdaDir()`.
|
|
53
|
+
*/
|
|
54
|
+
export function adbBin() {
|
|
55
|
+
const exe = process.platform === "win32" ? "adb.exe" : "adb";
|
|
56
|
+
return path.join(binDir(), "platform-tools", exe);
|
|
57
|
+
}
|
|
49
58
|
export function connectLockPath() {
|
|
50
59
|
return path.join(rootDir(), "connect.lock");
|
|
51
60
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort readiness reporting to the ish backend.
|
|
3
|
+
*
|
|
4
|
+
* After the CLI computes its native-simulation readiness checks (the same
|
|
5
|
+
* array `ish check <platform> --json` emits), we POST them to
|
|
6
|
+
* `${apiUrl}/api/v1/connect/device` so the ish web app can render a live
|
|
7
|
+
* native-readiness panel. This is the native analog of how `ish connect
|
|
8
|
+
* <port>` registers a cloudflare tunnel: a side-channel that tells the
|
|
9
|
+
* backend "this developer's machine is (or isn't) ready to drive a native
|
|
10
|
+
* iOS/Android simulation right now."
|
|
11
|
+
*
|
|
12
|
+
* Contract (must match the backend exactly):
|
|
13
|
+
*
|
|
14
|
+
* POST /api/v1/connect/device
|
|
15
|
+
* {
|
|
16
|
+
* "platform": "ios" | "android",
|
|
17
|
+
* "checks": [ {key,name,group,status,message?,fix?}, ... ],
|
|
18
|
+
* "overall": "pass" | "warn" | "fail" | "skip",
|
|
19
|
+
* "cli_version": "<package.json version>"
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* This is COMPLETELY best-effort. It never throws, never blocks the command,
|
|
23
|
+
* never writes to stdout, and silently returns when the user isn't logged in
|
|
24
|
+
* or is offline. The CLI's normal output and exit code are unaffected. The
|
|
25
|
+
* only side-channel is an optional stderr line under a `debug` flag, mirroring
|
|
26
|
+
* how connect.ts logs its own warnings.
|
|
27
|
+
*/
|
|
28
|
+
import type { GlobalOpts } from "./command-helpers.js";
|
|
29
|
+
import type { Check, CheckStatus } from "../commands/doctor.js";
|
|
30
|
+
/**
|
|
31
|
+
* POST the platform-scoped readiness checks to the backend. Best-effort:
|
|
32
|
+
* resolves auth + posts inside a single try/catch, swallowing every error.
|
|
33
|
+
*
|
|
34
|
+
* @param platform The native platform the checks were scoped to.
|
|
35
|
+
* @param checks The exact `runChecks()` array (already scoped to `platform`).
|
|
36
|
+
* @param overall The aggregate status (`overall(checks)`).
|
|
37
|
+
* @param globals Resolved CLI globals — used only for `--api-url` / `--dev`
|
|
38
|
+
* / `--token` resolution. Pass the command's `globals`.
|
|
39
|
+
* @param opts.debug When true, log a one-line failure note to stderr (off by
|
|
40
|
+
* default so the post is silent). Mirrors connect.ts.
|
|
41
|
+
*/
|
|
42
|
+
export declare function reportReadiness(platform: "ios" | "android", checks: Check[], overall: CheckStatus, globals: Pick<GlobalOpts, "apiUrl" | "dev" | "token" | "tokenFile">, opts?: {
|
|
43
|
+
debug?: boolean;
|
|
44
|
+
}): Promise<void>;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Best-effort readiness reporting to the ish backend.
|
|
3
|
+
*
|
|
4
|
+
* After the CLI computes its native-simulation readiness checks (the same
|
|
5
|
+
* array `ish check <platform> --json` emits), we POST them to
|
|
6
|
+
* `${apiUrl}/api/v1/connect/device` so the ish web app can render a live
|
|
7
|
+
* native-readiness panel. This is the native analog of how `ish connect
|
|
8
|
+
* <port>` registers a cloudflare tunnel: a side-channel that tells the
|
|
9
|
+
* backend "this developer's machine is (or isn't) ready to drive a native
|
|
10
|
+
* iOS/Android simulation right now."
|
|
11
|
+
*
|
|
12
|
+
* Contract (must match the backend exactly):
|
|
13
|
+
*
|
|
14
|
+
* POST /api/v1/connect/device
|
|
15
|
+
* {
|
|
16
|
+
* "platform": "ios" | "android",
|
|
17
|
+
* "checks": [ {key,name,group,status,message?,fix?}, ... ],
|
|
18
|
+
* "overall": "pass" | "warn" | "fail" | "skip",
|
|
19
|
+
* "cli_version": "<package.json version>"
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* This is COMPLETELY best-effort. It never throws, never blocks the command,
|
|
23
|
+
* never writes to stdout, and silently returns when the user isn't logged in
|
|
24
|
+
* or is offline. The CLI's normal output and exit code are unaffected. The
|
|
25
|
+
* only side-channel is an optional stderr line under a `debug` flag, mirroring
|
|
26
|
+
* how connect.ts logs its own warnings.
|
|
27
|
+
*/
|
|
28
|
+
import { resolveApiUrl, resolveToken } from "./auth.js";
|
|
29
|
+
import { ApiClient } from "./api-client.js";
|
|
30
|
+
import pkg from "../../package.json" with { type: "json" };
|
|
31
|
+
/** Backend endpoint (relative to the `/api/v1` base ApiClient prepends). */
|
|
32
|
+
const DEVICE_ENDPOINT = "/connect/device";
|
|
33
|
+
/**
|
|
34
|
+
* POST the platform-scoped readiness checks to the backend. Best-effort:
|
|
35
|
+
* resolves auth + posts inside a single try/catch, swallowing every error.
|
|
36
|
+
*
|
|
37
|
+
* @param platform The native platform the checks were scoped to.
|
|
38
|
+
* @param checks The exact `runChecks()` array (already scoped to `platform`).
|
|
39
|
+
* @param overall The aggregate status (`overall(checks)`).
|
|
40
|
+
* @param globals Resolved CLI globals — used only for `--api-url` / `--dev`
|
|
41
|
+
* / `--token` resolution. Pass the command's `globals`.
|
|
42
|
+
* @param opts.debug When true, log a one-line failure note to stderr (off by
|
|
43
|
+
* default so the post is silent). Mirrors connect.ts.
|
|
44
|
+
*/
|
|
45
|
+
export async function reportReadiness(platform, checks, overall, globals, opts = {}) {
|
|
46
|
+
try {
|
|
47
|
+
const apiUrl = resolveApiUrl(globals.apiUrl, globals.dev);
|
|
48
|
+
// resolveToken throws when the user isn't logged in (or a network blip
|
|
49
|
+
// prevents an expired-token refresh) — that throw is caught below and we
|
|
50
|
+
// silently return, exactly as required for the offline / logged-out case.
|
|
51
|
+
const token = await resolveToken(globals.token, apiUrl, globals.tokenFile);
|
|
52
|
+
const client = new ApiClient({ apiUrl, token });
|
|
53
|
+
await client.post(DEVICE_ENDPOINT, {
|
|
54
|
+
platform,
|
|
55
|
+
checks,
|
|
56
|
+
overall,
|
|
57
|
+
cli_version: pkg.version,
|
|
58
|
+
},
|
|
59
|
+
// Tight timeout: `ish check` awaits this so the beacon lands before the
|
|
60
|
+
// process exits, but the panel POST must never stall the command. 2.5s
|
|
61
|
+
// is plenty on a healthy backend and bounds the worst case if the host
|
|
62
|
+
// is unreachable or hung.
|
|
63
|
+
{ timeout: 2_500 });
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
// Swallow everything. A logged-out user, an offline machine, an old
|
|
67
|
+
// backend without the endpoint, a 5xx — none of it should ever surface
|
|
68
|
+
// to the user or change the command's behavior.
|
|
69
|
+
if (opts.debug) {
|
|
70
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
71
|
+
console.error(`(readiness report skipped: ${reason})`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
@@ -230,6 +230,8 @@ To hand a study to someone **without an ish account** — a prospect, a stakehol
|
|
|
230
230
|
- **\`ish person create\` accepts inline flags** (mirrors \`person update\`): the file-only API (\`--file <path>\`) is preserved as an escape hatch but the common path is \`ish person create --name "X" --type ai --country US ...\` — \`--type\` defaults to \`ai\` when \`--file\` is omitted. See \`ish person create --help\` for the full inline-flag set including \`--household\` (MECE rule applies) and \`--accessibility-profile\`.
|
|
231
231
|
- **\`ish status\` now surfaces \`chat_endpoint\`** alongside \`workspace\`/\`study\`/\`ask\`. Stale or orphan active refs get a \`warning\` + \`hint\` field on the affected ref (instead of silently dropping the \`name\`). On \`workspace use <other>\`, the CLI cascade-clears \`study\`/\`ask\`/\`chat_endpoint\` (they belong to the previous workspace).
|
|
232
232
|
- **Share link URL host ≠ API host**: \`ish study share\` prints the backend-built \`share_url\` (the web frontend host). Use it verbatim — never reconstruct the URL from the API host or app URL; they differ. \`ish study unshare\` takes the **raw token** (from \`study share\` / \`study share --list\`), not a study id or alias.
|
|
233
|
+
- **Native app iterations (ios/android) name the app, not a URL**: \`ish iteration create --platform ios --app <bundle-id>\` stores the target as \`app_artifact\` (no URL). The iteration remembers it, so \`ish study run --local\` needs **no \`--app\` on reruns** (it defaults from the iteration). Pass \`--app <path-to.app|.apk>\` only to override with a fresh local build. \`--app\` is optional at create time (omit it for "chosen at run time"). Only \`browser\`/\`figma\` iterations require \`--url\`.
|
|
234
|
+
- **Local runs have no server-side screenshots**: \`ish study run --local\` (including ios/android) writes a per-step HTML debug report to \`~/.ish/debug/sim-*.html\` (path printed at the end of the run) instead of pushing screenshots to the server. \`ish study screenshots list\` on a local-only study finds none — open the debug report instead.
|
|
233
235
|
|
|
234
236
|
## When in doubt
|
|
235
237
|
|