@wix/himalaya-cli 0.1036.0 → 0.1038.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.
Files changed (117) hide show
  1. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/genie-chat/src/index.d.ts +119 -0
  2. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/_types/stdlib/wix-sdk/portfolio/src/index.d.ts +46 -9
  3. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-genie-chat.mjs +51 -1
  4. package/dist/authoring-runtime/node_modules/@wix/himalaya/dist/wix-portfolio.mjs +14 -2
  5. package/dist/cli.mjs +1323 -492
  6. package/dist/preview-assets.generated.bin +0 -0
  7. package/fixtures/wix-canned.json +51 -2
  8. package/guides/headless-loop.md +7 -0
  9. package/guides/himi-test.md +66 -1
  10. package/guides/sdk-reference.json +90 -12
  11. package/guides/trim-a-template.md +5 -0
  12. package/guides/wix-business-context.md +13 -1
  13. package/guides/wix-site-to-himi-app.md +10 -1
  14. package/package.json +2 -2
  15. package/templates/beauty-salon/tier5-src/himi-booking-bookings/index.ts +14 -8
  16. package/templates/beauty-salon/tier5-src/himi-branded-lite-home/index.ts +12 -1
  17. package/templates/beauty-salon-owner/dev/index.ts +2148 -1262
  18. package/templates/beauty-salon-owner/tier5-src/_shared/aria-chat.ts +338 -26
  19. package/templates/beauty-salon-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  20. package/templates/beauty-salon-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  21. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  22. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  23. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  24. package/templates/beauty-salon-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  25. package/templates/branded-lite/tier5-src/himi-booking-bookings/index.ts +14 -8
  26. package/templates/branded-lite/tier5-src/himi-branded-lite-home/index.ts +12 -1
  27. package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
  28. package/templates/branded-lite/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
  29. package/templates/community-club/tier5-src/himi-branded-lite-home/index.ts +12 -1
  30. package/templates/community-club-owner/dev/index.ts +2151 -1265
  31. package/templates/community-club-owner/tier5-src/_shared/aria-chat.ts +338 -26
  32. package/templates/community-club-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  33. package/templates/community-club-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  34. package/templates/community-club-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  35. package/templates/community-club-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  36. package/templates/community-club-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  37. package/templates/events-meetups/tier5-src/himi-branded-lite-home/index.ts +12 -1
  38. package/templates/events-meetups-owner/dev/index.ts +2151 -1265
  39. package/templates/events-meetups-owner/tier5-src/_shared/aria-chat.ts +338 -26
  40. package/templates/events-meetups-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  41. package/templates/events-meetups-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  42. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  43. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  44. package/templates/events-meetups-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  45. package/templates/gym-fitness/tier5-src/himi-booking-bookings/index.ts +14 -8
  46. package/templates/gym-fitness/tier5-src/himi-branded-lite-home/index.ts +12 -1
  47. package/templates/gym-fitness-owner/dev/index.ts +2148 -1262
  48. package/templates/gym-fitness-owner/tier5-src/_shared/aria-chat.ts +338 -26
  49. package/templates/gym-fitness-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  50. package/templates/gym-fitness-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  51. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  52. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  53. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  54. package/templates/gym-fitness-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  55. package/templates/nonprofit-cause/tier5-src/himi-branded-lite-home/index.ts +12 -1
  56. package/templates/nonprofit-cause-owner/dev/index.ts +2148 -1262
  57. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-chat.ts +338 -26
  58. package/templates/nonprofit-cause-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  59. package/templates/nonprofit-cause-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  60. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  61. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  62. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  63. package/templates/nonprofit-cause-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  64. package/templates/owner-lite/dev/index.ts +2145 -1259
  65. package/templates/owner-lite/tier5-src/_shared/aria-chat.ts +338 -26
  66. package/templates/owner-lite/tier5-src/_shared/aria-mentions.ts +202 -0
  67. package/templates/owner-lite/tier5-src/_shared/owner-module-data.ts +2 -2
  68. package/templates/owner-lite/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  69. package/templates/owner-lite/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  70. package/templates/owner-lite/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  71. package/templates/owner-lite/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  72. package/templates/podcast-creator/tier5-src/himi-branded-lite-home/index.ts +12 -1
  73. package/templates/podcast-creator-owner/dev/index.ts +2151 -1265
  74. package/templates/podcast-creator-owner/tier5-src/_shared/aria-chat.ts +338 -26
  75. package/templates/podcast-creator-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  76. package/templates/podcast-creator-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  77. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  78. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  79. package/templates/podcast-creator-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  80. package/templates/portfolio-creative/tier5-src/himi-branded-lite-home/index.ts +12 -1
  81. package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio/index.ts +2 -2
  82. package/templates/portfolio-creative/tier5-src/himi-branded-lite-portfolio-project/index.ts +3 -3
  83. package/templates/portfolio-creative-owner/dev/index.ts +2148 -1262
  84. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-chat.ts +338 -26
  85. package/templates/portfolio-creative-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  86. package/templates/portfolio-creative-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  87. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  88. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  89. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  90. package/templates/portfolio-creative-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  91. package/templates/restaurant/tier5-src/himi-branded-lite-home/index.ts +12 -1
  92. package/templates/restaurant-owner/dev/index.ts +2148 -1262
  93. package/templates/restaurant-owner/tier5-src/_shared/aria-chat.ts +338 -26
  94. package/templates/restaurant-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  95. package/templates/restaurant-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  96. package/templates/restaurant-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  97. package/templates/restaurant-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  98. package/templates/restaurant-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  99. package/templates/restaurant-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  100. package/templates/retail-store/tier5-src/himi-branded-lite-home/index.ts +12 -1
  101. package/templates/retail-store-owner/dev/index.ts +2151 -1265
  102. package/templates/retail-store-owner/tier5-src/_shared/aria-chat.ts +338 -26
  103. package/templates/retail-store-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  104. package/templates/retail-store-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  105. package/templates/retail-store-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  106. package/templates/retail-store-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  107. package/templates/retail-store-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
  108. package/templates/yoga-studio/tier5-src/himi-booking-bookings/index.ts +14 -8
  109. package/templates/yoga-studio/tier5-src/himi-branded-lite-home/index.ts +12 -1
  110. package/templates/yoga-studio-owner/dev/index.ts +2148 -1262
  111. package/templates/yoga-studio-owner/tier5-src/_shared/aria-chat.ts +338 -26
  112. package/templates/yoga-studio-owner/tier5-src/_shared/aria-mentions.ts +202 -0
  113. package/templates/yoga-studio-owner/tier5-src/_shared/owner-module-data.ts +2 -2
  114. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-appointments/index.ts +4 -1
  115. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-aria/index.ts +29 -0
  116. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-ask/index.ts +6 -0
  117. package/templates/yoga-studio-owner/tier5-src/himi-owner-lite-manage/index.ts +64 -6
package/dist/cli.mjs CHANGED
@@ -1123,6 +1123,14 @@ var init_mobile_ux_contract = __esm({
1123
1123
  | Screen | Job | Primary action | Data | Notes |
1124
1124
  | --- | --- | --- | --- | --- |
1125
1125
 
1126
+ ## Data access
1127
+ <!-- \`himi test\` checks every write to Wix it sees against this table. One row per screen id (and
1128
+ role); \`Wix writes\` is \`none\`, or the approved writes named by action (\`bookSelected\`),
1129
+ endpoint (\`POST /bookings/v2/bookings\`), capability (\`registerDirect\`) or kind (\`payment\`).
1130
+ \`himi docs himi-test\` -> "Writes to Wix". -->
1131
+ | Screen | Role | Reads | Wix writes | Prohibited |
1132
+ | --- | --- | --- | --- | --- |
1133
+
1126
1134
  ## Platform envelope
1127
1135
  - Required: iOS, Android
1128
1136
  - Optional: web, desktop, watch, widgets
@@ -1528,7 +1536,9 @@ export default defineService("home", {
1528
1536
  surfaceTertiary: { light: "#eef1f4", dark: "#1c2128" },
1529
1537
  textPrimary: { light: "#1f2328", dark: "#e6edf3" },
1530
1538
  textSecondary: { light: "#57606a", dark: "#9198a1" },
1531
- textOnAccent: { light: "#ffffff", dark: "#ffffff" },
1539
+ // Dark ink on the dark arm: white on #388bfd is 3.34:1 and fails AA, which the tokens
1540
+ // pass now warns about (#3275); #0d1117 on it is 5.66:1.
1541
+ textOnAccent: { light: "#ffffff", dark: "#0d1117" },
1532
1542
  accentPrimary: { light: "#0969da", dark: "#388bfd" },
1533
1543
  border: { light: "#d0d7de", dark: "#30363d" },
1534
1544
  danger: { light: "#cf222e", dark: "#f85149" },
@@ -3056,14 +3066,14 @@ function confinementMatcher(contentRoot, extraRoots = []) {
3056
3066
  linked = [];
3057
3067
  const scanned = /* @__PURE__ */ new Set();
3058
3068
  const queue = [...roots];
3059
- const CAP4 = 4096;
3069
+ const CAP5 = 4096;
3060
3070
  const take = (abs) => {
3061
3071
  const target = realpathOrSelf(abs);
3062
- if (scanned.has(target) || linked.length >= CAP4) return;
3072
+ if (scanned.has(target) || linked.length >= CAP5) return;
3063
3073
  linked.push(target);
3064
3074
  queue.push(target);
3065
3075
  };
3066
- while (queue.length && linked.length < CAP4) {
3076
+ while (queue.length && linked.length < CAP5) {
3067
3077
  const r = queue.shift();
3068
3078
  for (const nm of [r, join12(r, "node_modules")]) {
3069
3079
  if (scanned.has(nm)) continue;
@@ -8206,8 +8216,8 @@ function findSkipDerive(bundleJs) {
8206
8216
  if (touchedBase.length === 0) continue;
8207
8217
  const missingDerived = [...outputUnion].filter((o) => !keySet.has(o));
8208
8218
  if (missingDerived.length === 0) continue;
8209
- const snippet2 = objBody.replace(/\s+/g, " ").trim();
8210
- findings.push({ touchedBase, missingDerived, snippet: `{ ${snippet2.slice(0, 70)}${snippet2.length > 70 ? " \u2026" : ""} }` });
8219
+ const snippet3 = objBody.replace(/\s+/g, " ").trim();
8220
+ findings.push({ touchedBase, missingDerived, snippet: `{ ${snippet3.slice(0, 70)}${snippet3.length > 70 ? " \u2026" : ""} }` });
8211
8221
  }
8212
8222
  return findings;
8213
8223
  } catch {
@@ -8812,8 +8822,8 @@ function validateScreenDescriptor(screenId, descriptor, opts = {}) {
8812
8822
  if (a11yMode !== "off") {
8813
8823
  const severity = a11yMode === "warn" && !strict ? "warning" : "error";
8814
8824
  const gaps = accessibleNameGaps(descriptor);
8815
- const CAP4 = 20;
8816
- for (const gap of gaps.slice(0, CAP4)) {
8825
+ const CAP5 = 20;
8826
+ for (const gap of gaps.slice(0, CAP5)) {
8817
8827
  const row = Object.prototype.hasOwnProperty.call(A11Y_CONTRACT, gap.type) ? A11Y_CONTRACT[gap.type] : void 0;
8818
8828
  const why = gap.reason === "interactive" ? `it carries an \`onTap\`/\`onLongPress\`/\`onDoubleTap\`, which promotes ANY node to a control that owes a name (RFC 0058 \xA73.4)` : `RFC 0058 \xA73.3 requires a name for \`${gap.type}\` (announced as \`${gap.role}\`)`;
8819
8829
  const fix = gap.reason === "contract" ? `Give it \`modifiers.accessibilityLabel\`, or copy on a descendant \u2014 or, if it really is presentational, say so with \`modifiers.accessibilityRole: "decorative"\` (node-local) or \`modifiers.accessibilityHidden: true\` (whole subtree).` : `Give it \`modifiers.accessibilityLabel\`, or put readable copy on a descendant. An icon-only control names itself with \`Icon.contentDescription\`. \`decorative\` does not silence a control${row?.decorative ? ` \u2014 a tappable \`${gap.type}\` is one` : ""}.`;
@@ -8824,11 +8834,11 @@ function validateScreenDescriptor(screenId, descriptor, opts = {}) {
8824
8834
  message: `${gap.type} renders no accessible name anywhere in its subtree, and ${why}. A screen reader announces this node as an unnamed \`${gap.role}\`. ${fix}`
8825
8835
  });
8826
8836
  }
8827
- if (gaps.length > CAP4) {
8837
+ if (gaps.length > CAP5) {
8828
8838
  issues.push({
8829
8839
  severity,
8830
8840
  pass: "a11y",
8831
- message: `+${gaps.length - CAP4} more node(s) on this screen render no accessible name (${gaps.length} in total). Fix the ${CAP4} above and re-run to see the rest.`
8841
+ message: `+${gaps.length - CAP5} more node(s) on this screen render no accessible name (${gaps.length} in total). Fix the ${CAP5} above and re-run to see the rest.`
8832
8842
  });
8833
8843
  }
8834
8844
  }
@@ -11088,6 +11098,74 @@ var init_token_colors = __esm({
11088
11098
  }
11089
11099
  });
11090
11100
 
11101
+ // ../../core/ts/src/idiom.ts
11102
+ function authored(overlays, name) {
11103
+ return typeof name === "string" && overlays !== void 0 && Object.hasOwn(overlays, name) && overlays[name] !== null && typeof overlays[name] === "object" && !Array.isArray(overlays[name]);
11104
+ }
11105
+ function selectIdiomOverlay(doc, ctx = {}) {
11106
+ const policy = doc.idiomPolicy;
11107
+ const overlays = doc.idiom;
11108
+ const isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
11109
+ if (!isRecord3(policy) || !isRecord3(overlays)) return void 0;
11110
+ const kind = policy.policy ?? "follow-surface";
11111
+ if (kind === "none") return void 0;
11112
+ if (kind === "pinned") return authored(overlays, policy.pin) ? policy.pin : void 0;
11113
+ if (kind !== "follow-surface") return void 0;
11114
+ const surfaces = policy.surfaces;
11115
+ const candidates = [
11116
+ ctx.surface !== void 0 && isRecord3(surfaces) && Object.hasOwn(surfaces, ctx.surface) ? surfaces[ctx.surface] : void 0,
11117
+ ...ctx.native ?? [],
11118
+ policy.fallback
11119
+ ];
11120
+ return candidates.find((c) => authored(overlays, c));
11121
+ }
11122
+ function isPlainObject(v) {
11123
+ return v !== null && typeof v === "object" && !Array.isArray(v);
11124
+ }
11125
+ function deepMerge(base, overlay) {
11126
+ if (!isPlainObject(overlay) || !isPlainObject(base)) return overlay;
11127
+ const out = { ...base };
11128
+ for (const [key2, value] of Object.entries(overlay)) {
11129
+ if (UNSAFE_KEYS.has(key2)) continue;
11130
+ out[key2] = Object.hasOwn(base, key2) ? deepMerge(base[key2], value) : value;
11131
+ }
11132
+ return out;
11133
+ }
11134
+ function mergeIdiomOverlay(base, overlay) {
11135
+ const merged = deepMerge(base, overlay);
11136
+ const baseColors = base.colors;
11137
+ const overlayColors = overlay.colors;
11138
+ if (isPlainObject(baseColors) && isPlainObject(overlayColors)) {
11139
+ const colors = { ...merged.colors };
11140
+ for (const [name, value] of Object.entries(overlayColors)) {
11141
+ if (UNSAFE_KEYS.has(name)) continue;
11142
+ const was = Object.hasOwn(baseColors, name) ? baseColors[name] : void 0;
11143
+ if (typeof was === "string" && isPlainObject(value)) colors[name] = { light: was, ...value };
11144
+ }
11145
+ merged.colors = colors;
11146
+ }
11147
+ return merged;
11148
+ }
11149
+ function resolveIdiom(tokens, ctx = {}) {
11150
+ const doc = tokens;
11151
+ const name = selectIdiomOverlay(doc, ctx);
11152
+ if (name === void 0) return tokens;
11153
+ const merged = mergeIdiomOverlay(tokens, doc.idiom[name]);
11154
+ const { idiomPolicy: _applied, ...final } = merged;
11155
+ return final;
11156
+ }
11157
+ var IDIOM_CONTEXTS, UNSAFE_KEYS;
11158
+ var init_idiom = __esm({
11159
+ "../../core/ts/src/idiom.ts"() {
11160
+ IDIOM_CONTEXTS = {
11161
+ ios: { native: ["ios"] },
11162
+ android: { native: ["material"] },
11163
+ mac: { native: ["desktop", "ios"], surface: "mac" }
11164
+ };
11165
+ UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
11166
+ }
11167
+ });
11168
+
11091
11169
  // src/token-lint.ts
11092
11170
  import { existsSync as existsSync13, readFileSync as readFileSync14 } from "node:fs";
11093
11171
  import { join as join16 } from "node:path";
@@ -11178,11 +11256,109 @@ function tokenColorIssues(dir, tokensPathOverride) {
11178
11256
  `${tokensPath}: colors.${problem.path} is ${problem.value} \u2014 the native runtimes parse only #RRGGBB or #RRGGBBAA` + (problem.suggestion ? `. Write ${problem.suggestion}.` : `, and this value names no colour that can be converted for you. Write the hex it should be.`)
11179
11257
  );
11180
11258
  }
11259
+ const base = contrastFindings(colors);
11260
+ for (const f of base) warn(`${tokensPath}: ${f.render(f.schemes)}`);
11261
+ if (idiom !== null && typeof idiom === "object" && !Array.isArray(idiom)) {
11262
+ const seen = new Set(base.flatMap((f) => f.schemes.map((s) => `${f.key}|${s}`)));
11263
+ for (const [overlay, layer] of Object.entries(idiom)) {
11264
+ const layerColors = layer?.colors;
11265
+ if (layerColors === null || typeof layerColors !== "object" || Array.isArray(layerColors)) continue;
11266
+ const merged = mergeIdiomOverlay({ colors }, { colors: layerColors }).colors;
11267
+ for (const f of contrastFindings(merged)) {
11268
+ const added = f.schemes.filter((s) => !seen.has(`${f.key}|${s}`));
11269
+ if (added.length) warn(`${tokensPath}: under the idiom.${overlay} overlay, ${f.render(added)}`);
11270
+ }
11271
+ }
11272
+ }
11181
11273
  return issues;
11182
11274
  }
11275
+ function schemeValue(token, scheme) {
11276
+ let v = token;
11277
+ if (token !== null && typeof token === "object" && !Array.isArray(token)) {
11278
+ const o = token;
11279
+ v = scheme === "highContrast" ? o.highContrast ?? o.dark ?? o.light : scheme === "dark" ? o.dark ?? o.light : o.light;
11280
+ }
11281
+ return typeof v === "string" && isCanonicalHex(v) ? v.trim().toUpperCase() : void 0;
11282
+ }
11283
+ function paintedOver(fg, bg) {
11284
+ if (bg.length === 9 && bg.slice(7) !== "FF") return void 0;
11285
+ if (fg.length === 9) return mixHex(fg.slice(0, 7), bg.slice(0, 7), parseInt(fg.slice(7), 16) / 255);
11286
+ return fg;
11287
+ }
11288
+ function sameHueForContrast(fg, bg, target = AA_TEXT) {
11289
+ const base = fg.slice(0, 7);
11290
+ const opaqueBg = bg.slice(0, 7);
11291
+ const pole = contrastRatio("#000000", opaqueBg) >= contrastRatio("#FFFFFF", opaqueBg) ? "#000000" : "#FFFFFF";
11292
+ for (let step = 0; step <= 100; step++) {
11293
+ const candidate = mixHex(base, pole, 1 - step / 100);
11294
+ if (contrastRatio(candidate, opaqueBg) >= target) return candidate;
11295
+ }
11296
+ return void 0;
11297
+ }
11298
+ function contrastFindings(colors) {
11299
+ const declared = /* @__PURE__ */ new Set(["light"]);
11300
+ for (const v of Object.values(colors)) {
11301
+ if (v === null || typeof v !== "object" || Array.isArray(v)) continue;
11302
+ for (const s of SCHEMES) if (s in v) declared.add(s);
11303
+ }
11304
+ const findings = /* @__PURE__ */ new Map();
11305
+ for (const pair of CONTRAST_PAIRS) {
11306
+ const fgName = pair.fg;
11307
+ if (colors[fgName] === void 0 || colors[pair.bg] === void 0) continue;
11308
+ for (const scheme of SCHEMES) {
11309
+ if (!declared.has(scheme)) continue;
11310
+ const fg = schemeValue(colors[fgName], scheme);
11311
+ const bg = schemeValue(colors[pair.bg], scheme);
11312
+ if (!fg || !bg) continue;
11313
+ const painted = paintedOver(fg, bg);
11314
+ if (!painted) continue;
11315
+ const opaqueBg = bg.slice(0, 7);
11316
+ const ratio = contrastRatio(painted, opaqueBg);
11317
+ if (ratio >= AA_TEXT) continue;
11318
+ const key2 = `${fgName}|${pair.bg}|${fg}|${bg}`;
11319
+ const same2 = findings.get(key2);
11320
+ if (same2) {
11321
+ same2.schemes.push(scheme);
11322
+ continue;
11323
+ }
11324
+ const darker = contrastRatio("#000000", opaqueBg) >= contrastRatio("#FFFFFF", opaqueBg);
11325
+ const ink = darker ? "#000000" : "#FFFFFF";
11326
+ const suggestion = pair.suggest === "same-hue" ? sameHueForContrast(painted, opaqueBg) : pair.suggest === "ink" && contrastRatio(ink, opaqueBg) >= AA_TEXT ? ink : void 0;
11327
+ const fix = !suggestion ? ` Pick a ${fgName} that clears ${AA_TEXT}:1 against ${pair.bg}.` : pair.suggest === "same-hue" ? ` A ${darker ? "darker" : "lighter"} ${fgName} of the same hue that clears it is ${suggestion} (${contrastRatio(suggestion, opaqueBg).toFixed(2)}:1).` : ` ${suggestion} clears it (${contrastRatio(suggestion, opaqueBg).toFixed(2)}:1).`;
11328
+ findings.set(key2, {
11329
+ key: key2,
11330
+ schemes: [scheme],
11331
+ render: (schemes) => `colors.${fgName} (${fg}) on colors.${pair.bg} (${bg}) is ${ratio.toFixed(2)}:1 in ${schemes.join(" / ")} \u2014 below the WCAG AA ${AA_TEXT}:1 for text. ${fgName} is ${pair.use}, so that text is hard to read.${fix}`
11332
+ });
11333
+ }
11334
+ }
11335
+ return [...findings.values()];
11336
+ }
11337
+ var AA_TEXT, CONTRAST_PAIRS, SCHEMES, channels, toHex, mixHex;
11183
11338
  var init_token_lint = __esm({
11184
11339
  "src/token-lint.ts"() {
11185
11340
  init_token_colors();
11341
+ init_idiom();
11342
+ AA_TEXT = 4.5;
11343
+ CONTRAST_PAIRS = [
11344
+ {
11345
+ fg: "accentPrimary",
11346
+ bg: "surfacePrimary",
11347
+ use: "the text colour of secondary, plain and link buttons, selected tab labels and inline action text",
11348
+ suggest: "same-hue"
11349
+ },
11350
+ { fg: "accentText", bg: "surfacePrimary", use: "the dedicated colour for accent-coloured text", suggest: "same-hue" },
11351
+ { fg: "textOnAccent", bg: "accentPrimary", use: "the label on filled (primary) buttons and selected chips", suggest: "ink" },
11352
+ { fg: "textPrimary", bg: "surfacePrimary", use: "body and title text" },
11353
+ { fg: "textSecondary", bg: "surfacePrimary", use: "secondary text (subtitles, captions, metadata)" }
11354
+ ];
11355
+ SCHEMES = ["light", "dark", "highContrast"];
11356
+ channels = (hex2) => [1, 3, 5].map((i) => parseInt(hex2.slice(i, i + 2), 16));
11357
+ toHex = (rgb) => `#${rgb.map((c) => Math.round(Math.min(255, Math.max(0, c))).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
11358
+ mixHex = (a, b, w) => {
11359
+ const [ca, cb] = [channels(a), channels(b)];
11360
+ return toHex(ca.map((c, i) => c * w + cb[i] * (1 - w)));
11361
+ };
11186
11362
  }
11187
11363
  });
11188
11364
 
@@ -11841,8 +12017,8 @@ async function sharpOrThrow() {
11841
12017
  async function inspectIconPixels(png) {
11842
12018
  const sharp = await sharpOrThrow();
11843
12019
  const { data, info } = await sharp(png).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
11844
- const { width, height, channels: channels2 } = info;
11845
- if (channels2 !== 4) throw new Error(`app-icons: expected RGBA after ensureAlpha, got ${channels2} channels`);
12020
+ const { width, height, channels: channels3 } = info;
12021
+ if (channels3 !== 4) throw new Error(`app-icons: expected RGBA after ensureAlpha, got ${channels3} channels`);
11846
12022
  let minAlpha = 255;
11847
12023
  let transparent = 0;
11848
12024
  for (let i = 3; i < data.length; i += 4) {
@@ -12691,75 +12867,6 @@ var init_face_source = __esm({
12691
12867
  }
12692
12868
  });
12693
12869
 
12694
- // ../../core/ts/src/idiom.ts
12695
- function authored(overlays, name) {
12696
- return typeof name === "string" && overlays !== void 0 && Object.hasOwn(overlays, name) && overlays[name] !== null && typeof overlays[name] === "object" && !Array.isArray(overlays[name]);
12697
- }
12698
- function selectIdiomOverlay(doc, ctx = {}) {
12699
- const policy = doc.idiomPolicy;
12700
- const overlays = doc.idiom;
12701
- const isRecord3 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
12702
- if (!isRecord3(policy) || !isRecord3(overlays)) return void 0;
12703
- const kind = policy.policy ?? "follow-surface";
12704
- if (kind === "none") return void 0;
12705
- if (kind === "pinned") return authored(overlays, policy.pin) ? policy.pin : void 0;
12706
- if (kind !== "follow-surface") return void 0;
12707
- const surfaces = policy.surfaces;
12708
- const candidates = [
12709
- ctx.surface !== void 0 && isRecord3(surfaces) && Object.hasOwn(surfaces, ctx.surface) ? surfaces[ctx.surface] : void 0,
12710
- ...ctx.native ?? [],
12711
- policy.fallback
12712
- ];
12713
- return candidates.find((c) => authored(overlays, c));
12714
- }
12715
- function isPlainObject(v) {
12716
- return v !== null && typeof v === "object" && !Array.isArray(v);
12717
- }
12718
- function deepMerge(base, overlay) {
12719
- if (!isPlainObject(overlay) || !isPlainObject(base)) return overlay;
12720
- const out = { ...base };
12721
- for (const [key2, value] of Object.entries(overlay)) {
12722
- if (UNSAFE_KEYS.has(key2)) continue;
12723
- out[key2] = Object.hasOwn(base, key2) ? deepMerge(base[key2], value) : value;
12724
- }
12725
- return out;
12726
- }
12727
- function mergeIdiomOverlay(base, overlay) {
12728
- const merged = deepMerge(base, overlay);
12729
- const baseColors = base.colors;
12730
- const overlayColors = overlay.colors;
12731
- if (isPlainObject(baseColors) && isPlainObject(overlayColors)) {
12732
- const colors = { ...merged.colors };
12733
- for (const [name, value] of Object.entries(overlayColors)) {
12734
- if (UNSAFE_KEYS.has(name)) continue;
12735
- const was = Object.hasOwn(baseColors, name) ? baseColors[name] : void 0;
12736
- if (typeof was === "string" && isPlainObject(value)) colors[name] = { light: was, ...value };
12737
- }
12738
- merged.colors = colors;
12739
- }
12740
- return merged;
12741
- }
12742
- function resolveIdiom(tokens, ctx = {}) {
12743
- const doc = tokens;
12744
- const name = selectIdiomOverlay(doc, ctx);
12745
- if (name === void 0) return tokens;
12746
- const merged = mergeIdiomOverlay(tokens, doc.idiom[name]);
12747
- const { idiomPolicy: _applied, ...final } = merged;
12748
- return final;
12749
- }
12750
- var IDIOM_CONTEXTS, UNSAFE_KEYS;
12751
- var init_idiom = __esm({
12752
- "../../core/ts/src/idiom.ts"() {
12753
- "use strict";
12754
- IDIOM_CONTEXTS = {
12755
- ios: { native: ["ios"] },
12756
- android: { native: ["material"] },
12757
- mac: { native: ["desktop", "ios"], surface: "mac" }
12758
- };
12759
- UNSAFE_KEYS = /* @__PURE__ */ new Set(["__proto__", "constructor", "prototype"]);
12760
- }
12761
- });
12762
-
12763
12870
  // ../../core/web-renderer/src/idiom-controls.ts
12764
12871
  function pickRole(colors, role) {
12765
12872
  if (typeof role === "string") return role;
@@ -13417,12 +13524,12 @@ async function checkReport(appDir2, opts = {}) {
13417
13524
  function resolveFamilyName(input) {
13418
13525
  const catalog = loadCatalog();
13419
13526
  if (renderableFamilies(catalog).has(input)) return input;
13420
- const norm2 = input.toLowerCase().replace(/[^a-z0-9]/g, "");
13527
+ const norm3 = input.toLowerCase().replace(/[^a-z0-9]/g, "");
13421
13528
  for (const name of renderableFamilies(catalog)) {
13422
- if (name.toLowerCase().replace(/[^a-z0-9]/g, "") === norm2) return name;
13529
+ if (name.toLowerCase().replace(/[^a-z0-9]/g, "") === norm3) return name;
13423
13530
  }
13424
13531
  for (const [name, fam] of [...Object.entries(catalog.families), ...Object.entries(catalog.bundled)]) {
13425
- if ((fam.aliases ?? []).some((a) => a.toLowerCase().replace(/[^a-z0-9]/g, "") === norm2)) return name;
13532
+ if ((fam.aliases ?? []).some((a) => a.toLowerCase().replace(/[^a-z0-9]/g, "") === norm3)) return name;
13426
13533
  }
13427
13534
  return void 0;
13428
13535
  }
@@ -26851,7 +26958,7 @@ var init_renderNode = __esm({
26851
26958
  const hostRef = React5.useRef(null);
26852
26959
  const autoplayConsumed = React5.useRef(false);
26853
26960
  const item = resolvedItems[index];
26854
- const mediaKey = `${index}:${item?.id ?? item?.url ?? ""}`;
26961
+ const mediaKey2 = `${index}:${item?.id ?? item?.url ?? ""}`;
26855
26962
  React5.useEffect(() => {
26856
26963
  const outgoing = mediaRef.current;
26857
26964
  return () => {
@@ -26860,7 +26967,7 @@ var init_renderNode = __esm({
26860
26967
  } catch {
26861
26968
  }
26862
26969
  };
26863
- }, [mediaKey]);
26970
+ }, [mediaKey2]);
26864
26971
  const sessionId = String(props.sessionId || nodeId || "media");
26865
26972
  const provider = String(item?.provider || "direct").toLowerCase();
26866
26973
  const isYouTube = provider === "youtube";
@@ -27217,7 +27324,7 @@ var init_renderNode = __esm({
27217
27324
  provider,
27218
27325
  "."
27219
27326
  ] })
27220
- ] }) : isYouTube ? /* @__PURE__ */ jsx6("iframe", { title: String(item?.title ?? "YouTube video"), src: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(youtubeId(item))}?playsinline=1&autoplay=${props.autoplay === "never" ? 0 : 1}&mute=${props.muted === true ? 1 : 0}&controls=${chrome === "none" ? 0 : 1}`, allow: "autoplay; encrypted-media; picture-in-picture; fullscreen", allowFullScreen: true, style: { border: 0, width: "100%", flex: 1, minHeight: 0 } }, mediaKey) : isAudio ? /* @__PURE__ */ jsxs3("div", { style: { position: "relative", flex: 1, minHeight: 0, display: "grid", gridTemplateColumns: props.presentation === "compact" || props.presentation === "mini" ? "72px 1fr" : "1fr", alignItems: "center", gap: 16, padding: hidden ? 0 : 16, boxSizing: "border-box" }, children: [
27327
+ ] }) : isYouTube ? /* @__PURE__ */ jsx6("iframe", { title: String(item?.title ?? "YouTube video"), src: `https://www.youtube-nocookie.com/embed/${encodeURIComponent(youtubeId(item))}?playsinline=1&autoplay=${props.autoplay === "never" ? 0 : 1}&mute=${props.muted === true ? 1 : 0}&controls=${chrome === "none" ? 0 : 1}`, allow: "autoplay; encrypted-media; picture-in-picture; fullscreen", allowFullScreen: true, style: { border: 0, width: "100%", flex: 1, minHeight: 0 } }, mediaKey2) : isAudio ? /* @__PURE__ */ jsxs3("div", { style: { position: "relative", flex: 1, minHeight: 0, display: "grid", gridTemplateColumns: props.presentation === "compact" || props.presentation === "mini" ? "72px 1fr" : "1fr", alignItems: "center", gap: 16, padding: hidden ? 0 : 16, boxSizing: "border-box" }, children: [
27221
27328
  poster && /* @__PURE__ */ jsx6("img", { src: poster, alt: "", style: chrome === "custom" ? { position: "absolute", inset: 0, width: "100%", height: "100%", objectFit: "cover", opacity: 0.62 } : { width: props.presentation === "compact" || props.presentation === "mini" ? 72 : "min(55%, 260px)", aspectRatio: "1", objectFit: "cover", borderRadius: ctx.radius("md", 8), justifySelf: "center" } }),
27222
27329
  chrome === "custom" && /* @__PURE__ */ jsx6("div", { style: { position: "absolute", inset: 0, background: "linear-gradient(transparent,rgba(0,0,0,.75))" } }),
27223
27330
  !hidden && chrome !== "custom" && /* @__PURE__ */ jsxs3("div", { style: { textAlign: props.presentation === "compact" || props.presentation === "mini" ? "left" : "center", minWidth: 0 }, children: [
@@ -27226,10 +27333,10 @@ var init_renderNode = __esm({
27226
27333
  ] }),
27227
27334
  /* @__PURE__ */ jsx6("audio", { ref: (el) => {
27228
27335
  mediaRef.current = el;
27229
- }, ...commonMedia, children: renderMediaTracks() }, mediaKey)
27336
+ }, ...commonMedia, children: renderMediaTracks() }, mediaKey2)
27230
27337
  ] }) : /* @__PURE__ */ jsx6("video", { ref: (el) => {
27231
27338
  mediaRef.current = el;
27232
- }, poster: poster || void 0, ...commonMedia, style: { width: "100%", flex: 1, minHeight: 0, display: "block", objectFit: props.contentMode === "fill" ? "cover" : "contain", background: "#000" }, children: renderMediaTracks() }, mediaKey),
27339
+ }, poster: poster || void 0, ...commonMedia, style: { width: "100%", flex: 1, minHeight: 0, display: "block", objectFit: props.contentMode === "fill" ? "cover" : "contain", background: "#000" }, children: renderMediaTracks() }, mediaKey2),
27233
27340
  status === "idle" && slot("poster"),
27234
27341
  slot("overlay"),
27235
27342
  status === "loading" && slot("loading"),
@@ -36625,6 +36732,8 @@ async function bootScreenWorker(opts) {
36625
36732
  ...opts.inputOverride ?? {}
36626
36733
  };
36627
36734
  const appear = opts.skipAppear ? null : rootOnAppear(opts.descriptor);
36735
+ const hostCalls = [];
36736
+ const host = hostDispatch(opts.localDbSchema, declaredCapabilitiesOf(opts.descriptor), opts.capabilitySuccess === true, opts.clock ?? CRAWL_CLOCK);
36628
36737
  const w = await bootBundleWorker(opts.bundleJs, {
36629
36738
  wixCall: opts.wixCall,
36630
36739
  input,
@@ -36633,7 +36742,10 @@ async function bootScreenWorker(opts) {
36633
36742
  // while the worker believed it was 2026, so a modelled session carried a timestamp fifty-six
36634
36743
  // years before the "now" the app computes against — inconsistent rendered dates, and any
36635
36744
  // time-dependent branch taking the wrong arm.
36636
- dispatch: hostDispatch(opts.localDbSchema, declaredCapabilitiesOf(opts.descriptor), opts.capabilitySuccess === true, opts.clock ?? CRAWL_CLOCK),
36745
+ dispatch: (action, params) => {
36746
+ hostCalls.push(String(action));
36747
+ return host(action, params);
36748
+ },
36637
36749
  deviceParity: true,
36638
36750
  clock: opts.clock ?? CRAWL_CLOCK,
36639
36751
  seed: opts.seed ?? 1,
@@ -36656,7 +36768,12 @@ async function bootScreenWorker(opts) {
36656
36768
  }
36657
36769
  for (let i = 0; i < 20; i++) await yieldMacrotask();
36658
36770
  const { state, defects } = snapshot(w);
36659
- return { ...w, state: Object.keys(state).length ? state : w.services, defects, ...deadLifecycle ? { deadLifecycle } : {} };
36771
+ return { ...w, hostCalls, state: Object.keys(state).length ? state : w.services, defects, ...deadLifecycle ? { deadLifecycle } : {} };
36772
+ }
36773
+ function effectSince(w, mark, action, componentId) {
36774
+ const wix = w.calls.slice(mark.wix);
36775
+ const host = w.hostCalls.slice(mark.host);
36776
+ return wix.length || host.length ? { action, componentId, wix, host } : null;
36660
36777
  }
36661
36778
  function resolveSiteRows(itemsPath, valueOf) {
36662
36779
  if (!itemsPath || itemsPath === UNTRACKED_ITEMS) return void 0;
@@ -36717,7 +36834,10 @@ async function crawlScreenCapabilitySuccess(opts) {
36717
36834
  const r = await crawlScreenInner({ ...opts, capabilitySuccess: true });
36718
36835
  return {
36719
36836
  findings: r.findings.map((f) => ({ ...f, axis: f.axis ? `capability:success+${f.axis}` : "capability:success" })),
36720
- modelled: modelled.length
36837
+ modelled: modelled.length,
36838
+ // The success arm is where a paid flow goes on past `isAvailable` to `proceedToPayment`, so
36839
+ // its effects are the write inventory's too — unlike its counts, they cannot double anything.
36840
+ ...r.effects?.length ? { effects: r.effects } : {}
36721
36841
  };
36722
36842
  });
36723
36843
  }
@@ -36757,6 +36877,9 @@ async function crawlScreenJourney(opts) {
36757
36877
  });
36758
36878
  const sites = actionSitesForDescriptor(opts.descriptor);
36759
36879
  let stepsExercised = 0;
36880
+ const effects = [];
36881
+ const bootEffect = effectSince(worker, { wix: 0, host: 0 }, "boot", null);
36882
+ if (bootEffect) effects.push(bootEffect);
36760
36883
  for (const step of opts.journey.steps) {
36761
36884
  const declared = sites.find((site2) => site2.actionId === step.action && (step.component === void 0 || site2.componentId === step.component));
36762
36885
  if (!declared) {
@@ -36779,6 +36902,7 @@ async function crawlScreenJourney(opts) {
36779
36902
  fail2(`journey "${opts.journey.name}" names unavailable worker action ${step.action}`, step);
36780
36903
  continue;
36781
36904
  }
36905
+ const mark = { wix: worker.calls.length, host: worker.hostCalls.length };
36782
36906
  try {
36783
36907
  await handle.dispatch(parsed.action, ...workerArgs({ params: { ...site.params ?? {}, ...site.args ?? {}, ...step.params ?? {} } }));
36784
36908
  for (let i = 0; i < 8; i++) await yieldMacrotask();
@@ -36786,6 +36910,8 @@ async function crawlScreenJourney(opts) {
36786
36910
  } catch (error) {
36787
36911
  fail2(`journey "${opts.journey.name}" step ${step.action} threw: ${errText(error)}`, step);
36788
36912
  }
36913
+ const stepEffect = effectSince(worker, mark, site.actionId, site.componentId);
36914
+ if (stepEffect) effects.push(stepEffect);
36789
36915
  }
36790
36916
  const settled = snapshot(worker);
36791
36917
  if (worker.errors.length) {
@@ -36795,7 +36921,7 @@ async function crawlScreenJourney(opts) {
36795
36921
  for (const expected of opts.journey.expectText) {
36796
36922
  if (!values.includes(expected)) fail2(`journey "${opts.journey.name}" never rendered its expected visible result: ${JSON.stringify(expected)}`);
36797
36923
  }
36798
- return { screen: opts.screenId, journey: opts.journey.name, stepsExercised, findings };
36924
+ return { screen: opts.screenId, journey: opts.journey.name, stepsExercised, findings, ...effects.length ? { effects } : {} };
36799
36925
  });
36800
36926
  }
36801
36927
  async function crawlScreenInner(opts) {
@@ -36810,6 +36936,7 @@ async function crawlScreenInner(opts) {
36810
36936
  const actionsSkipped = [];
36811
36937
  const navEdges = [];
36812
36938
  const unobservedActions = [];
36939
+ const effects = [];
36813
36940
  let observations;
36814
36941
  let seedWords = [];
36815
36942
  const harvestStaticEdges = (state) => {
@@ -37093,6 +37220,8 @@ async function crawlScreenInner(opts) {
37093
37220
  harvestStaticEdges(first.state);
37094
37221
  drainWorkerEdges(first, "worker:boot");
37095
37222
  drainPollDirectives(first, "boot");
37223
+ const bootEffect = effectSince(first, { wix: 0, host: 0 }, "boot", null);
37224
+ if (bootEffect) effects.push(bootEffect);
37096
37225
  if (/\bwixApi\b/.test(bundleJs) && first.calls.length === 0) {
37097
37226
  const inputGated = /himalaya\??\.input\b|\binput\s*[.?]/.test(bundleJs);
37098
37227
  findings.push({
@@ -37178,6 +37307,7 @@ async function crawlScreenInner(opts) {
37178
37307
  const dispatched = typed ? { ...effective, args: { ...effective.args ?? {}, [typed.key]: typed.value } } : effective;
37179
37308
  const before = JSON.stringify(preState);
37180
37309
  const callsBefore = w.calls.length;
37310
+ const hostBefore = w.hostCalls.length;
37181
37311
  const errors = [];
37182
37312
  try {
37183
37313
  await handle.dispatch(parsed.action, ...workerArgs(dispatched));
@@ -37190,6 +37320,8 @@ async function crawlScreenInner(opts) {
37190
37320
  const navCountBefore = navEdges.length;
37191
37321
  drainWorkerEdges(w, `worker:${site.actionId}`);
37192
37322
  drainPollDirectives(w, trigger);
37323
+ const tapEffect = effectSince(w, { wix: callsBefore, host: hostBefore }, site.actionId, site.componentId);
37324
+ if (tapEffect) effects.push(tapEffect);
37193
37325
  if (!errors.length && navEdges.length === navCountBefore && w.calls.length === callsBefore && JSON.stringify(state) === before) {
37194
37326
  if (typed) {
37195
37327
  findings.push({
@@ -37254,6 +37386,7 @@ async function crawlScreenInner(opts) {
37254
37386
  actionsExercised: exercised,
37255
37387
  actionsSkipped,
37256
37388
  navEdges: edges,
37389
+ ...effects.length ? { effects } : {},
37257
37390
  ...observations ? { observations } : {},
37258
37391
  ...unobservedActions.length ? { unobservedActions } : {},
37259
37392
  findings: dedupe(findings),
@@ -37472,6 +37605,7 @@ async function crawlScreenDeep(opts, deep) {
37472
37605
  const queue = [[]];
37473
37606
  let dispatches = 0;
37474
37607
  let capped = false;
37608
+ const effects = [];
37475
37609
  const boot = () => bootScreenWorker({
37476
37610
  bundleJs,
37477
37611
  descriptor,
@@ -37492,6 +37626,13 @@ async function crawlScreenDeep(opts, deep) {
37492
37626
  }
37493
37627
  const path2 = queue.shift();
37494
37628
  const w = await boot();
37629
+ let pending = null;
37630
+ const flushStep = () => {
37631
+ if (!pending) return;
37632
+ const e = effectSince(w, pending.mark, pending.site.actionId, pending.site.componentId);
37633
+ if (e) effects.push(e);
37634
+ pending = null;
37635
+ };
37495
37636
  try {
37496
37637
  let threwAt = null;
37497
37638
  for (const [step, siteIdx] of path2.entries()) {
@@ -37513,6 +37654,8 @@ async function crawlScreenDeep(opts, deep) {
37513
37654
  break;
37514
37655
  }
37515
37656
  dispatches += 1;
37657
+ flushStep();
37658
+ pending = { mark: { wix: w.calls.length, host: w.hostCalls.length }, site };
37516
37659
  try {
37517
37660
  await handle.dispatch(parsed.action, ...workerArgs(effective));
37518
37661
  } catch (e) {
@@ -37533,8 +37676,12 @@ async function crawlScreenDeep(opts, deep) {
37533
37676
  break;
37534
37677
  }
37535
37678
  }
37536
- if (threwAt) continue;
37679
+ if (threwAt) {
37680
+ flushStep();
37681
+ continue;
37682
+ }
37537
37683
  for (let i = 0; i < 20; i++) await yieldMacrotask();
37684
+ flushStep();
37538
37685
  const { state, defects } = snapshot(w);
37539
37686
  const hash = deepStateSignature(state, defects);
37540
37687
  if (visited.has(hash)) continue;
@@ -37579,7 +37726,7 @@ async function crawlScreenDeep(opts, deep) {
37579
37726
  w.dispose();
37580
37727
  }
37581
37728
  }
37582
- return { findings: dedupe(findings), nodesVisited: visited.size, dispatches, capped };
37729
+ return { findings: dedupe(findings), nodesVisited: visited.size, dispatches, capped, ...effects.length ? { effects } : {} };
37583
37730
  });
37584
37731
  }
37585
37732
  function effectiveSiteForRow(site, row) {
@@ -40894,11 +41041,11 @@ function notCarriedDiff(local, base) {
40894
41041
  const CARRIED = /* @__PURE__ */ new Set(["screens", "bundles", "tokensHash", "appConfigHash"]);
40895
41042
  const PROVENANCE = /* @__PURE__ */ new Set(["releaseId", "gitSha", "shellSha", "label", "notes", "publishedAt", "publishedBy", "attestation", "forcedPastErrors", "app", "appSourceHash"]);
40896
41043
  const REPORTED_IN_DETAIL = /* @__PURE__ */ new Set(["assets", "minBinaryVersion", "iconHash"]);
40897
- const comparable = (key2, value) => key2 === "widgets" ? stableWidgets(value) : value;
41044
+ const comparable2 = (key2, value) => key2 === "widgets" ? stableWidgets(value) : value;
40898
41045
  for (const key2 of /* @__PURE__ */ new Set([...Object.keys(local), ...Object.keys(base)])) {
40899
41046
  if (CARRIED.has(key2) || PROVENANCE.has(key2) || REPORTED_IN_DETAIL.has(key2)) continue;
40900
- const l = comparable(key2, local[key2] ?? null);
40901
- const b = comparable(key2, base[key2] ?? null);
41047
+ const l = comparable2(key2, local[key2] ?? null);
41048
+ const b = comparable2(key2, base[key2] ?? null);
40902
41049
  const ls = JSON.stringify(l) ?? "null";
40903
41050
  const bs = JSON.stringify(b) ?? "null";
40904
41051
  if (ls === bs) continue;
@@ -41685,7 +41832,7 @@ function assertRedirectUri(value) {
41685
41832
  }
41686
41833
  return trimmed;
41687
41834
  }
41688
- var BOOK_SCHEMA_VERSION, KNOWN_VERTICALS;
41835
+ var BOOK_SCHEMA_VERSION, KNOWN_VERTICALS, REDIRECT_NEEDS_CLIENT;
41689
41836
  var init_types2 = __esm({
41690
41837
  "src/site-analyze/types.ts"() {
41691
41838
  BOOK_SCHEMA_VERSION = 2;
@@ -41719,6 +41866,7 @@ var init_types2 = __esm({
41719
41866
  "galleries",
41720
41867
  "inbox"
41721
41868
  ];
41869
+ REDIRECT_NEEDS_CLIENT = "--redirect-uri is the redirect REGISTERED on --client-id, so it needs --client-id too";
41722
41870
  }
41723
41871
  });
41724
41872
 
@@ -42064,6 +42212,7 @@ var init_home_descriptor = __esm({
42064
42212
  var template_trim_exports = {};
42065
42213
  __export(template_trim_exports, {
42066
42214
  TrimRefusal: () => TrimRefusal,
42215
+ compareErrors: () => compareErrors,
42067
42216
  leftoverNavRefs: () => leftoverNavRefs,
42068
42217
  linearSafePattern: () => linearSafePattern,
42069
42218
  parseApp: () => parseApp,
@@ -42832,6 +42981,16 @@ async function runTemplateTrim(req, deps) {
42832
42981
  if (!existsSync42(path2) || !readFileSync46(path2).equals(original)) writeFileSync15(path2, original);
42833
42982
  }
42834
42983
  for (const name of [...moved].reverse()) renameSync4(join39(backup, "tier5-src", name), join39(dir, "tier5-src", name));
42984
+ moved.length = 0;
42985
+ };
42986
+ const apply = () => {
42987
+ const moveDir = deps.moveDir ?? renameSync4;
42988
+ for (const name of plan.removedWorkers) {
42989
+ moveDir(join39(dir, "tier5-src", name), join39(backup, "tier5-src", name));
42990
+ moved.push(name);
42991
+ }
42992
+ if (plan.dev !== dev) writeFileSync15(devPath, plan.dev);
42993
+ if (plan.config !== config) writeFileSync15(configPath, plan.config);
42835
42994
  };
42836
42995
  const dropBackup = () => {
42837
42996
  rmSync8(backup, { recursive: true, force: true });
@@ -42870,13 +43029,7 @@ async function runTemplateTrim(req, deps) {
42870
43029
  return { code: 1, result: { ok: false, error: `could not back up the app before trimming it (${e.message}); nothing was changed`, rolledBack: true, ...base() } };
42871
43030
  }
42872
43031
  try {
42873
- const moveDir = deps.moveDir ?? renameSync4;
42874
- for (const name of plan.removedWorkers) {
42875
- moveDir(join39(dir, "tier5-src", name), join39(backup, "tier5-src", name));
42876
- moved.push(name);
42877
- }
42878
- if (plan.dev !== dev) writeFileSync15(devPath, plan.dev);
42879
- if (plan.config !== config) writeFileSync15(configPath, plan.config);
43032
+ apply();
42880
43033
  } catch (e) {
42881
43034
  return abort("the trim failed while writing", e);
42882
43035
  }
@@ -42893,9 +43046,53 @@ async function runTemplateTrim(req, deps) {
42893
43046
  } catch (e) {
42894
43047
  return abort("the trim could not be checked", e, validate3);
42895
43048
  }
43049
+ let compared;
43050
+ if (failure && validate3 && validate3.code !== 0 && comparable(validate3)) {
43051
+ try {
43052
+ restore();
43053
+ } catch (e) {
43054
+ return abort(String(failure.error), e, validate3);
43055
+ }
43056
+ let original;
43057
+ try {
43058
+ original = await deps.validate(dir, { fresh: true });
43059
+ } catch {
43060
+ }
43061
+ if (original && comparable(original)) {
43062
+ compared = compareErrors(validate3.errors ?? [], original.code === 0 ? [] : original.errors ?? []);
43063
+ }
43064
+ const clean2 = compared !== void 0 && compared.introduced.length === 0;
43065
+ if (clean2 || req.force) {
43066
+ try {
43067
+ apply();
43068
+ } catch (e) {
43069
+ return abort("the trim failed while re-applying it after validating the untrimmed app", e, validate3);
43070
+ }
43071
+ }
43072
+ if (clean2) failure = void 0;
43073
+ else if (compared) {
43074
+ failure = {
43075
+ ...failure,
43076
+ error: `himi validate --strict failed on the trimmed app \u2014 the trim introduced ${plural(compared.introduced.length, "error")} the untrimmed app does not have: ${summarize3(compared.introduced)}`,
43077
+ introducedErrors: compared.introduced,
43078
+ preExistingErrors: compared.preExisting
43079
+ };
43080
+ }
43081
+ }
42896
43082
  if (!failure) {
42897
43083
  dropBackup();
42898
- return { code: 0, result: { ok: true, ...base(validate3) } };
43084
+ const preExisting = compared?.preExisting ?? [];
43085
+ return {
43086
+ code: 0,
43087
+ result: {
43088
+ ok: true,
43089
+ ...base(validate3),
43090
+ ...preExisting.length ? {
43091
+ preExistingErrors: preExisting,
43092
+ note: `trimmed; ${plural(preExisting.length, "error")} ${preExisting.length === 1 ? "was" : "were"} already present before trim: ${summarize3(preExisting)}`
43093
+ } : {}
43094
+ }
43095
+ };
42899
43096
  }
42900
43097
  if (req.force) {
42901
43098
  return {
@@ -42914,7 +43111,36 @@ async function runTemplateTrim(req, deps) {
42914
43111
  result: { ok: false, ...failure, rolledBack: true, hint: "nothing changed \u2014 fix what validate reports (or keep more screens) and trim again, or pass --force to keep the trimmed tree anyway", ...base(validate3) }
42915
43112
  };
42916
43113
  }
42917
- var TrimRefusal, LIT, ATOM, QUANT, CAP, SAFE_ROUTE_PATTERN, lineOf, isStatic, entryLabel, isNode3, asItems, NAV_SCREEN_KEYS, sorted, refuse3;
43114
+ function comparable(v) {
43115
+ if (v.code === 0) return true;
43116
+ if (!v.errors?.length) return false;
43117
+ return v.counts === void 0 || v.counts.errors === v.errors.length;
43118
+ }
43119
+ function compareErrors(trimmed, original) {
43120
+ const left = /* @__PURE__ */ new Map();
43121
+ for (const e of original) left.set(e.id, (left.get(e.id) ?? 0) + 1);
43122
+ const out = { preExisting: [], introduced: [] };
43123
+ for (const e of trimmed) {
43124
+ const n = left.get(e.id) ?? 0;
43125
+ if (n > 0) {
43126
+ left.set(e.id, n - 1);
43127
+ out.preExisting.push(e);
43128
+ } else out.introduced.push(e);
43129
+ }
43130
+ return out;
43131
+ }
43132
+ function summarize3(errors) {
43133
+ const one = (e) => {
43134
+ const rule = e.pass ? e.ruleCode && e.ruleCode !== e.pass ? `${e.pass}/${e.ruleCode}` : e.pass : e.ruleCode;
43135
+ const place = e.screen ? `on ${e.screen}` : e.path ? `in ${e.path}` : void 0;
43136
+ const where = [rule, place].filter(Boolean).join(" ");
43137
+ const msg = e.message.length > 160 ? `${e.message.slice(0, 157)}...` : e.message;
43138
+ return where ? `${where}: ${msg}` : msg;
43139
+ };
43140
+ const shown2 = errors.slice(0, 5).map(one).join("; ");
43141
+ return errors.length > 5 ? `${shown2}; and ${errors.length - 5} more` : shown2;
43142
+ }
43143
+ var TrimRefusal, LIT, ATOM, QUANT, CAP, SAFE_ROUTE_PATTERN, lineOf, isStatic, entryLabel, isNode3, asItems, NAV_SCREEN_KEYS, sorted, refuse3, plural;
42918
43144
  var init_template_trim = __esm({
42919
43145
  "src/template-trim.ts"() {
42920
43146
  init_validate_descriptor();
@@ -42948,6 +43174,7 @@ var init_template_trim = __esm({
42948
43174
  NAV_SCREEN_KEYS = /* @__PURE__ */ new Set(["screenId", "parentScreenId", "contentScreenId", "badgeScreenId"]);
42949
43175
  sorted = (xs) => [...new Set(xs)].sort();
42950
43176
  refuse3 = (e) => ({ code: 2, result: { ok: false, error: e.message, ...e.detail } });
43177
+ plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
42951
43178
  }
42952
43179
  });
42953
43180
 
@@ -44229,6 +44456,184 @@ var init_eject = __esm({
44229
44456
  }
44230
44457
  });
44231
44458
 
44459
+ // src/wix-writes.ts
44460
+ import { existsSync as existsSync46, readFileSync as readFileSync50 } from "node:fs";
44461
+ import { join as join43 } from "node:path";
44462
+ function isProvenRead(method, path2) {
44463
+ if (/[#\x00-\x1F\x7F]/.test(path2)) return false;
44464
+ const verb = method.toUpperCase();
44465
+ if (verb === "GET") return true;
44466
+ const bare = path2.split("?")[0].replace(/\/+$/, "");
44467
+ return verb === "POST" && /\/(query|search|list[^/]*|get[^/]*|count|aggregate|facets|connections)$/i.test(bare);
44468
+ }
44469
+ function nativeWriteOf(verb) {
44470
+ const exact = NATIVE_WRITES[verb];
44471
+ if (exact) return { canonical: verb, ...exact };
44472
+ const member = MEMBER_VERB.exec(verb);
44473
+ if (member) return { canonical: `himi.wix.${member[1]}`, ...NATIVE_WRITES[`himi.wix.${member[1]}`] };
44474
+ return null;
44475
+ }
44476
+ function writesFromEffects(screen, effects) {
44477
+ const out = [];
44478
+ for (const e of effects ?? []) {
44479
+ const base = { screen, action: e.action, ...e.componentId ? { component: e.componentId } : {} };
44480
+ for (const call of e.wix) {
44481
+ const sp = call.indexOf(" ");
44482
+ const method = (sp < 0 ? call : call.slice(0, sp)).toUpperCase();
44483
+ const path2 = sp < 0 ? "" : call.slice(sp + 1);
44484
+ if (isProvenRead(method, path2)) continue;
44485
+ out.push({ ...base, via: "wix", method, path: path2.split("?")[0] });
44486
+ }
44487
+ for (const verb of e.host) {
44488
+ const native = nativeWriteOf(verb);
44489
+ if (native) out.push({ ...base, via: "native", capability: verb, kind: native.kind, what: native.what });
44490
+ }
44491
+ }
44492
+ return dedupeWrites(out);
44493
+ }
44494
+ function dedupeWrites(list) {
44495
+ const seen = /* @__PURE__ */ new Set();
44496
+ const out = [];
44497
+ for (const w of list) {
44498
+ const k = writeKey(w);
44499
+ if (seen.has(k)) continue;
44500
+ seen.add(k);
44501
+ out.push(w);
44502
+ }
44503
+ return out;
44504
+ }
44505
+ function unreachedNativeWrites(screen, bundleJs, observed) {
44506
+ if (!bundleJs) return [];
44507
+ const seen = new Set(observed.filter((w) => w.screen === screen && w.via === "native").map((w) => nativeWriteOf(w.capability)?.canonical));
44508
+ const out = [];
44509
+ for (const [verb, native] of Object.entries(NATIVE_WRITES)) {
44510
+ if (seen.has(verb)) continue;
44511
+ if (!BUNDLE_TEXT[verb].test(bundleJs)) continue;
44512
+ out.push({ screen, action: "unreached", via: "native", capability: verb, kind: native.kind, what: native.what });
44513
+ }
44514
+ return out;
44515
+ }
44516
+ function parseDataAccess(text5, from) {
44517
+ const body = text5.replace(/<!--[\s\S]*?(?:-->|$)/g, "");
44518
+ const heading = new RegExp(`(?:^|\\n)##[ \\t]+${DATA_ACCESS_HEADING}\\b[^\\n]*([\\s\\S]*?)(?=\\n##[ \\t]|$)`, "i").exec(body);
44519
+ if (!heading) return null;
44520
+ const lines = heading[1].split("\n").map((l) => l.trim()).filter((l) => l.startsWith("|"));
44521
+ if (lines.length < 2) return null;
44522
+ const cells = (line) => line.replace(/^\|/, "").replace(/\|\s*$/, "").split("|").map((c) => c.trim());
44523
+ const header = cells(lines[0]).map((c) => unquote(c).toLowerCase());
44524
+ const screenCol = header.findIndex((h) => /^screens?$/.test(h));
44525
+ let writesCol = header.findIndex((h) => /^wix writes?$/.test(h));
44526
+ if (writesCol < 0) writesCol = header.findIndex((h) => /^writes?$/.test(h));
44527
+ if (screenCol < 0 || writesCol < 0) return null;
44528
+ const rows = [];
44529
+ for (const line of lines.slice(1)) {
44530
+ if (/^\|?\s*:?-+:?\s*(\|\s*:?-+:?\s*)*\|?$/.test(line)) continue;
44531
+ const row = cells(line);
44532
+ const screen = unquote(row[screenCol] ?? "");
44533
+ if (!screen) continue;
44534
+ const cell = unquote(row[writesCol] ?? "");
44535
+ const entries = cell.split(/[,;]/).map((e) => e.trim()).filter((e) => e && !NONE.test(e));
44536
+ rows.push({ screen, allowed: NONE.test(cell) || !entries.length ? null : entries });
44537
+ }
44538
+ return rows.length ? { from, rows } : null;
44539
+ }
44540
+ function readDataAccess(dir) {
44541
+ for (const name of ["SPEC.md", "MOBILE-UX.md"]) {
44542
+ const p = join43(dir, name);
44543
+ if (!existsSync46(p)) continue;
44544
+ let text5;
44545
+ try {
44546
+ text5 = readFileSync50(p, "utf8");
44547
+ } catch {
44548
+ continue;
44549
+ }
44550
+ const plan = parseDataAccess(text5, name);
44551
+ if (plan) return plan;
44552
+ }
44553
+ return null;
44554
+ }
44555
+ function entryApproves(entry, w) {
44556
+ const e = entry.trim();
44557
+ const lower = e.toLowerCase();
44558
+ const actionTail = w.action.split(".").pop() ?? w.action;
44559
+ if (e === w.action || e === actionTail) return true;
44560
+ if (w.via === "native") {
44561
+ const tail = w.capability.split(".").pop();
44562
+ if (e === w.capability || e === tail || e === nativeWriteOf(w.capability)?.canonical) return true;
44563
+ if (w.kind === "payment" && /^payments?$/.test(lower)) return true;
44564
+ if (w.kind === "member-signup" && /^(sign-?up|register|registration|member[- ]signup)$/.test(lower)) return true;
44565
+ return false;
44566
+ }
44567
+ const m = /^(?:(GET|POST|PUT|PATCH|DELETE)\s+)?(\/\S*|https?:\/\/\S+)$/i.exec(e);
44568
+ if (!m) return false;
44569
+ if (m[1] && m[1].toUpperCase() !== w.method) return false;
44570
+ return pathPattern(m[2]).test(w.path);
44571
+ }
44572
+ function pathPattern(p) {
44573
+ const body = p.replace(/\/+$/, "").split("*").map((part) => part.replace(/[.+?^${}()|[\]\\]/g, "\\$&")).join("[^/]+");
44574
+ return new RegExp(`^${body}/?$`);
44575
+ }
44576
+ function unapprovedWriteFindings(writes, plan) {
44577
+ if (!plan) return [];
44578
+ const byScreen = /* @__PURE__ */ new Map();
44579
+ for (const row of plan.rows) {
44580
+ const list = byScreen.get(row.screen) ?? [];
44581
+ list.push(row);
44582
+ byScreen.set(row.screen, list);
44583
+ }
44584
+ const findings = [];
44585
+ for (const w of writes) {
44586
+ const rows = byScreen.get(w.screen) ?? byScreen.get("*");
44587
+ const unreached = w.action === "unreached";
44588
+ const trigger = w.action === "boot" || unreached ? "boot" : { tap: w.component ?? null, key: "write", action: w.action };
44589
+ const does = unreached ? `can write to Wix (${writeLabel(w)} \u2014 its worker bundle calls it, on a path this crawl did not reach)` : `writes to Wix (${writeLabel(w)})`;
44590
+ const hint = `Before publishing, reconcile this with the owner-approved plan: remove the action (or trim the screen with \`himi template trim\`) if the owner did not approve it, or name the write in ${plan.from}'s \`## ${DATA_ACCESS_HEADING}\` table \u2014 ` + (unreached ? "" : `its action (\`${w.action.split(".").pop()}\`), `) + (w.via === "wix" ? `its endpoint (\`${w.method} ${w.path}\`)` : `its capability (\`${w.capability.split(".").pop()}\`)`) + ` \u2014 once they have.`;
44591
+ if (!rows) {
44592
+ findings.push({
44593
+ screen: w.screen,
44594
+ trigger,
44595
+ invariant: "write-not-approved",
44596
+ severity: "warning",
44597
+ message: `${does}, and ${plan.from}'s data-access table has no row for this screen`,
44598
+ hint
44599
+ });
44600
+ continue;
44601
+ }
44602
+ const allowed2 = rows.flatMap((r) => r.allowed ?? []);
44603
+ if (allowed2.some((entry) => entryApproves(entry, w))) continue;
44604
+ findings.push({
44605
+ screen: w.screen,
44606
+ trigger,
44607
+ invariant: "write-not-approved",
44608
+ severity: "warning",
44609
+ message: allowed2.length ? `${does}, which ${plan.from}'s data-access table does not name for this screen (approved: ${allowed2.join(", ")})` : `${does}, but ${plan.from}'s data-access table approves no writes on this screen`,
44610
+ hint
44611
+ });
44612
+ }
44613
+ return findings;
44614
+ }
44615
+ var NATIVE_WRITES, MEMBER_VERB, BUNDLE_TEXT, writeKey, writeLabel, DATA_ACCESS_HEADING, NONE, unquote;
44616
+ var init_wix_writes = __esm({
44617
+ "src/wix-writes.ts"() {
44618
+ NATIVE_WRITES = {
44619
+ "himi.wix.registerDirect": { kind: "member-signup", what: "creates a site member" },
44620
+ "himi.wix.resetPassword": { kind: "member-account", what: "sends a site member a password-reset email" },
44621
+ "capability.wix_cashier.proceedToPayment": { kind: "payment", what: "charges the customer through the Wix cashier" }
44622
+ };
44623
+ MEMBER_VERB = /^[a-z_]+\.wix\.(registerDirect|resetPassword)$/;
44624
+ BUNDLE_TEXT = {
44625
+ "himi.wix.registerDirect": /["'`][a-z_]+\.wix\.registerDirect["'`]|\}(?:\.wix)?\.registerDirect`/,
44626
+ "himi.wix.resetPassword": /["'`][a-z_]+\.wix\.resetPassword["'`]|\}(?:\.wix)?\.resetPassword`/,
44627
+ "capability.wix_cashier.proceedToPayment": /capability\.wix_cashier\.proceedToPayment["'`]/
44628
+ };
44629
+ writeKey = (w) => [w.screen, w.action, w.via, w.method ?? "", w.path ?? "", w.capability ?? ""].join("|");
44630
+ writeLabel = (w) => w.via === "wix" ? `${w.method} ${w.path}` : `${w.capability} (${w.what})`;
44631
+ DATA_ACCESS_HEADING = "Data access";
44632
+ NONE = /^(|-+|—|–|none|no|nothing|n\/a|na|read[- ]only|reads? only|✗|×)$/i;
44633
+ unquote = (cell) => cell.replace(/`/g, "").trim();
44634
+ }
44635
+ });
44636
+
44232
44637
  // src/test.ts
44233
44638
  var test_exports = {};
44234
44639
  __export(test_exports, {
@@ -44245,14 +44650,14 @@ __export(test_exports, {
44245
44650
  scopeDescriptors: () => scopeDescriptors,
44246
44651
  writeRecordedMocks: () => writeRecordedMocks
44247
44652
  });
44248
- import { existsSync as existsSync46, mkdirSync as mkdirSync23, readFileSync as readFileSync50, writeFileSync as writeFileSync19 } from "node:fs";
44249
- import { join as join43, resolve as resolve40 } from "node:path";
44653
+ import { existsSync as existsSync47, mkdirSync as mkdirSync23, readFileSync as readFileSync51, writeFileSync as writeFileSync19 } from "node:fs";
44654
+ import { join as join44, resolve as resolve40 } from "node:path";
44250
44655
  function readTestConfig(dir) {
44251
44656
  const p = resolve40(dir, TEST_CONFIG_FILE);
44252
- if (!existsSync46(p)) return {};
44657
+ if (!existsSync47(p)) return {};
44253
44658
  let parsed;
44254
44659
  try {
44255
- parsed = JSON.parse(readFileSync50(p, "utf8"));
44660
+ parsed = JSON.parse(readFileSync51(p, "utf8"));
44256
44661
  } catch (e) {
44257
44662
  throw new Error(`${TEST_CONFIG_FILE} is not valid JSON \u2014 ${e.message}`);
44258
44663
  }
@@ -44285,6 +44690,10 @@ function readTestConfig(dir) {
44285
44690
  if (step.component !== void 0 && typeof step.component !== "string") throw new Error(`${TEST_CONFIG_FILE}: journeys[${i}].steps[${stepIndex}].component must be a string`);
44286
44691
  if (step.params !== void 0 && (!step.params || typeof step.params !== "object" || Array.isArray(step.params))) throw new Error(`${TEST_CONFIG_FILE}: journeys[${i}].steps[${stepIndex}].params must be an object`);
44287
44692
  });
44693
+ const input = journey.input;
44694
+ if (input !== void 0 && (!input || typeof input !== "object" || Array.isArray(input) || Object.values(input).some((v) => typeof v !== "string"))) {
44695
+ throw new Error(`${TEST_CONFIG_FILE}: journeys[${i}].input must be an object of string values \u2014 the screen input a nav edge would send, e.g. { "id": "svc-0001" }`);
44696
+ }
44288
44697
  if (!Array.isArray(journey.expectText) || !journey.expectText.length || journey.expectText.some((text5) => typeof text5 !== "string" || !text5.trim())) {
44289
44698
  throw new Error(`${TEST_CONFIG_FILE}: journeys[${i}] needs non-empty expectText values that prove a visible result`);
44290
44699
  }
@@ -44295,7 +44704,7 @@ function readTestConfig(dir) {
44295
44704
  function readNetRules(path2) {
44296
44705
  let parsed;
44297
44706
  try {
44298
- parsed = JSON.parse(readFileSync50(path2, "utf8"));
44707
+ parsed = JSON.parse(readFileSync51(path2, "utf8"));
44299
44708
  } catch (e) {
44300
44709
  throw new Error(`--net ${path2} could not be read as JSON \u2014 ${e.message}`);
44301
44710
  }
@@ -44316,7 +44725,7 @@ function readNetRules(path2) {
44316
44725
  function defaultNetRules(dir) {
44317
44726
  for (const rel of ["dev/net-mocks.json", "tests/net-mocks.json"]) {
44318
44727
  const p = resolve40(dir, rel);
44319
- if (existsSync46(p)) {
44728
+ if (existsSync47(p)) {
44320
44729
  const rules = readNetRules(p);
44321
44730
  if (rules.length) return { rules, from: rel };
44322
44731
  }
@@ -44332,7 +44741,7 @@ function readCannedResponses() {
44332
44741
  ];
44333
44742
  for (const url of candidates) {
44334
44743
  try {
44335
- const parsed = JSON.parse(readFileSync50(url, "utf8"));
44744
+ const parsed = JSON.parse(readFileSync51(url, "utf8"));
44336
44745
  const byKey = {};
44337
44746
  for (const [k, v] of Object.entries(parsed)) {
44338
44747
  if (!k.startsWith("//")) byKey[k] = v;
@@ -44354,9 +44763,7 @@ function makeRecordingWixCall(send2, token, app) {
44354
44763
  refused.push(`${verb} ${path2}`);
44355
44764
  throw new Error(`--record refused ${verb} ${path2}: the path carries a fragment or control character \u2014 not a provable read`);
44356
44765
  }
44357
- const bare = path2.split("?")[0].replace(/\/+$/, "");
44358
- const readShaped = verb === "GET" || verb === "POST" && /\/(query|search|list[^/]*|get[^/]*|count|aggregate|facets|connections)$/i.test(bare);
44359
- if (!readShaped) {
44766
+ if (!isProvenRead(verb, path2)) {
44360
44767
  refused.push(`${verb} ${path2}`);
44361
44768
  throw new Error(`--record refused ${verb} ${path2}: recording sends only proven reads (GET, or POST ending in /query, /search, /list*, /get*, /count, /aggregate, /facets, /connections) \u2014 if this IS a read, add its rule to dev/net-mocks.json by hand`);
44362
44769
  }
@@ -44424,8 +44831,8 @@ function writeRecordedMocks(opts) {
44424
44831
  mkdir: (p) => mkdirSync23(p, { recursive: true }),
44425
44832
  write: (p, s) => writeFileSync19(p, s)
44426
44833
  };
44427
- fs.mkdir(join43(opts.dir, "dev"));
44428
- fs.write(join43(opts.dir, "dev", "net-mocks.json"), JSON.stringify(opts.mocks, null, 2) + "\n");
44834
+ fs.mkdir(join44(opts.dir, "dev"));
44835
+ fs.write(join44(opts.dir, "dev", "net-mocks.json"), JSON.stringify(opts.mocks, null, 2) + "\n");
44429
44836
  return { ok: true, rules, to: "dev/net-mocks.json" };
44430
44837
  }
44431
44838
  function scopeDescriptors(descriptors, requestedIds, invalidScreens) {
@@ -44493,6 +44900,7 @@ async function runCrawl(opts) {
44493
44900
  const suppressed = [];
44494
44901
  const matchSuppression = (screen, actionId, componentId, invariant) => suppressions.find((s) => (s.screen === void 0 || s.screen === screen) && (s.action === void 0 || s.action === actionId) && (s.component === void 0 || s.component === componentId) && (s.invariant === void 0 || s.invariant === invariant));
44495
44902
  const results = [];
44903
+ const writes = [];
44496
44904
  const uncoveredByScreen = /* @__PURE__ */ new Map();
44497
44905
  const coverageFns = /* @__PURE__ */ new Map();
44498
44906
  const bundleOf = new Map(descriptors.map((d) => [d.screenId, d]));
@@ -44530,18 +44938,21 @@ async function runCrawl(opts) {
44530
44938
  const cov = baked.bundleJs && deps.startWorkerCoverage ? await deps.startWorkerCoverage() : null;
44531
44939
  const r = await deps.crawlScreen(args);
44532
44940
  results.push(r);
44941
+ writes.push(...writesFromEffects(baked.screenId, r.effects));
44533
44942
  for (const journey of opts.config?.journeys?.filter((candidate) => candidate.screen === baked.screenId) ?? []) {
44534
44943
  if (!deps.crawlScreenJourney) {
44535
44944
  journeyFindings.push({ screen: baked.screenId, trigger: "boot", invariant: "journey-failed", severity: "error", message: `journey "${journey.name}" cannot run because this CLI's crawl engine is too old`, hint: "Upgrade @wix/himalaya-cli so typed journeys run instead of being silently skipped." });
44536
44945
  journeyResults.push({ name: journey.name, screen: baked.screenId, stepsExercised: 0, ok: false });
44537
44946
  continue;
44538
44947
  }
44539
- const jr = await deps.crawlScreenJourney({ ...args, journey });
44948
+ const jr = await deps.crawlScreenJourney({ ...args, ...journey.input ? { inputOverride: journey.input } : {}, journey });
44949
+ writes.push(...writesFromEffects(baked.screenId, jr.effects));
44540
44950
  journeyFindings.push(...jr.findings);
44541
44951
  journeyResults.push({ name: journey.name, screen: baked.screenId, stepsExercised: jr.stepsExercised, ok: jr.findings.length === 0 });
44542
44952
  }
44543
44953
  if (opts.deep && opts.deep >= 2 && deps.crawlScreenDeep && baked.bundleJs) {
44544
44954
  const dr = await deps.crawlScreenDeep(args, { depth: opts.deep });
44955
+ writes.push(...writesFromEffects(baked.screenId, dr.effects));
44545
44956
  deepTotals.nodesVisited += dr.nodesVisited;
44546
44957
  deepTotals.dispatches += dr.dispatches;
44547
44958
  deepTotals.capped = deepTotals.capped || dr.capped;
@@ -44581,6 +44992,7 @@ async function runCrawl(opts) {
44581
44992
  }
44582
44993
  if (deps.crawlScreenCapabilitySuccess && baked.bundleJs) {
44583
44994
  const cr = await deps.crawlScreenCapabilitySuccess(args);
44995
+ writes.push(...writesFromEffects(baked.screenId, cr.effects));
44584
44996
  capabilityModelled += cr.modelled;
44585
44997
  const keyOfC = deps.findingKey ?? ((f) => `${f.invariant}|${f.at ?? ""}|${f.screen}|${f.message}`);
44586
44998
  const seen = new Set([...r.findings, ...relationFindings].map(keyOfC));
@@ -44657,6 +45069,7 @@ async function runCrawl(opts) {
44657
45069
  list.push(...fns);
44658
45070
  coverageFns.set(target, list);
44659
45071
  }
45072
+ writes.push(...writesFromEffects(target, er.effects));
44660
45073
  edgesFollowed.push({ target, from: shape.from, params: shape.params, via: shape.via });
44661
45074
  if (!er.findings.some((f) => f.invariant === "no-request-issued") && !opts.tabRoots?.has(target) && !fetchProven.has(target)) {
44662
45075
  fetchProven.set(target, { from: shape.from, params: shape.params });
@@ -44681,7 +45094,11 @@ async function runCrawl(opts) {
44681
45094
  if (uncovered.length) uncoveredByScreen.set(screen, uncovered);
44682
45095
  }
44683
45096
  }
44684
- const all = [...results.flatMap((r) => r.findings), ...hostileFindings, ...deepFindings, ...journeyFindings, ...edgeFindings, ...relationFindings].filter((f) => {
45097
+ const inventory = dedupeWrites(writes);
45098
+ const unreachedWrites = targets.flatMap((d) => unreachedNativeWrites(d.screenId, d.bundleJs, inventory));
45099
+ const plan = readDataAccess(opts.dir);
45100
+ const writeFindings = unapprovedWriteFindings([...inventory, ...unreachedWrites], plan);
45101
+ const all = [...results.flatMap((r) => r.findings), ...hostileFindings, ...deepFindings, ...journeyFindings, ...edgeFindings, ...relationFindings, ...writeFindings].filter((f) => {
44685
45102
  if (f.invariant === "no-request-issued" && !f.axis && fetchProven.has(f.screen)) {
44686
45103
  const by = fetchProven.get(f.screen);
44687
45104
  if (!retired.some((x) => x.screen === f.screen)) retired.push({ screen: f.screen, from: by.from, params: by.params });
@@ -44735,6 +45152,9 @@ async function runCrawl(opts) {
44735
45152
  ...deps.crawlScreenRelations ? { relations: { checked: relationsChecked, byKind: relationsByKind } } : {},
44736
45153
  ...deps.crawlScreenCapabilitySuccess ? { capabilities: { modelled: capabilityModelled } } : {},
44737
45154
  ...journeyResults.length ? { journeys: journeyResults } : {},
45155
+ writes: inventory,
45156
+ ...unreachedWrites.length ? { unreachedWrites } : {},
45157
+ ...plan ? { writesPlan: { from: plan.from, rows: plan.rows.length } } : {},
44738
45158
  ...results.some((r) => r.observations) ? { observations: Object.fromEntries(results.filter((r) => r.observations).map((r) => [r.screen, r.observations])) } : {}
44739
45159
  };
44740
45160
  }
@@ -44820,6 +45240,17 @@ function formatReport(r) {
44820
45240
  }
44821
45241
  out.push("");
44822
45242
  }
45243
+ if (r.writes?.length || r.unreachedWrites?.length) {
45244
+ out.push(` Writes to Wix (${r.writes.length}) \u2014 every write the crawl saw a worker attempt (none reached Wix). A write behind a form or a selection shows only under --deep or a journey:`);
45245
+ const byScreenW = /* @__PURE__ */ new Map();
45246
+ for (const w of r.writes) byScreenW.set(w.screen, [...byScreenW.get(w.screen) ?? [], w]);
45247
+ for (const [screen, list] of [...byScreenW].sort(([a], [b]) => a.localeCompare(b))) {
45248
+ for (const w of list) out.push(` ${screen} ${w.action === "boot" ? "on appear" : w.action} ${writeLabel(w)}`);
45249
+ }
45250
+ for (const w of r.unreachedWrites ?? []) out.push(` ${w.screen} (not reached \u2014 in its worker bundle) ${writeLabel(w)}`);
45251
+ out.push(r.writesPlan ? ` checked against ${r.writesPlan.from}'s \`## ${DATA_ACCESS_HEADING}\` table (${r.writesPlan.rows} row${r.writesPlan.rows === 1 ? "" : "s"}) \u2014 unapproved writes are write-not-approved warnings above` : ` no \`## ${DATA_ACCESS_HEADING}\` table (Screen + Wix writes columns) in SPEC.md or MOBILE-UX.md, so nothing checked these against the approved plan \u2014 \`himi docs himi-test\` \u2192 "Writes to Wix"`);
45252
+ out.push("");
45253
+ }
44823
45254
  if (r.shots?.written && r.shots.dir) {
44824
45255
  out.push(` ${r.shots.written} screenshot${r.shots.written === 1 ? "" : "s"} \u2192 ${r.shots.dir}/`);
44825
45256
  }
@@ -44872,6 +45303,7 @@ var init_test = __esm({
44872
45303
  "src/test.ts"() {
44873
45304
  init_wix_gateway_rules();
44874
45305
  init_crawl_layers();
45306
+ init_wix_writes();
44875
45307
  TEST_CONFIG_FILE = "himi-test.json";
44876
45308
  triggerLabel = (f) => (f.trigger === "boot" ? "on appear" : `${f.trigger.key} ${f.trigger.tap ?? "(no id)"} \u2192 ${f.trigger.action}`) + (f.axis ? ` [${f.axis}]` : "");
44877
45309
  }
@@ -44882,14 +45314,14 @@ var mobile_ux_lint_exports = {};
44882
45314
  __export(mobile_ux_lint_exports, {
44883
45315
  mobileUxIssues: () => mobileUxIssues
44884
45316
  });
44885
- import { existsSync as existsSync47, readFileSync as readFileSync51 } from "node:fs";
44886
- import { join as join44 } from "node:path";
45317
+ import { existsSync as existsSync48, readFileSync as readFileSync52 } from "node:fs";
45318
+ import { join as join45 } from "node:path";
44887
45319
  function mobileUxIssues(dir, strict) {
44888
- const file = join44(dir, "MOBILE-UX.md");
44889
- if (!existsSync47(file)) return [];
45320
+ const file = join45(dir, "MOBILE-UX.md");
45321
+ if (!existsSync48(file)) return [];
44890
45322
  let text5;
44891
45323
  try {
44892
- text5 = readFileSync51(file, "utf8");
45324
+ text5 = readFileSync52(file, "utf8");
44893
45325
  } catch {
44894
45326
  return [];
44895
45327
  }
@@ -46564,8 +46996,8 @@ __export(functions_exports, {
46564
46996
  resolveFunctionsTarget: () => resolveFunctionsTarget,
46565
46997
  runRemoteFunctionOperation: () => runRemoteFunctionOperation
46566
46998
  });
46567
- import { readFileSync as readFileSync52, existsSync as existsSync48 } from "node:fs";
46568
- import { join as join45, resolve as resolve41 } from "node:path";
46999
+ import { readFileSync as readFileSync53, existsSync as existsSync49 } from "node:fs";
47000
+ import { join as join46, resolve as resolve41 } from "node:path";
46569
47001
  function resolveFunctionsTarget(input) {
46570
47002
  const explicitUrl = input.functionsUrl?.trim();
46571
47003
  if (explicitUrl) return { mode: "remote", baseUrl: explicitUrl };
@@ -46584,9 +47016,9 @@ async function loadFunctionsApp(dir) {
46584
47016
  }
46585
47017
  const sources = {};
46586
47018
  for (const definition of config.functions.functions) {
46587
- const source = [".ts", ".js", ".mts", ".mjs"].map((extension) => join45(loaded.dir, "functions", `${definition.name}${extension}`)).find(existsSync48);
47019
+ const source = [".ts", ".js", ".mts", ".mjs"].map((extension) => join46(loaded.dir, "functions", `${definition.name}${extension}`)).find(existsSync49);
46588
47020
  if (!source) throw new Error(`missing source for function ${definition.name}; expected functions/${definition.name}.ts`);
46589
- sources[definition.name] = readFileSync52(source, "utf8");
47021
+ sources[definition.name] = readFileSync53(source, "utf8");
46590
47022
  }
46591
47023
  const data = config.cms || config.data ? mergeDataResources(config) : void 0;
46592
47024
  const dataErrors = validateDataResources(data, loaded.name).filter((issue2) => issue2.severity === "error");
@@ -46738,10 +47170,10 @@ __export(studio_reel_exports, {
46738
47170
  utcDay: () => utcDay
46739
47171
  });
46740
47172
  import { createHash as createHash24 } from "node:crypto";
46741
- import { lstatSync as lstatSync7, readFileSync as readFileSync53 } from "node:fs";
46742
- import { basename as basename11, join as join46 } from "node:path";
47173
+ import { lstatSync as lstatSync7, readFileSync as readFileSync54 } from "node:fs";
47174
+ import { basename as basename11, join as join47 } from "node:path";
46743
47175
  function loadMemory2(dir) {
46744
- const m = readJson4(join46(dir, ".himi", "studio", "memory.json"));
47176
+ const m = readJson4(join47(dir, ".himi", "studio", "memory.json"));
46745
47177
  if (!isObj2(m) || m.version !== 1 || !isObj2(m.shots) || !Array.isArray(m.timeline)) return null;
46746
47178
  const shots = [];
46747
47179
  for (const [id, s] of Object.entries(m.shots)) {
@@ -46778,8 +47210,8 @@ function planReel(dir) {
46778
47210
  const memory = loadMemory2(dir);
46779
47211
  const { entries } = readProgress(dir);
46780
47212
  const summary = summarizeProgress(entries);
46781
- const manifest = readJson4(join46(dir, "himalaya.content.json"));
46782
- const tokens = readJson4(join46(dir, "tokens.json"));
47213
+ const manifest = readJson4(join47(dir, "himalaya.content.json"));
47214
+ const tokens = readJson4(join47(dir, "tokens.json"));
46783
47215
  const accentToken = tokens?.colors?.accentPrimary;
46784
47216
  const accentRaw = typeof accentToken === "string" ? accentToken : accentToken?.light;
46785
47217
  const accent = typeof accentRaw === "string" && ACCENT.test(accentRaw) ? accentRaw : DEFAULT_ACCENT;
@@ -46821,10 +47253,10 @@ function inlineImages(dir, plan, budget = REEL_BUDGET_BYTES) {
46821
47253
  const verified = (f) => {
46822
47254
  if (!HISTORY_NAME.test(f)) return null;
46823
47255
  try {
46824
- const path2 = join46(dir, HISTORY_DIR, f);
47256
+ const path2 = join47(dir, HISTORY_DIR, f);
46825
47257
  const st = lstatSync7(path2);
46826
47258
  if (!st.isFile() || st.nlink !== 1) return null;
46827
- const bytes2 = readFileSync53(path2);
47259
+ const bytes2 = readFileSync54(path2);
46828
47260
  return createHash24("sha256").update(bytes2).digest("hex") === f.slice(0, 64) && imageExtension(bytes2) === f.slice(65) ? bytes2 : null;
46829
47261
  } catch {
46830
47262
  return null;
@@ -46958,7 +47390,7 @@ var init_studio_reel = __esm({
46958
47390
  mimeOf = (file) => MIME2[file.slice(65)] ?? "image/png";
46959
47391
  readJson4 = (file) => {
46960
47392
  try {
46961
- return JSON.parse(readFileSync53(file, "utf8"));
47393
+ return JSON.parse(readFileSync54(file, "utf8"));
46962
47394
  } catch {
46963
47395
  return null;
46964
47396
  }
@@ -46989,9 +47421,9 @@ __export(native_decode_exports, {
46989
47421
  runNativeDecode: () => runNativeDecode
46990
47422
  });
46991
47423
  import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
46992
- import { existsSync as existsSync49, mkdtempSync as mkdtempSync2, readFileSync as readFileSync54, rmSync as rmSync11, writeFileSync as writeFileSync20 } from "node:fs";
47424
+ import { existsSync as existsSync50, mkdtempSync as mkdtempSync2, readFileSync as readFileSync55, rmSync as rmSync11, writeFileSync as writeFileSync20 } from "node:fs";
46993
47425
  import { tmpdir as tmpdir3 } from "node:os";
46994
- import { join as join47 } from "node:path";
47426
+ import { join as join48 } from "node:path";
46995
47427
  import { fileURLToPath as fileURLToPath20 } from "node:url";
46996
47428
  function nativeDecodePackageDir() {
46997
47429
  return fileURLToPath20(new URL("../../../core/runtime/ios/HimalayaCore/", import.meta.url));
@@ -47001,8 +47433,8 @@ function nativeDecodeAndroidHarnessDir() {
47001
47433
  }
47002
47434
  function nativeOracleShipped() {
47003
47435
  return {
47004
- ios: existsSync49(join47(nativeDecodePackageDir(), "Package.swift")),
47005
- android: existsSync49(join47(nativeDecodeAndroidHarnessDir(), "gradlew"))
47436
+ ios: existsSync50(join48(nativeDecodePackageDir(), "Package.swift")),
47437
+ android: existsSync50(join48(nativeDecodeAndroidHarnessDir(), "gradlew"))
47006
47438
  };
47007
47439
  }
47008
47440
  function detectNativeDecode(options = {}) {
@@ -47013,7 +47445,7 @@ function detectNativeDecode(options = {}) {
47013
47445
  return { platform: "ios", available: false, reason: `the iOS decoder oracle requires macOS (darwin); this host is ${hostPlatform}` };
47014
47446
  }
47015
47447
  if (!swiftAvailable) return { platform: "ios", available: false, reason: "swift is not on PATH" };
47016
- const packageAvailable = options.packageAvailable ?? existsSync49(join47(packageDir, "Package.swift"));
47448
+ const packageAvailable = options.packageAvailable ?? existsSync50(join48(packageDir, "Package.swift"));
47017
47449
  if (!packageAvailable) {
47018
47450
  return {
47019
47451
  platform: "ios",
@@ -47025,7 +47457,7 @@ function detectNativeDecode(options = {}) {
47025
47457
  }
47026
47458
  function localPropertiesSdk(harnessDir) {
47027
47459
  try {
47028
- const match = readFileSync54(join47(harnessDir, "local.properties"), "utf8").match(/^sdk\.dir\s*=\s*(.+)$/m);
47460
+ const match = readFileSync55(join48(harnessDir, "local.properties"), "utf8").match(/^sdk\.dir\s*=\s*(.+)$/m);
47029
47461
  return match?.[1]?.trim().replace(/\\([ :\\])/g, "$1") ?? null;
47030
47462
  } catch {
47031
47463
  return null;
@@ -47033,8 +47465,8 @@ function localPropertiesSdk(harnessDir) {
47033
47465
  }
47034
47466
  function detectAndroidNativeDecode(options = {}) {
47035
47467
  const harnessDir = options.harnessDir ?? nativeDecodeAndroidHarnessDir();
47036
- const gradlew = join47(harnessDir, "gradlew");
47037
- const gradlewAvailable = options.gradlewAvailable ?? existsSync49(gradlew);
47468
+ const gradlew = join48(harnessDir, "gradlew");
47469
+ const gradlewAvailable = options.gradlewAvailable ?? existsSync50(gradlew);
47038
47470
  if (!gradlewAvailable) {
47039
47471
  return {
47040
47472
  platform: "android",
@@ -47044,7 +47476,7 @@ function detectAndroidNativeDecode(options = {}) {
47044
47476
  }
47045
47477
  const androidHome = options.androidHome ?? process.env.ANDROID_HOME ?? "";
47046
47478
  const sdk = androidHome.trim() || (options.localPropertiesSdk !== void 0 ? options.localPropertiesSdk : localPropertiesSdk(harnessDir));
47047
- const sdkAvailable = options.sdkAvailable ?? (typeof sdk === "string" && sdk.length > 0 && existsSync49(sdk));
47479
+ const sdkAvailable = options.sdkAvailable ?? (typeof sdk === "string" && sdk.length > 0 && existsSync50(sdk));
47048
47480
  if (!sdkAvailable) {
47049
47481
  return {
47050
47482
  platform: "android",
@@ -47116,14 +47548,14 @@ function parseNativeDecodeOutput(stdout, expectedScreenIds, platform = "ios") {
47116
47548
  return verdicts;
47117
47549
  }
47118
47550
  function runNativeDecode(options) {
47119
- const descriptorDir = mkdtempSync2(join47(tmpdir3(), "himi-native-decode-"));
47551
+ const descriptorDir = mkdtempSync2(join48(tmpdir3(), "himi-native-decode-"));
47120
47552
  const startedAt = Date.now();
47121
47553
  try {
47122
47554
  for (const descriptor of options.descriptors) {
47123
47555
  if (typeof descriptor.descriptorJson !== "string") {
47124
47556
  throw new Error(`native decode cannot judge ${descriptor.screenId}: the release builder did not return its exact baked JSON bytes`);
47125
47557
  }
47126
- writeFileSync20(join47(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
47558
+ writeFileSync20(join48(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
47127
47559
  }
47128
47560
  options.progress?.("building cached Swift oracle");
47129
47561
  execFileSync5("swift", ["build", "--package-path", options.packageDir, "--product", "himi-decode-oracle"], {
@@ -47135,7 +47567,7 @@ function runNativeDecode(options) {
47135
47567
  encoding: "utf8"
47136
47568
  }).trim();
47137
47569
  options.progress?.(`running real Swift decoder over ${options.descriptors.length} screen${options.descriptors.length === 1 ? "" : "s"}`);
47138
- const stdout = execFileSync5(join47(binPath, "himi-decode-oracle"), ["--dir", descriptorDir], {
47570
+ const stdout = execFileSync5(join48(binPath, "himi-decode-oracle"), ["--dir", descriptorDir], {
47139
47571
  stdio: ["ignore", "pipe", "pipe"],
47140
47572
  encoding: "utf8"
47141
47573
  });
@@ -47153,15 +47585,15 @@ function runNativeDecode(options) {
47153
47585
  }
47154
47586
  }
47155
47587
  function runAndroidNativeDecode(options) {
47156
- const descriptorDir = mkdtempSync2(join47(tmpdir3(), "himi-native-decode-android-"));
47157
- const outputPath = join47(descriptorDir, "verdicts.ndjson");
47588
+ const descriptorDir = mkdtempSync2(join48(tmpdir3(), "himi-native-decode-android-"));
47589
+ const outputPath = join48(descriptorDir, "verdicts.ndjson");
47158
47590
  const startedAt = Date.now();
47159
47591
  try {
47160
47592
  for (const descriptor of options.descriptors) {
47161
47593
  if (typeof descriptor.descriptorJson !== "string") {
47162
47594
  throw new Error(`native decode cannot judge ${descriptor.screenId}: the release builder did not return its exact baked JSON bytes`);
47163
47595
  }
47164
- writeFileSync20(join47(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
47596
+ writeFileSync20(join48(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
47165
47597
  }
47166
47598
  options.progress?.(`running real Kotlin decoder over ${options.descriptors.length} screen${options.descriptors.length === 1 ? "" : "s"}`);
47167
47599
  execFileSync5(options.gradlew, [
@@ -47176,7 +47608,7 @@ function runAndroidNativeDecode(options) {
47176
47608
  stdio: ["ignore", "ignore", "pipe"],
47177
47609
  encoding: "utf8"
47178
47610
  });
47179
- const stdout = readFileSync54(outputPath, "utf8");
47611
+ const stdout = readFileSync55(outputPath, "utf8");
47180
47612
  return {
47181
47613
  platform: "android",
47182
47614
  verdicts: parseNativeDecodeOutput(stdout, options.descriptors.map((descriptor) => descriptor.screenId), "android"),
@@ -47246,13 +47678,13 @@ __export(browser_authoring_exports, {
47246
47678
  });
47247
47679
  import { createServer as createServer3 } from "node:http";
47248
47680
  import { createHash as createHash25, randomBytes as randomBytes4 } from "node:crypto";
47249
- import { existsSync as existsSync50, readdirSync as readdirSync24, readFileSync as readFileSync55, realpathSync as realpathSync5, statSync as statSync12 } from "node:fs";
47250
- import { join as join48, posix, relative as relative8, sep as sep12 } from "node:path";
47681
+ import { existsSync as existsSync51, readdirSync as readdirSync24, readFileSync as readFileSync56, realpathSync as realpathSync5, statSync as statSync12 } from "node:fs";
47682
+ import { join as join49, posix, relative as relative8, sep as sep12 } from "node:path";
47251
47683
  function humanMs(ms) {
47252
47684
  return ms < 1e3 ? `${ms}ms` : `${Math.round(ms / 1e3)}s`;
47253
47685
  }
47254
47686
  function collectWorkerSources(tier5SrcDir, bundleName) {
47255
- const root = join48(tier5SrcDir, bundleName);
47687
+ const root = join49(tier5SrcDir, bundleName);
47256
47688
  const files = {};
47257
47689
  const visitedDirs = /* @__PURE__ */ new Set();
47258
47690
  const walk2 = (abs, rel) => {
@@ -47260,16 +47692,16 @@ function collectWorkerSources(tier5SrcDir, bundleName) {
47260
47692
  if (visitedDirs.has(real)) return;
47261
47693
  visitedDirs.add(real);
47262
47694
  for (const name of readdirSync24(abs).sort()) {
47263
- const childAbs = join48(abs, name);
47695
+ const childAbs = join49(abs, name);
47264
47696
  const childRel = posix.join(rel, name);
47265
47697
  if (statSync12(childAbs).isDirectory()) {
47266
47698
  walk2(childAbs, childRel);
47267
47699
  } else if (/\.(ts|tsx|js|mjs|json)$/.test(name)) {
47268
- files[childRel] = readFileSync55(childAbs, "utf8");
47700
+ files[childRel] = readFileSync56(childAbs, "utf8");
47269
47701
  }
47270
47702
  }
47271
47703
  };
47272
- if (!existsSync50(root)) return null;
47704
+ if (!existsSync51(root)) return null;
47273
47705
  walk2(root, `/tier5-src/${bundleName}`);
47274
47706
  const entry = [`/tier5-src/${bundleName}/index.ts`, `/tier5-src/${bundleName}/index.js`].find((p) => p in files);
47275
47707
  if (!entry) return null;
@@ -47286,20 +47718,20 @@ function collectRelativeSiblings(files, tier5SrcDir) {
47286
47718
  const spec = m[1];
47287
47719
  const virtual = posix.normalize(posix.join(dir, spec));
47288
47720
  if (!virtual.startsWith("/tier5-src/")) continue;
47289
- const onDisk = join48(tier5SrcDir, virtual.slice("/tier5-src/".length));
47721
+ const onDisk = join49(tier5SrcDir, virtual.slice("/tier5-src/".length));
47290
47722
  const candidates = [
47291
47723
  onDisk,
47292
47724
  `${onDisk}.ts`,
47293
47725
  `${onDisk}.js`,
47294
47726
  onDisk.replace(/\.js$/, ".ts"),
47295
- join48(onDisk, "index.ts"),
47296
- join48(onDisk, "index.js")
47727
+ join49(onDisk, "index.ts"),
47728
+ join49(onDisk, "index.js")
47297
47729
  ];
47298
47730
  for (const abs of candidates) {
47299
- if (!existsSync50(abs) || statSync12(abs).isDirectory()) continue;
47731
+ if (!existsSync51(abs) || statSync12(abs).isDirectory()) continue;
47300
47732
  const key2 = `/tier5-src/${relative8(tier5SrcDir, abs).split(sep12).join(posix.sep)}`;
47301
47733
  if (key2 in files) break;
47302
- files[key2] = readFileSync55(abs, "utf8");
47734
+ files[key2] = readFileSync56(abs, "utf8");
47303
47735
  queue.push(key2);
47304
47736
  break;
47305
47737
  }
@@ -47334,7 +47766,7 @@ function collectSdkModules(files, resolveSubpath) {
47334
47766
  function sdkSubpathReader(resolve48) {
47335
47767
  return (sub) => {
47336
47768
  try {
47337
- return readFileSync55(resolve48(`@wix/himalaya/${sub}`), "utf8");
47769
+ return readFileSync56(resolve48(`@wix/himalaya/${sub}`), "utf8");
47338
47770
  } catch {
47339
47771
  return null;
47340
47772
  }
@@ -47506,8 +47938,8 @@ __export(coverage_exports, {
47506
47938
  suppressedNativeKeys: () => suppressedNativeKeys,
47507
47939
  trendAndStore: () => trendAndStore
47508
47940
  });
47509
- import { existsSync as existsSync51, mkdirSync as mkdirSync24, readFileSync as readFileSync56, writeFileSync as writeFileSync21 } from "node:fs";
47510
- import { dirname as dirname26, join as join49 } from "node:path";
47941
+ import { existsSync as existsSync52, mkdirSync as mkdirSync24, readFileSync as readFileSync57, writeFileSync as writeFileSync21 } from "node:fs";
47942
+ import { dirname as dirname26, join as join50 } from "node:path";
47511
47943
  function suppressedNativeKeys(oracle) {
47512
47944
  return [...oracle.ios ? [] : ["native-decode|ios"], ...oracle.android ? [] : ["native-decode|android"]];
47513
47945
  }
@@ -47687,11 +48119,11 @@ function assembleLedger(r, options = {}) {
47687
48119
  return out;
47688
48120
  }
47689
48121
  function trendAndStore(dir, entries, suppressed) {
47690
- const p = join49(dir, LEDGER_FILE);
48122
+ const p = join50(dir, LEDGER_FILE);
47691
48123
  let previous = null;
47692
48124
  try {
47693
- if (existsSync51(p)) {
47694
- const parsed = JSON.parse(readFileSync56(p, "utf8"));
48125
+ if (existsSync52(p)) {
48126
+ const parsed = JSON.parse(readFileSync57(p, "utf8"));
47695
48127
  if (Array.isArray(parsed.keys)) previous = parsed.keys.filter((k) => typeof k === "string");
47696
48128
  }
47697
48129
  } catch {
@@ -47730,11 +48162,11 @@ function formatLedger(entries, trend) {
47730
48162
  return out.join("\n");
47731
48163
  }
47732
48164
  function diffAndStoreObservations(dir, current) {
47733
- const p = join49(dir, OBS_FILE);
48165
+ const p = join50(dir, OBS_FILE);
47734
48166
  let previous = null;
47735
48167
  try {
47736
- if (existsSync51(p)) {
47737
- const parsed = JSON.parse(readFileSync56(p, "utf8"));
48168
+ if (existsSync52(p)) {
48169
+ const parsed = JSON.parse(readFileSync57(p, "utf8"));
47738
48170
  if (parsed.screens && typeof parsed.screens === "object" && !Array.isArray(parsed.screens) && Object.values(parsed.screens).every(
47739
48171
  (v) => Array.isArray(v) && v.every((l) => typeof l === "string")
47740
48172
  )) {
@@ -47783,8 +48215,8 @@ var LEDGER_FILE, OBS_FILE;
47783
48215
  var init_coverage = __esm({
47784
48216
  "src/coverage.ts"() {
47785
48217
  init_native_decode();
47786
- LEDGER_FILE = join49(".himi", "test-ledger.json");
47787
- OBS_FILE = join49(".himi", "test-observations.json");
48218
+ LEDGER_FILE = join50(".himi", "test-ledger.json");
48219
+ OBS_FILE = join50(".himi", "test-observations.json");
47788
48220
  }
47789
48221
  });
47790
48222
 
@@ -47795,11 +48227,11 @@ __export(design_kits_exports, {
47795
48227
  kitsDir: () => kitsDir,
47796
48228
  listKits: () => listKits2
47797
48229
  });
47798
- import { existsSync as existsSync52, readFileSync as readFileSync57, readdirSync as readdirSync25 } from "node:fs";
47799
- import { join as join50, basename as basename12 } from "node:path";
48230
+ import { existsSync as existsSync53, readFileSync as readFileSync58, readdirSync as readdirSync25 } from "node:fs";
48231
+ import { join as join51, basename as basename12 } from "node:path";
47800
48232
  import { fileURLToPath as fileURLToPath21 } from "node:url";
47801
48233
  function kitsDir() {
47802
- return KIT_DIR_CANDIDATES.find((p) => existsSync52(p));
48234
+ return KIT_DIR_CANDIDATES.find((p) => existsSync53(p));
47803
48235
  }
47804
48236
  function listKits2() {
47805
48237
  const dir = kitsDir();
@@ -47823,7 +48255,7 @@ In a published @wix/himalaya-cli this is a packaging bug \u2014 the kits should
47823
48255
  if (!kits.includes(name)) {
47824
48256
  return { ok: false, kit: name, kits, problems: [`no design kit named "${name}". Available: ${kits.join(", ")}`] };
47825
48257
  }
47826
- const raw = readFileSync57(join50(dir, `${name}.json`), "utf8");
48258
+ const raw = readFileSync58(join51(dir, `${name}.json`), "utf8");
47827
48259
  let parsed;
47828
48260
  try {
47829
48261
  parsed = JSON.parse(raw);
@@ -47847,9 +48279,9 @@ var init_design_kits = __esm({
47847
48279
  "src/design-kits.ts"() {
47848
48280
  KIT_DIR_CANDIDATES = [
47849
48281
  // in-repo: tools/himi-cli/src/design-kits.ts → <repo>/stdlib/design-kits/
47850
- join50(fileURLToPath21(new URL("../../..", import.meta.url)), "stdlib", "design-kits"),
48282
+ join51(fileURLToPath21(new URL("../../..", import.meta.url)), "stdlib", "design-kits"),
47851
48283
  // published CLI: <pkg>/dist/cli.mjs → <pkg>/stdlib/design-kits/
47852
- join50(fileURLToPath21(new URL("..", import.meta.url)), "stdlib", "design-kits")
48284
+ join51(fileURLToPath21(new URL("..", import.meta.url)), "stdlib", "design-kits")
47853
48285
  ];
47854
48286
  }
47855
48287
  });
@@ -47863,11 +48295,11 @@ __export(assets_exports, {
47863
48295
  removeAssetPayload: () => removeAssetPayload,
47864
48296
  verifyReport: () => verifyReport
47865
48297
  });
47866
- import { readFileSync as readFileSync58 } from "node:fs";
48298
+ import { readFileSync as readFileSync59 } from "node:fs";
47867
48299
  import { basename as basename13 } from "node:path";
47868
48300
  function declaredAssets(dir) {
47869
48301
  return collectAssets(dir).map((a) => {
47870
- const bytes2 = readFileSync58(a.path);
48302
+ const bytes2 = readFileSync59(a.path);
47871
48303
  return {
47872
48304
  name: a.name,
47873
48305
  sha256: assetSha256(bytes2),
@@ -47944,7 +48376,7 @@ function verifyReport(dir, app, manifest) {
47944
48376
  };
47945
48377
  }
47946
48378
  function addAssetPayload(manifest, filePath, opts) {
47947
- const bytes2 = readFileSync58(filePath);
48379
+ const bytes2 = readFileSync59(filePath);
47948
48380
  const name = opts.name ?? basename13(filePath);
47949
48381
  const sha2565 = assetSha256(bytes2);
47950
48382
  const record2 = {
@@ -49340,8 +49772,7 @@ async function localizeConfigResponse(res, kind, header) {
49340
49772
  }
49341
49773
  function makeRunHandler(opts) {
49342
49774
  const mount = opts.chrome === "app" ? "/app" : "/preview";
49343
- const serve = makeServeHandler({
49344
- store: opts.store,
49775
+ const serveOpts = {
49345
49776
  defaultApp: opts.app,
49346
49777
  // A local plane is a pin: nothing mutates, and the owner rungs cannot work anyway — a service
49347
49778
  // worker never sees a `Cookie` header (it is a forbidden request header in both directions), so
@@ -49356,7 +49787,9 @@ function makeRunHandler(opts) {
49356
49787
  webwixBridge: Boolean(opts.serve?.webwixFetch),
49357
49788
  manifestCacheTtlMs: 0,
49358
49789
  ...opts.serve
49359
- });
49790
+ };
49791
+ const serve = makeServeHandler({ store: opts.store, ...serveOpts });
49792
+ const ownerRungOn = serveOpts.webwixOwnerRung !== false;
49360
49793
  return async (request) => {
49361
49794
  const url = new URL(request.url);
49362
49795
  const path2 = url.pathname;
@@ -49377,6 +49810,10 @@ function makeRunHandler(opts) {
49377
49810
  if (path2.startsWith("/v1/flags/")) {
49378
49811
  return json4(200, { app: opts.app, definitions: {} }, { "cache-control": "no-store" });
49379
49812
  }
49813
+ if (path2 === "/v1/owner/sites" && !ownerRungOn && (request.method === "GET" || request.method === "HEAD")) {
49814
+ const headers2 = { "cache-control": "no-store" };
49815
+ return request.method === "HEAD" ? new Response(null, { status: 200, headers: { "content-type": "application/json; charset=utf-8", ...headers2 } }) : json4(200, { sites: [] }, headers2);
49816
+ }
49380
49817
  if (path2 === "/v1/telemetry/ingest") {
49381
49818
  if (request.method !== "POST") return json4(405, { error: "method_not_allowed" });
49382
49819
  try {
@@ -49579,21 +50016,21 @@ var init_preview_assets_generated = __esm({
49579
50016
  "../../packages/serve/src/preview-assets.generated.ts"() {
49580
50017
  init_preview_assets_format();
49581
50018
  PREVIEW_ASSET_MANIFEST = [
49582
- { key: "assets/index-BWCUbjcx.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1051200, sha256: "bc39b1298d3c675988ae421ad8950e8ef46a46b6e8e3644aaea24e6ab1a1cd21" },
49583
- { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 1051208, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
49584
- { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 1055818, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
49585
- { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 1544402, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
49586
- { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 1635326, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
49587
- { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 1726310, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
49588
- { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 1817290, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
49589
- { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 1908162, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
49590
- { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 1999142, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
49591
- { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 2107778, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
49592
- { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 2216862, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
49593
- { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 2325910, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
49594
- { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 2433610, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
49595
- { key: "index.html", contentType: "text/html; charset=utf-8", offset: 2521718, length: 4947, sha256: "3d489aa5d99040dfa05a2175da5de975b1ebdd9f6d855c5609074cbc2c592d27" },
49596
- { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 2526665, length: 17209, sha256: "dffc9718e2faa63ae2cc919e0a842f585ed565aaaa7c53778e90379442c731b6" }
50019
+ { key: "assets/index-BJFiKWYO.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1051133, sha256: "99b754e0231cb82ad2cafbca64ba3cd3bdc74aa53d8351f6a57828034c3a3f44" },
50020
+ { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 1051141, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
50021
+ { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 1055751, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
50022
+ { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 1544335, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
50023
+ { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 1635259, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
50024
+ { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 1726243, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
50025
+ { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 1817223, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
50026
+ { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 1908095, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
50027
+ { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 1999075, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
50028
+ { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 2107711, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
50029
+ { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 2216795, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
50030
+ { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 2325843, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
50031
+ { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 2433543, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
50032
+ { key: "index.html", contentType: "text/html; charset=utf-8", offset: 2521651, length: 4947, sha256: "1c4dfb3d465449d6a73677293df29d93b1c236b46602b553a58ef6e89c52de11" },
50033
+ { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 2526598, length: 17209, sha256: "dffc9718e2faa63ae2cc919e0a842f585ed565aaaa7c53778e90379442c731b6" }
49597
50034
  ];
49598
50035
  }
49599
50036
  });
@@ -49609,9 +50046,9 @@ __export(run_exports, {
49609
50046
  runPackage: () => runPackage,
49610
50047
  withoutProdServe: () => withoutProdServe
49611
50048
  });
49612
- import { cpSync as cpSync2, existsSync as existsSync53, mkdirSync as mkdirSync25, mkdtempSync as mkdtempSync3, readFileSync as readFileSync59, rmSync as rmSync12, statSync as statSync13 } from "node:fs";
50049
+ import { cpSync as cpSync2, existsSync as existsSync54, mkdirSync as mkdirSync25, mkdtempSync as mkdtempSync3, readFileSync as readFileSync60, rmSync as rmSync12, statSync as statSync13 } from "node:fs";
49613
50050
  import { tmpdir as tmpdir4 } from "node:os";
49614
- import { dirname as dirname27, join as join51 } from "node:path";
50051
+ import { dirname as dirname27, join as join52 } from "node:path";
49615
50052
  import { fileURLToPath as fileURLToPath22 } from "node:url";
49616
50053
  function makePackageRunHandler(opts) {
49617
50054
  const store = new FileReleaseStore(opts.releaseRoot);
@@ -49639,9 +50076,9 @@ async function fetchBytes(url) {
49639
50076
  }
49640
50077
  async function resolvePackage(source, workDir) {
49641
50078
  const isUrl2 = /^https?:\/\//i.test(source);
49642
- if (!isUrl2 && existsSync53(source) && statSync13(source).isDirectory()) {
49643
- const appDirManifest = join51(source, "release-manifest.json");
49644
- if (existsSync53(appDirManifest)) {
50079
+ if (!isUrl2 && existsSync54(source) && statSync13(source).isDirectory()) {
50080
+ const appDirManifest = join52(source, "release-manifest.json");
50081
+ if (existsSync54(appDirManifest)) {
49645
50082
  const opened2 = openPackageDir(source);
49646
50083
  if (!opened2.validation.ok) {
49647
50084
  throw new Error(`package is not valid:
@@ -49650,9 +50087,9 @@ async function resolvePackage(source, workDir) {
49650
50087
  const app2 = opened2.header?.app ?? opened2.manifest?.app;
49651
50088
  if (!app2) throw new Error(`${source} has no app id (no himi-package.json and no manifest.app)`);
49652
50089
  if (!isSafeAppId(app2)) throw new Error(`${source} declares an unsafe app id ${JSON.stringify(app2)} \u2014 refusing to open it`);
49653
- const root2 = workDir ?? mkdtempSync3(join51(tmpdir4(), "himi-run-"));
49654
- const target = join51(root2, app2);
49655
- if (join51(source) !== target) {
50090
+ const root2 = workDir ?? mkdtempSync3(join52(tmpdir4(), "himi-run-"));
50091
+ const target = join52(root2, app2);
50092
+ if (join52(source) !== target) {
49656
50093
  mkdirSync25(root2, { recursive: true });
49657
50094
  rmSync12(target, { recursive: true, force: true });
49658
50095
  cpSync2(source, target, { recursive: true });
@@ -49669,8 +50106,8 @@ async function resolvePackage(source, workDir) {
49669
50106
  }
49670
50107
  throw new Error(`${source} is a directory but has no release-manifest.json \u2014 is it a package?`);
49671
50108
  }
49672
- const bytes2 = isUrl2 ? await fetchBytes(source) : readFileSync59(source);
49673
- const root = workDir ?? mkdtempSync3(join51(tmpdir4(), "himi-run-"));
50109
+ const bytes2 = isUrl2 ? await fetchBytes(source) : readFileSync60(source);
50110
+ const root = workDir ?? mkdtempSync3(join52(tmpdir4(), "himi-run-"));
49674
50111
  const { app, appDir: appDir2 } = unpackTo(bytes2, root);
49675
50112
  const opened = openPackageDir(appDir2);
49676
50113
  if (!opened.validation.ok) {
@@ -49693,14 +50130,14 @@ async function loadPreviewAssets() {
49693
50130
  const mod = await Promise.resolve().then(() => (init_preview_assets_generated(), preview_assets_generated_exports));
49694
50131
  const here = dirname27(fileURLToPath22(import.meta.url));
49695
50132
  const candidates = [
49696
- join51(here, "preview-assets.generated.bin"),
50133
+ join52(here, "preview-assets.generated.bin"),
49697
50134
  // published: copied next to the bundled CLI
49698
- join51(here, "../../../packages/serve/src/preview-assets.generated.bin")
50135
+ join52(here, "../../../packages/serve/src/preview-assets.generated.bin")
49699
50136
  // in-repo checkout
49700
50137
  ];
49701
50138
  for (const path2 of candidates) {
49702
- if (!existsSync53(path2)) continue;
49703
- const blob = readFileSync59(path2);
50139
+ if (!existsSync54(path2)) continue;
50140
+ const blob = readFileSync60(path2);
49704
50141
  return mod.assemblePreviewAssetsFromBlob(new Uint8Array(blob.buffer, blob.byteOffset, blob.byteLength));
49705
50142
  }
49706
50143
  return /* @__PURE__ */ new Map();
@@ -49797,9 +50234,9 @@ __export(run_watch_exports, {
49797
50234
  watchedContentDir: () => watchedContentDir,
49798
50235
  writeRunRecord: () => writeRunRecord
49799
50236
  });
49800
- import { closeSync as closeSync2, constants, existsSync as existsSync54, fstatSync, lstatSync as lstatSync8, statSync as statSync14, mkdirSync as mkdirSync26, openSync as openSync2, readFileSync as readFileSync60, realpathSync as realpathSync6, renameSync as renameSync5, rmSync as rmSync13, writeFileSync as writeFileSync22, watch as fsWatch } from "node:fs";
50237
+ import { closeSync as closeSync2, constants, existsSync as existsSync55, fstatSync, lstatSync as lstatSync8, statSync as statSync14, mkdirSync as mkdirSync26, openSync as openSync2, readFileSync as readFileSync61, realpathSync as realpathSync6, renameSync as renameSync5, rmSync as rmSync13, writeFileSync as writeFileSync22, watch as fsWatch } from "node:fs";
49801
50238
  import { randomUUID as randomUUID3 } from "node:crypto";
49802
- import { extname as extname4, join as join52, resolve as resolve42, sep as sep13 } from "node:path";
50239
+ import { extname as extname4, join as join53, resolve as resolve42, sep as sep13 } from "node:path";
49803
50240
  function diffReleases(prev, next) {
49804
50241
  const bundles = Object.keys(next.bundles).filter((b) => prev.bundles[b]?.sha256 !== next.bundles[b].sha256).sort();
49805
50242
  const changedBundles = new Set(bundles);
@@ -49839,7 +50276,7 @@ function watchContent(dir, onChange, opts = {}) {
49839
50276
  let timer = null;
49840
50277
  const pending = /* @__PURE__ */ new Set();
49841
50278
  const watcher2 = watch2(dir, { recursive: true }, (_event, file) => {
49842
- if (!file || ignoredChange(file, (rel) => isDir(join52(dir, rel)))) return;
50279
+ if (!file || ignoredChange(file, (rel) => isDir(join53(dir, rel)))) return;
49843
50280
  pending.add(file);
49844
50281
  if (timer) clearTimeout(timer);
49845
50282
  timer = setTimeout(() => {
@@ -49860,7 +50297,7 @@ function watchedContentDir(source) {
49860
50297
  if (!source || /^https?:\/\//i.test(source)) return null;
49861
50298
  const dir = resolve42(source);
49862
50299
  try {
49863
- return statSync14(dir).isDirectory() && existsSync54(join52(dir, "himalaya.content.json")) ? dir : null;
50300
+ return statSync14(dir).isDirectory() && existsSync55(join53(dir, "himalaya.content.json")) ? dir : null;
49864
50301
  } catch {
49865
50302
  return null;
49866
50303
  }
@@ -49917,7 +50354,7 @@ function serialize(task) {
49917
50354
  });
49918
50355
  }
49919
50356
  function himiDir(dir) {
49920
- const root = join52(dir, ".himi");
50357
+ const root = join53(dir, ".himi");
49921
50358
  try {
49922
50359
  return lstatSync8(root).isDirectory() ? root : null;
49923
50360
  } catch (e) {
@@ -49929,11 +50366,11 @@ function himiDir(dir) {
49929
50366
  function writeRunRecord(dir, record2) {
49930
50367
  const root = himiDir(dir);
49931
50368
  if (!root) return false;
49932
- const tmp = join52(root, `.run.json.${process.pid}.${randomUUID3()}.tmp`);
50369
+ const tmp = join53(root, `.run.json.${process.pid}.${randomUUID3()}.tmp`);
49933
50370
  try {
49934
50371
  writeFileSync22(tmp, `${JSON.stringify(record2, null, 2)}
49935
50372
  `, { flag: "wx" });
49936
- renameSync5(tmp, join52(root, "run.json"));
50373
+ renameSync5(tmp, join53(root, "run.json"));
49937
50374
  } catch (e) {
49938
50375
  rmSync13(tmp, { force: true });
49939
50376
  throw e;
@@ -49941,7 +50378,7 @@ function writeRunRecord(dir, record2) {
49941
50378
  return true;
49942
50379
  }
49943
50380
  function removeRunRecord(dir, owner) {
49944
- const root = join52(dir, ".himi");
50381
+ const root = join53(dir, ".himi");
49945
50382
  try {
49946
50383
  if (!lstatSync8(root).isDirectory()) return;
49947
50384
  } catch {
@@ -49956,16 +50393,16 @@ function removeRunRecord(dir, owner) {
49956
50393
  }
49957
50394
  if (typeof pid === "number" && pid !== owner) return;
49958
50395
  }
49959
- rmSync13(join52(root, "run.json"), { force: true });
50396
+ rmSync13(join53(root, "run.json"), { force: true });
49960
50397
  }
49961
50398
  function boardFile(dir, pathname) {
49962
50399
  let rel = null;
49963
- if (pathname === "/__himi/studio/" || pathname === "/__himi/studio/index.html") rel = join52("studio", "index.html");
49964
- else if (pathname === "/__himi/studio/state.js") rel = join52("studio", "state.js");
49965
- else if (pathname === "/__himi/studio/timeline.js") rel = join52("studio", "timeline.js");
50400
+ if (pathname === "/__himi/studio/" || pathname === "/__himi/studio/index.html") rel = join53("studio", "index.html");
50401
+ else if (pathname === "/__himi/studio/state.js") rel = join53("studio", "state.js");
50402
+ else if (pathname === "/__himi/studio/timeline.js") rel = join53("studio", "timeline.js");
49966
50403
  else {
49967
50404
  const m = /^\/__himi\/history\/([0-9a-f]{64}\.(png|jpg|gif|webp))$/.exec(pathname);
49968
- if (m) rel = join52("history", m[1]);
50405
+ if (m) rel = join53("history", m[1]);
49969
50406
  }
49970
50407
  if (!rel) return null;
49971
50408
  const bytes2 = readConfined(resolve42(dir, ".himi"), rel);
@@ -49981,7 +50418,7 @@ function readConfined(root, rel) {
49981
50418
  const parts = rel.split(sep13);
49982
50419
  let at = root;
49983
50420
  for (let i = 0; i < parts.length; i++) {
49984
- at = join52(at, parts[i]);
50421
+ at = join53(at, parts[i]);
49985
50422
  const st = lstatSync8(at);
49986
50423
  if (st.isSymbolicLink() || !(i === parts.length - 1 ? st.isFile() : st.isDirectory())) return null;
49987
50424
  }
@@ -49990,7 +50427,7 @@ function readConfined(root, rel) {
49990
50427
  const fd = openSync2(at, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
49991
50428
  try {
49992
50429
  if (!fstatSync(fd).isFile()) return null;
49993
- return readFileSync60(fd);
50430
+ return readFileSync61(fd);
49994
50431
  } finally {
49995
50432
  closeSync2(fd);
49996
50433
  }
@@ -50041,7 +50478,7 @@ __export(run_browser_exports, {
50041
50478
  runInBrowser: () => runInBrowser
50042
50479
  });
50043
50480
  import { createServer as createServer5 } from "node:http";
50044
- import { existsSync as existsSync55, readFileSync as readFileSync61 } from "node:fs";
50481
+ import { existsSync as existsSync56, readFileSync as readFileSync62 } from "node:fs";
50045
50482
  import { spawnSync as spawnSync3 } from "node:child_process";
50046
50483
  import { dirname as dirname28, resolve as resolve43 } from "node:path";
50047
50484
  import { fileURLToPath as fileURLToPath23 } from "node:url";
@@ -50049,7 +50486,7 @@ function stagedDir() {
50049
50486
  return process.env.HIMI_BROWSER_PLANE_OUT ? resolve43(process.env.HIMI_BROWSER_PLANE_OUT) : resolve43(REPO, "packages", "serve", "dist", "browser-plane");
50050
50487
  }
50051
50488
  function missingStaged() {
50052
- return [...SERVED].filter((f) => !existsSync55(resolve43(stagedDir(), f))).sort();
50489
+ return [...SERVED].filter((f) => !existsSync56(resolve43(stagedDir(), f))).sort();
50053
50490
  }
50054
50491
  function ensureBrowserPlaneStaged() {
50055
50492
  if (missingStaged().length === 0) return { ok: true, detail: "already staged" };
@@ -50134,7 +50571,7 @@ async function runInBrowser(releaseRoot, app, header, opts = {}) {
50134
50571
  }
50135
50572
  const name = path2 === "/" || path2 === "/index.html" ? "host.html" : path2.slice(1);
50136
50573
  if (SERVED.has(name)) {
50137
- if (!existsSync55(resolve43(stagedDir(), name))) {
50574
+ if (!existsSync56(resolve43(stagedDir(), name))) {
50138
50575
  const restaged = ensureBrowserPlaneStaged();
50139
50576
  if (!restaged.ok) {
50140
50577
  res.writeHead(503, { "content-type": "application/json" });
@@ -50144,7 +50581,7 @@ async function runInBrowser(releaseRoot, app, header, opts = {}) {
50144
50581
  }
50145
50582
  let bytes2;
50146
50583
  try {
50147
- bytes2 = readFileSync61(resolve43(stagedDir(), name));
50584
+ bytes2 = readFileSync62(resolve43(stagedDir(), name));
50148
50585
  } catch (e) {
50149
50586
  res.writeHead(503, { "content-type": "application/json" });
50150
50587
  res.end(JSON.stringify({ error: "browser_plane_unstaged", file: name, detail: String(e) }));
@@ -50605,7 +51042,7 @@ __export(preflight_exports, {
50605
51042
  parseStoreConfig: () => parseStoreConfig,
50606
51043
  requireEnv: () => requireEnv
50607
51044
  });
50608
- import { existsSync as existsSync56, readFileSync as readFileSync62 } from "node:fs";
51045
+ import { existsSync as existsSync57, readFileSync as readFileSync63 } from "node:fs";
50609
51046
  import { isAbsolute as isAbsolute3, relative as relative9, resolve as resolve44 } from "node:path";
50610
51047
  function isInsideRepo(p) {
50611
51048
  const rel = relative9(REPO_ROOT3, resolve44(p));
@@ -50662,10 +51099,10 @@ function parseStoreConfig(text5) {
50662
51099
  }
50663
51100
  function loadStoreConfig(app) {
50664
51101
  const p = storeConfigPath(app);
50665
- if (!existsSync56(p)) {
51102
+ if (!existsSync57(p)) {
50666
51103
  throw new Error(`No store config at ${p}. Create apps/${app}/store/store.config.yaml first (see /setup-store-signing).`);
50667
51104
  }
50668
- return parseStoreConfig(readFileSync62(p, "utf8"));
51105
+ return parseStoreConfig(readFileSync63(p, "utf8"));
50669
51106
  }
50670
51107
  var init_preflight = __esm({
50671
51108
  "../store-release/preflight.ts"() {
@@ -50684,19 +51121,19 @@ __export(credential_profile_exports, {
50684
51121
  profilePath: () => profilePath,
50685
51122
  resolveStoreCredentials: () => resolveStoreCredentials
50686
51123
  });
50687
- import { existsSync as existsSync57, readFileSync as readFileSync63 } from "node:fs";
51124
+ import { existsSync as existsSync58, readFileSync as readFileSync64 } from "node:fs";
50688
51125
  import { homedir as homedir5 } from "node:os";
50689
- import { join as join53 } from "node:path";
51126
+ import { join as join54 } from "node:path";
50690
51127
  function credentialsDir(home = homedir5()) {
50691
- return join53(home, ".himalaya", "store-credentials");
51128
+ return join54(home, ".himalaya", "store-credentials");
50692
51129
  }
50693
51130
  function profilePath(name, home = homedir5()) {
50694
51131
  if (!PROFILE_NAME.test(name)) {
50695
51132
  throw new Error(
50696
- `credential profile ${JSON.stringify(name)} is not a profile name \u2014 use letters, digits, ".", "_" or "-" starting with a letter or digit. It names ${join53(credentialsDir(home), "<name>.json")}; it is never a path.`
51133
+ `credential profile ${JSON.stringify(name)} is not a profile name \u2014 use letters, digits, ".", "_" or "-" starting with a letter or digit. It names ${join54(credentialsDir(home), "<name>.json")}; it is never a path.`
50697
51134
  );
50698
51135
  }
50699
- return join53(credentialsDir(home), `${name}.json`);
51136
+ return join54(credentialsDir(home), `${name}.json`);
50700
51137
  }
50701
51138
  function parseProfile(text5, label2) {
50702
51139
  let doc;
@@ -50727,8 +51164,8 @@ function resolveStoreCredentials(opts) {
50727
51164
  const { app, cfg } = opts;
50728
51165
  const env = opts.env ?? process.env;
50729
51166
  const home = opts.home ?? homedir5();
50730
- const read = opts.readProfile ?? ((p) => readFileSync63(p, "utf8"));
50731
- const exists = opts.profileExists ?? existsSync57;
51167
+ const read = opts.readProfile ?? ((p) => readFileSync64(p, "utf8"));
51168
+ const exists = opts.profileExists ?? existsSync58;
50732
51169
  const ios = cfg.ios ?? {};
50733
51170
  const flagName = opts.profileFlag?.trim();
50734
51171
  const named = namedProfile(cfg, opts.profileFlag);
@@ -50808,9 +51245,9 @@ __export(preflight_app_exports, {
50808
51245
  preflightApp: () => preflightApp,
50809
51246
  profileAccount: () => profileAccount
50810
51247
  });
50811
- import { readFileSync as readFileSync64, existsSync as existsSync58, readdirSync as readdirSync26, statSync as statSync15 } from "node:fs";
51248
+ import { readFileSync as readFileSync65, existsSync as existsSync59, readdirSync as readdirSync26, statSync as statSync15 } from "node:fs";
50812
51249
  import { createHash as createHash26 } from "node:crypto";
50813
- import { join as join54 } from "node:path";
51250
+ import { join as join55 } from "node:path";
50814
51251
  function analyze(projectYml, storeConfig, icon, account) {
50815
51252
  const f = [];
50816
51253
  const has = (re, s) => re.test(s);
@@ -50874,13 +51311,13 @@ function profileAccount(app, storeConfig, opts = {}) {
50874
51311
  }
50875
51312
  }
50876
51313
  async function preflightApp(app, opts = {}) {
50877
- const projectYml = existsSync58(iosProjectYml(app)) ? readFileSync64(iosProjectYml(app), "utf8") : "";
50878
- const storeConfig = existsSync58(storeConfigPath(app)) ? readFileSync64(storeConfigPath(app), "utf8") : "";
51314
+ const projectYml = existsSync59(iosProjectYml(app)) ? readFileSync65(iosProjectYml(app), "utf8") : "";
51315
+ const storeConfig = existsSync59(storeConfigPath(app)) ? readFileSync65(storeConfigPath(app), "utf8") : "";
50879
51316
  if (!storeConfig) return [{ level: "error", message: `no apps/${app}/store/store.config.yaml \u2014 run /setup-store-signing first.` }];
50880
51317
  const findings = analyze(
50881
51318
  projectYml,
50882
51319
  storeConfig,
50883
- await appIconState(join54(appDir(app), "ios")),
51320
+ await appIconState(join55(appDir(app), "ios")),
50884
51321
  profileAccount(app, storeConfig, opts)
50885
51322
  );
50886
51323
  findings.push(...analyzeListing(metadataFilesPresent(app), listScreenshots(app)));
@@ -50909,17 +51346,17 @@ function analyzeSubmissionRecord(appLevel, review, opts) {
50909
51346
  return f;
50910
51347
  }
50911
51348
  function appLevelMetadataFiles(app, locale = "en-US") {
50912
- const root = join54(storeDir(app), "metadata");
51349
+ const root = join55(storeDir(app), "metadata");
50913
51350
  const out = /* @__PURE__ */ new Set();
50914
- if (existsSync58(root)) {
51351
+ if (existsSync59(root)) {
50915
51352
  for (const n of readdirSync26(root)) if (n.endsWith(".txt")) out.add(n);
50916
51353
  }
50917
- if (existsSync58(join54(root, locale, "privacy_url.txt"))) out.add("privacy_url.txt");
51354
+ if (existsSync59(join55(root, locale, "privacy_url.txt"))) out.add("privacy_url.txt");
50918
51355
  return out;
50919
51356
  }
50920
51357
  function reviewInfoFiles(app) {
50921
- const dir = join54(storeDir(app), "review_information");
50922
- if (!existsSync58(dir)) return /* @__PURE__ */ new Set();
51358
+ const dir = join55(storeDir(app), "review_information");
51359
+ if (!existsSync59(dir)) return /* @__PURE__ */ new Set();
50923
51360
  return new Set(readdirSync26(dir).filter((n) => n.endsWith(".txt")));
50924
51361
  }
50925
51362
  function isValidAppStoreSize(w, h) {
@@ -51029,12 +51466,12 @@ function analyzePlayListing(assets) {
51029
51466
  return f;
51030
51467
  }
51031
51468
  function playListingAssets(app, locale = "en-US") {
51032
- const dir = join54(storeDir(app), "play", locale, "images");
51469
+ const dir = join55(storeDir(app), "play", locale, "images");
51033
51470
  const read = (names) => {
51034
51471
  for (const name of names) {
51035
- const p = join54(dir, name);
51472
+ const p = join55(dir, name);
51036
51473
  try {
51037
- if (statSync15(p).isFile()) return { name, bytes: readFileSync64(p) };
51474
+ if (statSync15(p).isFile()) return { name, bytes: readFileSync65(p) };
51038
51475
  } catch {
51039
51476
  }
51040
51477
  }
@@ -51065,26 +51502,26 @@ function analyzeListing(metaFiles, shots) {
51065
51502
  return f;
51066
51503
  }
51067
51504
  function metadataFilesPresent(app, locale = "en-US") {
51068
- const dir = join54(storeDir(app), "metadata", locale);
51069
- if (!existsSync58(dir)) return /* @__PURE__ */ new Set();
51505
+ const dir = join55(storeDir(app), "metadata", locale);
51506
+ if (!existsSync59(dir)) return /* @__PURE__ */ new Set();
51070
51507
  return new Set(readdirSync26(dir).filter((n) => n.endsWith(".txt")));
51071
51508
  }
51072
51509
  function listScreenshots(app, locale = "en-US") {
51073
51510
  const out = [];
51074
- const flat = join54(storeDir(app), "screenshots", locale);
51075
- if (existsSync58(flat)) {
51511
+ const flat = join55(storeDir(app), "screenshots", locale);
51512
+ if (existsSync59(flat)) {
51076
51513
  for (const png of readdirSync26(flat).filter((n) => n.endsWith(".png"))) {
51077
- const sz = pngSize(readFileSync64(join54(flat, png)));
51514
+ const sz = pngSize(readFileSync65(join55(flat, png)));
51078
51515
  if (sz) out.push({ name: png, w: sz.w, h: sz.h });
51079
51516
  }
51080
51517
  }
51081
- const legacy = join54(storeDir(app), "metadata", locale, "screenshots");
51082
- if (existsSync58(legacy)) {
51518
+ const legacy = join55(storeDir(app), "metadata", locale, "screenshots");
51519
+ if (existsSync59(legacy)) {
51083
51520
  for (const deviceDir of readdirSync26(legacy)) {
51084
- const d = join54(legacy, deviceDir);
51521
+ const d = join55(legacy, deviceDir);
51085
51522
  if (!statSync15(d).isDirectory()) continue;
51086
51523
  for (const png of readdirSync26(d).filter((n) => n.endsWith(".png"))) {
51087
- const sz = pngSize(readFileSync64(join54(d, png)));
51524
+ const sz = pngSize(readFileSync65(join55(d, png)));
51088
51525
  if (sz) out.push({ name: `${deviceDir}/${png}`, w: sz.w, h: sz.h });
51089
51526
  }
51090
51527
  }
@@ -51092,13 +51529,13 @@ function listScreenshots(app, locale = "en-US") {
51092
51529
  return out;
51093
51530
  }
51094
51531
  function placeholderHashes2() {
51095
- const templateIos = join54(appDir("_template"), "ios");
51096
- if (!existsSync58(templateIos)) return [LEGACY_PLACEHOLDER_SHA2562];
51532
+ const templateIos = join55(appDir("_template"), "ios");
51533
+ if (!existsSync59(templateIos)) return [LEGACY_PLACEHOLDER_SHA2562];
51097
51534
  for (const target of readdirSync26(templateIos)) {
51098
- const p = join54(templateIos, target, "Assets.xcassets", "AppIcon.appiconset", "icon_1024.png");
51535
+ const p = join55(templateIos, target, "Assets.xcassets", "AppIcon.appiconset", "icon_1024.png");
51099
51536
  try {
51100
51537
  if (!statSync15(p).isFile()) continue;
51101
- return [LEGACY_PLACEHOLDER_SHA2562, createHash26("sha256").update(readFileSync64(p)).digest("hex")];
51538
+ return [LEGACY_PLACEHOLDER_SHA2562, createHash26("sha256").update(readFileSync65(p)).digest("hex")];
51102
51539
  } catch {
51103
51540
  }
51104
51541
  }
@@ -51129,12 +51566,12 @@ async function pixelIssues(bytes2) {
51129
51566
  }
51130
51567
  }
51131
51568
  async function appIconState(iosDir) {
51132
- if (!existsSync58(iosDir)) return { exists: false, isPlaceholder: false, storeIssues: [] };
51569
+ if (!existsSync59(iosDir)) return { exists: false, isPlaceholder: false, storeIssues: [] };
51133
51570
  for (const target of readdirSync26(iosDir)) {
51134
- const p = join54(iosDir, target, "Assets.xcassets", "AppIcon.appiconset", "icon_1024.png");
51571
+ const p = join55(iosDir, target, "Assets.xcassets", "AppIcon.appiconset", "icon_1024.png");
51135
51572
  try {
51136
51573
  if (!statSync15(p).isFile()) continue;
51137
- const bytes2 = readFileSync64(p);
51574
+ const bytes2 = readFileSync65(p);
51138
51575
  const hash = createHash26("sha256").update(bytes2).digest("hex");
51139
51576
  const pixels = await pixelIssues(bytes2);
51140
51577
  return {
@@ -51206,7 +51643,7 @@ __export(version_exports, {
51206
51643
  setAndroidVersion: () => setAndroidVersion,
51207
51644
  setIosVersion: () => setIosVersion
51208
51645
  });
51209
- import { readFileSync as readFileSync65, writeFileSync as writeFileSync23 } from "node:fs";
51646
+ import { readFileSync as readFileSync66, writeFileSync as writeFileSync23 } from "node:fs";
51210
51647
  function readIosVersion(projectYml) {
51211
51648
  const marketing = matchScalar(projectYml, "CFBundleShortVersionString");
51212
51649
  const build2 = matchScalar(projectYml, "CFBundleVersion");
@@ -51264,8 +51701,8 @@ function setAndroidVersion(gradle, v) {
51264
51701
  function bumpVersion(app, opts) {
51265
51702
  const iosPath = iosProjectYml(app);
51266
51703
  const gradlePath = androidGradle(app);
51267
- const iosYml = readFileSync65(iosPath, "utf8");
51268
- const gradle = readFileSync65(gradlePath, "utf8");
51704
+ const iosYml = readFileSync66(iosPath, "utf8");
51705
+ const gradle = readFileSync66(gradlePath, "utf8");
51269
51706
  const current = readIosVersion(iosYml);
51270
51707
  const next = {
51271
51708
  marketing: opts.marketing ?? current.marketing,
@@ -51282,7 +51719,7 @@ var init_version = __esm({
51282
51719
  });
51283
51720
 
51284
51721
  // ../store-release/register-bundle.ts
51285
- import { readFileSync as readFileSync66 } from "node:fs";
51722
+ import { readFileSync as readFileSync67 } from "node:fs";
51286
51723
  import { createPrivateKey as createPrivateKey2, sign } from "node:crypto";
51287
51724
  function b64url2(b) {
51288
51725
  return Buffer.from(b).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
@@ -51300,7 +51737,7 @@ function bundleIdBody(identifier, name, platform = "IOS") {
51300
51737
  async function registerBundleId(opts) {
51301
51738
  const env = requireEnv(["ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_KEY_PATH"]);
51302
51739
  assertCredentialOutsideRepo("ASC_KEY_PATH", env.ASC_KEY_PATH);
51303
- const jwt = ascJwt(env.ASC_KEY_ID, env.ASC_ISSUER_ID, readFileSync66(env.ASC_KEY_PATH, "utf8"), Math.floor(Date.now() / 1e3));
51740
+ const jwt = ascJwt(env.ASC_KEY_ID, env.ASC_ISSUER_ID, readFileSync67(env.ASC_KEY_PATH, "utf8"), Math.floor(Date.now() / 1e3));
51304
51741
  const res = await fetch(`${ASC_API}/v1/bundleIds`, {
51305
51742
  method: "POST",
51306
51743
  headers: { Authorization: `Bearer ${jwt}`, "Content-Type": "application/json" },
@@ -51354,7 +51791,7 @@ __export(asc_status_exports, {
51354
51791
  ascStatus: () => ascStatus,
51355
51792
  buildVerdict: () => buildVerdict
51356
51793
  });
51357
- import { readFileSync as readFileSync67 } from "node:fs";
51794
+ import { readFileSync as readFileSync68 } from "node:fs";
51358
51795
  function buildVerdict(builds) {
51359
51796
  if (!builds.length) {
51360
51797
  return "NO builds registered \u2014 if you uploaded recently and it's been >15 min, the binary was REJECTED IN PROCESSING (reason is email-only; run validate-binary.ts / scan-purpose-strings.ts to reproduce it).";
@@ -51368,7 +51805,7 @@ function buildVerdict(builds) {
51368
51805
  function client(creds) {
51369
51806
  const env = creds ?? requireEnv(["ASC_KEY_ID", "ASC_ISSUER_ID", "ASC_KEY_PATH"]);
51370
51807
  assertCredentialOutsideRepo("ASC_KEY_PATH", env.ASC_KEY_PATH);
51371
- const jwt = ascJwt(env.ASC_KEY_ID, env.ASC_ISSUER_ID, readFileSync67(env.ASC_KEY_PATH, "utf8"), Math.floor(Date.now() / 1e3));
51808
+ const jwt = ascJwt(env.ASC_KEY_ID, env.ASC_ISSUER_ID, readFileSync68(env.ASC_KEY_PATH, "utf8"), Math.floor(Date.now() / 1e3));
51372
51809
  return async (path2) => {
51373
51810
  const r = await fetch(`${ASC}${path2}`, { headers: { Authorization: `Bearer ${jwt}` } });
51374
51811
  return { status: r.status, json: await r.json().catch(() => ({})) };
@@ -51528,7 +51965,7 @@ __export(secrets_exports, {
51528
51965
  runRemoteSecretsCommand: () => runRemoteSecretsCommand,
51529
51966
  runSecretsCommand: () => runSecretsCommand
51530
51967
  });
51531
- import { readFileSync as readFileSync68 } from "node:fs";
51968
+ import { readFileSync as readFileSync69 } from "node:fs";
51532
51969
  function environment(value) {
51533
51970
  if (value === "preview" || value === "production") return value;
51534
51971
  throw new Error("--environment must be preview or production");
@@ -51559,7 +51996,7 @@ async function runSecretsCommand(operation, flags, io, options = {}) {
51559
51996
  io.log(JSON.stringify(await vault.list({ app, environment: env })));
51560
51997
  return 0;
51561
51998
  }
51562
- const value = options.readValue ? options.readValue() : readFileSync68(0, "utf8").replace(/\r?\n$/, "");
51999
+ const value = options.readValue ? options.readValue() : readFileSync69(0, "utf8").replace(/\r?\n$/, "");
51563
52000
  if (operation === "set") await vault.set({ app, environment: env, name, value });
51564
52001
  else if (operation === "rotate") await vault.rotate({ app, environment: env, name, value });
51565
52002
  else throw new Error(`unknown secrets operation ${operation}`);
@@ -51588,7 +52025,7 @@ async function runRemoteSecretsCommand(operation, flags, io, options) {
51588
52025
  }
51589
52026
  let body;
51590
52027
  if (operation !== "list") {
51591
- const value = options.readValue ? options.readValue() : readFileSync68(0, "utf8").replace(/\r?\n$/, "");
52028
+ const value = options.readValue ? options.readValue() : readFileSync69(0, "utf8").replace(/\r?\n$/, "");
51592
52029
  body = JSON.stringify({ value });
51593
52030
  }
51594
52031
  const response = await (options.fetchImpl ?? fetch)(url, { method: operation === "list" ? "GET" : operation === "set" ? "PUT" : "POST", headers: headers2, body });
@@ -51624,15 +52061,15 @@ __export(notify_exports, {
51624
52061
  runNotify: () => runNotify,
51625
52062
  saveAdminSession: () => saveAdminSession
51626
52063
  });
51627
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync27, readFileSync as readFileSync69, writeFileSync as writeFileSync24 } from "node:fs";
52064
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync27, readFileSync as readFileSync70, writeFileSync as writeFileSync24 } from "node:fs";
51628
52065
  import { homedir as homedir6 } from "node:os";
51629
- import { dirname as dirname30, join as join55 } from "node:path";
52066
+ import { dirname as dirname30, join as join56 } from "node:path";
51630
52067
  function defaultSessionsPath() {
51631
- return process.env.HIMI_ADMIN_SESSIONS_FILE ?? join55(homedir6(), ".himalaya", "admin-sessions.json");
52068
+ return process.env.HIMI_ADMIN_SESSIONS_FILE ?? join56(homedir6(), ".himalaya", "admin-sessions.json");
51632
52069
  }
51633
52070
  function loadSessionsFile(path2 = defaultSessionsPath()) {
51634
52071
  try {
51635
- return JSON.parse(readFileSync69(path2, "utf8"));
52072
+ return JSON.parse(readFileSync70(path2, "utf8"));
51636
52073
  } catch {
51637
52074
  return {};
51638
52075
  }
@@ -52043,14 +52480,17 @@ var init_visitor_auth = __esm({
52043
52480
  });
52044
52481
 
52045
52482
  // src/site-analyze/copy-clean.ts
52483
+ function isTemplatePlaceholder(text5) {
52484
+ return PLACEHOLDER_RE.test(text5);
52485
+ }
52046
52486
  function cleanText(raw, cap) {
52047
52487
  const s = String(raw ?? "").replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\s+/g, " ").trim();
52048
52488
  return s.length > cap ? `${s.slice(0, cap - 1).trimEnd()}\u2026` : s;
52049
52489
  }
52050
52490
  function stripPii(text5) {
52051
52491
  return text5.replace(EMAIL_RE2, "").replace(PHONE_SHAPE_RE, (m) => {
52052
- const digits = (m.match(ANY_DIGIT_RE) ?? []).length;
52053
- return digits >= PHONE_MIN_DIGITS && PHONE_DIGIT_RUN_RE.test(m) ? "" : m;
52492
+ const digits2 = (m.match(ANY_DIGIT_RE) ?? []).length;
52493
+ return digits2 >= PHONE_MIN_DIGITS && PHONE_DIGIT_RUN_RE.test(m) ? "" : m;
52054
52494
  }).replace(/\s+/g, " ").trim();
52055
52495
  }
52056
52496
  function isBoilerplate(text5) {
@@ -52086,7 +52526,7 @@ var init_copy_clean = __esm({
52086
52526
  PHONE_DIGIT_RUN_RE = new RegExp(`[${DIGIT}]{3}`);
52087
52527
  ANY_DIGIT_RE = new RegExp(`[${DIGIT}]`, "g");
52088
52528
  BOILERPLATE_RE = /powered by wix|this site was designed with|wix\.com|all rights reserved|©|\(c\)\s*\d{4}|privacy policy|terms (of use|and conditions)|accessibility statement|cookie (policy|settings|preferences)|accept all|manage preferences|do not sell/i;
52089
- PLACEHOLDER_RE = /i'?m a (paragraph|title|description)|click here to add|add your own text|double click to edit|this is a great (place|space)|tell your visitors|lorem ipsum|service title|item title|your paragraph text/i;
52529
+ PLACEHOLDER_RE = /i'?m a (paragraph|title|description)|click here to add|add your own text|double click to edit|this is a great (place|space)|tell your visitors|lorem ipsum|service title|item title|your paragraph text|tell clients about what you['’]?re offering|to add a description,? click|click manage services|booking settings\s*>\s*booking polic|in your dashboard,? go to|go to booking settings/i;
52090
52530
  ABOUT_HEADING_RE_SOURCE = "^(?:about\\b|who we are|who are we|our story|our mission|\u05DE\u05D9 \u05D0\u05E0\u05D7\u05E0\u05D5)";
52091
52531
  }
52092
52532
  });
@@ -52158,6 +52598,49 @@ function planDetail(p) {
52158
52598
  const bits = [cycle, perks.slice(0, 4).join("; ")].filter(Boolean);
52159
52599
  return bits.length ? bits.join(" \xB7 ") : void 0;
52160
52600
  }
52601
+ function moneyText(m) {
52602
+ if (!m || typeof m !== "object") return void 0;
52603
+ const o = m;
52604
+ const formatted = str4(o.formattedValue ?? o.formatted);
52605
+ if (formatted.trim()) return formatted.trim();
52606
+ const raw = o.value ?? o.amount;
52607
+ const value = typeof raw === "number" ? String(raw) : str4(raw);
52608
+ if (!value.trim()) return void 0;
52609
+ return [value.trim(), str4(o.currency).trim()].filter(Boolean).join(" ");
52610
+ }
52611
+ function servicePrice(s) {
52612
+ const payment = s.payment && typeof s.payment === "object" ? s.payment : void 0;
52613
+ if (!payment) return void 0;
52614
+ const rate = str4(payment.rateType).toUpperCase();
52615
+ const fixed = payment.fixed;
52616
+ const varied = payment.varied;
52617
+ const custom = payment.custom;
52618
+ if (rate === "NO_FEE") return void 0;
52619
+ if (rate === "VARIED" || !rate && varied) {
52620
+ const min = moneyText(varied?.minPrice);
52621
+ return moneyText(varied?.defaultPrice) ?? (min ? `from ${min}` : void 0);
52622
+ }
52623
+ if (rate === "CUSTOM" || !rate && custom) {
52624
+ return copyOrDrop(custom?.description, 80) || void 0;
52625
+ }
52626
+ const legacy = typeof payment.price === "number" || typeof payment.price === "string" ? moneyText({ value: payment.price, currency: payment.currency ?? s.currency }) : moneyText(payment.price);
52627
+ return moneyText(fixed?.price) ?? legacy;
52628
+ }
52629
+ function serviceDetail(s) {
52630
+ const payment = s.payment && typeof s.payment === "object" ? s.payment : void 0;
52631
+ if (!payment) return void 0;
52632
+ if (str4(payment.rateType).toUpperCase() === "NO_FEE") return "free";
52633
+ const options = payment.options && typeof payment.options === "object" ? payment.options : {};
52634
+ const hasPlanIds = asArr(payment.pricingPlanIds).length > 0;
52635
+ const modes = [
52636
+ options.online === true && "online",
52637
+ options.inPerson === true && "in person",
52638
+ (options.pricingPlan === true || hasPlanIds) && "with a pricing plan"
52639
+ ].filter((m) => !!m);
52640
+ if (!modes.length) return void 0;
52641
+ const how = modes.length === 1 ? `pay ${modes[0]} only` : `pay ${modes.slice(0, -1).join(", ")} or ${modes[modes.length - 1]}`;
52642
+ return options.deposit === true ? `${how} (deposit)` : how;
52643
+ }
52161
52644
  function formLabels(x, depth = 0, out = []) {
52162
52645
  if (x == null || depth > 5) return out;
52163
52646
  if (Array.isArray(x)) {
@@ -52178,7 +52661,7 @@ function formDetail(f) {
52178
52661
  return names.length ? names.join(", ") : void 0;
52179
52662
  }
52180
52663
  function side(call, def) {
52181
- if (!call) return Promise.resolve({ status: "error", count: 0, note: "no-auth", items: [] });
52664
+ if (!call) return Promise.resolve({ status: "skipped", count: 0, note: "no-auth", items: [] });
52182
52665
  const attempts = [
52183
52666
  { method: def.method, path: def.path, body: def.body, itemsOf: def.itemsOf },
52184
52667
  ...def.fallbacks ?? []
@@ -52208,14 +52691,20 @@ function side(call, def) {
52208
52691
  })();
52209
52692
  }
52210
52693
  function toItems(def, raw) {
52211
- return raw.slice(0, 12).map((it, i) => ({
52212
- id: str4((def.idOf ? def.idOf(it) : it.id) ?? `row-${i}`),
52213
- title: def.titleOf(it) || "(untitled)",
52214
- imageUrl: def.imageOf?.(it),
52215
- price: def.priceOf?.(it) || str4(it.price?.formatted) || str4(it.formattedPrice) || void 0,
52216
- detail: def.detailOf?.(it),
52217
- description: copyOrDrop(def.descriptionOf ? def.descriptionOf(it) : genericDescription(it), 240) || void 0
52218
- }));
52694
+ return raw.slice(0, 12).map((it, i) => {
52695
+ const rawDescription = def.descriptionOf ? def.descriptionOf(it) : genericDescription(it);
52696
+ const description = copyOrDrop(rawDescription, 240) || void 0;
52697
+ const template = rawDescription && !description && isTemplatePlaceholder(rawDescription) ? stripPii(cleanText(rawDescription, 90)) : void 0;
52698
+ return {
52699
+ id: str4((def.idOf ? def.idOf(it) : it.id) ?? `row-${i}`),
52700
+ title: def.titleOf(it) || "(untitled)",
52701
+ imageUrl: def.imageOf?.(it),
52702
+ price: def.priceOf?.(it) || str4(it.price?.formatted) || str4(it.formattedPrice) || void 0,
52703
+ detail: def.detailOf?.(it),
52704
+ description,
52705
+ ...template ? { templateText: template } : {}
52706
+ };
52707
+ });
52219
52708
  }
52220
52709
  async function runDualProbes(owner, visitor, definitions2 = ALL) {
52221
52710
  const rows = await Promise.all(definitions2.map(async (def) => {
@@ -52225,7 +52714,10 @@ async function runDualProbes(owner, visitor, definitions2 = ALL) {
52225
52714
  vertical: def.vertical,
52226
52715
  owner: { status: o.status, count: o.count, httpStatus: o.httpStatus, note: o.note },
52227
52716
  visitor: { status: vis.status, count: vis.count, httpStatus: vis.httpStatus, note: vis.note },
52228
- appInvisible: o.count > 0 && vis.count === 0 && vis.status !== "wrong-path",
52717
+ // Only a visitor request that was actually MADE can show the app cannot see a row. With no
52718
+ // client id (#3269) the visitor side is `skipped`, and every owner-visible vertical would
52719
+ // otherwise read APP-INVISIBLE off a request nobody sent (codebot review on #3283).
52720
+ appInvisible: o.count > 0 && vis.count === 0 && vis.status !== "wrong-path" && vis.status !== "skipped",
52229
52721
  imageless,
52230
52722
  items: toItems(def, o.items.length ? o.items : vis.items)
52231
52723
  };
@@ -52416,7 +52908,9 @@ var init_probes = __esm({
52416
52908
  body: { query: { paging: { limit: 50 } } },
52417
52909
  itemsOf: (d) => asArr(d.services),
52418
52910
  titleOf: (s) => str4(s.name),
52419
- imageOf: serviceImage
52911
+ imageOf: serviceImage,
52912
+ priceOf: servicePrice,
52913
+ detailOf: serviceDetail
52420
52914
  },
52421
52915
  {
52422
52916
  vertical: "events",
@@ -52953,7 +53447,7 @@ var init_traits = __esm({
52953
53447
  });
52954
53448
 
52955
53449
  // src/site-analyze/tokens.ts
52956
- function channels(css) {
53450
+ function channels2(css) {
52957
53451
  const canonical3 = canonicalHex(css);
52958
53452
  if (!canonical3) return null;
52959
53453
  return [
@@ -52963,11 +53457,11 @@ function channels(css) {
52963
53457
  ];
52964
53458
  }
52965
53459
  function isNearWhite(css) {
52966
- const c = channels(css);
53460
+ const c = channels2(css);
52967
53461
  return !!c && c[0] >= 240 && c[1] >= 240 && c[2] >= 240;
52968
53462
  }
52969
53463
  function isNearBlack(css) {
52970
- const c = channels(css);
53464
+ const c = channels2(css);
52971
53465
  return !!c && c[0] <= 80 && c[1] <= 80 && c[2] <= 80;
52972
53466
  }
52973
53467
  function usableCssColor(v) {
@@ -53106,12 +53600,12 @@ var init_tokens3 = __esm({
53106
53600
  function channel2(hex2, offset) {
53107
53601
  return Number.parseInt(hex2.slice(offset, offset + 2), 16);
53108
53602
  }
53109
- function toHex(r, g, b) {
53603
+ function toHex2(r, g, b) {
53110
53604
  return `#${[r, g, b].map((value) => Math.round(value).toString(16).padStart(2, "0")).join("")}`.toUpperCase();
53111
53605
  }
53112
53606
  function mix2(color, background, colorWeight) {
53113
53607
  const values = [1, 3, 5].map((offset) => channel2(color, offset) * colorWeight + channel2(background, offset) * (1 - colorWeight));
53114
- return toHex(values[0], values[1], values[2]);
53608
+ return toHex2(values[0], values[1], values[2]);
53115
53609
  }
53116
53610
  function relativeLuminance2(color) {
53117
53611
  const linear = [1, 3, 5].map((offset) => {
@@ -53855,6 +54349,10 @@ function taglineFrom(identity, pages, published) {
53855
54349
  const hero = copyOrDrop(home?.heroCopy, CAP2.tagline);
53856
54350
  return hero ? { text: hero, source: "hero" } : null;
53857
54351
  }
54352
+ function tabLabel(label2) {
54353
+ const trimmed = label2.replace(/\s+page$/i, "").trim();
54354
+ return trimmed && trimmed !== label2.trim() && !/^(front|landing|title|web)$/i.test(trimmed) ? trimmed : label2;
54355
+ }
53858
54356
  function suggestKeys(args) {
53859
54357
  const { nav, sections, tagline, installedVerticals } = args;
53860
54358
  const out = {};
@@ -53868,7 +54366,7 @@ function suggestKeys(args) {
53868
54366
  const allowed2 = (rule) => !rule.vertical || installed.has(rule.vertical);
53869
54367
  for (const label2 of nav) {
53870
54368
  for (const rule of KEY_RULES) {
53871
- if (rule.tab && allowed2(rule) && rule.re.test(label2)) set(rule.tab, label2);
54369
+ if (rule.tab && allowed2(rule) && rule.re.test(label2)) set(rule.tab, tabLabel(label2));
53872
54370
  }
53873
54371
  }
53874
54372
  for (const section2 of sections) {
@@ -54458,6 +54956,234 @@ var init_net_guard = __esm({
54458
54956
  }
54459
54957
  });
54460
54958
 
54959
+ // src/site-analyze/placeholders.ts
54960
+ function textFields(look) {
54961
+ if (look.unavailable) return [];
54962
+ const out = [];
54963
+ const add = (text5, where, narrative = false) => {
54964
+ if (text5 && text5.trim()) out.push({ text: text5, where, narrative });
54965
+ };
54966
+ for (const label2 of look.navLabels) add(label2, "nav");
54967
+ for (const p of look.pages) {
54968
+ for (const label2 of p.navLabels) add(label2, "nav");
54969
+ add(p.title, `${p.url} (title)`);
54970
+ add(p.heroCopy, p.url, true);
54971
+ for (const h of p.headings) add(h, p.url);
54972
+ for (const c of p.ctaLabels ?? []) add(c, p.url);
54973
+ for (const s of p.sections ?? []) {
54974
+ add(s.heading, p.url);
54975
+ add(s.body, p.url, true);
54976
+ }
54977
+ for (const f of p.faq ?? []) {
54978
+ add(f.q, `${p.url} (faq)`, true);
54979
+ add(f.a, `${p.url} (faq)`, true);
54980
+ }
54981
+ for (const t of p.testimonials ?? []) add(t, `${p.url} (testimonials)`, true);
54982
+ add(p.about, `${p.url} (about)`, true);
54983
+ add(p.meta?.ogTitle, `${p.url} (og:title)`);
54984
+ add(p.meta?.description, `${p.url} (meta)`, true);
54985
+ for (const t of p.templateText ?? []) add(t, p.url);
54986
+ }
54987
+ return out;
54988
+ }
54989
+ function pushWhere(map, key2, value, where) {
54990
+ const row = map.get(key2) ?? { value, where: [] };
54991
+ if (!row.where.includes(where) && row.where.length < CAP3.where) row.where.push(where);
54992
+ map.set(key2, row);
54993
+ }
54994
+ function templateContacts(identity, fields) {
54995
+ const hits = /* @__PURE__ */ new Map();
54996
+ const phone = identity.contact.phone ?? "";
54997
+ if (phone && TEMPLATE_PHONES.has(digits(phone))) pushWhere(hits, `p:${digits(phone)}`, phone.trim(), "identity.contact.phone");
54998
+ const email = identity.contact.email ?? "";
54999
+ if (email && TEMPLATE_EMAIL_RE.test(email)) pushWhere(hits, `e:${email.toLowerCase()}`, email.trim(), "identity.contact.email");
55000
+ const address = identity.contact.address ?? "";
55001
+ if (address && TEMPLATE_ADDRESS_RE.test(address)) pushWhere(hits, "a:terry", "500 Terry Francine Street", "identity.contact.address");
55002
+ for (const f of fields) {
55003
+ for (const m of f.text.matchAll(PHONE_IN_TEXT_RE)) {
55004
+ const d = digits(m[0]);
55005
+ if (TEMPLATE_PHONES.has(d)) pushWhere(hits, `p:${d}`, m[0].trim(), f.where);
55006
+ }
55007
+ const e = f.text.match(TEMPLATE_EMAIL_RE)?.[0];
55008
+ if (e) pushWhere(hits, `e:${e.toLowerCase()}`, e, f.where);
55009
+ if (TEMPLATE_ADDRESS_RE.test(f.text)) pushWhere(hits, "a:terry", "500 Terry Francine Street", f.where);
55010
+ }
55011
+ return [...hits.values()].slice(0, CAP3.perKind).map((h) => ({
55012
+ kind: "template-contact",
55013
+ value: h.value,
55014
+ where: h.where,
55015
+ note: "a Wix template's sample contact detail, not the business's \u2014 get the real one before any screen shows it"
55016
+ }));
55017
+ }
55018
+ function editorText(fields, probes) {
55019
+ const hits = /* @__PURE__ */ new Map();
55020
+ const all = [
55021
+ ...fields,
55022
+ ...probes.flatMap((p) => p.items.flatMap((it) => [
55023
+ { text: it.title, where: `catalog:${p.vertical}` },
55024
+ { text: it.description ?? "", where: `catalog:${p.vertical}` },
55025
+ // Already dropped from `description` by `copyOrDrop`; `toItems` kept the fact.
55026
+ { text: it.templateText ?? "", where: `catalog:${p.vertical}` }
55027
+ ]))
55028
+ ];
55029
+ for (const f of all) {
55030
+ if (!f.text || !isTemplatePlaceholder(f.text)) continue;
55031
+ const value = snippet(stripPii(f.text));
55032
+ pushWhere(hits, value.toLowerCase(), value, f.where);
55033
+ }
55034
+ return [...hits.values()].slice(0, CAP3.perKind).map((h) => ({
55035
+ kind: "editor-text",
55036
+ value: h.value,
55037
+ where: h.where,
55038
+ note: "unedited Wix template/editor text the published site still shows \u2014 dropped from book.copy; do not ship it, and tell the owner the site shows it"
55039
+ }));
55040
+ }
55041
+ function stripLead(name) {
55042
+ const [left = "", ...rest] = name.split(/\s+&\s+/);
55043
+ const words = left.split(/\s+/);
55044
+ let cut = 0;
55045
+ words.forEach((w, i) => {
55046
+ if (LEAD_STOPWORDS.has(w.toLowerCase())) cut = i + 1;
55047
+ });
55048
+ const kept = words.slice(cut).join(" ");
55049
+ return kept ? [kept, ...rest].join(" & ") : "";
55050
+ }
55051
+ function nameMismatches(identity, look, fields) {
55052
+ const name = (identity.businessName || identity.siteDisplayName || "").trim();
55053
+ const n = norm2(name);
55054
+ if (n.length < 3) return [];
55055
+ const pageNames = /* @__PURE__ */ new Set([
55056
+ ...(look.sitePages ?? []).map((p) => norm2(p.title)),
55057
+ ...look.navLabels.map(norm2)
55058
+ ]);
55059
+ const differs = (candidate) => {
55060
+ const c = norm2(candidate);
55061
+ return c.length >= 3 && c !== n && !c.includes(n) && !n.includes(c) && !pageNames.has(c);
55062
+ };
55063
+ const hits = /* @__PURE__ */ new Map();
55064
+ for (const f of fields) {
55065
+ if (/\((?:og:)?title\)$/.test(f.where)) {
55066
+ const parts = f.text.split(TITLE_SPLIT_RE).map((s) => s.trim()).filter(Boolean);
55067
+ const site = parts.length >= 2 ? parts[parts.length - 1] : "";
55068
+ if (site && differs(site)) pushWhere(hits, norm2(site), site, f.where);
55069
+ }
55070
+ for (const m of f.text.matchAll(COPYRIGHT_BY_RE)) {
55071
+ const who = m[1].trim();
55072
+ if (differs(who)) pushWhere(hits, norm2(who), who, f.where);
55073
+ }
55074
+ if (!f.narrative) continue;
55075
+ for (const m of f.text.matchAll(AMP_NAME_RE)) {
55076
+ const who = stripLead(m[1].trim());
55077
+ if (who.includes("&") && differs(who)) pushWhere(hits, norm2(who), who, f.where);
55078
+ }
55079
+ }
55080
+ return [...hits.values()].slice(0, CAP3.perKind).map((h) => ({
55081
+ kind: "name-mismatch",
55082
+ value: h.value,
55083
+ where: h.where,
55084
+ note: `the business is "${name}" but the site also calls itself "${h.value}" \u2014 likely the template's name left in the copy; confirm with the owner which is theirs before reusing any sentence that says it`
55085
+ }));
55086
+ }
55087
+ function mediaKey(url) {
55088
+ return wixMediaFileId(url) ?? url.split(/[?#]/)[0];
55089
+ }
55090
+ function duplicateMedia(look, probes) {
55091
+ const out = [];
55092
+ const scan = (urls, where) => {
55093
+ const counts = /* @__PURE__ */ new Map();
55094
+ for (const u of urls) {
55095
+ if (!u) continue;
55096
+ const k = mediaKey(u);
55097
+ counts.set(k, (counts.get(k) ?? 0) + 1);
55098
+ }
55099
+ for (const [k, count] of counts) {
55100
+ if (count < DUPLICATE_MEDIA_MIN || out.length >= CAP3.perKind) continue;
55101
+ out.push({
55102
+ kind: "duplicate-media",
55103
+ value: k.slice(0, 80),
55104
+ where: [where],
55105
+ note: `the same image is used ${count} times here \u2014 template filler, not ${count} distinct photos; do not present it as the business's gallery`
55106
+ });
55107
+ }
55108
+ };
55109
+ if (!look.unavailable) for (const p of look.pages) scan(p.imageUrls, p.url);
55110
+ for (const p of probes) scan(p.items.map((it) => it.imageUrl ?? ""), `catalog:${p.vertical}`);
55111
+ return out;
55112
+ }
55113
+ function detectPlaceholders(args) {
55114
+ const fields = textFields(args.look);
55115
+ return [
55116
+ ...templateContacts(args.identity, fields),
55117
+ ...editorText(fields, args.probes),
55118
+ ...nameMismatches(args.identity, args.look, fields),
55119
+ ...duplicateMedia(args.look, args.probes)
55120
+ ];
55121
+ }
55122
+ function placeholderWarnings(findings) {
55123
+ const byKind = /* @__PURE__ */ new Map();
55124
+ for (const f of findings) byKind.set(f.kind, [...byKind.get(f.kind) ?? [], f]);
55125
+ const quote = (rows) => rows.slice(0, 3).map((r) => `"${r.value}"`).join(", ");
55126
+ const out = [];
55127
+ const contact = byKind.get("template-contact");
55128
+ if (contact) out.push(`template placeholder contact details on the site: ${quote(contact)} \u2014 the Wix template's samples, not the business's; get the real ones before any screen shows them (book.placeholders)`);
55129
+ const editor = byKind.get("editor-text");
55130
+ if (editor) out.push(`${editor.length} unedited Wix editor/template string(s) on the published site, e.g. ${quote(editor.slice(0, 1))} \u2014 dropped from book.copy, never ship them (book.placeholders)`);
55131
+ const names = byKind.get("name-mismatch");
55132
+ if (names) out.push(`the site names the business inconsistently \u2014 it also says ${quote(names)}; likely the template's name left in the copy, so confirm with the owner (book.placeholders)`);
55133
+ const media = byKind.get("duplicate-media");
55134
+ if (media) out.push(`${media.length} place(s) repeat one image ${DUPLICATE_MEDIA_MIN}+ times (${media.slice(0, 3).map((m) => m.where[0]).join(", ")}) \u2014 template filler, not real photos (book.placeholders)`);
55135
+ return out;
55136
+ }
55137
+ var TEMPLATE_PHONES, TEMPLATE_EMAIL_RE, TEMPLATE_ADDRESS_RE, PHONE_IN_TEXT_RE, DUPLICATE_MEDIA_MIN, CAP3, digits, norm2, snippet, LEAD_STOPWORDS, AMP_NAME_RE, COPYRIGHT_BY_RE, TITLE_SPLIT_RE;
55138
+ var init_placeholders = __esm({
55139
+ "src/site-analyze/placeholders.ts"() {
55140
+ init_copy_clean();
55141
+ init_rest();
55142
+ TEMPLATE_PHONES = /* @__PURE__ */ new Set(["1234567890", "11234567890", "5555555555", "15555555555", "0000000000"]);
55143
+ TEMPLATE_EMAIL_RE = /[\w.+-]+@(?:my-?site|my-?domain|mydomain|your-?domain|your-?site|example|domain)\.(?:com|org|net|co)\b/i;
55144
+ TEMPLATE_ADDRESS_RE = /\b500 Terry Franc(?:ine|ois)\b/i;
55145
+ PHONE_IN_TEXT_RE = /\+?\d[\d\s().-]{6,}\d/g;
55146
+ DUPLICATE_MEDIA_MIN = 3;
55147
+ CAP3 = { perKind: 6, where: 6, snippet: 90 };
55148
+ digits = (s) => s.replace(/\D/g, "");
55149
+ norm2 = (s) => s.toLowerCase().normalize("NFKD").replace(/[^\p{L}\p{N}]+/gu, "");
55150
+ snippet = (s) => {
55151
+ const t = s.replace(/\s+/g, " ").trim();
55152
+ return t.length > CAP3.snippet ? `${t.slice(0, CAP3.snippet - 1).trimEnd()}\u2026` : t;
55153
+ };
55154
+ LEAD_STOPWORDS = /* @__PURE__ */ new Set([
55155
+ "at",
55156
+ "by",
55157
+ "with",
55158
+ "from",
55159
+ "choose",
55160
+ "call",
55161
+ "contact",
55162
+ "welcome",
55163
+ "about",
55164
+ "to",
55165
+ "for",
55166
+ "in",
55167
+ "on",
55168
+ "our",
55169
+ "why",
55170
+ "is",
55171
+ "are",
55172
+ "do",
55173
+ "does",
55174
+ "the",
55175
+ "and",
55176
+ "meet",
55177
+ "join",
55178
+ "trust",
55179
+ "hire"
55180
+ ]);
55181
+ AMP_NAME_RE = new RegExp("(\\p{Lu}[\\p{L}'\u2019]*(?:\\s+\\p{Lu}[\\p{L}'\u2019]*){0,2}\\s+&\\s+\\p{Lu}[\\p{L}'\u2019]*(?:\\s+\\p{Lu}[\\p{L}'\u2019]*){0,1})", "gu");
55182
+ COPYRIGHT_BY_RE = /©\s*\d{4}\s+by\s+([^.|\n©]{2,40})/giu;
55183
+ TITLE_SPLIT_RE = /\s+[|–—]\s+|\s+-\s+/;
55184
+ }
55185
+ });
55186
+
54461
55187
  // src/site-analyze/analyze.ts
54462
55188
  function siteFetchFor(ports) {
54463
55189
  return ports.siteFetch ?? guardedSiteFetch;
@@ -54468,14 +55194,15 @@ function localeFromSiteLanguage(languageCode) {
54468
55194
  return void 0;
54469
55195
  }
54470
55196
  async function analyzeSite(input, ports) {
54471
- const clientId = input.clientId.trim();
55197
+ const clientId = input.clientId?.trim() || void 0;
54472
55198
  const siteUrl = input.siteUrl.trim();
54473
- if (!clientId) throw new Error("clientId is required");
54474
55199
  if (!siteUrl) throw new Error("siteUrl is required");
54475
55200
  assertPublicSiteUrl(siteUrl);
54476
55201
  const redirectUri = input.redirectUri?.trim() ? assertRedirectUri(input.redirectUri) : void 0;
54477
- const visitorToken = await mintVisitorToken(ports.fetch, clientId);
54478
- const visitor = visitorTransportFrom(ports.fetch, visitorToken, input.msid ?? ports.ownerAuth?.msid);
55202
+ if (redirectUri && !clientId) {
55203
+ throw Object.assign(new Error(REDIRECT_NEEDS_CLIENT), { exit: 2 });
55204
+ }
55205
+ const visitor = clientId ? visitorTransportFrom(ports.fetch, await mintVisitorToken(ports.fetch, clientId), input.msid ?? ports.ownerAuth?.msid) : void 0;
54479
55206
  const owner = ports.ownerAuth ? ownerTransportFrom(ports.fetch, ports.ownerAuth) : void 0;
54480
55207
  const lookP = (ports.browser ? ports.browser.crawl({ siteUrl, budget: 12 }) : Promise.resolve({
54481
55208
  unavailable: ports.lookUnavailableReason ?? "no_browser_on_plane",
@@ -54613,12 +55340,14 @@ async function analyzeSite(input, ports) {
54613
55340
  });
54614
55341
  plan.provisioning.appearance = { homeLayout: traits.homeLayout.id };
54615
55342
  plan.provisioning.navigation = { navKind: traits.navKind.id };
54616
- plan.provisioning.wix = {
54617
- clientId,
54618
- siteUrl,
54619
- // Already validated and trimmed at the top of this function, with the other input guards.
54620
- ...redirectUri ? { redirectUri } : {}
54621
- };
55343
+ if (clientId) {
55344
+ plan.provisioning.wix = {
55345
+ clientId,
55346
+ siteUrl,
55347
+ // Already validated and trimmed at the top of this function, with the other input guards.
55348
+ ...redirectUri ? { redirectUri } : {}
55349
+ };
55350
+ }
54622
55351
  const locale = localeFromSiteLanguage(identity.locale.languageCode || identity.language?.site || "");
54623
55352
  if (locale) plan.provisioning.locale = locale;
54624
55353
  const velo = buildVeloChapter({ look, owner: ownerVelo });
@@ -54638,7 +55367,7 @@ async function analyzeSite(input, ports) {
54638
55367
  const book = {
54639
55368
  schemaVersion: BOOK_SCHEMA_VERSION,
54640
55369
  generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
54641
- input: { clientId, siteUrl, msid: input.msid, hasOwner: !!owner },
55370
+ input: { ...clientId ? { clientId } : {}, siteUrl, msid: input.msid, hasOwner: !!owner },
54642
55371
  identity,
54643
55372
  probes,
54644
55373
  media: mergeMedia(media, look, identity.logoUrl),
@@ -54653,7 +55382,10 @@ async function analyzeSite(input, ports) {
54653
55382
  velo,
54654
55383
  taxonomy,
54655
55384
  traits,
54656
- permissions
55385
+ permissions,
55386
+ // Template leftovers, recorded in the BOOK (the durable artifact the next agent reads) and
55387
+ // repeated as warnings below for whoever is watching this run (#3276).
55388
+ placeholders: detectPlaceholders({ identity, look, probes })
54657
55389
  };
54658
55390
  if (Object.keys(book.copy.suggestedCopyKeys).length) {
54659
55391
  plan.provisioning.copy = { ...book.copy.suggestedCopyKeys };
@@ -54680,6 +55412,12 @@ async function analyzeSite(input, ports) {
54680
55412
  `${cmsRead.truncated} further CMS collection(s) were not read \u2014 the listing stops at ${CMS_LIST_LIMIT}, so the applied app ships only the ones above.`
54681
55413
  );
54682
55414
  }
55415
+ warnings.push(...placeholderWarnings(book.placeholders));
55416
+ if (!clientId) {
55417
+ warnings.push(
55418
+ owner ? "no --client-id: the VISITOR side of every vertical probe was skipped, so the book cannot say what a shipped app will see, and there is no `wix` binding to --apply. Re-run with --client-id (`himi site client list --site <msid>`) for both." : "no --client-id: this was the URL-only pass (published pages, look, copy). The vertical probes were skipped, so the catalog has no counts, and there is no `wix` binding to --apply. Re-run with --client-id (`himi site client list --site <msid>`) for both."
55419
+ );
55420
+ }
54683
55421
  return {
54684
55422
  book,
54685
55423
  provisioningOverlay: plan.provisioning,
@@ -54797,6 +55535,7 @@ var init_analyze = __esm({
54797
55535
  init_permissions();
54798
55536
  init_published_html();
54799
55537
  init_net_guard();
55538
+ init_placeholders();
54800
55539
  SCREENSHOT_NAME = /^(home|shop|book|menu|blog|about|contact|events|gallery|plans|faq|library|members|other)-\d+\.png$/i;
54801
55540
  }
54802
55541
  });
@@ -54868,7 +55607,7 @@ function renderBookMarkdown(book) {
54868
55607
  lines.push(`# Business book`);
54869
55608
  lines.push("");
54870
55609
  lines.push(`schema ${book.schemaVersion} \xB7 generated ${book.generatedAt}`);
54871
- lines.push(`site: ${yn(book.input.siteUrl)} \xB7 msid: ${yn(book.input.msid)} \xB7 owner: ${book.input.hasOwner}`);
55610
+ lines.push(`site: ${yn(book.input.siteUrl)} \xB7 msid: ${yn(book.input.msid)} \xB7 owner: ${book.input.hasOwner}` + (book.input.clientId ? "" : book.input.hasOwner ? " \xB7 client: none \u2014 visitor-side probes skipped (owner side ran)" : " \xB7 client: none \u2014 URL-only pass, vertical probes skipped"));
54872
55611
  lines.push("");
54873
55612
  lines.push(`## Identity`);
54874
55613
  lines.push(`- name: ${yn(id.businessName)} (${yn(id.siteDisplayName)})`);
@@ -54877,13 +55616,29 @@ function renderBookMarkdown(book) {
54877
55616
  lines.push(`- language: site=${yn(id.language.site)} \xB7 business=${yn(id.language.business)}${id.language.site && id.language.business && id.language.site !== id.language.business ? " (mismatch)" : ""}`);
54878
55617
  lines.push(`- locale: ${yn(id.locale.languageCode)}/${yn(id.locale.country)} ${yn(id.locale.currency)} ${yn(id.locale.timeZone)}`);
54879
55618
  lines.push(`- logo: ${yn(id.logoUrl)}`);
54880
- lines.push(`- contact (PII, do not log): address=${id.contact.address ? "yes" : "no"} email=${id.contact.email ? "yes" : "no"} phone=${id.contact.phone ? "yes" : "no"}`);
55619
+ const templated = new Set(
55620
+ (book.placeholders ?? []).filter((f) => f.kind === "template-contact").flatMap((f) => f.where).filter((w) => w.startsWith("identity.contact."))
55621
+ );
55622
+ const contactFlag = (field) => !id.contact[field] ? "no" : templated.has(`identity.contact.${field}`) ? "TEMPLATE PLACEHOLDER (not the business's)" : "yes";
55623
+ lines.push(`- contact (PII, do not log): address=${contactFlag("address")} email=${contactFlag("email")} phone=${contactFlag("phone")}`);
54881
55624
  lines.push(`- source: ${id.source}`);
54882
55625
  if (id.source === "crawl") {
54883
55626
  lines.push(`- NAME IS A GUESS: no owner token, so the name above was read off the crawled page`);
54884
55627
  lines.push(` \`<title>\` rather than the owner-entered business name. Verify it before it ships.`);
54885
55628
  }
54886
55629
  lines.push("");
55630
+ lines.push(`## Placeholders`);
55631
+ const placeholders = book.placeholders ?? [];
55632
+ if (!placeholders.length) {
55633
+ lines.push(!book.placeholders ? `- not checked (book written before this chapter existed)` : look.unavailable ? `- page text NOT checked \u2014 the crawl did not run (${look.unavailable}); only identity and catalog fields were scanned, and they showed none` : `- none detected (template contact values, editor text, a second business name, repeated images)`);
55634
+ } else {
55635
+ lines.push(`The site still carries TEMPLATE content. None of it is the business's \u2014 keep it out of the`);
55636
+ lines.push(`app, and tell the owner their site shows it. Sample content is never the deliverable.`);
55637
+ for (const f of placeholders) {
55638
+ lines.push(`- ${f.kind}: "${f.value}" \u2014 ${f.note} (${f.where.join(", ")})`);
55639
+ }
55640
+ }
55641
+ lines.push("");
54887
55642
  lines.push(`## Copy`);
54888
55643
  lines.push(`The customer's OWN words. Reuse these before inventing any \u2014 screen titles, tab`);
54889
55644
  lines.push(`labels, section headings, CTA text, empty states. Write in \`${yn(copy.language)}\`.`);
@@ -55190,7 +55945,7 @@ var init_brand_font = __esm({
55190
55945
 
55191
55946
  // src/site-analyze/write-artifacts.ts
55192
55947
  import { mkdirSync as mkdirSync28, writeFileSync as writeFileSync25 } from "node:fs";
55193
- import { join as join56 } from "node:path";
55948
+ import { join as join57 } from "node:path";
55194
55949
  function attachVeloSources(book, sources, put) {
55195
55950
  const byPath = new Map((sources ?? []).map((s) => [s.path, s.text]));
55196
55951
  for (const f of book.velo.files) {
@@ -55345,12 +56100,12 @@ async function downloadMedia(book, fetchImpl, put) {
55345
56100
  }
55346
56101
  async function writeBusinessBook(result, opts) {
55347
56102
  const out = opts.outDir;
55348
- mkdirSync28(join56(out, "evidence/pages"), { recursive: true });
55349
- mkdirSync28(join56(out, "media/catalog"), { recursive: true });
56103
+ mkdirSync28(join57(out, "evidence/pages"), { recursive: true });
56104
+ mkdirSync28(join57(out, "media/catalog"), { recursive: true });
55350
56105
  const files = [];
55351
56106
  const put = (rel, body) => {
55352
- const p = join56(out, rel);
55353
- mkdirSync28(join56(p, ".."), { recursive: true });
56107
+ const p = join57(out, rel);
56108
+ mkdirSync28(join57(p, ".."), { recursive: true });
55354
56109
  writeFileSync25(p, body);
55355
56110
  files.push(rel);
55356
56111
  };
@@ -55444,8 +56199,8 @@ var init_customer_build = __esm({
55444
56199
  });
55445
56200
 
55446
56201
  // src/site-analyze/apply.ts
55447
- import { existsSync as existsSync59, readFileSync as readFileSync70, writeFileSync as writeFileSync26, copyFileSync as copyFileSync3, mkdirSync as mkdirSync29, realpathSync as realpathSync7, statSync as statSync16 } from "node:fs";
55448
- import { dirname as dirname31, isAbsolute as isAbsolute4, join as join57, resolve as resolve45, sep as sep14 } from "node:path";
56202
+ import { existsSync as existsSync60, readFileSync as readFileSync71, writeFileSync as writeFileSync26, copyFileSync as copyFileSync3, mkdirSync as mkdirSync29, realpathSync as realpathSync7, statSync as statSync16 } from "node:fs";
56203
+ import { dirname as dirname31, isAbsolute as isAbsolute4, join as join58, resolve as resolve45, sep as sep14 } from "node:path";
55449
56204
  import { spawnSync as spawnSync4 } from "node:child_process";
55450
56205
  function unpairedRedirectNote(overlay) {
55451
56206
  if (!overlay.wix?.clientId || overlay.wix.redirectUri) return void 0;
@@ -55460,7 +56215,7 @@ function committedShellProvisioning(path2) {
55460
56215
  function findRepoRoot(cwd) {
55461
56216
  let dir = cwd;
55462
56217
  for (let i = 0; i < 8; i++) {
55463
- if (SHELL_NAMES2.some((name) => existsSync59(join57(dir, shellAppDir(name), "package.json")))) return dir;
56218
+ if (SHELL_NAMES2.some((name) => existsSync60(join58(dir, shellAppDir(name), "package.json")))) return dir;
55464
56219
  const next = dirname31(dir);
55465
56220
  if (next === dir) break;
55466
56221
  dir = next;
@@ -55528,11 +56283,11 @@ function mergeCustomerOwned(committed, overlay) {
55528
56283
  }
55529
56284
  function buildMergedContract(options) {
55530
56285
  const { appDir: appDir2, packageDir, overlay } = options;
55531
- const committedPath = join57(appDir2, "provisioning.json");
55532
- if (!existsSync59(committedPath)) {
56286
+ const committedPath = join58(appDir2, "provisioning.json");
56287
+ if (!existsSync60(committedPath)) {
55533
56288
  throw new Error(`shell has no committed provisioning.json at ${committedPath} to build a contract from`);
55534
56289
  }
55535
- const committed = JSON.parse(readFileSync70(committedPath, "utf8"));
56290
+ const committed = JSON.parse(readFileSync71(committedPath, "utf8"));
55536
56291
  const merged = mergeCustomerOwned(committed, overlay);
55537
56292
  const overlayFontFamily = (overlay.branding ?? {}).fontFamily;
55538
56293
  const branding = merged.branding ?? {};
@@ -55544,8 +56299,8 @@ function buildMergedContract(options) {
55544
56299
  };
55545
56300
  const stage = (fromRel, toRel) => {
55546
56301
  const from = contained(packageDir, fromRel);
55547
- const to = contained(join57(appDir2, STAGED), toRel.slice(STAGED.length + 1));
55548
- if (!from || !to || !existsSync59(from)) return void 0;
56302
+ const to = contained(join58(appDir2, STAGED), toRel.slice(STAGED.length + 1));
56303
+ if (!from || !to || !existsSync60(from)) return void 0;
55549
56304
  let real;
55550
56305
  let realRoot;
55551
56306
  try {
@@ -55567,11 +56322,11 @@ function buildMergedContract(options) {
55567
56322
  };
55568
56323
  const icon = stage("media/icon.png", `${STAGED}/icon.png`);
55569
56324
  if (icon) branding.appIconPath = icon;
55570
- const facesPath = join57(packageDir, "media/fonts/faces.json");
55571
- if (existsSync59(facesPath)) {
56325
+ const facesPath = join58(packageDir, "media/fonts/faces.json");
56326
+ if (existsSync60(facesPath)) {
55572
56327
  let faces = [];
55573
56328
  try {
55574
- const parsed = JSON.parse(readFileSync70(facesPath, "utf8"));
56329
+ const parsed = JSON.parse(readFileSync71(facesPath, "utf8"));
55575
56330
  faces = Array.isArray(parsed) ? parsed : [];
55576
56331
  } catch {
55577
56332
  faces = [];
@@ -55592,9 +56347,9 @@ function buildMergedContract(options) {
55592
56347
  merged.branding = branding;
55593
56348
  const body = `${JSON.stringify(merged, null, 2)}
55594
56349
  `;
55595
- const path2 = join57(packageDir, "provisioning.contract.json");
56350
+ const path2 = join58(packageDir, "provisioning.contract.json");
55596
56351
  writeFileSync26(path2, body);
55597
- const localPath = join57(appDir2, LOCAL_CONTRACT_REL);
56352
+ const localPath = join58(appDir2, LOCAL_CONTRACT_REL);
55598
56353
  mkdirSync29(dirname31(localPath), { recursive: true });
55599
56354
  writeFileSync26(localPath, body);
55600
56355
  staged.push(localPath);
@@ -55604,8 +56359,8 @@ async function applyOverlays(input) {
55604
56359
  const cwd = resolve45(input.cwd);
55605
56360
  const shell = input.shell ?? DEFAULT_BRANDABLE_SHELL;
55606
56361
  assertKnownShell(shell);
55607
- const overlay = JSON.parse(readFileSync70(input.overlayPath, "utf8"));
55608
- const tokens = JSON.parse(readFileSync70(input.tokensPath, "utf8"));
56362
+ const overlay = JSON.parse(readFileSync71(input.overlayPath, "utf8"));
56363
+ const tokens = JSON.parse(readFileSync71(input.tokensPath, "utf8"));
55609
56364
  const wrote = [];
55610
56365
  const notes = [];
55611
56366
  const repoRoot2 = findRepoRoot(cwd);
@@ -55615,7 +56370,7 @@ async function applyOverlays(input) {
55615
56370
  `shell "${shell}" cannot bake a customer brand \u2014 it ships no prepare:customer script`
55616
56371
  );
55617
56372
  }
55618
- const appDir2 = join57(repoRoot2, shellAppDir(shell));
56373
+ const appDir2 = join58(repoRoot2, shellAppDir(shell));
55619
56374
  const packageDir = dirname31(resolve45(input.overlayPath));
55620
56375
  const { path: contract, staged } = buildMergedContract({ appDir: appDir2, packageDir, overlay });
55621
56376
  wrote.push(...staged);
@@ -55637,10 +56392,10 @@ async function applyOverlays(input) {
55637
56392
  note: `prepare:customer --contract with a merged contract (committed provisioning.json untouched)${unpairedInRepo ? ` \xB7 ${unpairedInRepo}` : ""}`
55638
56393
  };
55639
56394
  }
55640
- const pkgProv = join57(cwd, "provisioning.json");
56395
+ const pkgProv = join58(cwd, "provisioning.json");
55641
56396
  assertNotCommitted(pkgProv, input.forbiddenProvisioning);
55642
- if (existsSync59(pkgProv)) {
55643
- const cur = JSON.parse(readFileSync70(pkgProv, "utf8"));
56397
+ if (existsSync60(pkgProv)) {
56398
+ const cur = JSON.parse(readFileSync71(pkgProv, "utf8"));
55644
56399
  const merged = deepMerge4(cur, overlay);
55645
56400
  writeFileSync26(pkgProv, JSON.stringify(merged, null, 2) + "\n");
55646
56401
  wrote.push(pkgProv);
@@ -55648,9 +56403,9 @@ async function applyOverlays(input) {
55648
56403
  writeFileSync26(pkgProv, JSON.stringify(overlay, null, 2) + "\n");
55649
56404
  wrote.push(pkgProv);
55650
56405
  }
55651
- const pkgTokens = join57(cwd, "tokens.json");
55652
- if (existsSync59(pkgTokens) && (tokens.colors || tokens.typography)) {
55653
- const cur = JSON.parse(readFileSync70(pkgTokens, "utf8"));
56406
+ const pkgTokens = join58(cwd, "tokens.json");
56407
+ if (existsSync60(pkgTokens) && (tokens.colors || tokens.typography)) {
56408
+ const cur = JSON.parse(readFileSync71(pkgTokens, "utf8"));
55654
56409
  const merged = deepMerge4(cur, tokens);
55655
56410
  const mergedColors = merged.colors;
55656
56411
  if (mergedColors && typeof mergedColors === "object" && !Array.isArray(mergedColors)) {
@@ -55669,9 +56424,9 @@ async function applyOverlays(input) {
55669
56424
  wrote.push(pkgTokens);
55670
56425
  }
55671
56426
  const homeAction = homeRefreshActionId(shell);
55672
- const homeSrc = join57(cwd, "dev/index.ts");
55673
- if (homeAction && existsSync59(homeSrc)) {
55674
- const prev = readFileSync70(homeSrc, "utf8");
56427
+ const homeSrc = join58(cwd, "dev/index.ts");
56428
+ if (homeAction && existsSync60(homeSrc)) {
56429
+ const prev = readFileSync71(homeSrc, "utf8");
55675
56430
  const patched = patchBrandParams(prev, overlay, homeAction);
55676
56431
  let next = patched.src;
55677
56432
  if (!patched.foreign && overlay.installedVerticals?.length) {
@@ -55694,13 +56449,13 @@ async function applyOverlays(input) {
55694
56449
  notes.push(`${patched.unparsed} onAppear params block(s) in ${homeSrc} could not be read \u2014 any brand values inside them were not applied`);
55695
56450
  }
55696
56451
  }
55697
- if (!input.iconPath || !existsSync59(input.iconPath)) {
56452
+ if (!input.iconPath || !existsSync60(input.iconPath)) {
55698
56453
  const book = input.iconPath ? dirname31(dirname31(resolve45(input.iconPath))) : void 0;
55699
56454
  notes.push(
55700
56455
  book ? `no icon installed \u2014 ${siteLogoAbsence(book).hint}` : "no icon installed and no business book to say why \u2014 `himi icon generate` draws one from the accent, or `himi icon set <file.png>` installs the customer's own mark"
55701
56456
  );
55702
56457
  }
55703
- if (input.iconPath && existsSync59(input.iconPath)) {
56458
+ if (input.iconPath && existsSync60(input.iconPath)) {
55704
56459
  try {
55705
56460
  const installed = await installIcon(cwd, input.iconPath, { derivePlay: true });
55706
56461
  wrote.push(...installed.wrote);
@@ -58713,31 +59468,47 @@ function ctasOf(texts) {
58713
59468
  return out.slice(0, 8);
58714
59469
  }
58715
59470
  function copyFromEval(evaled) {
59471
+ const templateText = dedupe2(
59472
+ [
59473
+ ...(evaled.sections ?? []).flatMap((s) => [s?.heading, s?.body]),
59474
+ ...(evaled.faq ?? []).flatMap((f) => [f?.q, f?.a]),
59475
+ ...evaled.ctaLabels ?? [],
59476
+ ...evaled.testimonials ?? [],
59477
+ evaled.about,
59478
+ evaled.meta?.description,
59479
+ evaled.meta?.ogTitle,
59480
+ evaled.meta?.ogDescription
59481
+ ].map((raw) => {
59482
+ const text5 = cleanText(raw, 400);
59483
+ return text5 && isTemplatePlaceholder(text5) ? stripPii(cleanText(text5, 90)) : "";
59484
+ }),
59485
+ 8
59486
+ );
58716
59487
  const sections = [];
58717
59488
  for (const raw of evaled.sections ?? []) {
58718
- const heading = copyOrDrop(raw?.heading, CAP3.heading);
59489
+ const heading = copyOrDrop(raw?.heading, CAP4.heading);
58719
59490
  if (!heading) continue;
58720
- const body = copyOrDrop(raw?.body, CAP3.body);
59491
+ const body = copyOrDrop(raw?.body, CAP4.body);
58721
59492
  sections.push(body ? { heading, body } : { heading });
58722
- if (sections.length >= CAP3.sections) break;
59493
+ if (sections.length >= CAP4.sections) break;
58723
59494
  }
58724
59495
  const faq = [];
58725
59496
  for (const raw of evaled.faq ?? []) {
58726
- const q = copyOrDrop(raw?.q, CAP3.q);
58727
- const a = copyOrDrop(raw?.a, CAP3.a);
59497
+ const q = copyOrDrop(raw?.q, CAP4.q);
59498
+ const a = copyOrDrop(raw?.a, CAP4.a);
58728
59499
  if (q && a && q !== a) faq.push({ q, a });
58729
- if (faq.length >= CAP3.faq) break;
59500
+ if (faq.length >= CAP4.faq) break;
58730
59501
  }
58731
- const ctaLabels = dedupe2((evaled.ctaLabels ?? []).map((c) => copyOrDrop(c, CAP3.cta)), CAP3.ctas);
59502
+ const ctaLabels = dedupe2((evaled.ctaLabels ?? []).map((c) => copyOrDrop(c, CAP4.cta)), CAP4.ctas);
58732
59503
  const testimonials = dedupe2(
58733
- (evaled.testimonials ?? []).map((t) => copyOrDrop(t, CAP3.testimonial)),
58734
- CAP3.testimonials
59504
+ (evaled.testimonials ?? []).map((t) => copyOrDrop(t, CAP4.testimonial)),
59505
+ CAP4.testimonials
58735
59506
  );
58736
- const about = copyOrDrop(evaled.about, CAP3.about);
59507
+ const about = copyOrDrop(evaled.about, CAP4.about);
58737
59508
  const meta = {};
58738
- const description = copyOrDrop(evaled.meta?.description, CAP3.meta);
58739
- const ogTitle = copyOrDrop(evaled.meta?.ogTitle, CAP3.meta);
58740
- const ogDescription = copyOrDrop(evaled.meta?.ogDescription, CAP3.meta);
59509
+ const description = copyOrDrop(evaled.meta?.description, CAP4.meta);
59510
+ const ogTitle = copyOrDrop(evaled.meta?.ogTitle, CAP4.meta);
59511
+ const ogDescription = copyOrDrop(evaled.meta?.ogDescription, CAP4.meta);
58741
59512
  if (description) meta.description = description;
58742
59513
  if (ogTitle) meta.ogTitle = ogTitle;
58743
59514
  if (ogDescription) meta.ogDescription = ogDescription;
@@ -58747,7 +59518,8 @@ function copyFromEval(evaled) {
58747
59518
  ...faq.length ? { faq } : {},
58748
59519
  ...testimonials.length ? { testimonials } : {},
58749
59520
  ...about ? { about } : {},
58750
- ...Object.keys(meta).length ? { meta } : {}
59521
+ ...Object.keys(meta).length ? { meta } : {},
59522
+ ...templateText.length ? { templateText } : {}
58751
59523
  };
58752
59524
  }
58753
59525
  function forwardedRequestHeaders(all) {
@@ -59011,7 +59783,7 @@ async function createPlaywrightBrowser(load = () => import("playwright"), siteFe
59011
59783
  };
59012
59784
  return { port, screenshots, siteFetch, pin };
59013
59785
  }
59014
- var PLAYWRIGHT_HINT, PAGE_EVAL, CAP3, DOCUMENT_FETCH_TIMEOUT_MS, MAX_DOCUMENT_BYTES, SUBRESOURCE_PIN_TIMEOUT_MS, NON_NETWORK_SCHEMES, FORWARDED_REQUEST_HEADERS;
59786
+ var PLAYWRIGHT_HINT, PAGE_EVAL, CAP4, DOCUMENT_FETCH_TIMEOUT_MS, MAX_DOCUMENT_BYTES, SUBRESOURCE_PIN_TIMEOUT_MS, NON_NETWORK_SCHEMES, FORWARDED_REQUEST_HEADERS;
59015
59787
  var init_crawl_look = __esm({
59016
59788
  "src/site-analyze/crawl-look.ts"() {
59017
59789
  init_dist();
@@ -59206,7 +59978,7 @@ var init_crawl_look = __esm({
59206
59978
  } catch (e4) {}
59207
59979
  return { wst, nav, headings, hero, imgs, hrefs, buttons, html, htmlLang, velo, sections, ctaLabels, faq, testimonials, about, meta };
59208
59980
  })()`;
59209
- CAP3 = {
59981
+ CAP4 = {
59210
59982
  heading: 120,
59211
59983
  body: 300,
59212
59984
  cta: 80,
@@ -59604,7 +60376,7 @@ async function veloCall(fetchImpl, method, url, opts = {}) {
59604
60376
  }
59605
60377
  return { status: res.status, json: json5, text: text5 };
59606
60378
  }
59607
- function snippet(x, cap = 180) {
60379
+ function snippet2(x, cap = 180) {
59608
60380
  const s = typeof x === "string" ? x : JSON.stringify(x) ?? "";
59609
60381
  return s.replace(/\s+/g, " ").slice(0, cap);
59610
60382
  }
@@ -59627,7 +60399,7 @@ async function resolveGridAppId(fetchImpl, token, msid, siteUrl) {
59627
60399
  return { gridAppId: app.id, source: "lifecycle-provision", mutable: app.mutable !== false };
59628
60400
  }
59629
60401
  if (publishedId) return { gridAppId: publishedId, source: "published-html", mutable: false };
59630
- return { source: `provision ${provisioned.status}: ${snippet(provisioned.json ?? provisioned.text)}` };
60402
+ return { source: `provision ${provisioned.status}: ${snippet2(provisioned.json ?? provisioned.text)}` };
59631
60403
  }
59632
60404
  function sameVeloPath(listed, wanted) {
59633
60405
  const n = (p) => p.replace(/^\.?\//, "");
@@ -59724,7 +60496,7 @@ async function runSiteFunction(opts) {
59724
60496
  revision: snap.revision,
59725
60497
  codeAppId: snap.codeAppId,
59726
60498
  publish: { revision: httpOk(snap.rev), rc: httpOk(snap.rc), live: httpOk(snap.live), status: snap.live?.status },
59727
- error: ok ? void 0 : snippet(snap.live?.json ?? snap.rev.json)
60499
+ error: ok ? void 0 : snippet2(snap.live?.json ?? snap.rev.json)
59728
60500
  };
59729
60501
  }
59730
60502
  const { id, listed } = await withGrid(fetchImpl, opts.token, opts.msid, opts.siteUrl);
@@ -59769,7 +60541,7 @@ async function runSiteFunction(opts) {
59769
60541
  next
59770
60542
  );
59771
60543
  if (!httpOk(patched)) {
59772
- return { ...base, error: `UpdateFiles ${patched.status}: ${snippet(patched.json ?? patched.text)}` };
60544
+ return { ...base, error: `UpdateFiles ${patched.status}: ${snippet2(patched.json ?? patched.text)}` };
59773
60545
  }
59774
60546
  let report2 = {
59775
60547
  ...base,
@@ -59786,7 +60558,7 @@ async function runSiteFunction(opts) {
59786
60558
  revision: snap.revision,
59787
60559
  codeAppId: snap.codeAppId,
59788
60560
  publish: { revision: httpOk(snap.rev), rc: httpOk(snap.rc), live: httpOk(snap.live), status: snap.live?.status },
59789
- error: pubOk ? report2.error : snippet(snap.live?.json ?? snap.rev.json)
60561
+ error: pubOk ? report2.error : snippet2(snap.live?.json ?? snap.rev.json)
59790
60562
  };
59791
60563
  }
59792
60564
  if (opts.invoke) {
@@ -59823,8 +60595,8 @@ async function invokeAndReport(fetchImpl, opts, base, name, method) {
59823
60595
  return {
59824
60596
  ...base,
59825
60597
  ok,
59826
- invoke: { url: hit.url, status: hit.r.status, body: hit.r.json ?? snippet(hit.r.text) },
59827
- error: ok ? void 0 : `invoke ${hit.r.status}: ${snippet(hit.r.json ?? hit.r.text)}`
60598
+ invoke: { url: hit.url, status: hit.r.status, body: hit.r.json ?? snippet2(hit.r.text) },
60599
+ error: ok ? void 0 : `invoke ${hit.r.status}: ${snippet2(hit.r.json ?? hit.r.text)}`
59828
60600
  };
59829
60601
  }
59830
60602
  var HTTP_METHODS2, WRITE_LAYOUTS, VELO_PROVISION_URL, SITE_PUBLISH_URL, PUBLISH_RC_URL, SITE_FUNCTION_USAGE, NAME_RE, EXPORT_HEAD, WIX_HTTP_IMPORT_LINE;
@@ -59849,18 +60621,31 @@ var init_site_function = __esm({
59849
60621
  // src/site-analyze/cli.ts
59850
60622
  var cli_exports = {};
59851
60623
  __export(cli_exports, {
60624
+ APPLY_NEEDS_CLIENT: () => APPLY_NEEDS_CLIENT,
60625
+ REDIRECT_NEEDS_CLIENT: () => REDIRECT_NEEDS_CLIENT,
59852
60626
  SITE_FUNCTION_USAGE: () => SITE_FUNCTION_USAGE,
59853
60627
  assetFetchFor: () => assetFetchFor,
59854
60628
  noteWithWarnings: () => noteWithWarnings,
60629
+ ownerSiteFor: () => ownerSiteFor,
59855
60630
  runSiteAnalyze: () => runSiteAnalyze,
59856
60631
  runSiteFunctionCli: () => runSiteFunctionCli
59857
60632
  });
59858
- import { readFileSync as readFileSync71 } from "node:fs";
60633
+ import { readFileSync as readFileSync72 } from "node:fs";
59859
60634
  import { resolve as resolve46 } from "node:path";
59860
60635
  function assetFetchFor(_opts) {
59861
60636
  return pinnedFetch2;
59862
60637
  }
60638
+ function ownerSiteFor(opts, env = process.env) {
60639
+ const flag = opts.siteFlag?.trim();
60640
+ if (flag) return flag;
60641
+ if (!opts.clientId?.trim() || opts.explicitClient !== true) return void 0;
60642
+ return env.WIX_SITE_ID?.trim() || void 0;
60643
+ }
59863
60644
  async function runSiteAnalyze(opts) {
60645
+ if (!opts.clientId?.trim()) {
60646
+ if (opts.apply) throw Object.assign(new Error(APPLY_NEEDS_CLIENT), { exit: 2 });
60647
+ if (opts.redirectUri?.trim()) throw Object.assign(new Error(REDIRECT_NEEDS_CLIENT), { exit: 2 });
60648
+ }
59864
60649
  const fetchImpl = opts.fetchImpl ?? fetch;
59865
60650
  const assetFetch = assetFetchFor(opts);
59866
60651
  const siteFetch = guardedSiteFetch;
@@ -59868,9 +60653,11 @@ async function runSiteAnalyze(opts) {
59868
60653
  const outDir = resolve46(cwd, opts.outDir ?? ".himi/business-book");
59869
60654
  let ownerAuth;
59870
60655
  let msid;
60656
+ const ownerSite = ownerSiteFor(opts);
59871
60657
  try {
59872
- msid = resolveSiteId(opts.siteFlag);
59873
- const token = await siteToken(msid);
60658
+ if (!ownerSite) throw new Error("no owner site for this run");
60659
+ const token = await siteToken(ownerSite);
60660
+ msid = ownerSite;
59874
60661
  ownerAuth = { token, msid };
59875
60662
  } catch {
59876
60663
  msid = opts.siteFlag?.trim() || void 0;
@@ -59942,7 +60729,7 @@ async function runSiteFunctionCli(opts) {
59942
60729
  let source;
59943
60730
  if (opts.sourcePath) {
59944
60731
  const p = resolve46(opts.cwd ?? process.cwd(), opts.sourcePath);
59945
- source = readFileSync71(p, "utf8");
60732
+ source = readFileSync72(p, "utf8");
59946
60733
  }
59947
60734
  let body;
59948
60735
  if (opts.bodyRaw != null) {
@@ -59969,7 +60756,7 @@ async function runSiteFunctionCli(opts) {
59969
60756
  fetchImpl: opts.fetchImpl
59970
60757
  });
59971
60758
  }
59972
- var FUNCTION_OPS;
60759
+ var APPLY_NEEDS_CLIENT, FUNCTION_OPS;
59973
60760
  var init_cli = __esm({
59974
60761
  "src/site-analyze/cli.ts"() {
59975
60762
  init_analyze();
@@ -59980,6 +60767,8 @@ var init_cli = __esm({
59980
60767
  init_rest();
59981
60768
  init_wix_owner();
59982
60769
  init_site_function();
60770
+ init_types2();
60771
+ APPLY_NEEDS_CLIENT = "--apply needs --client-id: the applied app must be bound to the site's own Headless client, and a URL-only run has none. `himi site client list --site <msid>` shows the site's clients (`himi site client ensure --site <msid> --yes` mints one); WIX_CLIENT_ID is read as a fallback.";
59983
60772
  FUNCTION_OPS = ["list", "get", "put", "publish", "invoke"];
59984
60773
  }
59985
60774
  });
@@ -60199,7 +60988,7 @@ __export(site_create_exports, {
60199
60988
  runSiteList: () => runSiteList,
60200
60989
  writeProvisioningBinding: () => writeProvisioningBinding
60201
60990
  });
60202
- import { existsSync as existsSync60, readFileSync as readFileSync72, writeFileSync as writeFileSync27 } from "node:fs";
60991
+ import { existsSync as existsSync61, readFileSync as readFileSync73, writeFileSync as writeFileSync27 } from "node:fs";
60203
60992
  import { resolve as resolvePath2 } from "node:path";
60204
60993
  function oauthClientName(projectName) {
60205
60994
  return `${projectName} client`;
@@ -60348,7 +61137,7 @@ async function runSiteCreate(opts) {
60348
61137
  if (!wantOauthApp) return { ...base, nextSteps: nextSteps(metaSiteId) };
60349
61138
  return { ...base, clientId, redirectUri, nextSteps: nextSteps(metaSiteId, clientId) };
60350
61139
  }
60351
- function writeProvisioningBinding(target, binding2, fs = { existsSync: existsSync60, readFileSync: readFileSync72, writeFileSync: writeFileSync27 }) {
61140
+ function writeProvisioningBinding(target, binding2, fs = { existsSync: existsSync61, readFileSync: readFileSync73, writeFileSync: writeFileSync27 }) {
60352
61141
  const msid = binding2.siteId;
60353
61142
  const path2 = resolvePath2(target);
60354
61143
  const committed = committedShellProvisioning(path2);
@@ -61021,11 +61810,11 @@ var init_context = __esm({
61021
61810
  });
61022
61811
 
61023
61812
  // src/cli.ts
61024
- import { mkdtempSync as mkdtempSync4, readFileSync as readFileSync73, mkdirSync as mkdirSync30, writeFileSync as writeFileSync28, existsSync as existsSync61, rmSync as rmSync14, readdirSync as readdirSync27, statSync as statSync17, lstatSync as lstatSync9, accessSync, constants as constants2, renameSync as renameSync6, cpSync as cpSync3 } from "node:fs";
61813
+ import { mkdtempSync as mkdtempSync4, readFileSync as readFileSync74, mkdirSync as mkdirSync30, writeFileSync as writeFileSync28, existsSync as existsSync62, rmSync as rmSync14, readdirSync as readdirSync27, statSync as statSync17, lstatSync as lstatSync9, accessSync, constants as constants2, renameSync as renameSync6, cpSync as cpSync3 } from "node:fs";
61025
61814
  import { execFile, execFileSync as execFileSync7, spawnSync as spawnSync5, spawn as spawn6 } from "node:child_process";
61026
61815
  import { createHash as createHash27, randomUUID as randomUUID5 } from "node:crypto";
61027
61816
  import { tmpdir as tmpdir5, homedir as homedir7, hostname } from "node:os";
61028
- import { join as join58, resolve as resolve47, relative as relative10, basename as basename14, dirname as dirname32 } from "node:path";
61817
+ import { join as join59, resolve as resolve47, relative as relative10, basename as basename14, dirname as dirname32 } from "node:path";
61029
61818
  import { fileURLToPath as fileURLToPath24 } from "node:url";
61030
61819
 
61031
61820
  // ../serve-cli/src/client.ts
@@ -64318,8 +65107,8 @@ var str6 = (v) => typeof v === "string" ? v : void 0;
64318
65107
  var num2 = (v) => typeof v === "string" && v !== "" && Number.isFinite(Number(v)) ? Number(v) : void 0;
64319
65108
  function embeddedTemplateCatalog() {
64320
65109
  return availableTemplates().map((name) => {
64321
- const path2 = join58(templateSourceDir(name), TEMPLATE_META_FILE);
64322
- const meta = JSON.parse(readFileSync73(path2, "utf8"));
65110
+ const path2 = join59(templateSourceDir(name), TEMPLATE_META_FILE);
65111
+ const meta = JSON.parse(readFileSync74(path2, "utf8"));
64323
65112
  if (!meta || typeof meta.title !== "string" || typeof meta.description !== "string" || !Array.isArray(meta.tags)) {
64324
65113
  throw new Error(`${path2} must be { title: string, description: string, tags: string[], category?: string }`);
64325
65114
  }
@@ -64833,11 +65622,14 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
64833
65622
  refuses a WEB client \u2014 so a WEB one carrying your
64834
65623
  redirect looks right and is not a candidate.
64835
65624
  himi site analyze --read-only --site <msid> [--client-id <id>] [--json]
64836
- himi site analyze --client-id <id> --url <https://\u2026> [--site <msid>] [--out <dir>] [--apply]
65625
+ himi site analyze --url <https://\u2026> [--client-id <id>] [--site <msid>] [--out <dir>] [--apply]
64837
65626
  [--shell ${SHELL_ENUM}]
64838
65627
  collect a Wix business book (APIs + Playwright look) and
64839
65628
  optionally bake identity/verticals/palette/homeLayout into
64840
- a brandable shell's content package. --shell takes the same
65629
+ a brandable shell's content package. --url alone is the
65630
+ visitor-only pass (published pages, look, copy); --client-id
65631
+ adds the vertical catalog counts and the app binding, and
65632
+ --apply requires it. --shell takes the same
64841
65633
  names 'himi dist --shell' does; default branded-lite. Owner token: WIX_TEST_TOKEN
64842
65634
  or 'wix token --site'. Look/screenshots need Chromium:
64843
65635
  npx playwright install chromium.
@@ -64948,7 +65740,7 @@ var COMMANDS = [
64948
65740
  { name: "login", summary: "obtain a scoped serve-control token from Wix identity and store it in ~/.himalaya/serve-targets.json (so push/promote need no hand-pasted token). Three modes: --browser (your own Wix account, no VPN/BO), --wix (HEADLESS \u2014 the machine's own Wix CLI credential; the mode a CI pod or agent container can satisfy), --dpx (Wix employee, reads ~/.dpx/session/token). RFC 0026 WS3.", required: "[--browser|--wix|--dpx] [--target <t>] [--issue-url <url>]" },
64949
65741
  { name: "logout", summary: "remove the stored control token for a target", required: "[--target <t>]" },
64950
65742
  { name: "notify", summary: "`setup` optionally configures Branded/Ping from local customer key files; `status --refresh` reads live backend readiness. Setup may be deferred while building. These use serve-control and require owner plus Wix account authentication. `send`, `tokens`, `doctor`: send a REAL push notification to registered devices (RFC 0036 WS5) \u2014 APNs .p8 over HTTP/2 for iOS, FCM v1 for Android. `send` targets one device (--token / --device <registry id>) or fans out to the whole app (--all), reporting a per-device result; a token the provider says is uninstalled (APNs 410 Unregistered / FCM UNREGISTERED) is auto-disabled, while a merely wrong one (BadDeviceToken \u2014 usually a sandbox/production mismatch) is recorded and left enabled. `tokens` lists the registry. `doctor` reports which credentials resolve and which sender each platform picks. Talks to the admin-server via --admin-url/HIMALAYA_ADMIN_URL, authenticated with --admin-session/HIMALAYA_ADMIN_SESSION. NOT `himi push`, which publishes a release.", required: "setup|status [--content <dir>] [--target <t>] [--refresh] [--yes] [--no-push-setup] | send|tokens|doctor [--app <a>] [--token <t> | --device <id> | --all] [--title <s>] [--body <s>] [--link <url>] [--platform ios|android] [--badge <n>] [--channel <id>] [--silent] [--dry-run] [--json]" },
64951
- { name: "site", summary: "PROVISION a Wix project, then analyze or extend it. `create` is the one door that makes a site instead of adopting one: POST /funnel/projects/v1/create (a WIX site, a HEADLESS backend, a BRANDED_APP, or a VIBE site), and for headless it also mints the OAuth app whose id IS the `WIX_CLIENT_ID` \u2014 with --redirect-uri already registered, because an unregistered redirect makes native signInDirect fail at redirect-session. `--verticals` REQUIRES `--template` and reports only what Wix confirmed: measured, installing onto a project created from the BLANK template returns 200 while the vertical's API answers 428 forever, because there is no site for it to attach to (the install asks for CREATE_SITE). A real template ships its verticals installed and populated, and installing further ones onto it then works. It MUTATES your Wix account and needs --yes; without it you get the payload and nothing is sent. `list` is the read-only site inventory (with each site's premium flag, which is what explains an installed-but-428 vertical). Auth is the account-scoped `wix token` (no --site), falling back to WIX_API_KEY + WIX_ACCOUNT_ID. `client` is the same OAuth step for a site that ALREADY exists \u2014 the one himi adopts rather than makes \u2014 so a clientId no longer means a manual trip to the dashboard: `client ensure` reconciles rather than mints, because a site can already carry a client with NO registered redirect and shipping that one fails signInDirect at runtime; it reuses a client that carries --redirect-uri, PATCHes one that matches --name but lacks it (unioning, so another app's redirect survives), and creates only when there is nothing to reuse (--new forces a fresh one). --name IS the idempotency key. `client` auth is SITE-scoped (`wix token --site <msid>`) because minting is owner-of-that-site, and --yes gates every mutation: a site that already has a usable client resolves WITHOUT it, but --write-provisioning needs --yes even then because it rewrites a manifest on disk (the dry run's planned.provisioning names the file). That query endpoint IGNORES paging (measured: query.paging, cursorPaging and a top-level paging all return the full set at offset 0), so when pagingMetadata reports more clients than came back, `ensure` REFUSES EVERY outcome \u2014 create, patch AND reuse. Reuse is refused too because pickClient breaks ties on the OLDEST createdDate, so an omitted older same-named MOBILE client with the same redirect would win a later complete read and the resolved clientId would MOVE between runs \u2014 and it gets written into provisioning.json; convergence is what makes a stray duplicate untidy rather than harmful. --name cannot narrow the query (no filter is sent; the name is applied to the rows after they arrive), so the recovery is the dashboard; --new skips the query entirely. A `patched` result is verified against what Wix STORED \u2014 the redirect list is read off the PATCH response, and re-read via GET when the response omits it, because falling back to the list himi just sent would make the guard pass vacuously on exactly the silent no-op it exists to catch. `client list` is the read-only inventory of a site's clients: act on its `shippable` column rather than `hasRedirect`, because shippable ALSO requires a MOBILE applicationType and `ensure` refuses a WEB client \u2014 a WEB one carrying your redirect looks right and is not a candidate \u2014 and it sets truncated:true rather than presenting a partial inventory as the whole one. `analyze --read-only` returns API-only generation context as JSON (identity, access probes, CMS permissions and selected redacted Velo source); it needs no URL/browser and never uploads or applies anything. `analyze` reads a site into a business-book folder (REST + Playwright, GET-only); `site function` list/get/put/publish/invokes that site's Velo HTTP functions and is not `himi functions` (serve-plane jobs). `--apply` bakes into a brandable shell; `--shell` takes the same names `himi dist --shell` does and defaults to branded-lite, and a shell that ships no prepare:customer is refused by name rather than half-built. It never overwrites any shell's committed apps/<shell>/provisioning.json.", required: "create <name> [--type wix|headless|branded-app|vibe] [--template <guid>] [--verticals stores,bookings,\u2026] [--folder <guid>] [--redirect-uri <uri>] [--no-oauth-app] [--write-provisioning [path]] [--site-url <url>] [--json] --yes | list | client ensure|list --site <msid> [--name <name>] [--redirect-uri <uri>] [--new] [--write-provisioning [path]] [--site-url <url>] [--json] --yes | analyze --read-only [--site <msid>] [--client-id <id>] [--json] | analyze --client-id <id> --url <https> [--site <msid>] [--out <dir>] [--redirect-uri <uri>] [--apply] [--shell " + SHELL_ENUM + "] | function list|get|put|publish|invoke [--site <msid>] [--url <https>] [--method GET|POST|PUT|DELETE] [--source <file>] [--body <json>] [--publish] [--invoke]" },
65743
+ { name: "site", summary: "PROVISION a Wix project, then analyze or extend it. `create` is the one door that makes a site instead of adopting one: POST /funnel/projects/v1/create (a WIX site, a HEADLESS backend, a BRANDED_APP, or a VIBE site), and for headless it also mints the OAuth app whose id IS the `WIX_CLIENT_ID` \u2014 with --redirect-uri already registered, because an unregistered redirect makes native signInDirect fail at redirect-session. `--verticals` REQUIRES `--template` and reports only what Wix confirmed: measured, installing onto a project created from the BLANK template returns 200 while the vertical's API answers 428 forever, because there is no site for it to attach to (the install asks for CREATE_SITE). A real template ships its verticals installed and populated, and installing further ones onto it then works. It MUTATES your Wix account and needs --yes; without it you get the payload and nothing is sent. `list` is the read-only site inventory (with each site's premium flag, which is what explains an installed-but-428 vertical). Auth is the account-scoped `wix token` (no --site), falling back to WIX_API_KEY + WIX_ACCOUNT_ID. `client` is the same OAuth step for a site that ALREADY exists \u2014 the one himi adopts rather than makes \u2014 so a clientId no longer means a manual trip to the dashboard: `client ensure` reconciles rather than mints, because a site can already carry a client with NO registered redirect and shipping that one fails signInDirect at runtime; it reuses a client that carries --redirect-uri, PATCHes one that matches --name but lacks it (unioning, so another app's redirect survives), and creates only when there is nothing to reuse (--new forces a fresh one). --name IS the idempotency key. `client` auth is SITE-scoped (`wix token --site <msid>`) because minting is owner-of-that-site, and --yes gates every mutation: a site that already has a usable client resolves WITHOUT it, but --write-provisioning needs --yes even then because it rewrites a manifest on disk (the dry run's planned.provisioning names the file). That query endpoint IGNORES paging (measured: query.paging, cursorPaging and a top-level paging all return the full set at offset 0), so when pagingMetadata reports more clients than came back, `ensure` REFUSES EVERY outcome \u2014 create, patch AND reuse. Reuse is refused too because pickClient breaks ties on the OLDEST createdDate, so an omitted older same-named MOBILE client with the same redirect would win a later complete read and the resolved clientId would MOVE between runs \u2014 and it gets written into provisioning.json; convergence is what makes a stray duplicate untidy rather than harmful. --name cannot narrow the query (no filter is sent; the name is applied to the rows after they arrive), so the recovery is the dashboard; --new skips the query entirely. A `patched` result is verified against what Wix STORED \u2014 the redirect list is read off the PATCH response, and re-read via GET when the response omits it, because falling back to the list himi just sent would make the guard pass vacuously on exactly the silent no-op it exists to catch. `client list` is the read-only inventory of a site's clients: act on its `shippable` column rather than `hasRedirect`, because shippable ALSO requires a MOBILE applicationType and `ensure` refuses a WEB client \u2014 a WEB one carrying your redirect looks right and is not a candidate \u2014 and it sets truncated:true rather than presenting a partial inventory as the whole one. `analyze --read-only` returns API-only generation context as JSON (identity, access probes, CMS permissions and selected redacted Velo source); it needs no URL/browser and never uploads or applies anything. `analyze` reads a site into a business-book folder (REST + Playwright, GET-only); `--url` alone is the visitor-only pass over the published pages, and `--client-id` adds the vertical probes and the app binding `--apply` requires; `site function` list/get/put/publish/invokes that site's Velo HTTP functions and is not `himi functions` (serve-plane jobs). `--apply` bakes into a brandable shell; `--shell` takes the same names `himi dist --shell` does and defaults to branded-lite, and a shell that ships no prepare:customer is refused by name rather than half-built. It never overwrites any shell's committed apps/<shell>/provisioning.json.", required: "create <name> [--type wix|headless|branded-app|vibe] [--template <guid>] [--verticals stores,bookings,\u2026] [--folder <guid>] [--redirect-uri <uri>] [--no-oauth-app] [--write-provisioning [path]] [--site-url <url>] [--json] --yes | list | client ensure|list --site <msid> [--name <name>] [--redirect-uri <uri>] [--new] [--write-provisioning [path]] [--site-url <url>] [--json] --yes | analyze --read-only [--site <msid>] [--client-id <id>] [--json] | analyze --url <https> [--client-id <id>] [--site <msid>] [--out <dir>] [--redirect-uri <uri>] [--apply] [--shell " + SHELL_ENUM + "] | function list|get|put|publish|invoke [--site <msid>] [--url <https>] [--method GET|POST|PUT|DELETE] [--source <file>] [--body <json>] [--publish] [--invoke]" },
64952
65744
  { name: "doctor", summary: 'off-VPN preflight in one shot: probe the PUBLIC data plane (www.wixapis.com \u2014 reachable off-VPN), the INTERNAL serve/control plane for the selected target (VPN-gated), and report auth state (dpx session on disk + stored control token). --dpx additionally runs a live, EADDRINUSE-safe `dpx whoami`. Machine-readable with --json (an unreachable internal plane carries a stable code:"needs_vpn" so an agent branches on the code, not the prose); human-readable otherwise. Never loads the authoring runtime \u2014 works off-repo.', required: "[--target <t>] [--dpx] [--json] [--data-url <url>] [--timeout <ms>]" },
64953
65745
  { name: "upgrade", summary: "install the newest published CLI (`npm i -g @wix/himalaya-cli@latest`) and report what moved. This is the explicit door: himi also updates itself when the running build is more than a week old \u2014 unattended it just does it and re-runs your command, at a terminal it asks first \u2014 and `HIMI_NO_UPDATE_CHECK=1` turns that off, leaving this command as the way to catch up. Reports `unchanged` rather than claiming success when npm exits 0 without moving the version, which is what a registry mirror serving its own pinned copy looks like.", required: "[--json]" },
64954
65746
  { name: "push", summary: "lint + build + publish a release (default --draft: servable via ?release= but NOT live). REFUSES to publish on any error unless --force; --strict promotes warnings. Prints preview + agent-context links. The published slug is NAMESPACED per owner \u2014 `<ownerPrefix>.<name>`, composed offline from the control token's ownerPrefix claim, so the CLI and the server derive the identical string and himalaya.content.json is NOT rewritten (a legacy token with no ownerPrefix publishes the declared name unchanged). An app claimed BEFORE namespacing is stored under its FLAT slug, so the claim route answers `alreadyYours` naming that slug and the push adopts it \u2014 reported as `adoptedSlug`, since it is the one case where the published `app` differs from what the declared name composes to. `--no-claim` skips the route that knows this, so a pre-namespacing app pushed with it still fails 403. --visibility public|unlisted decides whether /v1/apps + the Himi Player picker LIST the app (also declarable as `visibility` in himalaya.content.json; omitted, a brand-new app defaults to unlisted \u2014 share by link \u2014 and an existing setting is kept). --account-id names which account owns the app when your Wix session resolves to several (the auto-claim of a brand-new slug cannot guess \u2014 without it a multi-account caller is told to name one). An unforced push of a release that carries SCREENS also refuses when `.himi/ux-audit.json` holds no mobile-UX drive recorded against that release id, or holds one that FAILED (#2955) \u2014 a published draft IS the owner-private preview a customer is handed; `himi ux record` clears it and `--force` overrides it, which the success envelope then says out loud. `--only <selector>` publishes ONLY the named items as a release derived from the app's live release (or `--from-release <id>`), running the identical build + lint gate and simply publishing less; it requires an explicit `--draft` or `--live` because a partial publish has no safe default, never claims a slug, and carries neither attestation nor a source archive (both named in the output). Selectors are comma-separated in ONE `--only`: `changed` (every item whose bytes differ from the base), `screen:<id|glob>`, `bundle:<name>`, `tokens`, `app-config` \u2014 a selected screen drags its logic bundle and a selected bundle drags the screens that reference it, or the bytes land under a key nothing reads.", required: "[--content <dir>] [--draft] [--live] [--force] [--strict] [--label] [--notes] [--visibility public|unlisted] [--no-claim] [--account-id <id>] [--only <selector>] [--from-release <id>] [--accept-regressions] [--accept-partial] [--app <slug>] (--control-token)" },
@@ -64988,7 +65780,7 @@ var COMMANDS = [
64988
65780
  ];
64989
65781
  function cliVersion() {
64990
65782
  try {
64991
- return JSON.parse(readFileSync73(new URL("../package.json", import.meta.url), "utf8")).version ?? "unknown";
65783
+ return JSON.parse(readFileSync74(new URL("../package.json", import.meta.url), "utf8")).version ?? "unknown";
64992
65784
  } catch {
64993
65785
  return "unknown";
64994
65786
  }
@@ -65106,7 +65898,7 @@ function ageInDays(publishedAt, now = Date.now()) {
65106
65898
  }
65107
65899
  function packageVersion(path2) {
65108
65900
  try {
65109
- const version2 = JSON.parse(readFileSync73(path2, "utf8")).version;
65901
+ const version2 = JSON.parse(readFileSync74(path2, "utf8")).version;
65110
65902
  return typeof version2 === "string" && version2 ? version2 : void 0;
65111
65903
  } catch {
65112
65904
  return void 0;
@@ -65115,7 +65907,7 @@ function packageVersion(path2) {
65115
65907
  function resolvedSdkVersion(fromDir = process.cwd(), bundledManifest = new URL("./authoring-runtime/node_modules/@wix/himalaya/package.json", import.meta.url)) {
65116
65908
  let dir = resolve47(fromDir);
65117
65909
  for (; ; ) {
65118
- const version2 = packageVersion(join58(dir, "node_modules", "@wix", "himalaya", "package.json"));
65910
+ const version2 = packageVersion(join59(dir, "node_modules", "@wix", "himalaya", "package.json"));
65119
65911
  if (version2) return { version: version2, source: "local" };
65120
65912
  const parent = dirname32(dir);
65121
65913
  if (parent === dir) break;
@@ -65125,7 +65917,7 @@ function resolvedSdkVersion(fromDir = process.cwd(), bundledManifest = new URL("
65125
65917
  if (bundled) return { version: bundled, source: "bundled" };
65126
65918
  try {
65127
65919
  const root = execFileSync7("npm", ["root", "-g"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
65128
- const version2 = root ? packageVersion(join58(root, "@wix", "himalaya", "package.json")) : void 0;
65920
+ const version2 = root ? packageVersion(join59(root, "@wix", "himalaya", "package.json")) : void 0;
65129
65921
  return version2 ? { version: version2, source: "global" } : {};
65130
65922
  } catch {
65131
65923
  return {};
@@ -65172,11 +65964,11 @@ var UPDATE_RETRY_AFTER_FAILURE_MS = 60 * 60 * 1e3;
65172
65964
  var UPDATE_SNOOZE_MS = 24 * 60 * 60 * 1e3;
65173
65965
  var UPDATE_REEXEC_ENV = "HIMI_UPDATED_ONCE";
65174
65966
  function updateCachePath(env = process.env) {
65175
- return env.HIMI_UPDATE_CACHE_FILE ?? join58(homedir7(), ".himalaya", "update-check.json");
65967
+ return env.HIMI_UPDATE_CACHE_FILE ?? join59(homedir7(), ".himalaya", "update-check.json");
65176
65968
  }
65177
65969
  function readUpdateCache(path2) {
65178
65970
  try {
65179
- const parsed = JSON.parse(readFileSync73(path2, "utf8"));
65971
+ const parsed = JSON.parse(readFileSync74(path2, "utf8"));
65180
65972
  return parsed && typeof parsed === "object" ? parsed : {};
65181
65973
  } catch {
65182
65974
  return {};
@@ -65244,12 +66036,12 @@ function createUpdateRunner(spawn7 = spawnSync5) {
65244
66036
  },
65245
66037
  installedVersion: () => {
65246
66038
  const root = globalNodeModulesRoot();
65247
- return root ? packageVersion(join58(root, "@wix", "himalaya-cli", "package.json")) : void 0;
66039
+ return root ? packageVersion(join59(root, "@wix", "himalaya-cli", "package.json")) : void 0;
65248
66040
  },
65249
66041
  reexec: (argv) => {
65250
66042
  const root = globalNodeModulesRoot();
65251
- const entry = root ? join58(root, "@wix", "himalaya-cli", "dist", "cli.mjs") : void 0;
65252
- if (!entry || !existsSync61(entry)) return void 0;
66043
+ const entry = root ? join59(root, "@wix", "himalaya-cli", "dist", "cli.mjs") : void 0;
66044
+ if (!entry || !existsSync62(entry)) return void 0;
65253
66045
  const result = spawnSync5(process.execPath, [entry, ...argv], {
65254
66046
  // inherit keeps PIPED STDIN working — commands that read stdin must survive a re-exec.
65255
66047
  stdio: "inherit",
@@ -65296,7 +66088,7 @@ function staleUpdateLockFile(cachePath, now) {
65296
66088
  for (const path2 of [`${cachePath}.lock.break`, `${cachePath}.lock`]) {
65297
66089
  let content;
65298
66090
  try {
65299
- content = readFileSync73(path2, "utf8");
66091
+ content = readFileSync74(path2, "utf8");
65300
66092
  } catch {
65301
66093
  continue;
65302
66094
  }
@@ -65319,7 +66111,7 @@ function acquireUpdateLock(cachePath, now, hooks = {}) {
65319
66111
  const mine = lockStamp(now);
65320
66112
  const readLock = (path2) => {
65321
66113
  try {
65322
- return readFileSync73(path2, "utf8");
66114
+ return readFileSync74(path2, "utf8");
65323
66115
  } catch {
65324
66116
  return void 0;
65325
66117
  }
@@ -65430,6 +66222,52 @@ function loaderArgs(execArgv) {
65430
66222
  return out;
65431
66223
  }
65432
66224
  var spawnUpdateRefresh = createUpdateRefreshSpawner();
66225
+ function createTrimValidator(deps = {}, spawnFn = spawn6) {
66226
+ const outcome = (code, report2, said) => {
66227
+ let counts;
66228
+ let errors;
66229
+ try {
66230
+ const parsed = JSON.parse(report2);
66231
+ counts = parsed.counts;
66232
+ if (Array.isArray(parsed.findings)) {
66233
+ const text5 = (v) => typeof v === "string" && v ? v : void 0;
66234
+ errors = parsed.findings.filter((f) => f && f.severity === "error" && typeof f.id === "string").map((f) => ({
66235
+ id: f.id,
66236
+ message: text5(f.message) ?? "",
66237
+ ...text5(f.pass) ? { pass: text5(f.pass) } : {},
66238
+ ...text5(f.screen) ? { screen: text5(f.screen) } : {},
66239
+ ...text5(f.path) ? { path: text5(f.path) } : {},
66240
+ ...text5(f.ruleCode) ? { ruleCode: text5(f.ruleCode) } : {}
66241
+ }));
66242
+ }
66243
+ } catch {
66244
+ }
66245
+ return { code, counts, ...errors ? { errors } : {}, ...code !== 0 ? { tail: said.slice(-4e3) } : {} };
66246
+ };
66247
+ return async (dir, opts = {}) => {
66248
+ const argv = ["validate", "--content", resolve47(dir), "--strict", "--skip-version-check"];
66249
+ if (!opts.fresh) {
66250
+ const out = [];
66251
+ const err = [];
66252
+ const code2 = await run(argv, { log: (l) => out.push(l), error: (l) => err.push(l) }, deps);
66253
+ return outcome(code2, out[out.length - 1] ?? "", [...out, ...err].join("\n"));
66254
+ }
66255
+ const { code, stdout, stderr } = await new Promise((done) => {
66256
+ const chunks = { out: [], err: [] };
66257
+ const child = spawnFn(process.execPath, [...loaderArgs(process.execArgv), fileURLToPath24(import.meta.url), ...argv], {
66258
+ cwd: process.cwd(),
66259
+ env: process.env,
66260
+ stdio: ["ignore", "pipe", "pipe"]
66261
+ });
66262
+ child.stdout?.on("data", (b) => chunks.out.push(b));
66263
+ child.stderr?.on("data", (b) => chunks.err.push(b));
66264
+ child.on("error", (e) => done({ code: 2, stdout: "", stderr: String(e) }));
66265
+ child.on("close", (c) => done({ code: c ?? 1, stdout: Buffer.concat(chunks.out).toString("utf8"), stderr: Buffer.concat(chunks.err).toString("utf8") }));
66266
+ });
66267
+ return outcome(code, stdout.trim(), `${stdout}
66268
+ ${stderr}`);
66269
+ };
66270
+ }
65433
66271
  async function maybeSelfUpdate(argv, io, opts = {}) {
65434
66272
  const env = opts.env ?? process.env;
65435
66273
  const running = opts.running ?? cliVersion();
@@ -65857,7 +66695,7 @@ function executableOnPath(name, env = process.env) {
65857
66695
  const suffixes = process.platform === "win32" ? (env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean) : [""];
65858
66696
  for (const dir of entries) {
65859
66697
  for (const suffix of suffixes) {
65860
- const candidate = join58(dir, `${name}${suffix}`);
66698
+ const candidate = join59(dir, `${name}${suffix}`);
65861
66699
  try {
65862
66700
  if (!statSync17(candidate).isFile()) continue;
65863
66701
  if (process.platform !== "win32") accessSync(candidate, constants2.X_OK);
@@ -65966,7 +66804,7 @@ async function nativeConfigAdvisories(app, configSource) {
65966
66804
  const { iosProjectYml: iosProjectYml2 } = await Promise.resolve().then(() => (init_paths(), paths_exports));
65967
66805
  const { androidOptedIn: androidOptedIn3, LABEL_IOS_SPLASH: LABEL_IOS_SPLASH2 } = await Promise.resolve().then(() => (init_splash(), splash_exports));
65968
66806
  const iosYmlPath = iosProjectYml2(app);
65969
- const iosYml = existsSync61(iosYmlPath) ? readFileSync73(iosYmlPath, "utf8") : "";
66807
+ const iosYml = existsSync62(iosYmlPath) ? readFileSync74(iosYmlPath, "utf8") : "";
65970
66808
  let optedIn = iosYml.includes(LABEL_IOS_SPLASH2);
65971
66809
  if (!optedIn) {
65972
66810
  const { androidManifestPath: androidManifestPath3 } = await Promise.resolve().then(() => (init_gen_native_config(), gen_native_config_exports));
@@ -65980,7 +66818,7 @@ async function nativeConfigAdvisories(app, configSource) {
65980
66818
  }
65981
66819
  function readConfigSource(dir) {
65982
66820
  try {
65983
- return readFileSync73(join58(dir, "config.ts"), "utf8");
66821
+ return readFileSync74(join59(dir, "config.ts"), "utf8");
65984
66822
  } catch {
65985
66823
  return null;
65986
66824
  }
@@ -65996,7 +66834,7 @@ function keptPlan(kept) {
65996
66834
  }
65997
66835
  async function initFromRemoteTemplate(name, parentDir, template, c) {
65998
66836
  const dir = resolve47(parentDir, name);
65999
- if (existsSync61(join58(dir, "himalaya.content.json"))) {
66837
+ if (existsSync62(join59(dir, "himalaya.content.json"))) {
66000
66838
  throw new Error(`content package already exists at ${dir}`);
66001
66839
  }
66002
66840
  const r = await c.fetchTemplateSource(TEMPLATE_APP_PREFIX + template);
@@ -66039,7 +66877,7 @@ function symlinkDestinationIssue(dest) {
66039
66877
  function restoreDestinationIssue(dest, force) {
66040
66878
  const link = symlinkDestinationIssue(dest);
66041
66879
  if (link) return link;
66042
- if (!existsSync61(dest)) return null;
66880
+ if (!existsSync62(dest)) return null;
66043
66881
  if (!statSync17(dest).isDirectory()) {
66044
66882
  return { error: `${dest} exists and is not a directory`, hint: "pass --out/--name to land somewhere else, or remove the file" };
66045
66883
  }
@@ -66054,7 +66892,7 @@ function restoreDestinationIssue(dest, force) {
66054
66892
  function isHimalayaMonorepoCheckout(start) {
66055
66893
  let dir = resolve47(start);
66056
66894
  for (; ; ) {
66057
- if (existsSync61(join58(dir, "tools", "himi-cli", "src", "cli.ts")) && existsSync61(join58(dir, "core", "serve", "src", "server.ts"))) return true;
66895
+ if (existsSync62(join59(dir, "tools", "himi-cli", "src", "cli.ts")) && existsSync62(join59(dir, "core", "serve", "src", "server.ts"))) return true;
66058
66896
  const parent = dirname32(dir);
66059
66897
  if (parent === dir) return false;
66060
66898
  dir = parent;
@@ -66097,7 +66935,7 @@ function materializeForPreview(appDir2, slug, displayName) {
66097
66935
  Object.entries(files).map(([rel, body]) => [rel, rel === "himalaya.content.json" ? substituteIdentity(body, slug) : body])
66098
66936
  );
66099
66937
  const repoRoot2 = resolve47(fileURLToPath24(new URL("../../..", import.meta.url)));
66100
- const root = join58(repoRoot2, "node_modules", ".cache", "himi-template-preview");
66938
+ const root = join59(repoRoot2, "node_modules", ".cache", "himi-template-preview");
66101
66939
  const stillRunning = (pid) => {
66102
66940
  try {
66103
66941
  process.kill(pid, 0);
@@ -66111,12 +66949,12 @@ function materializeForPreview(appDir2, slug, displayName) {
66111
66949
  if (!entry.isDirectory()) continue;
66112
66950
  const pid = Number(entry.name.startsWith(`${slug}-`) ? entry.name.slice(slug.length + 1) : NaN);
66113
66951
  if (Number.isInteger(pid) && pid > 0 && pid !== process.pid && !stillRunning(pid)) {
66114
- rmSync14(join58(root, entry.name), { recursive: true, force: true });
66952
+ rmSync14(join59(root, entry.name), { recursive: true, force: true });
66115
66953
  }
66116
66954
  }
66117
66955
  } catch {
66118
66956
  }
66119
- const dest = join58(root, `${slug}-${process.pid}`);
66957
+ const dest = join59(root, `${slug}-${process.pid}`);
66120
66958
  rmSync14(dest, { recursive: true, force: true });
66121
66959
  mkdirSync30(dest, { recursive: true });
66122
66960
  materializeSource(forBuild, displayName, dest, slug);
@@ -66124,8 +66962,8 @@ function materializeForPreview(appDir2, slug, displayName) {
66124
66962
  }
66125
66963
  async function appId(dir) {
66126
66964
  try {
66127
- const { readFileSync: readFileSync74 } = await import("node:fs");
66128
- const m = JSON.parse(readFileSync74(join58(dir, "himalaya.content.json"), "utf8"));
66965
+ const { readFileSync: readFileSync75 } = await import("node:fs");
66966
+ const m = JSON.parse(readFileSync75(join59(dir, "himalaya.content.json"), "utf8"));
66129
66967
  if (m.name) return m.name;
66130
66968
  } catch {
66131
66969
  }
@@ -66133,8 +66971,8 @@ async function appId(dir) {
66133
66971
  }
66134
66972
  async function contentVisibility(dir) {
66135
66973
  try {
66136
- const { readFileSync: readFileSync74 } = await import("node:fs");
66137
- const m = JSON.parse(readFileSync74(join58(dir, "himalaya.content.json"), "utf8"));
66974
+ const { readFileSync: readFileSync75 } = await import("node:fs");
66975
+ const m = JSON.parse(readFileSync75(join59(dir, "himalaya.content.json"), "utf8"));
66138
66976
  if (m.visibility === "public" || m.visibility === "unlisted") return m.visibility;
66139
66977
  } catch {
66140
66978
  }
@@ -66358,7 +67196,7 @@ async function recordedReleaseForContentQuietly(dir, resolution, onDrift) {
66358
67196
  try {
66359
67197
  const { buildContentForValidation: buildContentForValidation2, buildReleaseFromContent: buildReleaseFromContent2, bundleOutDir: bundleOutDir2 } = await loadAuthoring();
66360
67198
  const bres = await buildContentForValidation2(dir, { resolution });
66361
- out = mkdtempSync4(join58(tmpdir5(), "himi-ux-record-"));
67199
+ out = mkdtempSync4(join59(tmpdir5(), "himi-ux-record-"));
66362
67200
  const hasWorkers = bres.built.length > 0 || bres.compileErrors.length > 0;
66363
67201
  const outcome = await buildReleaseFromContent2(dir, out, { ...hasWorkers ? { tier5Dir: bundleOutDir2(dir) } : {}, collect: { lint: true } });
66364
67202
  const releaseId = outcome.manifest.releaseId;
@@ -66536,7 +67374,7 @@ async function producedWidgetFamilies(dir) {
66536
67374
  let js;
66537
67375
  for (const candidate of [`${name}.bundle.js`, `${name}.js`, name]) {
66538
67376
  try {
66539
- js = readFileSync73(join58(outDir, candidate), "utf8");
67377
+ js = readFileSync74(join59(outDir, candidate), "utf8");
66540
67378
  break;
66541
67379
  } catch {
66542
67380
  }
@@ -66567,7 +67405,7 @@ async function producedWidgetFamilies(dir) {
66567
67405
  }
66568
67406
  return { families, source: "dist/bundles" };
66569
67407
  }
66570
- var boardOpen = (dir) => existsSync61(join58(dir, ".himi", "studio", "index.html"));
67408
+ var boardOpen = (dir) => existsSync62(join59(dir, ".himi", "studio", "index.html"));
66571
67409
  var BOARD_REFRESHERS = /* @__PURE__ */ new Set(["render", "test", "validate", "build", "push", "icon", "site", "ux", "progress"]);
66572
67410
  async function leaveForBoard(dir, record2, value) {
66573
67411
  if (!boardOpen(dir)) return;
@@ -66932,19 +67770,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
66932
67770
  const { runTemplateTrim: runTemplateTrim2 } = await Promise.resolve().then(() => (init_template_trim(), template_trim_exports));
66933
67771
  const outcome = await runTemplateTrim2(
66934
67772
  { dir: contentDir(flags), keep: flags.keep, links: flags.links, dryRun: flags["dry-run"] === true, force: flags.force === true },
66935
- {
66936
- validate: async (dir) => {
66937
- const out = [];
66938
- const err = [];
66939
- const code = await run(["validate", "--content", dir, "--strict"], { log: (l) => out.push(l), error: (l) => err.push(l) }, deps);
66940
- let counts;
66941
- try {
66942
- counts = JSON.parse(out[out.length - 1] ?? "").counts;
66943
- } catch {
66944
- }
66945
- return { code, counts, ...code !== 0 ? { tail: [...out, ...err].join("\n").slice(-4e3) } : {} };
66946
- }
66947
- }
67773
+ { validate: createTrimValidator(deps) }
66948
67774
  );
66949
67775
  if (outcome.code === 2) io.error(JSON.stringify(outcome.result));
66950
67776
  else print(outcome.result);
@@ -66955,7 +67781,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
66955
67781
  const sourceDir = resolve47(str6(flags["source-dir"]) ?? appDir2);
66956
67782
  let meta;
66957
67783
  try {
66958
- meta = JSON.parse(readFileSync73(join58(sourceDir, TEMPLATE_META_FILE), "utf8"));
67784
+ meta = JSON.parse(readFileSync74(join59(sourceDir, TEMPLATE_META_FILE), "utf8"));
66959
67785
  } catch {
66960
67786
  io.error(JSON.stringify({ error: `no ${TEMPLATE_META_FILE} in ${sourceDir} \u2014 a template must declare { title, description, tags } to be published`, hint: `add ${TEMPLATE_META_FILE} next to himalaya.content.json (or pass --source-dir <dir>)` }));
66961
67787
  return 2;
@@ -66965,7 +67791,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
66965
67791
  return 2;
66966
67792
  }
66967
67793
  try {
66968
- const appearance = extractTemplateAppearance(JSON.parse(readFileSync73(join58(sourceDir, "tokens.json"), "utf8")));
67794
+ const appearance = extractTemplateAppearance(JSON.parse(readFileSync74(join59(sourceDir, "tokens.json"), "utf8")));
66969
67795
  meta = { ...appearance, ...meta };
66970
67796
  } catch {
66971
67797
  }
@@ -66977,7 +67803,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
66977
67803
  const force = flags.force === true;
66978
67804
  const { buildContentForValidation: buildContentForValidation2, buildReleaseFromContent: buildReleaseFromContent2, bundleOutDir: bundleOutDir2 } = await loadAuthoring();
66979
67805
  const bres = await buildContentForValidation2(buildDir, { resolution: resolutionFlag(flags) });
66980
- const out = mkdtempSync4(join58(tmpdir5(), "himi-template-"));
67806
+ const out = mkdtempSync4(join59(tmpdir5(), "himi-template-"));
66981
67807
  const hasWorkers = bres.built.length > 0 || bres.compileErrors.length > 0;
66982
67808
  const outcome = await buildReleaseFromContent2(buildDir, out, {
66983
67809
  ...hasWorkers ? { tier5Dir: bundleOutDir2(buildDir) } : {},
@@ -67030,7 +67856,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67030
67856
  let iconHash;
67031
67857
  if (iconPath) {
67032
67858
  try {
67033
- const bytes2 = readFileSync73(iconPath);
67859
+ const bytes2 = readFileSync74(iconPath);
67034
67860
  payload.icon = bytes2.toString("base64");
67035
67861
  iconHash = createHash27("sha256").update(bytes2).digest("hex");
67036
67862
  } catch {
@@ -67263,7 +68089,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67263
68089
  const { navIssues: navIssues2 } = await Promise.resolve().then(() => (init_nav_lint(), nav_lint_exports));
67264
68090
  const { buildContentForValidation: buildContentForValidation2, buildReleaseFromContent: buildReleaseFromContent2, bundleOutDir: bundleOutDir2 } = await loadAuthoring();
67265
68091
  const bres = await buildContentForValidation2(dir, { resolution: resolutionFlag(flags) });
67266
- const out = mkdtempSync4(join58(tmpdir5(), "himi-validate-"));
68092
+ const out = mkdtempSync4(join59(tmpdir5(), "himi-validate-"));
67267
68093
  const hasWorkers = bres.built.length > 0 || bres.compileErrors.length > 0;
67268
68094
  const outcome = await buildReleaseFromContent2(dir, out, {
67269
68095
  ...hasWorkers ? { tier5Dir: bundleOutDir2(dir) } : {},
@@ -67306,7 +68132,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67306
68132
  const path2 = dpxSessionTokenPath();
67307
68133
  let raw;
67308
68134
  try {
67309
- raw = readFileSync73(path2, "utf8");
68135
+ raw = readFileSync74(path2, "utf8");
67310
68136
  } catch {
67311
68137
  throw new Error(`no DPX BO session at ${path2}; run dpx auth login, write dpx auth token to that path, then himi login --dpx --target bo`);
67312
68138
  }
@@ -67426,13 +68252,13 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67426
68252
  case "studio": {
67427
68253
  const { openStudio: openStudio2 } = await Promise.resolve().then(() => (init_studio(), studio_exports));
67428
68254
  const dir = contentDir(flags);
67429
- if (!existsSync61(join58(dir, "himalaya.content.json"))) {
68255
+ if (!existsSync62(join59(dir, "himalaya.content.json"))) {
67430
68256
  io.error(JSON.stringify({ ok: false, error: "not_a_content_package", dir, hint: "run from the app directory (or pass --content <dir>) \u2014 the board reads that package's .himi/" }));
67431
68257
  return 2;
67432
68258
  }
67433
68259
  if (pos[1] === "reel") {
67434
68260
  const { buildReel: buildReel2 } = await Promise.resolve().then(() => (init_studio_reel(), studio_reel_exports));
67435
- const out = resolve47(str6(flags.out) ?? join58(dir, ".himi", "making-of.html"));
68261
+ const out = resolve47(str6(flags.out) ?? join59(dir, ".himi", "making-of.html"));
67436
68262
  const { html, plan } = buildReel2(dir);
67437
68263
  mkdirSync30(dirname32(out), { recursive: true });
67438
68264
  writeFileSync28(out, html);
@@ -67482,7 +68308,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67482
68308
  const { appendProgress: appendProgress2, readProgress: readProgress2, summarizeProgress: summarizeProgress2, parseStep: parseStep2, notifyDesktop: notifyDesktop2, appLabel: appLabel2, journalPath: journalPath2, ProgressError: ProgressError2, JOURNEY_STEPS: JOURNEY_STEPS2 } = await Promise.resolve().then(() => (init_progress(), progress_exports));
67483
68309
  const dir = contentDir(flags);
67484
68310
  const journal = journalPath2(dir);
67485
- const hint = existsSync61(join58(dir, "himalaya.content.json")) ? {} : { hint: "no himalaya.content.json here \u2014 run from the app directory (or pass --content <dir>) so the build board finds this journal" };
68311
+ const hint = existsSync62(join59(dir, "himalaya.content.json")) ? {} : { hint: "no himalaya.content.json here \u2014 run from the app directory (or pass --content <dir>) so the build board finds this journal" };
67486
68312
  const message = pos.slice(1).join(" ").trim();
67487
68313
  if (!message) {
67488
68314
  if (flags.step !== void 0 || flags.image !== void 0 || flags.notify === true) {
@@ -67574,7 +68400,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67574
68400
  case "test": {
67575
68401
  const dir = contentDir(flags);
67576
68402
  const structuredOutput = flags.json === true || io.isTTY !== true;
67577
- if (existsSync61(join58(dir, "functions"))) {
68403
+ if (existsSync62(join59(dir, "functions"))) {
67578
68404
  const { invokeLocalFunction: invokeLocalFunction2, loadFunctionsApp: loadFunctionsApp2 } = await Promise.resolve().then(() => (init_functions(), functions_exports));
67579
68405
  const loaded = await loadFunctionsApp2(dir);
67580
68406
  const results = [];
@@ -67661,7 +68487,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67661
68487
  }
67662
68488
  }
67663
68489
  const bres = await buildContentBundles2(dir, { resolution: resolutionFlag(flags) });
67664
- const out = mkdtempSync4(join58(tmpdir5(), "himi-test-"));
68490
+ const out = mkdtempSync4(join59(tmpdir5(), "himi-test-"));
67665
68491
  const hasWorkers = bres.built.length > 0 || bres.compileErrors.length > 0;
67666
68492
  const outcome = await buildReleaseFromContent2(dir, out, {
67667
68493
  ...hasWorkers ? { tier5Dir: bundleOutDir2(dir) } : {},
@@ -67706,7 +68532,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67706
68532
  io.error(JSON.stringify({ error: `screen "${screen.screenId}" has no Tier-5 worker, so there is nothing for the browser loop to build` }));
67707
68533
  return 2;
67708
68534
  }
67709
- const sources = collectWorkerSources2(join58(dir, "tier5-src"), screen.bundleName);
68535
+ const sources = collectWorkerSources2(join59(dir, "tier5-src"), screen.bundleName);
67710
68536
  if (!sources) {
67711
68537
  io.error(JSON.stringify({ error: `no worker source found at tier5-src/${screen.bundleName} \u2014 the browser loop compiles SOURCE, not the built bundle` }));
67712
68538
  return 2;
@@ -67867,7 +68693,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67867
68693
  const sp = await runShotsPass2({
67868
68694
  descriptors: scopeDescriptors2(outcome.descriptors ?? [], pos.slice(1), invalidScreens),
67869
68695
  tokens: outcome.tokens,
67870
- outDir: join58(dir, ".himi", "shots"),
68696
+ outDir: join59(dir, ".himi", "shots"),
67871
68697
  width: 390,
67872
68698
  layers: { rules: shotRules, ...cmsSeed ? { cmsSeed } : {}, ...canned ? { canned: canned.byKey } : {} },
67873
68699
  ...budgetMs ? { budgetMs } : {},
@@ -67905,7 +68731,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67905
68731
  releaseId: result.releaseId,
67906
68732
  releasePayload: release,
67907
68733
  descriptors: attestedDescriptors,
67908
- screenshots: Object.fromEntries(writtenShots.filter((name) => crawledScreens.has(name.replace(/\.png$/, ""))).map((name) => [name.replace(/\.png$/, ""), join58(dir, ".himi", "shots", name)])),
68734
+ screenshots: Object.fromEntries(writtenShots.filter((name) => crawledScreens.has(name.replace(/\.png$/, ""))).map((name) => [name.replace(/\.png$/, ""), join59(dir, ".himi", "shots", name)])),
67909
68735
  coverage: {
67910
68736
  screens: crawledScreens.size,
67911
68737
  actionsDispatched: result.actions.exercised,
@@ -67995,7 +68821,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
67995
68821
  return 2;
67996
68822
  }
67997
68823
  try {
67998
- const raw = stateArg.trim().startsWith("{") ? stateArg : readFileSync73(stateArg.replace(/^@/, ""), "utf8");
68824
+ const raw = stateArg.trim().startsWith("{") ? stateArg : readFileSync74(stateArg.replace(/^@/, ""), "utf8");
67999
68825
  injectedState = JSON.parse(raw);
68000
68826
  } catch (e) {
68001
68827
  io.error(JSON.stringify({ error: `--state must be inline JSON or a path to a .json file \u2014 ${e.message}` }));
@@ -68010,7 +68836,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
68010
68836
  return 2;
68011
68837
  }
68012
68838
  try {
68013
- const raw = inputArg.trim().startsWith("{") ? inputArg : readFileSync73(inputArg.replace(/^@/, ""), "utf8");
68839
+ const raw = inputArg.trim().startsWith("{") ? inputArg : readFileSync74(inputArg.replace(/^@/, ""), "utf8");
68014
68840
  const parsed = JSON.parse(raw);
68015
68841
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("must be a JSON object");
68016
68842
  injectedInput = parsed;
@@ -68059,7 +68885,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
68059
68885
  }
68060
68886
  const { buildContentBundles: buildContentBundles2, buildReleaseFromContent: buildReleaseFromContent2, bundleOutDir: bundleOutDir2 } = await loadAuthoring();
68061
68887
  const bres = await buildContentBundles2(dir, { resolution: resolutionFlag(flags) });
68062
- const out = mkdtempSync4(join58(tmpdir5(), "himi-render-"));
68888
+ const out = mkdtempSync4(join59(tmpdir5(), "himi-render-"));
68063
68889
  const hasWorkers = bres.built.length > 0 || bres.compileErrors.length > 0;
68064
68890
  const outcome = await buildReleaseFromContent2(dir, out, {
68065
68891
  ...hasWorkers ? { tier5Dir: bundleOutDir2(dir) } : {},
@@ -68073,7 +68899,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
68073
68899
  const diagnostics = (outcome.lint ?? []).find((s) => s.screenId === screenId)?.issues ?? [];
68074
68900
  let pngFile;
68075
68901
  let attestationPngFile;
68076
- const shotPath = join58(dir, ".himi", "shots", `${screenId}.png`);
68902
+ const shotPath = join59(dir, ".himi", "shots", `${screenId}.png`);
68077
68903
  const pngTarget = typeof flags.png === "string" ? str6(flags.png) : flags.png === true ? shotPath : void 0;
68078
68904
  if (pngTarget && r.pngBase64) {
68079
68905
  pngFile = writePngFile(pngTarget, r.pngBase64);
@@ -68203,7 +69029,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
68203
69029
  installIcon: installIcon2,
68204
69030
  siteBookIcon: siteBookIcon2,
68205
69031
  siteLogoAbsence: siteLogoAbsence2,
68206
- summarize: summarize3
69032
+ summarize: summarize4
68207
69033
  } = await Promise.resolve().then(() => (init_icon(), icon_exports));
68208
69034
  const requireVerified = sub === "check";
68209
69035
  const report2 = async (status, extra = {}) => {
@@ -68228,11 +69054,11 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
68228
69054
  }))
68229
69055
  ];
68230
69056
  if (flags.json === true) {
68231
- print({ ok: !blocksRelease2(status, { requireVerified }), ...extra, ...status, findings, summary: summarize3(status) });
69057
+ print({ ok: !blocksRelease2(status, { requireVerified }), ...extra, ...status, findings, summary: summarize4(status) });
68232
69058
  return blocksRelease2(status, { requireVerified }) ? 1 : 0;
68233
69059
  }
68234
69060
  for (const [k, v] of Object.entries(extra)) io.log(`${k}: ${Array.isArray(v) ? v.join(", ") : String(v)}`);
68235
- io.log(`launcher icon: ${summarize3(status)}`);
69061
+ io.log(`launcher icon: ${summarize4(status)}`);
68236
69062
  if (status.path) io.log(` file: ${status.path}${status.header ? ` (${status.header.width}x${status.header.height})` : ""}`);
68237
69063
  for (const f of findings) io.log(` ${f.level === "error" ? "\u2717" : "\u26A0"} ${f.message}`);
68238
69064
  if (status.verdict === "ok" || status.verdict === "unverified") {
@@ -68409,7 +69235,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
68409
69235
  io.error(JSON.stringify(linked));
68410
69236
  return 1;
68411
69237
  }
68412
- if (existsSync61(dest)) {
69238
+ if (existsSync62(dest)) {
68413
69239
  if (!statSync17(dest).isDirectory()) {
68414
69240
  io.error(JSON.stringify({
68415
69241
  error: `${dest} exists and is not a directory`,
@@ -68422,7 +69248,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
68422
69248
  io.error(JSON.stringify({ error: `${dest} already exists and is not empty`, hint: "pass --force to write into it anyway (merging, and naming what it keeps), --replace to restore exactly what was archived, or --out/--name to land somewhere else" }));
68423
69249
  return 1;
68424
69250
  }
68425
- if (replace && occupied && !existsSync61(join58(dest, "himalaya.content.json"))) {
69251
+ if (replace && occupied && !existsSync62(join59(dest, "himalaya.content.json"))) {
68426
69252
  io.error(JSON.stringify({
68427
69253
  error: `${dest} is not a content package \u2014 refusing to --replace it`,
68428
69254
  hint: "--replace only clears a directory that holds himalaya.content.json. Check --out/--name, or pass --force to merge into it instead."
@@ -68561,7 +69387,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
68561
69387
  const force = flags.force === true;
68562
69388
  const { buildContentForValidation: buildContentForValidation2, buildReleaseFromContent: buildReleaseFromContent2, bundleOutDir: bundleOutDir2 } = await loadAuthoring();
68563
69389
  const bres = await buildContentForValidation2(dir, { resolution: resolutionFlag(flags) });
68564
- const out = mkdtempSync4(join58(tmpdir5(), "himi-push-"));
69390
+ const out = mkdtempSync4(join59(tmpdir5(), "himi-push-"));
68565
69391
  const hasWorkers = bres.built.length > 0 || bres.compileErrors.length > 0;
68566
69392
  const outcome = await buildReleaseFromContent2(dir, out, {
68567
69393
  ...hasWorkers ? { tier5Dir: bundleOutDir2(dir) } : {},
@@ -69082,7 +69908,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
69082
69908
  }
69083
69909
  const { buildContentForValidation: buildContentForValidation2, buildReleaseFromContent: buildReleaseFromContent2, bundleOutDir: bundleOutDir2 } = await loadAuthoring();
69084
69910
  const bres = await buildContentForValidation2(dir, { resolution: resolutionFlag(flags) });
69085
- const built = mkdtempSync4(join58(tmpdir5(), "himi-pack-"));
69911
+ const built = mkdtempSync4(join59(tmpdir5(), "himi-pack-"));
69086
69912
  const hasWorkers = bres.built.length > 0 || bres.compileErrors.length > 0;
69087
69913
  const outcome = await buildReleaseFromContent2(dir, built, {
69088
69914
  ...hasWorkers ? { tier5Dir: bundleOutDir2(dir) } : {},
@@ -69285,7 +70111,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
69285
70111
  }));
69286
70112
  return 1;
69287
70113
  }
69288
- const archive = readFileSync73(resolve47(file));
70114
+ const archive = readFileSync74(resolve47(file));
69289
70115
  const result = await client2().createPackageShare({
69290
70116
  app: opened.header.app,
69291
70117
  releaseId: opened.header.releaseId,
@@ -69339,11 +70165,11 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
69339
70165
  const app = await appId(dir2);
69340
70166
  const { buildContentBundles: buildContentBundles2, buildReleaseFromContent: buildReleaseFromContent2, bundleOutDir: bundleOutDir2 } = await loadAuthoring();
69341
70167
  const bres = await buildContentBundles2(dir2, { resolution: resolutionFlag(flags) });
69342
- temp = mkdtempSync4(join58(tmpdir5(), "himi-run-build-"));
70168
+ temp = mkdtempSync4(join59(tmpdir5(), "himi-run-build-"));
69343
70169
  const hasWorkers = bres.built.length > 0 || bres.compileErrors.length > 0;
69344
70170
  const initial = await buildReleaseFromContent2(dir2, temp, { ...hasWorkers ? { tier5Dir: bundleOutDir2(dir2) } : {} });
69345
70171
  servedMissing = initial.missingBundles;
69346
- source = join58(temp, app);
70172
+ source = join59(temp, app);
69347
70173
  }
69348
70174
  const chromeArg = str6(flags.chrome) ?? "preview";
69349
70175
  if (chromeArg !== "preview" && chromeArg !== "app") {
@@ -69379,7 +70205,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
69379
70205
  if (!recordKit) return;
69380
70206
  startedAt = (/* @__PURE__ */ new Date()).toISOString();
69381
70207
  const failure = record2(served2, { ok: true, at: startedAt });
69382
- if (!recorded) io.error(JSON.stringify({ run: "run_record_skipped", message: `${failure ?? `${join58(dir, ".himi")} is not a real directory`}, so the build board will not frame this preview.` }));
70208
+ if (!recorded) io.error(JSON.stringify({ run: "run_record_skipped", message: `${failure ?? `${join59(dir, ".himi")} is not a real directory`}, so the build board will not frame this preview.` }));
69383
70209
  await refreshBoard();
69384
70210
  };
69385
70211
  const retract = async () => {
@@ -69410,7 +70236,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
69410
70236
  }
69411
70237
  const watchKit = watch2 ? await Promise.resolve().then(() => (init_run_watch(), run_watch_exports)) : null;
69412
70238
  const bus = watchKit ? new watchKit.ReloadBus() : void 0;
69413
- if (watch2) served = mkdtempSync4(join58(tmpdir5(), "himi-run-served-"));
70239
+ if (watch2) served = mkdtempSync4(join59(tmpdir5(), "himi-run-served-"));
69414
70240
  const running = await runPackage2(source, {
69415
70241
  port,
69416
70242
  chrome: chromeArg,
@@ -69431,8 +70257,8 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
69431
70257
  await announce(running);
69432
70258
  if (watchKit && bus) {
69433
70259
  const { buildContentBundles: buildContentBundles2, buildReleaseFromContent: buildReleaseFromContent2, bundleOutDir: bundleOutDir2 } = await loadAuthoring();
69434
- const live2 = join58(running.releaseRoot, running.app);
69435
- const manifestOf = (tree) => JSON.parse(readFileSync73(join58(tree, "release-manifest.json"), "utf8"));
70260
+ const live2 = join59(running.releaseRoot, running.app);
70261
+ const manifestOf = (tree) => JSON.parse(readFileSync74(join59(tree, "release-manifest.json"), "utf8"));
69436
70262
  let current = manifestOf(live2);
69437
70263
  const failed2 = async (message) => {
69438
70264
  if (stopping) return;
@@ -69443,7 +70269,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
69443
70269
  };
69444
70270
  rebuild = watchKit.serialize(async () => {
69445
70271
  const t0 = Date.now();
69446
- const next = mkdtempSync4(join58(tmpdir5(), "himi-run-build-"));
70272
+ const next = mkdtempSync4(join59(tmpdir5(), "himi-run-build-"));
69447
70273
  try {
69448
70274
  const bres = await buildContentBundles2(dir, { resolution: resolutionFlag(flags) });
69449
70275
  if (bres.compileErrors.length) {
@@ -69460,14 +70286,14 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
69460
70286
  return;
69461
70287
  }
69462
70288
  if (stopping) return;
69463
- const fresh = manifestOf(join58(next, running.app));
70289
+ const fresh = manifestOf(join59(next, running.app));
69464
70290
  const diff = watchKit.diffReleases(current, fresh);
69465
70291
  const retired = `${live2}.retired-${Date.now()}`;
69466
70292
  renameSync6(live2, retired);
69467
70293
  try {
69468
- renameSync6(join58(next, running.app), live2);
70294
+ renameSync6(join59(next, running.app), live2);
69469
70295
  } catch {
69470
- cpSync3(join58(next, running.app), live2, { recursive: true });
70296
+ cpSync3(join59(next, running.app), live2, { recursive: true });
69471
70297
  }
69472
70298
  rmSync14(retired, { recursive: true, force: true });
69473
70299
  current = fresh;
@@ -70065,7 +70891,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
70065
70891
  io.error(JSON.stringify({ error: `patch ${kind} needs --file`, usage: PATCH_USAGE }));
70066
70892
  return 2;
70067
70893
  }
70068
- if (!existsSync61(file)) {
70894
+ if (!existsSync62(file)) {
70069
70895
  io.error(JSON.stringify({ error: `no such file: ${file}` }));
70070
70896
  return 2;
70071
70897
  }
@@ -70121,7 +70947,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
70121
70947
  lintNote: "`himi patch` sends a raw file. Nothing here built or linted your app \u2014 `himi push --only <selector>` is the same edit with the full lint gate in front of it."
70122
70948
  };
70123
70949
  if (kind === "tokens" || kind === "app-config") {
70124
- const body = readFileSync73(file, "utf8");
70950
+ const body = readFileSync74(file, "utf8");
70125
70951
  const patch2 = kind === "tokens" ? { tokens: body } : { appConfig: body };
70126
70952
  return runReleasePatch({
70127
70953
  ...common,
@@ -70136,7 +70962,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
70136
70962
  io.error(JSON.stringify({ error: "patch screen needs a <screenId>", usage: PATCH_USAGE }));
70137
70963
  return 2;
70138
70964
  }
70139
- const descriptor = readFileSync73(file, "utf8");
70965
+ const descriptor = readFileSync74(file, "utf8");
70140
70966
  try {
70141
70967
  JSON.parse(descriptor);
70142
70968
  } catch (e) {
@@ -70176,7 +71002,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
70176
71002
  extra: { kind, screen: id, file: { path: file, bytes: Buffer.byteLength(descriptor, "utf8") } }
70177
71003
  });
70178
71004
  }
70179
- const bytes2 = readFileSync73(file);
71005
+ const bytes2 = readFileSync74(file);
70180
71006
  const sha = createHash27("sha256").update(bytes2).digest("hex");
70181
71007
  const forScreens = (str6(flags["for-screen"]) ?? "").split(",").map((s) => s.trim()).filter(Boolean);
70182
71008
  if (flags["no-rebind"] !== void 0 && flags["no-rebind"] !== true) {
@@ -70320,7 +71146,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
70320
71146
  }
70321
71147
  let def;
70322
71148
  try {
70323
- const parsed = JSON.parse(arg.startsWith("@") ? readFileSync73(arg.slice(1), "utf8") : arg);
71149
+ const parsed = JSON.parse(arg.startsWith("@") ? readFileSync74(arg.slice(1), "utf8") : arg);
70324
71150
  if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
70325
71151
  io.error(JSON.stringify({ error: "invalid_definition", problems: [`--definition must be a JSON object, got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`] }));
70326
71152
  return 2;
@@ -70784,7 +71610,7 @@ ${label2}`);
70784
71610
  const platform = str6(flags.platform)?.split(",").map((p) => p.trim()).filter(Boolean);
70785
71611
  {
70786
71612
  const distDir = contentDir(flags);
70787
- if (existsSync61(join58(distDir, "himalaya.content.json"))) {
71613
+ if (existsSync62(join59(distDir, "himalaya.content.json"))) {
70788
71614
  const issues = await iconIssues(distDir, flags.strict === true, { requireDeclared: true, requireVerified: true });
70789
71615
  const errors = issues.filter((i) => i.severity === "error");
70790
71616
  for (const i of issues.filter((i2) => i2.severity === "warning")) {
@@ -71053,7 +71879,7 @@ ${label2}`);
71053
71879
  const dpxPath = dpxSessionTokenPath();
71054
71880
  let dpx;
71055
71881
  try {
71056
- dpx = readFileSync73(dpxPath, "utf8").trim();
71882
+ dpx = readFileSync74(dpxPath, "utf8").trim();
71057
71883
  } catch {
71058
71884
  io.error(JSON.stringify({ error: `no dpx session at ${dpxPath} \u2014 run \`dpx auth login\` then \`dpx auth token > ${dpxPath}\` (dpx 2.x keeps its own state in ~/.dpx/auth.json and does NOT write the legacy session file), or omit --dpx` }));
71059
71885
  return 1;
@@ -71455,7 +72281,7 @@ ${report2.ok ? "OK \u2014 data + serve plane reachable." : typeof report2.hint =
71455
72281
  const path2 = dpxSessionTokenPath();
71456
72282
  let raw;
71457
72283
  try {
71458
- raw = readFileSync73(path2, "utf8");
72284
+ raw = readFileSync74(path2, "utf8");
71459
72285
  } catch {
71460
72286
  io.error(JSON.stringify({ error: `no DPX BO session at ${path2}` }));
71461
72287
  return 1;
@@ -71722,10 +72548,11 @@ ${report2.ok ? "OK \u2014 data + serve plane reachable." : typeof report2.hint =
71722
72548
  }
71723
72549
  }
71724
72550
  if (sub !== "analyze") {
71725
- io.error(JSON.stringify({ error: sub ? `unknown site subcommand "${sub}"` : "site needs a subcommand", usage: `himi site create <name> [--type wix|headless|branded-app|vibe] --yes | himi site list | himi site client ensure|list --site <msid> | himi site analyze --client-id <id> --url <https://\u2026> [--site <msid>] [--out <dir>] [--apply [--shell <id>]] | himi site function list|get|put|publish|invoke \u2026` }));
72551
+ io.error(JSON.stringify({ error: sub ? `unknown site subcommand "${sub}"` : "site needs a subcommand", usage: `himi site create <name> [--type wix|headless|branded-app|vibe] --yes | himi site list | himi site client ensure|list --site <msid> | himi site analyze --url <https://\u2026> [--client-id <id>] [--site <msid>] [--out <dir>] [--apply [--shell <id>]] | himi site function list|get|put|publish|invoke \u2026` }));
71726
72552
  return 2;
71727
72553
  }
71728
- const clientId = str6(flags["client-id"]) ?? process.env.WIX_CLIENT_ID;
72554
+ const clientFlag = str6(flags["client-id"]);
72555
+ const clientId = clientFlag ?? process.env.WIX_CLIENT_ID;
71729
72556
  const siteUrl = str6(flags.url) ?? str6(flags["site-url"]);
71730
72557
  if (flags["read-only"] === true) {
71731
72558
  if (flags.apply || flags.out || flags.shell || flags["redirect-uri"]) {
@@ -71755,24 +72582,27 @@ ${report2.ok ? "OK \u2014 data + serve plane reachable." : typeof report2.hint =
71755
72582
  print({ ok: true, context });
71756
72583
  return 0;
71757
72584
  }
71758
- if (!clientId || !siteUrl) {
72585
+ const analyzeUsage = "himi site analyze --url https://\u2026 [--client-id <headless-oauth-client-id>] [--site <msid>] [--out .himi/business-book] [--redirect-uri <uri>] [--apply [--shell " + SHELL_ENUM + "]]";
72586
+ if (!siteUrl) {
72587
+ io.error(JSON.stringify({ error: "himi site analyze needs --url", usage: analyzeUsage }));
72588
+ return 2;
72589
+ }
72590
+ const redirectFlag = str6(flags["redirect-uri"]);
72591
+ if (!clientId && (flags.apply === true || redirectFlag)) {
72592
+ const { APPLY_NEEDS_CLIENT: APPLY_NEEDS_CLIENT2, REDIRECT_NEEDS_CLIENT: REDIRECT_NEEDS_CLIENT2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
71759
72593
  io.error(JSON.stringify({
71760
- error: "himi site analyze needs --client-id and --url",
71761
- // Analyze is GET-only and takes the client as INPUT, so a missing one used to
71762
- // be a dead end that named only the dashboard. Name the command that resolves
71763
- // it instead — that is what lets an unattended loop start from a site it did
71764
- // not create.
71765
- ...clientId ? {} : {
71766
- hint: "no --client-id? `himi site client ensure --site <msid> --yes` resolves (or mints) the site's Headless client and prints it; `himi site client list --site <msid>` shows what the site already has. WIX_CLIENT_ID is read as a fallback."
71767
- },
71768
- usage: "himi site analyze --client-id <headless-oauth-client-id> --url https://\u2026 [--site <msid>] [--out .himi/business-book] [--redirect-uri <uri>] [--apply [--shell " + SHELL_ENUM + "]]"
72594
+ error: flags.apply === true ? APPLY_NEEDS_CLIENT2 : REDIRECT_NEEDS_CLIENT2,
72595
+ usage: analyzeUsage
71769
72596
  }));
71770
72597
  return 2;
71771
72598
  }
71772
72599
  try {
71773
72600
  const { runSiteAnalyze: runSiteAnalyze2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
71774
72601
  const r = await runSiteAnalyze2({
71775
- clientId,
72602
+ ...clientId ? { clientId } : {},
72603
+ // The AMBIENT `WIX_SITE_ID` selects owner mode only beside an explicit --client-id,
72604
+ // so exported env vars cannot turn a bare `--url` run into an owner read (#3283).
72605
+ explicitClient: !!clientFlag,
71776
72606
  siteUrl,
71777
72607
  siteFlag: str6(flags.site),
71778
72608
  outDir: str6(flags.out),
@@ -71782,14 +72612,14 @@ ${report2.ok ? "OK \u2014 data + serve plane reachable." : typeof report2.hint =
71782
72612
  // Analyze cannot discover it (GET-only; the registered list lives behind
71783
72613
  // `oauth-app/v1`), and the shell refuses to reuse its own default once a customer
71784
72614
  // client is in play — so it is passed in by whoever registered it.
71785
- redirectUri: str6(flags["redirect-uri"]),
72615
+ redirectUri: redirectFlag,
71786
72616
  cwd: str6(flags.content) ?? process.cwd()
71787
72617
  });
71788
72618
  print({ ok: true, ...r });
71789
72619
  return 0;
71790
72620
  } catch (e) {
71791
72621
  io.error(JSON.stringify({ error: e.message }));
71792
- return 1;
72622
+ return e.exit === 2 ? 2 : 1;
71793
72623
  }
71794
72624
  }
71795
72625
  default:
@@ -71840,6 +72670,7 @@ export {
71840
72670
  buildDoctorReport,
71841
72671
  compareVersions2 as compareVersions,
71842
72672
  createScopedRegistryResolver,
72673
+ createTrimValidator,
71843
72674
  createUpdateRefreshSpawner,
71844
72675
  createUpdateRunner,
71845
72676
  credentialKind,