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