@wix/himalaya-cli 0.1156.0 → 0.1158.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.
@@ -137,6 +137,8 @@ export interface WebViewConfig {
137
137
  coverBandColor?: string;
138
138
  hideWixAds?: boolean;
139
139
  hideHostedSiteFooter?: boolean;
140
+ sessionBridge?: boolean;
141
+ hostedSiteAdapter?: "" | "wix" | "base44";
140
142
  }
141
143
  export declare function vstack(props: {
142
144
  spacing?: number;
@@ -3036,6 +3036,14 @@ export interface WebViewConfig {
3036
3036
  * script, like {@link hideHostedSiteHeader}. No-op on web (cross-origin iframe).
3037
3037
  */
3038
3038
  hideHostedSiteFooter?: boolean;
3039
+ /**
3040
+ * RFC 0062 §5.8 — sign the page in natively. Runs only when {@link hostedSiteAdapter} names an
3041
+ * adapter AND the app declares that adapter's capability; the host seeds the adapter's session
3042
+ * into the page's own storage at document start, on the exact {@link allowedOrigins} only.
3043
+ */
3044
+ sessionBridge?: boolean;
3045
+ /** RFC 0062 §5.8 — which site adapter the host runs: `""` (none) | `"wix"` | `"base44"`. */
3046
+ hostedSiteAdapter?: "" | "wix" | "base44";
3039
3047
  }
3040
3048
  /**
3041
3049
  * Embeddable WebView component (web-in-SDUI). WebViews have no intrinsic height,
@@ -1051,6 +1051,13 @@ export declare const screenDescriptorSchema: {
1051
1051
  readonly hideHostedSiteFooter: {
1052
1052
  readonly type: "boolean";
1053
1053
  };
1054
+ readonly sessionBridge: {
1055
+ readonly type: "boolean";
1056
+ };
1057
+ readonly hostedSiteAdapter: {
1058
+ readonly type: "string";
1059
+ readonly enum: readonly ["", "wix", "base44"];
1060
+ };
1054
1061
  };
1055
1062
  };
1056
1063
  };
@@ -2012,6 +2019,13 @@ export declare const widgetSnapshotSchema: {
2012
2019
  readonly hideHostedSiteFooter: {
2013
2020
  readonly type: "boolean";
2014
2021
  };
2022
+ readonly sessionBridge: {
2023
+ readonly type: "boolean";
2024
+ };
2025
+ readonly hostedSiteAdapter: {
2026
+ readonly type: "string";
2027
+ readonly enum: readonly ["", "wix", "base44"];
2028
+ };
2015
2029
  };
2016
2030
  };
2017
2031
  };
@@ -8634,7 +8634,10 @@ var screenDescriptorSchema = {
8634
8634
  coverBandColor: { type: "string" },
8635
8635
  // RFC 0062 — binary-shipped chrome hiding for a hosted site (no page script over the air).
8636
8636
  hideWixAds: { type: "boolean" },
8637
- hideHostedSiteFooter: { type: "boolean" }
8637
+ hideHostedSiteFooter: { type: "boolean" },
8638
+ // RFC 0062 §5.8 — the native session bridge (fields 47/48).
8639
+ sessionBridge: { type: "boolean" },
8640
+ hostedSiteAdapter: { type: "string", enum: ["", "wix", "base44"] }
8638
8641
  }
8639
8642
  }
8640
8643
  }
@@ -13169,6 +13172,10 @@ function siteWebViewPreset(site) {
13169
13172
  hideHostedSiteFooter: chrome.hideSiteFooter,
13170
13173
  // Wix ads only exist on Wix free sites; a Base44 site never gets the Wix selectors.
13171
13174
  hideWixAds: site.kind === "wix" && chrome.hideWixAds,
13175
+ // RFC 0062 §5.8 — a Base44 app is signed in natively and its session carried into the page.
13176
+ // The host still refuses unless the app declares the `base44` capability. Wix keeps its
13177
+ // item-2 bridge (not built yet), so a Wix site carries neither field.
13178
+ ...site.kind === "base44" ? { hostedSiteAdapter: "base44", sessionBridge: true } : {},
13172
13179
  pullToRefresh: true,
13173
13180
  showProgressBar: true,
13174
13181
  syncStatusBarAppearance: true,
@@ -7587,7 +7587,10 @@ var screenDescriptorSchema = {
7587
7587
  coverBandColor: { type: "string" },
7588
7588
  // RFC 0062 — binary-shipped chrome hiding for a hosted site (no page script over the air).
7589
7589
  hideWixAds: { type: "boolean" },
7590
- hideHostedSiteFooter: { type: "boolean" }
7590
+ hideHostedSiteFooter: { type: "boolean" },
7591
+ // RFC 0062 §5.8 — the native session bridge (fields 47/48).
7592
+ sessionBridge: { type: "boolean" },
7593
+ hostedSiteAdapter: { type: "string", enum: ["", "wix", "base44"] }
7591
7594
  }
7592
7595
  }
7593
7596
  }
package/dist/cli.mjs CHANGED
@@ -6834,6 +6834,7 @@ function resolveTranslation(value, catalog, sourceCatalog) {
6834
6834
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
6835
6835
  var init_bindings = __esm({
6836
6836
  "../../core/ts/src/bindings.ts"() {
6837
+ "use strict";
6837
6838
  BINDING_SIGILS = ["service", "flag", "item", "t"];
6838
6839
  BINDING_SIGIL_RE = new RegExp(
6839
6840
  `^\\$(?:${BINDING_SIGILS.filter((s) => s !== "t").join("|")})\\b|^\\$t:`
@@ -7672,7 +7673,10 @@ var init_validator = __esm({
7672
7673
  coverBandColor: { type: "string" },
7673
7674
  // RFC 0062 — binary-shipped chrome hiding for a hosted site (no page script over the air).
7674
7675
  hideWixAds: { type: "boolean" },
7675
- hideHostedSiteFooter: { type: "boolean" }
7676
+ hideHostedSiteFooter: { type: "boolean" },
7677
+ // RFC 0062 §5.8 — the native session bridge (fields 47/48).
7678
+ sessionBridge: { type: "boolean" },
7679
+ hostedSiteAdapter: { type: "string", enum: ["", "wix", "base44"] }
7676
7680
  }
7677
7681
  }
7678
7682
  }
@@ -73422,6 +73426,7 @@ function pageId(c, used) {
73422
73426
  }
73423
73427
  function deriveSiteShell(input) {
73424
73428
  const origin = normalizeSiteOrigin(input.siteUrl);
73429
+ const kind = input.kind ?? siteKindForOrigin(origin);
73425
73430
  const notes = [];
73426
73431
  const published = input.look.sitePages ?? [];
73427
73432
  const mainPageId = input.look.mainPageId;
@@ -73440,7 +73445,14 @@ function deriveSiteShell(input) {
73440
73445
  };
73441
73446
  const navLinks = input.look.navLinks ?? [];
73442
73447
  let homeTitle = "Home";
73443
- if (navLinks.length) {
73448
+ const routes = (input.routes ?? []).filter((r) => isNavigableRoute(r.path));
73449
+ if (routes.length) {
73450
+ for (const r of routes) {
73451
+ const path2 = r.path.startsWith("/") ? r.path : `/${r.path}`;
73452
+ add(routeTitle(r), path2, `${origin.replace(/\/$/, "")}${path2}`);
73453
+ }
73454
+ notes.push(`site pages: taken from the Base44 app book's ${routes.length} route(s) \u2014 edit \`site.pages\` to rename or reorder them.`);
73455
+ } else if (navLinks.length) {
73444
73456
  for (const link of navLinks) {
73445
73457
  const path2 = pathOnSite(origin, link.href);
73446
73458
  if (path2 === null) continue;
@@ -73501,22 +73513,39 @@ function deriveSiteShell(input) {
73501
73513
  ...placement === "tab" ? {} : { placement }
73502
73514
  }))
73503
73515
  ];
73504
- if (!published.length && !navLinks.length) {
73516
+ if (!published.length && !navLinks.length && !routes.length) {
73505
73517
  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.");
73506
73518
  }
73507
73519
  const site = {
73508
73520
  origin,
73509
- kind: "wix",
73521
+ kind,
73510
73522
  pages,
73511
73523
  // Hidden by default — the product decision for the site shell (RFC 0062 §6); the owner can
73512
- // turn any of them back on here, and the flags ride the OTA descriptor.
73513
- chrome: { hideSiteHeader: true, hideSiteFooter: true, hideWixAds: true }
73524
+ // turn any of them back on here, and the flags ride the OTA descriptor. A Base44 app has no
73525
+ // Wix ads to hide, so it never carries the flag (RFC 0062 §5.8).
73526
+ chrome: kind === "wix" ? { hideSiteHeader: true, hideSiteFooter: true, hideWixAds: true } : { hideSiteHeader: true, hideSiteFooter: true }
73514
73527
  };
73515
73528
  assertSiteShellConfig(site);
73516
73529
  const tabTitles = pages.filter((p) => (p.placement ?? "tab") === "tab").map((p) => p.title);
73517
73530
  notes.unshift(`site pages: ${pages.length} derived \u2014 tabs ${tabTitles.join(", ")}; edit \`site.pages\` in provisioning.overlay.json to change them.`);
73518
73531
  return { site, notes };
73519
73532
  }
73533
+ function siteKindForOrigin(origin) {
73534
+ const host = new URL(origin).hostname.toLowerCase();
73535
+ const isBase44 = ["base44.app", "base44.com"].some((d) => host === d || host.endsWith(`.${d}`));
73536
+ return isBase44 ? "base44" : "wix";
73537
+ }
73538
+ function isNavigableRoute(path2) {
73539
+ const p = path2.trim();
73540
+ if (!p || p === "/" || p.includes(":") || p.includes("*")) return false;
73541
+ return !/^\/?(login|signin|sign-in|register|signup|logout)(\/|$)/i.test(p);
73542
+ }
73543
+ function routeTitle(r) {
73544
+ const fromComponent = r.component?.replace(/(Page|Screen|View)$/, "").replace(/([a-z0-9])([A-Z])/g, "$1 $2").trim();
73545
+ if (fromComponent) return fromComponent;
73546
+ const seg = r.path.split("/").filter(Boolean).pop() ?? r.path;
73547
+ return seg.replace(/[-_]+/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
73548
+ }
73520
73549
  var DERIVED_SITE_MAX_PAGES, SITE_PAGE_TITLE_MAX, ROLE_ICON, TAB_ROLES, SYSTEM_SEGMENT, SYSTEM_TITLE, HOME_WORD, oneLine2;
73521
73550
  var init_site_shell_derive = __esm({
73522
73551
  "src/site-analyze/site-shell-derive.ts"() {
@@ -74174,12 +74203,13 @@ __export(cli_exports2, {
74174
74203
  SITE_FUNCTION_USAGE: () => SITE_FUNCTION_USAGE,
74175
74204
  applyNeedsClient: () => applyNeedsClient,
74176
74205
  assetFetchFor: () => assetFetchFor,
74206
+ base44BookRoutes: () => base44BookRoutes,
74177
74207
  noteWithWarnings: () => noteWithWarnings,
74178
74208
  ownerSiteFor: () => ownerSiteFor,
74179
74209
  runSiteAnalyze: () => runSiteAnalyze,
74180
74210
  runSiteFunctionCli: () => runSiteFunctionCli
74181
74211
  });
74182
- import { readFileSync as readFileSync88 } from "node:fs";
74212
+ import { existsSync as existsSync71, readFileSync as readFileSync88 } from "node:fs";
74183
74213
  import { resolve as resolve55 } from "node:path";
74184
74214
  function applyNeedsClient(shell) {
74185
74215
  if (shell === void 0 || !SHELL_NAMES2.includes(shell)) return true;
@@ -74230,7 +74260,11 @@ async function runSiteAnalyze(opts) {
74230
74260
  );
74231
74261
  if (opts.shell !== void 0 && isSiteShell(opts.shell)) {
74232
74262
  try {
74233
- const derived = deriveSiteShell({ siteUrl: result.book.input.siteUrl, look: result.book.look });
74263
+ const kind = siteKindForOrigin(result.book.input.siteUrl);
74264
+ const book = kind === "base44" ? base44BookRoutes(resolve55(cwd, ".himi/base44-book/book.json"), result.book.input.siteUrl) : void 0;
74265
+ const routes = book?.routes;
74266
+ if (book?.skipped) result.warnings = [...result.warnings ?? [], book.skipped];
74267
+ const derived = deriveSiteShell({ siteUrl: result.book.input.siteUrl, look: result.book.look, kind, routes });
74234
74268
  result.provisioningOverlay.site = derived.site;
74235
74269
  const warnings2 = (result.warnings ?? []).map(
74236
74270
  (w) => !opts.clientId?.trim() && w.startsWith("no --client-id:") ? `no --client-id: fine for --shell ${opts.shell} \u2014 a site shell binds no client, so --apply works without one. Only the vertical probes were skipped, so the catalog has no counts.` : w
@@ -74325,6 +74359,30 @@ async function runSiteFunctionCli(opts) {
74325
74359
  fetchImpl: opts.fetchImpl
74326
74360
  });
74327
74361
  }
74362
+ function base44BookRoutes(bookPath, siteUrl) {
74363
+ if (!existsSync71(bookPath)) return void 0;
74364
+ try {
74365
+ const book = JSON.parse(readFileSync88(bookPath, "utf8"));
74366
+ if (book.kind !== "base44-app-book" || !Array.isArray(book.routes)) return void 0;
74367
+ const host = (u) => {
74368
+ try {
74369
+ return typeof u === "string" ? new URL(u).host.toLowerCase() : "";
74370
+ } catch {
74371
+ return "";
74372
+ }
74373
+ };
74374
+ const want = host(siteUrl);
74375
+ const have = [host(book.input?.url), host(book.identity?.publishedUrl)].filter(Boolean);
74376
+ if (!have.includes(want)) {
74377
+ return { skipped: `site pages: ignored .himi/base44-book \u2014 it was read from ${have[0] || "an unknown app"}, not ${want}; re-run \`himi base44 analyze --url ${siteUrl}\` for this site.` };
74378
+ }
74379
+ return {
74380
+ routes: book.routes.filter((r) => !!r && typeof r.path === "string").map((r) => ({ path: r.path, ...typeof r.component === "string" ? { component: r.component } : {} }))
74381
+ };
74382
+ } catch {
74383
+ return void 0;
74384
+ }
74385
+ }
74328
74386
  var APPLY_NEEDS_CLIENT, FUNCTION_OPS;
74329
74387
  var init_cli2 = __esm({
74330
74388
  "src/site-analyze/cli.ts"() {
@@ -74559,7 +74617,7 @@ __export(site_create_exports, {
74559
74617
  runSiteList: () => runSiteList,
74560
74618
  writeProvisioningBinding: () => writeProvisioningBinding
74561
74619
  });
74562
- import { existsSync as existsSync71, readFileSync as readFileSync89, writeFileSync as writeFileSync34 } from "node:fs";
74620
+ import { existsSync as existsSync72, readFileSync as readFileSync89, writeFileSync as writeFileSync34 } from "node:fs";
74563
74621
  import { resolve as resolvePath2 } from "node:path";
74564
74622
  function oauthClientName(projectName) {
74565
74623
  return `${projectName} client`;
@@ -74708,7 +74766,7 @@ async function runSiteCreate(opts) {
74708
74766
  if (!wantOauthApp) return { ...base, nextSteps: nextSteps(metaSiteId) };
74709
74767
  return { ...base, clientId, redirectUri, nextSteps: nextSteps(metaSiteId, clientId) };
74710
74768
  }
74711
- function writeProvisioningBinding(target, binding2, fs = { existsSync: existsSync71, readFileSync: readFileSync89, writeFileSync: writeFileSync34 }) {
74769
+ function writeProvisioningBinding(target, binding2, fs = { existsSync: existsSync72, readFileSync: readFileSync89, writeFileSync: writeFileSync34 }) {
74712
74770
  const msid = binding2.siteId;
74713
74771
  const path2 = resolvePath2(target);
74714
74772
  const committed = committedShellProvisioning(path2);
@@ -75381,7 +75439,7 @@ var init_context = __esm({
75381
75439
  });
75382
75440
 
75383
75441
  // src/cli.ts
75384
- import { mkdtempSync as mkdtempSync5, readFileSync as readFileSync90, mkdirSync as mkdirSync39, writeFileSync as writeFileSync35, existsSync as existsSync72, 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";
75442
+ import { mkdtempSync as mkdtempSync5, readFileSync as readFileSync90, mkdirSync as mkdirSync39, writeFileSync as writeFileSync35, existsSync as existsSync73, 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";
75385
75443
  import { execFile as execFile2, execFileSync as execFileSync7, spawnSync as spawnSync5, spawn as spawn7 } from "node:child_process";
75386
75444
  import { AsyncLocalStorage } from "node:async_hooks";
75387
75445
  import { createHash as createHash36, randomUUID as randomUUID8 } from "node:crypto";
@@ -78609,7 +78667,7 @@ function createUpdateRunner(spawn8 = spawnSync5) {
78609
78667
  reexec: (argv) => {
78610
78668
  const root = globalNodeModulesRoot();
78611
78669
  const entry = root ? join74(root, "@wix", "himalaya-cli", "dist", "cli.mjs") : void 0;
78612
- if (!entry || !existsSync72(entry)) return void 0;
78670
+ if (!entry || !existsSync73(entry)) return void 0;
78613
78671
  const result = spawnSync5(process.execPath, [entry, ...argv], {
78614
78672
  // inherit keeps PIPED STDIN working — commands that read stdin must survive a re-exec.
78615
78673
  stdio: "inherit",
@@ -79380,7 +79438,7 @@ async function nativeConfigAdvisories(app, configSource) {
79380
79438
  const { iosProjectYml: iosProjectYml2 } = await Promise.resolve().then(() => (init_paths2(), paths_exports));
79381
79439
  const { androidOptedIn: androidOptedIn3, LABEL_IOS_SPLASH: LABEL_IOS_SPLASH2 } = await Promise.resolve().then(() => (init_splash(), splash_exports));
79382
79440
  const iosYmlPath = iosProjectYml2(app);
79383
- const iosYml = existsSync72(iosYmlPath) ? readFileSync90(iosYmlPath, "utf8") : "";
79441
+ const iosYml = existsSync73(iosYmlPath) ? readFileSync90(iosYmlPath, "utf8") : "";
79384
79442
  let optedIn = iosYml.includes(LABEL_IOS_SPLASH2);
79385
79443
  if (!optedIn) {
79386
79444
  const { androidManifestPath: androidManifestPath3 } = await Promise.resolve().then(() => (init_gen_native_config(), gen_native_config_exports));
@@ -79446,7 +79504,7 @@ function keptPlan(kept) {
79446
79504
  }
79447
79505
  async function initFromRemoteTemplate(name, parentDir, template, c) {
79448
79506
  const dir = resolve56(parentDir, name);
79449
- if (existsSync72(join74(dir, "himalaya.content.json"))) {
79507
+ if (existsSync73(join74(dir, "himalaya.content.json"))) {
79450
79508
  throw new Error(`content package already exists at ${dir}`);
79451
79509
  }
79452
79510
  const r = await c.fetchTemplateSource(TEMPLATE_APP_PREFIX + template);
@@ -79489,7 +79547,7 @@ function symlinkDestinationIssue(dest) {
79489
79547
  function restoreDestinationIssue(dest, force) {
79490
79548
  const link = symlinkDestinationIssue(dest);
79491
79549
  if (link) return link;
79492
- if (!existsSync72(dest)) return null;
79550
+ if (!existsSync73(dest)) return null;
79493
79551
  if (!statSync20(dest).isDirectory()) {
79494
79552
  return { error: `${dest} exists and is not a directory`, hint: "pass --out/--name to land somewhere else, or remove the file" };
79495
79553
  }
@@ -79504,7 +79562,7 @@ function restoreDestinationIssue(dest, force) {
79504
79562
  function isHimalayaMonorepoCheckout(start) {
79505
79563
  let dir = resolve56(start);
79506
79564
  for (; ; ) {
79507
- if (existsSync72(join74(dir, "tools", "himi-cli", "src", "cli.ts")) && existsSync72(join74(dir, "core", "serve", "src", "server.ts"))) return true;
79565
+ if (existsSync73(join74(dir, "tools", "himi-cli", "src", "cli.ts")) && existsSync73(join74(dir, "core", "serve", "src", "server.ts"))) return true;
79508
79566
  const parent = dirname40(dir);
79509
79567
  if (parent === dir) return false;
79510
79568
  dir = parent;
@@ -79832,7 +79890,7 @@ Seed your coding agent with \`himi agent-init\`. Guides: ${idx.portalUrl}`);
79832
79890
  ...res.removed.length ? { removed: res.removed, note: "older copies at the app root, moved to .himi/legacy-agent-files/ (a backup, in case one became app code) \u2014 the current ones live in .agents/skills/himalaya/" } : {},
79833
79891
  // In a git repo the refresh moves the diff identity an earlier `himi test` attested (that
79834
79892
  // hash is shared with the repo gates and deliberately exempts nothing but its own metadata).
79835
- ...existsSync72(join74(dir, ".himi", "attestations", "latest.json")) ? { next: "if `himi push` then refuses the attestation as stale, re-run `himi test` \u2014 in a git checkout this refresh changed the tree it covered" } : {},
79893
+ ...existsSync73(join74(dir, ".himi", "attestations", "latest.json")) ? { next: "if `himi push` then refuses the attestation as stale, re-run `himi test` \u2014 in a git checkout this refresh changed the tree it covered" } : {},
79836
79894
  ...res.skipped.length ? { skipped: res.skipped } : {},
79837
79895
  portalUrl: idx.portalUrl
79838
79896
  });
@@ -80065,7 +80123,7 @@ async function producedWidgetFamilies(dir) {
80065
80123
  }
80066
80124
  return { families, source: "dist/bundles" };
80067
80125
  }
80068
- var boardOpen = (dir) => existsSync72(join74(dir, ".himi", "studio", "index.html"));
80126
+ var boardOpen = (dir) => existsSync73(join74(dir, ".himi", "studio", "index.html"));
80069
80127
  var BOARD_REFRESHERS = /* @__PURE__ */ new Set(["render", "test", "validate", "build", "push", "icon", "site", "ux", "progress"]);
80070
80128
  async function leaveForBoard(dir, record2, value) {
80071
80129
  if (!boardOpen(dir)) return;
@@ -80381,7 +80439,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
80381
80439
  const owed = openApprovals2(readApprovals2(dir).events).length;
80382
80440
  next.push(`carried the planning journal into the app (${carried.join(", ")}${owed ? `; ${owed} approval${owed === 1 ? "" : "s"} still owed` : ""}) \u2014 \`himi progress --brief\` there shows it`);
80383
80441
  }
80384
- const planInParent = ["SPEC.md", "MOBILE-UX.md"].filter((f) => existsSync72(join74(parentDir, f)));
80442
+ const planInParent = ["SPEC.md", "MOBILE-UX.md"].filter((f) => existsSync73(join74(parentDir, f)));
80385
80443
  if (planInParent.length) {
80386
80444
  next.push(`${planInParent.join(" and ")} in ${parentDir} ${planInParent.length > 1 ? "are" : "is"} not the app's \u2014 if that is the approved plan, move it over the template's boilerplate in ${dir}`);
80387
80445
  }
@@ -80661,7 +80719,7 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
80661
80719
  const result = { ...outcome.result, ...fromSpec };
80662
80720
  const adoptDir = contentDir(flags);
80663
80721
  const kept = flags["dry-run"] !== true && (outcome.code === 0 || outcome.result.forcedKeep === true);
80664
- if (kept && existsSync72(join74(adoptDir, adoptBits.ADOPT_FILE))) {
80722
+ if (kept && existsSync73(join74(adoptDir, adoptBits.ADOPT_FILE))) {
80665
80723
  try {
80666
80724
  const inv = adoptBits.inspectApp(adoptDir);
80667
80725
  adoptBits.writeInventory(adoptDir, inv, { companions: "if-missing" });
@@ -81225,7 +81283,7 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
81225
81283
  case "studio": {
81226
81284
  const { openStudio: openStudio2 } = await Promise.resolve().then(() => (init_studio(), studio_exports));
81227
81285
  const dir = contentDir(flags);
81228
- if (!existsSync72(join74(dir, "himalaya.content.json"))) {
81286
+ if (!existsSync73(join74(dir, "himalaya.content.json"))) {
81229
81287
  io.error(JSON.stringify({ ok: false, error: "not_a_content_package", dir, hint: "run from the app directory (or pass --content <dir>) \u2014 the board reads that package's .himi/" }));
81230
81288
  return 2;
81231
81289
  }
@@ -81303,7 +81361,7 @@ ${handover.hint[0].toUpperCase()}${handover.hint.slice(1)}.`);
81303
81361
  } = await Promise.resolve().then(() => (init_progress(), progress_exports));
81304
81362
  const dir = contentDir(flags);
81305
81363
  const journal2 = journalPath2(dir);
81306
- const isContentPackage = existsSync72(join74(dir, "himalaya.content.json"));
81364
+ const isContentPackage = existsSync73(join74(dir, "himalaya.content.json"));
81307
81365
  const hint = isContentPackage ? {} : { hint: "no himalaya.content.json here \u2014 run from the app directory (or pass --content <dir>) so the build board finds this journal" };
81308
81366
  const message = pos.slice(1).join(" ").trim();
81309
81367
  const approvalFlags = ["approve", "done", "drop"].filter((f) => flags[f] !== void 0);
@@ -81455,7 +81513,7 @@ ${handover.hint[0].toUpperCase()}${handover.hint.slice(1)}.`);
81455
81513
  case "test": {
81456
81514
  const dir = contentDir(flags);
81457
81515
  const structuredOutput = flags.json === true || io.isTTY !== true;
81458
- if (existsSync72(join74(dir, "functions"))) {
81516
+ if (existsSync73(join74(dir, "functions"))) {
81459
81517
  const { invokeLocalFunction: invokeLocalFunction2, loadFunctionsApp: loadFunctionsApp2 } = await Promise.resolve().then(() => (init_functions(), functions_exports));
81460
81518
  const loaded = await loadFunctionsApp2(dir);
81461
81519
  const results = [];
@@ -82386,7 +82444,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
82386
82444
  io.error(JSON.stringify(linked));
82387
82445
  return 1;
82388
82446
  }
82389
- if (existsSync72(dest)) {
82447
+ if (existsSync73(dest)) {
82390
82448
  if (!statSync20(dest).isDirectory()) {
82391
82449
  io.error(JSON.stringify({
82392
82450
  error: `${dest} exists and is not a directory`,
@@ -82399,7 +82457,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
82399
82457
  io.error(JSON.stringify({ error: `${dest} already exists and is not empty`, hint: "pass --force to write into it anyway (merging, and naming what it keeps), --replace to restore exactly what was archived, or --out/--name to land somewhere else" }));
82400
82458
  return 1;
82401
82459
  }
82402
- if (replace && occupied && !existsSync72(join74(dest, "himalaya.content.json"))) {
82460
+ if (replace && occupied && !existsSync73(join74(dest, "himalaya.content.json"))) {
82403
82461
  io.error(JSON.stringify({
82404
82462
  error: `${dest} is not a content package \u2014 refusing to --replace it`,
82405
82463
  hint: "--replace only clears a directory that holds himalaya.content.json. Check --out/--name, or pass --force to merge into it instead."
@@ -84162,7 +84220,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
84162
84220
  io.error(JSON.stringify({ error: `patch ${kind} needs --file`, usage: PATCH_USAGE }));
84163
84221
  return 2;
84164
84222
  }
84165
- if (!existsSync72(file)) {
84223
+ if (!existsSync73(file)) {
84166
84224
  io.error(JSON.stringify({ error: `no such file: ${file}` }));
84167
84225
  return 2;
84168
84226
  }
@@ -84885,7 +84943,7 @@ ${label2}`);
84885
84943
  const platform = str9(flags.platform)?.split(",").map((p) => p.trim()).filter(Boolean);
84886
84944
  {
84887
84945
  const distDir = contentDir(flags);
84888
- if (existsSync72(join74(distDir, "himalaya.content.json"))) {
84946
+ if (existsSync73(join74(distDir, "himalaya.content.json"))) {
84889
84947
  const issues = await iconIssues(distDir, flags.strict === true, { requireDeclared: true, requireVerified: true });
84890
84948
  const errors = issues.filter((i) => i.severity === "error");
84891
84949
  for (const i of issues.filter((i2) => i2.severity === "warning")) {
@@ -450,6 +450,14 @@ I want to create a mobile app for my business. Follow this skill:
450
450
  https://apps.wix.com/_api/himi-serve/skill.md
451
451
  ```
452
452
 
453
+ ## Home-screen widgets
454
+
455
+ Read `himi docs widgets` **before** you write a `defineWidget`. A widget is a frozen poster frame:
456
+ its `render` runs once, at build time, over each service's **initial** `state`. Actions, `fetch` and
457
+ Wix data never reach it, and there is no live-data path yet, so a widget written over fetched data
458
+ ships its placeholder. Design it around initial state, constants or a `timeline`, and deep-link the
459
+ tap into the screen that has the live data.
460
+
453
461
  ## Native UX evidence gate
454
462
 
455
463
  Before declaring a multi-screen app ready, run `himi ux audit --content . --strict` and resolve its findings. After driving each available native shell, persist the actual checks with `himi ux record`; record an unavailable platform with its reason rather than claiming coverage.
package/guides/index.json CHANGED
@@ -190,6 +190,12 @@
190
190
  "category": "Workers & SDKs",
191
191
  "file": "cms-collections.md"
192
192
  },
193
+ {
194
+ "id": "widgets",
195
+ "title": "Home-screen widgets (what a widget can show)",
196
+ "category": "Workers & SDKs",
197
+ "file": "widgets.md"
198
+ },
193
199
  {
194
200
  "id": "wix-sdk-overview",
195
201
  "title": "Wix SDK overview (front door)",
@@ -0,0 +1,233 @@
1
+ # Home-screen widgets
2
+
3
+ A widget is a **frozen poster frame** of your app. It is drawn by the operating system on a home
4
+ screen, lock screen or watch face, and opened with a tap. Your worker never runs
5
+ inside it. This page explains where a widget's content comes from, because that is the part that
6
+ surprises people: **a widget shows what your worker's state is at build time, not what the worker
7
+ later fetches.**
8
+
9
+ ## The one rule: a widget renders initial state, once
10
+
11
+ You declare a widget with `defineWidget` in the same worker file as your `defineService`:
12
+
13
+ ```ts
14
+ import { defineWidget } from "@wix/himalaya/widget";
15
+
16
+ defineWidget("<widget id>", {
17
+ family: "ios_home", // which OS surface — see the table below
18
+ size: "small",
19
+ deepLink: "<your app scheme>://screen/<screen id>",
20
+ refresh: { ttlSeconds: 900 },
21
+ render: (services) => ({ /* a component tree of literals */ }),
22
+ });
23
+ ```
24
+
25
+ When your app is built or served, the platform **produces** each widget:
26
+
27
+ 1. It evaluates your worker in a sandbox and records each `defineService`'s **initial `state`**.
28
+ 2. It calls `render(services)` **once, synchronously**, where `services` is
29
+ `{ <service name>: <its initial state> }`.
30
+ 3. It freezes the tree `render` returned and serves it as the widget's snapshot.
31
+
32
+ So, inside that sandbox:
33
+
34
+ | What your worker does | What the widget sees |
35
+ |---|---|
36
+ | `state: { … }` in `defineService` | ✅ exactly that object |
37
+ | Constants and plain functions in the file | ✅ available to `render` |
38
+ | An action (`load`, `refresh`, …), sync or `async` | ❌ never runs — the widget keeps the initial state |
39
+ | `fetch`, Wix data queries, any Wix SDK call | ❌ `fetch` throws; nothing it would return arrives |
40
+ | `setTimeout` / `setInterval` | ❌ never fire |
41
+ | `$service:` / `$item` bindings in the tree | ❌ rejected — the snapshot must be literals |
42
+
43
+ `render` must therefore be **synchronous and side-effect free**: no network, no awaiting, no
44
+ bindings, only **resolved literal values**. Reading the clock is the one allowed dependency.
45
+ `Date.now()` works and is evaluated at produce time, so a value computed from it is as fresh as
46
+ the last produce (see [refresh](#refresh-taps-and-previews)). A `render` that throws, or returns a
47
+ tree with a binding in it, makes that widget fail to produce.
48
+
49
+ ### There is no live-data path yet
50
+
51
+ Data your worker fetches **cannot** reach a widget today. `refresh.pushTopic` exists in the type
52
+ definitions, but nothing consumes it yet: setting it does not make the widget update when data
53
+ changes. Plan the widget around what you know when the app is built — a call to action, a
54
+ constant, or a schedule (see [timelines](#content-whose-future-you-know-live-time-and-timelines)) — and let the
55
+ tap take the user into the app, where the live data is.
56
+
57
+ ## A widget that stays inside the contract
58
+
59
+ This widget shows the studio's name and a fixed call to action, both from initial state. Tapping
60
+ it opens the booking screen, which loads the real schedule.
61
+
62
+ <!-- widget-example: within-contract -->
63
+ ```ts
64
+ import { defineService } from "@wix/himalaya/state";
65
+ import { defineWidget } from "@wix/himalaya/widget";
66
+
67
+ defineService("studio", {
68
+ state: { name: "Harbor Yoga", cta: "Book your next class" },
69
+ });
70
+
71
+ defineWidget("studio.book", {
72
+ family: "ios_home",
73
+ size: "small",
74
+ deepLink: "harboryoga://screen/book",
75
+ refresh: { ttlSeconds: 86400 },
76
+ render: (services) => {
77
+ const studio = services.studio as { name: string; cta: string };
78
+ return {
79
+ id: "root",
80
+ body: { package: "himalaya", type: "VStack", props: { spacing: 4, children: [
81
+ { id: "name", body: { package: "himalaya", type: "Text", props: { text: studio.name, style: "headline" } } },
82
+ { id: "cta", body: { package: "himalaya", type: "Text", props: { text: studio.cta, style: "caption" } } },
83
+ ] } },
84
+ };
85
+ },
86
+ });
87
+ ```
88
+
89
+ It produces exactly this frozen tree:
90
+
91
+ <!-- widget-example: within-contract.payload -->
92
+ ```json
93
+ {
94
+ "id": "root",
95
+ "body": { "package": "himalaya", "type": "VStack", "props": { "spacing": 4, "children": [
96
+ { "id": "name", "body": { "package": "himalaya", "type": "Text", "props": { "text": "Harbor Yoga", "style": "headline" } } },
97
+ { "id": "cta", "body": { "package": "himalaya", "type": "Text", "props": { "text": "Book your next class", "style": "caption" } } }
98
+ ] } }
99
+ }
100
+ ```
101
+
102
+ ## The mistake: a widget over fetched data
103
+
104
+ This is the natural way to write it, and it ships the placeholder:
105
+
106
+ <!-- widget-example: anti-pattern -->
107
+ ```ts
108
+ import { defineService } from "@wix/himalaya/state";
109
+ import { defineWidget } from "@wix/himalaya/widget";
110
+
111
+ defineService("latest", {
112
+ state: { title: "Loading…" },
113
+ actions: {
114
+ async load(s) {
115
+ const res = await fetch("https://example.com/latest.json");
116
+ const { title } = (await res.json()) as { title: string };
117
+ return { ...s, title };
118
+ },
119
+ },
120
+ });
121
+
122
+ defineWidget("latest.story", {
123
+ family: "ios_home",
124
+ size: "small",
125
+ deepLink: "myapp://screen/home",
126
+ refresh: { ttlSeconds: 900 },
127
+ render: (services) => ({
128
+ id: "w",
129
+ body: { package: "himalaya", type: "Text", props: { text: (services.latest as { title: string }).title } },
130
+ }),
131
+ });
132
+ ```
133
+
134
+ `load` never runs at produce time, so `render` reads the initial state and the widget says
135
+ "Loading…" forever:
136
+
137
+ <!-- widget-example: anti-pattern.payload -->
138
+ ```json
139
+ { "id": "w", "body": { "package": "himalaya", "type": "Text", "props": { "text": "Loading…" } } }
140
+ ```
141
+
142
+ Calling the action yourself at the top level (`svc.load()`) does not help either: at produce time
143
+ every action resolves to the initial state.
144
+
145
+ ## Content whose future you know: live time and timelines
146
+
147
+ **Countdowns and clock text: `Text.timeRef`.** A `Text` with
148
+ `timeRef: { at: <epoch seconds>, style: "timer" | "relative" | "date" | "time" | "offset" }`
149
+ shows a time value that overrides the `Text`'s `text`. How live it is depends on the platform:
150
+
151
+ - **iPhone (home, lock, Live Activity)**: the OS ticks it every second, with no refresh and no worker.
152
+ - **Android home screen**: it is formatted only when the widget is redrawn, which the OS
153
+ schedules (typically no more often than every 15 minutes), so do not rely on it to count seconds.
154
+ - **`web_card`**: `timeRef` is ignored and the `Text`'s own `text` is shown, so always set a
155
+ `text` fallback.
156
+
157
+ ```ts
158
+ { id: "starts", body: { package: "himalaya", type: "Text",
159
+ props: { text: "Starts soon", timeRef: { at: Math.floor(Date.now() / 1000) + 3600, style: "timer" } } } }
160
+ ```
161
+
162
+ **Discrete future states: `timeline`.** When you know in advance which *different content* the
163
+ widget should show later — today's agenda, a streak that resets at midnight — add a `timeline`. It returns frozen frames, each with the epoch
164
+ second it becomes current, and the OS advances through them with no network and no worker:
165
+
166
+ ```ts
167
+ // frame(title, time) returns a component tree; DAY_START is today's midnight in epoch seconds.
168
+ defineWidget("studio.agenda", {
169
+ family: "ios_home",
170
+ size: "small",
171
+ deepLink: "harboryoga://screen/schedule",
172
+ refresh: { ttlSeconds: 900 },
173
+ render: () => frame("Morning flow", "7:00"), // also frame 0 for older clients
174
+ timeline: () => [
175
+ { startsAt: DAY_START + 7 * 3600, component: frame("Morning flow", "7:00") },
176
+ { startsAt: DAY_START + 12 * 3600, component: frame("Lunch stretch", "12:00") },
177
+ ],
178
+ });
179
+ ```
180
+
181
+ `timeline` follows the same rule as `render`: synchronous, no network, literals only. It is
182
+ evaluated at produce time, so its frames are only as current as the last produce, and it keeps at
183
+ most 48 frames — never bake a second-by-second countdown into it; that is what `timeRef` is for.
184
+
185
+ ## Families and sizes
186
+
187
+ | `family` | Surface |
188
+ |---|---|
189
+ | `ios_home` | iPhone home screen |
190
+ | `ios_lock` | iPhone lock screen |
191
+ | `android_home` | Android home screen |
192
+ | `web_card` | A card on the web preview |
193
+ | `ios_live_activity` | iPhone Live Activity |
194
+ | `watch_complication` | Watch face complication |
195
+
196
+ `size` is the OS size hint: `small`, `medium` or `large` for home-screen widgets, and `inline`,
197
+ `circular` or `rectangular` for lock-screen and complication slots.
198
+
199
+ One worker can declare several widgets, one per family and size. To give a widget a *different
200
+ layout* from your phone screen while reading the same services, use `defineView` with a
201
+ `widget:<family>` surface instead. It is the same frozen contract, chosen per surface, and
202
+ `size`, `deepLink`, `refresh` and `timeline` are accepted only on those frozen surfaces.
203
+
204
+ **In-car screens are not a widget family.** The car display is authored with
205
+ `defineView("<view id>", { surface: "car", deepLink, refresh, render })`: frozen like a widget,
206
+ with the same rules for `render`. `deepLink` and `refresh` are required, as for a widget; `size` is
207
+ not, because the car lays out its own rows. It never goes through `defineWidget`.
208
+
209
+ ## Refresh, taps and previews
210
+
211
+ - **`refresh.ttlSeconds`** is how long a snapshot may be served before it is produced again. A new
212
+ produce still renders the *initial* state, so a shorter TTL only helps content that is computed
213
+ from the clock at produce time. It does not make fetched data appear.
214
+ - **`refresh.preferredRefreshSeconds`** (optional) is a hint to the OS background-refresh
215
+ scheduler about how often to redraw the widget; when unset, `ttlSeconds` is used. The OS may
216
+ clamp or defer it (Android never redraws more often than every 15 minutes), so treat it as a
217
+ request, not a schedule.
218
+ - **`deepLink`** is where a tap takes the user. Point it at the screen that shows the live version
219
+ of what the widget summarises.
220
+ - **Preview a widget** while you work: `himi run --watch --content .` serves it at
221
+ `GET /v1/widget/<widget id>`, and `GET /v1/widgets` lists every widget the app produces.
222
+ `himi surfaces --measure-widgets` reports which families your package actually produces.
223
+
224
+ ## What `himi validate` checks
225
+
226
+ A widget is **declared** when a worker calls `defineWidget` (or `defineView` with a `widget:`
227
+ surface). If a declared widget fails to produce — `render` threw, returned a binding, or used a
228
+ component its family does not allow — `himi validate` reports `widget-not-produced`. It is a
229
+ warning by default and an **error under `--strict`**, so a package cannot ship a widget it
230
+ declared but that renders nothing.
231
+
232
+ A widget that produces the *placeholder* (the mistake above) is not an error: it renders, it just
233
+ shows initial state. Check the snapshot at `GET /v1/widget/<widget id>` before you ship.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1156.0",
3
+ "version": "0.1158.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": "5d6bcf8d364821a37f27c35e0d3c50376b277479f44cc1e94d5c6729"
70
+ "falconPackageHash": "e2949fb643b50db7dff29f93ca7ee3235cd80cdac4a32d59ba2f89a1"
71
71
  }