@wix/himalaya-cli 0.1246.0 → 0.1247.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 +122 -54
- package/guides/headless-loop.md +4 -0
- package/guides/himi-json-output.md +84 -0
- package/guides/index.json +6 -0
- package/package.json +2 -2
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"] },
|
|
@@ -132926,6 +132925,72 @@ var init_studio = __esm({
|
|
|
132926
132925
|
}
|
|
132927
132926
|
});
|
|
132928
132927
|
|
|
132928
|
+
// src/push-errors.ts
|
|
132929
|
+
var push_errors_exports = {};
|
|
132930
|
+
__export(push_errors_exports, {
|
|
132931
|
+
PUSH_FAILURE_CODES: () => PUSH_FAILURE_CODES,
|
|
132932
|
+
pushFailure: () => pushFailure,
|
|
132933
|
+
uxEvidenceCode: () => uxEvidenceCode
|
|
132934
|
+
});
|
|
132935
|
+
function pushFailure(code, fields) {
|
|
132936
|
+
return { ...fields, ok: false, code };
|
|
132937
|
+
}
|
|
132938
|
+
function uxEvidenceCode(state) {
|
|
132939
|
+
return UX_CODES[state];
|
|
132940
|
+
}
|
|
132941
|
+
var PUSH_FAILURE_CODES, UX_CODES;
|
|
132942
|
+
var init_push_errors = __esm({
|
|
132943
|
+
"src/push-errors.ts"() {
|
|
132944
|
+
PUSH_FAILURE_CODES = [
|
|
132945
|
+
"attestation_history_corrupt",
|
|
132946
|
+
"attestation_stale",
|
|
132947
|
+
"headless_run_failed",
|
|
132948
|
+
"ux_evidence_absent",
|
|
132949
|
+
"ux_evidence_unrecorded",
|
|
132950
|
+
"ux_evidence_stale",
|
|
132951
|
+
"ux_evidence_outdated",
|
|
132952
|
+
"ux_evidence_failed",
|
|
132953
|
+
"ux_evidence_inherited_screens",
|
|
132954
|
+
"ux_evidence_unreadable",
|
|
132955
|
+
"lint_errors",
|
|
132956
|
+
"control_token_expired",
|
|
132957
|
+
"app_source_contains_secrets",
|
|
132958
|
+
"app_source_too_large",
|
|
132959
|
+
"publish_body_too_large",
|
|
132960
|
+
"needs_vpn",
|
|
132961
|
+
"serve_unreachable",
|
|
132962
|
+
"no_serve_plane",
|
|
132963
|
+
"upload_http_error",
|
|
132964
|
+
"claim_failed",
|
|
132965
|
+
"invalid_flag_value",
|
|
132966
|
+
"invalid_selector",
|
|
132967
|
+
"duplicate_only",
|
|
132968
|
+
"incompatible_push_flags",
|
|
132969
|
+
"from_release_needs_a_value",
|
|
132970
|
+
"conflicting_blast_radius",
|
|
132971
|
+
"no_blast_radius",
|
|
132972
|
+
"invalid_visibility",
|
|
132973
|
+
"release_read_failed",
|
|
132974
|
+
"base_not_found",
|
|
132975
|
+
"no_live_release",
|
|
132976
|
+
"invalid_patch",
|
|
132977
|
+
"would_regress",
|
|
132978
|
+
"not_carried_by_patch",
|
|
132979
|
+
"authoring_unavailable",
|
|
132980
|
+
"push_failed"
|
|
132981
|
+
];
|
|
132982
|
+
UX_CODES = {
|
|
132983
|
+
absent: "ux_evidence_absent",
|
|
132984
|
+
unrecorded: "ux_evidence_unrecorded",
|
|
132985
|
+
stale: "ux_evidence_stale",
|
|
132986
|
+
outdated: "ux_evidence_outdated",
|
|
132987
|
+
failed: "ux_evidence_failed",
|
|
132988
|
+
"inherited-screens": "ux_evidence_inherited_screens",
|
|
132989
|
+
unreadable: "ux_evidence_unreadable"
|
|
132990
|
+
};
|
|
132991
|
+
}
|
|
132992
|
+
});
|
|
132993
|
+
|
|
132929
132994
|
// src/release-patch.ts
|
|
132930
132995
|
var release_patch_exports = {};
|
|
132931
132996
|
__export(release_patch_exports, {
|
|
@@ -163742,11 +163807,11 @@ async function contentVisibility(dir) {
|
|
|
163742
163807
|
}
|
|
163743
163808
|
return void 0;
|
|
163744
163809
|
}
|
|
163745
|
-
function visibilityFlag(flags, io2) {
|
|
163810
|
+
function visibilityFlag(flags, io2, failure = (fields) => fields) {
|
|
163746
163811
|
if (flags.visibility === void 0) return {};
|
|
163747
163812
|
const v2 = str9(flags.visibility);
|
|
163748
163813
|
if (v2 !== "public" && v2 !== "unlisted") {
|
|
163749
|
-
io2.error(JSON.stringify({ error: `--visibility must be public or unlisted (got "${v2 ?? ""}")` }));
|
|
163814
|
+
io2.error(JSON.stringify(failure({ error: `--visibility must be public or unlisted (got "${v2 ?? ""}")` })));
|
|
163750
163815
|
return null;
|
|
163751
163816
|
}
|
|
163752
163817
|
return { value: v2 };
|
|
@@ -164296,6 +164361,9 @@ async function runCommand(argv, io2, deps) {
|
|
|
164296
164361
|
const { _: pos, flags } = parseFlags(argv);
|
|
164297
164362
|
const flagErrors = switchFlagErrors(argv);
|
|
164298
164363
|
const command = pos[0];
|
|
164364
|
+
const pushErrors = command === "push" ? await Promise.resolve().then(() => (init_push_errors(), push_errors_exports)) : void 0;
|
|
164365
|
+
const uxEvidenceCode2 = (state) => pushErrors?.uxEvidenceCode(state) ?? "ux_evidence_absent";
|
|
164366
|
+
const pushFailure2 = command === "push" ? pushErrors.pushFailure : (_code, fields) => fields;
|
|
164299
164367
|
let updateNotice;
|
|
164300
164368
|
const print = (o2) => io2.log(JSON.stringify(
|
|
164301
164369
|
o2 && typeof o2 === "object" && !Array.isArray(o2) ? tailLast(updateNotice ? { ...o2, upgrade: upgradeEnvelope(updateNotice) } : o2) : o2,
|
|
@@ -164316,7 +164384,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
|
|
|
164316
164384
|
return command === "help" || flags.help ? 0 : 2;
|
|
164317
164385
|
}
|
|
164318
164386
|
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." }));
|
|
164387
|
+
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
164388
|
return 2;
|
|
164321
164389
|
}
|
|
164322
164390
|
if (flags["skip-version-check"] !== true) {
|
|
@@ -164343,11 +164411,11 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
|
|
|
164343
164411
|
const base = serveBase();
|
|
164344
164412
|
if (!isLoopbackBase(base)) return null;
|
|
164345
164413
|
if (await servePlaneAnswers(() => client2().health("serve"))) return null;
|
|
164346
|
-
io2.error(JSON.stringify(foreignPlaneError(targetName(), base)));
|
|
164414
|
+
io2.error(JSON.stringify(pushFailure2("no_serve_plane", foreignPlaneError(targetName(), base))));
|
|
164347
164415
|
return 1;
|
|
164348
164416
|
};
|
|
164349
164417
|
const printServeFailure = (r2, extra) => {
|
|
164350
|
-
print({ ok: false, ...extra, status: r2.status, body: r2.json ?? r2.text });
|
|
164418
|
+
print(pushFailure2("upload_http_error", { ok: false, ...extra, status: r2.status, body: r2.json ?? r2.text }));
|
|
164351
164419
|
return 1;
|
|
164352
164420
|
};
|
|
164353
164421
|
const serveFailure = async (r2, extra) => {
|
|
@@ -164360,33 +164428,33 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
|
|
|
164360
164428
|
const target = targetName();
|
|
164361
164429
|
const baseFound = await fetchReleaseManifest(client2, app, opts.fromRelease);
|
|
164362
164430
|
if ("error" in baseFound) {
|
|
164363
|
-
io2.error(JSON.stringify(baseFound));
|
|
164431
|
+
io2.error(JSON.stringify(pushFailure2("release_read_failed", baseFound)));
|
|
164364
164432
|
return 1;
|
|
164365
164433
|
}
|
|
164366
164434
|
const base = baseFound.manifest;
|
|
164367
164435
|
if (!base) {
|
|
164368
|
-
io2.error(JSON.stringify({
|
|
164436
|
+
io2.error(JSON.stringify(pushFailure2(opts.explicitBase ? "base_not_found" : "no_live_release", {
|
|
164369
164437
|
ok: false,
|
|
164370
164438
|
error: opts.explicitBase ? "base_not_found" : "no_live_release",
|
|
164371
164439
|
app,
|
|
164372
164440
|
...opts.explicitBase ? { releaseId: opts.fromRelease } : {},
|
|
164373
164441
|
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
164442
|
hint: opts.explicitBase ? `list what exists: \`himi context --app ${app} --target ${target}\`` : "publish the whole app first: `himi push --draft` then `himi promote`."
|
|
164375
|
-
}));
|
|
164443
|
+
})));
|
|
164376
164444
|
return 1;
|
|
164377
164445
|
}
|
|
164378
164446
|
let regressions = [];
|
|
164379
164447
|
{
|
|
164380
164448
|
const liveFound = await fetchReleaseManifest(client2, app);
|
|
164381
164449
|
if ("error" in liveFound) {
|
|
164382
|
-
io2.error(JSON.stringify(liveFound));
|
|
164450
|
+
io2.error(JSON.stringify(pushFailure2("release_read_failed", liveFound)));
|
|
164383
164451
|
return 1;
|
|
164384
164452
|
}
|
|
164385
164453
|
const live3 = liveFound.manifest;
|
|
164386
164454
|
if (live3 && live3.releaseId !== base.releaseId) {
|
|
164387
164455
|
regressions = regressionDiff2(base, live3, plan.touched);
|
|
164388
164456
|
if (regressions.length > 0 && !opts.acceptRegressions) {
|
|
164389
|
-
io2.error(JSON.stringify({
|
|
164457
|
+
io2.error(JSON.stringify(pushFailure2("would_regress", {
|
|
164390
164458
|
ok: false,
|
|
164391
164459
|
patched: false,
|
|
164392
164460
|
error: "would_regress",
|
|
@@ -164398,14 +164466,14 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
|
|
|
164398
164466
|
moves: plan.touched,
|
|
164399
164467
|
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
164468
|
proof: "core/serve-control/tests/routes-release-branch-patch.test.ts:126"
|
|
164401
|
-
}, null, 2));
|
|
164469
|
+
}), null, 2));
|
|
164402
164470
|
return 1;
|
|
164403
164471
|
}
|
|
164404
164472
|
}
|
|
164405
164473
|
}
|
|
164406
164474
|
const notCarried = opts.notCarried ?? [];
|
|
164407
164475
|
if (notCarried.length > 0 && !opts.acceptPartial) {
|
|
164408
|
-
io2.error(JSON.stringify({
|
|
164476
|
+
io2.error(JSON.stringify(pushFailure2("not_carried_by_patch", {
|
|
164409
164477
|
ok: false,
|
|
164410
164478
|
patched: false,
|
|
164411
164479
|
error: "not_carried_by_patch",
|
|
@@ -164415,25 +164483,25 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
|
|
|
164415
164483
|
notCarried,
|
|
164416
164484
|
carriedByPatch: ["screens", "bundles", "tokens", "appConfig"],
|
|
164417
164485
|
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));
|
|
164486
|
+
}), null, 2));
|
|
164419
164487
|
return 1;
|
|
164420
164488
|
}
|
|
164421
164489
|
if (opts.ux?.evidence && opts.ux.evidence.state !== "recorded" && !opts.force) {
|
|
164422
|
-
print({
|
|
164490
|
+
print(pushFailure2(uxEvidenceCode2(opts.ux.evidence.state), {
|
|
164423
164491
|
ok: false,
|
|
164424
164492
|
patched: false,
|
|
164425
164493
|
pushed: false,
|
|
164426
164494
|
uxEvidence: opts.ux.evidence,
|
|
164427
164495
|
releaseId: opts.ux.ctx.releaseId,
|
|
164428
164496
|
hint: uxHandoverHint(opts.ux.evidence, opts.ux.ctx)
|
|
164429
|
-
});
|
|
164497
|
+
}));
|
|
164430
164498
|
return 1;
|
|
164431
164499
|
}
|
|
164432
164500
|
let r2;
|
|
164433
164501
|
try {
|
|
164434
164502
|
r2 = await client2().releasePatch(app, base.releaseId, plan.patch, draft, opts.meta, opts.force);
|
|
164435
164503
|
} catch (e2) {
|
|
164436
|
-
io2.error(JSON.stringify({ ok: false, patched: false, error: "serve_unreachable", app, message: e2.message }));
|
|
164504
|
+
io2.error(JSON.stringify(pushFailure2("serve_unreachable", { ok: false, patched: false, error: "serve_unreachable", app, message: e2.message })));
|
|
164437
164505
|
return 1;
|
|
164438
164506
|
}
|
|
164439
164507
|
if (!r2.ok) {
|
|
@@ -164499,17 +164567,17 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
|
|
|
164499
164567
|
const wantDraft = flags.draft === true;
|
|
164500
164568
|
const wantLive = flags.live === true;
|
|
164501
164569
|
if (wantDraft && wantLive) {
|
|
164502
|
-
io2.error(JSON.stringify({ ok: false, error: "conflicting_blast_radius", message: "--draft and --live are mutually exclusive; pass exactly one." }));
|
|
164570
|
+
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
164571
|
return null;
|
|
164504
164572
|
}
|
|
164505
164573
|
if (!wantDraft && !wantLive) {
|
|
164506
|
-
io2.error(JSON.stringify({
|
|
164574
|
+
io2.error(JSON.stringify(pushFailure2("no_blast_radius", {
|
|
164507
164575
|
ok: false,
|
|
164508
164576
|
error: "no_blast_radius",
|
|
164509
164577
|
message: "a patch must say where it lands: --draft (nothing changes on any device) or --live (every device, immediately).",
|
|
164510
164578
|
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
164579
|
usage
|
|
164512
|
-
}));
|
|
164580
|
+
})));
|
|
164513
164581
|
return null;
|
|
164514
164582
|
}
|
|
164515
164583
|
return wantDraft;
|
|
@@ -166688,15 +166756,15 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166688
166756
|
if (flags.only !== void 0) {
|
|
166689
166757
|
onlyRaw = str9(flags.only);
|
|
166690
166758
|
if (!onlyRaw) {
|
|
166691
|
-
io2.error(JSON.stringify({ error: "--only needs a selector", usage: ONLY_USAGE }));
|
|
166759
|
+
io2.error(JSON.stringify(pushFailure2("invalid_selector", { error: "--only needs a selector", usage: ONLY_USAGE })));
|
|
166692
166760
|
return 2;
|
|
166693
166761
|
}
|
|
166694
166762
|
if (argv.filter((a2) => a2 === "--only" || a2.startsWith("--only=")).length > 1) {
|
|
166695
|
-
io2.error(JSON.stringify({
|
|
166763
|
+
io2.error(JSON.stringify(pushFailure2("duplicate_only", {
|
|
166696
166764
|
error: "duplicate_only",
|
|
166697
166765
|
usage: ONLY_USAGE,
|
|
166698
166766
|
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
|
-
}));
|
|
166767
|
+
})));
|
|
166700
166768
|
return 2;
|
|
166701
166769
|
}
|
|
166702
166770
|
for (const [flag, why] of [
|
|
@@ -166704,12 +166772,12 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166704
166772
|
["visibility", "the patch route does not stamp visibility; set it with `himi visibility` instead"]
|
|
166705
166773
|
]) {
|
|
166706
166774
|
if (flags[flag] !== void 0) {
|
|
166707
|
-
io2.error(JSON.stringify({ error: `--${flag} is not meaningful with --only`, message: why, usage: ONLY_USAGE }));
|
|
166775
|
+
io2.error(JSON.stringify(pushFailure2("incompatible_push_flags", { error: `--${flag} is not meaningful with --only`, message: why, usage: ONLY_USAGE })));
|
|
166708
166776
|
return 2;
|
|
166709
166777
|
}
|
|
166710
166778
|
}
|
|
166711
166779
|
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 }));
|
|
166780
|
+
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
166781
|
return 2;
|
|
166714
166782
|
}
|
|
166715
166783
|
if (patchBlastRadius(ONLY_USAGE) === null) return 2;
|
|
@@ -166728,14 +166796,14 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166728
166796
|
const { loadTargetsFile: loadTargetsFile2 } = await Promise.resolve().then(() => (init_config(), config_exports));
|
|
166729
166797
|
const configured = loadTargetsFile2();
|
|
166730
166798
|
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({
|
|
166799
|
+
io2.error(JSON.stringify(pushFailure2("control_token_expired", {
|
|
166732
166800
|
ok: false,
|
|
166733
166801
|
pushed: false,
|
|
166734
166802
|
error: "control_token_expired",
|
|
166735
166803
|
target: pushTarget,
|
|
166736
166804
|
...usable.length ? { usableTargets: usable } : {},
|
|
166737
166805
|
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
|
-
}));
|
|
166806
|
+
})));
|
|
166739
166807
|
return 1;
|
|
166740
166808
|
}
|
|
166741
166809
|
let app = resolveAppSlug(declared, (pushToken ? decodeControlTokenClaims(pushToken) : null) ?? {});
|
|
@@ -166763,7 +166831,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166763
166831
|
...await navIssues2(dir, strict)
|
|
166764
166832
|
]);
|
|
166765
166833
|
if (hasErrors && !force) {
|
|
166766
|
-
print({ ...report3, pushed: false, hint: "lint found errors \u2014 fix them, or re-run `himi push --force` to publish anyway" });
|
|
166834
|
+
print(pushFailure2("lint_errors", { ...report3, pushed: false, hint: "lint found errors \u2014 fix them, or re-run `himi push --force` to publish anyway" }));
|
|
166767
166835
|
return 1;
|
|
166768
166836
|
}
|
|
166769
166837
|
const declaredData = await loadDataResourcesQuietly(dir);
|
|
@@ -166782,28 +166850,28 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166782
166850
|
bundles: Object.keys(manifest.bundles ?? {})
|
|
166783
166851
|
});
|
|
166784
166852
|
if (!parsed.ok) {
|
|
166785
|
-
io2.error(JSON.stringify(parsed.error, null, 2));
|
|
166853
|
+
io2.error(JSON.stringify(pushFailure2("invalid_selector", parsed.error), null, 2));
|
|
166786
166854
|
return 2;
|
|
166787
166855
|
}
|
|
166788
166856
|
const fromRelease = str9(flags["from-release"]);
|
|
166789
166857
|
const baseForPlan = await fetchReleaseManifest(client2, app, fromRelease);
|
|
166790
166858
|
if ("error" in baseForPlan) {
|
|
166791
|
-
io2.error(JSON.stringify(baseForPlan));
|
|
166859
|
+
io2.error(JSON.stringify(pushFailure2("release_read_failed", baseForPlan)));
|
|
166792
166860
|
return 1;
|
|
166793
166861
|
}
|
|
166794
166862
|
if (!baseForPlan.manifest) {
|
|
166795
|
-
io2.error(JSON.stringify({
|
|
166863
|
+
io2.error(JSON.stringify(pushFailure2(fromRelease ? "base_not_found" : "no_live_release", {
|
|
166796
166864
|
ok: false,
|
|
166797
166865
|
error: fromRelease ? "base_not_found" : "no_live_release",
|
|
166798
166866
|
app,
|
|
166799
166867
|
message: fromRelease ? `no release ${fromRelease} for ${app}.` : `no published release for "${app}" to patch \u2014 run \`himi push\` first.`,
|
|
166800
166868
|
hint: "if this app is registered under a different slug, pass --app <slug>."
|
|
166801
|
-
}));
|
|
166869
|
+
})));
|
|
166802
166870
|
return 1;
|
|
166803
166871
|
}
|
|
166804
166872
|
const planned = planFromPayload2(payload, baseForPlan.manifest, parsed.selectors);
|
|
166805
166873
|
if (!planned.ok) {
|
|
166806
|
-
io2.error(JSON.stringify(planned.error, null, 2));
|
|
166874
|
+
io2.error(JSON.stringify(pushFailure2("invalid_patch", planned.error), null, 2));
|
|
166807
166875
|
return 1;
|
|
166808
166876
|
}
|
|
166809
166877
|
const patchUxEvidence = handoverUxEvidence(dir, {
|
|
@@ -166873,21 +166941,21 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166873
166941
|
const collected = collectAppSource2(dir);
|
|
166874
166942
|
const findings = secretFindings2(collected.entries);
|
|
166875
166943
|
if (findings.length > 0) {
|
|
166876
|
-
io2.error(JSON.stringify({
|
|
166944
|
+
io2.error(JSON.stringify(pushFailure2("app_source_contains_secrets", {
|
|
166877
166945
|
ok: false,
|
|
166878
166946
|
pushed: false,
|
|
166879
166947
|
error: `refusing to archive the source: ${findings[0].path} looks like it contains ${findings[0].reason}`,
|
|
166880
166948
|
file: findings[0].path,
|
|
166881
166949
|
found: findings.map((f2) => f2.path),
|
|
166882
166950
|
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
|
-
}));
|
|
166951
|
+
})));
|
|
166884
166952
|
return 1;
|
|
166885
166953
|
}
|
|
166886
166954
|
const zip = zipAppSource2(collected.entries);
|
|
166887
166955
|
const encoded2 = zip.toString("base64");
|
|
166888
166956
|
if (encoded2.length > MAX_APP_SOURCE_BYTES2) {
|
|
166889
166957
|
const largest = largestEntries2(collected.entries);
|
|
166890
|
-
io2.error(JSON.stringify({
|
|
166958
|
+
io2.error(JSON.stringify(pushFailure2("app_source_too_large", {
|
|
166891
166959
|
ok: false,
|
|
166892
166960
|
pushed: false,
|
|
166893
166961
|
error: "app_source_too_large",
|
|
@@ -166897,7 +166965,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166897
166965
|
files: collected.entries.length,
|
|
166898
166966
|
largest,
|
|
166899
166967
|
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
|
-
}));
|
|
166968
|
+
})));
|
|
166901
166969
|
return 1;
|
|
166902
166970
|
}
|
|
166903
166971
|
appSourceB64 = encoded2;
|
|
@@ -166914,22 +166982,22 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166914
166982
|
if (attestationErrors.length > 0 && !force) {
|
|
166915
166983
|
const remedy = `\`himi test --content ${contentArg2}\` (or \`himi render <screenId> --content ${contentArg2}\`)`;
|
|
166916
166984
|
const corrupted = evidence.historyErrors.length > 0;
|
|
166917
|
-
print({
|
|
166985
|
+
print(pushFailure2(corrupted ? "attestation_history_corrupt" : "attestation_stale", {
|
|
166918
166986
|
ok: false,
|
|
166919
166987
|
pushed: false,
|
|
166920
166988
|
errors: attestationErrors,
|
|
166921
166989
|
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
|
-
});
|
|
166990
|
+
}));
|
|
166923
166991
|
return 1;
|
|
166924
166992
|
}
|
|
166925
166993
|
const attestation = evidence.selected ? summaryOf(evidence.selected) : void 0;
|
|
166926
166994
|
if (attestation?.report.ok === false && !force) {
|
|
166927
|
-
print({
|
|
166995
|
+
print(pushFailure2("headless_run_failed", {
|
|
166928
166996
|
ok: false,
|
|
166929
166997
|
pushed: false,
|
|
166930
166998
|
attestation,
|
|
166931
166999
|
hint: "the latest verified headless run failed \u2014 fix it and rerun the tool, or re-run `himi push --force` to publish anyway"
|
|
166932
|
-
});
|
|
167000
|
+
}));
|
|
166933
167001
|
return 1;
|
|
166934
167002
|
}
|
|
166935
167003
|
const uxEvidence = handoverUxEvidence(dir, {
|
|
@@ -166941,7 +167009,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166941
167009
|
inheritedScreens: []
|
|
166942
167010
|
});
|
|
166943
167011
|
if (uxEvidence && uxEvidence.state !== "recorded" && !force) {
|
|
166944
|
-
print({
|
|
167012
|
+
print(pushFailure2(uxEvidenceCode2(uxEvidence.state), {
|
|
166945
167013
|
ok: false,
|
|
166946
167014
|
pushed: false,
|
|
166947
167015
|
uxEvidence,
|
|
@@ -166959,7 +167027,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166959
167027
|
// `inherited-screens` and has no base to restore.
|
|
166960
167028
|
nondeterministicWorkers: drifting
|
|
166961
167029
|
})
|
|
166962
|
-
});
|
|
167030
|
+
}));
|
|
166963
167031
|
return 1;
|
|
166964
167032
|
}
|
|
166965
167033
|
const forcedPastErrors = force && (hasErrors || attestation?.report.ok === false || attestationErrors.length > 0);
|
|
@@ -166970,7 +167038,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166970
167038
|
bakedBytes: assetRecords.filter(([, a2]) => a2.baked).reduce((n2, [, a2]) => n2 + a2.bytes, 0),
|
|
166971
167039
|
otaOnly: assetRecords.filter(([, a2]) => !a2.baked).map(([name]) => name)
|
|
166972
167040
|
} : null;
|
|
166973
|
-
const visArg = visibilityFlag(flags, io2);
|
|
167041
|
+
const visArg = visibilityFlag(flags, io2, (fields) => pushFailure2("invalid_visibility", fields));
|
|
166974
167042
|
if (!visArg) return 2;
|
|
166975
167043
|
const visibility = visArg.value ?? await contentVisibility(dir);
|
|
166976
167044
|
const meta = {
|
|
@@ -166989,7 +167057,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166989
167057
|
const publishBodyBytesFor = (slug) => publishReleaseBodyBytes(publishBodyFor(slug), meta, draft);
|
|
166990
167058
|
const refuseOversizedBody = (slug, bodyBytes) => {
|
|
166991
167059
|
const withoutSource = appSourceB64 ? bodyBytes - appSourceB64.length : bodyBytes;
|
|
166992
|
-
io2.error(JSON.stringify({
|
|
167060
|
+
io2.error(JSON.stringify(pushFailure2("publish_body_too_large", {
|
|
166993
167061
|
ok: false,
|
|
166994
167062
|
app: slug,
|
|
166995
167063
|
pushed: false,
|
|
@@ -166998,7 +167066,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
166998
167066
|
limit: MAX_PUBLISH_BODY_BYTES,
|
|
166999
167067
|
...appSourceB64 ? { appSourceBytes: appSourceB64.length } : {},
|
|
167000
167068
|
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
|
-
}));
|
|
167069
|
+
})));
|
|
167002
167070
|
return 1;
|
|
167003
167071
|
};
|
|
167004
167072
|
const composedBodyBytes = publishBodyBytesFor(app);
|
|
@@ -167009,14 +167077,14 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
167009
167077
|
if (ownerIssue && pushToken && !tokenCoversApp(pushToken, app)) {
|
|
167010
167078
|
const claim = await autoClaimSlug({ ownerIssue, controlToken: pushToken, app, accountId: str9(flags["account-id"]) });
|
|
167011
167079
|
if (claim.fatal) {
|
|
167012
|
-
io2.error(JSON.stringify({
|
|
167080
|
+
io2.error(JSON.stringify(pushFailure2("claim_failed", {
|
|
167013
167081
|
ok: false,
|
|
167014
167082
|
app,
|
|
167015
167083
|
pushed: false,
|
|
167016
167084
|
status: claim.fatal.status,
|
|
167017
167085
|
...claim.fatal.body,
|
|
167018
167086
|
...claimHint(claim.fatal.status, claim.fatal.error)
|
|
167019
|
-
}));
|
|
167087
|
+
})));
|
|
167020
167088
|
return 1;
|
|
167021
167089
|
}
|
|
167022
167090
|
if (claim.refusal) io2.error(JSON.stringify({ warning: claim.refusal.error, hint: claim.refusal.hint }));
|
|
@@ -167037,16 +167105,16 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
167037
167105
|
r2 = await client2().publishRelease(publishPayload, meta, draft);
|
|
167038
167106
|
} catch (e2) {
|
|
167039
167107
|
const target = targetName();
|
|
167040
|
-
io2.error(JSON.stringify({
|
|
167108
|
+
io2.error(JSON.stringify(pushFailure2("needs_vpn", {
|
|
167041
167109
|
...serveUnreachableError(target, serveBase(), e2.message),
|
|
167042
167110
|
version: cliVersion()
|
|
167043
|
-
}));
|
|
167111
|
+
})));
|
|
167044
167112
|
return 1;
|
|
167045
167113
|
}
|
|
167046
167114
|
if (!r2.ok) {
|
|
167047
167115
|
const authHint = pushAuthHint(r2.status, pushToken, pushTarget, !!ownerIssue);
|
|
167048
167116
|
const hint = authHint ?? releaseIdExistsHint(r2.status, r2.json);
|
|
167049
|
-
print({ ok: false, status: r2.status, body: r2.json ?? r2.text, ...hint ? { hint } : {} });
|
|
167117
|
+
print(pushFailure2("upload_http_error", { ok: false, status: r2.status, body: r2.json ?? r2.text, ...hint ? { hint } : {} }));
|
|
167050
167118
|
return 1;
|
|
167051
167119
|
}
|
|
167052
167120
|
const release = r2.json?.releaseId ?? manifest.releaseId;
|
|
@@ -170283,15 +170351,15 @@ Code: ${userCode}`);
|
|
|
170283
170351
|
}
|
|
170284
170352
|
} catch (err) {
|
|
170285
170353
|
if (err instanceof AuthoringUnavailable) {
|
|
170286
|
-
io2.error(JSON.stringify({
|
|
170354
|
+
io2.error(JSON.stringify(pushFailure2("authoring_unavailable", {
|
|
170287
170355
|
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
170356
|
version: cliVersion(),
|
|
170289
170357
|
upgrade: "npm i -g @wix/himalaya-cli@latest",
|
|
170290
170358
|
portal: DEVELOPER_PORTAL_URL
|
|
170291
|
-
}));
|
|
170359
|
+
})));
|
|
170292
170360
|
return 1;
|
|
170293
170361
|
}
|
|
170294
|
-
io2.error(JSON.stringify({ error: err.message }));
|
|
170362
|
+
io2.error(JSON.stringify(pushFailure2("push_failed", { error: err.message })));
|
|
170295
170363
|
const code = err.exitCode;
|
|
170296
170364
|
return code === 2 ? 2 : 1;
|
|
170297
170365
|
}
|
package/guides/headless-loop.md
CHANGED
|
@@ -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.
|
|
3
|
+
"version": "0.1247.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": "
|
|
70
|
+
"falconPackageHash": "ba75b3fdbeb557376aad2c794b64762b5e246a1c72a94d970720534c"
|
|
71
71
|
}
|