@wix/himalaya-cli 0.1291.0 → 0.1292.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.
@@ -7421,8 +7421,22 @@ function looksLikeCardData(field) {
7421
7421
  const joined = tokens.join("");
7422
7422
  return BANNED.has(joined);
7423
7423
  }
7424
+ var ENVELOPE_IMPLIES = {
7425
+ location_background: ["location"],
7426
+ audio_record_background: ["audio_record"]
7427
+ };
7424
7428
  function validateLadderConfig(config) {
7425
7429
  const caps = new Set(config.capabilities ?? []);
7430
+ if (Array.isArray(config.capabilityEnvelope)) {
7431
+ const built = new Set(config.capabilityEnvelope);
7432
+ for (const id of config.capabilityEnvelope) for (const implied of ENVELOPE_IMPLIES[id] ?? []) built.add(implied);
7433
+ const outside = [...caps].filter((id) => !built.has(id)).sort();
7434
+ if (outside.length) {
7435
+ throw new Error(
7436
+ `defineApp: capabilities has ${outside.map((id) => `"${id}"`).join(", ")} but capabilityEnvelope does not. The envelope is what the binary is built for, so no install would carry ${outside.length === 1 ? "its" : "their"} native config. Add ${outside.length === 1 ? "it" : "them"} to capabilityEnvelope (a new binary), or drop ${outside.length === 1 ? "it" : "them"} from capabilities.`
7437
+ );
7438
+ }
7439
+ }
7426
7440
  if (config.commerce && config.commerce.checkout) {
7427
7441
  if (!caps.has("wix_cashier")) {
7428
7442
  throw new Error(
package/dist/cli.mjs CHANGED
@@ -26725,6 +26725,7 @@ __export(capabilities_exports, {
26725
26725
  capabilityInfo: () => capabilityInfo,
26726
26726
  capabilityIssues: () => capabilityIssues,
26727
26727
  contentShell: () => contentShell,
26728
+ envelopeSubsetIssues: () => envelopeSubsetIssues,
26728
26729
  formatCapability: () => formatCapability,
26729
26730
  formatCapabilityList: () => formatCapabilityList,
26730
26731
  formatDistPlan: () => formatDistPlan,
@@ -26936,6 +26937,7 @@ function capabilityIssues(usage, shell, strict, lastDist) {
26936
26937
  });
26937
26938
  }
26938
26939
  }
26940
+ issues.push(...envelopeSubsetIssues(usage.app, usage.envelope));
26939
26941
  const screensByBundle = /* @__PURE__ */ new Map();
26940
26942
  for (const s2 of usage.screens) if (s2.bundle) screensByBundle.set(s2.bundle, [...screensByBundle.get(s2.bundle) ?? [], s2]);
26941
26943
  for (const [bundle, screens] of [...screensByBundle].sort(([a2], [b2]) => a2.localeCompare(b2))) {
@@ -27025,11 +27027,24 @@ function capabilityIssues(usage, shell, strict, lastDist) {
27025
27027
  }
27026
27028
  return issues;
27027
27029
  }
27030
+ function envelopeSubsetIssues(app, envelope) {
27031
+ if (!envelope) return [];
27032
+ const built = expandCapabilities(envelope.map(catalogKey));
27033
+ return [...new Set(app.map(catalogKey))].filter((id) => isKnownCapability(id) && !built.has(id)).sort().map((id) => ({
27034
+ screen: null,
27035
+ pass: "capabilities",
27036
+ severity: "error",
27037
+ capability: id,
27038
+ message: `config.ts capabilities has "${id}" but capabilityEnvelope does not \u2014 the envelope is what the binary is built for, so no install would carry "${id}"'s native config and it could never be turned on. Add "${id}" to capabilityEnvelope (a new binary), or drop it from capabilities.`
27039
+ }));
27040
+ }
27028
27041
  function capabilityConfigFindings(sourceDir, builtDir) {
27029
27042
  const out2 = [];
27030
27043
  const configPath = join39(sourceDir, "config.ts");
27031
27044
  if (existsSync39(configPath)) {
27032
- for (const id of new Set(parseStringArray(readFileSync38(configPath, "utf8"), "capabilities"))) {
27045
+ const configSrc = readFileSync38(configPath, "utf8");
27046
+ const declared = parseStringArray(configSrc, "capabilities");
27047
+ for (const id of new Set(declared)) {
27033
27048
  if (isKnownCapability(id)) continue;
27034
27049
  out2.push({
27035
27050
  screen: null,
@@ -27039,6 +27054,7 @@ function capabilityConfigFindings(sourceDir, builtDir) {
27039
27054
  message: `config.ts capabilities names unknown capability "${clip(id)}".${didYouMean(id)} Run \`himi capabilities\` for the list.`
27040
27055
  });
27041
27056
  }
27057
+ if (/\bcapabilityEnvelope\s*:\s*\[/.test(configSrc)) out2.push(...envelopeSubsetIssues(declared, parseStringArray(configSrc, "capabilityEnvelope")));
27042
27058
  }
27043
27059
  const sourceShell = contentShell(sourceDir);
27044
27060
  const builtShell = contentShell(builtDir);
@@ -130974,6 +130990,7 @@ function manifestContent(m2) {
130974
130990
  publishedBy: _publishedBy,
130975
130991
  attestation: _attestation,
130976
130992
  forcedPastErrors: _forcedPastErrors,
130993
+ allowedBinaryGap: _allowedBinaryGap,
130977
130994
  ...content
130978
130995
  } = m2;
130979
130996
  return content;
@@ -134657,7 +134674,12 @@ async function buildFromLoaded(preloaded, dest, opts = {}) {
134657
134674
  let handlers;
134658
134675
  let minBinaryVersion = null;
134659
134676
  const requiredCapabilities = new Set(config.capabilities ?? []);
134660
- const capabilityUsage = { app: [...config.capabilities ?? []], screens: [], bundles: {} };
134677
+ const capabilityUsage = {
134678
+ app: [...config.capabilities ?? []],
134679
+ ...Array.isArray(config.capabilityEnvelope) ? { envelope: [...config.capabilityEnvelope] } : {},
134680
+ screens: [],
134681
+ bundles: {}
134682
+ };
134661
134683
  const noteScreenCapabilities = (screenId, bundle, declared) => {
134662
134684
  const ids = Array.isArray(declared) ? declared.filter((c2) => typeof c2 === "string") : [];
134663
134685
  capabilityUsage.screens.push({ screenId, bundle, declared: ids });
@@ -136646,7 +136668,8 @@ var init_push_errors = __esm({
136646
136668
  "would_regress",
136647
136669
  "not_carried_by_patch",
136648
136670
  "authoring_unavailable",
136649
- "push_failed"
136671
+ "push_failed",
136672
+ "binary_capability_gap"
136650
136673
  ];
136651
136674
  UX_CODES = {
136652
136675
  absent: "ux_evidence_absent",
@@ -136972,7 +136995,7 @@ function regressionDiff(base, live3, touched) {
136972
136995
  const b2 = base.authoringDocs?.[name]?.sha256 ?? null;
136973
136996
  if (l2 !== b2) rows.push({ item: `doc:${name}`, live: l2, base: b2, effect: `${name} reverts to ${base.releaseId}` });
136974
136997
  }
136975
- const PROVENANCE = /* @__PURE__ */ new Set(["releaseId", "gitSha", "shellSha", "label", "notes", "publishedAt", "publishedBy", "attestation", "forcedPastErrors"]);
136998
+ const PROVENANCE = /* @__PURE__ */ new Set(["releaseId", "gitSha", "shellSha", "label", "notes", "publishedAt", "publishedBy", "attestation", "forcedPastErrors", "allowedBinaryGap"]);
136976
136999
  const REPORTED_IN_DETAIL = /* @__PURE__ */ new Set(["app", "screens", "bundles", "tokensHash", "appConfigHash", "iconHash", "minBinaryVersion", "assets", "authoringDocs"]);
136977
137000
  for (const key2 of /* @__PURE__ */ new Set([...Object.keys(live3), ...Object.keys(base)])) {
136978
137001
  if (PROVENANCE.has(key2) || REPORTED_IN_DETAIL.has(key2) || moves.has(key2)) continue;
@@ -137016,7 +137039,7 @@ function notCarriedDiff(local, base) {
137016
137039
  rows.push({ slice: "icon", local: String(local.iconHash ?? "none").slice(0, 12), base: String(base.iconHash ?? "none").slice(0, 12), effect: "the launcher icon stays as the base published it" });
137017
137040
  }
137018
137041
  const CARRIED = /* @__PURE__ */ new Set(["screens", "bundles", "tokensHash", "appConfigHash"]);
137019
- const PROVENANCE = /* @__PURE__ */ new Set(["releaseId", "gitSha", "shellSha", "label", "notes", "publishedAt", "publishedBy", "attestation", "forcedPastErrors", "app", "appSourceHash"]);
137042
+ const PROVENANCE = /* @__PURE__ */ new Set(["releaseId", "gitSha", "shellSha", "label", "notes", "publishedAt", "publishedBy", "attestation", "forcedPastErrors", "allowedBinaryGap", "app", "appSourceHash"]);
137020
137043
  const REPORTED_IN_DETAIL = /* @__PURE__ */ new Set(["assets", "minBinaryVersion", "iconHash"]);
137021
137044
  const comparable2 = (key2, value) => key2 === "widgets" ? stableWidgets(value) : value;
137022
137045
  for (const key2 of /* @__PURE__ */ new Set([...Object.keys(local), ...Object.keys(base)])) {
@@ -165641,6 +165664,7 @@ var BOOLEAN_FLAGS = /* @__PURE__ */ new Set([
165641
165664
  "live",
165642
165665
  "accept-regressions",
165643
165666
  "accept-partial",
165667
+ "allow-binary-gap",
165644
165668
  "no-rebind",
165645
165669
  "help",
165646
165670
  "static",
@@ -166026,6 +166050,9 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
166026
166050
  (decodes the token; no round trip), so it works offline.
166027
166051
  himi push [--content <dir>] [--draft] [--force] [--strict] [--label <n>] [--notes <t>] [--visibility public|unlisted]
166028
166052
  lint, then publish (default --draft); refuses on errors unless --force
166053
+ the plane refuses a release declaring a permission-bearing capability
166054
+ (camera, location, push, \u2026) the app's last \`himi dist\` lacks
166055
+ (binary_capability_gap) unless --allow-binary-gap
166029
166056
  the published slug is namespaced per owner (<ownerPrefix>.<name>),
166030
166057
  composed offline from the token; himalaya.content.json is not rewritten.
166031
166058
  off-repo defaults to the public wix target; in-repo defaults to local.
@@ -166572,7 +166599,7 @@ var COMMANDS2 = [
166572
166599
  { name: "base44", summary: "read a BASE44 app into an app book (book.json + BOOK.md) for the agent building a Himi app from it \u2014 the Base44 counterpart of `site analyze`, READ-ONLY. `analyze --url <published app>` is the VISITOR pass: the app id, name, favicon and colours from the published SPA, its router paths, and screenshots + look tokens from the Playwright crawl. `analyze --app-id <id>` adds the OWNER pass and needs a session: the project source under <out>/source/ (the read half of `base44 eject` \u2014 GET api/apps/<id>/eject \u2014 through the same deny-list and secret scan `himi push` uses, widened by the book's token shapes; `eject` itself is never run because it CREATES a copy of the app), every entity's JSON schema, required fields and RLS rules with up to --sample redacted rows (default 5, 0 = off), backend functions, agents, connectors (names and scopes only, never tokens), secret NAMES (never values), auth visibility + providers, and the route \u2192 component \u2192 entities map from the source router. Every chapter it could not read is listed in `gaps` rather than failing the run. `login` runs Base44's device sign-in and stores the session in ~/.himi/base44/session.json (never the repo; --from-base44-cli imports `npx base44 login`'s session instead); BASE44_ACCESS_TOKEN in the environment wins, for CI. `whoami` prints who is signed in (never the token); `logout` removes the stored session. `auth-check --url <tenant> --ios-app-id <PREFIX.bundle.id>` and/or `--android-package <package> --android-sha256 <fingerprint>` checks public native-login associations and prints missing entries; it never reads an owner token. --json prints the book itself. himi docs base44-app-context", required: "analyze (--url <https://\u2026> | --app-id <id>) [--out .himi/base44-book] [--sample <n>] [--json] | login [--from-base44-cli] | whoami | logout | auth-check --url <https://tenant> [--ios-app-id <PREFIX.bundle.id>] [--android-package <package> --android-sha256 <fingerprint>]" },
166573
166600
  { 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>]" },
166574
166601
  { 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]" },
166575
- { 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; a repeat draft's postcard adds what changed since the last one (\"Since the last draft: the home screen changed.\", from the release manifests' screen hashes, remembered in .himi/last-draft.json); 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)" },
166602
+ { 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; a repeat draft's postcard adds what changed since the last one (\"Since the last draft: the home screen changed.\", from the release manifests' screen hashes, remembered in .himi/last-draft.json); 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. The plane REFUSES (409 binary_capability_gap) a release declaring a PERMISSION-BEARING capability (iOS usage string or entitlement, Android permission \u2014 camera, location, push, \u2026) that the app's last successful `himi dist` was not built with: an OTA push cannot add an OS permission, and on iOS asking for one whose purpose string is missing kills the app. Run `himi dist` first, or pass `--allow-binary-gap` when every installed binary already carries it \u2014 recorded on the release as `allowedBinaryGap`. A gap in a permission-free capability (share, clipboard, \u2026) stays the `needsBinary` advice.", required: "[--content <dir>] [--draft] [--live] [--force] [--strict] [--label] [--notes] [--visibility public|unlisted] [--allow-binary-gap] [--no-claim] [--account-id <id>] [--only <selector>] [--from-release <id>] [--accept-regressions] [--accept-partial] [--app <slug>] (--control-token)" },
166576
166603
  { name: "pack", summary: 'write the app as a PORTABLE .himi package \u2014 one self-contained file holding every screen, Tier-5 bundle, token, asset and the app-config. Packages are live by default: they boot bundled content offline, then Himi Player follows its baked-in production ring. `--snapshot` makes a frozen reproducible artifact. Package-supplied origins are never trusted. --format dir writes the same tree unpacked, for static hosting. --release-dir packs a release that is already built on disk (needs --app) instead of rebuilding from source. `--dev` ALSO packs the app\'s own source archive into the container (`source.zip`, header `contents: "dev"`), so the package can be handed to someone who then keeps building \u2014 collected through the same deny-list and secret scan `himi push` uses, and refused if a credential is found. A dev package IS the codebase: it is never the default, it is always labelled in the output, the web host will not open one, and it is archive-only (`--dev --format dir` is refused, because a reader that predates the source entry drops it before hashing a directory and would call the package tampered). Restore one with `himi import`.', required: "[--content <dir>] [--out <file|dir>] [--format zip|dir] [--name <title>] [--snapshot] [--dev] [--strict] [--force] | --release-dir <dir> --app <a>" },
166577
166604
  { name: "import", summary: "turn a `himi pack --dev` package back into an EDITABLE content package \u2014 the file counterpart of `himi pull`, needing no serve plane, no account and no network. Refuses an invalid package before trusting its source (the content hash is recomputed over every entry on open, so the archive is provably the one that was packed), and refuses a RELEASE package by naming the absence \u2014 it carries built output and no source \u2014 rather than reporting a missing file. `--name` re-substitutes the app identity through the same machinery `himi init --template` and `himi pull --name` use, so the restored tree is a NEW app rather than a second copy of the same one, and a name that also appears inside a screen id (`feed` in `feed_home`) is left alone. A non-empty destination needs `--force` and a merge NAMES what it kept (`left: [...]`), the same contract `himi pull --force` has; a shape clash is refused whole before anything is written. There is no `--replace`: a file you were handed is not the plane's per-release archive.", required: "<package.himi> [--out <dir>] [--name <new>] [--force]" },
166578
166605
  { name: "pull", summary: "restore an app's SOURCE from the serve plane into an editable content package \u2014 the other half of `himi push`, which archives the content package on every publish. Owner or editor only: a viewer may download the RUNNING app (`/v1/owner/package`), not the codebase it was built from. A PAST release is restorable, not only the live one \u2014 the archive is content-addressed per release, so it has none of the live-single-slot problem that makes `himi pack` current-release-only. Two limits: pruning keeps the current release plus the newest ~20 ledger entries, and a release id identifies OTA content rather than source, so a source-only change republishes under the same id and replaces that id's archive. A release published before source archiving shipped, or pushed with `--no-source`, answers `app_source_not_found` and says to push again. `--name` re-substitutes the app identity through the same machinery `himi init --template` uses, so the restored tree is a NEW app rather than a second copy of the same one, and a name that also appears inside a screen id (`feed` in `feed_home`) is left alone. A NON-EMPTY destination is refused unless you say which you mean: `--force` merges and reports \u2014 it writes the archive over what is there, keeps files the release does not carry, and NAMES every one of them as `left: [...]` so a tree that matches no release is never silent (#2437); `--replace` restores exactly what was archived, removing those files first (`removed: [...]`) and implying `--force`. `--replace` refuses a directory holding no `himalaya.content.json`, so a mistyped `--out` cannot clear an unrelated tree, and the only paths it can remove are the ones a push would have archived \u2014 `.git/`, `node_modules/`, `dist/`, `.env*` and key material are out of its reach by construction.", required: "<app> [--release <id>] [--out <dir>] [--name <new>] [--force|--replace] [--target <t>]" },
@@ -166589,7 +166616,7 @@ var COMMANDS2 = [
166589
166616
  { name: "deeplinks", summary: "nav map + web/player deeplinks for a release (GET /v1/preview/\u2026/deeplinks)", required: "--app --release" },
166590
166617
  { name: "logs", summary: "worker console read-back (GET /v1/telemetry/logs); --follow tails via the seq cursor", required: "--app [--release] [--screen] [--level] [--follow]" },
166591
166618
  { name: "promote", summary: "flip a stored draft LIVE (POST /v1/admin/release/activate); --ring also points a rollout ring at it in the same command (POST /v1/admin/promote), which ring-based clients need in order to resolve at all \u2014 without it they stay unresolved and the warning goes to stderr; --percent sets that ring's rollout share; a release pushed past errors needs an explicit --force override", required: "--app --release [--ring <ring>] [--percent 0-100] [--force] (--control-token)" },
166592
- { name: "patch", summary: 'change ONE item of a published app \u2014 a screen descriptor, a Tier-5 bundle, tokens, or the app-config baseline \u2014 from a RAW FILE, with no content package and no build. The low-level door: it does not lint your package, because there is no package here; `himi push --only <selector>` is the same edit with the full build + lint gate and is what to reach for when the package is in hand. Derives a new release from the app\'s current LIVE release, or from `--from-release <id>`. An explicit `--draft` or `--live` is REQUIRED \u2014 a patch has no default blast radius \u2014 and `--notes` is required with `--live` on a screen or app-config patch, because off-repo `gitSha` is "dev" and the ledger notes are then the only durable record of bytes that correspond to no commit. A branch does not merge: activating one derived from an older release reverts every item the patch does not move, so that case is refused until `--accept-regressions`. A bundle patch must repoint a screen (`--for-screen`) or the bytes land under a manifest key nothing reads. It cannot ADD a screen (screens are git-only, RFC 0012), never claims a slug \u2014 the app must already be published \u2014 and carries no attestation, source archive, assets, icon or minBinaryVersion, each named in the output.', required: "screen <id> --file <descriptor.json> [--bundle <sha256|none>] | bundle --file <bundle.js> (--for-screen <id>[,<id>] | --no-rebind) | tokens --file <tokens.json> | app-config --file <app-config.json> ; (--draft|--live) --app <a> [--from-release <id>] [--accept-regressions] [--label <n>] [--notes <t>] [--force] [--target <t>] (--control-token)" },
166619
+ { name: "patch", summary: 'change ONE item of a published app \u2014 a screen descriptor, a Tier-5 bundle, tokens, or the app-config baseline \u2014 from a RAW FILE, with no content package and no build. The low-level door: it does not lint your package, because there is no package here; `himi push --only <selector>` is the same edit with the full build + lint gate and is what to reach for when the package is in hand. Derives a new release from the app\'s current LIVE release, or from `--from-release <id>`. An explicit `--draft` or `--live` is REQUIRED \u2014 a patch has no default blast radius \u2014 and `--notes` is required with `--live` on a screen or app-config patch, because off-repo `gitSha` is "dev" and the ledger notes are then the only durable record of bytes that correspond to no commit. A branch does not merge: activating one derived from an older release reverts every item the patch does not move, so that case is refused until `--accept-regressions`. A bundle patch must repoint a screen (`--for-screen`) or the bytes land under a manifest key nothing reads. It cannot ADD a screen (screens are git-only, RFC 0012), never claims a slug \u2014 the app must already be published \u2014 and carries no attestation, source archive, assets, icon or minBinaryVersion, each named in the output.', required: "screen <id> --file <descriptor.json> [--bundle <sha256|none>] | bundle --file <bundle.js> (--for-screen <id>[,<id>] | --no-rebind) | tokens --file <tokens.json> | app-config --file <app-config.json> ; (--draft|--live) --app <a> [--from-release <id>] [--accept-regressions] [--label <n>] [--notes <t>] [--force] [--allow-binary-gap] [--target <t>] (--control-token)" },
166593
166620
  { name: "flags", summary: "read and change the app's OPERATIONAL flags (RFC 0013) \u2014 `list` prints what the DEVICE door serves, `set <key> --definition` creates or replaces one, `ramp <key>` moves its rollout share or kills it, `rm <key>` deletes it and echoes the definition back so the removal is reversible. Every write is followed by a read of the same door the device uses, and the answer is reported as `servedNow`: flags are stored in the app's git-committed flags.json, so a control plane with no checkout accepts a write it will never serve.", required: "list|set|ramp|rm [<key>] [--definition <json|@file>] [--percent 0-100] [--enabled true|false] [--app <a>] (--control-token)" },
166594
166621
  { name: "visibility", summary: "read (no value) or set whether the public registry (/v1/apps + the Himi Player picker) LISTS the app. `public` lists it for everyone; `unlisted` hides it from listings while direct reads by app id \u2014 deep links, previews \u2014 keep working. App from --app or the content dir. Setting it needs no republish.", required: "[public|unlisted] [--app <a>] [--content <dir>] (--control-token)" },
166595
166622
  { name: "dist", summary: "build an INSTALLABLE QA binary (signed IPA / APK) on the Wix Job Service Mac pool and return an install link \u2014 the serve plane brokers the job, the pool builds it. WAITS for the build by default (~4 min) and prints the install link; `--no-wait` returns a jobId immediately instead. Omitting `--release` builds whatever is LIVE for the app. A named draft is refused unless `--allow-draft` makes that exceptional build explicit. `status <jobId>` / `watch <jobId>` / `list --app <a>` / `cancel <jobId>` inspect and stop builds (`watch` reports up front whether the job mints its own signing identity or borrows one, so a doomed iOS build is cancellable in seconds rather than after the archive). Off-repo, name a target \u2014 `--target wix` for a self-serve Wix login or `--target bo` for the internal employee path (the in-repo default is a local dev plane that isn't reachable off-repo); self-serve callers may build their OWN apps under a tighter budget (2 in flight / 5 per 24h), while a manage-only owner-role credential is refused. `--shell` picks the native host to sign into (branded-lite | owner-lite | brandy | base44 | player | demo-app) and defaults to the content package's himalaya.content.json `shell` (templates carry the one they were written for), else branded-lite. The per-app shells ADD the release's declared capabilities to the binary, while Himi Player and demo-app refuse a release declaring one they lack; `--plan` previews it without building; `brandy` is the site shell \u2014 the customer's own website as native-framed tabs. `--signing` defaults to `wix-managed` \u2014 the Wix Apple account mints a per-app signing identity for the build, so the caller provisions no Apple credentials of their own; `--signing byo --site <id>` reuses an existing customer ad-hoc profile and certificate for iOS; requires an owned site and a completed personal signing binding. `--android-signing` is the Android counterpart and a SEPARATE axis (a keystore, not an Apple account): `qa-keystore` (default \u2014 the repo's committed QA key, identical on every pool machine, so a build installs over the previous one), `wix-keystore` (the Wix key the pool machine carries; the build FAILS naming the paths searched if it carries none, rather than quietly signing with something else), or `debug` (the old per-machine key \u2014 QA must uninstall any previous build first). NOT the normal way to ship: screens, Tier-5 bundles, tokens and app-config all go over the air with `himi push` in seconds \u2014 a binary trip is only needed for a NATIVE delta (a new SDUI component, a new declared capability/entitlement, a new bundle id, or a minBinaryVersion floor bump). Refuses to build an app whose launcher icon is missing, a placeholder, or one a store would reject \u2014 the one thing a binary carries that no later OTA push can fix; `--force` builds anyway", required: "[--app <a>] [--platform ios,android] [--release <id>] [--allow-draft] [--target <t>] [--shell branded-lite|owner-lite|brandy|base44|player|demo-app] [--signing wix-managed|byo] [--site <id>] [--android-signing qa-keystore|wix-keystore|debug] [--borrow-ios-id <bundleId>] [--no-wait] [--force] [--plan] | status <jobId> [--target <t>] | watch <jobId> [--target <t>] [--interval <ms>] | list --app <a> [--target <t>] | cancel <jobId> [--target <t>]" },
@@ -168598,11 +168625,27 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
168598
168625
  }
168599
168626
  let r2;
168600
168627
  try {
168601
- r2 = await client2().releasePatch(app, base.releaseId, plan.patch, draft, opts.meta, opts.force);
168628
+ const patchMeta = flags["allow-binary-gap"] === true ? { ...opts.meta, allowBinaryGap: true } : opts.meta;
168629
+ r2 = await client2().releasePatch(app, base.releaseId, plan.patch, draft, patchMeta, opts.force);
168602
168630
  } catch (e2) {
168603
168631
  io2.error(JSON.stringify(pushFailure2("serve_unreachable", { ok: false, patched: false, error: "serve_unreachable", app, message: e2.message })));
168604
168632
  return 1;
168605
168633
  }
168634
+ if (!r2.ok && r2.status === 409 && r2.json?.error === "binary_capability_gap") {
168635
+ const j2 = r2.json;
168636
+ print(pushFailure2("binary_capability_gap", {
168637
+ ok: false,
168638
+ patched: false,
168639
+ app,
168640
+ baseReleaseId: base.releaseId,
168641
+ error: "binary_capability_gap",
168642
+ missing: j2.missing ?? [],
168643
+ dists: j2.dists ?? [],
168644
+ message: j2.message,
168645
+ hint: j2.hint ?? `run \`himi dist\` first, or re-run with --allow-binary-gap`
168646
+ }));
168647
+ return 1;
168648
+ }
168606
168649
  if (!r2.ok) {
168607
168650
  const body = r2.json ?? {};
168608
168651
  const hint = body.error === "preview_token_unconfigured" ? `this serve plane cannot mint preview tokens, and a patch will not fall back to the older per-item routes: those always patch the CURRENT LIVE release, so a --from-release you chose would be silently ignored. Use a target that has it (--target wix).` : body.error === "missing_doc" ? `release ${base.releaseId} predates content-addressed documents, so it cannot be activated item-by-item \u2014 run a full \`himi push\` once, then patch.` : body.error === "item_too_large" ? `${body.item ?? "an item"} is ${body.bytes ?? "?"} bytes against a ${body.limit ?? "?"} byte cap. Descriptors this large usually carry inlined data \u2014 move it into the Tier-5 worker's state.` : body.error === "forced_past_errors" ? `${base.releaseId} was published past validation errors and a branch inherits that marker \u2014 review the evidence, then re-run with --force.` : void 0;
@@ -171160,7 +171203,8 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
171160
171203
  ...str9(flags.notes) ? { notes: str9(flags.notes) } : {},
171161
171204
  ...visibility ? { visibility } : {},
171162
171205
  ...attestation ? { attestation } : {},
171163
- ...forcedPastErrors ? { forcedPastErrors: true } : {}
171206
+ ...forcedPastErrors ? { forcedPastErrors: true } : {},
171207
+ ...flags["allow-binary-gap"] === true ? { allowBinaryGap: true } : {}
171164
171208
  };
171165
171209
  const publishBodyFor = (slug) => ({
171166
171210
  ...payload,
@@ -171225,6 +171269,20 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
171225
171269
  })));
171226
171270
  return 1;
171227
171271
  }
171272
+ if (!r2.ok && r2.status === 409 && r2.json?.error === "binary_capability_gap") {
171273
+ const j2 = r2.json;
171274
+ print(pushFailure2("binary_capability_gap", {
171275
+ ok: false,
171276
+ app,
171277
+ pushed: false,
171278
+ error: "binary_capability_gap",
171279
+ missing: j2.missing ?? [],
171280
+ dists: j2.dists ?? [],
171281
+ message: j2.message,
171282
+ hint: j2.hint ?? "run `himi dist` first, or re-run `himi push --allow-binary-gap`"
171283
+ }));
171284
+ return 1;
171285
+ }
171228
171286
  if (!r2.ok) {
171229
171287
  const authHint = pushAuthHint(r2.status, pushToken, pushTarget, !!ownerIssue);
171230
171288
  const hint = authHint ?? releaseIdExistsHint(r2.status, r2.json);
@@ -55,6 +55,7 @@ keeps its existing fields and adds the code:
55
55
  | `serve_unreachable` | A partial-push upload could not reach the publishing service. |
56
56
  | `no_serve_plane` | A loopback service answering a partial push is not a himi publishing service. |
57
57
  | `upload_http_error` | The upload returned a non-success HTTP status. `status` and the original `body` remain available; server errors never replace this CLI code. |
58
+ | `binary_capability_gap` | The publish was refused (409): it declares a permission-bearing capability (iOS usage string or entitlement, Android permission) the app's last successful `himi dist` was not built with. `missing` names the ids; `dists` lists each platform's last dist that lacks them (`platform`, `jobId`, `missing`). The same code comes from `himi push --only` and `himi patch`. Run `himi dist` first, or re-run with `--allow-binary-gap`. |
58
59
  | `claim_failed` | Auto-claim returned a fatal refusal. Existing server fields and `status` remain available. |
59
60
  | `invalid_flag_value` | A flag violates the CLI flag grammar. |
60
61
  | `invalid_selector` | `--only` is missing a selector or its selector syntax is invalid. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1291.0",
3
+ "version": "0.1292.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": [
@@ -70,5 +70,5 @@
70
70
  "artifactId": "himalaya-cli"
71
71
  }
72
72
  },
73
- "falconPackageHash": "0e0aab5246aa6f8b622e65ac490679cc26e226218f468ec3c3825057"
73
+ "falconPackageHash": "f2cddbdb94ceba1e08a45fef82f62b1e3debb12f1c9fa20e0984d0c3"
74
74
  }