@wix/himalaya-cli 0.1619.0 → 0.1621.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/cli.mjs
CHANGED
|
@@ -208648,13 +208648,19 @@ import { copyFileSync as copyFileSync3, mkdtempSync as mkdtempSync12, readFileSy
|
|
|
208648
208648
|
import { tmpdir as tmpdir13 } from "node:os";
|
|
208649
208649
|
import { basename as basename29, dirname as dirname66, join as join131, resolve as resolve93 } from "node:path";
|
|
208650
208650
|
import { fileURLToPath as fileURLToPath39 } from "node:url";
|
|
208651
|
-
function parseVerdict(value2) {
|
|
208651
|
+
function parseVerdict(value2, captured) {
|
|
208652
208652
|
const v2 = value2;
|
|
208653
208653
|
if (!v2 || !VERDICTS.has(v2.design) || !VERDICTS.has(v2.parity) || !CONFIDENCE.has(v2.confidence) || !Array.isArray(v2.findings)) return null;
|
|
208654
208654
|
for (const f2 of v2.findings) {
|
|
208655
|
-
if (!f2 || !PANES.has(f2.surface) || typeof f2.region !== "string" || typeof f2.what !== "string") return null;
|
|
208655
|
+
if (!f2 || !PANES.has(f2.surface) || !KINDS2.has(f2.kind) || typeof f2.region !== "string" || typeof f2.what !== "string") return null;
|
|
208656
208656
|
}
|
|
208657
|
-
|
|
208657
|
+
const findings = captured ? v2.findings.filter((f2) => captured.has(f2.surface)) : v2.findings;
|
|
208658
|
+
const blank = (panes) => findings.some((f2) => f2.kind === "blank" && panes.includes(f2.surface));
|
|
208659
|
+
const onlySkipped = (panes) => findings.length < v2.findings.length && v2.findings.some((f2) => panes.includes(f2.surface)) && !findings.some((f2) => panes.includes(f2.surface));
|
|
208660
|
+
const own6 = (said, panes) => (said === "partial" || said === "mismatch") && onlySkipped(panes) ? "match" : said;
|
|
208661
|
+
const design = blank(["web"]) ? worse(v2.design, "mismatch") : own6(v2.design, ["web"]);
|
|
208662
|
+
const parity = blank(["ios", "android"]) ? worse(v2.parity, "mismatch") : own6(v2.parity, ["ios", "android"]);
|
|
208663
|
+
return { verdict: worse(design, parity), design, parity, findings, confidence: v2.confidence };
|
|
208658
208664
|
}
|
|
208659
208665
|
function judgeArgs(schema2) {
|
|
208660
208666
|
return [
|
|
@@ -208710,14 +208716,14 @@ async function judgeStrip(strip3, screen, variant, opts = {}) {
|
|
|
208710
208716
|
} catch {
|
|
208711
208717
|
return cannotTell("the judge's answer was not JSON");
|
|
208712
208718
|
}
|
|
208713
|
-
return parseVerdict(parsed) ?? cannotTell("the judge's answer did not match tools/judges/fidelity/verdict.schema.json");
|
|
208719
|
+
return parseVerdict(parsed, opts.captured) ?? cannotTell("the judge's answer did not match tools/judges/fidelity/verdict.schema.json");
|
|
208714
208720
|
}
|
|
208715
|
-
var SOURCE_JUDGE_DIR, JUDGE_MODEL, spawnProcess, cannotTell, SEVERITY, worse, VERDICTS, CONFIDENCE, PANES;
|
|
208721
|
+
var SOURCE_JUDGE_DIR, JUDGE_MODEL, spawnProcess, cannotTell, SEVERITY, worse, VERDICTS, CONFIDENCE, PANES, KINDS2;
|
|
208716
208722
|
var init_judge = __esm({
|
|
208717
208723
|
"src/fidelity/judge.ts"() {
|
|
208718
208724
|
init_define_HHP_CATALOG();
|
|
208719
208725
|
SOURCE_JUDGE_DIR = resolve93(dirname66(fileURLToPath39(import.meta.url)), "../../../judges/fidelity");
|
|
208720
|
-
JUDGE_MODEL = "sonnet";
|
|
208726
|
+
JUDGE_MODEL = "claude-sonnet-5-5";
|
|
208721
208727
|
spawnProcess = (cmd, args, { cwd, input, timeoutMs }) => new Promise((done) => {
|
|
208722
208728
|
let stdout = "", stderr = "", timedOut = false;
|
|
208723
208729
|
const child = spawn7(cmd, args, { cwd, stdio: ["pipe", "pipe", "pipe"] });
|
|
@@ -208745,6 +208751,7 @@ var init_judge = __esm({
|
|
|
208745
208751
|
VERDICTS = /* @__PURE__ */ new Set(["match", "partial", "mismatch", "cannot_tell"]);
|
|
208746
208752
|
CONFIDENCE = /* @__PURE__ */ new Set(["low", "medium", "high"]);
|
|
208747
208753
|
PANES = /* @__PURE__ */ new Set(["web", "ios", "android"]);
|
|
208754
|
+
KINDS2 = /* @__PURE__ */ new Set(["blank", "missing", "colour", "clipping", "order", "type_scale"]);
|
|
208748
208755
|
}
|
|
208749
208756
|
});
|
|
208750
208757
|
|
|
@@ -209005,8 +209012,8 @@ async function runFidelity(root, opts = {}) {
|
|
|
209005
209012
|
verdict = skippedJudge(!w2?.file ? "no web render, and web is the reference for both design and parity" : frames.some((f2) => f2.noReference) ? `the mockup does not style ${variant}, and there is no native pane to compare with web` : "fewer than two captured panes, so there is nothing to compare");
|
|
209006
209013
|
} else {
|
|
209007
209014
|
stripFile = await strip3(judged2, stripPath2);
|
|
209008
|
-
verdict = await judge(stripFile, screen, variant, opts.judgeDir ? { judgeDir: opts.judgeDir } : {});
|
|
209009
209015
|
const captured = new Set(frames.filter((f2) => f2.file).map((f2) => f2.surface));
|
|
209016
|
+
verdict = await judge(stripFile, screen, variant, { captured, ...opts.judgeDir ? { judgeDir: opts.judgeDir } : {} });
|
|
209010
209017
|
const parity = captured.has("ios") || captured.has("android") ? verdict.parity : "cannot_tell";
|
|
209011
209018
|
verdict = { ...verdict, parity, verdict: worse(verdict.design, parity), findings: verdict.findings.filter((f2) => captured.has(f2.surface)) };
|
|
209012
209019
|
}
|
|
@@ -209067,18 +209074,25 @@ var init_run2 = __esm({
|
|
|
209067
209074
|
// src/fidelity/native.ts
|
|
209068
209075
|
var native_exports = {};
|
|
209069
209076
|
__export(native_exports, {
|
|
209077
|
+
BLANK_CONTRAST: () => BLANK_CONTRAST,
|
|
209070
209078
|
defaultRunner: () => defaultRunner3,
|
|
209071
209079
|
deviceFor: () => deviceFor,
|
|
209072
209080
|
nativeCapture: () => nativeCapture,
|
|
209073
209081
|
parseAppLocales: () => parseAppLocales,
|
|
209074
209082
|
parseNightMode: () => parseNightMode,
|
|
209075
209083
|
parsePlistArray: () => parsePlistArray,
|
|
209084
|
+
resumedPackages: () => resumedPackages,
|
|
209076
209085
|
sameLocalOrigin: () => sameLocalOrigin,
|
|
209077
209086
|
screenLink: () => screenLink
|
|
209078
209087
|
});
|
|
209079
209088
|
import { spawn as spawn9 } from "node:child_process";
|
|
209080
209089
|
import { readFileSync as readFileSync143, writeFileSync as writeFileSync60 } from "node:fs";
|
|
209081
209090
|
import { join as join135 } from "node:path";
|
|
209091
|
+
function resumedPackages(dumpsys) {
|
|
209092
|
+
const lines = dumpsys.split("\n").filter((l2) => /\b(?:topResumedActivity|mResumedActivity|ResumedActivity)\b/.test(l2));
|
|
209093
|
+
if (!lines.length) return void 0;
|
|
209094
|
+
return lines.flatMap((l2) => [...l2.matchAll(/\s([A-Za-z0-9_.]+)\/[A-Za-z0-9_.$]+/g)].map((m2) => m2[1]));
|
|
209095
|
+
}
|
|
209082
209096
|
function deviceFor(devices, platform, variant) {
|
|
209083
209097
|
if (variant === "tablet") return platform === "ios" ? devices.iosTablet : devices.androidTablet;
|
|
209084
209098
|
return platform === "ios" ? devices.ios : devices.android;
|
|
@@ -209100,12 +209114,18 @@ function nativeCapture(devices, opts) {
|
|
|
209100
209114
|
const maxWaitMs = opts.maxWaitMs ?? 3e4;
|
|
209101
209115
|
const iosBundle = opts.iosBundleId ?? "dev.himalaya.player.dev";
|
|
209102
209116
|
const androidPackage = opts.androidPackage ?? "dev.himalaya.player.dev";
|
|
209117
|
+
const packages = /* @__PURE__ */ new Map();
|
|
209103
209118
|
const rtl = opts.rtlLanguage ?? "he";
|
|
209104
209119
|
const picker = opts.pickerAppId ?? "himi-player";
|
|
209105
209120
|
const blackFraction = opts.blackFraction ?? (async (file2) => {
|
|
209106
209121
|
const r3 = await run2("magick", [file2, "-colorspace", "Gray", "-threshold", "5%", "-format", "%[fx:1-mean]", "info:"]);
|
|
209107
209122
|
return r3.code === 0 ? Number(r3.stdout.toString().trim()) || 0 : 0;
|
|
209108
209123
|
});
|
|
209124
|
+
const contrast = opts.contrast ?? (async (file2) => {
|
|
209125
|
+
const r3 = await run2("magick", [file2, "-gravity", "South", "-crop", "100%x90%+0+0", "+repage", "-colorspace", "Gray", "-format", "%[fx:standard_deviation]", "info:"]);
|
|
209126
|
+
const v2 = r3.code === 0 ? Number.parseFloat(r3.stdout.toString()) : NaN;
|
|
209127
|
+
return Number.isFinite(v2) ? v2 : 1;
|
|
209128
|
+
});
|
|
209109
209129
|
const saved = /* @__PURE__ */ new Map();
|
|
209110
209130
|
const applied = /* @__PURE__ */ new Map();
|
|
209111
209131
|
const originChecked = /* @__PURE__ */ new Map();
|
|
@@ -209114,6 +209134,18 @@ function nativeCapture(devices, opts) {
|
|
|
209114
209134
|
if (r3.code !== 0) throw new Error(`${cmd} ${args.join(" ")} exited ${r3.code}`);
|
|
209115
209135
|
return r3.stdout.toString().trim();
|
|
209116
209136
|
};
|
|
209137
|
+
async function playerPackage(id) {
|
|
209138
|
+
const known = packages.get(id);
|
|
209139
|
+
if (known) return known;
|
|
209140
|
+
let pkg = androidPackage;
|
|
209141
|
+
if (!opts.androidPackage) {
|
|
209142
|
+
const r3 = await run2("adb", ["-s", id, "shell", "pm", "list", "packages", androidPackage]);
|
|
209143
|
+
const listed = r3.code === 0 ? r3.stdout.toString().split("\n").map((l2) => l2.trim().replace(/^package:/, "")).filter(Boolean) : [];
|
|
209144
|
+
pkg = listed.includes(androidPackage) ? androidPackage : listed.find((l2) => l2.startsWith(`${androidPackage}.`)) ?? androidPackage;
|
|
209145
|
+
}
|
|
209146
|
+
packages.set(id, pkg);
|
|
209147
|
+
return pkg;
|
|
209148
|
+
}
|
|
209117
209149
|
async function remember(platform, id) {
|
|
209118
209150
|
const key3 = `${platform}:${id}`;
|
|
209119
209151
|
if (saved.has(key3)) return;
|
|
@@ -209123,7 +209155,7 @@ function nativeCapture(devices, opts) {
|
|
|
209123
209155
|
saved.set(key3, { appearance, language: languages.code === 0 ? languages.stdout.toString().trim() : void 0 });
|
|
209124
209156
|
} else {
|
|
209125
209157
|
const night = await ok3("adb", ["-s", id, "shell", "cmd", "uimode", "night"]);
|
|
209126
|
-
const locales = await run2("adb", ["-s", id, "shell", "cmd", "locale", "get-app-locales",
|
|
209158
|
+
const locales = await run2("adb", ["-s", id, "shell", "cmd", "locale", "get-app-locales", await playerPackage(id)]);
|
|
209127
209159
|
saved.set(key3, { appearance: parseNightMode(night), language: locales.code === 0 ? parseAppLocales(locales.stdout.toString()) : void 0 });
|
|
209128
209160
|
}
|
|
209129
209161
|
}
|
|
@@ -209140,7 +209172,7 @@ function nativeCapture(devices, opts) {
|
|
|
209140
209172
|
} else {
|
|
209141
209173
|
await ok3("adb", ["-s", id, "shell", "cmd", "uimode", "night", dark ? "yes" : "no"]);
|
|
209142
209174
|
const wasRtl = applied.get(key3) === "rtl";
|
|
209143
|
-
if (right || wasRtl) await ok3("adb", ["-s", id, "shell", "cmd", "locale", "set-app-locales",
|
|
209175
|
+
if (right || wasRtl) await ok3("adb", ["-s", id, "shell", "cmd", "locale", "set-app-locales", await playerPackage(id), "--locales", right ? rtl : ""]);
|
|
209144
209176
|
}
|
|
209145
209177
|
applied.set(key3, want);
|
|
209146
209178
|
}
|
|
@@ -209169,19 +209201,31 @@ function nativeCapture(devices, opts) {
|
|
|
209169
209201
|
writeFileSync60(file2, r3.stdout);
|
|
209170
209202
|
return r3.stdout;
|
|
209171
209203
|
}
|
|
209204
|
+
async function notInFront(platform, id) {
|
|
209205
|
+
if (platform !== "android") return "";
|
|
209206
|
+
const r3 = await run2("adb", ["-s", id, "shell", "dumpsys", "activity", "activities"]);
|
|
209207
|
+
const resumed = r3.code === 0 ? resumedPackages(r3.stdout.toString()) : void 0;
|
|
209208
|
+
const player = await playerPackage(id);
|
|
209209
|
+
return !resumed || resumed.includes(player) ? "" : `the Player was not in front (${resumed.join(", ") || "nothing resumed"})`;
|
|
209210
|
+
}
|
|
209172
209211
|
async function settle2(platform, id, file2, notFrame) {
|
|
209173
209212
|
let previous;
|
|
209213
|
+
let why = "the screen did not stop changing";
|
|
209174
209214
|
for (let waited = 0; ; waited += settleMs) {
|
|
209175
209215
|
const frame = await screenshot(platform, id, file2);
|
|
209176
|
-
if (previous && frame.equals(previous)
|
|
209177
|
-
|
|
209216
|
+
if (previous && frame.equals(previous)) {
|
|
209217
|
+
const front = await notInFront(platform, id);
|
|
209218
|
+
why = notFrame && frame.equals(notFrame) ? "the Player still showed the picker" : front ? front : await contrast(file2) < BLANK_CONTRAST ? "the screen was still blank or loading" : "";
|
|
209219
|
+
if (!why) return { frame };
|
|
209220
|
+
} else why = "the screen did not stop changing";
|
|
209221
|
+
if (waited >= maxWaitMs) return { frame, unsettled: `${why} within ${Math.round(maxWaitMs / 1e3)}s` };
|
|
209178
209222
|
previous = frame;
|
|
209179
209223
|
await sleep(settleMs);
|
|
209180
209224
|
}
|
|
209181
209225
|
}
|
|
209182
209226
|
async function open(platform, id, link) {
|
|
209183
209227
|
if (platform === "ios") await ok3("xcrun", ["simctl", "openurl", id, link]);
|
|
209184
|
-
else await ok3("adb", ["-s", id, "shell", "am", "start", "-W", "-a", "android.intent.action.VIEW", "-d", `'${link.replace(/'/g, "%27")}'`]);
|
|
209228
|
+
else await ok3("adb", ["-s", id, "shell", "am", "start", "-W", "-a", "android.intent.action.VIEW", "-p", await playerPackage(id), "-d", `'${link.replace(/'/g, "%27")}'`]);
|
|
209185
209229
|
}
|
|
209186
209230
|
const capture = (async (screen, entry, variant, outDir) => {
|
|
209187
209231
|
const frames = [];
|
|
@@ -209205,11 +209249,19 @@ function nativeCapture(devices, opts) {
|
|
|
209205
209249
|
await apply(platform, id, variant);
|
|
209206
209250
|
await open(platform, id, `himi://app/${encodeURIComponent(picker)}`);
|
|
209207
209251
|
const file2 = join135(outDir, `${screen}.${variant}.${platform}.png`);
|
|
209208
|
-
const pickerFrame = await settle2(platform, id, file2);
|
|
209252
|
+
const pickerFrame = (await settle2(platform, id, file2)).frame;
|
|
209209
209253
|
await open(platform, id, screenLink(opts.appId, screen, entry, opts.release));
|
|
209210
|
-
await settle2(platform, id, file2, pickerFrame);
|
|
209254
|
+
const { unsettled } = await settle2(platform, id, file2, pickerFrame);
|
|
209255
|
+
if (unsettled) {
|
|
209256
|
+
frames.push({ surface: platform, screen, variant, skipped: { reason: `capture_unsettled: ${unsettled}` } });
|
|
209257
|
+
continue;
|
|
209258
|
+
}
|
|
209211
209259
|
if (platform === "android" && await blackFraction(file2) > 0.95) {
|
|
209212
|
-
await settle2(platform, id, file2);
|
|
209260
|
+
const retry = await settle2(platform, id, file2, pickerFrame);
|
|
209261
|
+
if (retry.unsettled) {
|
|
209262
|
+
frames.push({ surface: platform, screen, variant, skipped: { reason: `capture_unsettled: ${retry.unsettled}` } });
|
|
209263
|
+
continue;
|
|
209264
|
+
}
|
|
209213
209265
|
if (await blackFraction(file2) > 0.95) {
|
|
209214
209266
|
frames.push({ surface: platform, screen, variant, skipped: { reason: "black frame" } });
|
|
209215
209267
|
continue;
|
|
@@ -209233,7 +209285,7 @@ function nativeCapture(devices, opts) {
|
|
|
209233
209285
|
await run2("xcrun", ["simctl", "terminate", id, iosBundle]);
|
|
209234
209286
|
} else {
|
|
209235
209287
|
if (was.appearance) await run2("adb", ["-s", id, "shell", "cmd", "uimode", "night", was.appearance]);
|
|
209236
|
-
await run2("adb", ["-s", id, "shell", "cmd", "locale", "set-app-locales", androidPackage, "--locales", was.language ?? ""]);
|
|
209288
|
+
await run2("adb", ["-s", id, "shell", "cmd", "locale", "set-app-locales", packages.get(id) ?? androidPackage, "--locales", was.language ?? ""]);
|
|
209237
209289
|
}
|
|
209238
209290
|
} catch {
|
|
209239
209291
|
}
|
|
@@ -209265,10 +209317,11 @@ function parseAppLocales(out2) {
|
|
|
209265
209317
|
function parsePlistArray(out2) {
|
|
209266
209318
|
return out2.replace(/^\s*\(|\)\s*$/g, "").split(",").map((s2) => s2.trim().replace(/^"|"$/g, "")).filter(Boolean);
|
|
209267
209319
|
}
|
|
209268
|
-
var defaultRunner3;
|
|
209320
|
+
var BLANK_CONTRAST, defaultRunner3;
|
|
209269
209321
|
var init_native = __esm({
|
|
209270
209322
|
"src/fidelity/native.ts"() {
|
|
209271
209323
|
init_define_HHP_CATALOG();
|
|
209324
|
+
BLANK_CONTRAST = 0.03;
|
|
209272
209325
|
defaultRunner3 = (cmd, args) => new Promise((resolve95) => {
|
|
209273
209326
|
const child = spawn9(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
|
|
209274
209327
|
const chunks = [];
|
|
@@ -215542,16 +215595,16 @@ WHAT A PATCH DOES NOT CARRY \u2014 all reported in the output, never silent
|
|
|
215542
215595
|
async function patchCommand(ctx) {
|
|
215543
215596
|
const { io: io2, pos, flags, client: client2, runReleasePatch, patchBlastRadius } = ctx;
|
|
215544
215597
|
const PATCH_USAGE = "himi patch screen <id> --file <d.json> | bundle --file <b.js> (--for-screen <id>[,<id>]|--no-rebind) | tokens --file <t.json> | app-config --file <c.json> ; --app <a> (--draft|--live) [--from-release <id>] [--accept-regressions] [--notes <t>]";
|
|
215545
|
-
const
|
|
215598
|
+
const KINDS3 = ["screen", "bundle", "tokens", "app-config"];
|
|
215546
215599
|
if (flags.help === true) {
|
|
215547
215600
|
io2.log(PATCH_HELP);
|
|
215548
215601
|
return 0;
|
|
215549
215602
|
}
|
|
215550
215603
|
const kind = pos[1];
|
|
215551
|
-
if (!kind || !
|
|
215604
|
+
if (!kind || !KINDS3.includes(kind)) {
|
|
215552
215605
|
io2.error(JSON.stringify({
|
|
215553
215606
|
error: kind ? `unknown patch kind "${kind}"` : "patch needs a <kind>",
|
|
215554
|
-
kinds:
|
|
215607
|
+
kinds: KINDS3,
|
|
215555
215608
|
usage: PATCH_USAGE
|
|
215556
215609
|
}));
|
|
215557
215610
|
return 2;
|
package/guides/hhp.md
CHANGED
|
@@ -218,7 +218,7 @@ himi fidelity --content <dir> [--screen a,b] [--variant light,dark,rtl,tablet] [
|
|
|
218
218
|
For each screen and variant (`light`, `dark`, `rtl`, `tablet`), the command does four things:
|
|
219
219
|
|
|
220
220
|
1. **Captures the mockup.** It draws the page in Chromium at the variant's width. Dark mode is drawn by emulating `prefers-color-scheme`. RTL is drawn by setting `dir=rtl` on the page.
|
|
221
|
-
2. **Captures the web render** from the app's recorded data, as `himi render` does by default. Native frames come from Himi Player on the devices you pass: `--ios <udid>` and `--android <serial>`, plus `--ios-tablet` and `--android-tablet` for the tablet variant. Only those devices are touched. For each variant the command sets dark or light appearance and, for `rtl`, the app's right-to-left language (iOS `AppleLanguages`, Android per-app locale, API 33+). It opens `himi://app/<app>/screen/<screen>` and captures once two frames match. Every setting it changed is restored afterwards, also on Ctrl-C. A variant with no device is reported `skipped`, never passed. Without `--release <id>` or `--serve <port>` the Player shows the app's **live** release, not your working copy. `--serve <port>` serves the working copy on `127.0.0.1:<port>` as `himi run` does, so native and web render the same release. The Player reads a local origin only from a Debug build that compiled it in (see [View your package on a device](himi-player-implementation.md#view-your-package-on-a-device)): build it with `HIMALAYA_API_BASE_URL_INFO=http://127.0.0.1:<port>` on iOS and `-PhimalayaDevServeBaseUrl=http://10.0.2.2:<port>` on Android. An iOS device whose Player reads another origin is skipped with the rebuild to run. Android's origin cannot be read back from the installed app, so check that build yourself. A screen that declares `params` is skipped natively for now, because the Player's deep link does not carry them yet. On a fresh iOS simulator, open one `himi://` link by hand first and accept iOS's "Open in Himi Player?" prompt: the command cannot answer it, and until it is accepted every iOS capture shows the prompt.
|
|
221
|
+
2. **Captures the web render** from the app's recorded data, as `himi render` does by default. Native frames come from Himi Player on the devices you pass: `--ios <udid>` and `--android <serial>`, plus `--ios-tablet` and `--android-tablet` for the tablet variant. Only those devices are touched. For each variant the command sets dark or light appearance and, for `rtl`, the app's right-to-left language (iOS `AppleLanguages`, Android per-app locale, API 33+). It opens `himi://app/<app>/screen/<screen>` and captures once two frames match, the frame is not a blank or loading page, and, on Android, the Player is the app in front rather than the launcher. A screen that has not settled that way after 30 seconds is reported `skipped` as `capture_unsettled`, never judged. Every setting it changed is restored afterwards, also on Ctrl-C. A variant with no device is reported `skipped`, never passed. Without `--release <id>` or `--serve <port>` the Player shows the app's **live** release, not your working copy. `--serve <port>` serves the working copy on `127.0.0.1:<port>` as `himi run` does, so native and web render the same release. The Player reads a local origin only from a Debug build that compiled it in (see [View your package on a device](himi-player-implementation.md#view-your-package-on-a-device)): build it with `HIMALAYA_API_BASE_URL_INFO=http://127.0.0.1:<port>` on iOS and `-PhimalayaDevServeBaseUrl=http://10.0.2.2:<port>` on Android. An iOS device whose Player reads another origin is skipped with the rebuild to run. Android's origin cannot be read back from the installed app, so check that build yourself. A screen that declares `params` is skipped natively for now, because the Player's deep link does not carry them yet. On a fresh iOS simulator, open one `himi://` link by hand first and accept iOS's "Open in Himi Player?" prompt: the command cannot answer it, and until it is accepted every iOS capture shows the prompt.
|
|
222
222
|
3. **Scores mockup↔web** with ImageMagick SSIM, globally and per tile. The worst tile says where the screens differ most.
|
|
223
223
|
4. **Asks a fresh model** two questions, using the repo's fidelity judge prompt. The model never ran in the session that wrote the app.
|
|
224
224
|
- **Design:** does WEB look like the MOCKUP? This judges the port.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1621.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
|
|
6
6
|
"keywords": [
|
|
@@ -72,5 +72,5 @@
|
|
|
72
72
|
"artifactId": "himalaya-cli"
|
|
73
73
|
}
|
|
74
74
|
},
|
|
75
|
-
"falconPackageHash": "
|
|
75
|
+
"falconPackageHash": "c21c6aa4541e5ed5dda0d5f14934ad131349b6f6f125a9d5ff58315a"
|
|
76
76
|
}
|
|
@@ -652,6 +652,20 @@ const register = withIapPresentation<S>({
|
|
|
652
652
|
},
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
+
/** A read that answers at once (a cache, a local fixture, a headless run whose transport never
|
|
656
|
+
* suspends) is over before the action that started it returns. Its `readChanged` is a dispatch
|
|
657
|
+
* of this same service from a detached job, which a headless run never services after the active
|
|
658
|
+
* action returns, and which on a device would commit under the skeleton the action is about to
|
|
659
|
+
* return. Wait a bounded number of turns and fold the settled status into the action's own
|
|
660
|
+
* result instead; a read that is still in flight keeps the skeleton and lands through
|
|
661
|
+
* `readChanged` as before. `undefined` means a newer action took over while this one waited. */
|
|
662
|
+
async function settledWithin(next: S, category: string, gen: number): Promise<S | undefined> {
|
|
663
|
+
const controller = reader(category);
|
|
664
|
+
for (let i = 0; i < 500 && controller.getStatus().refreshing; i++) await Promise.resolve();
|
|
665
|
+
if (gen !== generation) return undefined;
|
|
666
|
+
return controller.getStatus().refreshing ? next : currentReadState(next);
|
|
667
|
+
}
|
|
668
|
+
|
|
655
669
|
type A = (s: S, arg?: unknown) => S | undefined | Promise<S | undefined>;
|
|
656
670
|
const svc = register<S, Record<string, A>>("himiBrandedShop", {
|
|
657
671
|
state: BOOT,
|
|
@@ -659,8 +673,12 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
|
|
|
659
673
|
actions: {
|
|
660
674
|
load: (s, arg) => load(s, arg),
|
|
661
675
|
// Pull to refresh: the categories and the scope again, from Wix.
|
|
662
|
-
refresh: (s, arg) =>
|
|
663
|
-
|
|
676
|
+
refresh: async (s, arg) => {
|
|
677
|
+
const next = await load(s, arg, true);
|
|
678
|
+
// The result owns its scope; another action may have changed the globals during load.
|
|
679
|
+
return next ? settledWithin(next, next.categorySelected, next.scopeGen) : undefined;
|
|
680
|
+
},
|
|
681
|
+
retry: (s) => settledWithin(showScope(s, s.categorySelected, true), s.categorySelected, generation),
|
|
664
682
|
recover: async () => { await fetchScope(selectedCategory, generation, true); return undefined; },
|
|
665
683
|
readChanged: (s, arg) => {
|
|
666
684
|
if (!arg || typeof arg !== 'object') return undefined;
|
|
@@ -683,7 +701,7 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
|
|
|
683
701
|
// A background page landed for the current scope.
|
|
684
702
|
scopeGrew: (s) => derive(s),
|
|
685
703
|
// Single-select: one chip is always chosen.
|
|
686
|
-
selectCategory: (s, arg) => {
|
|
704
|
+
selectCategory: async (s, arg) => {
|
|
687
705
|
// The chip row sends { selectedIds }; a deselected chip falls back to All.
|
|
688
706
|
const selection = arg as { selectedIds?: unknown; refresh?: boolean } | undefined;
|
|
689
707
|
const id = selection?.selectedIds !== undefined ? selected(arg)[0] ?? ALL : idOf(arg);
|
|
@@ -691,12 +709,15 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
|
|
|
691
709
|
const cancelsPendingRoute = !!pendingLink || selectedCategory !== id;
|
|
692
710
|
categoryIntentVersion++;
|
|
693
711
|
pendingLink = undefined;
|
|
694
|
-
|
|
712
|
+
// undefined, not s: s is the snapshot from before any in-flight load, loading flag included.
|
|
713
|
+
if (id !== ALL && !categories.some((c) => c.id === id)) return undefined;
|
|
695
714
|
// showScope compares the prior selection before committing the requested category.
|
|
696
715
|
// categorySelected is committed with the tap (showScope), so a tap back to the chip
|
|
697
716
|
// on screen while another category loads is a no-op on the right scope.
|
|
698
|
-
if (id === s.categorySelected) return cancelsPendingRoute || refresh ? showScope(s, id, refresh) :
|
|
699
|
-
const
|
|
717
|
+
if (id === s.categorySelected) return cancelsPendingRoute || refresh ? settledWithin(showScope(s, id, refresh), id, generation) : undefined;
|
|
718
|
+
const shown = showScope(s, id, refresh);
|
|
719
|
+
const next = await settledWithin(shown, id, generation);
|
|
720
|
+
if (!next) return undefined;
|
|
700
721
|
const pending = pendingAuxiliary;
|
|
701
722
|
if (pending && pending.version === auxiliaryVersion && pending.cartCount >= 0) {
|
|
702
723
|
pendingAuxiliary = undefined;
|
|
@@ -652,6 +652,20 @@ const register = withIapPresentation<S>({
|
|
|
652
652
|
},
|
|
653
653
|
});
|
|
654
654
|
|
|
655
|
+
/** A read that answers at once (a cache, a local fixture, a headless run whose transport never
|
|
656
|
+
* suspends) is over before the action that started it returns. Its `readChanged` is a dispatch
|
|
657
|
+
* of this same service from a detached job, which a headless run never services after the active
|
|
658
|
+
* action returns, and which on a device would commit under the skeleton the action is about to
|
|
659
|
+
* return. Wait a bounded number of turns and fold the settled status into the action's own
|
|
660
|
+
* result instead; a read that is still in flight keeps the skeleton and lands through
|
|
661
|
+
* `readChanged` as before. `undefined` means a newer action took over while this one waited. */
|
|
662
|
+
async function settledWithin(next: S, category: string, gen: number): Promise<S | undefined> {
|
|
663
|
+
const controller = reader(category);
|
|
664
|
+
for (let i = 0; i < 500 && controller.getStatus().refreshing; i++) await Promise.resolve();
|
|
665
|
+
if (gen !== generation) return undefined;
|
|
666
|
+
return controller.getStatus().refreshing ? next : currentReadState(next);
|
|
667
|
+
}
|
|
668
|
+
|
|
655
669
|
type A = (s: S, arg?: unknown) => S | undefined | Promise<S | undefined>;
|
|
656
670
|
const svc = register<S, Record<string, A>>("himiBrandedShop", {
|
|
657
671
|
state: BOOT,
|
|
@@ -659,8 +673,12 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
|
|
|
659
673
|
actions: {
|
|
660
674
|
load: (s, arg) => load(s, arg),
|
|
661
675
|
// Pull to refresh: the categories and the scope again, from Wix.
|
|
662
|
-
refresh: (s, arg) =>
|
|
663
|
-
|
|
676
|
+
refresh: async (s, arg) => {
|
|
677
|
+
const next = await load(s, arg, true);
|
|
678
|
+
// The result owns its scope; another action may have changed the globals during load.
|
|
679
|
+
return next ? settledWithin(next, next.categorySelected, next.scopeGen) : undefined;
|
|
680
|
+
},
|
|
681
|
+
retry: (s) => settledWithin(showScope(s, s.categorySelected, true), s.categorySelected, generation),
|
|
664
682
|
recover: async () => { await fetchScope(selectedCategory, generation, true); return undefined; },
|
|
665
683
|
readChanged: (s, arg) => {
|
|
666
684
|
if (!arg || typeof arg !== 'object') return undefined;
|
|
@@ -683,7 +701,7 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
|
|
|
683
701
|
// A background page landed for the current scope.
|
|
684
702
|
scopeGrew: (s) => derive(s),
|
|
685
703
|
// Single-select: one chip is always chosen.
|
|
686
|
-
selectCategory: (s, arg) => {
|
|
704
|
+
selectCategory: async (s, arg) => {
|
|
687
705
|
// The chip row sends { selectedIds }; a deselected chip falls back to All.
|
|
688
706
|
const selection = arg as { selectedIds?: unknown; refresh?: boolean } | undefined;
|
|
689
707
|
const id = selection?.selectedIds !== undefined ? selected(arg)[0] ?? ALL : idOf(arg);
|
|
@@ -691,12 +709,15 @@ const svc = register<S, Record<string, A>>("himiBrandedShop", {
|
|
|
691
709
|
const cancelsPendingRoute = !!pendingLink || selectedCategory !== id;
|
|
692
710
|
categoryIntentVersion++;
|
|
693
711
|
pendingLink = undefined;
|
|
694
|
-
|
|
712
|
+
// undefined, not s: s is the snapshot from before any in-flight load, loading flag included.
|
|
713
|
+
if (id !== ALL && !categories.some((c) => c.id === id)) return undefined;
|
|
695
714
|
// showScope compares the prior selection before committing the requested category.
|
|
696
715
|
// categorySelected is committed with the tap (showScope), so a tap back to the chip
|
|
697
716
|
// on screen while another category loads is a no-op on the right scope.
|
|
698
|
-
if (id === s.categorySelected) return cancelsPendingRoute || refresh ? showScope(s, id, refresh) :
|
|
699
|
-
const
|
|
717
|
+
if (id === s.categorySelected) return cancelsPendingRoute || refresh ? settledWithin(showScope(s, id, refresh), id, generation) : undefined;
|
|
718
|
+
const shown = showScope(s, id, refresh);
|
|
719
|
+
const next = await settledWithin(shown, id, generation);
|
|
720
|
+
if (!next) return undefined;
|
|
700
721
|
const pending = pendingAuxiliary;
|
|
701
722
|
if (pending && pending.version === auxiliaryVersion && pending.cartCount >= 0) {
|
|
702
723
|
pendingAuxiliary = undefined;
|