@wix/himalaya-cli 0.855.0 → 0.856.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 +282 -54
- package/guides/himi-test.md +1 -1
- package/guides/mobile-ux.md +28 -7
- package/guides/tier-5-add-a-screen.md +4 -0
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -832,19 +832,120 @@ var init_source_deny = __esm({
|
|
|
832
832
|
});
|
|
833
833
|
|
|
834
834
|
// src/mobile-ux-contract.ts
|
|
835
|
+
function mobileUxDocumentBody(mobile) {
|
|
836
|
+
const comments = /<!--[\s\S]*?(?:-->|$)/g;
|
|
837
|
+
let out = "";
|
|
838
|
+
let last = 0;
|
|
839
|
+
let blankBefore = true;
|
|
840
|
+
for (const match of mobile.matchAll(comments)) {
|
|
841
|
+
const offset = match.index ?? 0;
|
|
842
|
+
const between = mobile.slice(last, offset);
|
|
843
|
+
const newline = between.lastIndexOf("\n");
|
|
844
|
+
if (newline >= 0) blankBefore = between.slice(newline + 1).trim() === "";
|
|
845
|
+
else blankBefore = blankBefore && between.trim() === "";
|
|
846
|
+
out += between;
|
|
847
|
+
const ownsTheLine = offset === 0 || mobile[offset - 1] === "\n";
|
|
848
|
+
const precedesHeading = blankBefore && mobile.startsWith("#", offset + match[0].length);
|
|
849
|
+
out += ownsTheLine || precedesHeading ? "\n" : " ";
|
|
850
|
+
last = offset + match[0].length;
|
|
851
|
+
}
|
|
852
|
+
return out + mobile.slice(last);
|
|
853
|
+
}
|
|
854
|
+
function mobileUxSection(text2, heading) {
|
|
855
|
+
const match = new RegExp(`(?:^|\\n)##[ \\t]+${heading}\\b[^\\n]*([\\s\\S]*?)(?=\\n##[ \\t]|$)`, "i").exec(text2);
|
|
856
|
+
return match ? match[1] : null;
|
|
857
|
+
}
|
|
858
|
+
function sectionTables(markdown) {
|
|
859
|
+
const lines = markdown.split("\n").map((line) => line.trim());
|
|
860
|
+
const tables = [];
|
|
861
|
+
for (let i = 1; i < lines.length; i++) {
|
|
862
|
+
if (!DELIMITER_ROW.test(lines[i])) continue;
|
|
863
|
+
const header = lines[i - 1];
|
|
864
|
+
if (!header.includes("|")) continue;
|
|
865
|
+
const table = { header: splitCells(header), data: [] };
|
|
866
|
+
let j = i + 1;
|
|
867
|
+
for (; j < lines.length; j++) {
|
|
868
|
+
const line = lines[j];
|
|
869
|
+
if (!line.includes("|")) break;
|
|
870
|
+
if (DELIMITER_ROW.test(line)) continue;
|
|
871
|
+
const cells = splitCells(line);
|
|
872
|
+
if (cells.every((cell) => cell === "")) continue;
|
|
873
|
+
table.data.push(cells);
|
|
874
|
+
}
|
|
875
|
+
tables.push(table);
|
|
876
|
+
i = j;
|
|
877
|
+
}
|
|
878
|
+
return tables;
|
|
879
|
+
}
|
|
880
|
+
function namesOneCta(cell) {
|
|
881
|
+
if (!cell) return false;
|
|
882
|
+
return !/[,;]|\s\/\s/.test(cell);
|
|
883
|
+
}
|
|
884
|
+
function declaredStates(text2) {
|
|
885
|
+
const contract = section(text2, "Screen contract");
|
|
886
|
+
if (!contract) return [];
|
|
887
|
+
const states = [];
|
|
888
|
+
for (const bullet of contract.matchAll(/^[ \t]*[-*][ \t]*[^:\n]*\bstates?[ \t]*:[ \t]*(\S.*)$/gim)) {
|
|
889
|
+
const items = bullet[1].split(/[,;/]/).map((item) => item.trim().replace(/[.:]+$/, "")).filter(Boolean);
|
|
890
|
+
if (!items.length || items.some((item) => !/^[\w][\w.\-+]*$/.test(item))) continue;
|
|
891
|
+
states.push(...items);
|
|
892
|
+
}
|
|
893
|
+
return [...new Set(states.map((state) => state.toLowerCase()))];
|
|
894
|
+
}
|
|
895
|
+
function primaryCtaStructureGap(text2) {
|
|
896
|
+
const acceptance = section(text2, "Implementation acceptance");
|
|
897
|
+
if (acceptance === null) return "There is no `## Implementation acceptance` section to read it from.";
|
|
898
|
+
const tables = sectionTables(acceptance).filter((table) => table.data.length > 0);
|
|
899
|
+
if (!tables.length) return "Its acceptance table has no rows.";
|
|
900
|
+
const ctaTables = tables.map((table) => ({ table, cta: table.header.findIndex((cell) => /primary\s*(?:cta|call[- ]?to[- ]?action|action)/i.test(cell)) })).filter((entry) => entry.cta >= 0);
|
|
901
|
+
if (!ctaTables.length) return "Its acceptance table has no `Primary CTA` column.";
|
|
902
|
+
const unanswered = ctaTables.flatMap(({ table, cta }) => table.data.filter((row) => !namesOneCta(row[cta] ?? "")));
|
|
903
|
+
if (unanswered.length) {
|
|
904
|
+
return `${unanswered.length} acceptance row(s) do not name exactly one primary CTA (first: ${JSON.stringify(unanswered[0][0] ?? "")}).`;
|
|
905
|
+
}
|
|
906
|
+
const data = ctaTables.flatMap(({ table }) => table.data);
|
|
907
|
+
const flat = (value) => value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
908
|
+
const rowText = data.map((row) => flat(row.join(" ")));
|
|
909
|
+
const missing = declaredStates(text2).filter((state) => !rowText.some((row) => row.includes(flat(state))));
|
|
910
|
+
if (missing.length) return `The acceptance table has no row for the declared state(s): ${missing.join(", ")}.`;
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
function hasUnterminatedComment(mobile) {
|
|
914
|
+
let i = 0;
|
|
915
|
+
for (; ; ) {
|
|
916
|
+
const open = mobile.indexOf("<!--", i);
|
|
917
|
+
if (open < 0) return false;
|
|
918
|
+
const close = mobile.indexOf("-->", open + 4);
|
|
919
|
+
if (close < 0) return true;
|
|
920
|
+
i = close + 3;
|
|
921
|
+
}
|
|
922
|
+
}
|
|
835
923
|
function mobileUxMarkerFindings(mobile) {
|
|
924
|
+
const text2 = mobileUxDocumentBody(mobile);
|
|
836
925
|
const findings = [];
|
|
837
|
-
if (!/^## Implementation acceptance\b/im.test(
|
|
838
|
-
findings.push({ code: "missing-acceptance-matrix", message: "MOBILE-UX.md must contain the
|
|
926
|
+
if (!/^## Implementation acceptance\b/im.test(text2)) {
|
|
927
|
+
findings.push({ code: "missing-acceptance-matrix", message: "MOBILE-UX.md must contain the heading `## Implementation acceptance` at the start of a line. Case is not checked, but use this spelling: other tooling addresses the document by it." });
|
|
839
928
|
}
|
|
840
|
-
if (!/^## Evidence\b/im.test(
|
|
841
|
-
findings.push({ code: "missing-evidence-section", message: "MOBILE-UX.md must contain the
|
|
929
|
+
if (!/^## Evidence\b/im.test(text2)) {
|
|
930
|
+
findings.push({ code: "missing-evidence-section", message: "MOBILE-UX.md must contain the heading `## Evidence` at the start of a line. Case is not checked, but use this spelling: other tooling addresses the document by it." });
|
|
842
931
|
}
|
|
843
|
-
if (!/one primary CTA per state/i.test(
|
|
844
|
-
|
|
932
|
+
if (!/one primary CTA per state/i.test(text2)) {
|
|
933
|
+
const gap = primaryCtaStructureGap(text2);
|
|
934
|
+
if (gap) {
|
|
935
|
+
findings.push({
|
|
936
|
+
code: "missing-primary-cta-rule",
|
|
937
|
+
message: "MOBILE-UX.md must establish one primary CTA per state: give `## Implementation acceptance` a `Primary CTA` column with a row per declared state and exactly one CTA in each cell, or keep the phrase `one primary CTA per state`. " + gap
|
|
938
|
+
});
|
|
939
|
+
}
|
|
845
940
|
}
|
|
846
|
-
if (!hasWidthEvidence(
|
|
847
|
-
findings.push({ code: "missing-width-evidence", message: widthEvidenceMessage(
|
|
941
|
+
if (!hasWidthEvidence(text2)) {
|
|
942
|
+
findings.push({ code: "missing-width-evidence", message: widthEvidenceMessage(text2) });
|
|
943
|
+
}
|
|
944
|
+
if (findings.length && hasUnterminatedComment(mobile)) {
|
|
945
|
+
return [{
|
|
946
|
+
code: "unterminated-html-comment",
|
|
947
|
+
message: "MOBILE-UX.md has an HTML comment opened with `<!--` and never closed with `-->`, so everything after it is commented out \u2014 in this check and in any renderer. Close the comment and re-run: the headings and phrases below it are not being read."
|
|
948
|
+
}];
|
|
848
949
|
}
|
|
849
950
|
return findings;
|
|
850
951
|
}
|
|
@@ -911,7 +1012,7 @@ function widthEvidenceMessage(mobile) {
|
|
|
911
1012
|
}
|
|
912
1013
|
return base;
|
|
913
1014
|
}
|
|
914
|
-
var SPEC_TEMPLATE, MOBILE_UX_TEMPLATE, WIDTH_LITERAL, COMPACT_MAX, WIDER_MIN, PLAUSIBLE_WIDTH, YEAR, inRange, looksLikeYear, UNIT_ATTACHED, UNIT_WORD_ATTACHED, WIDTH_WINDOW, EDGE_WINDOW, FILENAME_AFTER, ANTI_CUE_BEFORE, PAIR_HEIGHT_BEFORE, ANTI_CUE_AFTER, isCompact, isWider;
|
|
1015
|
+
var SPEC_TEMPLATE, MOBILE_UX_MARKER_NOTE, MOBILE_UX_TEMPLATE, section, DELIMITER_ROW, splitCells, WIDTH_LITERAL, COMPACT_MAX, WIDER_MIN, PLAUSIBLE_WIDTH, YEAR, inRange, looksLikeYear, UNIT_ATTACHED, UNIT_WORD_ATTACHED, WIDTH_WINDOW, EDGE_WINDOW, FILENAME_AFTER, ANTI_CUE_BEFORE, PAIR_HEIGHT_BEFORE, ANTI_CUE_AFTER, isCompact, isWider;
|
|
915
1016
|
var init_mobile_ux_contract = __esm({
|
|
916
1017
|
"src/mobile-ux-contract.ts"() {
|
|
917
1018
|
SPEC_TEMPLATE = `# App specification
|
|
@@ -938,8 +1039,33 @@ var init_mobile_ux_contract = __esm({
|
|
|
938
1039
|
- Required: iOS, Android
|
|
939
1040
|
- Optional: web, desktop, watch, widgets
|
|
940
1041
|
`;
|
|
1042
|
+
MOBILE_UX_MARKER_NOTE = `<!-- LOAD-BEARING \u2014 read this before you rewrite the file.
|
|
1043
|
+
|
|
1044
|
+
\`himi validate\` warns, and \`himi validate --strict\` / \`himi ux audit --strict\` REFUSE, a
|
|
1045
|
+
MOBILE-UX.md that satisfies none of the forms below. Everything else here is yours to
|
|
1046
|
+
replace freely. This comment is not itself an answer: HTML comments are stripped before the
|
|
1047
|
+
check runs, so keeping it satisfies nothing.
|
|
1048
|
+
|
|
1049
|
+
1. The headings \`## Implementation acceptance\` and \`## Evidence\`, each at the start of
|
|
1050
|
+
a line. Case is not checked; use these spellings anyway, because other tooling
|
|
1051
|
+
addresses this document by them.
|
|
1052
|
+
2. One primary CTA per state \u2014 EITHER the phrase \`one primary CTA per state\`, OR a
|
|
1053
|
+
\`Primary CTA\` column in the acceptance table below, with a row for every state you
|
|
1054
|
+
declare under \`## Screen contract\` and exactly one CTA named in each row's cell.
|
|
1055
|
+
3. Width evidence \u2014 EITHER the scaffold's own sentence, \`compact (320/360)\` followed by
|
|
1056
|
+
\`expanded\`, OR two widths you actually rendered, EACH SPELLED AS A WIDTH ON ITSELF:
|
|
1057
|
+
one below 600px and one at 600px or wider, within about 400 characters of each other.
|
|
1058
|
+
A number is spelled as a width when it carries a unit (\`320px\`, \`834 pt\`,
|
|
1059
|
+
\`320 pixels\`, \`320 wide\`) or sits in a screenshot filename (\`home-320.png\`,
|
|
1060
|
+
\`home-390x844.png\`). Nothing is read from the words around the number, so a BARE
|
|
1061
|
+
figure never counts however you label it \u2014 \`Widths: 320, 390, 834\` and
|
|
1062
|
+
\`compact (320) and expanded (834)\` both name no width. One unit per number fixes it.
|
|
1063
|
+
|
|
1064
|
+
Full contract: \`himi docs mobile-ux\` -> "Strict audit contract". -->`;
|
|
941
1065
|
MOBILE_UX_TEMPLATE = `# Mobile UX
|
|
942
1066
|
|
|
1067
|
+
${MOBILE_UX_MARKER_NOTE}
|
|
1068
|
+
|
|
943
1069
|
## Flows
|
|
944
1070
|
### <Task>
|
|
945
1071
|
1. Entry:
|
|
@@ -982,6 +1108,9 @@ in preview fixtures or labelled, capability-limited fallback states\u2014not ord
|
|
|
982
1108
|
> Keep this document safe to publish with a release: do not include credentials, personal data,
|
|
983
1109
|
> internal URLs, private research, or unreviewed notes. Read \`himi docs mobile-ux\` before filling it in.
|
|
984
1110
|
`;
|
|
1111
|
+
section = mobileUxSection;
|
|
1112
|
+
DELIMITER_ROW = /^\|?\s*:?-+:?\s*(?:\|\s*:?-+:?\s*)+\|?$/;
|
|
1113
|
+
splitCells = (line) => line.replace(/^\|/, "").replace(/\|\s*$/, "").split("|").map((cell) => cell.trim());
|
|
985
1114
|
WIDTH_LITERAL = /compact \(320\/360\)[\s\S]{0,400}?expanded/i;
|
|
986
1115
|
COMPACT_MAX = 599;
|
|
987
1116
|
WIDER_MIN = 600;
|
|
@@ -4729,7 +4858,6 @@ function resolveTranslation(value, catalog, sourceCatalog) {
|
|
|
4729
4858
|
var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
|
|
4730
4859
|
var init_bindings = __esm({
|
|
4731
4860
|
"../../core/ts/src/bindings.ts"() {
|
|
4732
|
-
"use strict";
|
|
4733
4861
|
BINDING_SIGILS = ["service", "flag", "item", "t"];
|
|
4734
4862
|
BINDING_SIGIL_RE = new RegExp(
|
|
4735
4863
|
`^\\$(?:${BINDING_SIGILS.filter((s) => s !== "t").join("|")})\\b|^\\$t:`
|
|
@@ -6606,7 +6734,7 @@ function validateWorkerAction(action, context) {
|
|
|
6606
6734
|
}
|
|
6607
6735
|
}
|
|
6608
6736
|
if (context.actionCatalogWithheld) {
|
|
6609
|
-
return actionIssue(action, "warning", `worker actionId "${action.actionId}" was not cross-checked: this surface has precomputed service state but no precomputed action catalog`);
|
|
6737
|
+
return actionIssue(action, "warning", `worker actionId "${action.actionId}" was not cross-checked: this surface has precomputed service state but no precomputed action catalog. This is a property of the surface doing the checking, not of your app \u2014 there is nothing to fix in the content package; re-run the check where the bundle can be inspected (\`himi validate\` / \`himi test\` locally) to have it cross-checked`);
|
|
6610
6738
|
}
|
|
6611
6739
|
const reason = context.bundleInspectionError ? `the bound bundle could not be inspected: ${context.bundleInspectionError}` : "no inspectable logic bundle is bound";
|
|
6612
6740
|
return actionIssue(action, "error", `worker actionId "${action.actionId}" cannot resolve because ${reason}`);
|
|
@@ -26475,7 +26603,6 @@ function shouldRevealError(args) {
|
|
|
26475
26603
|
var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
|
|
26476
26604
|
var init_validation = __esm({
|
|
26477
26605
|
"../../core/ts/src/validation.ts"() {
|
|
26478
|
-
"use strict";
|
|
26479
26606
|
EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
|
|
26480
26607
|
URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
|
|
26481
26608
|
INTEGER_RE = /^[+-]?[0-9]+$/;
|
|
@@ -29898,7 +30025,6 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
|
|
|
29898
30025
|
var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
|
|
29899
30026
|
var init_worker_subtree = __esm({
|
|
29900
30027
|
"../../core/ts/src/worker-subtree.ts"() {
|
|
29901
|
-
"use strict";
|
|
29902
30028
|
init_worker_subtree_safe_types_generated();
|
|
29903
30029
|
WORKER_SUBTREE_CEILINGS = Object.freeze({
|
|
29904
30030
|
maxEncodedBytes: 65536,
|
|
@@ -46300,9 +46426,9 @@ function ipv6Private(address) {
|
|
|
46300
46426
|
const value = address.toLowerCase().replace(/^\[|\]$/g, "");
|
|
46301
46427
|
const sections = value.split("::");
|
|
46302
46428
|
if (sections.length > 2) return true;
|
|
46303
|
-
const parse = (
|
|
46304
|
-
if (!
|
|
46305
|
-
const parts =
|
|
46429
|
+
const parse = (section2) => {
|
|
46430
|
+
if (!section2) return [];
|
|
46431
|
+
const parts = section2.split(":");
|
|
46306
46432
|
const last = parts.at(-1) ?? "";
|
|
46307
46433
|
if (!last.includes(".")) return parts.map((part) => Number.parseInt(part, 16));
|
|
46308
46434
|
parts.pop();
|
|
@@ -47927,6 +48053,7 @@ __export(native_decode_exports, {
|
|
|
47927
48053
|
nativeDecodeAndroidHarnessDir: () => nativeDecodeAndroidHarnessDir,
|
|
47928
48054
|
nativeDecodePackageDir: () => nativeDecodePackageDir,
|
|
47929
48055
|
nativeDecodePlatforms: () => nativeDecodePlatforms,
|
|
48056
|
+
nativeOracleShipped: () => nativeOracleShipped,
|
|
47930
48057
|
parseNativeDecodeOutput: () => parseNativeDecodeOutput,
|
|
47931
48058
|
runAndroidNativeDecode: () => runAndroidNativeDecode,
|
|
47932
48059
|
runNativeDecode: () => runNativeDecode
|
|
@@ -47942,6 +48069,12 @@ function nativeDecodePackageDir() {
|
|
|
47942
48069
|
function nativeDecodeAndroidHarnessDir() {
|
|
47943
48070
|
return fileURLToPath19(new URL("../../../tools/wix-sdk-android-tests/", import.meta.url));
|
|
47944
48071
|
}
|
|
48072
|
+
function nativeOracleShipped() {
|
|
48073
|
+
return {
|
|
48074
|
+
ios: existsSync41(join41(nativeDecodePackageDir(), "Package.swift")),
|
|
48075
|
+
android: existsSync41(join41(nativeDecodeAndroidHarnessDir(), "gradlew"))
|
|
48076
|
+
};
|
|
48077
|
+
}
|
|
47945
48078
|
function detectNativeDecode(options = {}) {
|
|
47946
48079
|
const hostPlatform = options.hostPlatform ?? process.platform;
|
|
47947
48080
|
const packageDir = options.packageDir ?? nativeDecodePackageDir();
|
|
@@ -48443,11 +48576,15 @@ __export(coverage_exports, {
|
|
|
48443
48576
|
diffAndStoreObservations: () => diffAndStoreObservations,
|
|
48444
48577
|
formatDelta: () => formatDelta,
|
|
48445
48578
|
formatLedger: () => formatLedger,
|
|
48579
|
+
suppressedNativeKeys: () => suppressedNativeKeys,
|
|
48446
48580
|
trendAndStore: () => trendAndStore
|
|
48447
48581
|
});
|
|
48448
48582
|
import { existsSync as existsSync43, mkdirSync as mkdirSync21, readFileSync as readFileSync50, writeFileSync as writeFileSync18 } from "node:fs";
|
|
48449
48583
|
import { dirname as dirname25, join as join43 } from "node:path";
|
|
48450
|
-
function
|
|
48584
|
+
function suppressedNativeKeys(oracle) {
|
|
48585
|
+
return [...oracle.ios ? [] : ["native-decode|ios"], ...oracle.android ? [] : ["native-decode|android"]];
|
|
48586
|
+
}
|
|
48587
|
+
function assembleLedger(r, options = {}) {
|
|
48451
48588
|
const out = [];
|
|
48452
48589
|
for (const c of r.crawled ?? []) {
|
|
48453
48590
|
const noRow = (c.actionsSkipped ?? []).filter((s) => s.why === "no-row-value");
|
|
@@ -48569,8 +48706,9 @@ function assembleLedger(r) {
|
|
|
48569
48706
|
remedy: "npx playwright install chromium (~170MB, one time) \u2014 then every himi test writes a PNG per screen to .himi/shots/"
|
|
48570
48707
|
});
|
|
48571
48708
|
}
|
|
48709
|
+
const oracle = options.nativeOracle ?? nativeOracleShipped();
|
|
48572
48710
|
const nativeIos = r.nativeDecode?.ios;
|
|
48573
|
-
if (!nativeIos || "unavailable" in nativeIos) {
|
|
48711
|
+
if (oracle.ios && (!nativeIos || "unavailable" in nativeIos)) {
|
|
48574
48712
|
const reason = nativeIos && "unavailable" in nativeIos ? ` (${nativeIos.reason})` : "";
|
|
48575
48713
|
out.push({
|
|
48576
48714
|
key: "native-decode|ios",
|
|
@@ -48581,7 +48719,7 @@ function assembleLedger(r) {
|
|
|
48581
48719
|
});
|
|
48582
48720
|
}
|
|
48583
48721
|
const nativeAndroid = r.nativeDecode?.android;
|
|
48584
|
-
if (!nativeAndroid || "unavailable" in nativeAndroid) {
|
|
48722
|
+
if (oracle.android && (!nativeAndroid || "unavailable" in nativeAndroid)) {
|
|
48585
48723
|
const reason = nativeAndroid && "unavailable" in nativeAndroid ? ` (${nativeAndroid.reason})` : "";
|
|
48586
48724
|
out.push({
|
|
48587
48725
|
key: "native-decode|android",
|
|
@@ -48611,7 +48749,7 @@ function assembleLedger(r) {
|
|
|
48611
48749
|
}
|
|
48612
48750
|
return out;
|
|
48613
48751
|
}
|
|
48614
|
-
function trendAndStore(dir, entries) {
|
|
48752
|
+
function trendAndStore(dir, entries, suppressed) {
|
|
48615
48753
|
const p = join43(dir, LEDGER_FILE);
|
|
48616
48754
|
let previous = null;
|
|
48617
48755
|
try {
|
|
@@ -48622,19 +48760,21 @@ function trendAndStore(dir, entries) {
|
|
|
48622
48760
|
} catch {
|
|
48623
48761
|
}
|
|
48624
48762
|
const current = entries.map((e) => e.key);
|
|
48763
|
+
const stored = [.../* @__PURE__ */ new Set([...current, ...(previous ?? []).filter((key2) => (suppressed ?? []).includes(key2))])];
|
|
48625
48764
|
try {
|
|
48626
48765
|
mkdirSync21(dirname25(p), { recursive: true });
|
|
48627
|
-
writeFileSync18(p, JSON.stringify({ keys:
|
|
48766
|
+
writeFileSync18(p, JSON.stringify({ keys: stored, ts: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
48628
48767
|
} catch {
|
|
48629
48768
|
return null;
|
|
48630
48769
|
}
|
|
48631
48770
|
if (previous === null) return { opened: 0, closed: 0 };
|
|
48632
|
-
const
|
|
48771
|
+
const baseline = previous.filter((key2) => !(suppressed ?? []).includes(key2));
|
|
48772
|
+
const prev = new Set(baseline);
|
|
48633
48773
|
const cur = new Set(current);
|
|
48634
48774
|
return {
|
|
48635
48775
|
opened: current.filter((k) => !prev.has(k)).length,
|
|
48636
|
-
closed:
|
|
48637
|
-
previousTotal:
|
|
48776
|
+
closed: baseline.filter((k) => !cur.has(k)).length,
|
|
48777
|
+
previousTotal: baseline.length
|
|
48638
48778
|
};
|
|
48639
48779
|
}
|
|
48640
48780
|
function formatLedger(entries, trend) {
|
|
@@ -48705,6 +48845,7 @@ function formatDelta(delta) {
|
|
|
48705
48845
|
var LEDGER_FILE, OBS_FILE;
|
|
48706
48846
|
var init_coverage = __esm({
|
|
48707
48847
|
"src/coverage.ts"() {
|
|
48848
|
+
init_native_decode();
|
|
48708
48849
|
LEDGER_FILE = join43(".himi", "test-ledger.json");
|
|
48709
48850
|
OBS_FILE = join43(".himi", "test-observations.json");
|
|
48710
48851
|
}
|
|
@@ -50923,7 +51064,7 @@ function scalar2(v) {
|
|
|
50923
51064
|
}
|
|
50924
51065
|
function parseStoreConfig(text2) {
|
|
50925
51066
|
const cfg = {};
|
|
50926
|
-
let
|
|
51067
|
+
let section2 = null;
|
|
50927
51068
|
for (const raw of text2.split("\n")) {
|
|
50928
51069
|
const line = strip2(raw);
|
|
50929
51070
|
if (!line.trim()) continue;
|
|
@@ -50933,14 +51074,14 @@ function parseStoreConfig(text2) {
|
|
|
50933
51074
|
const [, key2, rest] = kv;
|
|
50934
51075
|
if (!indented) {
|
|
50935
51076
|
if (rest.trim() === "") {
|
|
50936
|
-
|
|
50937
|
-
cfg[key2] =
|
|
51077
|
+
section2 = {};
|
|
51078
|
+
cfg[key2] = section2;
|
|
50938
51079
|
} else {
|
|
50939
|
-
|
|
51080
|
+
section2 = null;
|
|
50940
51081
|
cfg[key2] = scalar2(rest);
|
|
50941
51082
|
}
|
|
50942
|
-
} else if (
|
|
50943
|
-
|
|
51083
|
+
} else if (section2) {
|
|
51084
|
+
section2[key2] = scalar2(rest);
|
|
50944
51085
|
}
|
|
50945
51086
|
}
|
|
50946
51087
|
return cfg;
|
|
@@ -53635,11 +53776,11 @@ function suggestKeys(args) {
|
|
|
53635
53776
|
if (rule.tab && allowed2(rule) && rule.re.test(label2)) set(rule.tab, label2);
|
|
53636
53777
|
}
|
|
53637
53778
|
}
|
|
53638
|
-
for (const
|
|
53779
|
+
for (const section2 of sections) {
|
|
53639
53780
|
for (const rule of KEY_RULES) {
|
|
53640
|
-
if (!allowed2(rule) || !rule.re.test(
|
|
53641
|
-
set(rule.title,
|
|
53642
|
-
if (
|
|
53781
|
+
if (!allowed2(rule) || !rule.re.test(section2.heading)) continue;
|
|
53782
|
+
set(rule.title, section2.heading);
|
|
53783
|
+
if (section2.body) set(rule.subtitle, section2.body);
|
|
53643
53784
|
}
|
|
53644
53785
|
}
|
|
53645
53786
|
if (tagline) set("home.onboardingBody", tagline.text);
|
|
@@ -53672,12 +53813,12 @@ function buildCopyChapter(args) {
|
|
|
53672
53813
|
pageUrl: page.url
|
|
53673
53814
|
}))
|
|
53674
53815
|
);
|
|
53675
|
-
for (const
|
|
53816
|
+
for (const section2 of candidates) {
|
|
53676
53817
|
if (faq.length >= CAP.faq) break;
|
|
53677
|
-
if (!
|
|
53678
|
-
if (about &&
|
|
53679
|
-
if (faq.some((f) => f.q.toLowerCase() ===
|
|
53680
|
-
faq.push({ q:
|
|
53818
|
+
if (!section2.heading || !section2.body || !/\?\s*$/.test(section2.heading)) continue;
|
|
53819
|
+
if (about && section2.body === about) continue;
|
|
53820
|
+
if (faq.some((f) => f.q.toLowerCase() === section2.heading.toLowerCase())) continue;
|
|
53821
|
+
faq.push({ q: section2.heading, a: section2.body, pageUrl: section2.pageUrl });
|
|
53681
53822
|
}
|
|
53682
53823
|
}
|
|
53683
53824
|
const testimonials = dedupe2(pages.flatMap((p) => p.testimonials ?? []), CAP.testimonials);
|
|
@@ -60627,7 +60768,7 @@ function parseEvidence(path) {
|
|
|
60627
60768
|
}
|
|
60628
60769
|
}
|
|
60629
60770
|
function blankMatrixRows(text2) {
|
|
60630
|
-
const acceptance = text2
|
|
60771
|
+
const acceptance = mobileUxSection(mobileUxDocumentBody(text2), "Implementation acceptance") ?? "";
|
|
60631
60772
|
return /\|\s*(?:Ready|Empty|Add\s*\/\s*edit|Saved\s*\/\s*completed|Error\s*\/\s*recovery)\s*\|\s*\|\s*\|\s*\|\s*\|/i.test(acceptance);
|
|
60632
60773
|
}
|
|
60633
60774
|
function usesAppNetwork(source) {
|
|
@@ -61377,7 +61518,7 @@ var COMMANDS = [
|
|
|
61377
61518
|
{ name: "functions", summary: "inspect/invoke server functions and operate durable jobs or schedules", required: "list|describe|openapi|invoke | jobs list|get|cancel|retry|dlq | schedule describe|invoke [--app <a>] [--environment preview|production] [--release <id>]" },
|
|
61378
61519
|
{ name: "eject", summary: "take ownership of a shared flow module (auth|checkout|screens): copies its source into tier5-src/_ejected/ and repoints this package's imports at the copy. The ladder's escape hatch \u2014 for when the factory options cannot express what you need. AFTER EJECTING, CENTRAL FIXES NO LONGER REACH THIS APP, including future corrections to the fail-closed entitlement and confirm-the-session rules. The floor is unaffected: credentials still go only through the native auth actions and money only through the cashier rail.", required: "<auth|checkout|screens> [--content <dir>]" },
|
|
61379
61520
|
{ name: "validate", summary: "lint the content package (no push): fixed-contract TypeScript checking for tier5-src workers + schema/proto + token refs + component allowlist + $service bindings + strict-enum foot-guns + capability decls + worker boot dry-run + build/compile failures. Type issues are advisory warnings normally; --strict promotes them and hygiene warnings to errors. Exit 1 on any error. Off-repo, the CLI supplies its compiler and SDK declarations.", required: "[--content <dir>] [--strict]" },
|
|
61380
|
-
{ name: "test", summary: "DERIVED behavioral crawl \u2014 no test files to write. Boots each screen's real worker, replays its onAppear, then dispatches every action site the descriptor already declares (each from a fresh boot) and checks a narrow set of unambiguous invariants: no uncaught throw, never stuck on `loading`, no `$service:` token or NaN/undefined/[object Object] reaching visible text, no binding aimed at state the worker never defines, no nav param silently dropped, no dead nav target. Catches what validate/render cannot \u2014 a WRONG REDUCER passes both with ok:true. Runs the worker in a DEVICE-PARITY sandbox (no setTimeout/setInterval, as on JSC/WebView) so a bundle that would die on device fails here. Crawls on HERMETIC DATA by default \u2014 zero network: the app's own dev/net-mocks.json first (--net <file> overrides), then the config's CMS `seed:` rows, then a canned fixture of frozen real vertical responses shipped with the CLI (exercises code paths, not API-contract truth) \u2014 so the success/mapping path runs, not just the failure UI. Two boot-only HOSTILE passes (every list empty; every request 500) then check the failure/empty day \u2014 corpus-proven, so their findings are errors. NAV EDGES are followed too: every himi.open site ($item params resolved against row 0 of its list, $service against worker state) and worker _nav directive is harvested, and each baked target is booted WITH the params the source sends \u2014 detail screens finally run their content arm; edge findings are errors tagged edge:<source>. --native runs both real native decoders over the exact baked JSON bytes; --native ios or --native android selects one. It is explicit and needs the selected local toolchain.
|
|
61521
|
+
{ name: "test", summary: "DERIVED behavioral crawl \u2014 no test files to write. Boots each screen's real worker, replays its onAppear, then dispatches every action site the descriptor already declares (each from a fresh boot) and checks a narrow set of unambiguous invariants: no uncaught throw, never stuck on `loading`, no `$service:` token or NaN/undefined/[object Object] reaching visible text, no binding aimed at state the worker never defines, no nav param silently dropped, no dead nav target. Catches what validate/render cannot \u2014 a WRONG REDUCER passes both with ok:true. Runs the worker in a DEVICE-PARITY sandbox (no setTimeout/setInterval, as on JSC/WebView) so a bundle that would die on device fails here. Crawls on HERMETIC DATA by default \u2014 zero network: the app's own dev/net-mocks.json first (--net <file> overrides), then the config's CMS `seed:` rows, then a canned fixture of frozen real vertical responses shipped with the CLI (exercises code paths, not API-contract truth) \u2014 so the success/mapping path runs, not just the failure UI. Two boot-only HOSTILE passes (every list empty; every request 500) then check the failure/empty day \u2014 corpus-proven, so their findings are errors. NAV EDGES are followed too: every himi.open site ($item params resolved against row 0 of its list, $service against worker state) and worker _nav directive is harvested, and each baked target is booted WITH the params the source sends \u2014 detail screens finally run their content arm; edge findings are errors tagged edge:<source>. --native runs both real native decoders over the exact baked JSON bytes; --native ios or --native android selects one. It is explicit, runs only from a Himalaya SOURCE CHECKOUT (the decoder oracles are that tree's own HimalayaCore Swift package and Gradle harness, which a published install does not ship), and needs the selected local toolchain. From a source checkout, default runs name missing native verdicts in the coverage ledger; elsewhere the ledger stays silent about them rather than offering a remedy you cannot run. --offline strips all data layers (the old reject-all axis); --no-hostile skips the hostile passes. --record (EXPLICIT, never automatic) refreshes dev/net-mocks.json from live reads first: it boots each screen (load + root onAppear ONLY \u2014 nothing the app's own launch would not fire; the tap/action crawl never sees the live transport, and only proven reads \u2014 GET, or read-shaped POST (/query, /search, /list*, \u2026) \u2014 ever reach the wire), freezes the round-trips, then crawls hermetically on the fresh fixture; record against a QA site, not production. Nodes it cannot judge headless are named, never silently passed. Exit 1 on any error. The CLI carries the authoring runtime off-repo; no per-app SDK install is required. --browser runs the whole loop (build \u2192 extract \u2192 validate \u2192 render \u2192 test) in YOUR browser instead of here, so the source never reaches a pod: it opens one screen's worker + descriptor in a tab, blocks on a loopback callback, and prints the report it posts back. One screen per tab \u2014 name it. --no-open prints the URL rather than launching a browser (SSH); --timeout <ms> bounds the wait, which matters because the page has no execution deadline of its own and an infinite loop in the worker hangs the tab.", required: "[<screenId>\u2026] [--content <dir>] [--net <file>] [--budget <ms>] [--deep [n]] [--record] [--native [ios|android]] [--no-shots] [--offline] [--no-hostile] [--all-screens] [--browser] [--no-open] [--timeout <ms>] [--json]" },
|
|
61381
61522
|
{ name: "ux", summary: "audit or record mobile UX evidence. `audit` checks the public-safe contract, seeded blank acceptance rows, placeholder source copy, app-specific mocks, and iOS/Android audit records. It warns while existing templates migrate; `--strict` promotes missing evidence. `record` stores checks performed on an installed iOS/Android build: `--checks` is required for passed|failed and empty for unavailable, which drove nothing and needs a `--reason` instead.", required: "audit [--content <dir>] [--strict] [--json] | record --content <dir> --platform ios|android --status passed|failed|unavailable [--checks back,keyboard,...] [--reason <text>]" },
|
|
61382
61523
|
{ name: "render", summary: "headless-render a screen with the web renderer: render-tree + diagnostics, optional --png <file>, --scheme dark, --tree. Local by default (content dir). Pass --remote --app --release to hit GET /v1/preview/\u2026/render on the serve plane after a push. Boots the worker so $service bindings resolve; --static raw bindings; --live real Wix fetches; --state injects worker state; --input supplies pre-boot navigation params; --width/--device for PNG viewport; --rtl for dir=rtl + $env:isRTL.", required: "<screenId> [--content <dir>] [--scheme dark] [--png <file>] [--tree] [--static] [--live] [--state <json|file>] [--input <json|file>] [--width <px> | --device <name>] [--rtl] [--remote --app --release]" },
|
|
61383
61524
|
{ name: "cms", summary: "inspect, provision, and seed the app's CMS (Wix Data) collections, declared as a `cms` block in config.ts. `query` is READ-ONLY \u2014 by default it fetches ONLY the physical ids `config.ts` declares (item counts + schema drift) and reports `siteScanned: false`; it never lists the site, so an UNDECLARED `himi_<app>_` collection this app owns is not reached without `--all`. `--all` SCANS the site \u2014 every collection it lists is labeled `owned`/`adopted`/`foreign`, and it describes every foreign one \u2014 id, non-system field keys + types, and the site's read role \u2014 so an app with no `cms` block at all can DISCOVER a collection to adopt (`--all` is the one case where a missing `cms` block is not 'nothing to do'). Foreign collections are described up to a cap and are never item-counted (`itemCount: null`), because a row count is one query each; `foreign.count` is how many the scan LISTED \u2014 NOT a site census, since the list endpoint is paged and is not followed \u2014 and `foreign.truncated` says how many of those went undescribed, while `unlistedCollections` reports what the listing itself left out (from Wix's own paging total). The scan is OWNER-scoped \u2014 it needs --site <msid> (or WIX_SITE_ID) plus owner auth via `wix token`; a visitor token can read a public collection's ITEMS but can never enumerate collections, so there is no tokenless discovery path. `create` reconciles declared collections additively and idempotently, and REFUSES any id outside the app's `himi_<app>_` namespace, so it is safe against a production site. It reports declared-vs-site `permissionDrift` but never writes permissions unless asked with --apply-permissions, so a field reconcile can't silently revert access control changed in the dashboard. `seed` inserts the git-tracked `seed:` rows into EMPTY owned collections (--force to add to a non-empty one; --ai writes placeholder rows plus a refinement prompt for you to review). Adopted collections (declared by `wixId`) are read-only and never created, updated, or seeded. Site from --site <msid> or WIX_SITE_ID; owner auth via `wix token`.", required: "query|create|seed [--site <msid>] [--content <dir>] [--all] [--dry-run] [--force] [--allow-destructive] [--apply-permissions] [--only <id>] [--ai] [--count <n>]" },
|
|
@@ -61557,6 +61698,40 @@ function previewLogsUrl(serveBase, app, release) {
|
|
|
61557
61698
|
function previewTelemetryUrl(serveBase, app, release) {
|
|
61558
61699
|
return `${serveBase.replace(/\/$/, "")}/v1/telemetry/events?app=${encodeURIComponent(app)}&release=${encodeURIComponent(release)}`;
|
|
61559
61700
|
}
|
|
61701
|
+
function diagnoseBody(body) {
|
|
61702
|
+
const findings = body.findings;
|
|
61703
|
+
if (!Array.isArray(findings)) return body;
|
|
61704
|
+
const messageOf2 = (entry) => {
|
|
61705
|
+
const message = entry && typeof entry === "object" ? entry.message : void 0;
|
|
61706
|
+
return typeof message === "string" ? message : void 0;
|
|
61707
|
+
};
|
|
61708
|
+
const shown2 = new Set(findings.map(messageOf2).filter((message) => message !== void 0));
|
|
61709
|
+
const folded = (value) => Array.isArray(value) && value.length > 0 && value.every((entry) => {
|
|
61710
|
+
const message = messageOf2(entry);
|
|
61711
|
+
return message !== void 0 && shown2.has(message);
|
|
61712
|
+
});
|
|
61713
|
+
const out = {};
|
|
61714
|
+
const emptied = [];
|
|
61715
|
+
for (const [key2, value] of Object.entries(body)) {
|
|
61716
|
+
const fold = (key2 === "issues" || key2 === "failures" || key2 === "warnings") && folded(value);
|
|
61717
|
+
if (fold) emptied.push(key2);
|
|
61718
|
+
out[key2] = fold ? [] : value;
|
|
61719
|
+
}
|
|
61720
|
+
if (emptied.length) out.foldedIntoFindings = emptied;
|
|
61721
|
+
return out;
|
|
61722
|
+
}
|
|
61723
|
+
var PREVIEW_TOKEN_NOTE = "the preview link carries a bearer with a limited lifetime, and this call has no usable expiry for it \u2014 the plane stated none, or the one it stated is unreadable, already past, or beyond the hour this CLI will reason about. Run the `previewRenew` command printed beside this note \u2014 from the same plane and credentials as this call (any `--serve-url`/`--control-url`/token flags you passed) \u2014 for a fresh one.";
|
|
61724
|
+
function previewTokenNote(expiresAt, now = Date.now()) {
|
|
61725
|
+
const at = expiresAt ? Date.parse(expiresAt) : NaN;
|
|
61726
|
+
if (!Number.isFinite(at)) return PREVIEW_TOKEN_NOTE;
|
|
61727
|
+
const seconds = (at - now) / 1e3;
|
|
61728
|
+
if (seconds <= 0 || seconds > 3600) return PREVIEW_TOKEN_NOTE;
|
|
61729
|
+
const minutes = Math.max(1, Math.round(seconds / 60));
|
|
61730
|
+
return `the preview link carries a bearer that expires at ${expiresAt} \u2014 about ${minutes} minute${minutes === 1 ? "" : "s"} from now; run the \`previewRenew\` command printed beside this note \u2014 from the same plane and credentials as this call (any \`--serve-url\`/\`--control-url\`/token flags you passed) \u2014 for a fresh one.`;
|
|
61731
|
+
}
|
|
61732
|
+
function previewRenewCommand(app, release, target, scope = {}) {
|
|
61733
|
+
return `himi preview --app ${app} --release ${release}` + (scope.screen ? ` --screen ${scope.screen}` : "") + (scope.surface ? ` --surface ${scope.surface}` : "") + (target !== "local" ? ` --target ${target}` : "");
|
|
61734
|
+
}
|
|
61560
61735
|
function previewPlayerLink(app, release, screenId) {
|
|
61561
61736
|
return buildPlayerPreviewUrl({ app, releaseId: release, screenId, source: "cli" });
|
|
61562
61737
|
}
|
|
@@ -63214,9 +63389,11 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
|
|
|
63214
63389
|
result.shots = { ...sp.written.length ? { dir: sp.dir } : {}, written: sp.written.length, ...sp.unavailable ? { unavailable: true } : {} };
|
|
63215
63390
|
}
|
|
63216
63391
|
}
|
|
63217
|
-
const { assembleLedger: assembleLedger2, trendAndStore: trendAndStore2, formatLedger: formatLedger2, diffAndStoreObservations: diffAndStoreObservations2, formatDelta: formatDelta2 } = await Promise.resolve().then(() => (init_coverage(), coverage_exports));
|
|
63218
|
-
const
|
|
63219
|
-
const
|
|
63392
|
+
const { assembleLedger: assembleLedger2, trendAndStore: trendAndStore2, formatLedger: formatLedger2, diffAndStoreObservations: diffAndStoreObservations2, formatDelta: formatDelta2, suppressedNativeKeys: suppressedNativeKeys2 } = await Promise.resolve().then(() => (init_coverage(), coverage_exports));
|
|
63393
|
+
const { nativeOracleShipped: nativeOracleShipped2 } = await Promise.resolve().then(() => (init_native_decode(), native_decode_exports));
|
|
63394
|
+
const nativeOracle = nativeOracleShipped2();
|
|
63395
|
+
const ledger = assembleLedger2(result, { nativeOracle });
|
|
63396
|
+
const trend = trendAndStore2(dir, ledger, suppressedNativeKeys2(nativeOracle));
|
|
63220
63397
|
const delta = diffAndStoreObservations2(dir, result.observations ?? {});
|
|
63221
63398
|
const legacy = { ...result, ledger, ...trend ? { trend } : {}, ...delta ? { delta } : {}, ...recorded ? { recorded } : {} };
|
|
63222
63399
|
const release = await loadReleaseFromDir(app, out);
|
|
@@ -64048,7 +64225,16 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
64048
64225
|
...loopbackWarning ? { warning: loopbackWarning } : {},
|
|
64049
64226
|
...report.issues ? { issues: report.issues } : {},
|
|
64050
64227
|
previewWeb: base ? previewWebUrl(base, app, release, void 0, void 0, previewToken) : null,
|
|
64051
|
-
|
|
64228
|
+
// A timestamp alone leaves the reader to work out both how long they have and how to
|
|
64229
|
+
// get another one. Print the duration and the re-mint command beside it (#2502 item 3).
|
|
64230
|
+
// No `mintedHere` guard is needed here, unlike `himi preview`: this bearer can only
|
|
64231
|
+
// have come from the publish response two hundred lines up, so THIS call minted it by
|
|
64232
|
+
// construction. `push` has no `--preview-token` to paste one in with.
|
|
64233
|
+
...previewToken ? {
|
|
64234
|
+
previewTokenExpiresAt: previewTokenExpiresAt ?? null,
|
|
64235
|
+
previewRenew: previewRenewCommand(app, release, targetName()),
|
|
64236
|
+
previewTokenNote: previewTokenNote(previewTokenExpiresAt)
|
|
64237
|
+
} : {},
|
|
64052
64238
|
// RFC 0051 — the surfaces THIS RELEASE targets, read from the manifest the
|
|
64053
64239
|
// push just published (never a re-read of the local file: review found the
|
|
64054
64240
|
// two can disagree — e.g. an older CLI whose local parser drops a key the
|
|
@@ -64509,6 +64695,8 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
64509
64695
|
}
|
|
64510
64696
|
let previewToken = str5(flags["preview-token"]);
|
|
64511
64697
|
let previewTokenExpiresAt;
|
|
64698
|
+
let mintedHere = false;
|
|
64699
|
+
let mintFailure;
|
|
64512
64700
|
if (!previewToken) {
|
|
64513
64701
|
const target = resolveTarget({
|
|
64514
64702
|
target: str5(flags.target),
|
|
@@ -64521,15 +64709,24 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
64521
64709
|
telemetryToken: str5(flags["telemetry-token"]),
|
|
64522
64710
|
controlToken: str5(flags["control-token"])
|
|
64523
64711
|
});
|
|
64524
|
-
if (target.controlToken) {
|
|
64712
|
+
if (!target.controlToken) {
|
|
64713
|
+
mintFailure = `this target has no stored control credential, so none was requested \u2014 run \`himi login --target ${targetName()}\`, or pass --control-token`;
|
|
64714
|
+
} else {
|
|
64525
64715
|
try {
|
|
64526
64716
|
const minted = await new ServeCliClient(target).previewToken(app, release);
|
|
64527
|
-
if (minted.ok
|
|
64717
|
+
if (!minted.ok) {
|
|
64718
|
+
mintFailure = `the plane refused the mint request (HTTP ${minted.status}). Your credential reached it, so this is the plane's answer, not your session`;
|
|
64719
|
+
} else if (!minted.json || typeof minted.json !== "object") {
|
|
64720
|
+
mintFailure = "the plane accepted the mint request but its answer was not a JSON object, so no token could be read. Your credential is fine";
|
|
64721
|
+
} else {
|
|
64528
64722
|
const body = minted.json;
|
|
64529
64723
|
previewToken = typeof body.previewToken === "string" ? body.previewToken : void 0;
|
|
64530
64724
|
previewTokenExpiresAt = typeof body.previewTokenExpiresAt === "string" ? body.previewTokenExpiresAt : void 0;
|
|
64725
|
+
mintedHere = previewToken !== void 0;
|
|
64726
|
+
if (!mintedHere) mintFailure = "the plane answered the mint request without a token \u2014 HIMALAYA_PREVIEW_TOKEN_SECRET may be unconfigured there. Your credential is fine";
|
|
64531
64727
|
}
|
|
64532
|
-
} catch {
|
|
64728
|
+
} catch (e) {
|
|
64729
|
+
mintFailure = `the mint request could not be sent: ${e.message}. Your credential was never judged`;
|
|
64533
64730
|
}
|
|
64534
64731
|
}
|
|
64535
64732
|
}
|
|
@@ -64540,7 +64737,22 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
64540
64737
|
...screen ? { screen } : {},
|
|
64541
64738
|
...surface ? { surface } : {},
|
|
64542
64739
|
previewWeb: base ? previewWebUrl(base, app, release, screen, surface, previewToken) : null,
|
|
64543
|
-
|
|
64740
|
+
// Gated on the MINT, like the two fields below: a plane that returns an expiry but no
|
|
64741
|
+
// token would otherwise print a lifetime for a bearer the same envelope says does not
|
|
64742
|
+
// exist.
|
|
64743
|
+
...mintedHere && previewTokenExpiresAt ? { previewTokenExpiresAt } : {},
|
|
64744
|
+
// Same as the push envelope: the expiry alone leaves the reader to work out both how
|
|
64745
|
+
// long they have and how to get another link. `himi preview` re-mints, so the renew is
|
|
64746
|
+
// this very command — say so rather than making them find it again (#2502 item 3).
|
|
64747
|
+
//
|
|
64748
|
+
// Gated on the MINT, not on the expiry: a plane that mints a bearer but returns no
|
|
64749
|
+
// `previewTokenExpiresAt` would otherwise withhold the guidance in exactly the case
|
|
64750
|
+
// where the reader has no timestamp to reason from — while a bearer the caller pasted
|
|
64751
|
+
// in with `--preview-token` is one this command cannot promise to reissue.
|
|
64752
|
+
...mintedHere ? {
|
|
64753
|
+
previewRenew: previewRenewCommand(app, release, targetName(), { screen, surface }),
|
|
64754
|
+
previewTokenNote: previewTokenNote(previewTokenExpiresAt)
|
|
64755
|
+
} : previewToken || !mintFailure ? {} : { previewTokenNote: `no preview bearer was minted \u2014 ${mintFailure}. The link above carries no \`pvt\`, so it opens a LIVE release and will not open a draft.` },
|
|
64544
64756
|
previewPlayerDebug: previewPlayerDebugLink(app, release, screen, previewToken),
|
|
64545
64757
|
context: base ? previewContextUrl(base, app, release, screen) : null,
|
|
64546
64758
|
diagnose: base && screen ? previewDiagnoseUrl(base, app, release, screen) : null,
|
|
@@ -64718,8 +64930,16 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
64718
64930
|
if (!r.ok) throw Object.assign(new Error("crashes_read_failed"), { response: r });
|
|
64719
64931
|
out.crashes = r.json?.crashes ?? [];
|
|
64720
64932
|
}
|
|
64721
|
-
|
|
64722
|
-
|
|
64933
|
+
const eventCount = out.events?.length ?? 0;
|
|
64934
|
+
const logCount = out.logs?.length ?? 0;
|
|
64935
|
+
const narrowed = !!(level || str5(flags.screen) || str5(flags.screenId) || str5(flags.type) || str5(flags.event) || flags["crashes-only"] === true || flags.crashesOnly === true || num(flags.limit) !== void 0 || eventSince !== void 0 || logSince !== void 0);
|
|
64936
|
+
const wholeChannels = includeEvents && includeLogs && !narrowed;
|
|
64937
|
+
if (wholeChannels && eventCount === 0 && logCount === 0 && !includeSummary && !includeCrashes) {
|
|
64938
|
+
out.note = "no events and no worker console output on this release \u2014 open the web preview or Himi Player link for it, then retry or use --follow";
|
|
64939
|
+
} else if (wholeChannels && eventCount > 0 && logCount === 0) {
|
|
64940
|
+
out.note = "events arrived but the worker console channel is empty \u2014 commonly because the worker never called `console.*`, though an empty channel can also mean the lines aged out or never made it through ingestion. What the CLI can say is that this channel returned nothing, not that the app was never opened";
|
|
64941
|
+
} else if (narrowed && eventCount === 0 && logCount === 0 && !includeSummary && !includeCrashes) {
|
|
64942
|
+
out.note = "nothing matched this query. Drop --screen/--type/--level/--since/--limit/--crashes-only to see the whole event and console channels for this release";
|
|
64723
64943
|
}
|
|
64724
64944
|
return out;
|
|
64725
64945
|
};
|
|
@@ -64800,7 +65020,7 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
64800
65020
|
if (r.status !== 422) return printServeFailure(r);
|
|
64801
65021
|
}
|
|
64802
65022
|
const body = r.json ?? {};
|
|
64803
|
-
print(body);
|
|
65023
|
+
print(diagnoseBody(body));
|
|
64804
65024
|
return body.ok === false ? 1 : 0;
|
|
64805
65025
|
}
|
|
64806
65026
|
case "deeplinks": {
|
|
@@ -64856,8 +65076,12 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
64856
65076
|
print({ ok: false, status: r.status, body: r.json ?? r.text });
|
|
64857
65077
|
return 1;
|
|
64858
65078
|
}
|
|
64859
|
-
const
|
|
64860
|
-
|
|
65079
|
+
const rawLogs = r.json?.logs ?? [];
|
|
65080
|
+
const logs = byLevel(rawLogs);
|
|
65081
|
+
const narrowed = !!(level || str5(flags.screen) || str5(flags.screenId) || num2(flags.since) !== void 0 || num2(flags.limit) !== void 0);
|
|
65082
|
+
const scope = release ? "this release" : "this app";
|
|
65083
|
+
const note = logs.length > 0 ? void 0 : narrowed ? `no worker \`console.*\` lines matched this query${level && rawLogs.length ? ` (${rawLogs.length} line(s) were hidden by --level ${level})` : ""}. Drop --level/--screen/--since/--limit to see the whole console channel for ${scope}; the telemetry EVENT stream is a separate channel, read with \`himi telemetry\`.` : `no worker \`console.*\` output on ${scope}. This is the console channel \u2014 distinct from the telemetry EVENT stream, which \`himi telemetry --app <app> --release <id>\` reads: ${scope} can carry many events and no console lines \u2014 commonly because the worker never logged any, though an empty channel can also mean the lines aged out or never made it through ingestion. Boot-time \`console.log\` is visible via \`himi validate\` / \`himi render\`.`;
|
|
65084
|
+
print({ ok: true, logs, ...note ? { note } : {} });
|
|
64861
65085
|
return 0;
|
|
64862
65086
|
}
|
|
64863
65087
|
const interval = num2(flags.interval) ?? 2e3;
|
|
@@ -66139,12 +66363,14 @@ export {
|
|
|
66139
66363
|
ConfigLoadError,
|
|
66140
66364
|
PLACEHOLDER_CLIENT_ID,
|
|
66141
66365
|
PLACEHOLDER_SITE_URL,
|
|
66366
|
+
PREVIEW_TOKEN_NOTE,
|
|
66142
66367
|
PUBLIC_WIX_API_BASE,
|
|
66143
66368
|
autoClaimSlug,
|
|
66144
66369
|
buildDoctorReport,
|
|
66145
66370
|
compareVersions2 as compareVersions,
|
|
66146
66371
|
decodeControlTokenClaims,
|
|
66147
66372
|
defaultTargetForCommand,
|
|
66373
|
+
diagnoseBody,
|
|
66148
66374
|
doctorVersionReport,
|
|
66149
66375
|
foreignPlaneError,
|
|
66150
66376
|
formatSdkVersionLine,
|
|
@@ -66167,8 +66393,10 @@ export {
|
|
|
66167
66393
|
previewPlayerDebugLink,
|
|
66168
66394
|
previewPlayerLink,
|
|
66169
66395
|
previewRenderUrl,
|
|
66396
|
+
previewRenewCommand,
|
|
66170
66397
|
previewScreensUrl,
|
|
66171
66398
|
previewTelemetryUrl,
|
|
66399
|
+
previewTokenNote,
|
|
66172
66400
|
previewWebUrl,
|
|
66173
66401
|
probeControlToken,
|
|
66174
66402
|
probeReachable,
|
package/guides/himi-test.md
CHANGED
|
@@ -236,7 +236,7 @@ browser.
|
|
|
236
236
|
| `--no-shots` | Skip the per-screen PNGs written to `.himi/shots/`. |
|
|
237
237
|
| `--net <file>` | Use an explicit net-mocks file instead of the app's own. |
|
|
238
238
|
| `--budget <ms>` | Per-action settle budget (default 2000). |
|
|
239
|
-
| `--native` | Also decode every baked descriptor with the real native decoders; bare `--native` runs whichever toolchains are present, and `--native ios` or `--native android` selects one. |
|
|
239
|
+
| `--native` | Also decode every baked descriptor with the real native decoders; bare `--native` runs whichever toolchains are present, and `--native ios` or `--native android` selects one. **Source-checkout only** — the decoder oracles are that tree's own HimalayaCore Swift package and Gradle harness, which a published install does not ship, so it cannot run this at all (and its coverage ledger stays silent about native decoding rather than suggesting it). |
|
|
240
240
|
| `--all-screens` | Crawl every templated instance instead of one representative per structure. |
|
|
241
241
|
| `--browser` / `--no-open` / `--timeout <ms>` | Run the loop in your own browser tab (one screen per run; refuses record/net/offline/hostile/deep/budget/shots/native). |
|
|
242
242
|
| `himi test <screenId>…` | Focus one or more screens — the record and shots passes scope with them. |
|
package/guides/mobile-ux.md
CHANGED
|
@@ -137,18 +137,32 @@ nothing, so it takes no checks and needs a `--reason` instead.
|
|
|
137
137
|
|
|
138
138
|
### Strict audit contract
|
|
139
139
|
|
|
140
|
-
**Two commands read
|
|
141
|
-
|
|
140
|
+
**Two commands read this contract.** `himi validate` reports a gap as a **warning**, so a rewrite
|
|
141
|
+
that drops something the gate needs is caught in the loop you are already running rather than at
|
|
142
142
|
the end of it. `himi validate --strict` and `himi ux audit --strict` both promote that warning to a
|
|
143
|
-
**release blocker**. Both read the same
|
|
144
|
-
|
|
145
|
-
one primary call to action") fails the same as omitting them:
|
|
143
|
+
**release blocker**. Both read the same markers. The two HEADINGS are matched verbatim — other
|
|
144
|
+
tooling addresses the document by them, so a heading really is the thing itself:
|
|
146
145
|
|
|
147
146
|
- The heading `## Implementation acceptance`.
|
|
148
147
|
- The heading `## Evidence`.
|
|
149
|
-
- The phrase `one primary CTA per state`.
|
|
150
148
|
|
|
151
|
-
The
|
|
149
|
+
The **primary-CTA** marker is satisfied **either** structurally *or* by the legacy phrase,
|
|
150
|
+
because a semantically equivalent rewrite — "each state exposes exactly one primary call to
|
|
151
|
+
action" — used to fail exactly like omitting it, and four of five independent testers wrote a
|
|
152
|
+
better document, were refused, and pasted the sentence back:
|
|
153
|
+
|
|
154
|
+
- *structurally*: `## Implementation acceptance` carries a `Primary CTA` column, has a row for
|
|
155
|
+
every state you declare in a `… states:` bullet under `## Screen contract`, and names exactly one
|
|
156
|
+
CTA per row (a comma, a semicolon or a spaced slash reads as two; "Save and close", "Save &
|
|
157
|
+
Close" and "Save + Continue" are each one label and pass). The row check reads a `states:` bullet
|
|
158
|
+
only when every item is a **name** (`ready, empty, destructive-confirm`); a bullet answered with
|
|
159
|
+
prose declares nothing and the row check is skipped rather than guessed at — no rule can tell a
|
|
160
|
+
short description from a name, and inventing states from prose would fail exactly the documents
|
|
161
|
+
that took the most care.
|
|
162
|
+
- *or literally*: the phrase `one primary CTA per state` appears anywhere. It is not deprecated —
|
|
163
|
+
a document that passes today keeps passing.
|
|
164
|
+
|
|
165
|
+
The remaining marker, **width evidence**, is graded by **content, not by phrase** — it is a
|
|
152
166
|
fact about what you rendered rather than a string, so there is nothing to paste. Satisfy it either
|
|
153
167
|
way:
|
|
154
168
|
|
|
@@ -202,6 +216,13 @@ no wider width, two widths too far apart to be one statement, or no number spell
|
|
|
202
216
|
says so alongside the height-axis rule if that is what discarded your numbers. Read the branch it
|
|
203
217
|
gave you rather than pasting the example: the example is a form that works, not the only one.
|
|
204
218
|
|
|
219
|
+
**HTML comments are stripped before any of this runs.** The scaffold ships a `<!-- LOAD-BEARING -->`
|
|
220
|
+
note quoting the load-bearing strings verbatim, right beside the line telling you to replace the
|
|
221
|
+
example rows — so the note is guidance and never an answer: a document that kept the note and
|
|
222
|
+
deleted the prose satisfies nothing. An **unterminated** `<!--` swallows the rest of the file here
|
|
223
|
+
and in any renderer alike, so that is reported on its own rather than as four headings "missing"
|
|
224
|
+
that you can plainly see.
|
|
225
|
+
|
|
205
226
|
A `MOBILE-UX.md` that is **absent or empty** is not four missing markers. `himi ux audit` reports it
|
|
206
227
|
once, as a missing contract; `himi validate` stays silent about it entirely. Asking for the document
|
|
207
228
|
itself is the audit's job — most apps never adopt this contract, and a warning on every one of them
|
|
@@ -326,6 +326,10 @@ himi test <screenId> --record # refresh dev/net-mocks.json from live boot rea
|
|
|
326
326
|
himi test <screenId> --native # run BOTH real native decoders over the baked JSON bytes
|
|
327
327
|
```
|
|
328
328
|
|
|
329
|
+
`--native` runs only from a Himalaya **source checkout**: the decoder oracles are that tree's own HimalayaCore Swift
|
|
330
|
+
package and Gradle harness, which a published/off-repo install does not ship. Working off-repo,
|
|
331
|
+
skip it — the coverage ledger will not ask you for it either.
|
|
332
|
+
|
|
329
333
|
**Then see it.** `himi render <screenId> --png <file>` per scheme is the fast visual check,
|
|
330
334
|
and `himi push` gives you a draft preview to open in a browser. Neither drives a real
|
|
331
335
|
device, which is what `himi ux record` is for: it records per platform that a drive
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.856.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": [
|
|
@@ -63,5 +63,5 @@
|
|
|
63
63
|
"artifactId": "himalaya-cli"
|
|
64
64
|
}
|
|
65
65
|
},
|
|
66
|
-
"falconPackageHash": "
|
|
66
|
+
"falconPackageHash": "82a824b69c0914106e80e0fb205b046bd8e669b53f46887b3adf0eed"
|
|
67
67
|
}
|