@wix/himalaya-cli 0.1187.0 → 0.1188.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.
@@ -66,6 +66,16 @@ export interface SiteShellConfig {
66
66
  * site's own cookie endpoint signs the WebView in). Set by the shell from its member binding;
67
67
  * a Base44 site always bridges. */
68
68
  sessionBridge?: boolean;
69
+ /** The site's own Members Area pages a member notification can open — My Orders and My Bookings
70
+ * — as paths relative to {@link origin}. They are never navigation rows (a signed-in utility,
71
+ * not a destination), so they live here rather than in {@link pages}; the site analyzer fills
72
+ * them from the pages it actually found. Absent ⇒ that notification opens the home page. */
73
+ memberPages?: SiteMemberPages;
74
+ }
75
+ /** Members Area pages, by what a notification is about. */
76
+ export interface SiteMemberPages {
77
+ orders?: string;
78
+ bookings?: string;
69
79
  }
70
80
  /** At most four site tabs; the rest belong in the drawer. Four, not the usual phone-bar five,
71
81
  * because every tab keeps its WebView warm (`keepAlive`) and the native keep-alive stores
@@ -13373,6 +13373,10 @@ function assertSiteShellConfig(site) {
13373
13373
  seen.add(page.id);
13374
13374
  sitePageUrl(site.origin, page.path);
13375
13375
  }
13376
+ for (const [key, path] of Object.entries(site.memberPages ?? {})) {
13377
+ if (typeof path !== "string" || !path) throw new Error(`site memberPages.${key} must be a path`);
13378
+ sitePageUrl(site.origin, path);
13379
+ }
13376
13380
  if (site.layout !== void 0 && !SITE_LAYOUTS.includes(site.layout)) {
13377
13381
  throw new Error(`site layout ${JSON.stringify(site.layout)} must be one of ${SITE_LAYOUTS.join(", ")}`);
13378
13382
  }
package/dist/cli.mjs CHANGED
@@ -17406,12 +17406,20 @@ function needsNativeBuild(id) {
17406
17406
  }
17407
17407
  function capabilitiesUsedBy(bundleJs) {
17408
17408
  const out2 = /* @__PURE__ */ new Set();
17409
- for (const m of bundleJs.matchAll(/["'`]capability\.([a-z0-9_]+)(\.[A-Za-z0-9]+)?["'`]/g)) {
17410
- if (m[2] || isKnownCapability(m[1])) out2.add(m[1]);
17409
+ for (const m of capabilityLiterals(bundleJs)) {
17410
+ if (m.verb || isKnownCapability(m.id)) out2.add(m.id);
17411
17411
  }
17412
17412
  for (const [id, marker] of BRIDGE_MARKERS) if (marker.test(bundleJs)) out2.add(id);
17413
17413
  return [...out2].sort();
17414
17414
  }
17415
+ function capabilityDispatchesIn(bundleJs) {
17416
+ const out2 = /* @__PURE__ */ new Set();
17417
+ for (const m of capabilityLiterals(bundleJs)) if (m.verb) out2.add(`capability.${m.id}.${m.verb}`);
17418
+ return [...out2].sort();
17419
+ }
17420
+ function capabilityLiterals(bundleJs) {
17421
+ return [...bundleJs.matchAll(CAPABILITY_LITERAL)].map((m) => ({ id: m[1], ...m[2] ? { verb: m[2] } : {} }));
17422
+ }
17415
17423
  function distAddRefusal(rawId) {
17416
17424
  const id = catalogKey(rawId);
17417
17425
  if (!isKnownCapability(id)) return "is not a capability id the platform knows";
@@ -17438,7 +17446,7 @@ function usageStringCopyFor(permissionCopy) {
17438
17446
  }
17439
17447
  return out2;
17440
17448
  }
17441
- var BRIDGE_CAPABILITY_IDS, KNOWN_CAPABILITY_IDS, BRIDGE_MARKERS, NOT_DIST_ADDABLE;
17449
+ var BRIDGE_CAPABILITY_IDS, KNOWN_CAPABILITY_IDS, CAPABILITY_LITERAL, BRIDGE_MARKERS, NOT_DIST_ADDABLE;
17442
17450
  var init_known = __esm({
17443
17451
  "../../core/sdk/capability-catalog/src/known.ts"() {
17444
17452
  init_builtins();
@@ -17456,6 +17464,7 @@ var init_known = __esm({
17456
17464
  ])
17457
17465
  ].sort()
17458
17466
  );
17467
+ CAPABILITY_LITERAL = /["'`]capability\.([a-z0-9_]+)(?:\.([A-Za-z0-9_]+))?["'`]/g;
17459
17468
  BRIDGE_MARKERS = [
17460
17469
  ["wix_api", /himalaya\.wixApi\b|\bwixApi\.call\b/],
17461
17470
  ["webview", /himalaya\.webview\b/]
@@ -22425,8 +22434,8 @@ var init_shell_capabilities_generated = __esm({
22425
22434
  appDir: "apps/himi-player",
22426
22435
  shared: true,
22427
22436
  extensible: false,
22428
- built: ["audio_record_background", "calls", "clipboard", "speech"],
22429
- serves: ["app_info", "audio_record", "audio_record_background", "browser_auth", "calls", "clipboard", "file_picker", "files", "himi_apps", "iap", "keyboard", "local_database", "on_device_ai", "open_url", "pdf", "presigned_upload", "release_control", "secure_storage", "share", "speaker_diarization", "speech", "stream", "wix_api", "wix_cashier", "wix_platform"]
22437
+ built: ["audio_record_background", "biometrics", "calendar", "calls", "camera", "clipboard", "contacts", "haptic", "location", "media_playback", "motion", "photos", "speech"],
22438
+ serves: ["app_info", "audio_record", "audio_record_background", "biometrics", "browser_auth", "calendar", "calls", "camera", "clipboard", "contacts", "file_picker", "files", "haptic", "himi_apps", "iap", "keyboard", "local_database", "location", "media_playback", "motion", "on_device_ai", "open_url", "pdf", "photos", "presigned_upload", "push", "release_control", "secure_storage", "share", "speaker_diarization", "speech", "stream", "wix_api", "wix_cashier", "wix_platform"]
22430
22439
  }
22431
22440
  };
22432
22441
  }
@@ -22436,6 +22445,7 @@ var init_shell_capabilities_generated = __esm({
22436
22445
  var capabilities_exports = {};
22437
22446
  __export(capabilities_exports, {
22438
22447
  DECLARE_HINT: () => DECLARE_HINT,
22448
+ LAST_DIST_FILE: () => LAST_DIST_FILE,
22439
22449
  SHELLS: () => SHELLS,
22440
22450
  allCapabilities: () => allCapabilities,
22441
22451
  capabilitiesOfPackage: () => capabilitiesOfPackage,
@@ -22447,11 +22457,15 @@ __export(capabilities_exports, {
22447
22457
  formatCapabilityList: () => formatCapabilityList,
22448
22458
  formatDistPlan: () => formatDistPlan,
22449
22459
  isShell: () => isShell,
22460
+ lastDistFromJob: () => lastDistFromJob,
22461
+ needsBinaryNotice: () => needsBinaryNotice,
22462
+ readLastDist: () => readLastDist,
22450
22463
  shellDistAdds: () => shellDistAdds,
22451
22464
  shellServes: () => shellServes,
22452
- shellSupport: () => shellSupport
22465
+ shellSupport: () => shellSupport,
22466
+ writeLastDist: () => writeLastDist
22453
22467
  });
22454
- import { existsSync as existsSync33, readFileSync as readFileSync33 } from "node:fs";
22468
+ import { existsSync as existsSync33, lstatSync as lstatSync9, readFileSync as readFileSync33 } from "node:fs";
22455
22469
  import { join as join35 } from "node:path";
22456
22470
  function isShell(name) {
22457
22471
  return Object.hasOwn(SHELL_CAPABILITIES, name);
@@ -22515,6 +22529,47 @@ function contentShell(dir, flag) {
22515
22529
  return null;
22516
22530
  }
22517
22531
  }
22532
+ async function writeLastDist(dir, rec) {
22533
+ const { realDirIn: realDirIn2, writeFileAtomic: writeFileAtomic2 } = await Promise.resolve().then(() => (init_progress(), progress_exports));
22534
+ const himi = realDirIn2(dir, ".himi");
22535
+ if (!himi) return null;
22536
+ const path2 = join35(himi, "last-dist.json");
22537
+ writeFileAtomic2(path2, `${JSON.stringify(rec, null, 2)}
22538
+ `);
22539
+ return path2;
22540
+ }
22541
+ function readLastDist(dir) {
22542
+ const path2 = join35(dir, LAST_DIST_FILE);
22543
+ try {
22544
+ if (lstatSync9(join35(dir, ".himi")).isSymbolicLink() || lstatSync9(path2).isSymbolicLink()) return null;
22545
+ } catch {
22546
+ return null;
22547
+ }
22548
+ try {
22549
+ const v = JSON.parse(readFileSync33(path2, "utf8"));
22550
+ if (typeof v.app !== "string" || typeof v.jobId !== "string" || typeof v.shell !== "string" || !Array.isArray(v.capabilities)) return null;
22551
+ return { app: v.app, jobId: v.jobId, shell: v.shell, capabilities: v.capabilities.filter((x) => typeof x === "string") };
22552
+ } catch {
22553
+ return null;
22554
+ }
22555
+ }
22556
+ function lastDistFromJob(job, now = /* @__PURE__ */ new Date()) {
22557
+ if (job.status !== "SUCCESS" || !Array.isArray(job.buildCapabilities)) return null;
22558
+ if (typeof job.app !== "string" || typeof job.jobId !== "string" || typeof job.shell !== "string") return null;
22559
+ const capabilities = [...new Set(job.buildCapabilities.filter((x) => typeof x === "string").map(catalogKey))].sort();
22560
+ return { app: job.app, jobId: job.jobId, shell: job.shell, capabilities, finishedAt: now.toISOString() };
22561
+ }
22562
+ function needsBinaryNotice(json6) {
22563
+ const nb = json6?.needsBinary;
22564
+ if (!nb || !Array.isArray(nb.missing)) return null;
22565
+ const missing = nb.missing.filter((x) => typeof x === "string");
22566
+ if (!missing.length) return null;
22567
+ return {
22568
+ missing,
22569
+ ...typeof nb.lastDistJobId === "string" ? { lastDistJobId: nb.lastDistJobId } : {},
22570
+ message: `needs a new binary: ${missing.join(", ")} \u2014 run himi dist`
22571
+ };
22572
+ }
22518
22573
  function noImportLabel(id) {
22519
22574
  return BRIDGE_CAPABILITY_IDS.includes(id) || id === "wix_platform" ? "a bridge capability (no @himalaya import of its own)" : "declare-only (no @himalaya import of its own)";
22520
22575
  }
@@ -22583,7 +22638,7 @@ function didYouMean(id) {
22583
22638
  const best = KNOWN_CAPABILITY_IDS.map((k) => [k, editDistance2(id, k)]).sort((x, y) => x[1] - y[1])[0];
22584
22639
  return best && best[1] <= 2 ? ` Did you mean "${best[0]}"?` : "";
22585
22640
  }
22586
- function capabilityIssues(usage, shell, strict) {
22641
+ function capabilityIssues(usage, shell, strict, lastDist) {
22587
22642
  if (!usage) return [];
22588
22643
  const warn = strict ? "error" : "warning";
22589
22644
  const issues = [];
@@ -22662,7 +22717,19 @@ function capabilityIssues(usage, shell, strict) {
22662
22717
  for (const sc of usage.screens) for (const id of [...sc.declared, ...sc.bundle ? usage.bundles[sc.bundle] ?? [] : []]) required3.add(catalogKey(id));
22663
22718
  const unserved = [...required3].filter((id) => isKnownCapability(id) && !shellServes(shell, id)).sort();
22664
22719
  const addable = s.extensible ? unserved.filter((id) => shellDistAdds(shell, id)) : [];
22665
- if (addable.length) {
22720
+ const installed = lastDist && lastDist.shell === shell ? expandCapabilities(lastDist.capabilities.map(catalogKey)) : null;
22721
+ if (installed) {
22722
+ const stale = [...required3].filter((id) => isKnownCapability(id) && !installed.has(id) && (shellServes(shell, id) || s.extensible && shellDistAdds(shell, id))).sort();
22723
+ if (stale.length) {
22724
+ issues.push({
22725
+ screen: null,
22726
+ pass: "capabilities",
22727
+ severity: "warning",
22728
+ capability: stale.join(","),
22729
+ message: `needs a new binary: ${stale.join(", ")} \u2014 your last \`himi dist\` (${lastDist.jobId}) was not built with ${stale.length === 1 ? "it" : "them"}, so that install cannot call ${stale.length === 1 ? "it" : "them"} until you run \`himi dist\` again. Preview with \`himi dist --plan\`.`
22730
+ });
22731
+ }
22732
+ } else if (addable.length) {
22666
22733
  issues.push({
22667
22734
  screen: null,
22668
22735
  pass: "capabilities",
@@ -22722,7 +22789,7 @@ function capabilityConfigFindings(sourceDir, builtDir) {
22722
22789
  }
22723
22790
  return out2;
22724
22791
  }
22725
- var DECLARE_HINT, REGISTRY, PACKAGES, SHELLS, servedSets, MAX_SUGGEST_LEN2, clip;
22792
+ var DECLARE_HINT, REGISTRY, PACKAGES, SHELLS, servedSets, LAST_DIST_FILE, MAX_SUGGEST_LEN2, clip;
22726
22793
  var init_capabilities = __esm({
22727
22794
  "src/capabilities.ts"() {
22728
22795
  init_src2();
@@ -22734,6 +22801,7 @@ var init_capabilities = __esm({
22734
22801
  PACKAGES = CAPABILITY_PACKAGES;
22735
22802
  SHELLS = Object.keys(SHELL_CAPABILITIES).sort();
22736
22803
  servedSets = /* @__PURE__ */ new Map();
22804
+ LAST_DIST_FILE = join35(".himi", "last-dist.json");
22737
22805
  MAX_SUGGEST_LEN2 = 64;
22738
22806
  clip = (id) => id.length > MAX_SUGGEST_LEN2 ? `${id.slice(0, MAX_SUGGEST_LEN2)}\u2026` : id;
22739
22807
  }
@@ -22828,7 +22896,7 @@ var init_provisioning_facts = __esm({
22828
22896
 
22829
22897
  // ../../core/dev-server/src/app-assets.ts
22830
22898
  import { createHash as createHash10 } from "node:crypto";
22831
- import { existsSync as existsSync34, lstatSync as lstatSync9, readFileSync as readFileSync35, readdirSync as readdirSync19, realpathSync as realpathSync5, statSync as statSync8 } from "node:fs";
22899
+ import { existsSync as existsSync34, lstatSync as lstatSync10, readFileSync as readFileSync35, readdirSync as readdirSync19, realpathSync as realpathSync5, statSync as statSync8 } from "node:fs";
22832
22900
  import { join as join36, resolve as resolve24, sep as sep8 } from "node:path";
22833
22901
  function contentTypeFor(name) {
22834
22902
  const ext = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1).toLowerCase() : "";
@@ -22906,7 +22974,7 @@ function collectBrandFonts(appDir3) {
22906
22974
  if (rel.startsWith("/") || rel.split("/").includes("..")) continue;
22907
22975
  const path2 = join36(appDir3, rel);
22908
22976
  try {
22909
- if (!lstatSync9(path2).isFile()) continue;
22977
+ if (!lstatSync10(path2).isFile()) continue;
22910
22978
  const root = realpathSync5(appDir3);
22911
22979
  if (!realpathSync5(path2).startsWith(root + sep8)) continue;
22912
22980
  } catch {
@@ -28204,6 +28272,10 @@ function assertSiteShellConfig(site) {
28204
28272
  seen.add(page.id);
28205
28273
  sitePageUrl(site.origin, page.path);
28206
28274
  }
28275
+ for (const [key2, path2] of Object.entries(site.memberPages ?? {})) {
28276
+ if (typeof path2 !== "string" || !path2) throw new Error(`site memberPages.${key2} must be a path`);
28277
+ sitePageUrl(site.origin, path2);
28278
+ }
28207
28279
  if (site.layout !== void 0 && !SITE_LAYOUTS.includes(site.layout)) {
28208
28280
  throw new Error(`site layout ${JSON.stringify(site.layout)} must be one of ${SITE_LAYOUTS.join(", ")}`);
28209
28281
  }
@@ -44696,17 +44768,21 @@ var init_net_scenario = __esm({
44696
44768
  });
44697
44769
 
44698
44770
  // ../../core/dev-server/src/capability-fixtures.ts
44699
- function capabilityIdsIn(bundleJs) {
44700
- const out2 = /* @__PURE__ */ new Set();
44701
- for (const m of bundleJs.matchAll(/["'`]capability\.([a-z_0-9]+)\.([A-Za-z]+)["'`]/g)) {
44702
- if (STORE_BACKED.has(m[1])) continue;
44703
- out2.add(`capability.${m[1]}.${m[2]}`);
44771
+ function capabilityCoverageFor(bundleJs, granted) {
44772
+ const out2 = {};
44773
+ for (const action of capabilityDispatchesIn(bundleJs)) {
44774
+ const id = action.split(".")[1];
44775
+ out2[action] = !granted(id) ? "denied" : STORE_BACKED.has(id) ? "store" : CAPABILITY_SUCCESS[action] ? "success" : "unavailable-only";
44704
44776
  }
44705
- return [...out2].sort();
44777
+ return out2;
44778
+ }
44779
+ function capabilityIdsIn(bundleJs) {
44780
+ return capabilityDispatchesIn(bundleJs).filter((action) => !STORE_BACKED.has(action.split(".")[1]));
44706
44781
  }
44707
44782
  var decodeItem, MEDIA_EXT, MEDIA_MIME, FILE, CALL, MEDIA_STATUS, CAPABILITY_SUCCESS, STORE_BACKED;
44708
44783
  var init_capability_fixtures = __esm({
44709
44784
  "../../core/dev-server/src/capability-fixtures.ts"() {
44785
+ init_known();
44710
44786
  decodeItem = (raw) => {
44711
44787
  if (typeof raw !== "string") return {};
44712
44788
  try {
@@ -44889,7 +44965,34 @@ var init_capability_fixtures = __esm({
44889
44965
  const name = path2.split("/").pop() || path2;
44890
44966
  return { uri: `file:///${path2.replace(/^\/+/, "")}`, name, mimeType: typeof p.mimeType === "string" ? p.mimeType : "application/octet-stream", sizeBytes: 1024, modifiedAt: ctx.clock };
44891
44967
  },
44892
- "capability.files.remove": () => ({ removed: true })
44968
+ "capability.files.remove": () => ({ removed: true }),
44969
+ // --- camera -----------------------------------------------------------------------------------
44970
+ // `capturePhoto` THROWS on an envelope with no `uri` ("native handler returned no uri") and maps
44971
+ // `cancelled` to null, so the answer is a bare CapturedAsset — not wrapped, not cancelled.
44972
+ "capability.camera.capture": () => ({
44973
+ uri: "file:///crawl/photo-1.jpg",
44974
+ mime: "image/jpeg",
44975
+ width: 1024,
44976
+ height: 768,
44977
+ byteSize: 204800
44978
+ }),
44979
+ "capability.camera.pickDocument": () => ({
44980
+ uri: "file:///crawl/document-1.pdf",
44981
+ name: "document-1.pdf",
44982
+ mime: "application/pdf",
44983
+ byteSize: 40960
44984
+ }),
44985
+ "capability.camera.scanQR": () => ({ code: "crawl-qr-code" }),
44986
+ // --- location ---------------------------------------------------------------------------------
44987
+ // `getCurrent` reads `r.position` and returns null without it — the very "no fix" arm the main
44988
+ // axis already covers. The watch verbs are deliberately absent: `watchNext` is a long poll the
44989
+ // binding loops on until `stopped`, and a stateless fixture that always answered a position would
44990
+ // spin a `for await` for the crawl's whole budget.
44991
+ "capability.location.getPermissionStatus": () => ({ status: "granted" }),
44992
+ "capability.location.requestPermission": () => ({ status: "granted" }),
44993
+ "capability.location.getCurrent": (_p, ctx) => ({
44994
+ position: { lat: 32.0853, lng: 34.7818, accuracy: 10, timestamp: ctx.clock }
44995
+ })
44893
44996
  };
44894
44997
  STORE_BACKED = /* @__PURE__ */ new Set(["local_database", "secure_storage"]);
44895
44998
  }
@@ -45642,7 +45745,7 @@ function declaredCapabilitiesOf(descriptor) {
45642
45745
  const list = Array.isArray(raw) ? raw : [];
45643
45746
  return new Set(list.filter((c) => typeof c === "string"));
45644
45747
  }
45645
- function hostDispatch(localDbSchema, declaredCapabilities = /* @__PURE__ */ new Set(), capabilitySuccess = false, clock = 0) {
45748
+ function hostDispatch(localDbSchema, declaredCapabilities = /* @__PURE__ */ new Set(), capabilitySuccess = false, clock = 0, onDenied) {
45646
45749
  const localDb = /* @__PURE__ */ new Map();
45647
45750
  const writeQueue = [];
45648
45751
  const secureKv = /* @__PURE__ */ new Map();
@@ -45653,6 +45756,7 @@ function hostDispatch(localDbSchema, declaredCapabilities = /* @__PURE__ */ new
45653
45756
  const cap = CAPABILITY_ACTION.exec(action);
45654
45757
  if (cap) {
45655
45758
  if (!grants(declaredCapabilities, cap[1])) {
45759
+ onDenied?.(cap[1], action);
45656
45760
  throw new Error(
45657
45761
  `${cap[1]}: capability not declared \u2014 add "${cap[1]}" to this screen's logic_bundle.declaredCapabilities. An undeclared capability call is blocked on device.`
45658
45762
  );
@@ -46035,7 +46139,16 @@ async function bootScreenWorker(opts) {
46035
46139
  };
46036
46140
  const appear = opts.skipAppear ? null : rootOnAppear(opts.descriptor);
46037
46141
  const hostCalls = [];
46038
- const host = hostDispatch(opts.localDbSchema, declaredCapabilitiesOf(opts.descriptor), opts.capabilitySuccess === true, opts.clock ?? CRAWL_CLOCK);
46142
+ const deniedCapabilities = [];
46143
+ const host = hostDispatch(
46144
+ opts.localDbSchema,
46145
+ declaredCapabilitiesOf(opts.descriptor),
46146
+ opts.capabilitySuccess === true,
46147
+ opts.clock ?? CRAWL_CLOCK,
46148
+ (_id, action) => {
46149
+ deniedCapabilities.push(action);
46150
+ }
46151
+ );
46039
46152
  const w = await bootBundleWorker(opts.bundleJs, {
46040
46153
  wixCall: opts.wixCall,
46041
46154
  input,
@@ -46070,7 +46183,7 @@ async function bootScreenWorker(opts) {
46070
46183
  }
46071
46184
  for (let i = 0; i < 20; i++) await yieldMacrotask();
46072
46185
  const { state, defects } = snapshot(w);
46073
- return { ...w, hostCalls, state: Object.keys(state).length ? state : w.services, defects, ...deadLifecycle ? { deadLifecycle } : {} };
46186
+ return { ...w, hostCalls, deniedCapabilities, state: Object.keys(state).length ? state : w.services, defects, ...deadLifecycle ? { deadLifecycle } : {} };
46074
46187
  }
46075
46188
  function effectSince(w, mark, action, componentId) {
46076
46189
  const wix = w.calls.slice(mark.wix);
@@ -46131,12 +46244,14 @@ async function withSwallowedRejections(fn) {
46131
46244
  async function crawlScreenCapabilitySuccess(opts) {
46132
46245
  return withSwallowedRejections(async () => {
46133
46246
  const declared = declaredCapabilitiesOf(opts.descriptor);
46247
+ const coverage = opts.bundleJs ? capabilityCoverageFor(opts.bundleJs, (id) => grants(declared, id)) : {};
46134
46248
  const modelled = opts.bundleJs ? capabilityIdsIn(opts.bundleJs).filter((id) => CAPABILITY_SUCCESS[id] && grants(declared, id.split(".")[1])) : [];
46135
- if (!modelled.length) return { findings: [], modelled: 0 };
46249
+ if (!modelled.length) return { findings: [], modelled: 0, coverage };
46136
46250
  const r = await crawlScreenInner({ ...opts, capabilitySuccess: true });
46137
46251
  return {
46138
46252
  findings: r.findings.map((f) => ({ ...f, axis: f.axis ? `capability:success+${f.axis}` : "capability:success" })),
46139
46253
  modelled: modelled.length,
46254
+ coverage,
46140
46255
  // The success arm is where a paid flow goes on past `isAvailable` to `proceedToPayment`, so
46141
46256
  // its effects are the write inventory's too — unlike its counts, they cannot double anything.
46142
46257
  ...r.effects?.length ? { effects: r.effects } : {}
@@ -46530,11 +46645,29 @@ async function crawlScreenInner(opts) {
46530
46645
  hint: `${d.reason === "no-service" ? "services defined" : "actions declared"}: ${d.declared.join(", ") || "none"}. The host fires onAppear on device, so this screen stays on its initial state forever.`
46531
46646
  });
46532
46647
  };
46648
+ const deniedReported = /* @__PURE__ */ new Set();
46649
+ const reportDenied = (actions, trigger) => {
46650
+ for (const action of actions) {
46651
+ const id = CAPABILITY_ACTION.exec(action)?.[1] ?? action;
46652
+ if (deniedReported.has(id)) continue;
46653
+ deniedReported.add(id);
46654
+ findings.push({
46655
+ screen: screenId,
46656
+ trigger,
46657
+ invariant: "capability-undeclared",
46658
+ severity: "error",
46659
+ actual: action,
46660
+ message: `${action} was denied: "${id}" is not in this screen's logicBundle.declaredCapabilities \u2014 the call is blocked on device`,
46661
+ hint: `add "${id}" to this screen's logicBundle.declaredCapabilities (the runtime gate) and to config.ts capabilities (what the binary is built with). If this screen should never make the call, it is reaching a shared worker action meant for another screen. A try/catch around the call hides this on device: the control just does nothing.`
46662
+ });
46663
+ }
46664
+ };
46533
46665
  const first = await boot();
46534
46666
  if (first) {
46535
46667
  try {
46536
46668
  if (first.deadLifecycle) reportDeadLifecycle(first.deadLifecycle);
46537
46669
  check("boot", first.state, first.errors, first.defects);
46670
+ reportDenied(first.deniedCapabilities, "boot");
46538
46671
  observations = collectObservations(descriptor, first.state);
46539
46672
  seedWords = seedTokens(first.state);
46540
46673
  harvestStaticEdges(first.state);
@@ -46628,6 +46761,7 @@ async function crawlScreenInner(opts) {
46628
46761
  const before = JSON.stringify(preState);
46629
46762
  const callsBefore = w.calls.length;
46630
46763
  const hostBefore = w.hostCalls.length;
46764
+ const deniedBefore = w.deniedCapabilities.length;
46631
46765
  const errors = [];
46632
46766
  try {
46633
46767
  await handle.dispatch(parsed.action, ...workerArgs(dispatched));
@@ -46637,6 +46771,7 @@ async function crawlScreenInner(opts) {
46637
46771
  exercised += 1;
46638
46772
  const { state, defects } = snapshot(w);
46639
46773
  check(trigger, state, errors, defects);
46774
+ reportDenied(w.deniedCapabilities.slice(deniedBefore), trigger);
46640
46775
  const navCountBefore = navEdges.length;
46641
46776
  drainWorkerEdges(w, `worker:${site.actionId}`);
46642
46777
  drainPollDirectives(w, trigger);
@@ -51098,7 +51233,7 @@ __export(config_push_edit_exports, {
51098
51233
  enablePushInConfig: () => enablePushInConfig,
51099
51234
  previewPushConfigEdit: () => previewPushConfigEdit
51100
51235
  });
51101
- import { lstatSync as lstatSync10, readFileSync as readFileSync53 } from "node:fs";
51236
+ import { lstatSync as lstatSync11, readFileSync as readFileSync53 } from "node:fs";
51102
51237
  import { basename as basename13, join as join46 } from "node:path";
51103
51238
  import ts from "typescript";
51104
51239
  function enablePushInConfig(source, options) {
@@ -51328,7 +51463,7 @@ function backUp(dir, fileName, original) {
51328
51463
  }
51329
51464
  function isSymlink2(path2) {
51330
51465
  try {
51331
- return lstatSync10(path2).isSymbolicLink();
51466
+ return lstatSync11(path2).isSymbolicLink();
51332
51467
  } catch {
51333
51468
  return false;
51334
51469
  }
@@ -51991,7 +52126,7 @@ __export(app_source_exports, {
51991
52126
  zipAppSource: () => zipAppSource
51992
52127
  });
51993
52128
  import { createHash as createHash24 } from "node:crypto";
51994
- import { readdirSync as readdirSync27, lstatSync as lstatSync11, readFileSync as readFileSync55, mkdirSync as mkdirSync24, writeFileSync as writeFileSync19, existsSync as existsSync49, rmSync as rmSync11, rmdirSync as rmdirSync2, unlinkSync as unlinkSync3 } from "node:fs";
52129
+ import { readdirSync as readdirSync27, lstatSync as lstatSync12, readFileSync as readFileSync55, mkdirSync as mkdirSync24, writeFileSync as writeFileSync19, existsSync as existsSync49, rmSync as rmSync11, rmdirSync as rmdirSync2, unlinkSync as unlinkSync3 } from "node:fs";
51995
52130
  import { join as join48, resolve as resolve41, sep as sep11 } from "node:path";
51996
52131
  import { createRequire as createRequire3 } from "node:module";
51997
52132
  import { deflateRawSync as deflateRawSync2 } from "node:zlib";
@@ -52001,7 +52136,7 @@ function walkSourceTree(dir, file, skip, enter = () => {
52001
52136
  enter(relDir);
52002
52137
  for (const name of readdirSync27(join48(dir, relDir))) {
52003
52138
  const rel = relDir ? join48(relDir, name) : name;
52004
- const st = lstatSync11(join48(dir, rel));
52139
+ const st = lstatSync12(join48(dir, rel));
52005
52140
  if (st.isSymbolicLink()) {
52006
52141
  skip(rel);
52007
52142
  continue;
@@ -52082,7 +52217,7 @@ function newestSourceChangeMs(dir) {
52082
52217
  dir,
52083
52218
  (archivePath, hostRel) => {
52084
52219
  if (/\.md$/i.test(archivePath) || !counts(hostRel)) return;
52085
- const m = lstatSync11(join48(dir, hostRel)).mtimeMs;
52220
+ const m = lstatSync12(join48(dir, hostRel)).mtimeMs;
52086
52221
  if (m > newest) newest = m;
52087
52222
  },
52088
52223
  () => {
@@ -52094,7 +52229,7 @@ function newestSourceChangeMs(dir) {
52094
52229
  // Nor a root directory the render does not read: creating a file in `_qa/` moves `_qa/`.
52095
52230
  (hostRel) => {
52096
52231
  if (hostRel === "" || !inRead(hostRel)) return;
52097
- const m = lstatSync11(join48(dir, hostRel)).mtimeMs;
52232
+ const m = lstatSync12(join48(dir, hostRel)).mtimeMs;
52098
52233
  if (m > newest) newest = m;
52099
52234
  }
52100
52235
  );
@@ -52142,7 +52277,7 @@ function readAppSource(zip) {
52142
52277
  }
52143
52278
  function pathKind(p) {
52144
52279
  try {
52145
- return lstatSync11(p);
52280
+ return lstatSync12(p);
52146
52281
  } catch {
52147
52282
  return null;
52148
52283
  }
@@ -52239,7 +52374,7 @@ function pruneEmptyDirs(root, rel, needed, removed) {
52239
52374
  for (const name of readdirSync27(rel ? join48(root, rel) : root)) {
52240
52375
  const childRel = rel ? join48(rel, name) : name;
52241
52376
  const posix5 = childRel.split(sep11).join("/");
52242
- const kind = lstatSync11(join48(root, childRel));
52377
+ const kind = lstatSync12(join48(root, childRel));
52243
52378
  if (kind.isSymbolicLink() || !kind.isDirectory() || isDeniedSourceDir(name, rel)) {
52244
52379
  empty = false;
52245
52380
  continue;
@@ -52319,7 +52454,7 @@ __export(run_watch_exports, {
52319
52454
  watchedContentDir: () => watchedContentDir,
52320
52455
  writeRunRecord: () => writeRunRecord
52321
52456
  });
52322
- import { closeSync as closeSync3, constants, existsSync as existsSync50, fstatSync, lstatSync as lstatSync12, statSync as statSync13, mkdirSync as mkdirSync25, openSync as openSync3, readFileSync as readFileSync56, realpathSync as realpathSync7, renameSync as renameSync7, rmSync as rmSync12, writeFileSync as writeFileSync20, watch as fsWatch } from "node:fs";
52457
+ import { closeSync as closeSync3, constants, existsSync as existsSync50, fstatSync, lstatSync as lstatSync13, statSync as statSync13, mkdirSync as mkdirSync25, openSync as openSync3, readFileSync as readFileSync56, realpathSync as realpathSync7, renameSync as renameSync7, rmSync as rmSync12, writeFileSync as writeFileSync20, watch as fsWatch } from "node:fs";
52323
52458
  import { randomUUID as randomUUID3 } from "node:crypto";
52324
52459
  import { extname as extname4, join as join49, resolve as resolve42, sep as sep12 } from "node:path";
52325
52460
  function diffReleases(prev, next) {
@@ -52442,11 +52577,11 @@ function serialize(task) {
52442
52577
  function himiDir(dir) {
52443
52578
  const root = join49(dir, ".himi");
52444
52579
  try {
52445
- return lstatSync12(root).isDirectory() ? root : null;
52580
+ return lstatSync13(root).isDirectory() ? root : null;
52446
52581
  } catch (e) {
52447
52582
  if (e.code !== "ENOENT") return null;
52448
52583
  mkdirSync25(root, { recursive: true });
52449
- return lstatSync12(root).isDirectory() ? root : null;
52584
+ return lstatSync13(root).isDirectory() ? root : null;
52450
52585
  }
52451
52586
  }
52452
52587
  function writeRunRecord(dir, record2) {
@@ -52466,7 +52601,7 @@ function writeRunRecord(dir, record2) {
52466
52601
  function removeRunRecord(dir, owner) {
52467
52602
  const root = join49(dir, ".himi");
52468
52603
  try {
52469
- if (!lstatSync12(root).isDirectory()) return;
52604
+ if (!lstatSync13(root).isDirectory()) return;
52470
52605
  } catch {
52471
52606
  return;
52472
52607
  }
@@ -52484,7 +52619,7 @@ function removeRunRecord(dir, owner) {
52484
52619
  function removeInitWatchRecord(dir, owner) {
52485
52620
  const root = join49(dir, ".himi");
52486
52621
  try {
52487
- if (!lstatSync12(root).isDirectory()) return;
52622
+ if (!lstatSync13(root).isDirectory()) return;
52488
52623
  } catch {
52489
52624
  return;
52490
52625
  }
@@ -52521,12 +52656,12 @@ function boardFile(dir, pathname) {
52521
52656
  }
52522
52657
  function readConfined(root, rel) {
52523
52658
  try {
52524
- if (!lstatSync12(root).isDirectory()) return null;
52659
+ if (!lstatSync13(root).isDirectory()) return null;
52525
52660
  const parts = rel.split(sep12);
52526
52661
  let at2 = root;
52527
52662
  for (let i = 0; i < parts.length; i++) {
52528
52663
  at2 = join49(at2, parts[i]);
52529
- const st = lstatSync12(at2);
52664
+ const st = lstatSync13(at2);
52530
52665
  if (st.isSymbolicLink() || !(i === parts.length - 1 ? st.isFile() : st.isDirectory())) return null;
52531
52666
  }
52532
52667
  const canonicalRoot = realpathSync7(root);
@@ -52592,7 +52727,7 @@ __export(screen_state_exports, {
52592
52727
  withHomeCaveat: () => withHomeCaveat
52593
52728
  });
52594
52729
  import { createHash as createHash25 } from "node:crypto";
52595
- import { lstatSync as lstatSync13, readFileSync as readFileSync57 } from "node:fs";
52730
+ import { lstatSync as lstatSync14, readFileSync as readFileSync57 } from "node:fs";
52596
52731
  import { join as join50 } from "node:path";
52597
52732
  function quotableLine(text8) {
52598
52733
  if (!text8) return void 0;
@@ -52651,7 +52786,7 @@ function screenStateOf(services, html, opts = {}) {
52651
52786
  function readRecord(dir) {
52652
52787
  const file = join50(dir, SHOT_STATES_FILE);
52653
52788
  try {
52654
- if (!lstatSync13(file).isFile()) return {};
52789
+ if (!lstatSync14(file).isFile()) return {};
52655
52790
  const r = JSON.parse(readFileSync57(file, "utf8"));
52656
52791
  return r.v === 1 && r.shots && typeof r.shots === "object" ? r.shots : {};
52657
52792
  } catch {
@@ -52727,7 +52862,7 @@ __export(studio_reel_exports, {
52727
52862
  utcDay: () => utcDay
52728
52863
  });
52729
52864
  import { createHash as createHash26 } from "node:crypto";
52730
- import { lstatSync as lstatSync14, readFileSync as readFileSync58 } from "node:fs";
52865
+ import { lstatSync as lstatSync15, readFileSync as readFileSync58 } from "node:fs";
52731
52866
  import { basename as basename14, join as join51 } from "node:path";
52732
52867
  function loadMemory2(dir) {
52733
52868
  const m = readJson5(join51(dir, ".himi", "studio", "memory.json"));
@@ -52812,7 +52947,7 @@ function inlineImages(dir, plan, budget = REEL_BUDGET_BYTES) {
52812
52947
  if (!HISTORY_NAME.test(f)) return null;
52813
52948
  try {
52814
52949
  const path2 = join51(dir, HISTORY_DIR, f);
52815
- const st = lstatSync14(path2);
52950
+ const st = lstatSync15(path2);
52816
52951
  if (!st.isFile() || st.nlink !== 1) return null;
52817
52952
  const bytes2 = readFileSync58(path2);
52818
52953
  return createHash26("sha256").update(bytes2).digest("hex") === f.slice(0, 64) && imageExtension(bytes2) === f.slice(65) ? bytes2 : null;
@@ -53019,7 +53154,7 @@ __export(milestones_exports, {
53019
53154
  });
53020
53155
  import { createHash as createHash27, randomUUID as randomUUID4 } from "node:crypto";
53021
53156
  import { spawn as spawn5 } from "node:child_process";
53022
- import { closeSync as closeSync4, existsSync as existsSync51, lstatSync as lstatSync15, readFileSync as readFileSync59, statSync as statSync14 } from "node:fs";
53157
+ import { closeSync as closeSync4, existsSync as existsSync51, lstatSync as lstatSync16, readFileSync as readFileSync59, statSync as statSync14 } from "node:fs";
53023
53158
  import { createServer as createServer4 } from "node:net";
53024
53159
  import { basename as basename15, dirname as dirname29, isAbsolute as isAbsolute4, join as join52, relative as relative9, resolve as resolve43, sep as sep13 } from "node:path";
53025
53160
  function homeScreenIdOf(dir) {
@@ -53040,7 +53175,7 @@ function homeShotPath(dir, home) {
53040
53175
  if (!shot.startsWith(base + sep13)) return null;
53041
53176
  for (let p = shot; p !== root && p.startsWith(root + sep13); p = dirname29(p)) {
53042
53177
  try {
53043
- if (lstatSync15(p).isSymbolicLink()) return null;
53178
+ if (lstatSync16(p).isSymbolicLink()) return null;
53044
53179
  } catch {
53045
53180
  }
53046
53181
  }
@@ -53375,7 +53510,7 @@ function rememberDraft(dir, releaseId, m) {
53375
53510
  function lastDraft(dir) {
53376
53511
  try {
53377
53512
  const file = join52(dir, LAST_DRAFT_FILE);
53378
- if (!lstatSync15(file).isFile()) return null;
53513
+ if (!lstatSync16(file).isFile()) return null;
53379
53514
  const r = JSON.parse(readFileSync59(file, "utf8"));
53380
53515
  const m = r.manifest;
53381
53516
  if (r.v !== 1 || typeof r.releaseId !== "string" || !m || typeof m.screens !== "object" || !m.screens || typeof m.bundles !== "object" || !m.bundles) return null;
@@ -53437,7 +53572,7 @@ function recordInitSource(dir, opts = {}) {
53437
53572
  function readInitSource(dir) {
53438
53573
  const file = join52(dir, INIT_SOURCE_FILE);
53439
53574
  try {
53440
- if (!lstatSync15(file).isFile()) return null;
53575
+ if (!lstatSync16(file).isFile()) return null;
53441
53576
  const r = JSON.parse(readFileSync59(file, "utf8"));
53442
53577
  if (r.v !== 1 || typeof r.fingerprint !== "string" || !/^[0-9a-f]{64}$/.test(r.fingerprint) || typeof r.newestMs !== "number") return null;
53443
53578
  return {
@@ -53512,7 +53647,7 @@ function contentHomePictures(dir, home) {
53512
53647
  if (r.state.state !== "content") continue;
53513
53648
  for (const file of [shot, hist && join52(hist, `${r.sha256}.png`)]) {
53514
53649
  try {
53515
- if (!file || !lstatSync15(file).isFile() || sha256Of2(file) !== r.sha256) continue;
53650
+ if (!file || !lstatSync16(file).isFile() || sha256Of2(file) !== r.sha256) continue;
53516
53651
  } catch {
53517
53652
  continue;
53518
53653
  }
@@ -53525,7 +53660,7 @@ function contentHomePictures(dir, home) {
53525
53660
  function readInitWatch(dir) {
53526
53661
  const file = join52(dir, INIT_WATCH_FILE);
53527
53662
  try {
53528
- if (!lstatSync15(file).isFile()) return null;
53663
+ if (!lstatSync16(file).isFile()) return null;
53529
53664
  const r = JSON.parse(readFileSync59(file, "utf8"));
53530
53665
  if (r.v !== 1 || typeof r.pid !== "number" || !Number.isInteger(r.pid) || r.pid < 1) return null;
53531
53666
  if (typeof r.url !== "string" || !/^http:\/\/127\.0\.0\.1:\d+\//.test(r.url) || typeof r.board !== "string" || typeof r.log !== "string" || typeof r.startedAt !== "string") return null;
@@ -53558,7 +53693,7 @@ function watchReminder(dir, opts = {}) {
53558
53693
  const file = join52(dir, REMINDERS_FILE);
53559
53694
  let last = Number.NaN;
53560
53695
  try {
53561
- if (lstatSync15(file).isFile()) last = Date.parse(JSON.parse(readFileSync59(file, "utf8")).watch ?? "");
53696
+ if (lstatSync16(file).isFile()) last = Date.parse(JSON.parse(readFileSync59(file, "utf8")).watch ?? "");
53562
53697
  } catch {
53563
53698
  }
53564
53699
  if (Number.isFinite(last) && now - last >= 0 && now - last < WATCH_REMINDER_EVERY_MS) return void 0;
@@ -53681,7 +53816,7 @@ function afterHandoverPostcard(dir, opts = {}) {
53681
53816
  const file = join52(dir, ".himi", "making-of.html");
53682
53817
  let fresh = false;
53683
53818
  try {
53684
- const st = lstatSync15(file);
53819
+ const st = lstatSync16(file);
53685
53820
  fresh = st.isFile() && !st.isSymbolicLink() && st.mtimeMs >= statSync14(join52(dir, ".himi", "progress.jsonl")).mtimeMs;
53686
53821
  } catch {
53687
53822
  fresh = false;
@@ -54811,6 +54946,7 @@ var test_exports = {};
54811
54946
  __export(test_exports, {
54812
54947
  TEST_CONFIG_FILE: () => TEST_CONFIG_FILE,
54813
54948
  defaultNetRules: () => defaultNetRules,
54949
+ foldCapabilityCoverage: () => foldCapabilityCoverage,
54814
54950
  formatReport: () => formatReport,
54815
54951
  makeRecordingWixCall: () => makeRecordingWixCall,
54816
54952
  readCannedResponses: () => readCannedResponses,
@@ -55055,6 +55191,31 @@ async function runShotsPass(opts) {
55055
55191
  }
55056
55192
  return { dir: opts.outDir, written, ...Object.keys(states).length ? { states } : {} };
55057
55193
  }
55194
+ function declaredKey(descriptor) {
55195
+ return [...declaredCapabilitiesOf(descriptor)].sort().join(",");
55196
+ }
55197
+ function foldCapabilityCoverage(perScreen) {
55198
+ const acc = /* @__PURE__ */ new Map();
55199
+ for (const { screen, coverage } of perScreen) {
55200
+ for (const [call, arm] of Object.entries(coverage)) {
55201
+ const e = acc.get(call) ?? { screens: [], deniedOn: [] };
55202
+ if (!e.screens.includes(screen)) e.screens.push(screen);
55203
+ if (arm === "denied") {
55204
+ if (!e.deniedOn.includes(screen)) e.deniedOn.push(screen);
55205
+ } else e.granted = arm;
55206
+ acc.set(call, e);
55207
+ }
55208
+ }
55209
+ const counts = { success: 0, store: 0, "unavailable-only": 0, denied: 0 };
55210
+ const calls = {};
55211
+ for (const call of [...acc.keys()].sort()) {
55212
+ const e = acc.get(call);
55213
+ const arm = e.granted ?? "denied";
55214
+ counts[arm] += 1;
55215
+ calls[call] = { arm, screens: e.screens.sort(), ...e.granted && e.deniedOn.length ? { deniedOn: e.deniedOn.sort() } : {} };
55216
+ }
55217
+ return { counts, calls };
55218
+ }
55058
55219
  function withCovered(rows, coveredBy) {
55059
55220
  const byScreen = new Map(rows.map((r) => [r.screen, r]));
55060
55221
  const out2 = [...rows];
@@ -55079,7 +55240,7 @@ async function runCrawl(opts) {
55079
55240
  if (!opts.allScreens) {
55080
55241
  const groups = /* @__PURE__ */ new Map();
55081
55242
  for (const d of requested) {
55082
- const key2 = `${d.bundleName ?? "-"}|${deps.crawlSignature(d.descriptor)}`;
55243
+ const key2 = `${d.bundleName ?? "-"}|${deps.crawlSignature(d.descriptor)}|${declaredKey(d.descriptor)}`;
55083
55244
  const list = groups.get(key2) ?? [];
55084
55245
  list.push(d);
55085
55246
  groups.set(key2, list);
@@ -55105,6 +55266,7 @@ async function runCrawl(opts) {
55105
55266
  const relationFindings = [];
55106
55267
  let relationsChecked = 0;
55107
55268
  let capabilityModelled = 0;
55269
+ const capabilityCoverageByScreen = [];
55108
55270
  const relationsByKind = { inverse: 0, refresh: 0, loadMore: 0, filter: 0 };
55109
55271
  const deepFindings = [];
55110
55272
  const journeyFindings = [];
@@ -55197,6 +55359,7 @@ async function runCrawl(opts) {
55197
55359
  const cr = await deps.crawlScreenCapabilitySuccess(args);
55198
55360
  writes.push(...writesFromEffects(baked.screenId, cr.effects));
55199
55361
  capabilityModelled += cr.modelled;
55362
+ if (cr.coverage) capabilityCoverageByScreen.push({ screen: baked.screenId, coverage: cr.coverage });
55200
55363
  const keyOfC = deps.findingKey ?? ((f) => `${f.invariant}|${f.at ?? ""}|${f.screen}|${f.message}`);
55201
55364
  const seen = new Set([...r.findings, ...relationFindings].map(keyOfC));
55202
55365
  for (const f of cr.findings) if (!seen.has(keyOfC(f))) relationFindings.push(f);
@@ -55355,7 +55518,7 @@ async function runCrawl(opts) {
55355
55518
  ...retired.length ? { retired } : {},
55356
55519
  ...opts.deep && opts.deep >= 2 ? { deep: { depth: opts.deep, ...deepTotals } } : {},
55357
55520
  ...deps.crawlScreenRelations ? { relations: { checked: relationsChecked, byKind: relationsByKind } } : {},
55358
- ...deps.crawlScreenCapabilitySuccess ? { capabilities: { modelled: capabilityModelled } } : {},
55521
+ ...deps.crawlScreenCapabilitySuccess ? { capabilities: { modelled: capabilityModelled }, capabilityCoverage: foldCapabilityCoverage(capabilityCoverageByScreen) } : {},
55359
55522
  ...journeyResults.length ? { journeys: journeyResults } : {},
55360
55523
  writes: inventory,
55361
55524
  ...unreachedWrites.length ? { unreachedWrites } : {},
@@ -55507,6 +55670,7 @@ var TEST_CONFIG_FILE, mapResult, triggerLabel;
55507
55670
  var init_test = __esm({
55508
55671
  "src/test.ts"() {
55509
55672
  init_app_map();
55673
+ init_screen_crawl();
55510
55674
  init_wix_gateway_rules();
55511
55675
  init_crawl_layers();
55512
55676
  init_screen_state();
@@ -57420,7 +57584,7 @@ var init_cli_bridge = __esm({
57420
57584
  });
57421
57585
 
57422
57586
  // src/mcp/file-tools.ts
57423
- import { closeSync as closeSync5, constants as constants2, lstatSync as lstatSync16, mkdirSync as mkdirSync28, openSync as openSync4, readdirSync as readdirSync28, readFileSync as readFileSync67, realpathSync as realpathSync8, renameSync as renameSync8, rmSync as rmSync13, statSync as statSync15, writeSync, existsSync as existsSync58 } from "node:fs";
57587
+ import { closeSync as closeSync5, constants as constants2, lstatSync as lstatSync17, mkdirSync as mkdirSync28, openSync as openSync4, readdirSync as readdirSync28, readFileSync as readFileSync67, realpathSync as realpathSync8, renameSync as renameSync8, rmSync as rmSync13, statSync as statSync15, writeSync, existsSync as existsSync58 } from "node:fs";
57424
57588
  import { randomBytes as randomBytes4 } from "node:crypto";
57425
57589
  import { dirname as dirname31, isAbsolute as isAbsolute5, join as join60, relative as relative11, resolve as resolve47, sep as sep15 } from "node:path";
57426
57590
  import * as z from "zod";
@@ -57443,7 +57607,7 @@ function symlinkComponent(base, abs) {
57443
57607
  for (const part of relative11(base, abs).split(sep15).filter(Boolean)) {
57444
57608
  cur = join60(cur, part);
57445
57609
  try {
57446
- if (lstatSync16(cur).isSymbolicLink()) return relative11(base, cur);
57610
+ if (lstatSync17(cur).isSymbolicLink()) return relative11(base, cur);
57447
57611
  } catch {
57448
57612
  return void 0;
57449
57613
  }
@@ -57590,7 +57754,7 @@ function fileTools() {
57590
57754
  if (typeof app !== "string") return app;
57591
57755
  const p = confinedPath(app, a.path);
57592
57756
  if (typeof p !== "string") return refuse4("bad_path", p.error);
57593
- if (!existsSync58(p) || !lstatSync16(p).isFile()) return refuse4("not_found", `no file "${String(a.path)}"`);
57757
+ if (!existsSync58(p) || !lstatSync17(p).isFile()) return refuse4("not_found", `no file "${String(a.path)}"`);
57594
57758
  const size = statSync15(p).size;
57595
57759
  if (size > MAX_FILE_BYTES) return refuse4("too_large", `"${String(a.path)}" is ${size} bytes \u2014 over the ${MAX_FILE_BYTES}-byte read limit`);
57596
57760
  return ok({ ok: true, path: a.path, bytes: size, content: readFileSync67(p, "utf8") });
@@ -57617,7 +57781,7 @@ function fileTools() {
57617
57781
  if (typeof p !== "string") refused2.push({ path: f.path, error: p.error });
57618
57782
  else if (typeof f.content !== "string") refused2.push({ path: f.path, error: "content must be text" });
57619
57783
  else if (Buffer.byteLength(f.content) > MAX_FILE_BYTES) refused2.push({ path: f.path, error: `over the ${MAX_FILE_BYTES}-byte limit` });
57620
- else if (existsSync58(p) && !lstatSync16(p).isFile()) refused2.push({ path: f.path, error: "not a regular file" });
57784
+ else if (existsSync58(p) && !lstatSync17(p).isFile()) refused2.push({ path: f.path, error: "not a regular file" });
57621
57785
  else if (planned.some((q) => q.abs === p)) refused2.push({ path: f.path, error: "the same file twice in one call" });
57622
57786
  else planned.push({ abs: p, rel: String(f.path), content: f.content });
57623
57787
  }
@@ -57651,7 +57815,7 @@ function fileTools() {
57651
57815
  const p = confinedPath(app, rel);
57652
57816
  if (typeof p !== "string") refused2.push({ path: rel, error: p.error });
57653
57817
  else if (!existsSync58(p)) refused2.push({ path: rel, error: "no such file" });
57654
- else if (!lstatSync16(p).isFile()) refused2.push({ path: rel, error: "not a regular file" });
57818
+ else if (!lstatSync17(p).isFile()) refused2.push({ path: rel, error: "not a regular file" });
57655
57819
  else if (!planned.includes(p)) {
57656
57820
  planned.push(p);
57657
57821
  deleted.push(rel);
@@ -74566,6 +74730,16 @@ function isSystemPage(path2, title) {
74566
74730
  const last = path2.split("/").filter(Boolean).pop() ?? "";
74567
74731
  return SYSTEM_SEGMENT.test(last) || SYSTEM_TITLE.test(title);
74568
74732
  }
74733
+ function deriveMemberPages(origin, urls) {
74734
+ const found = {};
74735
+ for (const url of urls) {
74736
+ const path2 = pathOnSite(origin, url);
74737
+ if (!path2) continue;
74738
+ const key2 = MEMBER_PAGE_ROUTES[path2.split("/").filter(Boolean).join("/").toLowerCase()];
74739
+ if (key2 && !found[key2]) found[key2] = path2;
74740
+ }
74741
+ return found.orders || found.bookings ? found : void 0;
74742
+ }
74569
74743
  function boundedTitle(s) {
74570
74744
  const t = oneLine2(s).replace(/\s*\(new\)\s*$/i, "");
74571
74745
  return t.length <= SITE_PAGE_TITLE_MAX ? t : t.slice(0, SITE_PAGE_TITLE_MAX).trimEnd();
@@ -74698,10 +74872,15 @@ function deriveSiteShell(input) {
74698
74872
  if (!published.length && !navLinks.length && !routes.length) {
74699
74873
  notes.push("site pages: the analysis read no page list, so the map has only Home \u2014 add the site's pages to `site.pages` before --apply.");
74700
74874
  }
74875
+ const memberPages = deriveMemberPages(origin, navLinks.map((l) => l.href));
74876
+ if (memberPages) {
74877
+ notes.push(`member pages: ${Object.entries(memberPages).map(([k, v]) => `${k} \u2192 ${v}`).join(", ")} \u2014 order / booking notifications open them.`);
74878
+ }
74701
74879
  const site = {
74702
74880
  origin,
74703
74881
  kind,
74704
74882
  pages,
74883
+ ...memberPages ? { memberPages } : {},
74705
74884
  // Hidden by default — the product decision for the site shell (RFC 0062 §6); the owner can
74706
74885
  // turn any of them back on here, and the flags ride the OTA descriptor. A Base44 app has no
74707
74886
  // Wix ads to hide, so it never carries the flag (RFC 0062 §5.8).
@@ -74728,7 +74907,7 @@ function routeTitle(r) {
74728
74907
  const seg = r.path.split("/").filter(Boolean).pop() ?? r.path;
74729
74908
  return seg.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
74730
74909
  }
74731
- var DERIVED_SITE_MAX_PAGES, SITE_PAGE_TITLE_MAX, ROLE_ICON, TAB_ROLES, SYSTEM_SEGMENT, SYSTEM_TITLE, HOME_WORD, oneLine2, RESERVED_PAGE_IDS;
74910
+ var DERIVED_SITE_MAX_PAGES, SITE_PAGE_TITLE_MAX, ROLE_ICON, TAB_ROLES, SYSTEM_SEGMENT, SYSTEM_TITLE, HOME_WORD, MEMBER_PAGE_ROUTES, oneLine2, RESERVED_PAGE_IDS;
74732
74911
  var init_site_shell_derive = __esm({
74733
74912
  "src/site-analyze/site-shell-derive.ts"() {
74734
74913
  init_site_shell();
@@ -74755,6 +74934,10 @@ var init_site_shell_derive = __esm({
74755
74934
  SYSTEM_SEGMENT = /^(popup-.*|lightbox.*|.*thank-?you.*|checkout|cart(-page)?|payment-request(-page)?|expired-reset-link|access-restricted|reservation|(event|experience|reservation|order|item)-details|(product|service|category|group|project|collection|reviews-collection|donation-thank-you)-page|post|project|collection|group|booking-(form|calendar)|plan-customization|notifications?|followers?|following|my-(?!account$)[a-z-]+|profile|search(-results)?|blank(-\d+)?|404)$/i;
74756
74935
  SYSTEM_TITLE = /\b(thank you|\(popup\)|leave (a )?review|notifications|followers|side cart|order error|pending approval|my (rewards|subscriptions|bookings|orders|groups|wallet))\b|\b(group|service|product|project|collection|category|cart|payment request) pages?$|\b(event|experience|reservation|item) details\b/i;
74757
74936
  HOME_WORD = /^(home|home ?page|main|start|accueil|inicio|início|startseite|home ?sweet ?home|בית|דף הבית|ראשי|الرئيسية|ホーム|首页|홈)$/i;
74937
+ MEMBER_PAGE_ROUTES = {
74938
+ "account/my-orders": "orders",
74939
+ "account/my-bookings": "bookings"
74940
+ };
74758
74941
  oneLine2 = (s) => s.replace(/[\u0000-\u001F\u007F\s]+/g, " ").trim();
74759
74942
  RESERVED_PAGE_IDS = /* @__PURE__ */ new Set(["account", "notifications"]);
74760
74943
  }
@@ -76630,7 +76813,7 @@ var init_context = __esm({
76630
76813
  });
76631
76814
 
76632
76815
  // src/cli.ts
76633
- import { mkdtempSync as mkdtempSync5, readFileSync as readFileSync91, mkdirSync as mkdirSync39, writeFileSync as writeFileSync35, existsSync as existsSync74, rmSync as rmSync20, readdirSync as readdirSync32, statSync as statSync20, lstatSync as lstatSync17, accessSync, constants as constants3, renameSync as renameSync9, cpSync as cpSync3 } from "node:fs";
76816
+ import { mkdtempSync as mkdtempSync5, readFileSync as readFileSync91, mkdirSync as mkdirSync39, writeFileSync as writeFileSync35, existsSync as existsSync74, rmSync as rmSync20, readdirSync as readdirSync32, statSync as statSync20, lstatSync as lstatSync18, accessSync, constants as constants3, renameSync as renameSync9, cpSync as cpSync3 } from "node:fs";
76634
76817
  import { execFile as execFile2, execFileSync as execFileSync7, spawnSync as spawnSync5, spawn as spawn7 } from "node:child_process";
76635
76818
  import { AsyncLocalStorage } from "node:async_hooks";
76636
76819
  import { createHash as createHash36, randomUUID as randomUUID8 } from "node:crypto";
@@ -80744,7 +80927,7 @@ function renameRestoredSource(restored, declaredApp, renamed) {
80744
80927
  function symlinkDestinationIssue(dest) {
80745
80928
  let kind;
80746
80929
  try {
80747
- kind = lstatSync17(dest);
80930
+ kind = lstatSync18(dest);
80748
80931
  } catch {
80749
80932
  return null;
80750
80933
  }
@@ -82080,9 +82263,9 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
82080
82263
  message: `scaffold source (--source-dir ${sourceDir}): ${issue2.message}`
82081
82264
  }));
82082
82265
  const { navIssues: navIssues2 } = await Promise.resolve().then(() => (init_nav_lint(), nav_lint_exports));
82083
- const { capabilityIssues: capabilityIssues2, contentShell: contentShell2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
82266
+ const { capabilityIssues: capabilityIssues2, contentShell: contentShell2, readLastDist: readLastDist2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
82084
82267
  const { report: report3, hasErrors } = summarizeLint(app, bres, outcome, strict, [
82085
- ...capabilityIssues2(outcome.capabilityUsage, contentShell2(buildDir, str9(flags.shell)), strict),
82268
+ ...capabilityIssues2(outcome.capabilityUsage, contentShell2(buildDir, str9(flags.shell)), strict, readLastDist2(buildDir)),
82086
82269
  ...await cmsIssues(buildDir, app, strict, { local: true }),
82087
82270
  ...surfaceIssues(buildDir, outcome.surfaceNav, strict),
82088
82271
  ...tokenColorIssues(buildDir),
@@ -82367,9 +82550,9 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
82367
82550
  ...hasWorkers ? { tier5Dir: bundleOutDir2(dir) } : {},
82368
82551
  collect: { lint: true, strict }
82369
82552
  });
82370
- const { capabilityIssues: capabilityIssues2, contentShell: contentShell2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
82553
+ const { capabilityIssues: capabilityIssues2, contentShell: contentShell2, readLastDist: readLastDist2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
82371
82554
  const { report: report3, hasErrors } = summarizeLint(app, bres, outcome, strict, [
82372
- ...capabilityIssues2(outcome.capabilityUsage, contentShell2(dir, str9(flags.shell)), strict),
82555
+ ...capabilityIssues2(outcome.capabilityUsage, contentShell2(dir, str9(flags.shell)), strict, readLastDist2(dir)),
82373
82556
  ...await cmsIssues(dir, app, strict, { local: true }),
82374
82557
  ...surfaceIssues(dir, outcome.surfaceNav, strict),
82375
82558
  ...tokenColorIssues(dir),
@@ -83890,9 +84073,9 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
83890
84073
  collect: { lint: true, strict }
83891
84074
  });
83892
84075
  const { navIssues: navIssues2 } = await Promise.resolve().then(() => (init_nav_lint(), nav_lint_exports));
83893
- const { capabilityIssues: capabilityIssues2, contentShell: contentShell2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
84076
+ const { capabilityIssues: capabilityIssues2, contentShell: contentShell2, readLastDist: readLastDist2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
83894
84077
  const { report: report3, hasErrors } = summarizeLint(declared, bres, outcome, strict, [
83895
- ...capabilityIssues2(outcome.capabilityUsage, contentShell2(dir, str9(flags.shell)), strict),
84078
+ ...capabilityIssues2(outcome.capabilityUsage, contentShell2(dir, str9(flags.shell)), strict, readLastDist2(dir)),
83896
84079
  ...await cmsIssues(dir, declared, strict, { local: true }),
83897
84080
  ...surfaceIssues(dir, outcome.surfaceNav, strict),
83898
84081
  ...tokenColorIssues(dir),
@@ -84217,11 +84400,13 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
84217
84400
  // Its screen hashes say what changed since the last draft, on the postcard.
84218
84401
  manifest
84219
84402
  }) : null;
84403
+ const needsBinary = (await Promise.resolve().then(() => (init_capabilities(), capabilities_exports))).needsBinaryNotice(r.json);
84220
84404
  print({
84221
84405
  ok: true,
84222
84406
  app,
84223
84407
  releaseId: release,
84224
84408
  draft,
84409
+ ...needsBinary ? { needsBinary } : {},
84225
84410
  ...unchanged ? { unchanged: true, unchangedNote: "the serve plane already holds this exact release under this id, so nothing was written" } : {},
84226
84411
  ...afterDraft?.firstLook ? { firstLookPostcard: afterDraft.firstLook } : {},
84227
84412
  ...afterDraft?.postcard ? { postcard: afterDraft.postcard } : {},
@@ -84440,9 +84625,9 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
84440
84625
  collect: { lint: true, strict }
84441
84626
  });
84442
84627
  const { navIssues: navIssues2 } = await Promise.resolve().then(() => (init_nav_lint(), nav_lint_exports));
84443
- const { capabilityIssues: capabilityIssues2, contentShell: contentShell2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
84628
+ const { capabilityIssues: capabilityIssues2, contentShell: contentShell2, readLastDist: readLastDist2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
84444
84629
  const { report: report3, hasErrors } = summarizeLint(app, bres, outcome, strict, [
84445
- ...capabilityIssues2(outcome.capabilityUsage, contentShell2(dir, str9(flags.shell)), strict),
84630
+ ...capabilityIssues2(outcome.capabilityUsage, contentShell2(dir, str9(flags.shell)), strict, readLastDist2(dir)),
84446
84631
  ...await cmsIssues(dir, app, strict, { local: true }),
84447
84632
  ...surfaceIssues(dir, outcome.surfaceNav, strict),
84448
84633
  ...tokenColorIssues(dir),
@@ -86043,6 +86228,20 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
86043
86228
  const sub = pos[1];
86044
86229
  const c = client2();
86045
86230
  const showDistQr = io.isTTY === true && flags.json !== true;
86231
+ const rememberLastDist = async (jobs2) => {
86232
+ try {
86233
+ const dir = contentDir(flags);
86234
+ if (!existsSync74(join75(dir, "himalaya.content.json"))) return;
86235
+ const { lastDistFromJob: lastDistFromJob2, writeLastDist: writeLastDist2 } = await Promise.resolve().then(() => (init_capabilities(), capabilities_exports));
86236
+ const own3 = await appId(dir);
86237
+ for (const job of jobs2) {
86238
+ const rec = lastDistFromJob2(job);
86239
+ if (!rec || rec.app !== own3) continue;
86240
+ await writeLastDist2(dir, rec);
86241
+ }
86242
+ } catch {
86243
+ }
86244
+ };
86046
86245
  const humanDistJob = (job) => {
86047
86246
  if (job.platform !== "ios" || !job.artifact || typeof job.artifact !== "object") return job;
86048
86247
  const { installLink: _hidden, ...safeArtifact } = job.artifact;
@@ -86138,6 +86337,7 @@ ${label2}`);
86138
86337
  return distUnreachable(e);
86139
86338
  }
86140
86339
  const body = { status: r2.status, ...r2.json ?? {} };
86340
+ if (r2.ok) await rememberLastDist([body]);
86141
86341
  print(flags.json === true ? body : humanDistJob(body));
86142
86342
  if (showDistQr && r2.ok) await printDistQrForJob(body, String(body.app ?? ""));
86143
86343
  return r2.ok ? 0 : 1;
@@ -86178,6 +86378,7 @@ ${label2}`);
86178
86378
  }
86179
86379
  if (TERMINAL_DIST_STATUSES.has(status)) {
86180
86380
  const body = { status: s.status, ...rec };
86381
+ await rememberLastDist([body]);
86181
86382
  print(flags.json === true ? body : humanDistJob(body));
86182
86383
  if (showDistQr && status === "SUCCESS") await printDistQrForJob(body, String(rec.app ?? ""));
86183
86384
  return status === "SUCCESS" ? 0 : 1;
@@ -86392,6 +86593,7 @@ ${label2}`);
86392
86593
  }
86393
86594
  }
86394
86595
  }
86596
+ await rememberLastDist(done);
86395
86597
  const allOk = failures.length === 0 && done.every((d) => d.status === "SUCCESS");
86396
86598
  print({ ok: allOk, jobs: flags.json === true ? done : done.map(humanDistJob), ...failures.length ? { failures } : {} });
86397
86599
  await printDistQrsForJobs(done);
@@ -25,7 +25,7 @@ camera — import from @himalaya/camera
25
25
  exports: capturePhoto, pickDocument, scanQR
26
26
  declare: config.ts `capabilities` AND each calling screen's `logicBundle.declaredCapabilities`
27
27
  binary: needs a NATIVE build — iOS NSCameraUsageDescription; Android android.permission.CAMERA …
28
- shells: served by owner-lite; NOT in the branded-lite, brandy, demo-app, player binary
28
+ shells: served by owner-lite, player; `himi dist` adds it to base44, branded-lite, brandy; NOT in the demo-app binary
29
29
  ```
30
30
 
31
31
  Agents: the same data is the `himi_capabilities` MCP tool. `himi sdk <name>` also prints the
@@ -89,8 +89,23 @@ Name the shell your app ships in once, in `himalaya.content.json`, and both `him
89
89
 
90
90
  `himi test` boots every screen's real worker and dispatches its actions. A declared capability
91
91
  answers with its "unavailable" result there, so the crawl checks that your worker handles a
92
- declined camera or a missing position without breaking. In the web preview, camera capture and
93
- geolocation use the browser's own file picker and location prompt.
92
+ declined camera or a missing position without breaking; a second pass then answers the modelled
93
+ ones (camera, location, photos, speech, …) with a SUCCESS result, so the code behind a granted
94
+ permission runs too. Two things in the `--json` report are about capabilities:
95
+
96
+ - a **`capability-undeclared`** finding for every call the screen's gate denied — even one your
97
+ worker caught, which on device looks like a dead button rather than an error;
98
+ - **`capabilityCoverage`**: each `capability.<id>.<verb>` your workers call, and what the crawl
99
+ answers it with — `success` (modelled), `store` (a real session store: `local_database`,
100
+ `secure_storage`), `unavailable-only` (no model yet, so only the declined path runs), or `denied`.
101
+ See [himi-test → Capabilities](himi-test.md#capabilities).
102
+
103
+ The bundle scan only ever **reports**: it never adds a capability to a screen's declaration. The
104
+ per-screen list is the authorization boundary, so a worker shared by two screens cannot widen the
105
+ one that never makes the call.
106
+
107
+ In the web preview, camera capture and geolocation use the browser's own file picker and location
108
+ prompt.
94
109
 
95
110
  ## 5. Ship it
96
111
 
@@ -103,6 +118,14 @@ geolocation use the browser's own file picker and location prompt.
103
118
  entitlements, Android permissions / features) and registers their handlers at boot.
104
119
  `himi capabilities --shell <s>` shows such an id as `yes → dist`, and `himi validate` warns that
105
120
  it "needs a new `himi dist`" — the installed binary cannot call it until you rebuild.
121
+ - **"needs a new binary".** A successful `himi dist` remembers what its binary was built with —
122
+ on the job (`buildCapabilities`) and in your package (`.himi/last-dist.json`). From then on
123
+ `himi push` prints `needs a new binary: <ids> — run himi dist` when the release declares an id
124
+ that build lacks, a following device's draft offer says the same, and `himi validate` compares
125
+ against that build rather than the committed shell.
126
+ - **Never a silent strip.** On iOS, a capability whose entitlement the signing profile cannot grant
127
+ fails the dist job, naming the capability — the binary would otherwise build green and fail on
128
+ the first call. The minted profile's Apple capabilities are derived from the build's own set.
106
129
  - **Preview before you build:** `himi dist --plan` prints what the release declares, what the shell
107
130
  already serves, what the build would add and the exact native delta. No job is started.
108
131
  - **Write the permission prompt.** The OS shows a sentence when the app asks for camera, location,
@@ -125,6 +125,7 @@ Only **unambiguous** defects, so a green run stays worth trusting:
125
125
  | `poll-action-missing` | error | A `_poll` directive every host will silently ignore — an action the bundle never declares, a malformed `action`, a missing `id`, or a non-positive `intervalMs`. Each ends the same way: a screen that never refreshes, with nothing saying why. Nothing in the descriptor names the polled action, so no static check can see it. (A merely too-small interval is legal — the host clamps it.) |
126
126
  | `no-request-issued` | **warning** | A worker that uses the Wix transport but never fetches — "the screen loaded with sample data." A warning: a conditional or tap-triggered fetch is legitimate. |
127
127
  | `native-decode-failed` | error | Under `--native`, the real Swift/Kotlin decoder rejected the baked descriptor bytes (one bad enum blanks the whole screen on device). |
128
+ | `capability-undeclared` | error | A `capability.<id>.<verb>` call the screen's gate denied — reported from the gate itself, so it is raised **even when the worker catches the rejection** (which on device leaves a control that silently does nothing). Fix: declare the id on that screen's `logicBundle.declaredCapabilities` and in `config.ts` `capabilities`. One per capability per screen. See [Capabilities](#capabilities). |
128
129
  | `journey-failed` | error | A `himi-test.json` journey named an action the screen lacks, threw, or never rendered its `expectText`. |
129
130
  | `inverse-pair-diverges` | **warning** | An action and the action that undoes it do not return to the state the boot settled on — `add` then `remove` leaving a different list, or a counter it forgot to put back. |
130
131
  | `refresh-not-idempotent` | **warning** | `refresh` from a fresh boot settles somewhere other than the boot did, on identical data — usually a refresh that appends instead of replacing. |
@@ -267,6 +268,36 @@ went unproven, it names the gap and pairs it with a himi command or a source edi
267
268
  derived from what actually ran, never homework. Assembly:
268
269
  `tools/himi-cli/src/coverage.ts`.
269
270
 
271
+ ## Capabilities
272
+
273
+ A `capability.*` call is answered twice. The main pass answers every granted one with `null` — the
274
+ device's "unavailable" arm — so a screen's declined-camera / no-position path is checked. The
275
+ capability-success pass then re-crawls each screen whose bundle calls a **modelled** capability
276
+ (audio-record, speech, on-device-ai, calls, media-playback, calendar, files, photos, pdf, share,
277
+ open-url, camera, location) with a success answer shaped exactly like the one its SDK binding
278
+ destructures, so the code behind a granted permission runs too. `local_database` and
279
+ `secure_storage` are served from a real session store on both passes.
280
+
281
+ `--json` carries what each call got as `capabilityCoverage`:
282
+
283
+ ```jsonc
284
+ "capabilityCoverage": {
285
+ "counts": { "success": 2, "store": 1, "unavailable-only": 1, "denied": 0 },
286
+ "calls": {
287
+ "capability.camera.capture": { "arm": "success", "screens": ["notes"] },
288
+ "capability.local_database.getAll": { "arm": "store", "screens": ["notes"] },
289
+ "capability.location.getCurrent": { "arm": "success", "screens": ["notes"] },
290
+ "capability.haptic.play": { "arm": "unavailable-only", "screens": ["notes"] }
291
+ }
292
+ }
293
+ ```
294
+
295
+ `unavailable-only` means no model exists yet, so only the declined path of that call has run;
296
+ `denied` means no screen running the call grants its id (each denial is also a
297
+ `capability-undeclared` finding), and `deniedOn` lists screens that deny a call others allow. The
298
+ map is static — it says what a call would be answered with, not that a tap reached it; the gap
299
+ ledger says that.
300
+
270
301
  ## Writes to Wix
271
302
 
272
303
  Every run lists what the app **writes** to the Wix site, per screen and action — the inventory an
@@ -326,7 +326,7 @@ one never blocks a rollout — the dist routes just `501` with the reason.
326
326
  | `HIMI_DIST_ALLOWED_XCODE_VERSIONS` | comma-separated Xcode versions a caller may pin (`xcodeVersion` in the request body); unset ⇒ the built-in list. Nothing upstream constrains this field — the Job Service client validates it as `z.string().max(50)` — so without the allow-list it is caller-supplied text choosing a toolchain on a shared pool. A blank value reads as unset (the built-in list), never as "allow anything". |
327
327
  | `HIMI_DIST_APPLE_SIGNING` | **Minted iOS signing is ON by default** — set `0`/`false`/`off` to disable it and go back to scavenging a profile off the pool. Rarely what you want: minting ships the profile and the certificate with the job, so nothing has to pre-exist on the machine. (Earlier text here cited `0 profile(s) available` on four machines as a reason to keep minting on — that reading came from a TRUNCATED survey and is retracted below; build 992367 logged 352 profiles. The recommendation is unchanged, the evidence for it was wrong.) |
328
328
  | `HIMI_DIST_APPLE_API_KEY_NAME` | Apple account the identity is minted under; unset ⇒ `wix-enterprise` |
329
- | `HIMI_DIST_APPLE_CAPABILITIES` | COMPLETE capability set for a minted id (the sync call replaces rather than merges); unset ⇒ push + associated domains |
329
+ | `HIMI_DIST_APPLE_CAPABILITIES` | Apple capability types to ADD to every minted id. The set itself is derived per job: push + associated domains (the template baseline) plus the Apple capability of every entitlement in the iOS plan of the job's `buildCapabilities` (`appleCapabilitiesForBuild`). This variable only **widens** that derived set (e.g. `APP_GROUPS` for a widget) — it can never remove a derived capability, because the sync call replaces rather than merges and a narrower list would switch off what the content asked for. Unset ⇒ the derived set alone |
330
330
  | `QA_DIST_SLACK_CHANNEL` | Post QA build results to a channel instead of DMing the requester. Worth setting: a DM is unreadable by anything but that person, so a channel is what makes build results scriptable (and visible to the team). |
331
331
 
332
332
  Both per-credential caps are **clamped into the range that can be enforced**, and the two ranges
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1187.0",
3
+ "version": "0.1188.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -67,5 +67,5 @@
67
67
  "artifactId": "himalaya-cli"
68
68
  }
69
69
  },
70
- "falconPackageHash": "ffb3ffbafe40c1d9eed8d08db97af4d46cc1506f60fc49e11cf7df80"
70
+ "falconPackageHash": "67d96922cd1adeed2becc3bef5c4d027397fd441f83346cd018cb252"
71
71
  }