@wix/himalaya-cli 0.967.0 → 0.969.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 +349 -46
  2. package/package.json +2 -2
package/dist/cli.mjs CHANGED
@@ -537,7 +537,7 @@ var init_store = __esm({
537
537
 
538
538
  // ../../core/serve-storage/src/file-store.ts
539
539
  import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync, writeFileSync as writeFileSync2, readdirSync, existsSync } from "node:fs";
540
- import { join as join2, resolve } from "node:path";
540
+ import { join as join2, resolve, sep } from "node:path";
541
541
  var FileReleaseStore;
542
542
  var init_file_store = __esm({
543
543
  "../../core/serve-storage/src/file-store.ts"() {
@@ -547,8 +547,37 @@ var init_file_store = __esm({
547
547
  constructor(root) {
548
548
  this.root = resolve(root);
549
549
  }
550
+ /** The app's directory — and the ONE place an app id becomes a path in this store.
551
+ *
552
+ * IT VALIDATES, because `reset()` below deletes this path recursively and `appDir` was a bare
553
+ * `join(this.root, app)`. `remove()` called `this.safe(app)` first and `reset()` did not, three
554
+ * lines apart, so `reset("../../user-code/himalaya")` escaped the root and recursively removed
555
+ * whatever the process could write. Every other method reached the same unguarded join.
556
+ *
557
+ * Guarding the CALL SITES is what produced that bug: two of eight remembered. So the guard moves
558
+ * into the derivation, where forgetting it is not possible — a method that wants the directory
559
+ * has to come through here.
560
+ *
561
+ * IT IS NOT THE PUBLIC APP-ID GRAMMAR, and that distinction cost a CI round. The first cut of
562
+ * this called `assertAppId` — the grammar the ROUTES enforce — and refused `_template`, the
563
+ * internal app `buildRelease` resets through this store (build.ts). Nothing is wrong with that
564
+ * caller: this store's key is a local build-output directory name, which is a wider namespace
565
+ * than a published app id, and a published id is validated where it is published.
566
+ *
567
+ * What remains is the invariant that actually carries the security property, and it is enough on
568
+ * its own: a path that RESOLVES outside the root is refused. `../../user-code/himalaya` and any
569
+ * absolute path resolve outside; `_template` resolves inside. The root ITSELF is refused too —
570
+ * `reset("")` would otherwise recursively delete the whole store. `safe()` runs first as the
571
+ * cheap, readable half (no separator, no `..`), the same rule this class already applies to every
572
+ * screen id and sha it turns into a filename; the resolved-path check is the belt behind it, and
573
+ * catches what a string test cannot, including a separator this one does not know about.
574
+ */
550
575
  appDir(app) {
551
- return join2(this.root, app);
576
+ const dir = resolve(this.root, this.safe(app));
577
+ if (!dir.startsWith(this.root + sep)) {
578
+ throw new Error(`unsafe app id ${JSON.stringify(app)}: ${dir} is not inside the store root`);
579
+ }
580
+ return dir;
552
581
  }
553
582
  safe(name) {
554
583
  if (name.includes("/") || name.includes("..")) throw new Error(`unsafe name: ${name}`);
@@ -561,7 +590,7 @@ var init_file_store = __esm({
561
590
  mkdirSync2(join2(dir, "bundles"), { recursive: true });
562
591
  }
563
592
  async remove(app) {
564
- rmSync(this.appDir(this.safe(app)), { recursive: true, force: true });
593
+ rmSync(this.appDir(app), { recursive: true, force: true });
565
594
  }
566
595
  async putManifest(app, manifest) {
567
596
  validateFunctionManifest(manifest);
@@ -771,7 +800,7 @@ var init_base64 = __esm({
771
800
  });
772
801
 
773
802
  // src/source-deny.ts
774
- import { sep } from "node:path";
803
+ import { sep as sep2 } from "node:path";
775
804
  function isDeniedSourcePath(relPath) {
776
805
  const segments = relPath.split(/[\\/]/).filter(Boolean);
777
806
  if (segments.length === 0) return false;
@@ -781,7 +810,7 @@ function isDeniedSourcePath(relPath) {
781
810
  return DENY_FILES.some((re) => re.test(segments[segments.length - 1]));
782
811
  }
783
812
  function isDeniedSourceDir(name, parentRel) {
784
- const base = name.split(sep).pop() ?? name;
813
+ const base = name.split(sep2).pop() ?? name;
785
814
  if (DENY_DIRS.has(base)) return true;
786
815
  return parentRel === "" && DENY_ROOT_DIRS.has(base);
787
816
  }
@@ -1289,7 +1318,7 @@ function sentinelizeIdentity(body, identity) {
1289
1318
  }
1290
1319
  if (identity.name) {
1291
1320
  const re = new RegExp(`\\b(${NAME_FIELDS.join("|")})(["']?\\s*[:=]\\s*)(["'])${escapeRe(identity.name)}\\3`, "g");
1292
- out = out.replace(re, (_m, key2, sep11, q) => `${key2}${sep11}${q}__HIMI_APP_NAME__${q}`);
1321
+ out = out.replace(re, (_m, key2, sep12, q) => `${key2}${sep12}${q}__HIMI_APP_NAME__${q}`);
1293
1322
  }
1294
1323
  return out;
1295
1324
  }
@@ -2836,7 +2865,7 @@ __export(build_lib_exports, {
2836
2865
  umbrellaRewritePlugin: () => umbrellaRewritePlugin
2837
2866
  });
2838
2867
  import { fileURLToPath as fileURLToPath5, pathToFileURL as pathToFileURL2 } from "node:url";
2839
- import { basename as basename4, dirname as dirname8, join as join12, resolve as resolve8, sep as sep2 } from "node:path";
2868
+ import { basename as basename4, dirname as dirname8, join as join12, resolve as resolve8, sep as sep3 } from "node:path";
2840
2869
  import { readdirSync as readdirSync6, existsSync as existsSync8, readFileSync as readFileSync10, realpathSync as realpathSync2, statSync as statSync2 } from "node:fs";
2841
2870
  async function loadCompiler() {
2842
2871
  try {
@@ -2884,7 +2913,7 @@ function realpathOrSelf(p) {
2884
2913
  }
2885
2914
  }
2886
2915
  function isUnder(p, root) {
2887
- return p === root || p.startsWith(root + sep2);
2916
+ return p === root || p.startsWith(root + sep3);
2888
2917
  }
2889
2918
  function confinementRoots(contentRoot) {
2890
2919
  const roots = /* @__PURE__ */ new Set();
@@ -3174,7 +3203,7 @@ function tsExtResolvePlugin() {
3174
3203
  }
3175
3204
  function importMetaUrlPlugin({ scope } = {}) {
3176
3205
  const scopes = scope ? [.../* @__PURE__ */ new Set([realpathOrSelf(scope), resolve8(scope)])] : [];
3177
- const inScope = (p) => scopes.some((r) => p === r || p.startsWith(r + sep2));
3206
+ const inScope = (p) => scopes.some((r) => p === r || p.startsWith(r + sep3));
3178
3207
  return {
3179
3208
  name: "himalaya-import-meta-url",
3180
3209
  setup(b) {
@@ -3830,7 +3859,7 @@ var init_tier5_dir = __esm({
3830
3859
 
3831
3860
  // src/tier5-typecheck.ts
3832
3861
  import { existsSync as existsSync9, readFileSync as readFileSync11 } from "node:fs";
3833
- import { relative as relative2, resolve as resolve10, sep as sep3 } from "node:path";
3862
+ import { relative as relative2, resolve as resolve10, sep as sep4 } from "node:path";
3834
3863
  import { pathToFileURL as pathToFileURL3 } from "node:url";
3835
3864
  async function loadTypeScript() {
3836
3865
  const runtime = process.env.HIMI_AUTHORING_RUNTIME_DIR;
@@ -3869,7 +3898,7 @@ function pathsFor(options) {
3869
3898
  return declarationPaths(options.umbrellaDir);
3870
3899
  }
3871
3900
  function relativeFile(contentDir2, fileName) {
3872
- return relative2(contentDir2, fileName).split(sep3).join("/");
3901
+ return relative2(contentDir2, fileName).split(sep4).join("/");
3873
3902
  }
3874
3903
  function belongsToContentWorkers(contentDir2, diagnostic) {
3875
3904
  if (!diagnostic.file) return true;
@@ -11268,7 +11297,7 @@ __export(icon_exports, {
11268
11297
  });
11269
11298
  import { createHash as createHash5 } from "node:crypto";
11270
11299
  import { existsSync as existsSync15, mkdirSync as mkdirSync9, readFileSync as readFileSync18, statSync as statSync4, writeFileSync as writeFileSync8 } from "node:fs";
11271
- import { basename as basename5, dirname as dirname14, join as join19, resolve as resolve14, sep as sep4 } from "node:path";
11300
+ import { basename as basename5, dirname as dirname14, join as join19, resolve as resolve14, sep as sep5 } from "node:path";
11272
11301
  function readManifest(dir) {
11273
11302
  try {
11274
11303
  return JSON.parse(readFileSync18(join19(dir, MANIFEST), "utf8"));
@@ -11285,7 +11314,7 @@ function declaredIconName(dir) {
11285
11314
  function iconDest(dir, name) {
11286
11315
  const base = resolve14(dir);
11287
11316
  const abs = resolve14(base, name);
11288
- if (!abs.startsWith(base + sep4)) {
11317
+ if (!abs.startsWith(base + sep5)) {
11289
11318
  throw new Error(
11290
11319
  `himi icon: himalaya.content.json declares "icon": "${name}", which resolves outside the package (${abs}). Declare a path inside it, e.g. "${DEFAULT_CONTENT_ICON}".`
11291
11320
  );
@@ -12091,6 +12120,12 @@ var init_wix_gateway_rules = __esm({
12091
12120
  // src/ux-evidence.ts
12092
12121
  import { existsSync as existsSync21, mkdirSync as mkdirSync10, readFileSync as readFileSync23, writeFileSync as writeFileSync9 } from "node:fs";
12093
12122
  import { dirname as dirname15, isAbsolute as isAbsolute2, join as join25, relative as relative3, resolve as resolve15 } from "node:path";
12123
+ function isNativePlatform(value) {
12124
+ return typeof value === "string" && NATIVE_PLATFORMS.includes(value);
12125
+ }
12126
+ function isNativeAuditStatus(value) {
12127
+ return typeof value === "string" && NATIVE_AUDIT_STATUSES.includes(value);
12128
+ }
12094
12129
  function readText(path) {
12095
12130
  try {
12096
12131
  return existsSync21(path) ? readFileSync23(path, "utf8") : null;
@@ -12162,7 +12197,7 @@ function recordRenderWidth(content, entry) {
12162
12197
  return null;
12163
12198
  }
12164
12199
  }
12165
- function recordNativeAudit(content, platform, status, checks, reason) {
12200
+ function recordNativeAudit(content, platform, status, checks, reason, release) {
12166
12201
  const normalizedChecks = status === "unavailable" ? [] : [...new Set(checks.map((check) => check.trim()).filter(Boolean))];
12167
12202
  if (status !== "unavailable" && !normalizedChecks.length) {
12168
12203
  throw new Error("--checks needs at least one comma-separated check (for example: back,keyboard,voiceover)");
@@ -12173,13 +12208,89 @@ function recordNativeAudit(content, platform, status, checks, reason) {
12173
12208
  const parsed = parseEvidence(path);
12174
12209
  if (parsed.error) throw new Error(`${UX_EVIDENCE_PATH} ${parsed.error}`);
12175
12210
  const evidence = parsed.evidence ?? { version: 1, platforms: {} };
12176
- evidence.platforms[platform] = { status, checks: normalizedChecks, ...reason?.trim() ? { reason: reason.trim() } : {}, recordedAt: (/* @__PURE__ */ new Date()).toISOString() };
12211
+ evidence.platforms[platform] = {
12212
+ status,
12213
+ checks: normalizedChecks,
12214
+ ...reason?.trim() ? { reason: reason.trim() } : {},
12215
+ ...release?.releaseId && release.releaseHash ? { releaseId: release.releaseId, releaseHash: release.releaseHash } : {},
12216
+ recordedAt: (/* @__PURE__ */ new Date()).toISOString()
12217
+ };
12177
12218
  writeEvidence(path, evidence);
12178
12219
  return evidence;
12179
12220
  }
12221
+ function uxEvidenceForRelease(content, release) {
12222
+ const { releaseId, releaseHash } = release;
12223
+ const path = uxEvidencePath(content);
12224
+ const base = {
12225
+ path: UX_EVIDENCE_PATH,
12226
+ platforms: [],
12227
+ failedPlatforms: [],
12228
+ otherReleases: [],
12229
+ unstamped: 0,
12230
+ inheritedScreens: [],
12231
+ malformed: []
12232
+ };
12233
+ if (!existsSync21(path)) return { ...base, state: "absent" };
12234
+ const parsed = parseEvidence(path);
12235
+ if (parsed.error) return { ...base, state: "unreadable", error: parsed.error };
12236
+ if (!parsed.evidence) return { ...base, state: "absent" };
12237
+ const recorded = [];
12238
+ const malformed = [];
12239
+ for (const [key2, value] of Object.entries(parsed.evidence.platforms)) {
12240
+ if (!value || typeof value !== "object") continue;
12241
+ const entry = value;
12242
+ if (!isNativePlatform(key2)) {
12243
+ malformed.push(`${key2} (not a platform this tool drives)`);
12244
+ continue;
12245
+ }
12246
+ if (!isNativeAuditStatus(entry.status)) {
12247
+ malformed.push(`${key2} (status ${JSON.stringify(entry.status ?? null)} is not one \`himi ux record\` writes)`);
12248
+ continue;
12249
+ }
12250
+ if (!Array.isArray(entry.checks) || entry.checks.some((check) => typeof check !== "string")) {
12251
+ malformed.push(`${key2} (no \`checks\` array \u2014 not a record \`himi ux record\` wrote)`);
12252
+ continue;
12253
+ }
12254
+ if (typeof entry.recordedAt !== "string" || !entry.recordedAt) {
12255
+ malformed.push(`${key2} (no \`recordedAt\` \u2014 not a record \`himi ux record\` wrote)`);
12256
+ continue;
12257
+ }
12258
+ if (entry.reason !== void 0 && typeof entry.reason !== "string") {
12259
+ malformed.push(`${key2} (\`reason\` is not text)`);
12260
+ continue;
12261
+ }
12262
+ recorded.push([key2, entry]);
12263
+ }
12264
+ if (recorded.length === 0) return { ...base, state: "unrecorded", malformed };
12265
+ const forRelease = recorded.filter(([, entry]) => entry.releaseId === releaseId && entry.releaseHash === releaseHash);
12266
+ if (forRelease.length > 0) {
12267
+ const platforms = forRelease.map(([platform]) => platform);
12268
+ const failedPlatforms = forRelease.filter(([, entry]) => entry.status === "failed").map(([platform]) => platform);
12269
+ return failedPlatforms.length > 0 ? { ...base, state: "failed", platforms, failedPlatforms, malformed } : { ...base, state: "recorded", platforms, malformed };
12270
+ }
12271
+ if (recorded.some(([, entry]) => entry.releaseId === releaseId)) {
12272
+ return {
12273
+ ...base,
12274
+ state: "outdated",
12275
+ platforms: recorded.filter(([, entry]) => entry.releaseId === releaseId).map(([platform]) => platform),
12276
+ malformed
12277
+ };
12278
+ }
12279
+ const otherReleases = [...new Set(recorded.map(([, entry]) => entry.releaseId).filter((id) => !!id))].sort();
12280
+ return {
12281
+ ...base,
12282
+ state: "stale",
12283
+ otherReleases,
12284
+ unstamped: recorded.filter(([, entry]) => !entry.releaseId).length,
12285
+ malformed
12286
+ };
12287
+ }
12288
+ var NATIVE_PLATFORMS, NATIVE_AUDIT_STATUSES;
12180
12289
  var init_ux_evidence = __esm({
12181
12290
  "src/ux-evidence.ts"() {
12182
12291
  init_mobile_ux_contract();
12292
+ NATIVE_PLATFORMS = ["ios", "android"];
12293
+ NATIVE_AUDIT_STATUSES = ["passed", "failed", "unavailable"];
12183
12294
  }
12184
12295
  });
12185
12296
 
@@ -13853,7 +13964,7 @@ var init_app_module = __esm({
13853
13964
  // ../../core/dev-server/src/app-assets.ts
13854
13965
  import { createHash as createHash8 } from "node:crypto";
13855
13966
  import { existsSync as existsSync27, lstatSync as lstatSync4, readFileSync as readFileSync29, readdirSync as readdirSync14, realpathSync as realpathSync3, statSync as statSync5 } from "node:fs";
13856
- import { join as join31, resolve as resolve19, sep as sep5 } from "node:path";
13967
+ import { join as join31, resolve as resolve19, sep as sep6 } from "node:path";
13857
13968
  function contentTypeFor(name) {
13858
13969
  const ext = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1).toLowerCase() : "";
13859
13970
  return CONTENT_TYPES[ext] ?? "application/octet-stream";
@@ -13874,7 +13985,7 @@ function findAssetsDir(appDir2) {
13874
13985
  const rel = typeof declared === "string" && declared ? declared : "assets";
13875
13986
  const abs = resolve19(appDir2, rel);
13876
13987
  const base = resolve19(appDir2);
13877
- if (abs !== base && !abs.startsWith(base + sep5)) return null;
13988
+ if (abs !== base && !abs.startsWith(base + sep6)) return null;
13878
13989
  return existsSync27(abs) && statSync5(abs).isDirectory() ? abs : null;
13879
13990
  }
13880
13991
  function readOtaOnlyPatterns(appDir2) {
@@ -13932,7 +14043,7 @@ function collectBrandFonts(appDir2) {
13932
14043
  try {
13933
14044
  if (!lstatSync4(path).isFile()) continue;
13934
14045
  const root = realpathSync3(appDir2);
13935
- if (!realpathSync3(path).startsWith(root + sep5)) continue;
14046
+ if (!realpathSync3(path).startsWith(root + sep6)) continue;
13936
14047
  } catch {
13937
14048
  continue;
13938
14049
  }
@@ -15153,7 +15264,7 @@ function redactBodyString(body) {
15153
15264
  } catch {
15154
15265
  }
15155
15266
  }
15156
- return body.replace(FORM_ASSIGNMENT_RE, (m, name) => isSensitiveBodyName(name) ? `${name}=${REDACTED}` : m).replace(JSON_FIELD_RE, (m, name, sep11) => isSensitiveBodyName(name) ? `"${name}"${sep11}"${REDACTED}"` : m).replace(JSON_FIELD_UNTERMINATED_RE, (m, name, sep11) => isSensitiveBodyName(name) ? `"${name}"${sep11}"${REDACTED}` : m).replace(BEARER_RE, (_m, s) => `${s} ${REDACTED}`);
15267
+ return body.replace(FORM_ASSIGNMENT_RE, (m, name) => isSensitiveBodyName(name) ? `${name}=${REDACTED}` : m).replace(JSON_FIELD_RE, (m, name, sep12) => isSensitiveBodyName(name) ? `"${name}"${sep12}"${REDACTED}"` : m).replace(JSON_FIELD_UNTERMINATED_RE, (m, name, sep12) => isSensitiveBodyName(name) ? `"${name}"${sep12}"${REDACTED}` : m).replace(BEARER_RE, (_m, s) => `${s} ${REDACTED}`);
15157
15268
  }
15158
15269
  function createWixGatewayState() {
15159
15270
  return { mode: "record", rules: [], traffic: [], clients: /* @__PURE__ */ new Set(), seq: 0, ruleSeq: 0 };
@@ -18604,6 +18715,7 @@ function shouldRevealError(args) {
18604
18715
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
18605
18716
  var init_validation = __esm({
18606
18717
  "../../core/ts/src/validation.ts"() {
18718
+ "use strict";
18607
18719
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
18608
18720
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
18609
18721
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -22028,6 +22140,7 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
22028
22140
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
22029
22141
  var init_worker_subtree = __esm({
22030
22142
  "../../core/ts/src/worker-subtree.ts"() {
22143
+ "use strict";
22031
22144
  init_worker_subtree_safe_types_generated();
22032
22145
  WORKER_SUBTREE_CEILINGS = Object.freeze({
22033
22146
  maxEncodedBytes: 65536,
@@ -22255,8 +22368,8 @@ function ensureCountUpRuntime() {
22255
22368
  const neg = fixed.startsWith("-");
22256
22369
  const body = neg ? fixed.slice(1) : fixed;
22257
22370
  const [int, frac] = body.split(".");
22258
- const sep11 = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
22259
- return (neg ? "-" : "") + sep11 + (frac ? "." + frac : "");
22371
+ const sep12 = int.replace(/\B(?=(\d{3})+(?!\d))/g, ",");
22372
+ return (neg ? "-" : "") + sep12 + (frac ? "." + frac : "");
22260
22373
  };
22261
22374
  const animateEl = (el) => {
22262
22375
  const node = el;
@@ -30107,7 +30220,7 @@ var init_config2 = __esm({
30107
30220
  import { createServer as createServer2 } from "node:http";
30108
30221
  import { readFile as readFile2, stat } from "node:fs/promises";
30109
30222
  import { readFileSync as readFileSync40, existsSync as existsSync35, watch, mkdirSync as mkdirSync16, rmSync as rmSync4 } from "node:fs";
30110
- import { extname, resolve as resolve30, normalize as normalize3, sep as sep6, join as join34 } from "node:path";
30223
+ import { extname, resolve as resolve30, normalize as normalize3, sep as sep7, join as join34 } from "node:path";
30111
30224
  import { createHash as createHash15, randomUUID } from "node:crypto";
30112
30225
  import { fileURLToPath as fileURLToPath18, pathToFileURL as pathToFileURL6 } from "node:url";
30113
30226
  function deepMerge2(base, patch) {
@@ -30925,7 +31038,7 @@ function startBundleSourceWatcher(state) {
30925
31038
  try {
30926
31039
  const watcher = watch(TIER5_SRC_ROOT, { persistent: false, recursive: true }, (_event, filename) => {
30927
31040
  if (!filename || !/\.(ts|tsx)$/.test(filename)) return;
30928
- const name = filename.split(sep6)[0];
31041
+ const name = filename.split(sep7)[0];
30929
31042
  if (!name || name.startsWith(".")) return;
30930
31043
  pending.add(name);
30931
31044
  if (timer) clearTimeout(timer);
@@ -31100,7 +31213,7 @@ function startAppSourceWatcher(state) {
31100
31213
  try {
31101
31214
  const watcher = watch(appDir2, { persistent: false, recursive: true }, (_event, filename) => {
31102
31215
  if (!filename) return;
31103
- const rel = String(filename).split(sep6).join("/");
31216
+ const rel = String(filename).split(sep7).join("/");
31104
31217
  if (!classifyAppPath(rel)) return;
31105
31218
  pending.add(rel);
31106
31219
  if (timer) clearTimeout(timer);
@@ -34617,8 +34730,8 @@ async function crawlScreenInner(opts) {
34617
34730
  if (linkable.has(target)) return true;
34618
34731
  for (const base of linkable) {
34619
34732
  if (target.length > base.length + 1 && target.startsWith(base)) {
34620
- const sep11 = target[base.length];
34621
- if (sep11 === "_" || sep11 === "/") return true;
34733
+ const sep12 = target[base.length];
34734
+ if (sep12 === "_" || sep12 === "/") return true;
34622
34735
  }
34623
34736
  }
34624
34737
  return false;
@@ -38279,7 +38392,7 @@ __export(app_source_exports, {
38279
38392
  zipAppSource: () => zipAppSource
38280
38393
  });
38281
38394
  import { readdirSync as readdirSync21, lstatSync as lstatSync5, readFileSync as readFileSync44, mkdirSync as mkdirSync18, writeFileSync as writeFileSync14, existsSync as existsSync38, rmSync as rmSync6, rmdirSync, unlinkSync as unlinkSync3 } from "node:fs";
38282
- import { join as join37, resolve as resolve34, sep as sep7 } from "node:path";
38395
+ import { join as join37, resolve as resolve34, sep as sep8 } from "node:path";
38283
38396
  import { deflateRawSync as deflateRawSync2 } from "node:zlib";
38284
38397
  function walkSourceTree(dir, file, skip) {
38285
38398
  const walk2 = (relDir) => {
@@ -38299,7 +38412,7 @@ function walkSourceTree(dir, file, skip) {
38299
38412
  skip(rel);
38300
38413
  continue;
38301
38414
  }
38302
- file(rel.split(sep7).join("/"), rel);
38415
+ file(rel.split(sep8).join("/"), rel);
38303
38416
  }
38304
38417
  };
38305
38418
  walk2("");
@@ -38440,7 +38553,7 @@ function pruneEmptyDirs(root, rel, needed, removed) {
38440
38553
  let empty = true;
38441
38554
  for (const name of readdirSync21(rel ? join37(root, rel) : root)) {
38442
38555
  const childRel = rel ? join37(rel, name) : name;
38443
- const posix3 = childRel.split(sep7).join("/");
38556
+ const posix3 = childRel.split(sep8).join("/");
38444
38557
  const kind = lstatSync5(join37(root, childRel));
38445
38558
  if (kind.isSymbolicLink() || !kind.isDirectory() || isDeniedSourceDir(name, rel)) {
38446
38559
  empty = false;
@@ -38507,7 +38620,7 @@ __export(eject_exports, {
38507
38620
  ejectModule: () => ejectModule
38508
38621
  });
38509
38622
  import { existsSync as existsSync39, mkdirSync as mkdirSync19, readFileSync as readFileSync45, writeFileSync as writeFileSync15 } from "node:fs";
38510
- import { dirname as dirname25, join as join38, relative as relative5, resolve as resolve35, sep as sep8 } from "node:path";
38623
+ import { dirname as dirname25, join as join38, relative as relative5, resolve as resolve35, sep as sep9 } from "node:path";
38511
38624
  function locate(repoRootOrCwd, mod) {
38512
38625
  const inRepo = resolve35(repoRootOrCwd, `stdlib/flows/${mod}/src/index.ts`);
38513
38626
  if (existsSync39(inRepo)) return { path: inRepo, form: "source" };
@@ -38560,7 +38673,7 @@ function ejectModule(opts) {
38560
38673
  if (!existsSync39(f)) continue;
38561
38674
  const before = readFileSync45(f, "utf8");
38562
38675
  let after = before;
38563
- let rel = relative5(dirname25(f), target).split(sep8).join("/").replace(/\.tsx?$/, ".js");
38676
+ let rel = relative5(dirname25(f), target).split(sep9).join("/").replace(/\.tsx?$/, ".js");
38564
38677
  if (!rel.startsWith(".")) rel = `./${rel}`;
38565
38678
  for (const spec of specifiersFor(mod)) {
38566
38679
  const q = spec.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -41350,7 +41463,7 @@ import { createServer as createServer3 } from "node:http";
41350
41463
  import { createHash as createHash22, randomBytes as randomBytes5 } from "node:crypto";
41351
41464
  import { execFile as execFile2 } from "node:child_process";
41352
41465
  import { existsSync as existsSync44, readdirSync as readdirSync22, readFileSync as readFileSync50, realpathSync as realpathSync4, statSync as statSync8 } from "node:fs";
41353
- import { join as join43, posix, relative as relative6, sep as sep9 } from "node:path";
41466
+ import { join as join43, posix, relative as relative6, sep as sep10 } from "node:path";
41354
41467
  function humanMs(ms) {
41355
41468
  return ms < 1e3 ? `${ms}ms` : `${Math.round(ms / 1e3)}s`;
41356
41469
  }
@@ -41400,7 +41513,7 @@ function collectRelativeSiblings(files, tier5SrcDir) {
41400
41513
  ];
41401
41514
  for (const abs of candidates) {
41402
41515
  if (!existsSync44(abs) || statSync8(abs).isDirectory()) continue;
41403
- const key2 = `/tier5-src/${relative6(tier5SrcDir, abs).split(sep9).join(posix.sep)}`;
41516
+ const key2 = `/tier5-src/${relative6(tier5SrcDir, abs).split(sep10).join(posix.sep)}`;
41404
41517
  if (key2 in files) break;
41405
41518
  files[key2] = readFileSync50(abs, "utf8");
41406
41519
  queue.push(key2);
@@ -48865,7 +48978,7 @@ var init_home_descriptor = __esm({
48865
48978
 
48866
48979
  // src/site-analyze/apply.ts
48867
48980
  import { existsSync as existsSync52, readFileSync as readFileSync62, writeFileSync as writeFileSync21, copyFileSync as copyFileSync3, mkdirSync as mkdirSync24, realpathSync as realpathSync5, statSync as statSync11 } from "node:fs";
48868
- import { dirname as dirname29, isAbsolute as isAbsolute4, join as join50, resolve as resolve40, sep as sep10 } from "node:path";
48981
+ import { dirname as dirname29, isAbsolute as isAbsolute4, join as join50, resolve as resolve40, sep as sep11 } from "node:path";
48869
48982
  import { spawnSync as spawnSync4 } from "node:child_process";
48870
48983
  function unpairedRedirectNote(overlay) {
48871
48984
  if (!overlay.wix?.clientId || overlay.wix.redirectUri) return void 0;
@@ -48960,7 +49073,7 @@ function buildMergedContract(options) {
48960
49073
  const contained = (root, rel) => {
48961
49074
  if (isAbsolute4(rel)) return void 0;
48962
49075
  const full = resolve40(root, rel);
48963
- return full.startsWith(resolve40(root) + sep10) ? full : void 0;
49076
+ return full.startsWith(resolve40(root) + sep11) ? full : void 0;
48964
49077
  };
48965
49078
  const stage = (fromRel, toRel) => {
48966
49079
  const from = contained(packageDir, fromRel);
@@ -48974,7 +49087,7 @@ function buildMergedContract(options) {
48974
49087
  } catch {
48975
49088
  return void 0;
48976
49089
  }
48977
- if (!real.startsWith(realRoot + sep10)) return void 0;
49090
+ if (!real.startsWith(realRoot + sep11)) return void 0;
48978
49091
  try {
48979
49092
  if (!statSync11(real).isFile()) return void 0;
48980
49093
  mkdirSync24(dirname29(to), { recursive: true });
@@ -55263,7 +55376,7 @@ var COMMANDS = [
55263
55376
  { name: "eject", summary: "take ownership of a shared flow module (auth|checkout|screens): copies its source into tier5-src/_ejected/ and repoints this package's imports at the copy. The ladder's escape hatch \u2014 for when the factory options cannot express what you need. AFTER EJECTING, CENTRAL FIXES NO LONGER REACH THIS APP, including future corrections to the fail-closed entitlement and confirm-the-session rules. The floor is unaffected: credentials still go only through the native auth actions and money only through the cashier rail.", required: "<auth|checkout|screens> [--content <dir>]" },
55264
55377
  { name: "validate", summary: "lint the content package (no push): fixed-contract TypeScript checking for tier5-src workers + schema/proto + token refs + component allowlist + $service bindings + strict-enum foot-guns + capability decls + worker boot dry-run + build/compile failures. Type issues are advisory warnings normally; --strict promotes them and hygiene warnings to errors. Exit 1 on any error. Off-repo, the CLI supplies its compiler and SDK declarations.", required: "[--content <dir>] [--strict]" },
55265
55378
  { name: "test", summary: "DERIVED behavioral crawl \u2014 no test files to write. Boots each screen's real worker, replays its onAppear, then dispatches every action site the descriptor already declares (each from a fresh boot) and checks a narrow set of unambiguous invariants: no uncaught throw, never stuck on `loading`, no `$service:` token or NaN/undefined/[object Object] reaching visible text, no binding aimed at state the worker never defines, no nav param silently dropped, no dead nav target. Catches what validate/render cannot \u2014 a WRONG REDUCER passes both with ok:true. Runs the worker in a DEVICE-PARITY sandbox (no setTimeout/setInterval, as on JSC/WebView) so a bundle that would die on device fails here. Crawls on HERMETIC DATA by default \u2014 zero network: the app's own dev/net-mocks.json first (--net <file> overrides), then the config's CMS `seed:` rows, then a canned fixture of frozen real vertical responses shipped with the CLI (exercises code paths, not API-contract truth) \u2014 so the success/mapping path runs, not just the failure UI. Two boot-only HOSTILE passes (every list empty; every request 500) then check the failure/empty day \u2014 corpus-proven, so their findings are errors. NAV EDGES are followed too: every himi.open site ($item params resolved against row 0 of its list, $service against worker state) and worker _nav directive is harvested, and each baked target is booted WITH the params the source sends \u2014 detail screens finally run their content arm; edge findings are errors tagged edge:<source>. --native runs both real native decoders over the exact baked JSON bytes; --native ios or --native android selects one. It is explicit, runs only from a Himalaya SOURCE CHECKOUT (the decoder oracles are that tree's own HimalayaCore Swift package and Gradle harness, which a published install does not ship), and needs the selected local toolchain. From a source checkout, default runs name missing native verdicts in the coverage ledger; elsewhere the ledger stays silent about them rather than offering a remedy you cannot run. --offline strips all data layers (the old reject-all axis); --no-hostile skips the hostile passes. --record (EXPLICIT, never automatic) refreshes dev/net-mocks.json from live reads first: it boots each screen (load + root onAppear ONLY \u2014 nothing the app's own launch would not fire; the tap/action crawl never sees the live transport, and only proven reads \u2014 GET, or read-shaped POST (/query, /search, /list*, \u2026) \u2014 ever reach the wire), freezes the round-trips, then crawls hermetically on the fresh fixture; record against a QA site, not production. Nodes it cannot judge headless are named, never silently passed. Exit 1 on any error. The CLI carries the authoring runtime off-repo; no per-app SDK install is required. --browser runs the whole loop (build \u2192 extract \u2192 validate \u2192 render \u2192 test) in YOUR browser instead of here, so the source never reaches a pod: it opens one screen's worker + descriptor in a tab, blocks on a loopback callback, and prints the report it posts back. One screen per tab \u2014 name it. --no-open prints the URL rather than launching a browser (SSH); --timeout <ms> bounds the wait, which matters because the page has no execution deadline of its own and an infinite loop in the worker hangs the tab.", required: "[<screenId>\u2026] [--content <dir>] [--net <file>] [--budget <ms>] [--deep [n]] [--record] [--native [ios|android]] [--no-shots] [--offline] [--no-hostile] [--all-screens] [--browser] [--no-open] [--timeout <ms>] [--json]" },
55266
- { name: "ux", summary: "audit or record mobile UX evidence. `audit` checks the public-safe contract, seeded blank acceptance rows, placeholder source copy, app-specific mocks, and iOS/Android audit records. It warns while existing templates migrate; `--strict` promotes missing evidence. `record` stores checks performed on an installed iOS/Android build: `--checks` is required for passed|failed and empty for unavailable, which drove nothing and needs a `--reason` instead.", required: "audit [--content <dir>] [--strict] [--json] | record --content <dir> --platform ios|android --status passed|failed|unavailable [--checks back,keyboard,...] [--reason <text>]" },
55379
+ { name: "ux", summary: "audit or record mobile UX evidence. `audit` checks the public-safe contract, seeded blank acceptance rows, placeholder source copy, app-specific mocks, and iOS/Android audit records. It warns while existing templates migrate; `--strict` promotes missing evidence. `record` stores checks performed on an installed iOS/Android build: `--checks` is required for passed|failed and empty for unavailable, which drove nothing and needs a `--reason` instead. `record` also STAMPS the release it audited (it resolves the id from a build, the same one a push would publish), because an unforced `himi push` of a release with screens refuses when no drive is recorded for THAT release, or when the one that is recorded says `failed` \u2014 the artifact stopped being advice nobody reads (#2955). `unavailable` still satisfies it; only `failed` refuses.", required: "audit [--content <dir>] [--strict] [--json] | record --content <dir> --platform ios|android --status passed|failed|unavailable [--checks back,keyboard,...] [--reason <text>]" },
55267
55380
  { name: "render", summary: "headless-render a screen with the web renderer: render-tree + diagnostics, optional --png <file>, --scheme dark, --tree. Local by default (content dir). Pass --remote --app --release to hit GET /v1/preview/\u2026/render on the serve plane after a push. Boots the worker so $service bindings resolve; --static raw bindings; --live real Wix fetches; --state injects worker state; --input supplies pre-boot navigation params; --width/--device for PNG viewport; --rtl for dir=rtl + $env:isRTL.", required: "<screenId> [--content <dir>] [--scheme dark] [--png <file>] [--tree] [--static] [--live] [--state <json|file>] [--input <json|file>] [--width <px> | --device <name>] [--rtl] [--remote --app --release]" },
55268
55381
  { name: "cms", summary: "inspect, provision, and seed the app's CMS (Wix Data) collections, declared as a `cms` block in config.ts. `query` is READ-ONLY \u2014 by default it fetches ONLY the physical ids `config.ts` declares (item counts + schema drift) and reports `siteScanned: false`; it never lists the site, so an UNDECLARED `himi_<app>_` collection this app owns is not reached without `--all`. `--all` SCANS the site \u2014 every collection it lists is labeled `owned`/`adopted`/`foreign`, and it describes every foreign one \u2014 id, non-system field keys + types, and the site's read role \u2014 so an app with no `cms` block at all can DISCOVER a collection to adopt (`--all` is the one case where a missing `cms` block is not 'nothing to do'). Foreign collections are described up to a cap and are never item-counted (`itemCount: null`), because a row count is one query each; `foreign.count` is how many the scan LISTED \u2014 NOT a site census, since the list endpoint is paged and is not followed \u2014 and `foreign.truncated` says how many of those went undescribed, while `unlistedCollections` reports what the listing itself left out (from Wix's own paging total). The scan is OWNER-scoped \u2014 it needs --site <msid> (or WIX_SITE_ID) plus owner auth via `wix token`; a visitor token can read a public collection's ITEMS but can never enumerate collections, so there is no tokenless discovery path. `create` reconciles declared collections additively and idempotently, and REFUSES any id outside the app's `himi_<app>_` namespace, so it is safe against a production site. It reports declared-vs-site `permissionDrift` but never writes permissions unless asked with --apply-permissions, so a field reconcile can't silently revert access control changed in the dashboard. `seed` inserts the git-tracked `seed:` rows into EMPTY owned collections (--force to add to a non-empty one; --ai writes placeholder rows plus a refinement prompt for you to review). Adopted collections (declared by `wixId`) are read-only and never created, updated, or seeded. Site from --site <msid> or WIX_SITE_ID; owner auth via `wix token`.", required: "query|create|seed [--site <msid>] [--content <dir>] [--all] [--dry-run] [--force] [--allow-destructive] [--apply-permissions] [--only <id>] [--ai] [--count <n>]" },
55269
55382
  { name: "data", summary: "read-only plan or explicitly provision/seed declared KV and Wix Data resources. Preview and production are separate scopes. KV administration uses the operator data plane from --data-url/HIMI_DATA_ADMIN_URL and HIMI_DATA_ADMIN_TOKEN; draft push never calls it.", required: "plan|create|seed [--site <msid>] [--environment preview|production] [--content <dir>] [--data-url <url>] [--dry-run] [--force]" },
@@ -55274,7 +55387,7 @@ var COMMANDS = [
55274
55387
  { 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]" },
55275
55388
  { 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]" },
55276
55389
  { 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>]" },
55277
- { 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). `--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)" },
55390
+ { 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)" },
55278
55391
  { 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>" },
55279
55392
  { 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]" },
55280
55393
  { 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>]" },
@@ -56244,6 +56357,102 @@ function resolutionFlag(flags) {
56244
56357
  if (flags.source === true) return "source";
56245
56358
  return void 0;
56246
56359
  }
56360
+ async function recordedReleaseForContentQuietly(dir, resolution) {
56361
+ let out;
56362
+ try {
56363
+ const { buildContentForValidation: buildContentForValidation2, buildReleaseFromContent: buildReleaseFromContent2, bundleOutDir: bundleOutDir2 } = await loadAuthoring();
56364
+ const bres = await buildContentForValidation2(dir, { resolution });
56365
+ out = mkdtempSync4(join51(tmpdir5(), "himi-ux-record-"));
56366
+ const hasWorkers = bres.built.length > 0 || bres.compileErrors.length > 0;
56367
+ const outcome = await buildReleaseFromContent2(dir, out, { ...hasWorkers ? { tier5Dir: bundleOutDir2(dir) } : {} });
56368
+ const releaseId = outcome.manifest.releaseId;
56369
+ if (!releaseId) return null;
56370
+ const payload = await loadReleaseFromDir(await appId(dir), out);
56371
+ return { releaseId, releaseHash: releasePayloadHash(payload) };
56372
+ } catch {
56373
+ return null;
56374
+ } finally {
56375
+ if (out !== void 0) try {
56376
+ rmSync9(out, { recursive: true, force: true });
56377
+ } catch {
56378
+ }
56379
+ }
56380
+ }
56381
+ function releaseCarriesScreens(...manifests) {
56382
+ return manifests.some((m) => Object.keys(m?.screens ?? {}).length > 0);
56383
+ }
56384
+ function handoverUxEvidence(dir, release) {
56385
+ if (!release.carriesScreens) return null;
56386
+ const evidence = uxEvidenceForRelease(dir, { releaseId: release.releaseId, releaseHash: release.releaseHash });
56387
+ const inheritedScreens = release.inheritedScreens ?? [];
56388
+ if (inheritedScreens.length > 0) {
56389
+ return {
56390
+ ...evidence,
56391
+ state: "inherited-screens",
56392
+ inheritedScreens,
56393
+ ...evidence.state === "recorded" ? {} : { supersededState: evidence.state }
56394
+ };
56395
+ }
56396
+ return evidence;
56397
+ }
56398
+ function uxRecordCommands(contentPath) {
56399
+ return `\`himi ux audit --content ${contentPath} --strict\`, then \`himi ux record --content ${contentPath} --platform ios --status unavailable --reason "<why you could not drive it>"\``;
56400
+ }
56401
+ function uxRestoreCommands(ctx) {
56402
+ const pinned = ctx.baseReleaseId ? ` --release ${ctx.quote(ctx.baseReleaseId)}` : "";
56403
+ return {
56404
+ pull: `\`himi pull ${ctx.quote(ctx.app)}${pinned}${ctx.planeArgs ?? ""} --out ${ctx.quote("<dir>")}\``,
56405
+ record: uxRecordCommands(ctx.quote(`<dir>/${ctx.app}`))
56406
+ };
56407
+ }
56408
+ function uxHandoverHint(uxEvidence, ctx) {
56409
+ const { releaseId, contentArg, door, app } = ctx;
56410
+ const uxRemedy = uxRecordCommands(contentArg);
56411
+ if (uxEvidence.state === "inherited-screens") {
56412
+ const restore = uxRestoreCommands(ctx);
56413
+ const alsoOwed = uxEvidence.supersededState === "failed" ? ` This package's own drive FAILED on ${uxEvidence.failedPlatforms.join(", ")}, so you owe more than a record: fix what that drive found, then record it against the restored tree \u2014 re-recording a failure without changing the app writes the same failure down again.` : uxEvidence.supersededState ? ` This package also has no drive this gate accepts (${uxEvidence.supersededState}), so the record below is a step you take AFTER the restore, not instead of it.` : "";
56414
+ return `this patch derives from ${ctx.baseReleaseId ?? "a release"} which carries ${uxEvidence.inheritedScreens.length} screen(s) your package does not contain (${uxEvidence.inheritedScreens.join(", ")}), so a drive of this package cannot have seen them \u2014 and a tokens or bundle patch changes how they look or behave.${alsoOwed} Restore THAT release's source with ${restore.pull}, apply the change you were publishing to the restored tree, and only THEN record the drive there: ${restore.record} \u2014 the record is matched against the package you push, so one taken before your change describes the base and goes stale the moment you apply it. Patch from that tree. Do NOT run a full \`himi push\` from this package instead: publishing a tree that is missing those screens REMOVES them. If that release was published with \`--no-source\`, or before source archiving shipped, the pull answers \`app_source_not_found\` and the override below is the only exit. Only if you intend to hand a customer a preview of screens nobody drove, \`${door} --force\`.`;
56415
+ }
56416
+ if (uxEvidence.state === "failed") {
56417
+ return `a mobile-UX drive for ${releaseId} IS recorded, and it FAILED on ${uxEvidence.failedPlatforms.join(", ")} (see ${uxEvidence.path}). A published draft is the owner-private preview a customer is handed, so fix what the drive found and re-record it \u2014 re-running \`himi ux record\` over a failure without changing the app just writes the same failure down again. Only if you intend to hand a customer a preview whose UX you already know is broken, \`${door} --force\`.`;
56418
+ }
56419
+ const why = uxEvidence.state === "absent" ? `nothing has recorded a mobile-UX drive for this package \u2014 ${uxEvidence.path} does not exist` : uxEvidence.state === "unrecorded" ? uxEvidence.malformed.length ? `${uxEvidence.path} has no iOS or Android drive this gate can read \u2014 ${uxEvidence.malformed.join("; ")}` : `${uxEvidence.path} exists but carries no iOS or Android drive (\`himi render\` writes its rendered widths there too, so the file alone proves nothing)` : uxEvidence.state === "unreadable" ? `${ctx.quote(`${ctx.contentDir}/${uxEvidence.path}`)} ${uxEvidence.error} \u2014 delete it and record again, because re-recording over a malformed store does not repair it` : uxEvidence.state === "outdated" ? `a mobile-UX drive for ${releaseId} is recorded, but the package has CHANGED since \u2014 in-repo a release id is the git HEAD sha, so editing a screen, a token or a worker without committing keeps the id while replacing everything it identifies (a record written before this check also lands here, carrying no content hash)` : `the recorded mobile-UX drive is about ${uxEvidence.otherReleases.length ? `release ${uxEvidence.otherReleases.join(", ")}` : "no release at all"}, not ${releaseId}${uxEvidence.unstamped ? ` (${uxEvidence.unstamped} drive(s) name no release)` : ""}`;
56420
+ return `${why}. A published draft is the owner-private preview a customer is handed, so record the drive first: ${uxRemedy}, then re-run \`${door}\`. Only if you intend to hand over a preview WITHOUT that evidence, \`${door} --force\`.`;
56421
+ }
56422
+ var SECRET_FLAG_NAME = /token|secret|password|cookie|auth/i;
56423
+ var NEVER_ECHOED = /* @__PURE__ */ new Set(["force", "help", "json"]);
56424
+ var PLANE_FLAGS = ["target", "serve-url", "control-url"];
56425
+ function planeArguments(flags, quote) {
56426
+ return PLANE_FLAGS.filter((name) => typeof flags[name] === "string").map((name) => ` --${name} ${quote(String(flags[name]))}`).join("");
56427
+ }
56428
+ function echoedInvocation(command, flags, quote) {
56429
+ const parts = [command];
56430
+ for (const [name, value] of Object.entries(flags)) {
56431
+ if (value === false || NEVER_ECHOED.has(name) || SECRET_FLAG_NAME.test(name)) continue;
56432
+ parts.push(`--${name}`);
56433
+ if (typeof value === "string") parts.push(quote(value));
56434
+ }
56435
+ return parts.join(" ");
56436
+ }
56437
+ function uxHandoverNote(uxEvidence, ctx) {
56438
+ if (uxEvidence.state === "inherited-screens") {
56439
+ const restore = uxRestoreCommands(ctx);
56440
+ const drive = uxEvidence.supersededState === "failed" ? `A drive of this package is on record and it FAILED on ${uxEvidence.failedPlatforms.join(", ")} \u2014 and it could not have covered them in any case:` : uxEvidence.supersededState ? `No drive of this package is one this gate accepts (${uxEvidence.supersededState}), and a drive of this package could not have covered them in any case:` : "A drive of this package IS recorded, and it could not have covered them in any case:";
56441
+ return `published over ${uxEvidence.inheritedScreens.length} screen(s) nobody drove (${uxEvidence.inheritedScreens.join(", ")}). ${drive} the preview link below renders them from ${ctx.baseReleaseId ?? "the base release"}, and this package does not contain them. Restore that release's source with ${restore.pull}, apply the change you just published to it, drive and record there (${restore.record}), and publish again from that tree before a customer reviews it \u2014 the record is matched against the package you push, so one taken before your change describes the base.`;
56442
+ }
56443
+ if (uxEvidence.state === "failed") {
56444
+ return `published over a FAILED mobile-UX drive for ${ctx.releaseId} on ${uxEvidence.failedPlatforms.join(", ")}. The preview link below is the owner-private handover \u2014 fix what the drive found and publish again before a customer reviews it.`;
56445
+ }
56446
+ if (uxEvidence.state === "unreadable") {
56447
+ const why = uxEvidence.error ? ` \u2014 ${uxEvidence.path} ${uxEvidence.error}` : ` \u2014 ${uxEvidence.path} could not be read`;
56448
+ return `published with no mobile-UX drive this gate accepts for ${ctx.releaseId} (unreadable)${why}. The preview link below is the owner-private handover \u2014 DELETE ${ctx.quote(`${ctx.contentDir}/${uxEvidence.path}`)} and record the drive again with \`himi ux record --content ${ctx.contentArg} \u2026\`, because re-recording over a malformed store does not repair it, then publish again before a customer reviews it.`;
56449
+ }
56450
+ return `published with no mobile-UX drive this gate accepts for ${ctx.releaseId} (${uxEvidence.state}). The preview link below is the owner-private handover \u2014 record the drive with \`himi ux record --content ${ctx.contentArg} \u2026\` and publish again before a customer reviews it.`;
56451
+ }
56452
+ function uxHandoverOverride(uxEvidence, ctx) {
56453
+ if (!uxEvidence || uxEvidence.state === "recorded") return {};
56454
+ return { uxEvidence, uxEvidenceNote: uxHandoverNote(uxEvidence, ctx) };
56455
+ }
56247
56456
  async function loadAuthoring() {
56248
56457
  try {
56249
56458
  const [b, rb, crawl, vd, cov, wml, nf, rd] = await Promise.all([
@@ -56460,6 +56669,17 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
56460
56669
  }, null, 2));
56461
56670
  return 1;
56462
56671
  }
56672
+ if (opts.ux?.evidence && opts.ux.evidence.state !== "recorded" && !opts.force) {
56673
+ print({
56674
+ ok: false,
56675
+ patched: false,
56676
+ pushed: false,
56677
+ uxEvidence: opts.ux.evidence,
56678
+ releaseId: opts.ux.ctx.releaseId,
56679
+ hint: uxHandoverHint(opts.ux.evidence, opts.ux.ctx)
56680
+ });
56681
+ return 1;
56682
+ }
56463
56683
  let r;
56464
56684
  try {
56465
56685
  r = await client2().releasePatch(app, base.releaseId, plan.patch, draft, opts.meta, opts.force);
@@ -56502,6 +56722,9 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
56502
56722
  ...opts.lintNote ? { lintNote: opts.lintNote } : {},
56503
56723
  attestation: honesty.attestation,
56504
56724
  source: honesty.source,
56725
+ // Spread through the SAME helper the full push uses, so a forced partial publish records
56726
+ // the override in the same words rather than in a second phrasing that can drift.
56727
+ ...opts.ux ? uxHandoverOverride(opts.ux.evidence, opts.ux.ctx) : {},
56505
56728
  ...out.visibility ? { visibility: out.visibility } : {},
56506
56729
  ...opts.extra ?? {},
56507
56730
  previewWeb: sbase && release ? previewWebUrl(sbase, app, release, void 0, void 0, previewToken) : null,
@@ -57132,17 +57355,27 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
57132
57355
  const platform = str5(flags.platform);
57133
57356
  const status = str5(flags.status);
57134
57357
  const checks = str5(flags.checks)?.split(",").map((item) => item.trim()).filter(Boolean) ?? [];
57135
- if (platform !== "ios" && platform !== "android") {
57136
- io.error(JSON.stringify({ error: "ux record needs --platform ios|android" }));
57358
+ if (!isNativePlatform(platform)) {
57359
+ io.error(JSON.stringify({ error: `ux record needs --platform ${NATIVE_PLATFORMS.join("|")}` }));
57137
57360
  return 2;
57138
57361
  }
57139
- if (status !== "passed" && status !== "failed" && status !== "unavailable") {
57140
- io.error(JSON.stringify({ error: "ux record needs --status passed|failed|unavailable" }));
57362
+ if (!isNativeAuditStatus(status)) {
57363
+ io.error(JSON.stringify({ error: `ux record needs --status ${NATIVE_AUDIT_STATUSES.join("|")}` }));
57141
57364
  return 2;
57142
57365
  }
57366
+ const uxRelease = await recordedReleaseForContentQuietly(dir, resolutionFlag(flags));
57143
57367
  try {
57144
- const evidence = recordNativeAudit(dir, platform, status, checks, str5(flags.reason));
57145
- print({ ok: true, content: dir, evidence });
57368
+ const evidence = recordNativeAudit(dir, platform, status, checks, str5(flags.reason), uxRelease ?? void 0);
57369
+ print({
57370
+ ok: true,
57371
+ content: dir,
57372
+ releaseId: uxRelease?.releaseId ?? null,
57373
+ releaseHash: uxRelease?.releaseHash ?? null,
57374
+ ...uxRelease ? {} : {
57375
+ releaseIdNote: "this package could not be built, so the record names no release \u2014 `himi push` will not accept it as evidence for the release it publishes. Fix the build and re-run this command."
57376
+ },
57377
+ evidence
57378
+ });
57146
57379
  return 0;
57147
57380
  } catch (error) {
57148
57381
  io.error(JSON.stringify({ error: error.message }));
@@ -58161,6 +58394,9 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
58161
58394
  const kvDeclared = declaredData?.kv?.length ?? 0;
58162
58395
  const manifest = outcome.manifest;
58163
58396
  const payload = await loadReleaseFromDir(declared, out);
58397
+ const { shellQuote: shellQuote2 } = await Promise.resolve().then(() => (init_shell_quote(), shell_quote_exports));
58398
+ const contentRel = relative8(process.cwd(), dir) || ".";
58399
+ const contentArg = shellQuote2(contentRel);
58164
58400
  if (onlyRaw !== void 0) {
58165
58401
  const onlyDraft = flags.draft === true;
58166
58402
  const { parseOnlySelectors: parseOnlySelectors2, planFromPayload: planFromPayload2, notCarriedDiff: notCarriedDiff2 } = await Promise.resolve().then(() => (init_release_patch(), release_patch_exports));
@@ -58193,6 +58429,28 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
58193
58429
  io.error(JSON.stringify(planned.error, null, 2));
58194
58430
  return 1;
58195
58431
  }
58432
+ const patchUxEvidence = handoverUxEvidence(dir, {
58433
+ releaseId: manifest.releaseId,
58434
+ releaseHash: releasePayloadHash(payload),
58435
+ carriesScreens: releaseCarriesScreens(baseForPlan.manifest, manifest),
58436
+ // The screens this publish will serve whose BYTES this package has never held: the
58437
+ // derived release materializes them from the base. Local-only screens are not here —
58438
+ // those the author does have, and a drive of this package covers them.
58439
+ inheritedScreens: Object.keys(baseForPlan.manifest.screens ?? {}).filter((id) => !(id in (manifest.screens ?? {}))).sort()
58440
+ });
58441
+ const patchUxContext = {
58442
+ releaseId: manifest.releaseId,
58443
+ contentArg,
58444
+ contentDir: contentRel,
58445
+ app,
58446
+ door: echoedInvocation("himi push", flags, shellQuote2),
58447
+ // The base this plan DERIVES from, not the live release: under `--from-release` those
58448
+ // differ, and a restore of the wrong one hands the author a different set of screen
58449
+ // bytes to drive.
58450
+ baseReleaseId: baseForPlan.manifest.releaseId,
58451
+ quote: shellQuote2,
58452
+ planeArgs: planeArguments(flags, shellQuote2)
58453
+ };
58196
58454
  let localSourceHash;
58197
58455
  try {
58198
58456
  const { collectAppSource: collectAppSource2, zipAppSource: zipAppSource2 } = await Promise.resolve().then(() => (init_app_source(), app_source_exports));
@@ -58221,6 +58479,9 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
58221
58479
  },
58222
58480
  ...localSourceHash ? { localSourceHash } : {},
58223
58481
  notCarried: notCarriedDiff2(manifest, baseForPlan.manifest),
58482
+ // Reaches here ONLY on the forced path — the refusal above returned otherwise — so
58483
+ // this is the override's record, and the case it matters most for is a `failed` drive.
58484
+ ux: { evidence: patchUxEvidence, ctx: patchUxContext },
58224
58485
  lint: report.counts ?? null,
58225
58486
  extra: { selector: parsed.selectors.map((s) => "pattern" in s ? `${s.kind}:${s.pattern}` : s.kind) }
58226
58487
  });
@@ -58269,8 +58530,6 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
58269
58530
  const evidence = pushEvidence(dir, manifest.releaseId, (candidate) => verifyAttestation(candidate, dir, payload.screens, declared, payload, { requireLatest: false }).ok);
58270
58531
  const staleEvidence = evidence.staleRuns > 0 ? `${evidence.staleRuns} retained attestation(s) carry this release id, but none describes the content being published` : null;
58271
58532
  const attestationErrors = [...evidence.historyErrors, ...staleEvidence ? [staleEvidence] : []];
58272
- const { shellQuote: shellQuote2 } = await Promise.resolve().then(() => (init_shell_quote(), shell_quote_exports));
58273
- const contentArg = shellQuote2(relative8(process.cwd(), dir) || ".");
58274
58533
  if (attestationErrors.length > 0 && !force) {
58275
58534
  const remedy = `\`himi test --content ${contentArg}\` (or \`himi render <screenId> --content ${contentArg}\`)`;
58276
58535
  const corrupted = evidence.historyErrors.length > 0;
@@ -58292,6 +58551,35 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
58292
58551
  });
58293
58552
  return 1;
58294
58553
  }
58554
+ const uxEvidence = handoverUxEvidence(dir, {
58555
+ releaseId: manifest.releaseId,
58556
+ releaseHash: releasePayloadHash(payload),
58557
+ // On a FULL push the package is the release, so its own screens are the whole answer —
58558
+ // and nothing is inherited, which is why the limitation above is a `--only` concern only.
58559
+ carriesScreens: releaseCarriesScreens(manifest),
58560
+ inheritedScreens: []
58561
+ });
58562
+ if (uxEvidence && uxEvidence.state !== "recorded" && !force) {
58563
+ print({
58564
+ ok: false,
58565
+ pushed: false,
58566
+ uxEvidence,
58567
+ releaseId: manifest.releaseId,
58568
+ // The SAME reconstruction as the patch door: this one dropped `--content` too, and
58569
+ // fixing one door and not the other is the drift the shared helpers exist to stop.
58570
+ hint: uxHandoverHint(uxEvidence, {
58571
+ releaseId: manifest.releaseId,
58572
+ contentArg,
58573
+ contentDir: contentRel,
58574
+ app,
58575
+ door: echoedInvocation("himi push", flags, shellQuote2),
58576
+ quote: shellQuote2
58577
+ // No `baseReleaseId`: a full push derives from nothing, so it can never produce
58578
+ // `inherited-screens` and has no base to restore.
58579
+ })
58580
+ });
58581
+ return 1;
58582
+ }
58295
58583
  const forcedPastErrors = force && (hasErrors || attestation?.report.ok === false || attestationErrors.length > 0);
58296
58584
  const assetRecords = Object.entries(manifest.assets ?? {});
58297
58585
  const assetSummary = assetRecords.length ? {
@@ -58434,6 +58722,18 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
58434
58722
  ...!attestation && staleEvidence ? {
58435
58723
  attestationNote: `${staleEvidence} \u2014 this release was published with no headless evidence of its own, and the serve plane may still show the attestation from an earlier push of this release id. Re-attest with \`himi test --content ${contentArg}\` and push again to attach one.`
58436
58724
  } : {},
58725
+ // #2955 — the SAME honesty for the UX gate: `--force` published this preview without a
58726
+ // recorded mobile-UX drive for it, and the link below goes to a customer. Say it in the
58727
+ // success envelope, because a refusal nobody reads and an override nobody records are
58728
+ // the same silence one step apart.
58729
+ ...uxHandoverOverride(uxEvidence, {
58730
+ releaseId: manifest.releaseId,
58731
+ contentArg,
58732
+ contentDir: contentRel,
58733
+ app,
58734
+ door: echoedInvocation("himi push", flags, shellQuote2),
58735
+ quote: shellQuote2
58736
+ }),
58437
58737
  ...loopbackWarning ? { warning: loopbackWarning } : {},
58438
58738
  ...report.issues ? { issues: report.issues } : {},
58439
58739
  previewWeb: base ? previewWebUrl(base, app, release, void 0, void 0, previewToken) : null,
@@ -61147,6 +61447,9 @@ export {
61147
61447
  switchFlagErrors,
61148
61448
  templatePreviewAuthError,
61149
61449
  tokenCoversApp,
61450
+ uxHandoverHint,
61451
+ uxHandoverNote,
61452
+ uxHandoverOverride,
61150
61453
  wixPlayerTestflightTarget,
61151
61454
  writePngFile
61152
61455
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.967.0",
3
+ "version": "0.969.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": "21a64fc457cad496774edeab407735299722e76708687a0a3a6dcaaf"
67
+ "falconPackageHash": "5fbb2ba85ad9db892370efbcc59a8ef6629609514feb06522e4ea1f2"
68
68
  }