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