@wix/himalaya-cli 0.1246.0 → 0.1248.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -13465,7 +13465,6 @@ function resolveIdiom(tokens2, ctx = {}) {
13465
13465
  var IDIOM_CONTEXTS, UNSAFE_KEYS;
13466
13466
  var init_idiom = __esm({
13467
13467
  "../../core/ts/src/idiom.ts"() {
13468
- "use strict";
13469
13468
  IDIOM_CONTEXTS = {
13470
13469
  ios: { native: ["ios"] },
13471
13470
  android: { native: ["material"] },
@@ -25749,6 +25748,7 @@ function shouldRevealError(args) {
25749
25748
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
25750
25749
  var init_validation = __esm({
25751
25750
  "../../core/ts/src/validation.ts"() {
25751
+ "use strict";
25752
25752
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
25753
25753
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
25754
25754
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -26408,6 +26408,7 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
26408
26408
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
26409
26409
  var init_worker_subtree = __esm({
26410
26410
  "../../core/ts/src/worker-subtree.ts"() {
26411
+ "use strict";
26411
26412
  init_worker_subtree_safe_types_generated();
26412
26413
  WORKER_SUBTREE_CEILINGS = Object.freeze({
26413
26414
  maxEncodedBytes: 65536,
@@ -132926,6 +132927,72 @@ var init_studio = __esm({
132926
132927
  }
132927
132928
  });
132928
132929
 
132930
+ // src/push-errors.ts
132931
+ var push_errors_exports = {};
132932
+ __export(push_errors_exports, {
132933
+ PUSH_FAILURE_CODES: () => PUSH_FAILURE_CODES,
132934
+ pushFailure: () => pushFailure,
132935
+ uxEvidenceCode: () => uxEvidenceCode
132936
+ });
132937
+ function pushFailure(code, fields) {
132938
+ return { ...fields, ok: false, code };
132939
+ }
132940
+ function uxEvidenceCode(state) {
132941
+ return UX_CODES[state];
132942
+ }
132943
+ var PUSH_FAILURE_CODES, UX_CODES;
132944
+ var init_push_errors = __esm({
132945
+ "src/push-errors.ts"() {
132946
+ PUSH_FAILURE_CODES = [
132947
+ "attestation_history_corrupt",
132948
+ "attestation_stale",
132949
+ "headless_run_failed",
132950
+ "ux_evidence_absent",
132951
+ "ux_evidence_unrecorded",
132952
+ "ux_evidence_stale",
132953
+ "ux_evidence_outdated",
132954
+ "ux_evidence_failed",
132955
+ "ux_evidence_inherited_screens",
132956
+ "ux_evidence_unreadable",
132957
+ "lint_errors",
132958
+ "control_token_expired",
132959
+ "app_source_contains_secrets",
132960
+ "app_source_too_large",
132961
+ "publish_body_too_large",
132962
+ "needs_vpn",
132963
+ "serve_unreachable",
132964
+ "no_serve_plane",
132965
+ "upload_http_error",
132966
+ "claim_failed",
132967
+ "invalid_flag_value",
132968
+ "invalid_selector",
132969
+ "duplicate_only",
132970
+ "incompatible_push_flags",
132971
+ "from_release_needs_a_value",
132972
+ "conflicting_blast_radius",
132973
+ "no_blast_radius",
132974
+ "invalid_visibility",
132975
+ "release_read_failed",
132976
+ "base_not_found",
132977
+ "no_live_release",
132978
+ "invalid_patch",
132979
+ "would_regress",
132980
+ "not_carried_by_patch",
132981
+ "authoring_unavailable",
132982
+ "push_failed"
132983
+ ];
132984
+ UX_CODES = {
132985
+ absent: "ux_evidence_absent",
132986
+ unrecorded: "ux_evidence_unrecorded",
132987
+ stale: "ux_evidence_stale",
132988
+ outdated: "ux_evidence_outdated",
132989
+ failed: "ux_evidence_failed",
132990
+ "inherited-screens": "ux_evidence_inherited_screens",
132991
+ unreadable: "ux_evidence_unreadable"
132992
+ };
132993
+ }
132994
+ });
132995
+
132929
132996
  // src/release-patch.ts
132930
132997
  var release_patch_exports = {};
132931
132998
  __export(release_patch_exports, {
@@ -143740,23 +143807,23 @@ var init_preview_assets_generated = __esm({
143740
143807
  "../../packages/serve/src/preview-assets.generated.ts"() {
143741
143808
  init_preview_assets_format();
143742
143809
  PREVIEW_ASSET_MANIFEST = [
143743
- { key: "assets/index-CCVohdaU.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1194284, sha256: "60579d944c7c43f8809233a2156473912f13308a6013a0c2e8b3dee36b91abf8" },
143744
- { key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/octet-stream", offset: 1194292, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
143745
- { key: "assets/pdfDocument-lN1LAubz.js", contentType: "application/javascript; charset=utf-8", offset: 2511326, length: 489166, sha256: "3b61b9d31976437476b3e8e34999f82c9b14fea30155d98c30ee92135ff23573" },
143746
- { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 3000492, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
143747
- { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3005102, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
143748
- { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3493686, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
143749
- { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3584610, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
143750
- { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 3675594, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
143751
- { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 3766574, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
143752
- { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 3857446, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
143753
- { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 3948426, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
143754
- { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4057062, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
143755
- { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4166146, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
143756
- { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4275194, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
143757
- { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4382894, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
143758
- { key: "index.html", contentType: "text/html; charset=utf-8", offset: 4471002, length: 4947, sha256: "d4e51bbbba0a856f8060f0b9268c9efd0a7da047d82048d722cf8bcb9b326b01" },
143759
- { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4475949, length: 17344, sha256: "d057298d75e7373e5e2c4d11b7d1bcbea88e03af555ba63d97641c78509c58e4" }
143810
+ { key: "assets/index-C_dUZJT0.js", contentType: "application/javascript; charset=utf-8", offset: 8, length: 1194463, sha256: "6640d516f6b59bacd5f71dbcb264dbdc9aeef5502fb284d0680d91ae40ea141a" },
143811
+ { key: "assets/pdf.worker.min-BmVo14Nb.mjs", contentType: "application/octet-stream", offset: 1194471, length: 1317034, sha256: "a33cfe728c584fdba4fcc1fd54bcdc2f9f2f13889ddbb5b2bd1d0f8cbe49b84e" },
143812
+ { key: "assets/pdfDocument-sHOpLp0o.js", contentType: "application/javascript; charset=utf-8", offset: 2511505, length: 489166, sha256: "57445251df98761d808381a823b3f01c948f41faeae9e61d1bcd9ab8ef1eadc3" },
143813
+ { key: "fonts/Roboto-LICENSE.txt", contentType: "application/octet-stream", offset: 3000671, length: 4610, sha256: "994062dd7af4186a739de55dd9e151c63b6301f8e2cbafc608d35c2e87da0cd2" },
143814
+ { key: "fonts/Roboto-Variable.ttf", contentType: "font/ttf", offset: 3005281, length: 488584, sha256: "d7598e12c5dbef095ff8272cfc55da0250bd07fbdecbac8a530b9b277872a134" },
143815
+ { key: "fonts/WixMadeforDisplay-Bold.ttf", contentType: "font/ttf", offset: 3493865, length: 90924, sha256: "ec4c7d63772d980d6e147b2df6215fd2737e4b04cebe0cf7e11dc695fb63cdcf" },
143816
+ { key: "fonts/WixMadeforDisplay-ExtraBold.ttf", contentType: "font/ttf", offset: 3584789, length: 90984, sha256: "bf4c16a2774edd38a921edf3099469866cd74c094fd596f3fa4e81f54a04dfaa" },
143817
+ { key: "fonts/WixMadeforDisplay-Medium.ttf", contentType: "font/ttf", offset: 3675773, length: 90980, sha256: "368faca44fb9f7207e4af784e94750b1ee59c1098b8689e0f4a918926f244fb5" },
143818
+ { key: "fonts/WixMadeforDisplay-Regular.ttf", contentType: "font/ttf", offset: 3766753, length: 90872, sha256: "97ed8f1a6772abe26e3d8af5cecfc4b02faa94497d585b0398b5053b739a70c1" },
143819
+ { key: "fonts/WixMadeforDisplay-SemiBold.ttf", contentType: "font/ttf", offset: 3857625, length: 90980, sha256: "642feab8a48ddf6c7a89d7c2a7c0edfa0e6e773f671287e460e222703563191e" },
143820
+ { key: "fonts/WixMadeforText-Bold.ttf", contentType: "font/ttf", offset: 3948605, length: 108636, sha256: "37c179b4fefb63ea9637322b84583a2df53aa226c150294eea1675c14b1b8e29" },
143821
+ { key: "fonts/WixMadeforText-ExtraBold.ttf", contentType: "font/ttf", offset: 4057241, length: 109084, sha256: "62e2fe4718129f36ecdb0ae4de385bd6b81ecc876146447167c6c140fad699a9" },
143822
+ { key: "fonts/WixMadeforText-Medium.ttf", contentType: "font/ttf", offset: 4166325, length: 109048, sha256: "4ecf270136af928612c76fe4ebade91d8156975e5ea4c0cf9225501dd1788e02" },
143823
+ { key: "fonts/WixMadeforText-Regular.ttf", contentType: "font/ttf", offset: 4275373, length: 107700, sha256: "dee56f09fd3efc127c1ea95557edc74110c1460a46287da36e0af25efd341ef5" },
143824
+ { key: "fonts/WixMadeforText-SemiBold.ttf", contentType: "font/ttf", offset: 4383073, length: 88108, sha256: "dced97a77624bc5f8b1ae65850360908297f4588e09feb6f22792d29dc7e765b" },
143825
+ { key: "index.html", contentType: "text/html; charset=utf-8", offset: 4471181, length: 4947, sha256: "cecdf774237e111e8306c71e372f32f6fdad1f8ac7c0d7736723bf79fea24503" },
143826
+ { key: "t5-frame.html", contentType: "text/html; charset=utf-8", offset: 4476128, length: 17344, sha256: "d057298d75e7373e5e2c4d11b7d1bcbea88e03af555ba63d97641c78509c58e4" }
143760
143827
  ];
143761
143828
  }
143762
143829
  });
@@ -163742,11 +163809,11 @@ async function contentVisibility(dir) {
163742
163809
  }
163743
163810
  return void 0;
163744
163811
  }
163745
- function visibilityFlag(flags, io2) {
163812
+ function visibilityFlag(flags, io2, failure = (fields) => fields) {
163746
163813
  if (flags.visibility === void 0) return {};
163747
163814
  const v2 = str9(flags.visibility);
163748
163815
  if (v2 !== "public" && v2 !== "unlisted") {
163749
- io2.error(JSON.stringify({ error: `--visibility must be public or unlisted (got "${v2 ?? ""}")` }));
163816
+ io2.error(JSON.stringify(failure({ error: `--visibility must be public or unlisted (got "${v2 ?? ""}")` })));
163750
163817
  return null;
163751
163818
  }
163752
163819
  return { value: v2 };
@@ -164296,6 +164363,9 @@ async function runCommand(argv, io2, deps) {
164296
164363
  const { _: pos, flags } = parseFlags(argv);
164297
164364
  const flagErrors = switchFlagErrors(argv);
164298
164365
  const command = pos[0];
164366
+ const pushErrors = command === "push" ? await Promise.resolve().then(() => (init_push_errors(), push_errors_exports)) : void 0;
164367
+ const uxEvidenceCode2 = (state) => pushErrors?.uxEvidenceCode(state) ?? "ux_evidence_absent";
164368
+ const pushFailure2 = command === "push" ? pushErrors.pushFailure : (_code, fields) => fields;
164299
164369
  let updateNotice;
164300
164370
  const print = (o2) => io2.log(JSON.stringify(
164301
164371
  o2 && typeof o2 === "object" && !Array.isArray(o2) ? tailLast(updateNotice ? { ...o2, upgrade: upgradeEnvelope(updateNotice) } : o2) : o2,
@@ -164316,7 +164386,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
164316
164386
  return command === "help" || flags.help ? 0 : 2;
164317
164387
  }
164318
164388
  if (flagErrors.length > 0 && flags.help !== true) {
164319
- io2.error(JSON.stringify({ ok: false, error: "invalid_flag_value", problems: flagErrors, hint: "run `himi help` for the flag grammar." }));
164389
+ io2.error(JSON.stringify(pushFailure2("invalid_flag_value", { ok: false, error: "invalid_flag_value", problems: flagErrors, hint: "run `himi help` for the flag grammar." })));
164320
164390
  return 2;
164321
164391
  }
164322
164392
  if (flags["skip-version-check"] !== true) {
@@ -164343,11 +164413,11 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
164343
164413
  const base = serveBase();
164344
164414
  if (!isLoopbackBase(base)) return null;
164345
164415
  if (await servePlaneAnswers(() => client2().health("serve"))) return null;
164346
- io2.error(JSON.stringify(foreignPlaneError(targetName(), base)));
164416
+ io2.error(JSON.stringify(pushFailure2("no_serve_plane", foreignPlaneError(targetName(), base))));
164347
164417
  return 1;
164348
164418
  };
164349
164419
  const printServeFailure = (r2, extra) => {
164350
- print({ ok: false, ...extra, status: r2.status, body: r2.json ?? r2.text });
164420
+ print(pushFailure2("upload_http_error", { ok: false, ...extra, status: r2.status, body: r2.json ?? r2.text }));
164351
164421
  return 1;
164352
164422
  };
164353
164423
  const serveFailure = async (r2, extra) => {
@@ -164360,33 +164430,33 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
164360
164430
  const target = targetName();
164361
164431
  const baseFound = await fetchReleaseManifest(client2, app, opts.fromRelease);
164362
164432
  if ("error" in baseFound) {
164363
- io2.error(JSON.stringify(baseFound));
164433
+ io2.error(JSON.stringify(pushFailure2("release_read_failed", baseFound)));
164364
164434
  return 1;
164365
164435
  }
164366
164436
  const base = baseFound.manifest;
164367
164437
  if (!base) {
164368
- io2.error(JSON.stringify({
164438
+ io2.error(JSON.stringify(pushFailure2(opts.explicitBase ? "base_not_found" : "no_live_release", {
164369
164439
  ok: false,
164370
164440
  error: opts.explicitBase ? "base_not_found" : "no_live_release",
164371
164441
  app,
164372
164442
  ...opts.explicitBase ? { releaseId: opts.fromRelease } : {},
164373
164443
  message: opts.explicitBase ? `no release ${opts.fromRelease} for ${app}. Retention keeps the current release plus roughly the newest 20 ledger entries.` : `${app} has no live release to derive from. A patch changes part of an existing release; there is nothing here to be part of.`,
164374
164444
  hint: opts.explicitBase ? `list what exists: \`himi context --app ${app} --target ${target}\`` : "publish the whole app first: `himi push --draft` then `himi promote`."
164375
- }));
164445
+ })));
164376
164446
  return 1;
164377
164447
  }
164378
164448
  let regressions = [];
164379
164449
  {
164380
164450
  const liveFound = await fetchReleaseManifest(client2, app);
164381
164451
  if ("error" in liveFound) {
164382
- io2.error(JSON.stringify(liveFound));
164452
+ io2.error(JSON.stringify(pushFailure2("release_read_failed", liveFound)));
164383
164453
  return 1;
164384
164454
  }
164385
164455
  const live3 = liveFound.manifest;
164386
164456
  if (live3 && live3.releaseId !== base.releaseId) {
164387
164457
  regressions = regressionDiff2(base, live3, plan.touched);
164388
164458
  if (regressions.length > 0 && !opts.acceptRegressions) {
164389
- io2.error(JSON.stringify({
164459
+ io2.error(JSON.stringify(pushFailure2("would_regress", {
164390
164460
  ok: false,
164391
164461
  patched: false,
164392
164462
  error: "would_regress",
@@ -164398,14 +164468,14 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
164398
164468
  moves: plan.touched,
164399
164469
  hint: opts.explicitBase ? `patch the live release instead (drop --from-release), widen the patch to cover them, or re-run with --accept-regressions if reverting those ${regressions.length} item(s) is what you mean.` : `${app} was published again while this patch was being prepared, so the base it was planned against is no longer live. Re-run to plan against the new release, or pass --accept-regressions to derive from the older one anyway.`,
164400
164470
  proof: "core/serve-control/tests/routes-release-branch-patch.test.ts:126"
164401
- }, null, 2));
164471
+ }), null, 2));
164402
164472
  return 1;
164403
164473
  }
164404
164474
  }
164405
164475
  }
164406
164476
  const notCarried = opts.notCarried ?? [];
164407
164477
  if (notCarried.length > 0 && !opts.acceptPartial) {
164408
- io2.error(JSON.stringify({
164478
+ io2.error(JSON.stringify(pushFailure2("not_carried_by_patch", {
164409
164479
  ok: false,
164410
164480
  patched: false,
164411
164481
  error: "not_carried_by_patch",
@@ -164415,25 +164485,25 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
164415
164485
  notCarried,
164416
164486
  carriedByPatch: ["screens", "bundles", "tokens", "appConfig"],
164417
164487
  hint: "run a full `himi push` \u2014 it publishes assets, the icon, the surface envelope and minBinaryVersion. Re-run with --accept-partial only if you know those devices are already covered."
164418
- }, null, 2));
164488
+ }), null, 2));
164419
164489
  return 1;
164420
164490
  }
164421
164491
  if (opts.ux?.evidence && opts.ux.evidence.state !== "recorded" && !opts.force) {
164422
- print({
164492
+ print(pushFailure2(uxEvidenceCode2(opts.ux.evidence.state), {
164423
164493
  ok: false,
164424
164494
  patched: false,
164425
164495
  pushed: false,
164426
164496
  uxEvidence: opts.ux.evidence,
164427
164497
  releaseId: opts.ux.ctx.releaseId,
164428
164498
  hint: uxHandoverHint(opts.ux.evidence, opts.ux.ctx)
164429
- });
164499
+ }));
164430
164500
  return 1;
164431
164501
  }
164432
164502
  let r2;
164433
164503
  try {
164434
164504
  r2 = await client2().releasePatch(app, base.releaseId, plan.patch, draft, opts.meta, opts.force);
164435
164505
  } catch (e2) {
164436
- io2.error(JSON.stringify({ ok: false, patched: false, error: "serve_unreachable", app, message: e2.message }));
164506
+ io2.error(JSON.stringify(pushFailure2("serve_unreachable", { ok: false, patched: false, error: "serve_unreachable", app, message: e2.message })));
164437
164507
  return 1;
164438
164508
  }
164439
164509
  if (!r2.ok) {
@@ -164499,17 +164569,17 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
164499
164569
  const wantDraft = flags.draft === true;
164500
164570
  const wantLive = flags.live === true;
164501
164571
  if (wantDraft && wantLive) {
164502
- io2.error(JSON.stringify({ ok: false, error: "conflicting_blast_radius", message: "--draft and --live are mutually exclusive; pass exactly one." }));
164572
+ io2.error(JSON.stringify(pushFailure2("conflicting_blast_radius", { ok: false, error: "conflicting_blast_radius", message: "--draft and --live are mutually exclusive; pass exactly one." })));
164503
164573
  return null;
164504
164574
  }
164505
164575
  if (!wantDraft && !wantLive) {
164506
- io2.error(JSON.stringify({
164576
+ io2.error(JSON.stringify(pushFailure2("no_blast_radius", {
164507
164577
  ok: false,
164508
164578
  error: "no_blast_radius",
164509
164579
  message: "a patch must say where it lands: --draft (nothing changes on any device) or --live (every device, immediately).",
164510
164580
  hint: "`himi push` defaults to --draft because it publishes a whole release you then promote. A patch has no default, because the two outcomes are not comparable.",
164511
164581
  usage
164512
- }));
164582
+ })));
164513
164583
  return null;
164514
164584
  }
164515
164585
  return wantDraft;
@@ -166688,15 +166758,15 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166688
166758
  if (flags.only !== void 0) {
166689
166759
  onlyRaw = str9(flags.only);
166690
166760
  if (!onlyRaw) {
166691
- io2.error(JSON.stringify({ error: "--only needs a selector", usage: ONLY_USAGE }));
166761
+ io2.error(JSON.stringify(pushFailure2("invalid_selector", { error: "--only needs a selector", usage: ONLY_USAGE })));
166692
166762
  return 2;
166693
166763
  }
166694
166764
  if (argv.filter((a2) => a2 === "--only" || a2.startsWith("--only=")).length > 1) {
166695
- io2.error(JSON.stringify({
166765
+ io2.error(JSON.stringify(pushFailure2("duplicate_only", {
166696
166766
  error: "duplicate_only",
166697
166767
  usage: ONLY_USAGE,
166698
166768
  message: "--only was passed more than once, and only the last would have taken effect. Comma-separate the selectors instead: --only screen:a,screen:b"
166699
- }));
166769
+ })));
166700
166770
  return 2;
166701
166771
  }
166702
166772
  for (const [flag, why] of [
@@ -166704,12 +166774,12 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166704
166774
  ["visibility", "the patch route does not stamp visibility; set it with `himi visibility` instead"]
166705
166775
  ]) {
166706
166776
  if (flags[flag] !== void 0) {
166707
- io2.error(JSON.stringify({ error: `--${flag} is not meaningful with --only`, message: why, usage: ONLY_USAGE }));
166777
+ io2.error(JSON.stringify(pushFailure2("incompatible_push_flags", { error: `--${flag} is not meaningful with --only`, message: why, usage: ONLY_USAGE })));
166708
166778
  return 2;
166709
166779
  }
166710
166780
  }
166711
166781
  if (flags["from-release"] !== void 0 && !str9(flags["from-release"])) {
166712
- io2.error(JSON.stringify({ ok: false, error: "from_release_needs_a_value", message: "--from-release takes a release id to branch from. Omit the flag entirely to patch the live release.", usage: ONLY_USAGE }));
166782
+ io2.error(JSON.stringify(pushFailure2("from_release_needs_a_value", { ok: false, error: "from_release_needs_a_value", message: "--from-release takes a release id to branch from. Omit the flag entirely to patch the live release.", usage: ONLY_USAGE })));
166713
166783
  return 2;
166714
166784
  }
166715
166785
  if (patchBlastRadius(ONLY_USAGE) === null) return 2;
@@ -166728,14 +166798,14 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166728
166798
  const { loadTargetsFile: loadTargetsFile2 } = await Promise.resolve().then(() => (init_config(), config_exports));
166729
166799
  const configured = loadTargetsFile2();
166730
166800
  const usable = Object.keys(configured.targets ?? {}).filter((n2) => n2 !== pushTarget).filter((n2) => inspectControlToken(configured.targets?.[n2]?.controlToken).valid).sort().map((n2) => ({ target: n2, control: configured.targets?.[n2]?.control }));
166731
- io2.error(JSON.stringify({
166801
+ io2.error(JSON.stringify(pushFailure2("control_token_expired", {
166732
166802
  ok: false,
166733
166803
  pushed: false,
166734
166804
  error: "control_token_expired",
166735
166805
  target: pushTarget,
166736
166806
  ...usable.length ? { usableTargets: usable } : {},
166737
166807
  hint: `${pushAuthHint(401, pushToken, pushTarget, !!ownerIssue)}` + (usable.length ? ` \u2014 you are also logged in for ${usable.map((u2) => `${u2.target}${u2.control ? ` (${u2.control})` : ""}`).join(", ")}, but check the plane before switching: a different target publishes somewhere else and claims the slug there.` : "")
166738
- }));
166808
+ })));
166739
166809
  return 1;
166740
166810
  }
166741
166811
  let app = resolveAppSlug(declared, (pushToken ? decodeControlTokenClaims(pushToken) : null) ?? {});
@@ -166763,7 +166833,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166763
166833
  ...await navIssues2(dir, strict)
166764
166834
  ]);
166765
166835
  if (hasErrors && !force) {
166766
- print({ ...report3, pushed: false, hint: "lint found errors \u2014 fix them, or re-run `himi push --force` to publish anyway" });
166836
+ print(pushFailure2("lint_errors", { ...report3, pushed: false, hint: "lint found errors \u2014 fix them, or re-run `himi push --force` to publish anyway" }));
166767
166837
  return 1;
166768
166838
  }
166769
166839
  const declaredData = await loadDataResourcesQuietly(dir);
@@ -166782,28 +166852,28 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166782
166852
  bundles: Object.keys(manifest.bundles ?? {})
166783
166853
  });
166784
166854
  if (!parsed.ok) {
166785
- io2.error(JSON.stringify(parsed.error, null, 2));
166855
+ io2.error(JSON.stringify(pushFailure2("invalid_selector", parsed.error), null, 2));
166786
166856
  return 2;
166787
166857
  }
166788
166858
  const fromRelease = str9(flags["from-release"]);
166789
166859
  const baseForPlan = await fetchReleaseManifest(client2, app, fromRelease);
166790
166860
  if ("error" in baseForPlan) {
166791
- io2.error(JSON.stringify(baseForPlan));
166861
+ io2.error(JSON.stringify(pushFailure2("release_read_failed", baseForPlan)));
166792
166862
  return 1;
166793
166863
  }
166794
166864
  if (!baseForPlan.manifest) {
166795
- io2.error(JSON.stringify({
166865
+ io2.error(JSON.stringify(pushFailure2(fromRelease ? "base_not_found" : "no_live_release", {
166796
166866
  ok: false,
166797
166867
  error: fromRelease ? "base_not_found" : "no_live_release",
166798
166868
  app,
166799
166869
  message: fromRelease ? `no release ${fromRelease} for ${app}.` : `no published release for "${app}" to patch \u2014 run \`himi push\` first.`,
166800
166870
  hint: "if this app is registered under a different slug, pass --app <slug>."
166801
- }));
166871
+ })));
166802
166872
  return 1;
166803
166873
  }
166804
166874
  const planned = planFromPayload2(payload, baseForPlan.manifest, parsed.selectors);
166805
166875
  if (!planned.ok) {
166806
- io2.error(JSON.stringify(planned.error, null, 2));
166876
+ io2.error(JSON.stringify(pushFailure2("invalid_patch", planned.error), null, 2));
166807
166877
  return 1;
166808
166878
  }
166809
166879
  const patchUxEvidence = handoverUxEvidence(dir, {
@@ -166873,21 +166943,21 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166873
166943
  const collected = collectAppSource2(dir);
166874
166944
  const findings = secretFindings2(collected.entries);
166875
166945
  if (findings.length > 0) {
166876
- io2.error(JSON.stringify({
166946
+ io2.error(JSON.stringify(pushFailure2("app_source_contains_secrets", {
166877
166947
  ok: false,
166878
166948
  pushed: false,
166879
166949
  error: `refusing to archive the source: ${findings[0].path} looks like it contains ${findings[0].reason}`,
166880
166950
  file: findings[0].path,
166881
166951
  found: findings.map((f2) => f2.path),
166882
166952
  hint: "move it out of the content package (or into .himi/, .env, or a *.pem/*.p8 file, which are never archived), or push with --no-source"
166883
- }));
166953
+ })));
166884
166954
  return 1;
166885
166955
  }
166886
166956
  const zip = zipAppSource2(collected.entries);
166887
166957
  const encoded2 = zip.toString("base64");
166888
166958
  if (encoded2.length > MAX_APP_SOURCE_BYTES2) {
166889
166959
  const largest = largestEntries2(collected.entries);
166890
- io2.error(JSON.stringify({
166960
+ io2.error(JSON.stringify(pushFailure2("app_source_too_large", {
166891
166961
  ok: false,
166892
166962
  pushed: false,
166893
166963
  error: "app_source_too_large",
@@ -166897,7 +166967,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166897
166967
  files: collected.entries.length,
166898
166968
  largest,
166899
166969
  hint: `trim the content package \u2014 the largest entries are ${largest.map((e2) => `${e2.path} (${Math.round(e2.compressed / 1024)} KB compressed)`).join(", ")}. Sizes are COMPRESSED, because the cap is measured on the deflated archive \u2014 a big but compressible source file is rarely what costs you it. Build output, dependencies, native shells and tooling output (.himi/, and shots/ + evidence/ at the package root) are already excluded. \`--no-source\` publishes without the archive, but then \`himi pull\` cannot restore this release.`
166900
- }));
166970
+ })));
166901
166971
  return 1;
166902
166972
  }
166903
166973
  appSourceB64 = encoded2;
@@ -166914,22 +166984,22 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166914
166984
  if (attestationErrors.length > 0 && !force) {
166915
166985
  const remedy = `\`himi test --content ${contentArg2}\` (or \`himi render <screenId> --content ${contentArg2}\`)`;
166916
166986
  const corrupted = evidence.historyErrors.length > 0;
166917
- print({
166987
+ print(pushFailure2(corrupted ? "attestation_history_corrupt" : "attestation_stale", {
166918
166988
  ok: false,
166919
166989
  pushed: false,
166920
166990
  errors: attestationErrors,
166921
166991
  hint: corrupted ? `attestation history in .himi/attestations/ is corrupt, so the retained runs cannot be trusted \u2014 a fresh run does not remove the bad entry. Delete .himi/attestations/, re-attest with ${remedy}, then re-run \`himi push\`. Only if you intend to publish WITHOUT that evidence, \`himi push --force\`.` : `no retained attestation for this release passed live verification${drifting.length ? `. ${releaseDriftCause(drifting, "Re-attesting", "an attestation")}, then re-attest` : " \u2014 runs recorded at this commit before your latest edits describe a tree that no longer exists. Re-attest"} the CURRENT release with ${remedy}, then re-run \`himi push\`. Only if you intend to publish WITHOUT that evidence, \`himi push --force\`.`
166922
- });
166992
+ }));
166923
166993
  return 1;
166924
166994
  }
166925
166995
  const attestation = evidence.selected ? summaryOf(evidence.selected) : void 0;
166926
166996
  if (attestation?.report.ok === false && !force) {
166927
- print({
166997
+ print(pushFailure2("headless_run_failed", {
166928
166998
  ok: false,
166929
166999
  pushed: false,
166930
167000
  attestation,
166931
167001
  hint: "the latest verified headless run failed \u2014 fix it and rerun the tool, or re-run `himi push --force` to publish anyway"
166932
- });
167002
+ }));
166933
167003
  return 1;
166934
167004
  }
166935
167005
  const uxEvidence = handoverUxEvidence(dir, {
@@ -166941,7 +167011,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166941
167011
  inheritedScreens: []
166942
167012
  });
166943
167013
  if (uxEvidence && uxEvidence.state !== "recorded" && !force) {
166944
- print({
167014
+ print(pushFailure2(uxEvidenceCode2(uxEvidence.state), {
166945
167015
  ok: false,
166946
167016
  pushed: false,
166947
167017
  uxEvidence,
@@ -166959,7 +167029,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166959
167029
  // `inherited-screens` and has no base to restore.
166960
167030
  nondeterministicWorkers: drifting
166961
167031
  })
166962
- });
167032
+ }));
166963
167033
  return 1;
166964
167034
  }
166965
167035
  const forcedPastErrors = force && (hasErrors || attestation?.report.ok === false || attestationErrors.length > 0);
@@ -166970,7 +167040,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166970
167040
  bakedBytes: assetRecords.filter(([, a2]) => a2.baked).reduce((n2, [, a2]) => n2 + a2.bytes, 0),
166971
167041
  otaOnly: assetRecords.filter(([, a2]) => !a2.baked).map(([name]) => name)
166972
167042
  } : null;
166973
- const visArg = visibilityFlag(flags, io2);
167043
+ const visArg = visibilityFlag(flags, io2, (fields) => pushFailure2("invalid_visibility", fields));
166974
167044
  if (!visArg) return 2;
166975
167045
  const visibility = visArg.value ?? await contentVisibility(dir);
166976
167046
  const meta = {
@@ -166989,7 +167059,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166989
167059
  const publishBodyBytesFor = (slug) => publishReleaseBodyBytes(publishBodyFor(slug), meta, draft);
166990
167060
  const refuseOversizedBody = (slug, bodyBytes) => {
166991
167061
  const withoutSource = appSourceB64 ? bodyBytes - appSourceB64.length : bodyBytes;
166992
- io2.error(JSON.stringify({
167062
+ io2.error(JSON.stringify(pushFailure2("publish_body_too_large", {
166993
167063
  ok: false,
166994
167064
  app: slug,
166995
167065
  pushed: false,
@@ -166998,7 +167068,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
166998
167068
  limit: MAX_PUBLISH_BODY_BYTES,
166999
167069
  ...appSourceB64 ? { appSourceBytes: appSourceB64.length } : {},
167000
167070
  hint: appSourceB64 && withoutSource <= MAX_PUBLISH_BODY_BYTES ? "push with --no-source to publish without the source archive (this release then cannot be restored with `himi pull`), or trim the app's assets and bundles" : "trim the app's assets and bundles \u2014 this release is over the limit even without the source archive, so --no-source would not help"
167001
- }));
167071
+ })));
167002
167072
  return 1;
167003
167073
  };
167004
167074
  const composedBodyBytes = publishBodyBytesFor(app);
@@ -167009,14 +167079,14 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
167009
167079
  if (ownerIssue && pushToken && !tokenCoversApp(pushToken, app)) {
167010
167080
  const claim = await autoClaimSlug({ ownerIssue, controlToken: pushToken, app, accountId: str9(flags["account-id"]) });
167011
167081
  if (claim.fatal) {
167012
- io2.error(JSON.stringify({
167082
+ io2.error(JSON.stringify(pushFailure2("claim_failed", {
167013
167083
  ok: false,
167014
167084
  app,
167015
167085
  pushed: false,
167016
167086
  status: claim.fatal.status,
167017
167087
  ...claim.fatal.body,
167018
167088
  ...claimHint(claim.fatal.status, claim.fatal.error)
167019
- }));
167089
+ })));
167020
167090
  return 1;
167021
167091
  }
167022
167092
  if (claim.refusal) io2.error(JSON.stringify({ warning: claim.refusal.error, hint: claim.refusal.hint }));
@@ -167037,16 +167107,16 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
167037
167107
  r2 = await client2().publishRelease(publishPayload, meta, draft);
167038
167108
  } catch (e2) {
167039
167109
  const target = targetName();
167040
- io2.error(JSON.stringify({
167110
+ io2.error(JSON.stringify(pushFailure2("needs_vpn", {
167041
167111
  ...serveUnreachableError(target, serveBase(), e2.message),
167042
167112
  version: cliVersion()
167043
- }));
167113
+ })));
167044
167114
  return 1;
167045
167115
  }
167046
167116
  if (!r2.ok) {
167047
167117
  const authHint = pushAuthHint(r2.status, pushToken, pushTarget, !!ownerIssue);
167048
167118
  const hint = authHint ?? releaseIdExistsHint(r2.status, r2.json);
167049
- print({ ok: false, status: r2.status, body: r2.json ?? r2.text, ...hint ? { hint } : {} });
167119
+ print(pushFailure2("upload_http_error", { ok: false, status: r2.status, body: r2.json ?? r2.text, ...hint ? { hint } : {} }));
167050
167120
  return 1;
167051
167121
  }
167052
167122
  const release = r2.json?.releaseId ?? manifest.releaseId;
@@ -170283,15 +170353,15 @@ Code: ${userCode}`);
170283
170353
  }
170284
170354
  } catch (err) {
170285
170355
  if (err instanceof AuthoringUnavailable) {
170286
- io2.error(JSON.stringify({
170356
+ io2.error(JSON.stringify(pushFailure2("authoring_unavailable", {
170287
170357
  error: `the himi authoring runtime failed to load (himi v${cliVersion()}). Off-repo this almost always means an out-of-date CLI, NOT a platform limitation \u2014 upgrade: \`npm i -g @wix/himalaya-cli@latest\`. If you meant to run in-repo, run from a monorepo checkout.`,
170288
170358
  version: cliVersion(),
170289
170359
  upgrade: "npm i -g @wix/himalaya-cli@latest",
170290
170360
  portal: DEVELOPER_PORTAL_URL
170291
- }));
170361
+ })));
170292
170362
  return 1;
170293
170363
  }
170294
- io2.error(JSON.stringify({ error: err.message }));
170364
+ io2.error(JSON.stringify(pushFailure2("push_failed", { error: err.message })));
170295
170365
  const code = err.exitCode;
170296
170366
  return code === 2 ? 2 : 1;
170297
170367
  }
Binary file
@@ -278,6 +278,7 @@ Concrete patterns lifted from real usage:
278
278
  - **Screen root** — `withModifiers(root, { padding: { leading: 16, trailing: 16, top: 16, bottom: 40 }, background: { token: "surfacePrimary" } })`. Consistent gutter, comfortable bottom inset.
279
279
  - **Row with a leading label and trailing detail** — `hstack({ spacing: 8, children: [ icon, text, spacer(), trailingText ] })`. The `spacer()` does the push; don't hand-pad.
280
280
  - **Fill-width controls share a row** — put fill-width controls in **one** `HStack` and let them share it (weight/flex), rather than stacking `fillMaxWidth` elements. This is a real iOS↔Android parity requirement, not a preference — a full-width control alone in an HStack renders differently across platforms.
281
+ - **A screen that owns its top bar (search beside back)** — set `topBar: { leading: "none" }` and put the row in `stickyHeader`: `hstack([ iconButton({ icon: "arrow_left", accessibilityLabel: "Back", action: { actionId: "nav.back", presentation: "back" } }), withModifiers(textField({ placeholder: "Search", … }), { weight: 1 }) ])`. With **both** set, every shell draws no bar and no back control of its own (iOS hides the system bar, Android floats no chevron and reserves no band, web hides its bar on every look), so the row is the only top bar. `leading: "none"` without a `stickyHeader` keeps the bar's title and trailing actions but drops the back control (and the hamburger, for that screen). System back — hardware, gesture, edge swipe, browser — still pops. Any other `leading` value keeps today's chrome (#3486; demo: `gallery_search_topbar`, linked from the gallery's "Lists & data").
281
282
  - **Cell list** — `card([ vstack({ spacing: 0, alignment: "leading", children: [ …ListItem cells… ] }) ])`. `spacing: 0` because the cells own their own dividers/padding.
282
283
  - **Overlay** — `zstack({ alignment: "center", children: [ backdropImage(cornerRadius 12), withModifiers(label, { padding, background: { token: "accentPrimary" }, cornerRadius: 10 }) ] })`.
283
284
  - **Responsive** — `grid({ columns: 1, columnsMedium: 2, spacing: 12 })` for 1-up on phone / 2-up on tablet; `splitView({ primary, detail, primaryMaxWidth: 280 })` for master/detail that stacks on a phone; set `contentMaxWidth` in tokens so wide surfaces keep a readable column instead of running edge-to-edge.
@@ -3,6 +3,10 @@
3
3
  Author a Himi app from your own machine, push it to the production serve as a **draft**,
4
4
  preview it, read the worker's logs, then promote it live. Nothing is live until you promote.
5
5
 
6
+ For machine consumers, `himi push` failures carry a stable top-level `code`.
7
+ See [JSON failure codes](himi-json-output.md) (`himi docs himi-json-output`);
8
+ branch on that code rather than human prose or envelope shape.
9
+
6
10
  ## 1. Get the CLI
7
11
 
8
12
  ```sh
@@ -0,0 +1,84 @@
1
+ # himi JSON failure contract
2
+
3
+ Every `ok: false` JSON envelope must carry a top-level `code`: a stable, typed
4
+ snake_case identifier for consumers to branch on. Codes are never renamed or
5
+ repurposed when human wording changes. New codes may be added; consumers must
6
+ handle an unknown code as a failure, without parsing prose.
7
+
8
+ **Rollout:** this contract is enforced for **`himi push`**, including `--only`,
9
+ first. Other subcommands still have legacy failures without codes and are a
10
+ follow-up migration. This is an additive change: existing `error`, `errors`,
11
+ `message`, `hint`, status, body, and gate-specific fields stay as they were.
12
+ Some legacy envelopes have no singular `error` or `hint`; consumers must not
13
+ require those fields. Existing error-only push refusals now also carry `ok: false`.
14
+ Output channels and process exit codes are unchanged. Read JSON failures on both
15
+ stdout and stderr; progress/warnings are not failure envelopes.
16
+
17
+ `code` is the machine contract. `error`/`errors`/`message` describe the failure for
18
+ people and `hint`, when present, suggests recovery. Neither their wording nor the
19
+ presence of one of those legacy shapes identifies the failure class. The complete
20
+ push code vocabulary is listed below.
21
+
22
+ For example, an attestation that no longer describes the content being published
23
+ keeps its existing fields and adds the code:
24
+
25
+ ```json
26
+ {
27
+ "ok": false,
28
+ "pushed": false,
29
+ "errors": ["2 retained attestation(s) were recorded at this commit, but none describes the content being published"],
30
+ "hint": "…",
31
+ "code": "attestation_stale"
32
+ }
33
+ ```
34
+
35
+ ## Push codes
36
+
37
+ | Code | Meaning |
38
+ | --- | --- |
39
+ | `attestation_history_corrupt` | Retained attestation history failed integrity checks. Takes precedence if stale runs are also present. |
40
+ | `attestation_stale` | Retained runs at this commit do not describe the current release. |
41
+ | `headless_run_failed` | A verified headless run failed. |
42
+ | `ux_evidence_absent` | No UX evidence artifact exists. |
43
+ | `ux_evidence_unrecorded` | No usable native UX drive was recorded. |
44
+ | `ux_evidence_stale` | UX evidence belongs to a different release/commit. |
45
+ | `ux_evidence_outdated` | UX evidence predates edits at this commit. |
46
+ | `ux_evidence_failed` | A matching UX drive failed. |
47
+ | `ux_evidence_inherited_screens` | A partial push inherits screens not covered by local UX evidence. |
48
+ | `ux_evidence_unreadable` | The UX evidence artifact cannot be read. |
49
+ | `lint_errors` | Build/lint validation found errors. |
50
+ | `control_token_expired` | The selected control token has expired. |
51
+ | `app_source_contains_secrets` | Source archiving detected a possible credential. |
52
+ | `app_source_too_large` | The encoded source archive exceeds its limit. |
53
+ | `publish_body_too_large` | The complete publish request exceeds its limit. |
54
+ | `needs_vpn` | Full-push request to the publishing service could not complete (existing reachability code). This is not proof that VPN is the cause; read the target-aware error. |
55
+ | `serve_unreachable` | A partial-push upload could not reach the publishing service. |
56
+ | `no_serve_plane` | A loopback service answering a partial push is not a himi publishing service. |
57
+ | `upload_http_error` | The upload returned a non-success HTTP status. `status` and the original `body` remain available; server errors never replace this CLI code. |
58
+ | `claim_failed` | Auto-claim returned a fatal refusal. Existing server fields and `status` remain available. |
59
+ | `invalid_flag_value` | A flag violates the CLI flag grammar. |
60
+ | `invalid_selector` | `--only` is missing a selector or its selector syntax is invalid. |
61
+ | `duplicate_only` | `--only` was repeated. |
62
+ | `incompatible_push_flags` | A flag cannot be combined with `--only`. |
63
+ | `from_release_needs_a_value` | `--from-release` has no value. |
64
+ | `conflicting_blast_radius` | A partial push supplied both `--draft` and `--live`. |
65
+ | `no_blast_radius` | A partial push supplied neither `--draft` nor `--live`. |
66
+ | `invalid_visibility` | The visibility value is invalid. |
67
+ | `release_read_failed` | A partial push could not read the base/live release. |
68
+ | `base_not_found` | The explicitly selected base has no manifest. |
69
+ | `no_live_release` | There is no live release to patch. |
70
+ | `invalid_patch` | The selection cannot produce a valid patch. The existing `error` and plan details are preserved. |
71
+ | `would_regress` | A partial push would revert unselected live changes. |
72
+ | `not_carried_by_patch` | The package changed dependencies that a partial push cannot carry. |
73
+ | `authoring_unavailable` | The bundled authoring runtime failed to load. |
74
+ | `push_failed` | An otherwise unclassified exception stopped push (for example unreadable content). The original error is preserved. |
75
+
76
+ The `ux_evidence_*` codes map to the existing typed `uxEvidence.state`; a successful
77
+ `recorded` state has no refusal code. Successful pushes and warnings do not gain
78
+ failure codes. A forced push still follows the existing override rules.
79
+
80
+ ## Follow-ups
81
+
82
+ - Migrate failure envelopes in subcommands other than `push`, keeping this contract.
83
+ - Review/reword human messages separately; no message rewrite is part of this change.
84
+ - Review exit-code conventions separately; no exit code changes are part of this change.
package/guides/index.json CHANGED
@@ -53,6 +53,12 @@
53
53
  "category": "CLI & Loop",
54
54
  "file": "headless-loop.md"
55
55
  },
56
+ {
57
+ "id": "himi-json-output",
58
+ "title": "JSON failures — stable error codes",
59
+ "category": "CLI & Loop",
60
+ "file": "himi-json-output.md"
61
+ },
56
62
  {
57
63
  "id": "himi-mcp",
58
64
  "title": "himi in your coding agent (MCP)",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1246.0",
3
+ "version": "0.1248.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -67,5 +67,5 @@
67
67
  "artifactId": "himalaya-cli"
68
68
  }
69
69
  },
70
- "falconPackageHash": "5e90fa27e7fc17c28b74ef6630a0742b66132390f44c0270ca325392"
70
+ "falconPackageHash": "c3978ffa25b74036a72561b7d81106fb8230ea398edab38ac57b4270"
71
71
  }