@wix/himalaya-cli 0.1603.0 → 0.1604.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.
@@ -624,6 +624,13 @@ export interface PushConfig {
624
624
  * account-level app that is not about any single site. Only the "wix-ping"
625
625
  * registrar reads it. */
626
626
  pingScope?: "site" | "user";
627
+ /** Extra link prefixes stripped before `routes` match (#4709), for an app inheriting a
628
+ * legacy link space: e.g. `["legacyapp://", "https://legacy.example.com/app/"]` turns
629
+ * `legacyapp://widget/42` into the residual `widget/42`. Tried after the "wix-ping"
630
+ * rules; the longest matching prefix wins; scheme and host compare case-insensitively,
631
+ * the path exactly, and a prefix ends at `/`, `?` or `#`. A relative link (`/widget/42`)
632
+ * is a residual without any prefix. */
633
+ linkPrefixes?: string[];
627
634
  /** Token registrar. "generic" ⇒ POST the token to `registerUrl`; "wix-ping"
628
635
  * ⇒ the native Wix Ping registrar. */
629
636
  registrar: PushRegistrar;
@@ -49,8 +49,13 @@ export interface AdaptedLink {
49
49
  * link through unchanged rather than failing, so an older binary served a config
50
50
  * naming an adapter it has never heard of degrades to today's behavior instead of
51
51
  * dropping every notification (invariants 6/8).
52
+ *
53
+ * #4709 — `linkPrefixes` (`PushConfig.linkPrefixes`). In order: the "wix-ping" rules
54
+ * (when that is the adapter); then the LONGEST configured prefix the link starts with
55
+ * is stripped; then a relative link (`/widget/7`) is a residual as is. Anything else
56
+ * passes through unchanged. Only a Wix link yields a `metaSiteId`.
52
57
  */
53
- export declare function adaptPayloadLink(link: string, adapter?: string): AdaptedLink;
58
+ export declare function adaptPayloadLink(link: string, adapter?: string, linkPrefixes?: readonly string[]): AdaptedLink;
54
59
  /**
55
60
  * The himi link the first matching route produces, or null when nothing matches.
56
61
  *
@@ -67,6 +72,7 @@ export declare function matchPushRoute(residual: string, routes: readonly PushRo
67
72
  export declare function resolvePushRoute(link: string, config: {
68
73
  routes?: readonly PushRoute[];
69
74
  payloadAdapter?: string;
75
+ linkPrefixes?: readonly string[];
70
76
  } | undefined, payload?: PushPayloadContext): {
71
77
  link: string | null;
72
78
  params: Record<string, string>;
@@ -45,8 +45,17 @@ function redactFrogParam(value) {
45
45
  return value;
46
46
  }
47
47
  var BARE_ENDPOINT = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
48
+ function onConfigOrigin(name, configEndpoint) {
49
+ if (!configEndpoint || !configEndpoint.includes("://")) return name;
50
+ try {
51
+ const url = new URL(configEndpoint);
52
+ return `${url.protocol}//${url.host}/${name}`;
53
+ } catch {
54
+ return name;
55
+ }
56
+ }
48
57
  function eventEndpoint(eventEndpointValue, configEndpoint) {
49
- return frogEndpointUrl(eventEndpointValue && BARE_ENDPOINT.test(eventEndpointValue) ? eventEndpointValue : configEndpoint);
58
+ return frogEndpointUrl(eventEndpointValue && BARE_ENDPOINT.test(eventEndpointValue) ? onConfigOrigin(eventEndpointValue, configEndpoint) : configEndpoint);
50
59
  }
51
60
  function frogEndpointUrl(endpoint) {
52
61
  const e = (endpoint ?? "").trim() || ONEAPP_DEFAULT_ENDPOINT;
package/dist/cli.mjs CHANGED
@@ -1187,14 +1187,14 @@ var init_start_prompt = __esm({
1187
1187
  // src/source-deny.ts
1188
1188
  import { sep } from "node:path";
1189
1189
  function isDeniedSourcePath(relPath) {
1190
- const segments = relPath.split(/[\\/]/).filter(Boolean);
1191
- if (segments.length === 0) return false;
1192
- const dirs = segments.slice(0, -1);
1190
+ const segments2 = relPath.split(/[\\/]/).filter(Boolean);
1191
+ if (segments2.length === 0) return false;
1192
+ const dirs = segments2.slice(0, -1);
1193
1193
  if (dirs.some((seg2) => DENY_DIRS.has(seg2))) return true;
1194
1194
  if (dirs.length > 0 && DENY_ROOT_DIRS.has(dirs[0])) return true;
1195
- if (segments.length === 1 && segments[0] === "HIMALAYA.md") return true;
1196
- if (MCP_HOST_CONFIG_FILES.includes(segments.join("/"))) return true;
1197
- return DENY_FILES.some((re2) => re2.test(segments[segments.length - 1]));
1195
+ if (segments2.length === 1 && segments2[0] === "HIMALAYA.md") return true;
1196
+ if (MCP_HOST_CONFIG_FILES.includes(segments2.join("/"))) return true;
1197
+ return DENY_FILES.some((re2) => re2.test(segments2[segments2.length - 1]));
1198
1198
  }
1199
1199
  function isDeniedSourceDir(name, parentRel) {
1200
1200
  const base = name.split(sep).pop() ?? name;
@@ -17190,8 +17190,8 @@ function wireKind(value2) {
17190
17190
  }
17191
17191
  function jsonPointer(dotted) {
17192
17192
  if (dotted === "") return "";
17193
- const segments = dotted.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean).map((s2) => s2.replace(/~/g, "~0").replace(/\//g, "~1"));
17194
- return `/${segments.join("/")}`;
17193
+ const segments2 = dotted.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean).map((s2) => s2.replace(/~/g, "~0").replace(/\//g, "~1"));
17194
+ return `/${segments2.join("/")}`;
17195
17195
  }
17196
17196
  function componentPropFindings(descriptor) {
17197
17197
  const out2 = [];
@@ -18126,10 +18126,10 @@ function validateScreenDescriptor(screenId, descriptor, opts = {}) {
18126
18126
  const lbIntegrity = descriptor?.logicBundle?.integrity;
18127
18127
  const schemaIssuesAt = /* @__PURE__ */ new Map();
18128
18128
  const dottedCandidates = (pointer3) => {
18129
- const segments = pointer3.split("/").filter(Boolean).map((s2) => s2.replace(/~1/g, "/").replace(/~0/g, "~"));
18130
- if (segments.filter((s2) => /^\d+$/.test(s2)).length > 3) return [];
18129
+ const segments2 = pointer3.split("/").filter(Boolean).map((s2) => s2.replace(/~1/g, "/").replace(/~0/g, "~"));
18130
+ if (segments2.filter((s2) => /^\d+$/.test(s2)).length > 3) return [];
18131
18131
  let out2 = [""];
18132
- for (const seg2 of segments) {
18132
+ for (const seg2 of segments2) {
18133
18133
  const spellings = /^\d+$/.test(seg2) ? [`[${seg2}]`, `.${seg2}`] : [`.${seg2}`];
18134
18134
  out2 = out2.flatMap((acc) => spellings.map((s2) => acc === "" && s2.startsWith(".") ? s2.slice(1) : acc + s2));
18135
18135
  }
@@ -21785,7 +21785,7 @@ function ensureTemplates({
21785
21785
  root = REPO2,
21786
21786
  env = process.env,
21787
21787
  log = (m2) => console.error(m2),
21788
- spawn: spawn10 = spawnSync3,
21788
+ spawn: spawn11 = spawnSync3,
21789
21789
  generator = GENERATOR,
21790
21790
  waitMs = 10 * 6e4
21791
21791
  } = {}) {
@@ -21801,7 +21801,7 @@ function ensureTemplates({
21801
21801
  if (!refresh) log(`ensure-templates: generating ${TEMPLATES_REL} (gitignored; ~10s) \u2026`);
21802
21802
  rmSync5(stampPath(out2), { force: true });
21803
21803
  const { HIMI_TEMPLATES_OUT: _scratch, ...childEnv } = env;
21804
- const r3 = spawn10(process.execPath, ["--import", "tsx", generator], {
21804
+ const r3 = spawn11(process.execPath, ["--import", "tsx", generator], {
21805
21805
  cwd: root,
21806
21806
  env: childEnv,
21807
21807
  encoding: "utf8",
@@ -21866,7 +21866,7 @@ function ensureBundles({
21866
21866
  root = REPO3,
21867
21867
  env = process.env,
21868
21868
  log = (m2) => console.error(m2),
21869
- spawn: spawn10 = spawnSync4,
21869
+ spawn: spawn11 = spawnSync4,
21870
21870
  waitMs = 10 * 6e4
21871
21871
  } = {}) {
21872
21872
  if (env[BUNDLES_FRESH]) return false;
@@ -21880,7 +21880,7 @@ function ensureBundles({
21880
21880
  if (waited && bundlesReady(root)) return false;
21881
21881
  if (!refresh) log(`ensure-bundles: building ${BUNDLES_REL} (gitignored; a few seconds) \u2026`);
21882
21882
  rmSync6(stampPath(out2), { force: true });
21883
- const r3 = spawn10(process.execPath, [GENERATOR2], {
21883
+ const r3 = spawn11(process.execPath, [GENERATOR2], {
21884
21884
  cwd: root,
21885
21885
  env: { ...env, [BUNDLES_LOCK_HELD]: "1" },
21886
21886
  encoding: "utf8",
@@ -33273,9 +33273,9 @@ var init_wix_member_login = __esm({
33273
33273
  // ../../core/dev-server/src/wix-gateway.ts
33274
33274
  import { readFileSync as readFileSync29, existsSync as existsSync29 } from "node:fs";
33275
33275
  function isSensitiveBodyName(name) {
33276
- const segments = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
33277
- for (let i2 = 0; i2 < segments.length; i2++) {
33278
- const tail = normKey(segments.slice(i2).join(""));
33276
+ const segments2 = name.split(/[^A-Za-z0-9]+/).filter(Boolean);
33277
+ for (let i2 = 0; i2 < segments2.length; i2++) {
33278
+ const tail = normKey(segments2.slice(i2).join(""));
33279
33279
  for (const k2 of SECRET_BODY_KEYS) if (tail.startsWith(k2)) return true;
33280
33280
  }
33281
33281
  return false;
@@ -45382,7 +45382,7 @@ var init_pdf = __esm({
45382
45382
  var defineProperty = Object.defineProperty;
45383
45383
  var stringSlice = uncurryThis("".slice);
45384
45384
  var replace = uncurryThis("".replace);
45385
- var join136 = uncurryThis([].join);
45385
+ var join137 = uncurryThis([].join);
45386
45386
  var CONFIGURABLE_LENGTH = DESCRIPTORS && !fails(function() {
45387
45387
  return defineProperty(function() {
45388
45388
  }, "length", { value: 8 }).length !== 8;
@@ -45409,7 +45409,7 @@ var init_pdf = __esm({
45409
45409
  }
45410
45410
  var state = enforceInternalState(value2);
45411
45411
  if (!hasOwn(state, "source")) {
45412
- state.source = join136(TEMPLATE, typeof name == "string" ? name : "");
45412
+ state.source = join137(TEMPLATE, typeof name == "string" ? name : "");
45413
45413
  }
45414
45414
  return value2;
45415
45415
  };
@@ -46449,10 +46449,10 @@ var init_pdf = __esm({
46449
46449
  if (stringLength % 2 !== 0) throw new SyntaxError2("String should be an even number of characters");
46450
46450
  var maxLength = into ? min(into.length, stringLength / 2) : stringLength / 2;
46451
46451
  var bytes2 = into || new Uint8Array2(maxLength);
46452
- var segments = stringMatch(string7, /[\S\s]{2}/g);
46452
+ var segments2 = stringMatch(string7, /[\S\s]{2}/g);
46453
46453
  var written = 0;
46454
46454
  for (; written < maxLength; written++) {
46455
- var result = +("0x" + segments[written] + "0");
46455
+ var result = +("0x" + segments2[written] + "0");
46456
46456
  if (result !== result) {
46457
46457
  throw new SyntaxError2("String should only contain hex characters");
46458
46458
  }
@@ -47932,7 +47932,7 @@ var init_pdf = __esm({
47932
47932
  var charAt = uncurryThis("".charAt);
47933
47933
  var Uint8Array2 = globalThis2.Uint8Array;
47934
47934
  var $Array = globalThis2.Array;
47935
- var join136 = uncurryThis([].join);
47935
+ var join137 = uncurryThis([].join);
47936
47936
  var INCORRECT_BEHAVIOR_OR_DOESNT_EXISTS = !Uint8Array2 || !Uint8Array2.prototype.toBase64 || !(function() {
47937
47937
  try {
47938
47938
  var target = new Uint8Array2();
@@ -47978,7 +47978,7 @@ var init_pdf = __esm({
47978
47978
  result[written++] = "=";
47979
47979
  }
47980
47980
  }
47981
- return join136(result, "");
47981
+ return join137(result, "");
47982
47982
  }
47983
47983
  });
47984
47984
  },
@@ -47990,7 +47990,7 @@ var init_pdf = __esm({
47990
47990
  var anUint8Array = __webpack_require__3(4154);
47991
47991
  var notDetached = __webpack_require__3(5169);
47992
47992
  var numberToString2 = uncurryThis(1.1.toString);
47993
- var join136 = uncurryThis([].join);
47993
+ var join137 = uncurryThis([].join);
47994
47994
  var $Array = Array;
47995
47995
  var Uint8Array2 = globalThis2.Uint8Array;
47996
47996
  var INCORRECT_BEHAVIOR_OR_DOESNT_EXISTS = !Uint8Array2 || !Uint8Array2.prototype.toHex || !(function() {
@@ -48010,7 +48010,7 @@ var init_pdf = __esm({
48010
48010
  var hex2 = numberToString2(this[i2], 16);
48011
48011
  result[i2] = hex2.length === 1 ? "0" + hex2 : hex2;
48012
48012
  }
48013
- return join136(result, "");
48013
+ return join137(result, "");
48014
48014
  }
48015
48015
  });
48016
48016
  },
@@ -124646,12 +124646,12 @@ function parseShapeKind(raw) {
124646
124646
  if (k2 === "polygon" || k2 === "circle") return k2;
124647
124647
  return "unknown";
124648
124648
  }
124649
- function circleRing(centreLat, centreLng, radiusM, segments = CIRCLE_SEGMENTS) {
124650
- if (!(radiusM > 0) || segments < 3) return [];
124649
+ function circleRing(centreLat, centreLng, radiusM, segments2 = CIRCLE_SEGMENTS) {
124650
+ if (!(radiusM > 0) || segments2 < 3) return [];
124651
124651
  const dLat = radiusM / METRES_PER_DEG_LAT;
124652
124652
  const dLng = radiusM / (METRES_PER_DEG_LAT * Math.max(0.01, Math.abs(Math.cos(centreLat * Math.PI / 180))));
124653
- return Array.from({ length: segments }, (_2, i2) => {
124654
- const theta = 2 * Math.PI * i2 / segments;
124653
+ return Array.from({ length: segments2 }, (_2, i2) => {
124654
+ const theta = 2 * Math.PI * i2 / segments2;
124655
124655
  return { latitude: centreLat + dLat * Math.sin(theta), longitude: centreLng + dLng * Math.cos(theta) };
124656
124656
  });
124657
124657
  }
@@ -145351,8 +145351,8 @@ function materializeForPreview(appDir3, slug, displayName3) {
145351
145351
  }
145352
145352
  async function appId(dir) {
145353
145353
  try {
145354
- const { readFileSync: readFileSync141 } = await import("node:fs");
145355
- const m2 = JSON.parse(readFileSync141(join34(dir, "himalaya.content.json"), "utf8"));
145354
+ const { readFileSync: readFileSync142 } = await import("node:fs");
145355
+ const m2 = JSON.parse(readFileSync142(join34(dir, "himalaya.content.json"), "utf8"));
145356
145356
  if (m2.name) return m2.name;
145357
145357
  } catch {
145358
145358
  }
@@ -145814,12 +145814,12 @@ function npmInstallArgs(registry2, version5) {
145814
145814
  const pinned = `${normalizeRegistry(registry2) ?? registry2}/`;
145815
145815
  return ["i", "-g", `${CLI_PACKAGE}@${version5}`, `--registry=${pinned}`, `--@wix:registry=${pinned}`];
145816
145816
  }
145817
- function createUpdateRunner(spawn10 = spawnSync6) {
145817
+ function createUpdateRunner(spawn11 = spawnSync6) {
145818
145818
  return {
145819
145819
  install: async (registry2, version5) => {
145820
145820
  if (!isTrustedRegistry(registry2)) return { ok: false, detail: `refusing untrusted registry ${registry2}` };
145821
145821
  if (!INSTALLABLE_SPEC.test(version5)) return { ok: false, detail: `refusing install spec ${JSON.stringify(version5)}` };
145822
- const result = spawn10("npm", npmInstallArgs(registry2, version5), {
145822
+ const result = spawn11("npm", npmInstallArgs(registry2, version5), {
145823
145823
  // npm's stdout goes to OUR stderr: in `himi <cmd> --json | jq` (and `himi upgrade --json`) its
145824
145824
  // "added 212 packages" would otherwise land in stdout ahead of the envelope and break the
145825
145825
  // parse — the agent case this whole feature exists for (#2985 review). And no stdin: an
@@ -150245,21 +150245,21 @@ var init_Expression = __esm({
150245
150245
  * @returns {Array} Array of segment objects
150246
150246
  */
150247
150247
  _parse(pattern) {
150248
- const segments = [];
150248
+ const segments2 = [];
150249
150249
  let i2 = 0;
150250
150250
  let currentPart = "";
150251
150251
  while (i2 < pattern.length) {
150252
150252
  if (pattern[i2] === this.separator) {
150253
150253
  if (i2 + 1 < pattern.length && pattern[i2 + 1] === this.separator) {
150254
150254
  if (currentPart.trim()) {
150255
- segments.push(this._parseSegment(currentPart.trim()));
150255
+ segments2.push(this._parseSegment(currentPart.trim()));
150256
150256
  currentPart = "";
150257
150257
  }
150258
- segments.push({ type: "deep-wildcard" });
150258
+ segments2.push({ type: "deep-wildcard" });
150259
150259
  i2 += 2;
150260
150260
  } else {
150261
150261
  if (currentPart.trim()) {
150262
- segments.push(this._parseSegment(currentPart.trim()));
150262
+ segments2.push(this._parseSegment(currentPart.trim()));
150263
150263
  }
150264
150264
  currentPart = "";
150265
150265
  i2++;
@@ -150270,9 +150270,9 @@ var init_Expression = __esm({
150270
150270
  }
150271
150271
  }
150272
150272
  if (currentPart.trim()) {
150273
- segments.push(this._parseSegment(currentPart.trim()));
150273
+ segments2.push(this._parseSegment(currentPart.trim()));
150274
150274
  }
150275
- return segments;
150275
+ return segments2;
150276
150276
  }
150277
150277
  /**
150278
150278
  * Parse a single segment
@@ -150853,24 +150853,24 @@ var init_Matcher = __esm({
150853
150853
  * @returns {boolean}
150854
150854
  */
150855
150855
  matches(expression) {
150856
- const segments = expression.segments;
150857
- if (segments.length === 0) {
150856
+ const segments2 = expression.segments;
150857
+ if (segments2.length === 0) {
150858
150858
  return false;
150859
150859
  }
150860
150860
  if (expression.hasDeepWildcard()) {
150861
- return this._matchWithDeepWildcard(segments);
150861
+ return this._matchWithDeepWildcard(segments2);
150862
150862
  }
150863
- return this._matchSimple(segments);
150863
+ return this._matchSimple(segments2);
150864
150864
  }
150865
150865
  /**
150866
150866
  * @private
150867
150867
  */
150868
- _matchSimple(segments) {
150869
- if (this.path.length !== segments.length) {
150868
+ _matchSimple(segments2) {
150869
+ if (this.path.length !== segments2.length) {
150870
150870
  return false;
150871
150871
  }
150872
- for (let i2 = 0; i2 < segments.length; i2++) {
150873
- if (!this._matchSegment(segments[i2], this.path[i2], i2 === this.path.length - 1)) {
150872
+ for (let i2 = 0; i2 < segments2.length; i2++) {
150873
+ if (!this._matchSegment(segments2[i2], this.path[i2], i2 === this.path.length - 1)) {
150874
150874
  return false;
150875
150875
  }
150876
150876
  }
@@ -150879,17 +150879,17 @@ var init_Matcher = __esm({
150879
150879
  /**
150880
150880
  * @private
150881
150881
  */
150882
- _matchWithDeepWildcard(segments) {
150882
+ _matchWithDeepWildcard(segments2) {
150883
150883
  let pathIdx = this.path.length - 1;
150884
- let segIdx = segments.length - 1;
150884
+ let segIdx = segments2.length - 1;
150885
150885
  while (segIdx >= 0 && pathIdx >= 0) {
150886
- const segment = segments[segIdx];
150886
+ const segment = segments2[segIdx];
150887
150887
  if (segment.type === "deep-wildcard") {
150888
150888
  segIdx--;
150889
150889
  if (segIdx < 0) {
150890
150890
  return true;
150891
150891
  }
150892
- const nextSeg = segments[segIdx];
150892
+ const nextSeg = segments2[segIdx];
150893
150893
  let found = false;
150894
150894
  for (let i2 = pathIdx; i2 >= 0; i2--) {
150895
150895
  if (this._matchSegment(nextSeg, this.path[i2], i2 === this.path.length - 1)) {
@@ -158149,16 +158149,16 @@ function writeAppSource(entries, destDir) {
158149
158149
  const written = [];
158150
158150
  for (const entry of entries) {
158151
158151
  if (isDeniedSourcePath(entry.path)) continue;
158152
- const segments = entry.path.split("/");
158152
+ const segments2 = entry.path.split("/");
158153
158153
  let dir = root;
158154
- for (const segment of segments.slice(0, -1)) {
158154
+ for (const segment of segments2.slice(0, -1)) {
158155
158155
  dir = join42(dir, segment);
158156
158156
  const kind = pathKind(dir);
158157
158157
  if (kind?.isSymbolicLink()) unlinkSync3(dir);
158158
158158
  else if (kind) continue;
158159
158159
  mkdirSync27(dir);
158160
158160
  }
158161
- const target = join42(root, ...segments);
158161
+ const target = join42(root, ...segments2);
158162
158162
  if (pathKind(target)?.isSymbolicLink()) unlinkSync3(target);
158163
158163
  writeFileSync21(target, entry.bytes);
158164
158164
  written.push(entry.path);
@@ -158189,10 +158189,10 @@ function writeConflicts(entries, destDir) {
158189
158189
  };
158190
158190
  for (const entry of entries) {
158191
158191
  if (isDeniedSourcePath(entry.path)) continue;
158192
- const segments = entry.path.split("/");
158192
+ const segments2 = entry.path.split("/");
158193
158193
  let dir = root;
158194
158194
  let stop = false;
158195
- for (const [i2, segment] of segments.slice(0, -1).entries()) {
158195
+ for (const [i2, segment] of segments2.slice(0, -1).entries()) {
158196
158196
  dir = join42(dir, segment);
158197
158197
  const kind2 = pathKind(dir);
158198
158198
  if (!kind2 || kind2.isSymbolicLink()) {
@@ -158200,12 +158200,12 @@ function writeConflicts(entries, destDir) {
158200
158200
  break;
158201
158201
  }
158202
158202
  if (kind2.isDirectory()) continue;
158203
- say(segments.slice(0, i2 + 1).join("/"), "is a file here, but the release has a directory at that path");
158203
+ say(segments2.slice(0, i2 + 1).join("/"), "is a file here, but the release has a directory at that path");
158204
158204
  stop = true;
158205
158205
  break;
158206
158206
  }
158207
158207
  if (stop) continue;
158208
- const kind = pathKind(join42(root, ...segments));
158208
+ const kind = pathKind(join42(root, ...segments2));
158209
158209
  if (kind && !kind.isSymbolicLink() && kind.isDirectory()) {
158210
158210
  say(entry.path, "is a directory here, but the release has a file at that path");
158211
158211
  }
@@ -158219,8 +158219,8 @@ function removeStaleSource(entries, destDir) {
158219
158219
  const needed = /* @__PURE__ */ new Set();
158220
158220
  for (const entry of entries) {
158221
158221
  if (isDeniedSourcePath(entry.path)) continue;
158222
- const segments = entry.path.split("/");
158223
- for (let i2 = 1; i2 < segments.length; i2++) needed.add(segments.slice(0, i2).join("/"));
158222
+ const segments2 = entry.path.split("/");
158223
+ for (let i2 = 1; i2 < segments2.length; i2++) needed.add(segments2.slice(0, i2).join("/"));
158224
158224
  }
158225
158225
  if (existsSync49(root)) pruneEmptyDirs(root, "", needed, removed);
158226
158226
  return removed.sort();
@@ -161316,14 +161316,14 @@ function scoreOverlays(verticals, layout) {
161316
161316
  function pagePathForRole(url) {
161317
161317
  try {
161318
161318
  const u2 = new URL(url);
161319
- const segments = u2.pathname.split("/").filter(Boolean);
161319
+ const segments2 = u2.pathname.split("/").filter(Boolean);
161320
161320
  const wixHost = /(^|\.)(wixsite\.com|wixstudio\.com)$/i.test(u2.hostname);
161321
161321
  if (wixHost) {
161322
- const page = segments.slice(1);
161322
+ const page = segments2.slice(1);
161323
161323
  return { path: "/" + page.join("/"), isHome: page.length === 0 };
161324
161324
  }
161325
- const leaf = segments[segments.length - 1] ?? "";
161326
- const isHome = segments.length === 0 || /^(home|index)$/i.test(leaf);
161325
+ const leaf = segments2[segments2.length - 1] ?? "";
161326
+ const isHome = segments2.length === 0 || /^(home|index)$/i.test(leaf);
161327
161327
  return { path: u2.pathname.replace(/\/+$/, "") || "/", isHome };
161328
161328
  } catch {
161329
161329
  return { path: url, isHome: false };
@@ -168171,6 +168171,119 @@ var init_nav_slots = __esm({
168171
168171
  }
168172
168172
  });
168173
168173
 
168174
+ // ../../core/ts/src/push-routes.ts
168175
+ function adaptPayloadLink(link, adapter, linkPrefixes) {
168176
+ const raw = (link ?? "").trim();
168177
+ if (!raw) return { residual: raw, adapted: false };
168178
+ if (adapter === "wix-ping") {
168179
+ const wix = adaptWixLink(raw);
168180
+ if (wix) return wix;
168181
+ }
168182
+ const rest = stripLinkPrefix(raw, linkPrefixes);
168183
+ if (rest !== void 0) return { residual: stripLeadingSlashes(rest), adapted: true };
168184
+ if (raw.startsWith("/")) return { residual: stripLeadingSlashes(raw), adapted: true };
168185
+ return { residual: raw, adapted: false };
168186
+ }
168187
+ function stripLinkPrefix(raw, prefixes) {
168188
+ let best;
168189
+ let bestLength = -1;
168190
+ for (const entry of Array.isArray(prefixes) ? prefixes : []) {
168191
+ const prefix = typeof entry === "string" ? entry.trim() : "";
168192
+ if (!prefix || prefix.length <= bestLength || !startsWithLinkPrefix(raw, prefix)) continue;
168193
+ best = raw.slice(prefix.length);
168194
+ bestLength = prefix.length;
168195
+ }
168196
+ return best;
168197
+ }
168198
+ function startsWithLinkPrefix(raw, prefix) {
168199
+ if (raw.length < prefix.length) return false;
168200
+ const head3 = caseInsensitiveHeadLength(prefix);
168201
+ if (raw.slice(0, head3).toLowerCase() !== prefix.slice(0, head3).toLowerCase()) return false;
168202
+ if (raw.slice(head3, prefix.length) !== prefix.slice(head3)) return false;
168203
+ if (prefix.endsWith("/") || linkPrefixSchemeLength(prefix) === prefix.length || raw.length === prefix.length) return true;
168204
+ return "/?#".includes(raw.charAt(prefix.length));
168205
+ }
168206
+ function caseInsensitiveHeadLength(prefix) {
168207
+ const scheme = linkPrefixSchemeLength(prefix);
168208
+ if (scheme === 0 || !prefix.startsWith("//", scheme)) return scheme;
168209
+ const slash = prefix.indexOf("/", scheme + 2);
168210
+ return slash < 0 ? prefix.length : slash;
168211
+ }
168212
+ function linkPrefixSchemeLength(prefix) {
168213
+ for (let i2 = 0; i2 < prefix.length; i2++) {
168214
+ const c2 = prefix.charAt(i2);
168215
+ if (c2 === ":") return i2 > 0 ? i2 + 1 : 0;
168216
+ const letter = c2 >= "a" && c2 <= "z" || c2 >= "A" && c2 <= "Z";
168217
+ const later2 = c2 >= "0" && c2 <= "9" || c2 === "+" || c2 === "-" || c2 === ".";
168218
+ if (!letter && !(i2 > 0 && later2)) return 0;
168219
+ }
168220
+ return 0;
168221
+ }
168222
+ function adaptWixLink(raw) {
168223
+ let rest;
168224
+ if (raw.toLowerCase().startsWith(WIX_SCHEME_PREFIX)) {
168225
+ rest = raw.slice(WIX_SCHEME_PREFIX.length);
168226
+ } else {
168227
+ const host = hostOf(raw);
168228
+ if (!host || !WIX_UNIVERSAL_HOST.test(host)) return void 0;
168229
+ rest = raw.slice(raw.toLowerCase().indexOf(host) + host.length);
168230
+ }
168231
+ const [pathPart, queryPart = ""] = splitOnce(stripLeadingSlashes(rest), "?");
168232
+ const kept = [];
168233
+ let metaSiteId;
168234
+ for (const pair of queryPart.split("&")) {
168235
+ if (!pair) continue;
168236
+ const [key3, value2 = ""] = splitOnce(pair, "=");
168237
+ if (key3 === "metaSiteId") metaSiteId = decodeMaybe(value2);
168238
+ if (PING_BOOKKEEPING_KEYS.has(key3)) continue;
168239
+ kept.push(pair);
168240
+ }
168241
+ const residual = kept.length ? `${pathPart}?${kept.join("&")}` : pathPart;
168242
+ return { residual, metaSiteId: metaSiteId ?? siteFromPath(pathPart), adapted: true };
168243
+ }
168244
+ function siteFromPath(pathPart) {
168245
+ const site = siteSegment(segments(pathPart));
168246
+ return site ? decodeMaybe(site, false) : void 0;
168247
+ }
168248
+ function siteSegment(segs) {
168249
+ if (segs[0] === "my-account" && segs[1] && segs[1] !== "app") return segs[1];
168250
+ if (segs[0] === "shared-gallery" && segs[1]) return segs[1];
168251
+ if (segs[0] === "lists" && segs[1]) return segs[1];
168252
+ if (segs[0] === "spaces" && segs[1] === "lists" && segs[2]) return segs[2];
168253
+ if (segs[0] === "notifications" && segs[1] === "list" && segs[2]) return segs[2];
168254
+ return void 0;
168255
+ }
168256
+ function hostOf(url) {
168257
+ const m2 = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/([^/?#]+)/.exec(url);
168258
+ return m2 ? m2[1].toLowerCase() : null;
168259
+ }
168260
+ function splitOnce(s2, sep26) {
168261
+ const i2 = s2.indexOf(sep26);
168262
+ return i2 < 0 ? [s2, void 0] : [s2.slice(0, i2), s2.slice(i2 + sep26.length)];
168263
+ }
168264
+ function stripLeadingSlashes(s2) {
168265
+ return s2.replace(/^\/+/, "");
168266
+ }
168267
+ function segments(path2) {
168268
+ return path2.split("/").filter((s2) => s2.length > 0);
168269
+ }
168270
+ function decodeMaybe(value2, plusAsSpace = true) {
168271
+ try {
168272
+ return decodeURIComponent(plusAsSpace ? value2.replace(/\+/g, " ") : value2);
168273
+ } catch {
168274
+ return value2;
168275
+ }
168276
+ }
168277
+ var WIX_UNIVERSAL_HOST, WIX_SCHEME_PREFIX, PING_BOOKKEEPING_KEYS;
168278
+ var init_push_routes = __esm({
168279
+ "../../core/ts/src/push-routes.ts"() {
168280
+ init_define_HHP_CATALOG();
168281
+ WIX_UNIVERSAL_HOST = /^(?:manage\.wix\.app|www\.wix\.app|m-[A-Za-z0-9-]+\.branded\.wix\.app)$/;
168282
+ WIX_SCHEME_PREFIX = "wix:/";
168283
+ PING_BOOKKEEPING_KEYS = /* @__PURE__ */ new Set(["metaSiteId", "d"]);
168284
+ }
168285
+ });
168286
+
168174
168287
  // src/nav-lint.ts
168175
168288
  var nav_lint_exports = {};
168176
168289
  __export(nav_lint_exports, {
@@ -168209,6 +168322,7 @@ async function navIssues(dir, strict) {
168209
168322
  pushScreenMapIssues(cfg.push, provisioning, warn3);
168210
168323
  pushAuthIssues(cfg.push, cfg.auth, provisioning, warn3);
168211
168324
  pushFamilyIssues(cfg.push, warn3);
168325
+ pushLinkPrefixIssues(cfg.push, provisioning, warn3);
168212
168326
  const captcha = provisioning.siteId && !placeholderLogin(cfg.auth) ? captchaAuthFinding(cfg.auth, provisioning.siteId) : void 0;
168213
168327
  if (captcha && captcha.code !== "captcha_auth_site_case" && !issues.some((i2) => i2.ruleCode === "push-auth-site"))
168214
168328
  push("warning", "auth-captcha-site", captcha.message);
@@ -168375,6 +168489,51 @@ function pushRouteIssues(push, nav, provisioning, warn3) {
168375
168489
  }
168376
168490
  }
168377
168491
  }
168492
+ function pushLinkPrefixIssues(push, provisioning, warn3) {
168493
+ if (!isPlainObject2(push) || push.linkPrefixes === void 0) return;
168494
+ if (!Array.isArray(push.linkPrefixes)) {
168495
+ warn3(
168496
+ "push-link-prefixes-invalid",
168497
+ `push.linkPrefixes must be a list of non-empty strings, e.g. ["legacyapp://", "https://legacy.example.com/app/"]; the device reads anything else as no prefixes.`
168498
+ );
168499
+ return;
168500
+ }
168501
+ const prefixes = push.linkPrefixes.filter((p2) => typeof p2 === "string" && !!p2.trim());
168502
+ const skipped = push.linkPrefixes.length - prefixes.length;
168503
+ if (skipped > 0) {
168504
+ warn3(
168505
+ "push-link-prefixes-invalid",
168506
+ `push.linkPrefixes must hold non-empty strings only; the device skips ${skipped === 1 ? "the 1 entry" : `the ${skipped} entries`} that ${skipped === 1 ? "is" : "are"} not one and keeps the other prefixes.`
168507
+ );
168508
+ }
168509
+ for (const raw of prefixes) {
168510
+ const prefix = raw.trim();
168511
+ if (push.payloadAdapter === "wix-ping" && wixLinkSpacePrefix(prefix)) {
168512
+ warn3(
168513
+ "push-link-prefix-shadowed",
168514
+ `push.linkPrefixes: "${prefix}" is in the Wix link space, which the "wix-ping" adapter already strips first, so this prefix never applies. Remove it.`
168515
+ );
168516
+ }
168517
+ }
168518
+ const authored2 = Array.isArray(push.routes) && push.routes.length > 0;
168519
+ if (prefixes.length > 0 && !authored2 && derivesWixPushRoutes(push, provisioning.installedVerticals)) {
168520
+ warn3(
168521
+ "push-link-prefixes-derived-routes",
168522
+ `push.linkPrefixes is set, but the only push routes are the ones derived from the site's verticals, so a stripped link is matched against the Wix notification paths alone (e.g. "my-account/:site/stores/orders/:orderId"). If your legacy links use paths of their own, declare push.routes for them \u2014 a declared table replaces the derived one, so list the Wix routes you still need there too.`
168523
+ );
168524
+ return;
168525
+ }
168526
+ if (prefixes.length > 0 && !authored2 && push.payloadAdapter !== "wix-ping") {
168527
+ warn3(
168528
+ "push-link-prefixes-unrouted",
168529
+ `push.linkPrefixes is set but push.routes is empty, so a stripped link matches nothing and still opens the home screen. Declare a route for each destination, e.g. { match: "widget/:id", link: "<scheme>://screen/<screen>?id={id}" }.`
168530
+ );
168531
+ }
168532
+ }
168533
+ function wixLinkSpacePrefix(prefix) {
168534
+ const probe2 = `${prefix}/x`;
168535
+ return adaptPayloadLink(probe2, "wix-ping").adapted && !adaptPayloadLink(probe2, "").adapted;
168536
+ }
168378
168537
  function pushScreenMapIssues(push, provisioning, warn3) {
168379
168538
  if (!isPlainObject2(push) || !isPlainObject2(push.screens)) return;
168380
168539
  const destinations = WIX_MEMBER_PUSH_DESTINATIONS.map((d2) => d2.id);
@@ -168468,6 +168627,7 @@ var init_nav_lint = __esm({
168468
168627
  init_nav_slots();
168469
168628
  init_home_screen_id();
168470
168629
  init_wix_push_links();
168630
+ init_push_routes();
168471
168631
  init_captcha_auth();
168472
168632
  init_wix_placeholders();
168473
168633
  init_preview_surfaces();
@@ -183640,44 +183800,44 @@ var init_preview_assets_generated = __esm({
183640
183800
  init_define_HHP_CATALOG();
183641
183801
  init_preview_assets_format();
183642
183802
  PREVIEW_ASSET_MANIFEST = [
183643
- { key: "assets/DashboardHost--Wo75MEb.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 2971, sha256: "9f16ff8813c75250b296c2f7fd5904148408ec882daade74dfd7256ab841116f" },
183644
- { key: "assets/DevDrawer-CUUhA_4k.js", contentType: "application/javascript; charset=utf-8", offset: 2979, length: 16783, sha256: "d7379d5435e1f42d653f1dbc15f5584f9aa3cae7eabf8d5779342ab59af8f064" },
183645
- { key: "assets/IdiomCompare-BQ8xaS9X.js", contentType: "application/javascript; charset=utf-8", offset: 19762, length: 1926, sha256: "3c3ad7cb21733554a1029f317f5d17498648aa5bbe4d9ad654128299bda12985" },
183646
- { key: "assets/LightboxViewer-DXIIycZi.js", contentType: "application/javascript; charset=utf-8", offset: 21688, length: 8171, sha256: "f97499f47911dcf2f19f6199c12236559e32df337994b61edb72cd9f0f73743d" },
183647
- { key: "assets/OfflineReadingBar-DaSjhw5H.js", contentType: "application/javascript; charset=utf-8", offset: 29859, length: 1707, sha256: "cc0ced6ae7a34d34c90dbd765a9018880aa9657f1de7e38f4839ef494b68aaf8" },
183648
- { key: "assets/PhonePreviewControl-D8MpZ148.js", contentType: "application/javascript; charset=utf-8", offset: 31566, length: 37768, sha256: "d6837187711fb89cb762beccb1f23289a45b7c0adf56023c623893b82d5c64db" },
183649
- { key: "assets/SchemeCompare-C08F_1NC.js", contentType: "application/javascript; charset=utf-8", offset: 69334, length: 1858, sha256: "d24db4000236629dfe6191f91dab56421421477e2dda5a423d3766065a1107d9" },
183650
- { key: "assets/WixFrogTelemetrySink-D2Lk2X2m.js", contentType: "application/javascript; charset=utf-8", offset: 71192, length: 3165, sha256: "7a8b798af6b0900a8e925bfdb9ec62b378380975cf5c00af25d0cf33b39ba12f" },
183803
+ { key: "assets/DashboardHost-CEBpBObS.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 2971, sha256: "194b736fc9a86ba8276d269811ca790ed52b229b47c4555fb63cb2198af53f48" },
183804
+ { key: "assets/DevDrawer-uzQCfqE2.js", contentType: "application/javascript; charset=utf-8", offset: 2979, length: 16783, sha256: "03bc464dbd479b715f4f08217753f5eafb6726c3421d7c460f2bc425bae4eec9" },
183805
+ { key: "assets/IdiomCompare-DyYGHizT.js", contentType: "application/javascript; charset=utf-8", offset: 19762, length: 1926, sha256: "e2ad46f7e9daa653da190162d17c3c613c4647e67f381c1d291bc688d0e5d406" },
183806
+ { key: "assets/LightboxViewer-DWzwOzwg.js", contentType: "application/javascript; charset=utf-8", offset: 21688, length: 8171, sha256: "6492fd20cfc48db80b58d9725ac92558adc6a84fc038d1bab360f25b0331fcd2" },
183807
+ { key: "assets/OfflineReadingBar-DVNFthXe.js", contentType: "application/javascript; charset=utf-8", offset: 29859, length: 1707, sha256: "50a5764fab00aca9e1b0af6551cd635f94d8dc26199492bd82ae29d1b7286955" },
183808
+ { key: "assets/PhonePreviewControl-35dJ1iZb.js", contentType: "application/javascript; charset=utf-8", offset: 31566, length: 37768, sha256: "80ff868a49a490811753d4d12ba0b390142ff6b5ddc266947e8cfadbcd86854d" },
183809
+ { key: "assets/SchemeCompare-CQznjOL6.js", contentType: "application/javascript; charset=utf-8", offset: 69334, length: 1858, sha256: "4540c9568c814bbc5521b7337e4841c16d48c0244ba2cb2b1bd88ef32cd45b32" },
183810
+ { key: "assets/WixFrogTelemetrySink-CGDOaFQw.js", contentType: "application/javascript; charset=utf-8", offset: 71192, length: 3165, sha256: "211ade53145b59b513bd269b904e25b03134c604d394c9c69896fcb1439705e1" },
183651
183811
  { key: "assets/calendar-ics-BvfIYWjG.js", contentType: "application/javascript; charset=utf-8", offset: 74357, length: 2192, sha256: "1636167041e9829ceccd3544297b86b8475c38bd2f759c4dbc17c813dc6e9254" },
183652
- { key: "assets/files-transfer-DJkAGGn4.js", contentType: "application/javascript; charset=utf-8", offset: 76549, length: 6446, sha256: "d2d71e724ad7e8cc0f2511625a605232762cc613e14a328b5192ac4163b902f8" },
183812
+ { key: "assets/files-transfer-DrDuPSUh.js", contentType: "application/javascript; charset=utf-8", offset: 76549, length: 6446, sha256: "c6b0b85dfd90a86ed4989040cdfaff30fe169bb91cb52928e13d8e916f95674a" },
183653
183813
  { key: "assets/glassPreviewBrowser-DONaOehd.js", contentType: "application/javascript; charset=utf-8", offset: 82995, length: 6578, sha256: "4402c4cddd2a564e7466cdf1e12a0e0277c0400b288e1e5eda1d23e797599d31" },
183654
183814
  { key: "assets/image-preparation-Cj4gldm6.js", contentType: "application/javascript; charset=utf-8", offset: 89573, length: 2704, sha256: "32944ea587c2f3b84389b66b50a850437560238f1c8d33b6275973ee04eee865" },
183655
- { key: "assets/index-BoMtgO1j.js", contentType: "application/javascript; charset=utf-8", offset: 92277, length: 1494558, sha256: "eb0deba70b04f12f7a35e248bbe05e7adab4f655f596f2eb4bf501b2ff49f201" },
183656
- { key: "assets/jsx-runtime-CY_jTiqj.js", contentType: "application/javascript; charset=utf-8", offset: 1586835, length: 7624, sha256: "29dfba5b56f79a0b84d6b04d84d87617454baeba03c225f682ab2ab7ae2d5016" },
183657
- { key: "assets/pdf-TmaMTLyU.js", contentType: "application/javascript; charset=utf-8", offset: 1594459, length: 488199, sha256: "41a8a6ab79e3465a7b955a596951fac14aed11cd2108b294ae1028aa25cd9ea5" },
183658
- { key: "assets/pdf.worker.min-BQa_JDET.js", contentType: "application/javascript; charset=utf-8", offset: 2082658, length: 73, sha256: "41bcfda4f3c8c0a81c785ea87c69afc8e7d0ed8c333324aee94c273d881bb6aa" },
183659
- { key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/javascript; charset=utf-8", offset: 2082731, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
183660
- { key: "assets/pdfDocument-BzqLjXka.js", contentType: "application/javascript; charset=utf-8", offset: 3399765, length: 3453, sha256: "8c22a0d303cdc014532f5c98c74d9e16ad688c9c04b7b3c597205810311d8968" },
183661
- { key: "assets/pdfFetch-Cc8i1I1h.js", contentType: "application/javascript; charset=utf-8", offset: 3403218, length: 2266, sha256: "1d21ad008921942217c9eac0c35b1713773b9d89c71e3ab4412781f7bb88f463" },
183662
- { key: "assets/webviewBridgeHost-BmEurjBq.js", contentType: "application/javascript; charset=utf-8", offset: 3405484, length: 9888, sha256: "b8e03f5a683914103f1084c9e03038167716065647b2280a57a99d548cad9d1f" },
183663
- { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 3415372, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
183664
- { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3419982, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
183665
- { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3908566, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
183666
- { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3999490, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
183667
- { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 4090474, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
183668
- { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 4181454, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
183669
- { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 4272326, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
183670
- { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 4363306, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
183671
- { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4471942, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
183672
- { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4581026, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
183673
- { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4690074, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
183674
- { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4797774, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
183675
- { key: "himi-sw.js", contentType: "application/javascript; charset=utf-8", offset: 4885882, length: 6818, sha256: "835250c3d8004f27c2b67f9b471984351ba6df07b916d97e33b43c691200a18c" },
183676
- { key: "index.html", contentType: "text/html; charset=utf-8", offset: 4892700, length: 5037, sha256: "d3bd185a4b68f1301c35db67d6b2020c0cf1eaa67b335a1269c34babf3c46f6f" },
183677
- { key: "ios-glass-preview.css", contentType: "text/css; charset=utf-8", offset: 4897737, length: 11457, sha256: "6cb2b75264fa5fa2b4d28851878a5eab0712900e8b3a6c2370cc661d6d820805" },
183678
- { key: "kit-tab-bar.css", contentType: "text/css; charset=utf-8", offset: 4909194, length: 1171, sha256: "9e2c3a0faaf05d5471badf167a4d3bdf1c4e6dd65c432eea6d254158bfd99585" },
183679
- { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4910365, length: 23047, sha256: "20d1459e3f896f344e478e613d0c979a0de02e81a929a45d3a0ef6a53af36441" },
183680
- { key: "t5-worker-frame.html", contentType: "text/html; charset=utf-8", offset: 4933412, length: 23064, sha256: "8655cfe9c74212ec5dafecb49d2fdeb48b19f1bd8a710091e3e55631f9b4ab06" }
183815
+ { key: "assets/index-hchXMFoQ.js", contentType: "application/javascript; charset=utf-8", offset: 92277, length: 1494691, sha256: "20e599aca1ae4eaad4be69b096f64f856c626a41e22f534fb618a40e1520e895" },
183816
+ { key: "assets/jsx-runtime-CY_jTiqj.js", contentType: "application/javascript; charset=utf-8", offset: 1586968, length: 7624, sha256: "29dfba5b56f79a0b84d6b04d84d87617454baeba03c225f682ab2ab7ae2d5016" },
183817
+ { key: "assets/pdf-DR3F_KpR.js", contentType: "application/javascript; charset=utf-8", offset: 1594592, length: 488199, sha256: "21fe5e373cd532459a97c7b22676de3f119b7faf63c946598f34154668846daa" },
183818
+ { key: "assets/pdf.worker.min-BQa_JDET.js", contentType: "application/javascript; charset=utf-8", offset: 2082791, length: 73, sha256: "41bcfda4f3c8c0a81c785ea87c69afc8e7d0ed8c333324aee94c273d881bb6aa" },
183819
+ { key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/javascript; charset=utf-8", offset: 2082864, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
183820
+ { key: "assets/pdfDocument-DfO2lYMs.js", contentType: "application/javascript; charset=utf-8", offset: 3399898, length: 3453, sha256: "9450272aabf00ce5c5457a524542730b0b1a170a767a7448ee491f1c6e8bac36" },
183821
+ { key: "assets/pdfFetch-Cc8i1I1h.js", contentType: "application/javascript; charset=utf-8", offset: 3403351, length: 2266, sha256: "1d21ad008921942217c9eac0c35b1713773b9d89c71e3ab4412781f7bb88f463" },
183822
+ { key: "assets/webviewBridgeHost-CCdXyNs2.js", contentType: "application/javascript; charset=utf-8", offset: 3405617, length: 9888, sha256: "760042ac8ef57833ebce2abe3ec1370b3ce08eece817694ef5573f107222bfc6" },
183823
+ { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 3415505, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
183824
+ { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3420115, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
183825
+ { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3908699, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
183826
+ { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3999623, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
183827
+ { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 4090607, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
183828
+ { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 4181587, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
183829
+ { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 4272459, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
183830
+ { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 4363439, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
183831
+ { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4472075, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
183832
+ { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4581159, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
183833
+ { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4690207, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
183834
+ { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4797907, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
183835
+ { key: "himi-sw.js", contentType: "application/javascript; charset=utf-8", offset: 4886015, length: 6818, sha256: "835250c3d8004f27c2b67f9b471984351ba6df07b916d97e33b43c691200a18c" },
183836
+ { key: "index.html", contentType: "text/html; charset=utf-8", offset: 4892833, length: 5037, sha256: "7f402c56168cb2a6b7e9ac9058e0e3d3c8b61f663e6918f161ba01b0aba62c4c" },
183837
+ { key: "ios-glass-preview.css", contentType: "text/css; charset=utf-8", offset: 4897870, length: 11457, sha256: "6cb2b75264fa5fa2b4d28851878a5eab0712900e8b3a6c2370cc661d6d820805" },
183838
+ { key: "kit-tab-bar.css", contentType: "text/css; charset=utf-8", offset: 4909327, length: 1171, sha256: "9e2c3a0faaf05d5471badf167a4d3bdf1c4e6dd65c432eea6d254158bfd99585" },
183839
+ { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4910498, length: 23047, sha256: "20d1459e3f896f344e478e613d0c979a0de02e81a929a45d3a0ef6a53af36441" },
183840
+ { key: "t5-worker-frame.html", contentType: "text/html; charset=utf-8", offset: 4933545, length: 23064, sha256: "8655cfe9c74212ec5dafecb49d2fdeb48b19f1bd8a710091e3e55631f9b4ab06" }
183681
183841
  ];
183682
183842
  }
183683
183843
  });
@@ -189678,7 +189838,7 @@ function expandVars(value2, vars) {
189678
189838
  }
189679
189839
  return out2;
189680
189840
  }
189681
- function hostOf(entry) {
189841
+ function hostOf2(entry) {
189682
189842
  const h2 = entry.split("?")[0].toLowerCase();
189683
189843
  const bracketed = /^\[([^\]]*)\]/.exec(h2);
189684
189844
  if (bracketed) return bracketed[1];
@@ -190088,7 +190248,7 @@ var init_preflight_app = __esm({
190088
190248
  init_credential_profile();
190089
190249
  init_app_icon_rules();
190090
190250
  LOOPBACK_HOST = /^(?:localhost|127(?:\.\d{1,3}){3}|0\.0\.0\.0|\[?::1\]?)$/i;
190091
- isLoopback2 = (entry) => LOOPBACK_HOST.test(hostOf(entry));
190251
+ isLoopback2 = (entry) => LOOPBACK_HOST.test(hostOf2(entry));
190092
190252
  unquote3 = (v2) => v2.trim().replace(/^["']|["']$/g, "");
190093
190253
  APP_STORE_SIZES = [
190094
190254
  [1320, 2868],
@@ -190676,13 +190836,6 @@ var init_push_send = __esm({
190676
190836
  }
190677
190837
  });
190678
190838
 
190679
- // ../../core/ts/src/push-routes.ts
190680
- var init_push_routes = __esm({
190681
- "../../core/ts/src/push-routes.ts"() {
190682
- init_define_HHP_CATALOG();
190683
- }
190684
- });
190685
-
190686
190839
  // ../../core/serve-control/src/push-send-service.ts
190687
190840
  var PUSH_SEND_ERROR_CODES, PushSendError;
190688
190841
  var init_push_send_service = __esm({
@@ -198491,10 +198644,10 @@ function ricosVideoUrl(src) {
198491
198644
  const { url, id } = src;
198492
198645
  if (typeof url === "string" && url.trim() !== "") {
198493
198646
  const link = cleanLink(url);
198494
- const linkHost = link === void 0 ? void 0 : hostOf2(link);
198647
+ const linkHost = link === void 0 ? void 0 : hostOf3(link);
198495
198648
  if (link !== void 0 && linkHost !== void 0 && VIDEO_CARD_HOST.test(linkHost)) return link;
198496
198649
  const media = normalizeMediaRef(url);
198497
- const mediaHost = hostOf2(media);
198650
+ const mediaHost = hostOf3(media);
198498
198651
  return HTTPS.test(media) && !(mediaHost !== void 0 && VIDEO_CARD_HOST.test(mediaHost)) ? media : "";
198499
198652
  }
198500
198653
  const ref = typeof id === "string" ? id.trim().replace(/^video\//, "") : "";
@@ -198502,7 +198655,7 @@ function ricosVideoUrl(src) {
198502
198655
  if (/^\w[\w~.-]*\/[\w~./-]+$/.test(ref)) return `https://video.wixstatic.com/video/${ref}`;
198503
198656
  return "";
198504
198657
  }
198505
- var HTTPS, LINK3, HTTP_HOST, VIDEO_CARD_HOST, hostOf2;
198658
+ var HTTPS, LINK3, HTTP_HOST, VIDEO_CARD_HOST, hostOf3;
198506
198659
  var init_display = __esm({
198507
198660
  "../../stdlib/wix-sdk/rich-content/src/display.ts"() {
198508
198661
  init_define_HHP_CATALOG();
@@ -198511,7 +198664,7 @@ var init_display = __esm({
198511
198664
  LINK3 = /^(https?:\/\/[^\s/?#]\S*|mailto:\S+|tel:\S+)$/i;
198512
198665
  HTTP_HOST = /^https?:\/\/([^\s/?#]+)/i;
198513
198666
  VIDEO_CARD_HOST = /^(?:(?:www\.|m\.)?youtube\.com|youtu\.be|(?:www\.|player\.)?vimeo\.com)$/;
198514
- hostOf2 = (url) => HTTP_HOST.exec(url)?.[1]?.toLowerCase().replace(/:\d+$/, "");
198667
+ hostOf3 = (url) => HTTP_HOST.exec(url)?.[1]?.toLowerCase().replace(/:\d+$/, "");
198515
198668
  }
198516
198669
  });
198517
198670
 
@@ -201022,21 +201175,21 @@ function siteNameFromTitle(title, pageName) {
201022
201175
  if (!full || !page) return full;
201023
201176
  const parts = full.split(TITLE_SEPARATOR);
201024
201177
  if (parts.length < 3) return full;
201025
- const segments = [];
201178
+ const segments2 = [];
201026
201179
  const separators = [];
201027
201180
  for (let i2 = 0; i2 < parts.length; i2++) {
201028
- (i2 % 2 === 0 ? segments : separators).push(parts[i2]);
201181
+ (i2 % 2 === 0 ? segments2 : separators).push(parts[i2]);
201029
201182
  }
201030
- const last = segments.length - 1;
201031
- const head3 = norm(segments[0]) === page;
201032
- const tail = norm(segments[last]) === page;
201183
+ const last = segments2.length - 1;
201184
+ const head3 = norm(segments2[0]) === page;
201185
+ const tail = norm(segments2[last]) === page;
201033
201186
  if (head3 && tail) return full;
201034
201187
  const drop2 = head3 ? 0 : tail ? last : -1;
201035
201188
  if (drop2 === -1) return full;
201036
- const kept = segments.map((_2, i2) => i2).filter((i2) => i2 !== drop2);
201189
+ const kept = segments2.map((_2, i2) => i2).filter((i2) => i2 !== drop2);
201037
201190
  if (kept.length === 0) return full;
201038
- let out2 = segments[kept[0]];
201039
- for (const i2 of kept.slice(1)) out2 += `${separators[i2 - 1]}${segments[i2]}`;
201191
+ let out2 = segments2[kept[0]];
201192
+ for (const i2 of kept.slice(1)) out2 += `${separators[i2 - 1]}${segments2[i2]}`;
201040
201193
  return out2.trim() || full;
201041
201194
  }
201042
201195
  function identityFromCrawl(siteUrl, title, description, pageName) {
@@ -205744,6 +205897,7 @@ async function runFidelity(root, opts = {}) {
205744
205897
  }
205745
205898
  } finally {
205746
205899
  await session?.close();
205900
+ await native?.restore?.();
205747
205901
  }
205748
205902
  const report4 = { app: basename30(root), run: run2, dir, rows, status: worst(rows) };
205749
205903
  writeFileSync58(join134(dir, "report.json"), JSON.stringify(report4, null, 2) + "\n");
@@ -205786,6 +205940,186 @@ var init_run2 = __esm({
205786
205940
  }
205787
205941
  });
205788
205942
 
205943
+ // src/fidelity/native.ts
205944
+ var native_exports = {};
205945
+ __export(native_exports, {
205946
+ defaultRunner: () => defaultRunner3,
205947
+ deviceFor: () => deviceFor,
205948
+ nativeCapture: () => nativeCapture,
205949
+ parseAppLocales: () => parseAppLocales,
205950
+ parseNightMode: () => parseNightMode,
205951
+ parsePlistArray: () => parsePlistArray,
205952
+ screenLink: () => screenLink
205953
+ });
205954
+ import { spawn as spawn9 } from "node:child_process";
205955
+ import { readFileSync as readFileSync141, writeFileSync as writeFileSync59 } from "node:fs";
205956
+ import { join as join135 } from "node:path";
205957
+ function deviceFor(devices, platform, variant) {
205958
+ if (variant === "tablet") return platform === "ios" ? devices.iosTablet : devices.androidTablet;
205959
+ return platform === "ios" ? devices.ios : devices.android;
205960
+ }
205961
+ function screenLink(appId2, screen, entry, release2) {
205962
+ const query = new URLSearchParams();
205963
+ for (const [key3, value2] of Object.entries(entry.params ?? {})) {
205964
+ query.set(key3, typeof value2 === "string" ? value2 : JSON.stringify(value2));
205965
+ }
205966
+ if (release2) query.set("release", release2);
205967
+ const qs2 = query.toString();
205968
+ return `himi://app/${encodeURIComponent(appId2)}/screen/${encodeURIComponent(screen)}${qs2 ? `?${qs2}` : ""}`;
205969
+ }
205970
+ function nativeCapture(devices, opts) {
205971
+ const run2 = opts.run ?? defaultRunner3;
205972
+ const read = opts.readFile ?? ((p2) => readFileSync141(p2));
205973
+ const sleep = opts.sleep ?? ((ms2) => new Promise((r3) => setTimeout(r3, ms2)));
205974
+ const settleMs = opts.settleMs ?? 1e3;
205975
+ const maxWaitMs = opts.maxWaitMs ?? 3e4;
205976
+ const iosBundle = opts.iosBundleId ?? "dev.himalaya.player.dev";
205977
+ const androidPackage = opts.androidPackage ?? "dev.himalaya.player.dev";
205978
+ const rtl = opts.rtlLanguage ?? "he";
205979
+ const picker = opts.pickerAppId ?? "himi-player";
205980
+ const blackFraction = opts.blackFraction ?? (async (file2) => {
205981
+ const r3 = await run2("magick", [file2, "-colorspace", "Gray", "-threshold", "5%", "-format", "%[fx:1-mean]", "info:"]);
205982
+ return r3.code === 0 ? Number(r3.stdout.toString().trim()) || 0 : 0;
205983
+ });
205984
+ const saved = /* @__PURE__ */ new Map();
205985
+ const applied = /* @__PURE__ */ new Map();
205986
+ const ok3 = async (cmd, args) => {
205987
+ const r3 = await run2(cmd, args);
205988
+ if (r3.code !== 0) throw new Error(`${cmd} ${args.join(" ")} exited ${r3.code}`);
205989
+ return r3.stdout.toString().trim();
205990
+ };
205991
+ async function remember(platform, id) {
205992
+ const key3 = `${platform}:${id}`;
205993
+ if (saved.has(key3)) return;
205994
+ if (platform === "ios") {
205995
+ const appearance = await ok3("xcrun", ["simctl", "ui", id, "appearance"]);
205996
+ const languages = await run2("xcrun", ["simctl", "spawn", id, "defaults", "read", "-g", "AppleLanguages"]);
205997
+ saved.set(key3, { appearance, language: languages.code === 0 ? languages.stdout.toString().trim() : void 0 });
205998
+ } else {
205999
+ const night = await ok3("adb", ["-s", id, "shell", "cmd", "uimode", "night"]);
206000
+ const locales = await run2("adb", ["-s", id, "shell", "cmd", "locale", "get-app-locales", androidPackage]);
206001
+ saved.set(key3, { appearance: parseNightMode(night), language: locales.code === 0 ? parseAppLocales(locales.stdout.toString()) : void 0 });
206002
+ }
206003
+ }
206004
+ async function apply(platform, id, variant) {
206005
+ const key3 = `${platform}:${id}`;
206006
+ const want = variant === "dark" ? "dark" : variant === "rtl" ? "rtl" : "light";
206007
+ if (applied.get(key3) === want) return;
206008
+ await remember(platform, id);
206009
+ const dark = want === "dark", right = want === "rtl";
206010
+ if (platform === "ios") {
206011
+ await ok3("xcrun", ["simctl", "ui", id, "appearance", dark ? "dark" : "light"]);
206012
+ await ok3("xcrun", ["simctl", "spawn", id, "defaults", "write", "-g", "AppleLanguages", "-array", right ? rtl : "en"]);
206013
+ await run2("xcrun", ["simctl", "terminate", id, iosBundle]);
206014
+ } else {
206015
+ await ok3("adb", ["-s", id, "shell", "cmd", "uimode", "night", dark ? "yes" : "no"]);
206016
+ const wasRtl = applied.get(key3) === "rtl";
206017
+ if (right || wasRtl) await ok3("adb", ["-s", id, "shell", "cmd", "locale", "set-app-locales", androidPackage, "--locales", right ? rtl : ""]);
206018
+ }
206019
+ applied.set(key3, want);
206020
+ }
206021
+ async function screenshot(platform, id, file2) {
206022
+ if (platform === "ios") {
206023
+ await ok3("xcrun", ["simctl", "io", id, "screenshot", file2]);
206024
+ return read(file2);
206025
+ }
206026
+ const r3 = await run2("adb", ["-s", id, "exec-out", "screencap", "-p"]);
206027
+ if (r3.code !== 0) throw new Error(`adb -s ${id} exec-out screencap -p exited ${r3.code}`);
206028
+ writeFileSync59(file2, r3.stdout);
206029
+ return r3.stdout;
206030
+ }
206031
+ async function settle2(platform, id, file2, notFrame) {
206032
+ let previous;
206033
+ for (let waited = 0; ; waited += settleMs) {
206034
+ const frame = await screenshot(platform, id, file2);
206035
+ if (previous && frame.equals(previous) && !(notFrame && frame.equals(notFrame))) return frame;
206036
+ if (waited >= maxWaitMs) return frame;
206037
+ previous = frame;
206038
+ await sleep(settleMs);
206039
+ }
206040
+ }
206041
+ async function open(platform, id, link) {
206042
+ if (platform === "ios") await ok3("xcrun", ["simctl", "openurl", id, link]);
206043
+ else await ok3("adb", ["-s", id, "shell", "am", "start", "-W", "-a", "android.intent.action.VIEW", "-d", `'${link.replace(/'/g, "%27")}'`]);
206044
+ }
206045
+ const capture = (async (screen, entry, variant, outDir) => {
206046
+ const frames = [];
206047
+ for (const platform of ["ios", "android"]) {
206048
+ const id = deviceFor(devices, platform, variant);
206049
+ if (!id) {
206050
+ const flag = variant === "tablet" ? `--${platform}-tablet` : `--${platform}`;
206051
+ frames.push({ surface: platform, screen, variant, skipped: { reason: `no ${platform}${variant === "tablet" ? " tablet" : ""} device passed (${flag})` } });
206052
+ continue;
206053
+ }
206054
+ if (Object.keys(entry.params ?? {}).length) {
206055
+ frames.push({ surface: platform, screen, variant, skipped: { reason: "screen params are not carried by the Player's deep link yet (#5377)" } });
206056
+ continue;
206057
+ }
206058
+ try {
206059
+ await apply(platform, id, variant);
206060
+ await open(platform, id, `himi://app/${encodeURIComponent(picker)}`);
206061
+ const file2 = join135(outDir, `${screen}.${variant}.${platform}.png`);
206062
+ const pickerFrame = await settle2(platform, id, file2);
206063
+ await open(platform, id, screenLink(opts.appId, screen, entry, opts.release));
206064
+ await settle2(platform, id, file2, pickerFrame);
206065
+ if (platform === "android" && await blackFraction(file2) > 0.95) {
206066
+ await settle2(platform, id, file2);
206067
+ if (await blackFraction(file2) > 0.95) {
206068
+ frames.push({ surface: platform, screen, variant, skipped: { reason: "black frame" } });
206069
+ continue;
206070
+ }
206071
+ }
206072
+ frames.push({ surface: platform, screen, variant, file: file2 });
206073
+ } catch (e2) {
206074
+ frames.push({ surface: platform, screen, variant, skipped: { reason: `capture failed: ${e2.message}` } });
206075
+ }
206076
+ }
206077
+ return frames;
206078
+ });
206079
+ capture.restore = async () => {
206080
+ for (const [key3, was] of saved) {
206081
+ const [platform, id] = [key3.slice(0, key3.indexOf(":")), key3.slice(key3.indexOf(":") + 1)];
206082
+ try {
206083
+ if (platform === "ios") {
206084
+ if (was.appearance) await run2("xcrun", ["simctl", "ui", id, "appearance", was.appearance]);
206085
+ if (was.language) await run2("xcrun", ["simctl", "spawn", id, "defaults", "write", "-g", "AppleLanguages", "-array", ...parsePlistArray(was.language)]);
206086
+ else await run2("xcrun", ["simctl", "spawn", id, "defaults", "delete", "-g", "AppleLanguages"]);
206087
+ await run2("xcrun", ["simctl", "terminate", id, iosBundle]);
206088
+ } else {
206089
+ if (was.appearance) await run2("adb", ["-s", id, "shell", "cmd", "uimode", "night", was.appearance]);
206090
+ await run2("adb", ["-s", id, "shell", "cmd", "locale", "set-app-locales", androidPackage, "--locales", was.language ?? ""]);
206091
+ }
206092
+ } catch {
206093
+ }
206094
+ }
206095
+ saved.clear();
206096
+ applied.clear();
206097
+ };
206098
+ return capture;
206099
+ }
206100
+ function parseNightMode(out2) {
206101
+ return out2.match(/Night mode:\s*([a-z_]+)/i)?.[1]?.toLowerCase() ?? (/yes/i.test(out2) ? "yes" : "no");
206102
+ }
206103
+ function parseAppLocales(out2) {
206104
+ return out2.match(/\[([^\]]*)\]/)?.[1]?.trim() ?? "";
206105
+ }
206106
+ function parsePlistArray(out2) {
206107
+ return out2.replace(/^\s*\(|\)\s*$/g, "").split(",").map((s2) => s2.trim().replace(/^"|"$/g, "")).filter(Boolean);
206108
+ }
206109
+ var defaultRunner3;
206110
+ var init_native = __esm({
206111
+ "src/fidelity/native.ts"() {
206112
+ init_define_HHP_CATALOG();
206113
+ defaultRunner3 = (cmd, args) => new Promise((resolve94) => {
206114
+ const child = spawn9(cmd, args, { stdio: ["ignore", "pipe", "ignore"] });
206115
+ const chunks = [];
206116
+ child.stdout.on("data", (c2) => chunks.push(c2));
206117
+ child.on("error", () => resolve94({ code: 127, stdout: Buffer.alloc(0) }));
206118
+ child.on("close", (code) => resolve94({ code: code ?? 1, stdout: Buffer.concat(chunks) }));
206119
+ });
206120
+ }
206121
+ });
206122
+
205789
206123
  // src/fidelity/post.ts
205790
206124
  var post_exports = {};
205791
206125
  __export(post_exports, {
@@ -205819,7 +206153,7 @@ __export(fidelity_exports, {
205819
206153
  });
205820
206154
  import { execFileSync as execFileSync10 } from "node:child_process";
205821
206155
  import { existsSync as existsSync122, realpathSync as realpathSync19 } from "node:fs";
205822
- import { join as join135, relative as relative26, sep as sep25 } from "node:path";
206156
+ import { join as join136, relative as relative26, sep as sep25 } from "node:path";
205823
206157
  function himalayaRepoRoot(dir) {
205824
206158
  let top;
205825
206159
  try {
@@ -205827,7 +206161,7 @@ function himalayaRepoRoot(dir) {
205827
206161
  } catch {
205828
206162
  return null;
205829
206163
  }
205830
- if (!existsSync122(join135(top, "tools/judges/fidelity/JUDGE.md"))) return null;
206164
+ if (!existsSync122(join136(top, "tools/judges/fidelity/JUDGE.md"))) return null;
205831
206165
  const rel = relative26(realpathSync19(top), realpathSync19(dir));
205832
206166
  return rel.startsWith(`..${sep25}`) || rel === ".." ? null : top;
205833
206167
  }
@@ -205860,10 +206194,41 @@ async function fidelityCommand(ctx) {
205860
206194
  return 2;
205861
206195
  }
205862
206196
  const { runFidelity: runFidelity2, prCommentBody: prCommentBody2 } = await Promise.resolve().then(() => (init_run2(), run_exports2));
205863
- const judgeDir = join135(repo, "tools/judges/fidelity");
206197
+ const judgeDir = join136(repo, "tools/judges/fidelity");
206198
+ const deviceFlags = { ios: "ios", android: "android", iosTablet: "ios-tablet", androidTablet: "android-tablet" };
206199
+ const bare = Object.values(deviceFlags).filter((f2) => flags[f2] !== void 0 && !str3(flags[f2]));
206200
+ if (bare.length) {
206201
+ io2.error(failure2("fidelity_bad_flag", `a device flag needs a UDID or serial: --${bare.join(", --")}`));
206202
+ return 2;
206203
+ }
206204
+ const bareValue = ["release", "app-id"].filter((f2) => flags[f2] !== void 0 && !str3(flags[f2]));
206205
+ if (bareValue.length) {
206206
+ io2.error(failure2("fidelity_bad_flag", `--${bareValue.join(", --")} needs a value`));
206207
+ return 2;
206208
+ }
206209
+ const devices = Object.fromEntries(Object.entries(deviceFlags).map(([k2, f2]) => [k2, flags[f2] === void 0 ? void 0 : str3(flags[f2])]));
206210
+ const named = Object.entries(devices).filter(([, id]) => id !== void 0);
206211
+ if (named.length && flags["no-native"] === true) {
206212
+ io2.error(failure2("fidelity_bad_flag", "--no-native cannot be combined with a device flag"));
206213
+ return 2;
206214
+ }
206215
+ let native;
206216
+ if (flags["no-native"] === true) native = false;
206217
+ else if (named.length) {
206218
+ const { nativeCapture: nativeCapture2 } = await Promise.resolve().then(() => (init_native(), native_exports));
206219
+ const app = (flags["app-id"] === void 0 ? void 0 : str3(flags["app-id"])) || await appId(dir);
206220
+ const release2 = flags.release === void 0 ? void 0 : str3(flags.release);
206221
+ const capture = nativeCapture2(devices, { appId: app, ...release2 ? { release: release2 } : {} });
206222
+ native = capture;
206223
+ const onSignal = () => {
206224
+ void capture.restore().finally(() => process.exit(130));
206225
+ };
206226
+ process.once("SIGINT", onSignal);
206227
+ process.once("SIGTERM", onSignal);
206228
+ }
205864
206229
  let report4;
205865
206230
  try {
205866
- report4 = await runFidelity2(dir, { judgeDir, screens: list(flags.screen), variants, ...flags["no-native"] === true ? { native: false } : {} });
206231
+ report4 = await runFidelity2(dir, { judgeDir, screens: list(flags.screen), variants, ...native !== void 0 ? { native } : {} });
205867
206232
  } catch (e2) {
205868
206233
  const code = e2.code;
205869
206234
  if (code === "fidelity_not_hhp" || code === "fidelity_unknown_screen") {
@@ -205887,7 +206252,7 @@ async function fidelityCommand(ctx) {
205887
206252
  else {
205888
206253
  for (const r3 of report4.rows) io2.log(`${r3.status.padEnd(4)} ${r3.screen}/${r3.variant} design=${r3.judge.design} parity=${r3.judge.parity}${r3.reasons.length ? ` \u2014 ${r3.reasons[0]}` : ""}`);
205889
206254
  io2.log(`
205890
- ${report4.status.toUpperCase()} \u2014 report: ${join135(report4.dir, "report.json")}`);
206255
+ ${report4.status.toUpperCase()} \u2014 report: ${join136(report4.dir, "report.json")}`);
205891
206256
  }
205892
206257
  return report4.status === "fail" ? 1 : 0;
205893
206258
  }
@@ -205977,8 +206342,8 @@ async function publishSizeCommand(ctx) {
205977
206342
  }
205978
206343
  const dir = contentDir2(flags);
205979
206344
  const { existsSync: existsSync123 } = await import("node:fs");
205980
- const { join: join136 } = await import("node:path");
205981
- if (!existsSync123(join136(dir, "dev", "index.ts"))) return fail7("build_failed", "expected a content package with dev/index.ts; pass --content <dir>");
206345
+ const { join: join137 } = await import("node:path");
206346
+ if (!existsSync123(join137(dir, "dev", "index.ts"))) return fail7("build_failed", "expected a content package with dev/index.ts; pass --content <dir>");
205982
206347
  const declared = await appId2(dir);
205983
206348
  const app = typeof flags.app === "string" ? flags.app : declared;
205984
206349
  if (!isValidAppId(app)) return fail7("invalid_usage", "declared app is not a valid publish ID; pass --app <valid-slug>", { app }, 2);
@@ -206065,7 +206430,7 @@ init_define_HHP_CATALOG();
206065
206430
  init_client();
206066
206431
  init_config();
206067
206432
  import { rmSync as rmSync42 } from "node:fs";
206068
- import { spawn as spawn9 } from "node:child_process";
206433
+ import { spawn as spawn10 } from "node:child_process";
206069
206434
  import { homedir as homedir16 } from "node:os";
206070
206435
  import { resolve as resolve93 } from "node:path";
206071
206436
  import { fileURLToPath as fileURLToPath39 } from "node:url";
@@ -206423,7 +206788,7 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
206423
206788
  at its catalog viewport width (himi surfaces lists them);
206424
206789
  a widget family is NOT a device \u2014 fetch /v1/widget/<widgetId>;
206425
206790
  --rtl sets ambient direction (dir=rtl + $env:isRTL)
206426
- himi fidelity [--content <dir>] [--screen <id,\u2026>] [--variant <light,dark,rtl,tablet>] [--no-native] [--json] [--post-pr <n>]
206791
+ himi fidelity [--content <dir>] [--screen <id,\u2026>] [--variant <light,dark,rtl,tablet>] [--ios <udid>] [--android <serial>] [--ios-tablet <udid>] [--android-tablet <serial>] [--app-id <id>] [--release <id>] [--no-native] [--json] [--post-pr <n>]
206427
206792
  does each HHP screen look like its mockup? judge + SSIM per screen \xD7 variant (repo-only)
206428
206793
  himi icons [--json] the valid Icon.name vocabulary (offline)
206429
206794
  himi tokens kit [<name>] list the shipped design kits, or print one. himi docs
@@ -207086,7 +207451,7 @@ var COMMANDS = [
207086
207451
  { name: "progress", summary: 'the build JOURNAL an agent keeps for the owner while it works. `himi progress "<what changed>" --step <1-8|name> --image <png>` appends one postcard to .himi/progress.jsonl \u2014 the step is one of the customer journey\'s eight (discover, classify, plan, implement, brand, validate, review, download) \u2014 and COPIES each image, content-addressed, into .himi/history/, because .himi/shots/ is overwritten by every render and cleared by every full `himi test`, so a path into it would later show a different picture than the one the owner was sent. --image repeats. With NO --image, the home screen\'s current shot is attached automatically, only when it is no older than the last source change and not the picture the latest journaled postcard already shows \u2014 and never a shot that landed on loading placeholders, an empty or an error state (.himi/shot-states.json): then the newest home picture of the current source that shows content goes on instead (its .himi/history copy, `imageNote` saying so), or none \u2014 except on the HANDOVER card (`--step review`), where a repeat is the point and it attaches anyway; that card\'s output also names the making-of (`makingOf`, .himi/making-of.html, rebuilt when missing or older than the journal so it ends on this card) and its `next` says to hand it to the owner beside the preview link; `imageAttached` in the output says whether the postcard carries a picture (with `imageNote` saying why not, or which picture went on instead), and `--no-image` sends a text-only postcard. --notify also raises a best-effort desktop notification for an owner who stepped away. A bare `himi progress` prints the journal with the current step and elapsed time \u2014 also how an agent recovers its place after its context was compacted. APPROVALS: `--approve <gate> "<what>"` records one the customer just gave (gates: site-create, connect, private-data, draft, promote, push-signing, qa-build) in .himi/approvals.jsonl, and `--done <gate>` / `--drop <gate>` closes it \u2014 an approval is owed until then, and the chat that held it does not survive a new session. `--brief` prints plain text for a session start: the current step, the last postcard, every open approval, and whether this app\'s agent files are stale \u2014 the SessionStart hook `himi agent-init` installs runs exactly this. The CLI also journals milestones by itself \u2014 the first PNG render of the home screen (for an app a current `himi init` started: once as the template\'s "Starting point" while the source is what init wrote, then once as "First look at your app" after it diverges, Markdown edits not counting; once per app otherwise), a `himi test` crawl whose outcome changed since the last one (its counts or its home picture; an identical re-run writes none, and a repeat names what changed: "The home screen changed since the last crawl." or the changed counts, was \u2192 now), every draft push (with the home picture only when that shot is newer than the last source edit \u2014 otherwise the postcard says why and names `himi render <home> --png`), and ONE rule for a long silence: a successful home `render --png` more than ten minutes after the newest postcard of any kind, with a picture the latest postcard does not show, journals "Still building: here\'s the home screen now." with it (never twice within the window). When a crawl\'s or a draft\'s home picture is the first of an app whose source diverged, "First look at your app" is journaled first, from that shot, once. All are marked `"by": "himi"`; they never move the journey step, carry no link or credential, and step aside (folded out of every reader) when the agent journals the same step or picture within five minutes. Local only: nothing is sent anywhere.', required: '["<message>" [--step <1-8|name>] [--image <png>]\u2026 [--no-image] [--notify]] [--content <dir>] | --approve|--done|--drop <gate> ["<note>"] | --brief' },
207087
207452
  { name: "studio", summary: "open the BUILD BOARD \u2014 one local page the owner can leave open while an agent builds their app, which fills itself in: every screen as it renders (NEW/CHANGED badges, and each earlier version kept for a filmstrip), the journey stepper and the latest postcard from `himi progress`, the validate/test/UX/draft checks as they run, the site next to the app's home screen with its palette, and the navigation the test crawl followed. Writes .himi/studio/index.html and opens it; the page re-reads its state every two seconds, so nothing needs to be served. OPT-IN BY EXISTENCE: until this command (or `himi init`, which makes the board for every app it creates, without opening a browser) has run, no other command touches .himi/studio/ \u2014 after it, render/test/validate/push/icon/site/ux/progress each refresh the board as they finish (best-effort; never fails a command). Every image is a content-addressed copy in .himi/history/, never a live path into .himi/shots/. Local only, no secrets: it never reads a preview token, handover code or control token. --no-open prints the path instead (SSH, CI). `himi studio reel` writes the MAKING-OF: one self-contained HTML file (every image inlined, nothing fetched) with a flipbook of the home screen version by version, captioned by the postcard in effect at the time, the final screens, and every postcard \u2014 sized to stay attachable, thinning frames first. Defaults to .himi/making-of.html (never archived by push), which every successful `himi push --draft` also rebuilds and names as `reel` in its output; --out to put it elsewhere. The output names that file as the one to hand the owner (`handover: {file, hint}` in JSON; two plain lines at a terminal) \u2014 beside the preview link, never instead of it.", required: "[--content <dir>] [--no-open] | reel [--out <file>] [--no-open]" },
207088
207453
  { name: "render", summary: "headless-render a screen with the web renderer: render-tree + diagnostics, optional --png [<file>] (bare: .himi/shots/<id>.png, what the build board shows), --scheme dark, --tree. Local by default (content dir). Pass --remote --app --release to hit GET /v1/preview/\u2026/render on the serve plane after a push. Boots the worker so $service bindings resolve; --static raw bindings; --live real Wix fetches; --mocks (the DEFAULT whenever the app has recorded data; --no-mocks for the offline initial state) runs load() against the app's own recorded data (dev/net-mocks.json from `himi test --record`, then the config's cms seed rows, and the Base44 layer `himi test` uses \u2014 base44.entities seed rows and the analyze book's sample rows \u2014 hermetic, never a request; with neither it paints the initial state, and `mocks: {used: false}` says why), which is what the automatic pictures use, so they show the app's content instead of its offline placeholders; --state injects worker state; --input supplies pre-boot navigation params; --width/--device for PNG viewport; --rtl for dir=rtl + $env:isRTL.", required: "<screenId> [--content <dir>] [--scheme dark] [--png [<file>]] [--tree] [--static] [--live] [--mocks | --no-mocks] [--state <json|file>] [--input <json|file>] [--width <px> | --device <name>] [--rtl] [--remote --app --release]" },
207089
- { name: "fidelity", summary: "does each HHP screen look like the mockup it was designed from? Per screen and variant (light, dark, rtl, tablet) it captures the free-HTML mockup (hhp.json `mockup`) and the web render with the app's recorded data, scores mockup\u2194web (ImageMagick SSIM, global + worst tile), and asks a fresh model (tools/judges/fidelity) two questions: design (web vs mockup) and parity (native vs web, once native capture lands, #4935). pass/warn/fail per row; exit 1 if any row fails. Writes .himi/fidelity/<run>/report.json and a strip PNG per row. --post-pr <n> keeps one plain PR comment up to date. Advisory, not a merge gate; repo-only for now (refuses with code fidelity_outside_repo elsewhere).", required: "[--content <dir>] [--screen <id,\u2026>] [--variant <light,dark,rtl,tablet>] [--no-native] [--json] [--post-pr <n>]" },
207454
+ { name: "fidelity", summary: "does each HHP screen look like the mockup it was designed from? Per screen and variant (light, dark, rtl, tablet) it captures the free-HTML mockup (hhp.json `mockup`) and the web render with the app's recorded data, scores mockup\u2194web (ImageMagick SSIM, global + worst tile), and asks a fresh model (tools/judges/fidelity) two questions: design (web vs mockup) and parity (native vs web). Native frames come from Himi Player on the devices passed with --ios/--android (and --ios-tablet/--android-tablet for the tablet variant), opened by `himi://app/<app-id>/screen/<screen>`; only those devices are touched, their appearance and language are restored afterwards, and a variant with no device is reported skipped. pass/warn/fail per row; exit 1 if any row fails. Writes .himi/fidelity/<run>/report.json and a strip PNG per row. --post-pr <n> keeps one plain PR comment up to date. Advisory, not a merge gate; repo-only for now (refuses with code fidelity_outside_repo elsewhere).", required: "[--content <dir>] [--screen <id,\u2026>] [--variant <light,dark,rtl,tablet>] [--ios <udid>] [--android <serial>] [--ios-tablet <udid>] [--android-tablet <serial>] [--app-id <id>] [--release <id>] [--no-native] [--json] [--post-pr <n>]" },
207090
207455
  { name: "l10n", summary: "import translations into per-app RFC 0054 catalogs (`apps/<app>/l10n/<tag>.json`). `import --format oneapp <dir>` converts OneApp `messages_<locale>.json` files (flat OneApp key \u2192 ICU MessageFormat string): the OneApp key becomes the catalog row's `key` (what a descriptor references as `$t:<KEY>`), the row id is the extractor's content hash of the source-locale string, and `{{x}}` placeholders become ICU `{x}`. Messages using constructs outside the supported ICU subset (selectordinal, number/date skeletons, plural offset, HTML tags) are kept VERBATIM and listed in the report with key and locale. Merges with an existing catalog \u2014 manual translations and keys survive unless --overwrite; rows the import does not touch survive always. Default locales: every messages_<tag>.json found; --source (default en) names the locale supplying each row's `source` string. Warns when the app's `locale` config does not declare an imported tag, printing the config line to add. Deterministic output: re-running over the same inputs produces no diff.", required: "import --format oneapp <dir> (--app <slug> | --content <app dir>) [--locales en,he] [--source en] [--overwrite] [--json]" },
207091
207456
  { name: "cms", summary: "inspect, provision, and seed the app's CMS (Wix Data) collections, declared as a `cms` block in config.ts. `query` is READ-ONLY \u2014 by default it fetches ONLY the physical ids `config.ts` declares (item counts + schema drift) and reports `siteScanned: false`; it never lists the site, so an UNDECLARED `himi_<app>_` collection this app owns is not reached without `--all`. `--all` SCANS the site \u2014 every collection it lists is labeled `owned`/`adopted`/`foreign`, and it describes every foreign one \u2014 id, non-system field keys + types, and the site's read role \u2014 so an app with no `cms` block at all can DISCOVER a collection to adopt (`--all` is the one case where a missing `cms` block is not 'nothing to do'). Foreign collections are described up to a cap and are never item-counted (`itemCount: null`), because a row count is one query each; `foreign.count` is how many the scan LISTED \u2014 it follows the list endpoint's pages, and `listingIncomplete: true` says when the walk stopped short of the end, with `unlistedCollections` counting what it left out when Wix gave a paging total \u2014 and `foreign.truncated` says how many of those went undescribed, each still NAMED in `foreign.others` (id + displayName), so a collection the site already has is never hidden behind the cap. The scan is OWNER-scoped \u2014 it needs --site <msid> (or WIX_SITE_ID) plus owner auth via `wix token`; a visitor token can read a public collection's ITEMS but can never enumerate collections, so there is no tokenless discovery path. `create` reconciles declared collections additively and idempotently, and REFUSES any id outside the app's `himi_<app>_` namespace, so it is safe against a production site. It reports declared-vs-site `permissionDrift` but never writes permissions unless asked with --apply-permissions, so a field reconcile can't silently revert access control changed in the dashboard. `seed` inserts the git-tracked `seed:` rows into EMPTY owned collections (--force to add to a non-empty one; --ai writes placeholder rows plus a refinement prompt for you to review). Adopted collections (declared by `wixId`) are read-only and never created, updated, or seeded. Site from --site <msid> or WIX_SITE_ID; owner auth via `wix token`.", required: "query|create|seed [--site <msid>] [--content <dir>] [--all] [--dry-run] [--force] [--allow-destructive] [--apply-permissions] [--only <id>] [--ai] [--count <n>]" },
207092
207457
  { name: "data", summary: "read-only plan or explicitly provision/seed declared KV and Wix Data resources. Preview and production are separate scopes. KV administration uses the operator data plane from --data-url/HIMI_DATA_ADMIN_URL and HIMI_DATA_ADMIN_TOKEN; draft push never calls it.", required: "plan|create|seed [--site <msid>] [--environment preview|production] [--content <dir>] [--data-url <url>] [--dry-run] [--force]" },
@@ -209715,8 +210080,8 @@ function pushAuthHint(status, token, target, hasOwnerIssuer = true) {
209715
210080
  }
209716
210081
  async function contentVisibility(dir) {
209717
210082
  try {
209718
- const { readFileSync: readFileSync141 } = await import("node:fs");
209719
- const m2 = JSON.parse(readFileSync141(join96(dir, "himalaya.content.json"), "utf8"));
210083
+ const { readFileSync: readFileSync142 } = await import("node:fs");
210084
+ const m2 = JSON.parse(readFileSync142(join96(dir, "himalaya.content.json"), "utf8"));
209720
210085
  if (m2.visibility === "public" || m2.visibility === "unlisted") return m2.visibility;
209721
210086
  } catch {
209722
210087
  }
@@ -215835,7 +216200,7 @@ init_owner_claim();
215835
216200
  init_wix_placeholders();
215836
216201
  var defaultIO = { log: (l2) => console.log(l2), error: (l2) => console.error(l2), isTTY: process.stdout.isTTY === true };
215837
216202
  var CLI_ENTRY = fileURLToPath39(import.meta.url);
215838
- function createUpdateRefreshSpawner(spawnFn = spawn9, execArgv = process.execArgv) {
216203
+ function createUpdateRefreshSpawner(spawnFn = spawn10, execArgv = process.execArgv) {
215839
216204
  return () => {
215840
216205
  try {
215841
216206
  const child = spawnFn(process.execPath, [...loaderArgs(execArgv), fileURLToPath39(import.meta.url), UPDATE_REFRESH_COMMAND], {
@@ -215852,7 +216217,7 @@ function createUpdateRefreshSpawner(spawnFn = spawn9, execArgv = process.execArg
215852
216217
  };
215853
216218
  }
215854
216219
  var spawnUpdateRefresh = createUpdateRefreshSpawner();
215855
- function createTrimValidator(deps = {}, spawnFn = spawn9) {
216220
+ function createTrimValidator(deps = {}, spawnFn = spawn10) {
215856
216221
  const outcome = (code, report4, said) => {
215857
216222
  let counts;
215858
216223
  let errors;
Binary file
package/guides/hhp.md CHANGED
@@ -210,13 +210,13 @@ Declare the mockup each screen was designed from in `hhp.json`:
210
210
  A screen may also carry `params` (the navigation input a device opens it with, used by native capture, #4935) and `fidelity: { "warn": 80, "fail": 60 }`. Thresholds can also be set app-wide at the top level.
211
211
 
212
212
  ```bash
213
- himi fidelity --content <dir> [--screen a,b] [--variant light,dark,rtl,tablet] [--no-native] [--json] [--post-pr <n>]
213
+ himi fidelity --content <dir> [--screen a,b] [--variant light,dark,rtl,tablet] [--ios <udid>] [--android <serial>] [--ios-tablet <udid>] [--android-tablet <serial>] [--app-id <id>] [--release <id>] [--no-native] [--json] [--post-pr <n>]
214
214
  ```
215
215
 
216
216
  For each screen and variant (`light`, `dark`, `rtl`, `tablet`), the command does four things:
217
217
 
218
218
  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.
219
- 2. **Captures the web render** from the app's recorded data, as `himi render` does by default. Native frames come through a capture seam. Until #4935 lands, the seam reports iOS and Android as skipped.
219
+ 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 Hebrew 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>` the Player shows the app's **live** release, not your working copy. 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.
220
220
  3. **Scores mockup↔web** with ImageMagick SSIM, globally and per tile. The worst tile says where the screens differ most.
221
221
  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.
222
222
  - **Design:** does WEB look like the MOCKUP? This judges the port.
@@ -203,6 +203,42 @@ routes: [
203
203
  A notification whose link matches no route opens the app without navigating. `himi validate`
204
204
  warns when a declared route names a screen the app does not declare.
205
205
 
206
+ #### Links from a legacy link space
207
+
208
+ An app that replaces an older one may still receive that app's links: its old custom scheme
209
+ (`legacyapp://widget/42`) or an old web host (`https://legacy.example.com/app/widget/42`).
210
+ Declare those prefixes and route the rest:
211
+
212
+ ```ts
213
+ push: {
214
+ linkPrefixes: ["legacyapp://", "https://legacy.example.com/app/"],
215
+ routes: [{ match: "widget/:id", link: "<scheme>://screen/<widget-screen>?id={id}" }],
216
+ },
217
+ ```
218
+
219
+ Before matching, the device strips a configured prefix from the link, so `widget/42` is matched
220
+ against the routes. It applies these rules in order:
221
+
222
+ 1. With `payloadAdapter: "wix-ping"`, a Wix link is handled first, so a prefix inside the Wix link
223
+ space never applies.
224
+ 2. Otherwise the **longest** matching prefix is stripped. When a prefix starts with a scheme
225
+ (`legacyapp://`, `https://…`), its scheme and host match in any case and the rest matches
226
+ exactly. A prefix with no scheme (`legacy.example.com/app/`, `/app/`) matches exactly.
227
+ 3. A prefix only matches up to a `/`, `?` or `#`, so `https://old.example.com/m` does not strip
228
+ `https://old.example.com/mobile/…`.
229
+ 4. A relative link (`/widget/42`) is matched as is, with no prefix needed.
230
+ 5. Any other link passes through unchanged.
231
+
232
+ `himi validate` warns about a prefix that can never apply: one inside the Wix link space under
233
+ `wix-ping`, or a prefix list with no routes. It does not judge a prefix by its shape. A schemeless
234
+ prefix strips links written without a scheme, and a dotted custom scheme strips its own links.
235
+
236
+ With `wix-ping` and no `routes`, the build derives the routes from the site's verticals. A
237
+ stripped link is then matched against those Wix notification paths only
238
+ (`my-account/:site/stores/orders/:orderId`, …). If your legacy links use paths of their own,
239
+ declare `routes` for them. A declared table replaces the derived one, so list the Wix routes you
240
+ still need there too. `himi validate` warns when prefixes meet only derived routes.
241
+
206
242
  ### Notification action buttons
207
243
 
208
244
  Declare actionable categories in the app's source `config.ts`. The native shells render one
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1603.0",
3
+ "version": "0.1604.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": "3b93e9df5b17cf56b074bfe86e5cb5221719e281d1b61b77363bb6df"
75
+ "falconPackageHash": "d9895ee47dde266d1046d74260972f2c9428e600431befd9b0e4ddc6"
76
76
  }