@wix/himalaya-cli 0.1102.0 → 0.1104.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -3979,7 +3979,11 @@ function isBrandable(repoRoot2, name) {
3979
3979
  return false;
3980
3980
  }
3981
3981
  }
3982
- var SHELL_NAMES2, DEFAULT_BRANDABLE_SHELL, HOME_REFRESH_ACTIONS;
3982
+ function isSiteShell(name) {
3983
+ assertKnownShell(name);
3984
+ return SITE_SHELLS.has(name);
3985
+ }
3986
+ var SHELL_NAMES2, DEFAULT_BRANDABLE_SHELL, HOME_REFRESH_ACTIONS, SITE_SHELLS;
3983
3987
  var init_brandable_shells = __esm({
3984
3988
  "src/brandable-shells.ts"() {
3985
3989
  init_dist_jobs();
@@ -3988,6 +3992,7 @@ var init_brandable_shells = __esm({
3988
3992
  HOME_REFRESH_ACTIONS = {
3989
3993
  "branded-lite": "tier5.himiBrandedHome.refresh"
3990
3994
  };
3995
+ SITE_SHELLS = /* @__PURE__ */ new Set(["brandy"]);
3991
3996
  }
3992
3997
  });
3993
3998
 
@@ -24713,6 +24718,7 @@ function shouldRevealError(args) {
24713
24718
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
24714
24719
  var init_validation = __esm({
24715
24720
  "../../core/ts/src/validation.ts"() {
24721
+ "use strict";
24716
24722
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
24717
24723
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
24718
24724
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -29113,6 +29119,7 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
29113
29119
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
29114
29120
  var init_worker_subtree = __esm({
29115
29121
  "../../core/ts/src/worker-subtree.ts"() {
29122
+ "use strict";
29116
29123
  init_worker_subtree_safe_types_generated();
29117
29124
  WORKER_SUBTREE_CEILINGS = Object.freeze({
29118
29125
  maxEncodedBytes: 65536,
@@ -36813,8 +36820,86 @@ var init_app_registry_client = __esm({
36813
36820
  });
36814
36821
 
36815
36822
  // ../../core/ts/src/site-shell.ts
36823
+ function normalizeSiteOrigin(origin) {
36824
+ const raw = (typeof origin === "string" ? origin : "").trim();
36825
+ let url;
36826
+ try {
36827
+ url = new URL(raw);
36828
+ } catch {
36829
+ throw new Error(`site origin ${JSON.stringify(origin)} is not a URL`);
36830
+ }
36831
+ if (url.protocol !== "https:") {
36832
+ throw new Error(`site origin ${JSON.stringify(origin)} must be https`);
36833
+ }
36834
+ if (url.username || url.password) {
36835
+ throw new Error(`site origin ${JSON.stringify(origin)} must not carry credentials`);
36836
+ }
36837
+ if (url.search || url.hash) {
36838
+ throw new Error(`site origin ${JSON.stringify(origin)} must not carry a query or fragment`);
36839
+ }
36840
+ const path2 = url.pathname.replace(/\/+$/, "");
36841
+ return `https://${url.host.toLowerCase()}${path2}`;
36842
+ }
36843
+ function sitePageUrl(origin, path2) {
36844
+ const base = normalizeSiteOrigin(origin);
36845
+ const raw = (typeof path2 === "string" ? path2 : "").trim();
36846
+ if (/^[a-z][a-z0-9+.-]*:/i.test(raw) || raw.startsWith("//")) {
36847
+ throw new Error(`site page path ${JSON.stringify(path2)} must be relative to the site`);
36848
+ }
36849
+ if (raw.includes("\\")) {
36850
+ throw new Error(`site page path ${JSON.stringify(path2)} must not contain a backslash`);
36851
+ }
36852
+ const trimmed = raw.replace(/^\/+/, "");
36853
+ const joined = trimmed ? `${base}/${trimmed}` : base;
36854
+ const resolved2 = new URL(joined);
36855
+ const baseUrl = new URL(base);
36856
+ const basePath = baseUrl.pathname.replace(/\/+$/, "");
36857
+ const inside2 = resolved2.pathname === basePath || resolved2.pathname === `${basePath}/` || resolved2.pathname.startsWith(`${basePath}/`);
36858
+ if (resolved2.host !== baseUrl.host || !inside2) {
36859
+ throw new Error(`site page path ${JSON.stringify(path2)} resolves outside the site`);
36860
+ }
36861
+ return joined;
36862
+ }
36863
+ function siteScreenId(page) {
36864
+ return `${SITE_SCREEN_PREFIX}${page.id}`;
36865
+ }
36866
+ function assertSitePage(page) {
36867
+ if (!page || typeof page.id !== "string" || !PAGE_ID.test(page.id)) {
36868
+ throw new Error(`site page id ${JSON.stringify(page?.id)} must match ${PAGE_ID}`);
36869
+ }
36870
+ if (siteScreenId(page) === SITE_PAGE_SCREEN_ID) {
36871
+ throw new Error(`site page id "page" is reserved for the ${SITE_PAGE_SCREEN_ID} fallback screen`);
36872
+ }
36873
+ if (typeof page.title !== "string" || !page.title.trim()) {
36874
+ throw new Error(`site page ${page.id} needs a title`);
36875
+ }
36876
+ }
36877
+ function assertSiteShellConfig(site) {
36878
+ normalizeSiteOrigin(site.origin);
36879
+ if (site.kind !== "wix" && site.kind !== "base44") {
36880
+ throw new Error(`site kind ${JSON.stringify(site.kind)} must be "wix" or "base44"`);
36881
+ }
36882
+ if (!Array.isArray(site.pages) || site.pages.length === 0) {
36883
+ throw new Error("a site shell needs at least one page");
36884
+ }
36885
+ const seen = /* @__PURE__ */ new Set();
36886
+ for (const page of site.pages) {
36887
+ assertSitePage(page);
36888
+ if (seen.has(page.id)) throw new Error(`site page id ${page.id} is declared twice`);
36889
+ seen.add(page.id);
36890
+ sitePageUrl(site.origin, page.path);
36891
+ }
36892
+ if (!site.pages.some((page) => (page.placement ?? "tab") === "tab")) {
36893
+ throw new Error('a site shell needs at least one page with placement "tab"');
36894
+ }
36895
+ }
36896
+ var SITE_SHELL_MAX_TABS, SITE_SCREEN_PREFIX, SITE_PAGE_SCREEN_ID, PAGE_ID;
36816
36897
  var init_site_shell = __esm({
36817
36898
  "../../core/ts/src/site-shell.ts"() {
36899
+ SITE_SHELL_MAX_TABS = 4;
36900
+ SITE_SCREEN_PREFIX = "site_";
36901
+ SITE_PAGE_SCREEN_ID = "site_page";
36902
+ PAGE_ID = /^[a-z0-9_]+$/;
36818
36903
  }
36819
36904
  });
36820
36905
 
@@ -60017,7 +60102,7 @@ function classifyPageRole(url, title, _nav = "") {
60017
60102
  if (/\b(blog|journal|news|posts?|ebooks?|insight)\b/.test(hay)) return "blog";
60018
60103
  if (/\b(about|story|our-story)\b/.test(hay)) return "about";
60019
60104
  if (/\b(contact|find-us)\b/.test(hay)) return "contact";
60020
- if (/\b(event|meetup|workshop)\b/.test(hay)) return "events";
60105
+ if (/\b(events?|meetups?|workshops?)\b/.test(hay)) return "events";
60021
60106
  if (/\b(gallery|portfolio|work)\b/.test(hay)) return "gallery";
60022
60107
  return "other";
60023
60108
  }
@@ -60380,11 +60465,11 @@ function parsePublishedHtml(html) {
60380
60465
  PAGE_ENTRY_RE.lastIndex = 0;
60381
60466
  let m;
60382
60467
  while (m = PAGE_ENTRY_RE.exec(html)) {
60383
- const pageId = m[1];
60384
- if (seen.has(pageId) || pages.length >= 80) continue;
60385
- seen.add(pageId);
60468
+ const pageId2 = m[1];
60469
+ if (seen.has(pageId2) || pages.length >= 80) continue;
60470
+ seen.add(pageId2);
60386
60471
  pages.push({
60387
- pageId,
60472
+ pageId: pageId2,
60388
60473
  title: unescapeJsonString(m[2] ?? ""),
60389
60474
  slug: unescapeJsonString(m[3] ?? "")
60390
60475
  });
@@ -60679,7 +60764,7 @@ function veloHintFromEval(evaled, page) {
60679
60764
  if (!v) return void 0;
60680
60765
  const wixRendered = !!v.wixRendered;
60681
60766
  const pageModuleUrls = (v.pageModuleUrls ?? []).filter(Boolean).slice(0, 16);
60682
- const pageId = (v.pageId || page.pageId || "").trim() || void 0;
60767
+ const pageId2 = (v.pageId || page.pageId || "").trim() || void 0;
60683
60768
  const hasCode = pageModuleUrls.length > 0 || !!v.gridAppId || (v.wixCodeFeatures ?? []).length > 0;
60684
60769
  return {
60685
60770
  wixRendered,
@@ -60688,7 +60773,7 @@ function veloHintFromEval(evaled, page) {
60688
60773
  wixCodeFeatures: (v.wixCodeFeatures ?? []).slice(0, 24),
60689
60774
  pageModuleUrls,
60690
60775
  httpFunctionHints: (v.httpFunctionHints ?? []).filter(Boolean).slice(0, 16),
60691
- pages: [{ url: page.url, title: page.title, hasCode: wixRendered && hasCode, ...pageId ? { pageId } : {} }]
60776
+ pages: [{ url: page.url, title: page.title, hasCode: wixRendered && hasCode, ...pageId2 ? { pageId: pageId2 } : {} }]
60692
60777
  };
60693
60778
  }
60694
60779
  function pageIdFromVeloPath(path2) {
@@ -62811,13 +62896,15 @@ function mergeCustomerOwned(committed, overlay) {
62811
62896
  "locale",
62812
62897
  "wix",
62813
62898
  "copy",
62814
- "faq"
62899
+ "faq",
62900
+ "site"
62815
62901
  ];
62816
62902
  const customer = {};
62817
62903
  for (const key2 of CUSTOMER_OWNED) {
62818
62904
  const value = overlay[key2];
62819
62905
  if (value !== void 0) customer[key2] = value;
62820
62906
  }
62907
+ if (committed.site === void 0) delete customer.site;
62821
62908
  const overlayBranding = { ...customer.branding ?? {} };
62822
62909
  delete overlayBranding.appIconPath;
62823
62910
  delete overlayBranding.fonts;
@@ -62969,7 +63056,7 @@ async function applyOverlays(input) {
62969
63056
  const committedSite = existsSync64(committedPath) ? JSON.parse(readFileSync76(committedPath, "utf8")).site : void 0;
62970
63057
  if (committedSite !== void 0 && overlay.site === void 0) {
62971
63058
  throw new Error(
62972
- `shell "${shell}" is a site shell: its contract names the customer's website (a \`site\` block), which this analysis does not produce yet. Write the site block into a contract and run \`npm run prepare:customer -- --contract <file>\` in ${shellAppDir(shell)} (or \`himi dist --shell ${shell}\`).`
63059
+ `shell "${shell}" is a site shell: its contract names the customer's website (a \`site\` block), and this overlay has none. Re-run \`himi site analyze --url <site> --shell ${shell}\` to derive the site's page map (edit \`site.pages\` in provisioning.overlay.json if needed), then --apply.`
62973
63060
  );
62974
63061
  }
62975
63062
  const packageDir = dirname35(resolve49(input.overlayPath));
@@ -63175,6 +63262,170 @@ var init_apply2 = __esm({
63175
63262
  }
63176
63263
  });
63177
63264
 
63265
+ // src/site-analyze/site-shell-derive.ts
63266
+ function isSystemPage(path2, title) {
63267
+ const last = path2.split("/").filter(Boolean).pop() ?? "";
63268
+ return SYSTEM_SEGMENT.test(last) || SYSTEM_TITLE.test(title);
63269
+ }
63270
+ function boundedTitle(s) {
63271
+ const t = oneLine2(s).replace(/\s*\(new\)\s*$/i, "");
63272
+ return t.length <= SITE_PAGE_TITLE_MAX ? t : t.slice(0, SITE_PAGE_TITLE_MAX).trimEnd();
63273
+ }
63274
+ function pathOnSite(origin, url) {
63275
+ let u;
63276
+ let base;
63277
+ try {
63278
+ u = new URL(url);
63279
+ base = new URL(origin);
63280
+ } catch {
63281
+ return null;
63282
+ }
63283
+ const host = (h) => h.toLowerCase().replace(/^www\./, "");
63284
+ if (host(u.host) !== host(base.host)) return null;
63285
+ const basePath = base.pathname.replace(/\/+$/, "");
63286
+ const p = u.pathname.replace(/\/+$/, "");
63287
+ if (p !== basePath && !p.startsWith(`${basePath}/`)) return null;
63288
+ return p.slice(basePath.length) || "/";
63289
+ }
63290
+ function isHomePage(page, mainPageId) {
63291
+ return mainPageId !== void 0 && page.pageId === mainPageId || !page.slug || page.slug === "home";
63292
+ }
63293
+ function pageId(c, used) {
63294
+ const fromPath = (c.path.split("/").filter(Boolean).pop() ?? "").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
63295
+ const base = c.role !== "other" && c.role !== "home" && !used.has(c.role) ? c.role : fromPath || "page_1";
63296
+ let id = base === "page" ? "page_1" : base;
63297
+ for (let n = 2; used.has(id); n++) id = `${base}_${n}`;
63298
+ used.add(id);
63299
+ return id;
63300
+ }
63301
+ function deriveSiteShell(input) {
63302
+ const origin = normalizeSiteOrigin(input.siteUrl);
63303
+ const notes = [];
63304
+ const published = input.look.sitePages ?? [];
63305
+ const mainPageId = input.look.mainPageId;
63306
+ const candidates = [];
63307
+ const seenPaths = /* @__PURE__ */ new Set(["/"]);
63308
+ let systemDropped = 0;
63309
+ const add = (title, path2, url) => {
63310
+ if (seenPaths.has(path2) || !title) return;
63311
+ if (isSystemPage(path2, title)) {
63312
+ seenPaths.add(path2);
63313
+ systemDropped++;
63314
+ return;
63315
+ }
63316
+ seenPaths.add(path2);
63317
+ candidates.push({ title: boundedTitle(title), path: path2, role: classifyPageRole(url, title) });
63318
+ };
63319
+ const navLinks = input.look.navLinks ?? [];
63320
+ let homeTitle = "Home";
63321
+ if (navLinks.length) {
63322
+ for (const link of navLinks) {
63323
+ const path2 = pathOnSite(origin, link.href);
63324
+ if (path2 === null) continue;
63325
+ if (path2 === "/" && homeTitle === "Home" && HOME_WORD.test(oneLine2(link.label))) homeTitle = boundedTitle(link.label);
63326
+ add(link.label, path2, link.href);
63327
+ }
63328
+ } else if (input.look.navLabels.length && published.length) {
63329
+ const byTitle = new Map(published.map((p) => [oneLine2(p.title).toLowerCase(), p]));
63330
+ for (const label2 of input.look.navLabels) {
63331
+ if (homeTitle === "Home" && HOME_WORD.test(oneLine2(label2))) homeTitle = boundedTitle(label2);
63332
+ const page = byTitle.get(oneLine2(label2).toLowerCase());
63333
+ const path2 = page?.url ? pathOnSite(origin, page.url) : null;
63334
+ if (page && path2 !== null) add(label2, path2, page.url);
63335
+ }
63336
+ }
63337
+ if (!candidates.length && navLinks.length) {
63338
+ notes.push("site pages: the site's header menu links only to its home page, so the map has only Home \u2014 add the site's pages to `site.pages` before --apply.");
63339
+ } else if (!candidates.length) {
63340
+ for (const page of published) {
63341
+ if (isHomePage(page, mainPageId) || !page.url) continue;
63342
+ const path2 = pathOnSite(origin, page.url);
63343
+ if (path2 !== null) add(page.title, path2, page.url);
63344
+ }
63345
+ if (published.length) {
63346
+ notes.push("site pages: no header menu was crawled, so pages follow the published page list \u2014 reorder them in `site.pages` if the site's menu differs.");
63347
+ }
63348
+ }
63349
+ if (systemDropped) notes.push(`site pages: left out ${systemDropped} flow/utility page(s) (thank-you, popups, item templates, members-area pages).`);
63350
+ const dropped = Math.max(0, candidates.length - (DERIVED_SITE_MAX_PAGES - 1));
63351
+ const kept = candidates.slice(0, DERIVED_SITE_MAX_PAGES - 1);
63352
+ if (dropped) notes.push(`site pages: kept the first ${DERIVED_SITE_MAX_PAGES} pages; ${dropped} more are reachable from the site's own links but have no native row.`);
63353
+ const used = /* @__PURE__ */ new Set(["home"]);
63354
+ const placed = kept.map((c) => ({ c, id: pageId(c, used), placement: "drawer" }));
63355
+ let tabs = 1;
63356
+ for (const p of placed) {
63357
+ if (p.c.role === "members") p.placement = "overflow";
63358
+ else if (TAB_ROLES.has(p.c.role) && tabs < SITE_SHELL_MAX_TABS) {
63359
+ p.placement = "tab";
63360
+ tabs++;
63361
+ }
63362
+ }
63363
+ if (tabs < 2) {
63364
+ for (const p of placed) {
63365
+ if (tabs >= SITE_SHELL_MAX_TABS) break;
63366
+ if (p.placement === "drawer") {
63367
+ p.placement = "tab";
63368
+ tabs++;
63369
+ }
63370
+ }
63371
+ }
63372
+ const pages = [
63373
+ { id: "home", title: homeTitle, path: "/", icon: "home" },
63374
+ ...placed.map(({ c, id, placement }) => ({
63375
+ id,
63376
+ title: c.title,
63377
+ path: c.path,
63378
+ icon: ROLE_ICON[c.role],
63379
+ ...placement === "tab" ? {} : { placement }
63380
+ }))
63381
+ ];
63382
+ if (!published.length && !navLinks.length) {
63383
+ 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.");
63384
+ }
63385
+ const site = {
63386
+ origin,
63387
+ kind: "wix",
63388
+ pages,
63389
+ // Hidden by default — the product decision for the site shell (RFC 0062 §6); the owner can
63390
+ // turn any of them back on here, and the flags ride the OTA descriptor.
63391
+ chrome: { hideSiteHeader: true, hideSiteFooter: true, hideWixAds: true }
63392
+ };
63393
+ assertSiteShellConfig(site);
63394
+ const tabTitles = pages.filter((p) => (p.placement ?? "tab") === "tab").map((p) => p.title);
63395
+ notes.unshift(`site pages: ${pages.length} derived \u2014 tabs ${tabTitles.join(", ")}; edit \`site.pages\` in provisioning.overlay.json to change them.`);
63396
+ return { site, notes };
63397
+ }
63398
+ var DERIVED_SITE_MAX_PAGES, SITE_PAGE_TITLE_MAX, ROLE_ICON, TAB_ROLES, SYSTEM_SEGMENT, SYSTEM_TITLE, HOME_WORD, oneLine2;
63399
+ var init_site_shell_derive = __esm({
63400
+ "src/site-analyze/site-shell-derive.ts"() {
63401
+ init_site_shell();
63402
+ init_traits();
63403
+ DERIVED_SITE_MAX_PAGES = 10;
63404
+ SITE_PAGE_TITLE_MAX = 40;
63405
+ ROLE_ICON = {
63406
+ home: "home",
63407
+ shop: "bag",
63408
+ book: "calendar",
63409
+ menu: "list",
63410
+ blog: "newspaper",
63411
+ about: "info",
63412
+ contact: "mail",
63413
+ events: "ticket",
63414
+ gallery: "photo",
63415
+ plans: "tag",
63416
+ faq: "help",
63417
+ library: "book",
63418
+ members: "person",
63419
+ other: "globe"
63420
+ };
63421
+ TAB_ROLES = /* @__PURE__ */ new Set(["shop", "book", "menu", "events", "plans", "blog", "gallery", "library"]);
63422
+ 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;
63423
+ 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;
63424
+ HOME_WORD = /^(home|home ?page|main|start|accueil|inicio|início|startseite|home ?sweet ?home|בית|דף הבית|ראשי|الرئيسية|ホーム|首页|홈)$/i;
63425
+ oneLine2 = (s) => s.replace(/[\u0000-\u001F\u007F\s]+/g, " ").trim();
63426
+ }
63427
+ });
63428
+
63178
63429
  // ../../node_modules/tldts/dist/cjs/index.js
63179
63430
  var require_cjs = __commonJS({
63180
63431
  "../../node_modules/tldts/dist/cjs/index.js"(exports) {
@@ -66298,6 +66549,7 @@ async function createPlaywrightBrowser(load = () => import("playwright"), siteFe
66298
66549
  const seen = /* @__PURE__ */ new Set();
66299
66550
  const pages = [];
66300
66551
  const navAll = [];
66552
+ const navLinksAll = [];
66301
66553
  const veloHints = [];
66302
66554
  let htmlLang;
66303
66555
  let faviconUrl;
@@ -66321,7 +66573,7 @@ async function createPlaywrightBrowser(load = () => import("playwright"), siteFe
66321
66573
  } catch {
66322
66574
  continue;
66323
66575
  }
66324
- let pageId = evaled.velo?.pageId || void 0;
66576
+ let pageId2 = evaled.velo?.pageId || void 0;
66325
66577
  let publishedTitle = "";
66326
66578
  try {
66327
66579
  const html = await page.content();
@@ -66333,7 +66585,7 @@ async function createPlaywrightBrowser(load = () => import("playwright"), siteFe
66333
66585
  }
66334
66586
  if (pub.pages.length && !sitePages.length) sitePages = pub.pages;
66335
66587
  const hit = matchPublishedPage(url, title, pub.pages, pub.mainPageId);
66336
- if (!pageId) pageId = hit?.pageId;
66588
+ if (!pageId2) pageId2 = hit?.pageId;
66337
66589
  publishedTitle = hit?.title ?? "";
66338
66590
  if (!seededFromMap && pub.pages.length) {
66339
66591
  seededFromMap = true;
@@ -66365,13 +66617,17 @@ async function createPlaywrightBrowser(load = () => import("playwright"), siteFe
66365
66617
  ctas: ctasOf(evaled.buttons)
66366
66618
  },
66367
66619
  screenshotLocal: `evidence/pages/${filename}`,
66368
- ...pageId ? { pageId } : {},
66620
+ ...pageId2 ? { pageId: pageId2 } : {},
66369
66621
  ...copyFromEval(evaled)
66370
66622
  });
66371
- const hint = veloHintFromEval(evaled, { url, title, pageId });
66623
+ const hint = veloHintFromEval(evaled, { url, title, pageId: pageId2 });
66372
66624
  if (hint) veloHints.push(hint);
66373
66625
  if (!htmlLang && evaled.htmlLang) htmlLang = evaled.htmlLang;
66374
66626
  for (const n of evaled.nav) if (!navAll.includes(n)) navAll.push(n);
66627
+ for (const l of evaled.navLinks ?? []) {
66628
+ const key2 = `${l.label}\0${l.href.split("#")[0]}`;
66629
+ if (navLinksAll.length < 24 && !navLinksAll.some((x) => `${x.label}\0${x.href.split("#")[0]}` === key2)) navLinksAll.push(l);
66630
+ }
66375
66631
  if (!faviconUrl && evaled.velo?.favicon && !isGenericWixFavicon(evaled.velo.favicon)) {
66376
66632
  faviconUrl = evaled.velo.favicon;
66377
66633
  }
@@ -66393,6 +66649,7 @@ async function createPlaywrightBrowser(load = () => import("playwright"), siteFe
66393
66649
  return {
66394
66650
  pages,
66395
66651
  navLabels: navAll,
66652
+ ...navLinksAll.length ? { navLinks: navLinksAll } : {},
66396
66653
  theme: Object.keys(wst).length ? {
66397
66654
  colors: Object.fromEntries(Object.entries(wst).filter(([k]) => k.includes("color"))),
66398
66655
  fonts: Object.fromEntries(Object.entries(wst).filter(([k]) => k.includes("font")))
@@ -66479,8 +66736,13 @@ var init_crawl_look = __esm({
66479
66736
  if (probe.parentNode) probe.parentNode.removeChild(probe);
66480
66737
  } catch (e3) {}
66481
66738
  const txt = (el) => String((el && el.textContent) || "").replace(/\\s+/g, " ").trim();
66482
- const nav = Array.from(document.querySelectorAll("nav a, header a, [role=navigation] a"))
66483
- .map((a) => (a.textContent || "").trim()).filter(Boolean).slice(0, 24);
66739
+ const navAnchors = Array.from(document.querySelectorAll("nav a, header a, [role=navigation] a"));
66740
+ const nav = navAnchors.map((a) => (a.textContent || "").trim()).filter(Boolean).slice(0, 24);
66741
+ // WHERE each label goes \u2014 a site shell orders its tabs by these (RFC 0062). a.href is
66742
+ // already absolute; an anchor with no text or no href is not a page link.
66743
+ const navLinks = navAnchors
66744
+ .map((a) => ({ label: txt(a), href: String(a.href || "") }))
66745
+ .filter((l) => l.label && /^https?:/.test(l.href)).slice(0, 24);
66484
66746
  const headings = Array.from(document.querySelectorAll("h1,h2")).map((h) => (h.textContent || "").trim()).filter(Boolean).slice(0, 12);
66485
66747
  const hero = ((document.querySelector("h1") && document.querySelector("h1").textContent) || "").trim();
66486
66748
 
@@ -66605,7 +66867,7 @@ var init_crawl_look = __esm({
66605
66867
  if (pid) velo.pageId = pid[1];
66606
66868
  }
66607
66869
  } catch (e4) {}
66608
- return { wst, nav, headings, hero, imgs, hrefs, buttons, html, htmlLang, velo, sections, ctaLabels, faq, testimonials, about, meta };
66870
+ return { wst, nav, navLinks, headings, hero, imgs, hrefs, buttons, html, htmlLang, velo, sections, ctaLabels, faq, testimonials, about, meta };
66609
66871
  })()`;
66610
66872
  CAP4 = {
66611
66873
  heading: 120,
@@ -67271,6 +67533,7 @@ function ownerSiteFor(opts, env = process.env) {
67271
67533
  return env.WIX_SITE_ID?.trim() || void 0;
67272
67534
  }
67273
67535
  async function runSiteAnalyze(opts) {
67536
+ if (opts.shell !== void 0) assertKnownShell(opts.shell);
67274
67537
  if (!opts.clientId?.trim()) {
67275
67538
  if (opts.apply) throw Object.assign(new Error(APPLY_NEEDS_CLIENT), { exit: 2 });
67276
67539
  if (opts.redirectUri?.trim()) throw Object.assign(new Error(REDIRECT_NEEDS_CLIENT), { exit: 2 });
@@ -67303,6 +67566,18 @@ async function runSiteAnalyze(opts) {
67303
67566
  { clientId: opts.clientId, siteUrl: opts.siteUrl, msid, redirectUri: opts.redirectUri },
67304
67567
  ports
67305
67568
  );
67569
+ if (opts.shell !== void 0 && isSiteShell(opts.shell)) {
67570
+ try {
67571
+ const derived = deriveSiteShell({ siteUrl: result.book.input.siteUrl, look: result.book.look });
67572
+ result.provisioningOverlay.site = derived.site;
67573
+ result.warnings = [...result.warnings ?? [], ...derived.notes];
67574
+ } catch (e) {
67575
+ result.warnings = [
67576
+ ...result.warnings ?? [],
67577
+ `site pages: could not derive a site block for --shell ${opts.shell} (${e instanceof Error ? e.message : String(e)}) \u2014 write \`site\` into provisioning.overlay.json by hand before --apply.`
67578
+ ];
67579
+ }
67580
+ }
67306
67581
  const ownerCall = ownerAuth ? ownerTransportFrom(fetchImpl, ownerAuth) : void 0;
67307
67582
  const written = await writeBusinessBook(result, {
67308
67583
  outDir,
@@ -67392,6 +67667,8 @@ var init_cli = __esm({
67392
67667
  init_write_artifacts();
67393
67668
  init_net_guard();
67394
67669
  init_apply2();
67670
+ init_site_shell_derive();
67671
+ init_brandable_shells();
67395
67672
  init_crawl_look();
67396
67673
  init_rest();
67397
67674
  init_wix_owner();
@@ -71122,7 +71399,10 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
71122
71399
  visitor-only pass (published pages, look, copy); --client-id
71123
71400
  adds the vertical catalog counts and the app binding, and
71124
71401
  --apply requires it. --shell takes the same
71125
- names 'himi dist --shell' does; default branded-lite. Owner token: WIX_TEST_TOKEN
71402
+ names 'himi dist --shell' does; default branded-lite.
71403
+ A SITE shell (brandy) also derives the site's page map
71404
+ (tabs/drawer from its menu) into the overlay's \`site\`
71405
+ block \u2014 edit it, then --apply. Owner token: WIX_TEST_TOKEN
71126
71406
  or 'wix token --site'. Look/screenshots need Chromium:
71127
71407
  npx playwright install chromium.
71128
71408
  himi site function list|get|put|publish|invoke [--site <msid>] [--url <https>] [--method GET]
@@ -71233,7 +71513,7 @@ var COMMANDS = [
71233
71513
  { 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>]" },
71234
71514
  { name: "logout", summary: "remove the stored control token for a target", required: "[--target <t>]" },
71235
71515
  { 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]" },
71236
- { 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]" },
71516
+ { 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. `--shell brandy` (the site shell, RFC 0062) also derives the site's page map from its published pages and header menu \u2014 Home first, up to four tabs for the pages a visitor returns to, the rest in the drawer \u2014 into the overlay's `site` block, so it can be edited before `--apply`; only a site shell ever receives that block. 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]" },
71237
71517
  { 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>]" },
71238
71518
  { 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]" },
71239
71519
  { 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. A successful draft push also rebuilds the making-of (.himi/making-of.html, local and offline) and names it as `reel` \u2014 the file to hand the owner beside the preview link. Its postcard carries the home shot only when that shot is newer than the last source edit; otherwise the postcard says why and names `himi render <home> --png`, and the envelope's `postcardPicture: {skipped, render}` leads next[] with that render (the push never renders it itself). 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)" },
@@ -78437,7 +78717,7 @@ ${report2.ok ? "OK \u2014 data + serve plane reachable." : typeof report2.hint =
78437
78717
  }
78438
78718
  }
78439
78719
  if (sub !== "analyze") {
78440
- 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` }));
78720
+ 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>] [--shell <id>] [--apply] | himi site function list|get|put|publish|invoke \u2026` }));
78441
78721
  return 2;
78442
78722
  }
78443
78723
  const clientFlag = str8(flags["client-id"]);
@@ -78471,7 +78751,7 @@ ${report2.ok ? "OK \u2014 data + serve plane reachable." : typeof report2.hint =
78471
78751
  print({ ok: true, context });
78472
78752
  return 0;
78473
78753
  }
78474
- 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 + "]]";
78754
+ const analyzeUsage = "himi site analyze --url https://\u2026 [--client-id <headless-oauth-client-id>] [--site <msid>] [--out .himi/business-book] [--redirect-uri <uri>] [--shell " + SHELL_ENUM + "] [--apply]";
78475
78755
  if (!siteUrl) {
78476
78756
  io.error(JSON.stringify({ error: "himi site analyze needs --url", usage: analyzeUsage }));
78477
78757
  return 2;
@@ -156,6 +156,28 @@ The plane has no headless browser (`look.unavailable: no_browser_on_plane`). Own
156
156
 
157
157
  `--apply` writes `appearance.homeLayout` + `navigation.navKind` plus identity/colors. It never silently overwrites committed Amit1 `apps/himi-branded-lite/provisioning.json`.
158
158
 
159
+ ### A site shell: `--shell brandy`
160
+
161
+ `himi-brandy`, the site shell, opens the customer's live website as native-framed tabs, so its contract is a `site` block: which website, and which of its pages are tabs, drawer rows or overflow rows. `--shell brandy` derives that block into `provisioning.overlay.json`:
162
+
163
+ ```bash
164
+ himi site analyze --url https://owner.wixsite.com/site --client-id <id> --shell brandy # writes site.pages
165
+ # edit site.pages in .himi/business-book/provisioning.overlay.json if the map needs it
166
+ himi site analyze --url https://owner.wixsite.com/site --client-id <id> --shell brandy --apply
167
+ ```
168
+
169
+ The map comes from what the analysis already reads without owner credentials, because Wix has no public menu API: the published page list (`look.sitePages`), the header menu the crawl saw (`look.navLinks`, label and href), and each page's role.
170
+
171
+ - **Order and labels** follow the site's own menu. Links to other hosts, and repeat links to the same page, are dropped.
172
+ - **No menu crawled** (no browser, or the crawl failed): the published page list is used instead, and a note says so.
173
+ - **Home** is always the first tab, with the menu's own word for it.
174
+ - **Tabs:** the pages a visitor comes back to (shop, book, menu, events, plans, blog, gallery) take up to three more, in menu order. A small site fills its bar in menu order rather than show a single tab.
175
+ - **Drawer and overflow:** About, Contact and other pages go to the drawer; account pages go to overflow.
176
+ - **Size:** at most 10 pages, with titles capped at 40 characters.
177
+ - **Chrome:** the site's own header, footer and Wix ads are hidden (the site shell's default).
178
+
179
+ The block is validated with the shell's own rules before it is written. Only a site shell ever receives it: the analysis leaves it out for any other `--shell`, and the merge drops it for a shell whose committed contract has no `site`. `--apply --shell brandy` without a `site` block in the overlay is refused, because baking would ship the shell's demo site under the customer's brand.
180
+
159
181
  When the analysis recognized at least one vertical, `--apply` also patches the shell's baked home
160
182
  (`dev/index.ts`) to what the site actually runs. The Explore grid keeps only the site's
161
183
  destinations (adding a card the template baked none for, and leaving off a vertical the template
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1102.0",
3
+ "version": "0.1104.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": "49e51090f4e4f0dc82b4398a9f06fb4f52e4055be047ff22734a8d41"
70
+ "falconPackageHash": "3b9d4a5bc87c7f44c95094ff409a54b61fb726b2c3062b7a587718be"
71
71
  }