@wix/himalaya-cli 0.926.0 → 0.928.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -5241,6 +5241,7 @@ function resolveTranslation(value, catalog, sourceCatalog) {
5241
5241
  var BINDING_SIGILS, BINDING_SIGIL_RE, TRANSLATION_PREFIX, TRANSLATION_KEY_RE;
5242
5242
  var init_bindings = __esm({
5243
5243
  "../../core/ts/src/bindings.ts"() {
5244
+ "use strict";
5244
5245
  BINDING_SIGILS = ["service", "flag", "item", "t"];
5245
5246
  BINDING_SIGIL_RE = new RegExp(
5246
5247
  `^\\$(?:${BINDING_SIGILS.filter((s) => s !== "t").join("|")})\\b|^\\$t:`
@@ -18525,6 +18526,7 @@ function shouldRevealError(args) {
18525
18526
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
18526
18527
  var init_validation = __esm({
18527
18528
  "../../core/ts/src/validation.ts"() {
18529
+ "use strict";
18528
18530
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
18529
18531
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
18530
18532
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -21948,6 +21950,7 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
21948
21950
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
21949
21951
  var init_worker_subtree = __esm({
21950
21952
  "../../core/ts/src/worker-subtree.ts"() {
21953
+ "use strict";
21951
21954
  init_worker_subtree_safe_types_generated();
21952
21955
  WORKER_SUBTREE_CEILINGS = Object.freeze({
21953
21956
  maxEncodedBytes: 65536,
@@ -50980,6 +50983,57 @@ var ServeCliClient = class {
50980
50983
  setAppConfig(app, patch) {
50981
50984
  return this.request("control", "POST", "/v1/admin/app-config", { body: { app, patch }, headers: this.ctrlHeaders() });
50982
50985
  }
50986
+ // ---- flags (RFC 0013) ----
50987
+ //
50988
+ // ALL FOUR ARE ON THE CONTROL PLANE, the read included, and that is not an oversight about
50989
+ // which door a device uses. `/v1/flags/{app}` is implemented in serve-control; the deployed
50990
+ // artifact happens to answer it on the serve origin too, because one process hosts both
50991
+ // (`isControlPath` in packages/serve/src/index.ts). Locally they are two processes, and the
50992
+ // one on the serve port answers `/v1/flags/` with a STUB — `{definitions: {}}`, unconditional
50993
+ // 200, documented as such in core/serve/src/run-handler.ts. So reading "the device door" from
50994
+ // the serve base is right on prod and silently wrong everywhere else: the list comes back
50995
+ // empty and every write reports that it is not being served.
50996
+ //
50997
+ // The read is still the point of the pairing: it is the same handler and the same file the
50998
+ // write touched, which is what makes it able to contradict a 200.
50999
+ /** The kill-list for an app (GET /v1/revocations?app=…) — the integrities devices refuse.
51000
+ *
51001
+ * CONTROL, for the same reason `flags()` is: the route is serve-control's, and the process on
51002
+ * the local serve port answers it with a deliberate empty stub (core/serve/src/run-handler.ts
51003
+ * — "the kill-switch is INERT here"). Reading that would report every template as live,
51004
+ * including the ones somebody just killed, which is the one answer this must never give. */
51005
+ revocations(app) {
51006
+ return this.request("control", "GET", `/v1/revocations?app=${encodeURIComponent(app)}`);
51007
+ }
51008
+ /** Flag definitions as the plane would serve them (GET /v1/flags/{app}). No credential. */
51009
+ flags(app) {
51010
+ return this.request("control", "GET", `/v1/flags/${encodeURIComponent(app)}`);
51011
+ }
51012
+ /** Create or replace one flag definition (POST /v1/admin/flags). */
51013
+ setFlag(app, key2, definition) {
51014
+ return this.request("control", "POST", "/v1/admin/flags", { body: { app, key: key2, definition }, headers: this.ctrlHeaders() });
51015
+ }
51016
+ /** Ramp or kill a flag in place (POST /v1/admin/flags/ramp) — 404s an unknown key. */
51017
+ rampFlag(app, key2, change) {
51018
+ return this.request("control", "POST", "/v1/admin/flags/ramp", {
51019
+ body: {
51020
+ app,
51021
+ key: key2,
51022
+ // Spread-if-present, never `rolloutPercentage: change.rolloutPercentage`: the route reads
51023
+ // `typeof === "number"`, so an explicit `undefined` is harmless — but `enabled` is read as
51024
+ // `typeof === "boolean"`, and sending `{enabled: undefined}` in JSON drops the key anyway.
51025
+ // Being explicit here keeps the wire body equal to what the caller asked for, so a ramp
51026
+ // that only sets a percentage cannot also be read as "and leave enabled alone" by luck.
51027
+ ...typeof change.rolloutPercentage === "number" ? { rolloutPercentage: change.rolloutPercentage } : {},
51028
+ ...typeof change.enabled === "boolean" ? { enabled: change.enabled } : {}
51029
+ },
51030
+ headers: this.ctrlHeaders()
51031
+ });
51032
+ }
51033
+ /** Delete one flag definition (POST /v1/admin/flags/remove); echoes the definition it removed. */
51034
+ removeFlag(app, key2) {
51035
+ return this.request("control", "POST", "/v1/admin/flags/remove", { body: { app, key: key2 }, headers: this.ctrlHeaders() });
51036
+ }
50983
51037
  /** Write-through a design-tokens patch (POST /v1/admin/tokens). */
50984
51038
  setTokens(app, tokens, kit) {
50985
51039
  return this.request("control", "POST", "/v1/admin/tokens", { body: { app, tokens, ...kit ? { kit } : {} }, headers: this.ctrlHeaders() });
@@ -54382,6 +54436,9 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
54382
54436
  plane, no dev-server. With no argument it packs the current
54383
54437
  content package first \u2014 edit, run, look at it.
54384
54438
  Blocks until Ctrl-C.
54439
+ himi flags list|set|ramp|rm [<key>] [--app <a>]
54440
+ read/change operational flags (RFC 0013); writes report
54441
+ servedNow from the device door, never just ok
54385
54442
  himi visibility [public|unlisted] [--app <a>]
54386
54443
  read (no value) or set whether the public registry lists the app
54387
54444
  himi preview --app <app> --release <id> [--screen <id>] [--surface <id>] [--preview-token <token>]
@@ -54712,6 +54769,7 @@ var COMMANDS = [
54712
54769
  { name: "logs", summary: "worker console read-back (GET /v1/telemetry/logs); --follow tails via the seq cursor", required: "--app [--release] [--screen] [--level] [--follow]" },
54713
54770
  { name: "promote", summary: "flip a stored draft LIVE (POST /v1/admin/release/activate); --ring also points a rollout ring at it in the same command (POST /v1/admin/promote), which ring-based clients need in order to resolve at all \u2014 without it they stay unresolved and the warning goes to stderr; --percent sets that ring's rollout share; a release pushed past errors needs an explicit --force override", required: "--app --release [--ring <ring>] [--percent 0-100] [--force] (--control-token)" },
54714
54771
  { name: "patch", summary: 'change ONE item of a published app \u2014 a screen descriptor, a Tier-5 bundle, tokens, or the app-config baseline \u2014 from a RAW FILE, with no content package and no build. The low-level door: it does not lint your package, because there is no package here; `himi push --only <selector>` is the same edit with the full build + lint gate and is what to reach for when the package is in hand. Derives a new release from the app\'s current LIVE release, or from `--from-release <id>`. An explicit `--draft` or `--live` is REQUIRED \u2014 a patch has no default blast radius \u2014 and `--notes` is required with `--live` on a screen or app-config patch, because off-repo `gitSha` is "dev" and the ledger notes are then the only durable record of bytes that correspond to no commit. A branch does not merge: activating one derived from an older release reverts every item the patch does not move, so that case is refused until `--accept-regressions`. A bundle patch must repoint a screen (`--for-screen`) or the bytes land under a manifest key nothing reads. It cannot ADD a screen (screens are git-only, RFC 0012), never claims a slug \u2014 the app must already be published \u2014 and carries no attestation, source archive, assets, icon or minBinaryVersion, each named in the output.', required: "screen <id> --file <descriptor.json> [--bundle <sha256|none>] | bundle --file <bundle.js> (--for-screen <id>[,<id>] | --no-rebind) | tokens --file <tokens.json> | app-config --file <app-config.json> ; (--draft|--live) --app <a> [--from-release <id>] [--accept-regressions] [--label <n>] [--notes <t>] [--force] [--target <t>] (--control-token)" },
54772
+ { name: "flags", summary: "read and change the app's OPERATIONAL flags (RFC 0013) \u2014 `list` prints what the DEVICE door serves, `set <key> --definition` creates or replaces one, `ramp <key>` moves its rollout share or kills it, `rm <key>` deletes it and echoes the definition back so the removal is reversible. Every write is followed by a read of the same door the device uses, and the answer is reported as `servedNow`: flags are stored in the app's git-committed flags.json, so a control plane with no checkout accepts a write it will never serve.", required: "list|set|ramp|rm [<key>] [--definition <json|@file>] [--percent 0-100] [--enabled true|false] [--app <a>] (--control-token)" },
54715
54773
  { name: "visibility", summary: "read (no value) or set whether the public registry (/v1/apps + the Himi Player picker) LISTS the app. `public` lists it for everyone; `unlisted` hides it from listings while direct reads by app id \u2014 deep links, previews \u2014 keep working. App from --app or the content dir. Setting it needs no republish.", required: "[public|unlisted] [--app <a>] [--content <dir>] (--control-token)" },
54716
54774
  { name: "dist", summary: "build an INSTALLABLE QA binary (signed IPA / APK) on the Wix Job Service Mac pool and return an install link \u2014 the serve plane brokers the job, the pool builds it. WAITS for the build by default (~4 min) and prints the install link; `--no-wait` returns a jobId immediately instead. Omitting `--release` builds whatever is LIVE for the app. A named draft is refused unless `--allow-draft` makes that exceptional build explicit. `status <jobId>` / `watch <jobId>` / `list --app <a>` / `cancel <jobId>` inspect and stop builds (`watch` reports up front whether the job mints its own signing identity or borrows one, so a doomed iOS build is cancellable in seconds rather than after the archive). Off-repo, name a target \u2014 `--target wix` for a self-serve Wix login or `--target bo` for the internal employee path (the in-repo default is a local dev plane that isn't reachable off-repo); self-serve callers may build their OWN apps under a tighter budget (2 in flight / 5 per 24h), while a manage-only owner-role credential is refused. `--shell` picks the native host to sign into (branded-lite | owner-lite | player | demo-app). `--signing` defaults to `wix-managed` \u2014 the Wix Apple account mints a per-app signing identity for the build, so the caller provisions no Apple credentials of their own; `--signing byo` (sign with your own Apple account) is reserved and 501s. `--android-signing` is the Android counterpart and a SEPARATE axis (a keystore, not an Apple account): `qa-keystore` (default \u2014 the repo's committed QA key, identical on every pool machine, so a build installs over the previous one), `wix-keystore` (the Wix key the pool machine carries; the build FAILS naming the paths searched if it carries none, rather than quietly signing with something else), or `debug` (the old per-machine key \u2014 QA must uninstall any previous build first). NOT the normal way to ship: screens, Tier-5 bundles, tokens and app-config all go over the air with `himi push` in seconds \u2014 a binary trip is only needed for a NATIVE delta (a new SDUI component, a new declared capability/entitlement, a new bundle id, or a minBinaryVersion floor bump). Refuses to build an app whose launcher icon is missing, a placeholder, or one a store would reject \u2014 the one thing a binary carries that no later OTA push can fix; `--force` builds anyway", required: "[--app <a>] [--platform ios,android] [--release <id>] [--allow-draft] [--target <t>] [--shell branded-lite|owner-lite|player|demo-app] [--signing wix-managed|byo] [--android-signing qa-keystore|wix-keystore|debug] [--borrow-ios-id <bundleId>] [--no-wait] [--force] | status <jobId> [--target <t>] | watch <jobId> [--target <t>] [--interval <ms>] | list --app <a> [--target <t>] | cancel <jobId> [--target <t>]" },
54717
54775
  { name: "testflight", summary: 'build, sign and upload an app to TestFlight (internal testers \u2014 no Beta App Review), then WAIT until App Store Connect finishes processing it. Sugar for `store upload --track testflight`. Prompts first: a build number is consumed permanently on a real Apple account and cannot be reused \u2014 `--yes` for CI, `--dry-run` to see the plan without touching anything. The wait is the real check, not a nicety: fastlane prints "Successfully uploaded" for a binary App Store Connect then rejects in processing, and a rejected binary leaves NO build record at all (the reason is emailed). Public App Store review is deliberately NOT automatable here \u2014 `--track appstore` is refused; use /submit-ios-app when you mean to go public', required: "<app> [--track testflight|internal] [--profile <name>] [--yes] [--dry-run] [--skip-build] [--no-wait] [--json]" },
@@ -54886,6 +54944,12 @@ function diagnoseBody(body) {
54886
54944
  if (emptied.length) out.foldedIntoFindings = emptied;
54887
54945
  return out;
54888
54946
  }
54947
+ var GIT_FLAG_STORE_NOTE = "the plane accepted the write but is not serving it. Operational flags live in the app's git-committed flags.json (RFC 0013), so a control plane with no checkout \u2014 a hosted pod \u2014 writes to a filesystem that is not read back and is lost on restart. Commit the change to the app's flags.json, or point this command at a control plane that has the repo.";
54948
+ function servesAll(served, asked) {
54949
+ const stable = (v) => JSON.stringify(v, (_k, x) => x && typeof x === "object" && !Array.isArray(x) ? Object.fromEntries(Object.entries(x).sort(([a], [b]) => a.localeCompare(b))) : x);
54950
+ return Object.entries(asked).every(([k, v]) => stable(served[k]) === stable(v));
54951
+ }
54952
+ var GIT_FLAG_STORE_HALF_WRITE_NOTE = "the plane reported a failure, but this store writes flags.json BEFORE it commits \u2014 so the change may already be on the pod that served this request, uncommitted and lost on its next restart. servedNow says what that pod answers now; other pods may differ.";
54889
54953
  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.";
54890
54954
  function previewTokenNote(expiresAt, now = Date.now()) {
54891
54955
  const at = expiresAt ? Date.parse(expiresAt) : NaN;
@@ -54985,9 +55049,9 @@ function isPublicTarget(target, control) {
54985
55049
  }
54986
55050
  return isPublicServeOrigin(url ?? "");
54987
55051
  }
54988
- function serveUnreachableError(target, base, cause) {
55052
+ function serveUnreachableError(target, base, cause, plane = "serve") {
54989
55053
  return {
54990
- error: `cannot reach the serve plane for target "${target}" (${base || "no serve url"}): ${cause}. ${serveUnreachableHint(target)}`,
55054
+ error: `cannot reach the ${plane} plane for target "${target}" (${base || `no ${plane} url`}): ${cause}. ${serveUnreachableHint(target)}`,
54991
55055
  code: "needs_vpn",
54992
55056
  target
54993
55057
  };
@@ -55748,6 +55812,7 @@ himi v${cliVersion()} \u2014 run \`himi --version\`. If authoring commands fail
55748
55812
  })
55749
55813
  );
55750
55814
  const serveBase = () => resolveTarget({ target: str5(flags.target), defaultTarget: contextualDefaultTarget, serveUrl: str5(flags["serve-url"]) }).serve;
55815
+ const controlBase = () => resolveTarget({ target: str5(flags.target), defaultTarget: contextualDefaultTarget, controlUrl: str5(flags["control-url"]) }).control ?? "";
55751
55816
  const foreignPlaneRefusal = async () => {
55752
55817
  const base = serveBase();
55753
55818
  if (!isLoopbackBase(base)) return null;
@@ -58951,6 +59016,184 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
58951
59016
  extra: { kind, file: { path: file, bytes: bytes2.length, sha256: sha } }
58952
59017
  });
58953
59018
  }
59019
+ // OPERATIONAL FLAGS (RFC 0013) — the surface the admin portal's FlagsPanel used to own.
59020
+ //
59021
+ // The panel is going; this is what replaces it, and it is deliberately not a port. The panel
59022
+ // wrote `flags.json` into a pod filesystem and reported success, which is why "flags don't
59023
+ // work in production" was true and invisible for so long. Every write here is followed by a
59024
+ // READ of the device-facing door, and what that read says is what gets printed.
59025
+ case "flags": {
59026
+ const sub = pos[1];
59027
+ const app = str5(flags.app) ?? await appId(contentDir(flags));
59028
+ const usage = "usage: himi flags list | set <key> --definition <json|@file> | ramp <key> [--percent 0-100] [--enabled true|false] | rm <key>";
59029
+ if (sub !== "list" && sub !== "set" && sub !== "ramp" && sub !== "rm") {
59030
+ io.error(JSON.stringify({ error: `unknown flags subcommand: ${sub ?? "(none)"}`, hint: usage }));
59031
+ return 2;
59032
+ }
59033
+ const servedDefs = async () => {
59034
+ try {
59035
+ const r = await client2().flags(app);
59036
+ if (!r.ok) return null;
59037
+ const defs = r.json?.definitions;
59038
+ return Array.isArray(defs) ? defs : null;
59039
+ } catch {
59040
+ return null;
59041
+ }
59042
+ };
59043
+ if (sub === "list") {
59044
+ let r;
59045
+ try {
59046
+ r = await client2().flags(app);
59047
+ } catch (e) {
59048
+ io.error(JSON.stringify({
59049
+ ...serveUnreachableError(targetName(), controlBase(), e.message, "control"),
59050
+ // The deployed artifact answers control routes on its public serve origin too
59051
+ // (one process, `isControlPath`), so a reader who cannot reach the control host has
59052
+ // somewhere to point. Said as an override rather than done automatically: the local
59053
+ // serve port answers /v1/flags/ with a stub, and silently reading that is the
59054
+ // failure this whole command is built to avoid.
59055
+ hint: `flag routes live on the control plane; on a deployed target the same route is answered by the serve origin, so \`--control-url <serve origin>\` is the read-only way round a control host you cannot reach`
59056
+ }));
59057
+ return 1;
59058
+ }
59059
+ if (!r.ok) {
59060
+ print({ ok: false, status: r.status, body: r.json ?? r.text });
59061
+ return 1;
59062
+ }
59063
+ const defs = r.json?.definitions ?? [];
59064
+ print({ ok: true, app, count: defs.length, definitions: defs });
59065
+ return 0;
59066
+ }
59067
+ const key2 = pos[2];
59068
+ if (!key2) {
59069
+ io.error(JSON.stringify({ error: `flags ${sub} needs a key`, hint: usage }));
59070
+ return 2;
59071
+ }
59072
+ if (sub === "set") {
59073
+ const arg = str5(flags.definition);
59074
+ if (!arg) {
59075
+ io.error(JSON.stringify({ error: "flags set needs --definition <json|@file>", hint: usage }));
59076
+ return 2;
59077
+ }
59078
+ let def;
59079
+ try {
59080
+ const parsed = JSON.parse(arg.startsWith("@") ? readFileSync64(arg.slice(1), "utf8") : arg);
59081
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
59082
+ io.error(JSON.stringify({ error: "invalid_definition", problems: [`--definition must be a JSON object, got ${Array.isArray(parsed) ? "an array" : parsed === null ? "null" : typeof parsed}`] }));
59083
+ return 2;
59084
+ }
59085
+ def = parsed;
59086
+ } catch (e) {
59087
+ io.error(JSON.stringify({ error: "invalid_definition_json", detail: e.message }));
59088
+ return 2;
59089
+ }
59090
+ if (typeof def.key === "string" && def.key !== key2) {
59091
+ io.error(JSON.stringify({
59092
+ error: "definition_key_mismatch",
59093
+ key: key2,
59094
+ definitionKey: def.key,
59095
+ hint: `the stored flag takes its key from the command, so this would save "${def.key}"'s definition under "${key2}"`
59096
+ }));
59097
+ return 2;
59098
+ }
59099
+ const problems = [];
59100
+ if (typeof def.enabled !== "boolean") problems.push("enabled must be true or false");
59101
+ const variants = Array.isArray(def.variants) ? def.variants : null;
59102
+ if (!variants || variants.length === 0) problems.push("variants must be a non-empty array");
59103
+ if (def.rolloutPercentage !== void 0 && (typeof def.rolloutPercentage !== "number" || def.rolloutPercentage < 0 || def.rolloutPercentage > 100)) {
59104
+ problems.push(`rolloutPercentage must be 0..100, got ${JSON.stringify(def.rolloutPercentage)}`);
59105
+ }
59106
+ if (typeof def.defaultVariant !== "string") problems.push("defaultVariant must name a variant");
59107
+ else if (variants && variants.length > 0 && !variants.some((v) => v?.key === def.defaultVariant)) {
59108
+ problems.push(`defaultVariant "${def.defaultVariant}" is not one of the variants`);
59109
+ }
59110
+ if (problems.length > 0) {
59111
+ io.error(JSON.stringify({ error: "invalid_definition", problems, hint: 'e.g. {"enabled":true,"variants":[{"key":"on","weight":100},{"key":"off","weight":0}],"defaultVariant":"off"}' }));
59112
+ return 2;
59113
+ }
59114
+ const sent = { ...def, key: key2 };
59115
+ return await writeFlag(() => client2().setFlag(app, key2, sent), (now) => !!now && servesAll(now, sent));
59116
+ }
59117
+ if (sub === "ramp") {
59118
+ const percentGiven = flags.percent !== void 0;
59119
+ const percent = num(flags.percent);
59120
+ if (percentGiven && (percent === void 0 || percent < 0 || percent > 100)) {
59121
+ io.error(JSON.stringify({ error: `--percent must be 0..100, got ${String(flags.percent)}` }));
59122
+ return 2;
59123
+ }
59124
+ const enabledGiven = flags.enabled !== void 0;
59125
+ const enabledRaw = flags.enabled === true ? "true" : str5(flags.enabled);
59126
+ if (enabledGiven && enabledRaw !== "true" && enabledRaw !== "false") {
59127
+ io.error(JSON.stringify({ error: `--enabled must be true or false, got ${String(flags.enabled)}` }));
59128
+ return 2;
59129
+ }
59130
+ if (!percentGiven && !enabledGiven) {
59131
+ io.error(JSON.stringify({ error: "flags ramp needs --percent and/or --enabled", hint: usage }));
59132
+ return 2;
59133
+ }
59134
+ const change = {
59135
+ ...percentGiven ? { rolloutPercentage: percent } : {},
59136
+ ...enabledGiven ? { enabled: enabledRaw === "true" } : {}
59137
+ };
59138
+ return await writeFlag(() => client2().rampFlag(app, key2, change), (now) => !!now && servesAll(now, change));
59139
+ }
59140
+ const removedBefore = (await servedDefs())?.find((d) => d?.key === key2);
59141
+ return await writeFlag(() => client2().removeFlag(app, key2), (now) => now === void 0, removedBefore);
59142
+ async function writeFlag(send2, expected, undoCopy) {
59143
+ let r;
59144
+ try {
59145
+ r = await send2();
59146
+ } catch (e) {
59147
+ io.error(JSON.stringify(serveUnreachableError(targetName(), controlBase(), e.message, "control")));
59148
+ return 1;
59149
+ }
59150
+ if (!r.ok) {
59151
+ const served2 = r.status >= 500 ? await servedDefs() : null;
59152
+ const now2 = served2?.find((d) => d?.key === key2);
59153
+ const removed = r.json?.removed ?? (r.status >= 500 ? undoCopy : void 0);
59154
+ print({
59155
+ ok: false,
59156
+ status: r.status,
59157
+ app,
59158
+ key: key2,
59159
+ body: r.json ?? r.text,
59160
+ ...removed !== void 0 ? { removed } : {},
59161
+ ...r.status >= 500 ? {
59162
+ // The same two fields the success path prints, for the same reason: presence
59163
+ // alone would call a 500 that left the OLD definition in place "served".
59164
+ servedNow: served2 === null ? null : !!now2,
59165
+ asRequested: served2 === null ? null : expected(now2),
59166
+ warning: GIT_FLAG_STORE_HALF_WRITE_NOTE,
59167
+ hint: `check what is actually served: himi flags list --app ${app} --target ${targetName()}`
59168
+ } : {}
59169
+ });
59170
+ return 1;
59171
+ }
59172
+ const served = await servedDefs();
59173
+ const now = served?.find((d) => d?.key === key2);
59174
+ const wanted = expected(now);
59175
+ print({
59176
+ ok: true,
59177
+ app,
59178
+ key: key2,
59179
+ ...r.json ?? {},
59180
+ // A plane too old to echo what it deleted still leaves the caller with the copy this
59181
+ // command read before writing — otherwise `rm` succeeds against it and prints nothing
59182
+ // to paste back, which is the whole reason the pre-read exists.
59183
+ ...undoCopy !== void 0 && r.json?.removed === void 0 ? { removed: undoCopy } : {},
59184
+ // TWO fields, because one boolean cannot carry both facts without meaning different
59185
+ // things per verb. `servedNow` is plain presence — is the door serving this key at
59186
+ // all. `asRequested` is the judgement: does what it serves match what was just asked
59187
+ // for (for `rm`, that the key is gone). A replaced definition the plane kept serving
59188
+ // unchanged is servedNow:true / asRequested:false, which is the case a presence-only
59189
+ // check reported as success.
59190
+ servedNow: served === null ? null : !!now,
59191
+ asRequested: served === null ? null : wanted,
59192
+ ...served === null ? { note: "could not read the device door to confirm; the write was accepted" } : wanted ? {} : { warning: GIT_FLAG_STORE_NOTE, hint: `commit apps/${app}/flags.json instead` }
59193
+ });
59194
+ return 0;
59195
+ }
59196
+ }
58954
59197
  case "visibility": {
58955
59198
  const value = pos[1];
58956
59199
  if (value !== void 0 && value !== "public" && value !== "unlisted") {
@@ -60234,6 +60477,8 @@ if (isMain) {
60234
60477
  }
60235
60478
  export {
60236
60479
  ConfigLoadError,
60480
+ GIT_FLAG_STORE_HALF_WRITE_NOTE,
60481
+ GIT_FLAG_STORE_NOTE,
60237
60482
  PLACEHOLDER_CLIENT_ID,
60238
60483
  PLACEHOLDER_SITE_URL,
60239
60484
  PREVIEW_TOKEN_NOTE,
@@ -60286,6 +60531,7 @@ export {
60286
60531
  servePlaneAnswers,
60287
60532
  serveUnreachableError,
60288
60533
  serveUnreachableHint,
60534
+ servesAll,
60289
60535
  switchFlagErrors,
60290
60536
  templatePreviewAuthError,
60291
60537
  tokenCoversApp,
@@ -88,12 +88,24 @@ published and promoted like any other. Withdrawing one, or killing it, is addres
88
88
  `himi init --template <name>` takes:
89
89
 
90
90
  ```
91
+ himi-serve template list --target bo --control-token "$TOK" # catalog + kill-state
92
+ himi-serve template list --revoked --target bo --control-token "$TOK" # only what is not fully live
91
93
  himi-serve template remove --name <template> --target bo --control-token "$TOK" # withdraw from the catalog
92
94
  himi-serve template revoke --name <template> --target bo --control-token "$TOK" # kill-switch its current release
93
95
  himi-serve template unrevoke --name <template> --target bo --control-token "$TOK" # lift it again
94
96
  ```
95
97
 
96
- All three also take `--app <appId>` instead of `--name`. That exists for one case: a template
98
+ **`list` is the read-back for the other three.** Nothing stores "this template is revoked" —
99
+ `revoke` kills the bundle integrities of the catalog's release — so `list` derives it, reading
100
+ each template's manifest and its app's `/v1/revocations` and comparing them. Each row reports
101
+ `revoked` as `none`, `partial`, `all`, or `unknown` with the reason a read failed; `partial` is
102
+ its own answer because `revoke` stops at the first failure, so a half-killed release is a state
103
+ the plane really produces. **Any `unknown` row makes the command exit 1** (and `ok:false`, with
104
+ an `unknown` count) — a script asking "is anything killed?" reads the exit code, and answering
105
+ 0 over rows it could not read would be the same silence-as-clean-bill this verb exists to stop. Before this verb existed neither the CLI nor the admin portal could
106
+ tell you which templates were currently killed.
107
+
108
+ The three WRITES also take `--app <appId>` instead of `--name`. That exists for one case: a template
97
109
  name may legally begin with `himi-template-` (the name pattern is lowercase alphanumerics and
98
110
  inner hyphens), and such a name publishes to a **doubly** prefixed app id — so `--name` cannot
99
111
  express it without becoming ambiguous. The CLI refuses the ambiguous form and points at `--app`. `--app` only accepts ids inside the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.926.0",
3
+ "version": "0.928.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": "3b5786b0529fb62a083ffdac2a31ee756590bef38c0e1316c78dc50f"
67
+ "falconPackageHash": "c7d75efc8f00421a7e0aea8773d9c1b0e968e2e7afcf8a4ae8bce93f"
68
68
  }