@wix/himalaya-cli 0.972.0 → 0.974.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.mjs +481 -23
  2. package/package.json +2 -2
package/dist/cli.mjs CHANGED
@@ -5290,6 +5290,7 @@ function resolveTranslation(value, catalog, sourceCatalog) {
5290
5290
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
5291
5291
  var init_bindings = __esm({
5292
5292
  "../../core/ts/src/bindings.ts"() {
5293
+ "use strict";
5293
5294
  BINDING_SIGILS = ["service", "flag", "item", "t"];
5294
5295
  BINDING_SIGIL_RE = new RegExp(
5295
5296
  `^\\$(?:${BINDING_SIGILS.filter((s) => s !== "t").join("|")})\\b|^\\$t:`
@@ -39643,7 +39644,7 @@ async function pinnedFetch(url, init, addresses, maxResponseBytes) {
39643
39644
  const body = init.body === void 0 || init.body === null ? void 0 : new Uint8Array(await new Response(init.body).arrayBuffer());
39644
39645
  const headers2 = Object.fromEntries(new Headers(init.headers).entries());
39645
39646
  const request = url.protocol === "https:" ? httpsRequest : httpRequest;
39646
- const hostname = url.hostname.replace(/^\[|\]$/g, "");
39647
+ const hostname2 = url.hostname.replace(/^\[|\]$/g, "");
39647
39648
  return await new Promise((resolve43, reject) => {
39648
39649
  let settled = false;
39649
39650
  const finish2 = (fn) => {
@@ -39653,13 +39654,13 @@ async function pinnedFetch(url, init, addresses, maxResponseBytes) {
39653
39654
  };
39654
39655
  const req = request({
39655
39656
  protocol: url.protocol,
39656
- hostname,
39657
+ hostname: hostname2,
39657
39658
  port: url.port || (url.protocol === "https:" ? 443 : 80),
39658
39659
  path: `${url.pathname}${url.search}`,
39659
39660
  method: init.method ?? "GET",
39660
39661
  headers: headers2,
39661
39662
  lookup: (_hostname, _options, callback) => callback(null, address, isIP(address)),
39662
- ...url.protocol === "https:" && isIP(hostname) === 0 ? { servername: hostname } : {}
39663
+ ...url.protocol === "https:" && isIP(hostname2) === 0 ? { servername: hostname2 } : {}
39663
39664
  }, (response) => {
39664
39665
  const chunks = [];
39665
39666
  let total = 0;
@@ -48233,9 +48234,9 @@ function isFetchableFontUrl(raw) {
48233
48234
  if (host.includes(":")) return !isPrivateV6(host);
48234
48235
  return true;
48235
48236
  }
48236
- function resolveAll(hostname) {
48237
+ function resolveAll(hostname2) {
48237
48238
  return new Promise((resolve43, reject) => {
48238
- dnsLookup(hostname, { all: true, verbatim: true }, (err, addresses) => {
48239
+ dnsLookup(hostname2, { all: true, verbatim: true }, (err, addresses) => {
48239
48240
  if (err) reject(err);
48240
48241
  else resolve43(addresses);
48241
48242
  });
@@ -51211,9 +51212,9 @@ var init_site_oauth_client = __esm({
51211
51212
 
51212
51213
  // src/cli.ts
51213
51214
  import { mkdtempSync as mkdtempSync4, readFileSync as readFileSync65, mkdirSync as mkdirSync25, writeFileSync as writeFileSync23, existsSync as existsSync54, rmSync as rmSync9, readdirSync as readdirSync25, statSync as statSync12, lstatSync as lstatSync6, accessSync, constants } from "node:fs";
51214
- import { execFileSync as execFileSync7 } from "node:child_process";
51215
- import { createHash as createHash24 } from "node:crypto";
51216
- import { tmpdir as tmpdir5 } from "node:os";
51215
+ import { execFileSync as execFileSync7, spawnSync as spawnSync5 } from "node:child_process";
51216
+ import { createHash as createHash24, randomUUID as randomUUID3 } from "node:crypto";
51217
+ import { tmpdir as tmpdir5, homedir as homedir7, hostname } from "node:os";
51217
51218
  import { join as join51, resolve as resolve42, relative as relative8, basename as basename11, dirname as dirname30 } from "node:path";
51218
51219
  import { fileURLToPath as fileURLToPath24 } from "node:url";
51219
51220
 
@@ -54579,6 +54580,19 @@ ${overview}
54579
54580
 
54580
54581
  ${loop}
54581
54582
 
54583
+ ## Stay on a current CLI
54584
+
54585
+ A stale global install is the single most common cause of "this doesn't work off-repo": the old
54586
+ CLI mislabels its own age as a platform limit, and the error it prints sends you hunting the
54587
+ wrong thing. himi now notices \u2014 when the running build is over a week old it updates itself and
54588
+ re-runs your command (at a terminal it asks first), so in an unattended run this takes care of
54589
+ itself. \`himi upgrade\` does it on demand, and \`himi doctor --json\` reports
54590
+ \`versions.cli\` \u2014 \`running\`, \`latest\`, \`ageDays\`, \`status\`.
54591
+
54592
+ Before concluding that a command is broken or a capability is missing, check you are not on an
54593
+ old build. If a \`--json\` envelope carries an \`upgrade\` key, that is himi telling you this
54594
+ directly. Set \`HIMI_NO_UPDATE_CHECK=1\` in images that must not self-update.
54595
+
54582
54596
  ## Import paths (in-repo vs off-repo)
54583
54597
 
54584
54598
  Off-repo content packages import \`@wix/himalaya/<pkg>\`. In-repo Himalaya sources import
@@ -55332,6 +55346,12 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
55332
55346
  stored control token). --dpx also runs a live
55333
55347
  supported "dpx auth status". --json for agents; an
55334
55348
  unreachable internal plane carries code:"needs_vpn".
55349
+ himi upgrade [--json] install the newest published CLI and say what moved.
55350
+ himi also does this ITSELF when the running build is
55351
+ over a week old \u2014 unattended it updates and re-runs
55352
+ your command, at a terminal it asks first. Turn that
55353
+ off with HIMI_NO_UPDATE_CHECK=1 (or per-run with
55354
+ --skip-version-check) and use this command instead.
55335
55355
 
55336
55356
  Learn (works offline \u2014 no serve stack needed):
55337
55357
  himi guide feature highlights + the headless dev loop + topics
@@ -55420,6 +55440,7 @@ var COMMANDS = [
55420
55440
  { name: "notify", summary: "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: "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]" },
55421
55441
  { 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` reads a site into a business-book folder (REST + Playwright, GET-only); `site function` list/get/put/publish/invokes that site's Velo HTTP functions and is not `himi functions` (serve-plane jobs). `--apply` bakes into a brandable shell; `--shell` takes the same names `himi dist --shell` does and defaults to branded-lite, and a shell that ships no prepare:customer is refused by name rather than half-built. It never overwrites any shell's committed apps/<shell>/provisioning.json.", required: "create <name> [--type wix|headless|branded-app|vibe] [--template <guid>] [--verticals stores,bookings,\u2026] [--folder <guid>] [--redirect-uri <uri>] [--no-oauth-app] [--write-provisioning [path]] [--site-url <url>] [--json] --yes | list | client ensure|list --site <msid> [--name <name>] [--redirect-uri <uri>] [--new] [--write-provisioning [path]] [--site-url <url>] [--json] --yes | analyze --client-id <id> --url <https> [--site <msid>] [--out <dir>] [--redirect-uri <uri>] [--apply] [--shell " + SHELL_ENUM + "] | function list|get|put|publish|invoke [--site <msid>] [--url <https>] [--method GET|POST|PUT|DELETE] [--source <file>] [--body <json>] [--publish] [--invoke]" },
55422
55442
  { 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>]" },
55443
+ { 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]" },
55423
55444
  { name: "push", summary: "lint + build + publish a release (default --draft: servable via ?release= but NOT live). REFUSES to publish on any error unless --force; --strict promotes warnings. Prints preview + agent-context links. The published slug is NAMESPACED per owner \u2014 `<ownerPrefix>.<name>`, composed offline from the control token's ownerPrefix claim, so the CLI and the server derive the identical string and himalaya.content.json is NOT rewritten (a legacy token with no ownerPrefix publishes the declared name unchanged). An app claimed BEFORE namespacing is stored under its FLAT slug, so the claim route answers `alreadyYours` naming that slug and the push adopts it \u2014 reported as `adoptedSlug`, since it is the one case where the published `app` differs from what the declared name composes to. `--no-claim` skips the route that knows this, so a pre-namespacing app pushed with it still fails 403. --visibility public|unlisted decides whether /v1/apps + the Himi Player picker LIST the app (also declarable as `visibility` in himalaya.content.json; omitted, a brand-new app defaults to unlisted \u2014 share by link \u2014 and an existing setting is kept). --account-id names which account owns the app when your Wix session resolves to several (the auto-claim of a brand-new slug cannot guess \u2014 without it a multi-account caller is told to name one). An unforced push of a release that carries SCREENS also refuses when `.himi/ux-audit.json` holds no mobile-UX drive recorded against that release id, or holds one that FAILED (#2955) \u2014 a published draft IS the owner-private preview a customer is handed; `himi ux record` clears it and `--force` overrides it, which the success envelope then says out loud. `--only <selector>` publishes ONLY the named items as a release derived from the app's live release (or `--from-release <id>`), running the identical build + lint gate and simply publishing less; it requires an explicit `--draft` or `--live` because a partial publish has no safe default, never claims a slug, and carries neither attestation nor a source archive (both named in the output). Selectors are comma-separated in ONE `--only`: `changed` (every item whose bytes differ from the base), `screen:<id|glob>`, `bundle:<name>`, `tokens`, `app-config` \u2014 a selected screen drags its logic bundle and a selected bundle drags the screens that reference it, or the bytes land under a key nothing reads.", required: "[--content <dir>] [--draft] [--live] [--force] [--strict] [--label] [--notes] [--visibility public|unlisted] [--no-claim] [--account-id <id>] [--only <selector>] [--from-release <id>] [--accept-regressions] [--accept-partial] [--app <slug>] (--control-token)" },
55424
55445
  { 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>" },
55425
55446
  { 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]" },
@@ -55471,6 +55492,56 @@ function compareVersions2(a, b) {
55471
55492
  }
55472
55493
  return 0;
55473
55494
  }
55495
+ var PUBLIC_NPM_REGISTRY = "https://registry.npmjs.org";
55496
+ var WIX_NPM_MIRROR = "https://npm.dev.wixpress.com";
55497
+ var CLI_PACKAGE_PATH = "@wix%2fhimalaya-cli";
55498
+ var TRUSTED_REGISTRIES = /* @__PURE__ */ new Set([PUBLIC_NPM_REGISTRY, WIX_NPM_MIRROR]);
55499
+ function normalizeRegistry(url) {
55500
+ return url?.trim().replace(/\/+$/, "") || void 0;
55501
+ }
55502
+ function isTrustedRegistry(url) {
55503
+ const normalized = normalizeRegistry(url);
55504
+ return normalized !== void 0 && TRUSTED_REGISTRIES.has(normalized);
55505
+ }
55506
+ function trustedConfiguredRegistry(env) {
55507
+ const configured = normalizeRegistry(env.npm_config_registry ?? env.NPM_CONFIG_REGISTRY);
55508
+ return isTrustedRegistry(configured) ? configured : void 0;
55509
+ }
55510
+ async function fetchPackumentFrom(registry, fetchImpl, timeoutMs) {
55511
+ const controller = new AbortController();
55512
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
55513
+ try {
55514
+ const response = await fetchImpl(`${registry.replace(/\/$/, "")}/${CLI_PACKAGE_PATH}`, {
55515
+ headers: { accept: "application/json" },
55516
+ signal: controller.signal
55517
+ });
55518
+ if (!response.ok) return void 0;
55519
+ const json5 = await response.json();
55520
+ const latest = typeof json5?.["dist-tags"]?.latest === "string" ? json5["dist-tags"].latest : void 0;
55521
+ const time = {};
55522
+ for (const [version, when] of Object.entries(json5?.time ?? {})) if (typeof when === "string") time[version] = when;
55523
+ return latest ? { latest, time, registry: normalizeRegistry(registry) ?? registry } : void 0;
55524
+ } catch {
55525
+ return void 0;
55526
+ } finally {
55527
+ clearTimeout(timer);
55528
+ }
55529
+ }
55530
+ async function fetchCliPackument(fetchImpl = fetch, timeoutMs = DOCTOR_TIMEOUT_MS, env = process.env) {
55531
+ const configured = trustedConfiguredRegistry(env);
55532
+ const candidates = configured && configured !== PUBLIC_NPM_REGISTRY ? [configured, PUBLIC_NPM_REGISTRY] : [PUBLIC_NPM_REGISTRY];
55533
+ for (const registry of candidates) {
55534
+ const packument = await fetchPackumentFrom(registry, fetchImpl, timeoutMs);
55535
+ if (packument) return packument;
55536
+ }
55537
+ return void 0;
55538
+ }
55539
+ function ageInDays(publishedAt, now = Date.now()) {
55540
+ if (!publishedAt) return void 0;
55541
+ const at = Date.parse(publishedAt);
55542
+ if (!Number.isFinite(at)) return void 0;
55543
+ return Math.max(0, (now - at) / 864e5);
55544
+ }
55474
55545
  function packageVersion(path) {
55475
55546
  try {
55476
55547
  const version = JSON.parse(readFileSync65(path, "utf8")).version;
@@ -55500,18 +55571,10 @@ function resolvedSdkVersion(fromDir = process.cwd(), bundledManifest = new URL("
55500
55571
  }
55501
55572
  async function doctorVersionReport(running, fetchImpl = fetch, sdk = resolvedSdkVersion()) {
55502
55573
  const minimumSdk = "0.33.0";
55503
- const registry = (process.env.npm_config_registry ?? "https://npm.dev.wixpress.com").replace(/\/$/, "");
55504
- let latest;
55505
- const controller = new AbortController();
55506
- const timer = setTimeout(() => controller.abort(), DOCTOR_TIMEOUT_MS);
55507
- try {
55508
- const response = await fetchImpl(`${registry}/@wix%2fhimalaya-cli`, { headers: { accept: "application/json" }, signal: controller.signal });
55509
- const json5 = response.ok ? await response.json() : void 0;
55510
- latest = typeof json5?.["dist-tags"]?.latest === "string" ? json5["dist-tags"].latest : void 0;
55511
- } catch {
55512
- } finally {
55513
- clearTimeout(timer);
55514
- }
55574
+ const packument = await fetchCliPackument(fetchImpl);
55575
+ const latest = packument?.latest;
55576
+ const publishedAt = packument?.time[running];
55577
+ const ageDays = ageInDays(publishedAt);
55515
55578
  const cliOutdated = !!latest && compareVersions2(running, latest) < 0;
55516
55579
  const bundled = sdk.source === "bundled";
55517
55580
  const sdkOutdated = !bundled && !!sdk.version && compareVersions2(sdk.version, minimumSdk) < 0;
@@ -55519,6 +55582,8 @@ async function doctorVersionReport(running, fetchImpl = fetch, sdk = resolvedSdk
55519
55582
  cli: {
55520
55583
  running,
55521
55584
  ...latest ? { latest } : {},
55585
+ ...publishedAt ? { publishedAt } : {},
55586
+ ...ageDays === void 0 ? {} : { ageDays: Math.round(ageDays * 10) / 10 },
55522
55587
  status: latest ? cliOutdated ? "outdated" : "current" : "unknown",
55523
55588
  ...cliOutdated ? { hint: "Upgrade: npm i -g @wix/himalaya-cli@latest" } : {}
55524
55589
  },
@@ -55534,6 +55599,329 @@ function formatSdkVersionLine(sdk) {
55534
55599
  if (sdk.source === "bundled") return `sdk: bundled (${sdk.resolved ?? "unknown"})`;
55535
55600
  return `sdk: ${sdk.resolved ?? "not installed"} (minimum ${sdk.minimum}; ${sdk.status})`;
55536
55601
  }
55602
+ var CLI_PACKAGE = "@wix/himalaya-cli";
55603
+ var UNPUBLISHED_CLI_VERSION = "0.1.0";
55604
+ var UPDATE_MAX_AGE_DAYS = 7;
55605
+ var UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
55606
+ var UPDATE_SNOOZE_MS = 24 * 60 * 60 * 1e3;
55607
+ var UPDATE_REEXEC_ENV = "HIMI_UPDATED_ONCE";
55608
+ function updateCachePath(env = process.env) {
55609
+ return env.HIMI_UPDATE_CACHE_FILE ?? join51(homedir7(), ".himalaya", "update-check.json");
55610
+ }
55611
+ function readUpdateCache(path) {
55612
+ try {
55613
+ const parsed = JSON.parse(readFileSync65(path, "utf8"));
55614
+ return parsed && typeof parsed === "object" ? parsed : {};
55615
+ } catch {
55616
+ return {};
55617
+ }
55618
+ }
55619
+ function writeUpdateCache(path, cache) {
55620
+ try {
55621
+ mkdirSync25(dirname30(path), { recursive: true, mode: 448 });
55622
+ writeFileSync23(path, JSON.stringify(cache, null, 2) + "\n", { mode: 384 });
55623
+ } catch {
55624
+ }
55625
+ }
55626
+ function updateCheckSuppression(running, command, env = process.env, cwd = process.cwd()) {
55627
+ if (env[UPDATE_REEXEC_ENV] === "1") return "already-updated";
55628
+ if (env.HIMI_NO_UPDATE_CHECK === "1") return "opted-out";
55629
+ if (command === "upgrade" || command === "doctor") return "own-command";
55630
+ if (running === UNPUBLISHED_CLI_VERSION) return "unpublished";
55631
+ if (isHimalayaMonorepoCheckout(cwd)) return "monorepo";
55632
+ return void 0;
55633
+ }
55634
+ function decideUpdateAction(input) {
55635
+ if (!input.latest || compareVersions2(input.running, input.latest) >= 0) return "skip";
55636
+ if (input.ageDays === void 0 || input.ageDays < input.maxAgeDays) return "skip";
55637
+ if (!input.prefixWritable()) return "warn";
55638
+ return input.interactive ? "prompt" : "update";
55639
+ }
55640
+ function globalNodeModulesRoot() {
55641
+ try {
55642
+ const root = execFileSync7("npm", ["root", "-g"], { stdio: ["ignore", "pipe", "ignore"] }).toString().trim();
55643
+ return root || void 0;
55644
+ } catch {
55645
+ return void 0;
55646
+ }
55647
+ }
55648
+ function globalPrefixWritable() {
55649
+ const root = globalNodeModulesRoot();
55650
+ if (!root) return false;
55651
+ try {
55652
+ accessSync(root, constants.W_OK);
55653
+ return true;
55654
+ } catch {
55655
+ return false;
55656
+ }
55657
+ }
55658
+ function npmInstallArgs(registry, version) {
55659
+ const pinned = `${normalizeRegistry(registry) ?? registry}/`;
55660
+ return ["i", "-g", `${CLI_PACKAGE}@${version}`, `--registry=${pinned}`, `--@wix:registry=${pinned}`];
55661
+ }
55662
+ var INSTALLABLE_SPEC = /^(?:latest|\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)$/;
55663
+ function createUpdateRunner(spawn5 = spawnSync5) {
55664
+ return {
55665
+ install: async (registry, version) => {
55666
+ if (!isTrustedRegistry(registry)) return { ok: false, detail: `refusing untrusted registry ${registry}` };
55667
+ if (!INSTALLABLE_SPEC.test(version)) return { ok: false, detail: `refusing install spec ${JSON.stringify(version)}` };
55668
+ const result = spawn5("npm", npmInstallArgs(registry, version), {
55669
+ // npm's stdout goes to OUR stderr: in `himi <cmd> --json | jq` (and `himi upgrade --json`) its
55670
+ // "added 212 packages" would otherwise land in stdout ahead of the envelope and break the
55671
+ // parse — the agent case this whole feature exists for (#2985 review). And no stdin: an
55672
+ // install needs none, and must not consume input piped to the command we are about to re-run.
55673
+ stdio: ["ignore", 2, 2],
55674
+ cwd: homedir7()
55675
+ });
55676
+ if (result.error) return { ok: false, detail: result.error.message };
55677
+ return result.status === 0 ? { ok: true } : { ok: false, detail: `npm exited ${result.status}` };
55678
+ },
55679
+ installedVersion: () => {
55680
+ const root = globalNodeModulesRoot();
55681
+ return root ? packageVersion(join51(root, "@wix", "himalaya-cli", "package.json")) : void 0;
55682
+ },
55683
+ reexec: (argv) => {
55684
+ const root = globalNodeModulesRoot();
55685
+ const entry = root ? join51(root, "@wix", "himalaya-cli", "dist", "cli.mjs") : void 0;
55686
+ if (!entry || !existsSync54(entry)) return void 0;
55687
+ const result = spawnSync5(process.execPath, [entry, ...argv], {
55688
+ // inherit keeps PIPED STDIN working — commands that read stdin must survive a re-exec.
55689
+ stdio: "inherit",
55690
+ env: { ...process.env, [UPDATE_REEXEC_ENV]: "1" }
55691
+ });
55692
+ return result.status ?? 1;
55693
+ },
55694
+ confirm: async (question) => {
55695
+ const { createInterface } = await import("node:readline/promises");
55696
+ const rl = createInterface({ input: process.stdin, output: process.stderr });
55697
+ try {
55698
+ const answer = (await rl.question(question)).trim().toLowerCase();
55699
+ return answer === "" || answer === "y" || answer === "yes";
55700
+ } catch {
55701
+ return false;
55702
+ } finally {
55703
+ rl.close();
55704
+ }
55705
+ }
55706
+ };
55707
+ }
55708
+ var defaultUpdateRunner = createUpdateRunner();
55709
+ var UPDATE_LOCK_REPORT_AFTER_MS = 60 * 60 * 1e3;
55710
+ function thisHost() {
55711
+ return hostname().replace(/\s+/g, "_");
55712
+ }
55713
+ function lockStamp(now) {
55714
+ return `${now} ${process.pid} ${thisHost()} ${randomUUID3()}`;
55715
+ }
55716
+ function processAlive(pid) {
55717
+ try {
55718
+ process.kill(pid, 0);
55719
+ return true;
55720
+ } catch (error) {
55721
+ return error.code === "EPERM";
55722
+ }
55723
+ }
55724
+ function isAbandoned(content) {
55725
+ const [, pid, host] = content.split(" ");
55726
+ const owner = Number.parseInt(pid, 10);
55727
+ return Number.isFinite(owner) && owner > 0 && host === thisHost() && !processAlive(owner);
55728
+ }
55729
+ function staleUpdateLockFile(cachePath, now) {
55730
+ for (const path of [`${cachePath}.lock.break`, `${cachePath}.lock`]) {
55731
+ let content;
55732
+ try {
55733
+ content = readFileSync65(path, "utf8");
55734
+ } catch {
55735
+ continue;
55736
+ }
55737
+ const stamped = Number.parseInt(content.split(" ")[0], 10);
55738
+ let since = stamped;
55739
+ if (!Number.isFinite(since)) {
55740
+ try {
55741
+ since = statSync12(path).mtimeMs;
55742
+ } catch {
55743
+ continue;
55744
+ }
55745
+ }
55746
+ if (now - since >= UPDATE_LOCK_REPORT_AFTER_MS) return path;
55747
+ }
55748
+ return void 0;
55749
+ }
55750
+ function acquireUpdateLock(cachePath, now, hooks = {}) {
55751
+ const lockPath = `${cachePath}.lock`;
55752
+ const breakPath = `${cachePath}.lock.break`;
55753
+ const mine = lockStamp(now);
55754
+ const readLock = (path) => {
55755
+ try {
55756
+ return readFileSync65(path, "utf8");
55757
+ } catch {
55758
+ return void 0;
55759
+ }
55760
+ };
55761
+ const tryCreate = (path) => {
55762
+ try {
55763
+ writeFileSync23(path, mine, { flag: "wx", mode: 384 });
55764
+ return true;
55765
+ } catch {
55766
+ return false;
55767
+ }
55768
+ };
55769
+ const removeIfMine = (path) => {
55770
+ try {
55771
+ if (readLock(path) === mine) rmSync9(path, { force: true });
55772
+ } catch {
55773
+ }
55774
+ };
55775
+ const release = () => removeIfMine(lockPath);
55776
+ try {
55777
+ mkdirSync25(dirname30(lockPath), { recursive: true, mode: 448 });
55778
+ } catch {
55779
+ return () => {
55780
+ };
55781
+ }
55782
+ try {
55783
+ if (tryCreate(lockPath)) return release;
55784
+ const held = readLock(lockPath);
55785
+ if (held === void 0) return tryCreate(lockPath) ? release : void 0;
55786
+ if (!isAbandoned(held)) return void 0;
55787
+ hooks.beforeBreak?.();
55788
+ if (!tryCreate(breakPath)) return void 0;
55789
+ try {
55790
+ const current = readLock(lockPath);
55791
+ hooks.underBreakLock?.();
55792
+ if (current === void 0) return tryCreate(lockPath) ? release : void 0;
55793
+ if (!isAbandoned(current)) return void 0;
55794
+ rmSync9(lockPath, { force: true });
55795
+ return tryCreate(lockPath) ? release : void 0;
55796
+ } finally {
55797
+ removeIfMine(breakPath);
55798
+ }
55799
+ } catch {
55800
+ return void 0;
55801
+ }
55802
+ }
55803
+ function formatStaleLine(running, latest, ageDays) {
55804
+ const days = Math.round(ageDays);
55805
+ return `himi v${running} is ${days} day${days === 1 ? "" : "s"} old (latest ${latest})`;
55806
+ }
55807
+ function upgradeEnvelope(outcome) {
55808
+ return {
55809
+ running: outcome.running,
55810
+ ...outcome.latest ? { latest: outcome.latest } : {},
55811
+ ...outcome.ageDays === void 0 ? {} : { ageDays: outcome.ageDays },
55812
+ action: outcome.action,
55813
+ command: `npm i -g ${CLI_PACKAGE}@latest`,
55814
+ ...outcome.detail ? { detail: outcome.detail } : {}
55815
+ };
55816
+ }
55817
+ async function maybeSelfUpdate(argv, io, opts = {}) {
55818
+ const env = opts.env ?? process.env;
55819
+ const running = opts.running ?? cliVersion();
55820
+ const command = opts.command ?? "";
55821
+ if (updateCheckSuppression(running, command, env, opts.cwd ?? process.cwd())) return void 0;
55822
+ const now = opts.now ?? Date.now();
55823
+ const cachePath = opts.cachePath ?? updateCachePath(env);
55824
+ const cache = readUpdateCache(cachePath);
55825
+ if (cache.snoozedUntil && Date.parse(cache.snoozedUntil) > now) return void 0;
55826
+ let latest = cache.latest;
55827
+ let runningPublishedAt = cache.runningPublishedAt;
55828
+ const checkedAt = cache.checkedAt ? Date.parse(cache.checkedAt) : Number.NaN;
55829
+ const describesThisBuild = cache.running === running;
55830
+ const fresh = Number.isFinite(checkedAt) && now - checkedAt < UPDATE_CHECK_INTERVAL_MS && describesThisBuild;
55831
+ if (!describesThisBuild) runningPublishedAt = void 0;
55832
+ let answeredBy;
55833
+ let answeredLatest;
55834
+ if (!fresh) {
55835
+ const packument = await fetchCliPackument(opts.fetchImpl ?? fetch, DOCTOR_TIMEOUT_MS, env);
55836
+ if (packument) {
55837
+ latest = packument.latest;
55838
+ runningPublishedAt = packument.time[running];
55839
+ answeredBy = packument.registry;
55840
+ answeredLatest = packument.latest;
55841
+ writeUpdateCache(cachePath, { ...cache, running, checkedAt: new Date(now).toISOString(), latest, runningPublishedAt });
55842
+ }
55843
+ }
55844
+ const ageDays = ageInDays(runningPublishedAt, now);
55845
+ const fromEnv = Number(env.HIMI_UPDATE_MAX_AGE_DAYS);
55846
+ const maxAgeDays = opts.maxAgeDays ?? (fromEnv > 0 ? fromEnv : UPDATE_MAX_AGE_DAYS);
55847
+ const interactive = opts.interactive ?? (io.isTTY === true && process.stdin.isTTY === true);
55848
+ const action = decideUpdateAction({
55849
+ running,
55850
+ latest,
55851
+ ageDays,
55852
+ maxAgeDays,
55853
+ interactive,
55854
+ prefixWritable: opts.prefixWritable ?? globalPrefixWritable
55855
+ });
55856
+ if (action === "skip" || !latest || ageDays === void 0) return void 0;
55857
+ const base = { running, latest, ageDays: Math.round(ageDays * 10) / 10, command };
55858
+ const stale = formatStaleLine(running, latest, ageDays);
55859
+ if (action === "warn") {
55860
+ io.error(`${stale} \u2014 npm i -g ${CLI_PACKAGE}@latest`);
55861
+ return { ...base, action: "warned", detail: "global prefix is not writable" };
55862
+ }
55863
+ const runner2 = opts.runner ?? defaultUpdateRunner;
55864
+ if (action === "prompt") {
55865
+ const yes = await runner2.confirm(`${stale}. Update now? [Y/n] `);
55866
+ if (!yes) {
55867
+ writeUpdateCache(cachePath, {
55868
+ running,
55869
+ latest,
55870
+ runningPublishedAt,
55871
+ checkedAt: new Date(now).toISOString(),
55872
+ snoozedUntil: new Date(now + UPDATE_SNOOZE_MS).toISOString()
55873
+ });
55874
+ return { ...base, action: "declined" };
55875
+ }
55876
+ }
55877
+ const releaseLock = opts.acquireLock === void 0 ? acquireUpdateLock(cachePath, now) : opts.acquireLock();
55878
+ if (!releaseLock) {
55879
+ const blocker = staleUpdateLockFile(cachePath, now);
55880
+ if (blocker) io.error(`${stale}, but self-update has been blocked for over an hour by ${blocker} \u2014 if no himi is running, delete it`);
55881
+ return void 0;
55882
+ }
55883
+ try {
55884
+ if (!answeredBy) {
55885
+ const again = await fetchCliPackument(opts.fetchImpl ?? fetch, DOCTOR_TIMEOUT_MS, env);
55886
+ answeredBy = again?.registry;
55887
+ answeredLatest = again?.latest;
55888
+ }
55889
+ if (!answeredBy || !answeredLatest || compareVersions2(answeredLatest, running) <= 0) {
55890
+ if (answeredBy && answeredLatest) {
55891
+ writeUpdateCache(cachePath, { ...cache, running, latest: answeredLatest, runningPublishedAt, checkedAt: new Date(now).toISOString() });
55892
+ }
55893
+ return void 0;
55894
+ }
55895
+ const installFrom = answeredBy;
55896
+ const target = answeredLatest;
55897
+ io.error(`${stale} \u2014 updating to ${target}\u2026`);
55898
+ const installed = await runner2.install(installFrom, target);
55899
+ if (!installed.ok) {
55900
+ io.error(` update failed (${installed.detail ?? "unknown error"}) via ${installFrom} \u2014 continuing on v${running}`);
55901
+ return { ...base, action: "failed", detail: installed.detail };
55902
+ }
55903
+ const after = runner2.installedVersion();
55904
+ if (!after || compareVersions2(after, running) <= 0) {
55905
+ io.error(` npm reported success but v${running} is still installed \u2014 continuing without re-running`);
55906
+ return { ...base, action: "failed", detail: "version did not change" };
55907
+ }
55908
+ writeUpdateCache(cachePath, {
55909
+ running: after,
55910
+ latest: target,
55911
+ checkedAt: new Date(now).toISOString(),
55912
+ ...cache.snoozedUntil ? { snoozedUntil: cache.snoozedUntil } : {}
55913
+ });
55914
+ io.error(` installed ${after} \u2014 re-running: himi ${argv.join(" ")}`);
55915
+ const exitCode = runner2.reexec(argv);
55916
+ if (exitCode === void 0) {
55917
+ io.error(` could not locate the new install to re-run \u2014 continuing on v${running}`);
55918
+ return { ...base, action: "failed", detail: "new entry not found" };
55919
+ }
55920
+ return { ...base, latest: after, action: "updated", exitCode };
55921
+ } finally {
55922
+ releaseLock();
55923
+ }
55924
+ }
55537
55925
  function helpJson() {
55538
55926
  return {
55539
55927
  cli: "himi",
@@ -56586,11 +56974,16 @@ async function producedWidgetFamilies(dir) {
56586
56974
  }
56587
56975
  return { families, source: "dist/bundles" };
56588
56976
  }
56589
- async function run(argv, io = defaultIO) {
56977
+ async function run(argv, io = defaultIO, deps = {}) {
56590
56978
  const { _: pos, flags } = parseFlags(argv);
56591
56979
  const flagErrors = switchFlagErrors(argv);
56592
56980
  const command = pos[0];
56593
- const print = (o) => io.log(JSON.stringify(o, null, 2));
56981
+ let updateNotice;
56982
+ const print = (o) => io.log(JSON.stringify(
56983
+ updateNotice && o && typeof o === "object" && !Array.isArray(o) ? { ...o, upgrade: upgradeEnvelope(updateNotice) } : o,
56984
+ null,
56985
+ 2
56986
+ ));
56594
56987
  if (flags.version === true || command === "version" || pos.includes("-v")) {
56595
56988
  const v = cliVersion();
56596
56989
  io.log(flags.json ? JSON.stringify({ himi: v }, null, 2) : `himi v${v}`);
@@ -56608,6 +57001,11 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
56608
57001
  io.error(JSON.stringify({ ok: false, error: "invalid_flag_value", problems: flagErrors, hint: "run `himi help` for the flag grammar." }));
56609
57002
  return 2;
56610
57003
  }
57004
+ if (flags["skip-version-check"] !== true) {
57005
+ const update = await maybeSelfUpdate(argv, io, { command, ...deps.selfUpdate });
57006
+ if (update?.exitCode !== void 0) return update.exitCode;
57007
+ if (update) updateNotice = update;
57008
+ }
56611
57009
  const contextualDefaultTarget = defaultTargetForCommand(command, contentDir(flags));
56612
57010
  const targetName = () => resolveTargetName({ target: str5(flags.target), defaultTarget: contextualDefaultTarget });
56613
57011
  const client2 = () => new ServeCliClient(
@@ -60966,6 +61364,45 @@ ${label2}`);
60966
61364
  });
60967
61365
  return 0;
60968
61366
  }
61367
+ case "upgrade": {
61368
+ const up = deps.selfUpdate ?? {};
61369
+ const runner2 = up.runner ?? defaultUpdateRunner;
61370
+ const env = up.env ?? process.env;
61371
+ const before = cliVersion();
61372
+ const lockFile = `${up.cachePath ?? updateCachePath(env)}.lock`;
61373
+ const releaseLock = up.acquireLock ? up.acquireLock() : acquireUpdateLock(up.cachePath ?? updateCachePath(env), Date.now());
61374
+ if (!releaseLock) {
61375
+ const hint = `another himi process is updating right now \u2014 try again in a minute. If no himi is running, delete ${lockFile} (and ${lockFile}.break if present)`;
61376
+ if (flags.json) print({ ok: false, error: "update_in_progress", running: before, lock: lockFile, hint });
61377
+ else io.error(hint);
61378
+ return 1;
61379
+ }
61380
+ try {
61381
+ const answered = await fetchCliPackument(up.fetchImpl ?? fetch, DOCTOR_TIMEOUT_MS, env);
61382
+ const registry = answered?.registry ?? trustedConfiguredRegistry(env) ?? PUBLIC_NPM_REGISTRY;
61383
+ if (answered?.latest && compareVersions2(answered.latest, before) <= 0) {
61384
+ if (flags.json) print({ ok: true, running: before, installed: before, changed: false, registry, latest: answered.latest });
61385
+ else io.log(`himi ${before} (unchanged \u2014 ${registry} has nothing newer; its latest is ${answered.latest})`);
61386
+ return 0;
61387
+ }
61388
+ const version = answered?.latest ?? "latest";
61389
+ io.error(`upgrading ${CLI_PACKAGE} (running v${before}) to ${version} from ${registry}\u2026`);
61390
+ const installed = await runner2.install(registry, version);
61391
+ if (!installed.ok) {
61392
+ const detail = installed.detail ?? "unknown error";
61393
+ if (flags.json) print({ ok: false, error: "upgrade_failed", running: before, registry, detail });
61394
+ else io.error(`upgrade failed (${detail}) via ${registry}`);
61395
+ return 1;
61396
+ }
61397
+ const after = runner2.installedVersion();
61398
+ const moved = !!after && compareVersions2(after, before) > 0;
61399
+ if (flags.json) print({ ok: true, running: before, installed: after, changed: moved, registry });
61400
+ else io.log(moved ? `himi ${before} \u2192 ${after}` : `himi ${before} (unchanged \u2014 the registry has nothing newer to install)`);
61401
+ return 0;
61402
+ } finally {
61403
+ releaseLock();
61404
+ }
61405
+ }
60969
61406
  case "doctor": {
60970
61407
  const target = targetName();
60971
61408
  const timeoutMs = Number(str5(flags.timeout)) > 0 ? Number(str5(flags.timeout)) : DOCTOR_TIMEOUT_MS;
@@ -61418,36 +61855,50 @@ if (isMain) {
61418
61855
  });
61419
61856
  }
61420
61857
  export {
61858
+ CLI_PACKAGE,
61421
61859
  ConfigLoadError,
61422
61860
  GIT_FLAG_STORE_HALF_WRITE_NOTE,
61423
61861
  GIT_FLAG_STORE_NOTE,
61424
61862
  PLACEHOLDER_CLIENT_ID,
61425
61863
  PLACEHOLDER_SITE_URL,
61426
61864
  PREVIEW_TOKEN_NOTE,
61865
+ PUBLIC_NPM_REGISTRY,
61427
61866
  PUBLIC_WIX_API_BASE,
61867
+ WIX_NPM_MIRROR,
61868
+ acquireUpdateLock,
61869
+ ageInDays,
61428
61870
  autoClaimSlug,
61429
61871
  buildDoctorReport,
61430
61872
  compareVersions2 as compareVersions,
61873
+ createUpdateRunner,
61431
61874
  credentialKind,
61432
61875
  credentialSwapNotice,
61876
+ decideUpdateAction,
61433
61877
  decodeControlTokenClaims,
61434
61878
  defaultTargetForCommand,
61879
+ defaultUpdateRunner,
61435
61880
  diagnoseBody,
61436
61881
  doctorVersionReport,
61437
61882
  executableOnPath,
61883
+ fetchCliPackument,
61438
61884
  foreignPlaneError,
61439
61885
  formatSdkVersionLine,
61886
+ formatStaleLine,
61887
+ globalPrefixWritable,
61440
61888
  humanDuration,
61441
61889
  inspectControlToken,
61442
61890
  isHimalayaMonorepoCheckout,
61443
61891
  isLoopbackBase,
61444
61892
  isPublicTarget,
61893
+ isTrustedRegistry,
61445
61894
  loginModeMark,
61446
61895
  loginModeReport,
61447
61896
  loopbackTargetWarning,
61448
61897
  materializeForPreview,
61898
+ maybeSelfUpdate,
61449
61899
  nativeConfigAdvisories,
61450
61900
  nativeConfigTokenNotice,
61901
+ npmInstallArgs,
61451
61902
  ownerPlaneHint,
61452
61903
  ownerPlaneIsDark,
61453
61904
  parseFlags,
@@ -61467,6 +61918,7 @@ export {
61467
61918
  probeControlToken,
61468
61919
  probeReachable,
61469
61920
  pushAuthHint,
61921
+ readUpdateCache,
61470
61922
  renderDataSource,
61471
61923
  renderDeviceWidths,
61472
61924
  resolveAppSlug,
@@ -61477,12 +61929,18 @@ export {
61477
61929
  serveUnreachableHint,
61478
61930
  servesAll,
61479
61931
  shellShaDivergenceWarnings,
61932
+ staleUpdateLockFile,
61480
61933
  switchFlagErrors,
61481
61934
  templatePreviewAuthError,
61482
61935
  tokenCoversApp,
61936
+ trustedConfiguredRegistry,
61937
+ updateCachePath,
61938
+ updateCheckSuppression,
61939
+ upgradeEnvelope,
61483
61940
  uxHandoverHint,
61484
61941
  uxHandoverNote,
61485
61942
  uxHandoverOverride,
61486
61943
  wixPlayerTestflightTarget,
61487
- writePngFile
61944
+ writePngFile,
61945
+ writeUpdateCache
61488
61946
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.972.0",
3
+ "version": "0.974.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": [
@@ -64,5 +64,5 @@
64
64
  "artifactId": "himalaya-cli"
65
65
  }
66
66
  },
67
- "falconPackageHash": "9d8d2a86205140b8694ab07b45d09c35b40bfb375a3baf1f123ffacf"
67
+ "falconPackageHash": "5f80749d381fe4c9e3c3eb78d17c7c5ab989f19b80230378391c25af"
68
68
  }