@wix/himalaya-cli 0.1115.0 → 0.1117.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
@@ -13140,7 +13140,6 @@ function resolveIdiom(tokens, ctx = {}) {
13140
13140
  var IDIOM_CONTEXTS, UNSAFE_KEYS;
13141
13141
  var init_idiom = __esm({
13142
13142
  "../../core/ts/src/idiom.ts"() {
13143
- "use strict";
13144
13143
  IDIOM_CONTEXTS = {
13145
13144
  ios: { native: ["ios"] },
13146
13145
  android: { native: ["material"] },
@@ -24719,7 +24718,6 @@ function shouldRevealError(args) {
24719
24718
  var EMAIL_RE, URL_HOST_RE, INTEGER_RE, NUMBER_RE;
24720
24719
  var init_validation = __esm({
24721
24720
  "../../core/ts/src/validation.ts"() {
24722
- "use strict";
24723
24721
  EMAIL_RE = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*\.[A-Za-z]{2,}$/;
24724
24722
  URL_HOST_RE = /^[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)*(:[0-9]+)?$/;
24725
24723
  INTEGER_RE = /^[+-]?[0-9]+$/;
@@ -29120,7 +29118,6 @@ function validateWorkerSubtree(raw, slotId, policy, actionCatalog) {
29120
29118
  var WORKER_SUBTREE_CEILINGS, ID_PART, ACTION_ID;
29121
29119
  var init_worker_subtree = __esm({
29122
29120
  "../../core/ts/src/worker-subtree.ts"() {
29123
- "use strict";
29124
29121
  init_worker_subtree_safe_types_generated();
29125
29122
  WORKER_SUBTREE_CEILINGS = Object.freeze({
29126
29123
  maxEncodedBytes: 65536,
@@ -36092,10 +36089,10 @@ var init_render_fonts = __esm({
36092
36089
 
36093
36090
  // ../../core/dev-server/src/wix-cache-policy.ts
36094
36091
  function clampRule(rule, denyPrefixes, maxTTL) {
36095
- const denied = denyPrefixes.some((p) => rule.pathPrefix.startsWith(p));
36092
+ const denied2 = denyPrefixes.some((p) => rule.pathPrefix.startsWith(p));
36096
36093
  return {
36097
36094
  ...rule,
36098
- cacheable: rule.cacheable && !denied,
36095
+ cacheable: rule.cacheable && !denied2,
36099
36096
  ttl: Math.min(rule.ttl, maxTTL)
36100
36097
  };
36101
36098
  }
@@ -53857,7 +53854,8 @@ var init_cli_bridge = __esm({
53857
53854
  });
53858
53855
 
53859
53856
  // src/mcp/file-tools.ts
53860
- import { lstatSync as lstatSync12, mkdirSync as mkdirSync26, readdirSync as readdirSync27, readFileSync as readFileSync59, realpathSync as realpathSync7, rmSync as rmSync12, statSync as statSync13, writeFileSync as writeFileSync21, existsSync as existsSync54 } from "node:fs";
53857
+ import { closeSync as closeSync4, constants, lstatSync as lstatSync12, mkdirSync as mkdirSync26, openSync as openSync3, readdirSync as readdirSync27, readFileSync as readFileSync59, realpathSync as realpathSync7, renameSync as renameSync6, rmSync as rmSync12, statSync as statSync13, writeSync, existsSync as existsSync54 } from "node:fs";
53858
+ import { randomBytes as randomBytes4 } from "node:crypto";
53861
53859
  import { dirname as dirname30, isAbsolute as isAbsolute5, join as join53, relative as relative11, resolve as resolve45, sep as sep14 } from "node:path";
53862
53860
  import * as z from "zod";
53863
53861
  function within(root, p) {
@@ -53874,37 +53872,99 @@ function realish(p) {
53874
53872
  }
53875
53873
  return join53(realpathSync7(cur), ...tail);
53876
53874
  }
53875
+ function symlinkComponent(base, abs) {
53876
+ let cur = base;
53877
+ for (const part of relative11(base, abs).split(sep14).filter(Boolean)) {
53878
+ cur = join53(cur, part);
53879
+ try {
53880
+ if (lstatSync12(cur).isSymbolicLink()) return relative11(base, cur);
53881
+ } catch {
53882
+ return void 0;
53883
+ }
53884
+ }
53885
+ return void 0;
53886
+ }
53877
53887
  async function confinedAppDir(args, deps) {
53878
53888
  const root = resolve45(await deps.defaultDir());
53879
53889
  const dir = typeof args.dir === "string" && args.dir ? resolve45(root, args.dir) : root;
53880
53890
  if (!existsSync54(root)) return refuse4("no_workspace", `the workspace folder ${root} does not exist`);
53881
- if (!within(realpathSync7(root), realish(dir))) return refuse4("outside_workspace", `"${String(args.dir)}" is outside the workspace folder`);
53891
+ if (!within(root, dir) || !within(realpathSync7(root), realish(dir))) return refuse4("outside_workspace", `"${String(args.dir)}" is outside the workspace folder`);
53892
+ const link = symlinkComponent(root, dir);
53893
+ if (link) return refuse4("outside_workspace", `"${link}" is a symlink \u2014 the app folder must be a real folder in the workspace`);
53894
+ const rel = relative11(root, dir);
53895
+ if (rel && (isDeniedSourcePath(`${rel}/x`) || isDeniedSourcePath(`${rel.toLowerCase()}/x`))) return refuse4("denied", `"${rel}" is a folder this tool does not touch`);
53896
+ if (!existsSync54(join53(dir, CONTENT_MARKER))) {
53897
+ return refuse4("not_an_app", `"${rel || "."}" is not an app folder (no ${CONTENT_MARKER}) \u2014 see himi_list_apps, or create one with himi_init`);
53898
+ }
53882
53899
  return dir;
53883
53900
  }
53901
+ function onDiskSpelling(abs) {
53902
+ let cur = abs;
53903
+ const tail = [];
53904
+ while (!existsSync54(cur)) {
53905
+ const up = dirname30(cur);
53906
+ if (up === cur) break;
53907
+ tail.unshift(cur.slice(up.length + (up.endsWith(sep14) ? 0 : 1)));
53908
+ cur = up;
53909
+ }
53910
+ return join53(realpathSync7.native(cur), ...tail);
53911
+ }
53912
+ function denied(appDir3, abs) {
53913
+ const rel = relative11(appDir3, abs);
53914
+ if (isDeniedSourcePath(rel) || isDeniedSourcePath(rel.toLowerCase())) return true;
53915
+ try {
53916
+ return isDeniedSourcePath(relative11(realpathSync7.native(appDir3), onDiskSpelling(abs)));
53917
+ } catch {
53918
+ return true;
53919
+ }
53920
+ }
53884
53921
  function confinedPath(appDir3, rel) {
53885
53922
  if (typeof rel !== "string" || !rel.trim()) return { error: "a path is required" };
53886
53923
  if (rel.includes("\0")) return { error: "a path may not contain NUL" };
53887
53924
  if (isAbsolute5(rel)) return { error: `"${rel}" is absolute \u2014 give a path inside the app folder` };
53888
53925
  const abs = resolve45(appDir3, rel);
53926
+ if (!within(appDir3, abs) || abs === appDir3) return { error: `"${rel}" is outside the app folder` };
53889
53927
  const real = realish(abs);
53890
53928
  const base = existsSync54(appDir3) ? realpathSync7(appDir3) : realish(appDir3);
53891
53929
  if (!within(base, real) || real === base) return { error: `"${rel}" is outside the app folder` };
53892
- const first = relative11(base, real).split(sep14)[0];
53893
- if (RESERVED.has(first)) return { error: `"${rel}" is inside ${first}/, which this tool does not touch` };
53930
+ const link = symlinkComponent(appDir3, abs);
53931
+ if (link) return { error: `"${link}" is a symlink, which this tool does not follow` };
53932
+ if (denied(appDir3, abs)) return { error: `"${rel}" is on the deny-list (secrets, credentials, build output), which this tool does not touch` };
53894
53933
  return abs;
53895
53934
  }
53935
+ function writeNoFollow(appDir3, abs, content) {
53936
+ mkdirSync26(dirname30(abs), { recursive: true });
53937
+ const before = symlinkComponent(appDir3, abs);
53938
+ if (before) throw new Error(`"${before}" became a symlink`);
53939
+ const tmp = join53(dirname30(abs), `.himi-write-${process.pid}-${randomBytes4(6).toString("hex")}.tmp`);
53940
+ const fd = openSync3(tmp, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | (constants.O_NOFOLLOW ?? 0), 420);
53941
+ try {
53942
+ writeSync(fd, content);
53943
+ } finally {
53944
+ closeSync4(fd);
53945
+ }
53946
+ const link = symlinkComponent(appDir3, abs);
53947
+ if (link || !within(realpathSync7(appDir3), realpathSync7(dirname30(abs)))) {
53948
+ rmSync12(tmp, { force: true });
53949
+ throw new Error(`"${link ?? relative11(appDir3, dirname30(abs))}" changed into a link while writing`);
53950
+ }
53951
+ renameSync6(tmp, abs);
53952
+ }
53896
53953
  function listFiles(appDir3) {
53897
53954
  const files = [];
53898
53955
  const walk2 = (d) => {
53956
+ const parentRel = relative11(appDir3, d);
53899
53957
  for (const e of readdirSync27(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
53900
- if (RESERVED.has(e.name)) continue;
53901
53958
  if (e.isSymbolicLink()) continue;
53902
53959
  const p = join53(d, e.name);
53903
53960
  if (e.isDirectory()) {
53961
+ if (isDeniedSourceDir(e.name, parentRel) || isDeniedSourceDir(e.name.toLowerCase(), parentRel)) continue;
53904
53962
  if (!walk2(p)) return false;
53905
53963
  } else if (e.isFile()) {
53964
+ const rel = relative11(appDir3, p);
53965
+ if (isDeniedSourcePath(rel) || isDeniedSourcePath(rel.toLowerCase())) continue;
53906
53966
  if (files.length >= MAX_LISTED) return false;
53907
- files.push({ path: relative11(appDir3, p).split(sep14).join("/"), bytes: statSync13(p).size });
53967
+ files.push({ path: rel.split(sep14).join("/"), bytes: statSync13(p).size });
53908
53968
  }
53909
53969
  }
53910
53970
  return true;
@@ -53934,7 +53994,7 @@ function fileTools() {
53934
53994
  {
53935
53995
  name: "himi_list_files",
53936
53996
  title: "List app files",
53937
- description: `Every file in an app folder with its size (skips ${[...RESERVED].join(", ")}; at most ${MAX_LISTED}).`,
53997
+ description: `Every file in an app folder with its size \u2014 at most ${MAX_LISTED}, skipping symlinks and what the source deny-list keeps off the machine (secrets, credentials, build output).`,
53938
53998
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
53939
53999
  inputSchema: z.object({ dir: DIR }),
53940
54000
  handler: async (a, d) => {
@@ -53983,14 +54043,20 @@ function fileTools() {
53983
54043
  else if (typeof f.content !== "string") refused2.push({ path: f.path, error: "content must be text" });
53984
54044
  else if (Buffer.byteLength(f.content) > MAX_FILE_BYTES) refused2.push({ path: f.path, error: `over the ${MAX_FILE_BYTES}-byte limit` });
53985
54045
  else if (existsSync54(p) && !lstatSync12(p).isFile()) refused2.push({ path: f.path, error: "not a regular file" });
54046
+ else if (planned.some((q) => q.abs === p)) refused2.push({ path: f.path, error: "the same file twice in one call" });
53986
54047
  else planned.push({ abs: p, rel: String(f.path), content: f.content });
53987
54048
  }
53988
54049
  if (refused2.length) return { ...ok({ ok: false, error: "refused", written: [], refused: refused2 }), isError: true };
54050
+ const written = [];
53989
54051
  for (const f of planned) {
53990
- mkdirSync26(dirname30(f.abs), { recursive: true });
53991
- writeFileSync21(f.abs, f.content);
54052
+ try {
54053
+ writeNoFollow(app, f.abs, f.content);
54054
+ written.push(f.rel);
54055
+ } catch (e) {
54056
+ return { ...ok({ ok: false, error: "write_failed", message: e.message, written }), isError: true };
54057
+ }
53992
54058
  }
53993
- return ok({ ok: true, dir: app, written: planned.map((f) => f.rel) });
54059
+ return ok({ ok: true, dir: app, written });
53994
54060
  }
53995
54061
  },
53996
54062
  {
@@ -54004,28 +54070,32 @@ function fileTools() {
54004
54070
  if (typeof app !== "string") return app;
54005
54071
  const paths = Array.isArray(a.paths) ? a.paths : [];
54006
54072
  const planned = [];
54073
+ const deleted = [];
54007
54074
  const refused2 = [];
54008
54075
  for (const rel of paths) {
54009
54076
  const p = confinedPath(app, rel);
54010
54077
  if (typeof p !== "string") refused2.push({ path: rel, error: p.error });
54011
54078
  else if (!existsSync54(p)) refused2.push({ path: rel, error: "no such file" });
54012
54079
  else if (!lstatSync12(p).isFile()) refused2.push({ path: rel, error: "not a regular file" });
54013
- else planned.push(p);
54080
+ else if (!planned.includes(p)) {
54081
+ planned.push(p);
54082
+ deleted.push(rel);
54083
+ }
54014
54084
  }
54015
54085
  if (refused2.length) return { ...ok({ ok: false, error: "refused", deleted: [], refused: refused2 }), isError: true };
54016
54086
  for (const p of planned) rmSync12(p);
54017
- return ok({ ok: true, dir: app, deleted: paths });
54087
+ return ok({ ok: true, dir: app, deleted });
54018
54088
  }
54019
54089
  }
54020
54090
  ];
54021
54091
  }
54022
- var MAX_FILE_BYTES, MAX_FILES_PER_WRITE, MAX_LISTED, RESERVED, CONTENT_MARKER, ok, refuse4, DIR, WRITE_ANN;
54092
+ var MAX_FILE_BYTES, MAX_FILES_PER_WRITE, MAX_LISTED, CONTENT_MARKER, ok, refuse4, DIR, WRITE_ANN;
54023
54093
  var init_file_tools = __esm({
54024
54094
  "src/mcp/file-tools.ts"() {
54095
+ init_source_deny();
54025
54096
  MAX_FILE_BYTES = 1024 * 1024;
54026
54097
  MAX_FILES_PER_WRITE = 100;
54027
54098
  MAX_LISTED = 2e3;
54028
- RESERVED = /* @__PURE__ */ new Set([".git", "node_modules", "dist"]);
54029
54099
  CONTENT_MARKER = "himalaya.content.json";
54030
54100
  ok = (o) => ({ content: [{ type: "text", text: JSON.stringify(o, null, 2) }] });
54031
54101
  refuse4 = (error, message) => ({ content: [{ type: "text", text: JSON.stringify({ ok: false, error, message }, null, 2) }], isError: true });
@@ -54536,7 +54606,7 @@ __export(native_decode_exports, {
54536
54606
  runNativeDecode: () => runNativeDecode
54537
54607
  });
54538
54608
  import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
54539
- import { existsSync as existsSync55, mkdtempSync as mkdtempSync2, readFileSync as readFileSync61, rmSync as rmSync13, writeFileSync as writeFileSync22 } from "node:fs";
54609
+ import { existsSync as existsSync55, mkdtempSync as mkdtempSync2, readFileSync as readFileSync61, rmSync as rmSync13, writeFileSync as writeFileSync21 } from "node:fs";
54540
54610
  import { tmpdir as tmpdir3 } from "node:os";
54541
54611
  import { join as join54 } from "node:path";
54542
54612
  import { fileURLToPath as fileURLToPath22 } from "node:url";
@@ -54670,7 +54740,7 @@ function runNativeDecode(options) {
54670
54740
  if (typeof descriptor.descriptorJson !== "string") {
54671
54741
  throw new Error(`native decode cannot judge ${descriptor.screenId}: the release builder did not return its exact baked JSON bytes`);
54672
54742
  }
54673
- writeFileSync22(join54(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
54743
+ writeFileSync21(join54(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
54674
54744
  }
54675
54745
  options.progress?.("building cached Swift oracle");
54676
54746
  execFileSync5("swift", ["build", "--package-path", options.packageDir, "--product", "himi-decode-oracle"], {
@@ -54708,7 +54778,7 @@ function runAndroidNativeDecode(options) {
54708
54778
  if (typeof descriptor.descriptorJson !== "string") {
54709
54779
  throw new Error(`native decode cannot judge ${descriptor.screenId}: the release builder did not return its exact baked JSON bytes`);
54710
54780
  }
54711
- writeFileSync22(join54(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
54781
+ writeFileSync21(join54(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
54712
54782
  }
54713
54783
  options.progress?.(`running real Kotlin decoder over ${options.descriptors.length} screen${options.descriptors.length === 1 ? "" : "s"}`);
54714
54784
  execFileSync5(options.gradlew, [
@@ -54792,7 +54862,7 @@ __export(browser_authoring_exports, {
54792
54862
  sdkSubpathReader: () => sdkSubpathReader
54793
54863
  });
54794
54864
  import { createServer as createServer5 } from "node:http";
54795
- import { createHash as createHash29, randomBytes as randomBytes4 } from "node:crypto";
54865
+ import { createHash as createHash29, randomBytes as randomBytes5 } from "node:crypto";
54796
54866
  import { existsSync as existsSync56, readdirSync as readdirSync28, readFileSync as readFileSync62, realpathSync as realpathSync8, statSync as statSync14 } from "node:fs";
54797
54867
  import { join as join55, posix as posix2, relative as relative12, sep as sep15 } from "node:path";
54798
54868
  function humanMs(ms) {
@@ -54890,7 +54960,7 @@ function sdkSubpathReader(resolve54) {
54890
54960
  function encodeJob(job) {
54891
54961
  return Buffer.from(JSON.stringify(job), "utf8").toString("base64");
54892
54962
  }
54893
- function mintToken2(payload, nonce = randomBytes4(16).toString("hex")) {
54963
+ function mintToken2(payload, nonce = randomBytes5(16).toString("hex")) {
54894
54964
  const digest2 = createHash29("sha256").update(payload, "utf8").digest("base64url");
54895
54965
  return `${nonce}.${digest2}`;
54896
54966
  }
@@ -55053,7 +55123,7 @@ __export(coverage_exports, {
55053
55123
  suppressedNativeKeys: () => suppressedNativeKeys,
55054
55124
  trendAndStore: () => trendAndStore
55055
55125
  });
55056
- import { existsSync as existsSync57, mkdirSync as mkdirSync27, readFileSync as readFileSync63, writeFileSync as writeFileSync23 } from "node:fs";
55126
+ import { existsSync as existsSync57, mkdirSync as mkdirSync27, readFileSync as readFileSync63, writeFileSync as writeFileSync22 } from "node:fs";
55057
55127
  import { dirname as dirname31, join as join56 } from "node:path";
55058
55128
  function suppressedNativeKeys(oracle) {
55059
55129
  return [...oracle.ios ? [] : ["native-decode|ios"], ...oracle.android ? [] : ["native-decode|android"]];
@@ -55247,7 +55317,7 @@ function trendAndStore(dir, entries, suppressed) {
55247
55317
  const stored = [.../* @__PURE__ */ new Set([...current, ...(previous ?? []).filter((key2) => (suppressed ?? []).includes(key2))])];
55248
55318
  try {
55249
55319
  mkdirSync27(dirname31(p), { recursive: true });
55250
- writeFileSync23(p, JSON.stringify({ keys: stored, ts: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
55320
+ writeFileSync22(p, JSON.stringify({ keys: stored, ts: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
55251
55321
  } catch {
55252
55322
  return null;
55253
55323
  }
@@ -55292,7 +55362,7 @@ function diffAndStoreObservations(dir, current) {
55292
55362
  }
55293
55363
  try {
55294
55364
  mkdirSync27(dirname31(p), { recursive: true });
55295
- writeFileSync23(p, JSON.stringify({ screens: current, ts: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
55365
+ writeFileSync22(p, JSON.stringify({ screens: current, ts: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
55296
55366
  } catch {
55297
55367
  return null;
55298
55368
  }
@@ -55903,8 +55973,8 @@ ${releaseId}`;
55903
55973
  const pin = resolvePin(request, url);
55904
55974
  let bytes2 = null;
55905
55975
  if (pin) {
55906
- const denied = await authorizePinnedRead(request, url, app, pin);
55907
- if (denied) return { ok: false, response: denied };
55976
+ const denied2 = await authorizePinnedRead(request, url, app, pin);
55977
+ if (denied2) return { ok: false, response: denied2 };
55908
55978
  const m = await pinnedManifest(app, pin);
55909
55979
  if (!m) return { ok: false, response: json4(404, { error: "release_not_found", app, releaseId: pin }) };
55910
55980
  bytes2 = m.appConfigHash ? await pinnedBody(app, m.appConfigHash, () => store.getAppConfig(app)) : null;
@@ -55927,7 +55997,7 @@ ${releaseId}`;
55927
55997
  return new Response(body, { status: 200, headers: { "content-type": JSON_CT2, etag, "cache-control": cache, ...vary } });
55928
55998
  }
55929
55999
  async function authorizePinnedRead(request, url, app, releaseId, screenId) {
55930
- const denied = await checkPinnedRead({
56000
+ const denied2 = await checkPinnedRead({
55931
56001
  secret: opts.previewTokenSecret,
55932
56002
  token: resolvePreviewToken(request, url),
55933
56003
  app,
@@ -55937,7 +56007,7 @@ ${releaseId}`;
55937
56007
  authorizePreview: opts.authorizePreview,
55938
56008
  authorizeFollow: opts.authorizeFollow
55939
56009
  });
55940
- return denied && json4(denied.status, denied.body);
56010
+ return denied2 && json4(denied2.status, denied2.body);
55941
56011
  }
55942
56012
  async function honourRingPin(request) {
55943
56013
  const kept = { request, dropped: false };
@@ -56388,8 +56458,8 @@ ${releaseId}`;
56388
56458
  const app = cfgMatch?.[1] ?? resolveApp(request, url);
56389
56459
  const pin = resolvePin(request, url);
56390
56460
  if (app && pin) {
56391
- const denied = await authorizePinnedRead(request, url, app, pin);
56392
- if (denied) return denied;
56461
+ const denied2 = await authorizePinnedRead(request, url, app, pin);
56462
+ if (denied2) return denied2;
56393
56463
  }
56394
56464
  if (path2 === "/v1/template-source") {
56395
56465
  if (!app) return json4(400, { error: "missing_app" });
@@ -56472,8 +56542,8 @@ ${releaseId}`;
56472
56542
  if (!app) return json4(400, { error: "missing_app" });
56473
56543
  let m;
56474
56544
  if (pin) {
56475
- const denied = await authorizePinnedRead(request, url, app, pin);
56476
- if (denied) return denied;
56545
+ const denied2 = await authorizePinnedRead(request, url, app, pin);
56546
+ if (denied2) return denied2;
56477
56547
  m = await pinnedManifest(app, pin);
56478
56548
  if (!m) return json4(404, { error: "release_not_found", app, releaseId: pin });
56479
56549
  } else {
@@ -56659,8 +56729,8 @@ ${releaseId}`;
56659
56729
  const screenId = screenMatch[1];
56660
56730
  if (!app || !isSafeSegment(screenId)) return json4(404, { error: "screen_not_found", screenId });
56661
56731
  if (pin) {
56662
- const denied = await authorizePinnedRead(request, url, app, pin, screenId);
56663
- if (denied) return denied;
56732
+ const denied2 = await authorizePinnedRead(request, url, app, pin, screenId);
56733
+ if (denied2) return denied2;
56664
56734
  const m2 = await pinnedManifest(app, pin);
56665
56735
  if (!m2) return json4(404, { error: "release_not_found", app, releaseId: pin });
56666
56736
  const meta2 = m2.screens[screenId];
@@ -57453,7 +57523,7 @@ __export(run_watch_exports, {
57453
57523
  watchedContentDir: () => watchedContentDir,
57454
57524
  writeRunRecord: () => writeRunRecord
57455
57525
  });
57456
- import { closeSync as closeSync4, constants, existsSync as existsSync60, fstatSync, lstatSync as lstatSync13, statSync as statSync16, mkdirSync as mkdirSync29, openSync as openSync3, readFileSync as readFileSync67, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync15, writeFileSync as writeFileSync24, watch as fsWatch } from "node:fs";
57526
+ import { closeSync as closeSync5, constants as constants2, existsSync as existsSync60, fstatSync, lstatSync as lstatSync13, statSync as statSync16, mkdirSync as mkdirSync29, openSync as openSync4, readFileSync as readFileSync67, realpathSync as realpathSync9, renameSync as renameSync7, rmSync as rmSync15, writeFileSync as writeFileSync23, watch as fsWatch } from "node:fs";
57457
57527
  import { randomUUID as randomUUID4 } from "node:crypto";
57458
57528
  import { extname as extname4, join as join59, resolve as resolve47, sep as sep16 } from "node:path";
57459
57529
  function diffReleases(prev, next) {
@@ -57588,9 +57658,9 @@ function writeRunRecord(dir, record2) {
57588
57658
  if (!root) return false;
57589
57659
  const tmp = join59(root, `.run.json.${process.pid}.${randomUUID4()}.tmp`);
57590
57660
  try {
57591
- writeFileSync24(tmp, `${JSON.stringify(record2, null, 2)}
57661
+ writeFileSync23(tmp, `${JSON.stringify(record2, null, 2)}
57592
57662
  `, { flag: "wx" });
57593
- renameSync6(tmp, join59(root, "run.json"));
57663
+ renameSync7(tmp, join59(root, "run.json"));
57594
57664
  } catch (e) {
57595
57665
  rmSync15(tmp, { force: true });
57596
57666
  throw e;
@@ -57647,12 +57717,12 @@ function readConfined(root, rel) {
57647
57717
  }
57648
57718
  const canonicalRoot = realpathSync9(root);
57649
57719
  if (!realpathSync9(at2).startsWith(canonicalRoot + sep16)) return null;
57650
- const fd = openSync3(at2, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
57720
+ const fd = openSync4(at2, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
57651
57721
  try {
57652
57722
  if (!fstatSync(fd).isFile()) return null;
57653
57723
  return readFileSync67(fd);
57654
57724
  } finally {
57655
- closeSync4(fd);
57725
+ closeSync5(fd);
57656
57726
  }
57657
57727
  } catch {
57658
57728
  return null;
@@ -58874,7 +58944,7 @@ __export(version_exports, {
58874
58944
  setAndroidVersion: () => setAndroidVersion,
58875
58945
  setIosVersion: () => setIosVersion
58876
58946
  });
58877
- import { readFileSync as readFileSync72, writeFileSync as writeFileSync25 } from "node:fs";
58947
+ import { readFileSync as readFileSync72, writeFileSync as writeFileSync24 } from "node:fs";
58878
58948
  function readIosVersion(projectYml) {
58879
58949
  const marketing = matchScalar(projectYml, "CFBundleShortVersionString");
58880
58950
  const build2 = matchScalar(projectYml, "CFBundleVersion");
@@ -58939,8 +59009,8 @@ function bumpVersion(app, opts) {
58939
59009
  marketing: opts.marketing ?? current.marketing,
58940
59010
  build: opts.build === "increment" || opts.build === void 0 ? current.build + (opts.build === "increment" ? 1 : 0) : opts.build
58941
59011
  };
58942
- writeFileSync25(iosPath, setIosVersion(iosYml, next));
58943
- writeFileSync25(gradlePath, setAndroidVersion(gradle, next));
59012
+ writeFileSync24(iosPath, setIosVersion(iosYml, next));
59013
+ writeFileSync24(gradlePath, setAndroidVersion(gradle, next));
58944
59014
  return next;
58945
59015
  }
58946
59016
  var init_version = __esm({
@@ -59083,7 +59153,7 @@ var init_asc_status = __esm({
59083
59153
  });
59084
59154
 
59085
59155
  // ../../core/functions-secrets/src/vault.ts
59086
- import { createCipheriv, createDecipheriv, randomBytes as randomBytes5 } from "node:crypto";
59156
+ import { createCipheriv, createDecipheriv, randomBytes as randomBytes6 } from "node:crypto";
59087
59157
  import { mkdir, readFile as readFile2, rename, writeFile as writeFile2 } from "node:fs/promises";
59088
59158
  import { dirname as dirname34 } from "node:path";
59089
59159
  function createJsonSecretStore(filePath) {
@@ -59141,7 +59211,7 @@ function createSecretVault(options) {
59141
59211
  environment: input.environment,
59142
59212
  name: input.name,
59143
59213
  version: input.version,
59144
- iv: randomBytes5(12).toString("base64url"),
59214
+ iv: randomBytes6(12).toString("base64url"),
59145
59215
  ciphertext: "",
59146
59216
  authTag: ""
59147
59217
  };
@@ -59503,7 +59573,7 @@ __export(notify_exports, {
59503
59573
  runNotify: () => runNotify,
59504
59574
  saveAdminSession: () => saveAdminSession
59505
59575
  });
59506
- import { chmodSync as chmodSync3, mkdirSync as mkdirSync30, readFileSync as readFileSync76, writeFileSync as writeFileSync26 } from "node:fs";
59576
+ import { chmodSync as chmodSync3, mkdirSync as mkdirSync30, readFileSync as readFileSync76, writeFileSync as writeFileSync25 } from "node:fs";
59507
59577
  import { homedir as homedir6 } from "node:os";
59508
59578
  import { dirname as dirname35, join as join62 } from "node:path";
59509
59579
  function defaultSessionsPath() {
@@ -59520,7 +59590,7 @@ function saveAdminSession(adminUrl, session, path2 = defaultSessionsPath()) {
59520
59590
  const file = loadSessionsFile(path2);
59521
59591
  file.sessions = { ...file.sessions, [adminUrl]: session };
59522
59592
  mkdirSync30(dirname35(path2), { recursive: true, mode: 448 });
59523
- writeFileSync26(path2, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
59593
+ writeFileSync25(path2, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
59524
59594
  try {
59525
59595
  chmodSync3(dirname35(path2), 448);
59526
59596
  chmodSync3(path2, 384);
@@ -63279,7 +63349,7 @@ var init_brand_font = __esm({
63279
63349
  });
63280
63350
 
63281
63351
  // src/site-analyze/write-artifacts.ts
63282
- import { mkdirSync as mkdirSync31, writeFileSync as writeFileSync27 } from "node:fs";
63352
+ import { mkdirSync as mkdirSync31, writeFileSync as writeFileSync26 } from "node:fs";
63283
63353
  import { join as join63 } from "node:path";
63284
63354
  function attachVeloSources(book, sources, put) {
63285
63355
  const byPath = new Map((sources ?? []).map((s) => [s.path, s.text]));
@@ -63441,7 +63511,7 @@ async function writeBusinessBook(result, opts) {
63441
63511
  const put = (rel, body) => {
63442
63512
  const p = join63(out2, rel);
63443
63513
  mkdirSync31(join63(p, ".."), { recursive: true });
63444
- writeFileSync27(p, body);
63514
+ writeFileSync26(p, body);
63445
63515
  files.push(rel);
63446
63516
  };
63447
63517
  const book = result.book;
@@ -63534,7 +63604,7 @@ var init_customer_build = __esm({
63534
63604
  });
63535
63605
 
63536
63606
  // src/site-analyze/apply.ts
63537
- import { existsSync as existsSync65, readFileSync as readFileSync77, writeFileSync as writeFileSync28, copyFileSync as copyFileSync3, mkdirSync as mkdirSync32, realpathSync as realpathSync10, statSync as statSync18 } from "node:fs";
63607
+ import { existsSync as existsSync65, readFileSync as readFileSync77, writeFileSync as writeFileSync27, copyFileSync as copyFileSync3, mkdirSync as mkdirSync32, realpathSync as realpathSync10, statSync as statSync18 } from "node:fs";
63538
63608
  import { dirname as dirname36, isAbsolute as isAbsolute7, join as join64, resolve as resolve51, sep as sep17 } from "node:path";
63539
63609
  import { spawnSync as spawnSync4 } from "node:child_process";
63540
63610
  function unpairedRedirectNote(overlay) {
@@ -63686,10 +63756,10 @@ function buildMergedContract(options) {
63686
63756
  const body = `${JSON.stringify(merged, null, 2)}
63687
63757
  `;
63688
63758
  const path2 = join64(packageDir, "provisioning.contract.json");
63689
- writeFileSync28(path2, body);
63759
+ writeFileSync27(path2, body);
63690
63760
  const localPath = join64(appDir3, LOCAL_CONTRACT_REL);
63691
63761
  mkdirSync32(dirname36(localPath), { recursive: true });
63692
- writeFileSync28(localPath, body);
63762
+ writeFileSync27(localPath, body);
63693
63763
  staged.push(localPath);
63694
63764
  return { path: path2, staged };
63695
63765
  }
@@ -63788,10 +63858,10 @@ async function applyOverlays(input) {
63788
63858
  const cur = JSON.parse(readFileSync77(pkgProv, "utf8"));
63789
63859
  const merged = deepMerge4(cur, overlay);
63790
63860
  packageFaq = merged.faq;
63791
- writeFileSync28(pkgProv, JSON.stringify(merged, null, 2) + "\n");
63861
+ writeFileSync27(pkgProv, JSON.stringify(merged, null, 2) + "\n");
63792
63862
  wrote.push(pkgProv);
63793
63863
  } else {
63794
- writeFileSync28(pkgProv, JSON.stringify(overlay, null, 2) + "\n");
63864
+ writeFileSync27(pkgProv, JSON.stringify(overlay, null, 2) + "\n");
63795
63865
  wrote.push(pkgProv);
63796
63866
  }
63797
63867
  const pkgTokens = join64(cwd, "tokens.json");
@@ -63817,7 +63887,7 @@ async function applyOverlays(input) {
63817
63887
  if (primary && glow && typeof glow.color === "string") glow.color = primary;
63818
63888
  stripBrandFromIdiom(merged);
63819
63889
  }
63820
- writeFileSync28(pkgTokens, JSON.stringify(merged, null, 2) + "\n");
63890
+ writeFileSync27(pkgTokens, JSON.stringify(merged, null, 2) + "\n");
63821
63891
  wrote.push(pkgTokens);
63822
63892
  }
63823
63893
  const homeAction = homeRefreshActionId(shell);
@@ -63847,7 +63917,7 @@ async function applyOverlays(input) {
63847
63917
  }
63848
63918
  }
63849
63919
  if (next !== prev) {
63850
- writeFileSync28(homeSrc, next);
63920
+ writeFileSync27(homeSrc, next);
63851
63921
  wrote.push(homeSrc);
63852
63922
  }
63853
63923
  if (patched.unparsed) {
@@ -68216,6 +68286,7 @@ __export(cli_exports, {
68216
68286
  APPLY_NEEDS_CLIENT: () => APPLY_NEEDS_CLIENT,
68217
68287
  REDIRECT_NEEDS_CLIENT: () => REDIRECT_NEEDS_CLIENT,
68218
68288
  SITE_FUNCTION_USAGE: () => SITE_FUNCTION_USAGE,
68289
+ applyNeedsClient: () => applyNeedsClient,
68219
68290
  assetFetchFor: () => assetFetchFor,
68220
68291
  noteWithWarnings: () => noteWithWarnings,
68221
68292
  ownerSiteFor: () => ownerSiteFor,
@@ -68224,6 +68295,10 @@ __export(cli_exports, {
68224
68295
  });
68225
68296
  import { readFileSync as readFileSync78 } from "node:fs";
68226
68297
  import { resolve as resolve52 } from "node:path";
68298
+ function applyNeedsClient(shell) {
68299
+ if (shell === void 0 || !SHELL_NAMES2.includes(shell)) return true;
68300
+ return !isSiteShell(shell);
68301
+ }
68227
68302
  function assetFetchFor(_opts) {
68228
68303
  return pinnedFetch2;
68229
68304
  }
@@ -68236,7 +68311,7 @@ function ownerSiteFor(opts, env = process.env) {
68236
68311
  async function runSiteAnalyze(opts) {
68237
68312
  if (opts.shell !== void 0) assertKnownShell(opts.shell);
68238
68313
  if (!opts.clientId?.trim()) {
68239
- if (opts.apply) throw Object.assign(new Error(APPLY_NEEDS_CLIENT), { exit: 2 });
68314
+ if (opts.apply && applyNeedsClient(opts.shell)) throw Object.assign(new Error(APPLY_NEEDS_CLIENT), { exit: 2 });
68240
68315
  if (opts.redirectUri?.trim()) throw Object.assign(new Error(REDIRECT_NEEDS_CLIENT), { exit: 2 });
68241
68316
  }
68242
68317
  const fetchImpl = opts.fetchImpl ?? fetch;
@@ -68271,7 +68346,10 @@ async function runSiteAnalyze(opts) {
68271
68346
  try {
68272
68347
  const derived = deriveSiteShell({ siteUrl: result.book.input.siteUrl, look: result.book.look });
68273
68348
  result.provisioningOverlay.site = derived.site;
68274
- result.warnings = [...result.warnings ?? [], ...derived.notes];
68349
+ const warnings2 = (result.warnings ?? []).map(
68350
+ (w) => !opts.clientId?.trim() && w.startsWith("no --client-id:") ? `no --client-id: fine for --shell ${opts.shell} \u2014 a site shell binds no client, so --apply works without one. Only the vertical probes were skipped, so the catalog has no counts.` : w
68351
+ );
68352
+ result.warnings = [...warnings2, ...derived.notes];
68275
68353
  } catch (e) {
68276
68354
  result.warnings = [
68277
68355
  ...result.warnings ?? [],
@@ -68595,7 +68673,7 @@ __export(site_create_exports, {
68595
68673
  runSiteList: () => runSiteList,
68596
68674
  writeProvisioningBinding: () => writeProvisioningBinding
68597
68675
  });
68598
- import { existsSync as existsSync66, readFileSync as readFileSync79, writeFileSync as writeFileSync29 } from "node:fs";
68676
+ import { existsSync as existsSync66, readFileSync as readFileSync79, writeFileSync as writeFileSync28 } from "node:fs";
68599
68677
  import { resolve as resolvePath2 } from "node:path";
68600
68678
  function oauthClientName(projectName) {
68601
68679
  return `${projectName} client`;
@@ -68744,7 +68822,7 @@ async function runSiteCreate(opts) {
68744
68822
  if (!wantOauthApp) return { ...base, nextSteps: nextSteps(metaSiteId) };
68745
68823
  return { ...base, clientId, redirectUri, nextSteps: nextSteps(metaSiteId, clientId) };
68746
68824
  }
68747
- function writeProvisioningBinding(target, binding2, fs = { existsSync: existsSync66, readFileSync: readFileSync79, writeFileSync: writeFileSync29 }) {
68825
+ function writeProvisioningBinding(target, binding2, fs = { existsSync: existsSync66, readFileSync: readFileSync79, writeFileSync: writeFileSync28 }) {
68748
68826
  const msid = binding2.siteId;
68749
68827
  const path2 = resolvePath2(target);
68750
68828
  const committed = committedShellProvisioning(path2);
@@ -69417,7 +69495,7 @@ var init_context = __esm({
69417
69495
  });
69418
69496
 
69419
69497
  // src/cli.ts
69420
- import { mkdtempSync as mkdtempSync4, readFileSync as readFileSync80, mkdirSync as mkdirSync33, writeFileSync as writeFileSync30, existsSync as existsSync67, rmSync as rmSync16, readdirSync as readdirSync31, statSync as statSync19, lstatSync as lstatSync14, accessSync, constants as constants2, renameSync as renameSync7, cpSync as cpSync3 } from "node:fs";
69498
+ import { mkdtempSync as mkdtempSync4, readFileSync as readFileSync80, mkdirSync as mkdirSync33, writeFileSync as writeFileSync29, existsSync as existsSync67, rmSync as rmSync16, readdirSync as readdirSync31, statSync as statSync19, lstatSync as lstatSync14, accessSync, constants as constants3, renameSync as renameSync8, cpSync as cpSync3 } from "node:fs";
69421
69499
  import { execFile, execFileSync as execFileSync7, spawnSync as spawnSync5, spawn as spawn7 } from "node:child_process";
69422
69500
  import { AsyncLocalStorage } from "node:async_hooks";
69423
69501
  import { createHash as createHash31, randomUUID as randomUUID7 } from "node:crypto";
@@ -71867,9 +71945,12 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
71867
71945
  file is not the plane's per-release archive.
71868
71946
  himi share --file <package.himi> [--expires-at <ISO>] | revoke --app <app> <shareId>
71869
71947
  publish a content-addressed package link for a released app
71870
- himi mcp [plugin --out <dir>] the himi tools as a local MCP server over stdio, for coding
71948
+ himi mcp [plugin --out <dir>] [--with-files]
71949
+ the himi tools as a local MCP server over stdio, for coding
71871
71950
  agents (Claude Code, Cursor, Codex, VS Code). \`himi init\`
71872
- configures each; see \`himi docs himi-mcp\`.
71951
+ configures each; see \`himi docs himi-mcp\`. --with-files
71952
+ adds file tools confined to the workspace, for a host that
71953
+ cannot edit files (Claude Desktop chat).
71873
71954
  himi run [<package.himi|dir|url>] [--browser] [--watch [--idle-exit <min>]] [--lan] [--port <n>] [--chrome preview|app] [--screen <id>] [--surface <id>] [--no-open]
71874
71955
  serve a package on localhost + open it in a browser. No serve
71875
71956
  plane, no dev-server. With no argument it packs the current
@@ -72116,7 +72197,8 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
72116
72197
  a brandable shell's content package. --url alone is the
72117
72198
  visitor-only pass (published pages, look, copy); --client-id
72118
72199
  adds the vertical catalog counts and the app binding, and
72119
- --apply requires it. --shell takes the same
72200
+ --apply requires it (not for a site shell, which binds no
72201
+ client). --shell takes the same
72120
72202
  names 'himi dist --shell' does; default branded-lite.
72121
72203
  A SITE shell (brandy) also derives the site's page map
72122
72204
  (tabs/drawer from its menu) into the overlay's \`site\`
@@ -72231,7 +72313,7 @@ var COMMANDS = [
72231
72313
  { name: "login", summary: "obtain a scoped serve-control token from Wix identity and store it in ~/.himalaya/serve-targets.json (so push/promote need no hand-pasted token). Three modes: --browser (your own Wix account, no VPN/BO), --wix (HEADLESS \u2014 the machine's own Wix CLI credential; the mode a CI pod or agent container can satisfy), --dpx (Wix employee, reads ~/.dpx/session/token). RFC 0026 WS3.", required: "[--browser|--wix|--dpx] [--target <t>] [--issue-url <url>]" },
72232
72314
  { name: "logout", summary: "remove the stored control token for a target", required: "[--target <t>]" },
72233
72315
  { name: "notify", summary: "`setup` optionally configures Branded/Ping from local customer key files; `status --refresh` reads live backend readiness. Setup may be deferred while building. These use serve-control and require owner plus Wix account authentication. `send`, `tokens`, `doctor`: 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. `send --provider ping` is the hosted alternative: the serve plane sends one test notification to one site member through Ping as Himalaya, with owner login instead of provider keys, and reports acceptance, never delivery. NOT `himi push`, which publishes a release.", required: "setup|status [--content <dir>] [--target <t>] [--refresh] [--yes] [--no-push-setup] | send --provider ping [--content <dir>] [--app <a>] [--site <msid>] --member-email <email> --title <s> [--body <s>] --link <url> [--request-id <uuid>] [--dry-run] [--json] | 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]" },
72234
- { 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 --read-only` returns API-only generation context as JSON (identity, access probes, CMS permissions and selected redacted Velo source); it needs no URL/browser and never uploads or applies anything. `analyze` reads a site into a business-book folder (REST + Playwright, GET-only); `--url` alone is the visitor-only pass over the published pages, and `--client-id` adds the vertical probes and the app binding `--apply` requires; `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. `--shell brandy` (the site shell, RFC 0062) also derives the site's page map from its published pages and header menu \u2014 Home first, up to four tabs for the pages a visitor returns to, the rest in the drawer \u2014 into the overlay's `site` block, so it can be edited before `--apply`; only a site shell ever receives that block. 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 --read-only [--site <msid>] [--client-id <id>] [--json] | analyze --url <https> [--client-id <id>] [--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]" },
72316
+ { 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 --read-only` returns API-only generation context as JSON (identity, access probes, CMS permissions and selected redacted Velo source); it needs no URL/browser and never uploads or applies anything. `analyze` reads a site into a business-book folder (REST + Playwright, GET-only); `--url` alone is the visitor-only pass over the published pages, and `--client-id` adds the vertical probes and the app binding `--apply` requires; `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 (a site shell needs no `--client-id`: it is visitor-only and binds no client); `--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. `--shell brandy` (the site shell, RFC 0062) also derives the site's page map from its published pages and header menu \u2014 Home first, up to four tabs for the pages a visitor returns to, the rest in the drawer \u2014 into the overlay's `site` block, so it can be edited before `--apply`; only a site shell ever receives that block. 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 --read-only [--site <msid>] [--client-id <id>] [--json] | analyze --url <https> [--client-id <id>] [--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]" },
72235
72317
  { 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>]" },
72236
72318
  { name: "upgrade", summary: "install the newest published CLI (`npm i -g @wix/himalaya-cli@latest`) and report what moved. This is the explicit door: himi also updates itself when the running build is more than a week old \u2014 unattended it just does it and re-runs your command, at a terminal it asks first \u2014 and `HIMI_NO_UPDATE_CHECK=1` turns that off, leaving this command as the way to catch up. Reports `unchanged` rather than claiming success when npm exits 0 without moving the version, which is what a registry mirror serving its own pinned copy looks like.", required: "[--json]" },
72237
72319
  { 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. A successful draft push also rebuilds the making-of (.himi/making-of.html, local and offline) and names it as `reel` \u2014 the file to hand the owner beside the preview link. Its postcard carries the home shot only when that shot is newer than the last source edit; otherwise the postcard says why and names `himi render <home> --png`, and the envelope's `postcardPicture: {skipped, render}` leads next[] with that render (the push never renders it itself). 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)" },
@@ -72469,7 +72551,7 @@ function readUpdateCache(path2) {
72469
72551
  function writeUpdateCache(path2, cache) {
72470
72552
  try {
72471
72553
  mkdirSync33(dirname37(path2), { recursive: true, mode: 448 });
72472
- writeFileSync30(path2, JSON.stringify(cache, null, 2) + "\n", { mode: 384 });
72554
+ writeFileSync29(path2, JSON.stringify(cache, null, 2) + "\n", { mode: 384 });
72473
72555
  } catch {
72474
72556
  }
72475
72557
  }
@@ -72500,7 +72582,7 @@ function globalPrefixWritable() {
72500
72582
  const root = globalNodeModulesRoot();
72501
72583
  if (!root) return false;
72502
72584
  try {
72503
- accessSync(root, constants2.W_OK);
72585
+ accessSync(root, constants3.W_OK);
72504
72586
  return true;
72505
72587
  } catch {
72506
72588
  return false;
@@ -72611,7 +72693,7 @@ function acquireUpdateLock(cachePath, now, hooks = {}) {
72611
72693
  };
72612
72694
  const tryCreate = (path2) => {
72613
72695
  try {
72614
- writeFileSync30(path2, mine, { flag: "wx", mode: 384 });
72696
+ writeFileSync29(path2, mine, { flag: "wx", mode: 384 });
72615
72697
  return true;
72616
72698
  } catch {
72617
72699
  return false;
@@ -73199,7 +73281,7 @@ function executableOnPath(name, env = process.env) {
73199
73281
  const candidate = join65(dir, `${name}${suffix}`);
73200
73282
  try {
73201
73283
  if (!statSync19(candidate).isFile()) continue;
73202
- if (process.platform !== "win32") accessSync(candidate, constants2.X_OK);
73284
+ if (process.platform !== "win32") accessSync(candidate, constants3.X_OK);
73203
73285
  return true;
73204
73286
  } catch {
73205
73287
  }
@@ -73326,7 +73408,7 @@ function readConfigSource(dir) {
73326
73408
  }
73327
73409
  function writePngFile(path2, base64) {
73328
73410
  mkdirSync33(dirname37(path2), { recursive: true });
73329
- writeFileSync30(path2, Buffer.from(base64, "base64"));
73411
+ writeFileSync29(path2, Buffer.from(base64, "base64"));
73330
73412
  return path2;
73331
73413
  }
73332
73414
  async function seedAgentFilesFor(dir, flags, mode = "init") {
@@ -74949,7 +75031,7 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
74949
75031
  const files = himiPluginFiles2(skills.skillMd);
74950
75032
  for (const f of files) {
74951
75033
  mkdirSync33(dirname37(join65(dir, f.path)), { recursive: true });
74952
- writeFileSync30(join65(dir, f.path), f.text);
75034
+ writeFileSync29(join65(dir, f.path), f.text);
74953
75035
  }
74954
75036
  print({ ok: true, dir, files: files.map((f) => f.path), next: [`claude plugin validate ${dir}`, `claude --plugin-dir ${dir}`] });
74955
75037
  return 0;
@@ -74975,7 +75057,7 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
74975
75057
  const out2 = resolve53(str8(flags.out) ?? join65(dir, ".himi", "making-of.html"));
74976
75058
  const { html, plan } = buildReel2(dir);
74977
75059
  mkdirSync33(dirname37(out2), { recursive: true });
74978
- writeFileSync30(out2, html);
75060
+ writeFileSync29(out2, html);
74979
75061
  const { pathToFileURL: pathToFileURL9 } = await import("node:url");
74980
75062
  const url2 = pathToFileURL9(out2).href;
74981
75063
  const open2 = flags["no-open"] !== true;
@@ -77195,9 +77277,9 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
77195
77277
  const { describeChanges: describeChanges2, headline: headline2 } = await Promise.resolve().then(() => (init_build_changes(), build_changes_exports));
77196
77278
  const changes = describeChanges2(live2, join65(next, running.app), diff);
77197
77279
  const retired = `${live2}.retired-${Date.now()}`;
77198
- renameSync7(live2, retired);
77280
+ renameSync8(live2, retired);
77199
77281
  try {
77200
- renameSync7(join65(next, running.app), live2);
77282
+ renameSync8(join65(next, running.app), live2);
77201
77283
  } catch {
77202
77284
  cpSync3(join65(next, running.app), live2, { recursive: true });
77203
77285
  }
@@ -79552,10 +79634,11 @@ ${report3.ok ? "OK \u2014 data + serve plane reachable." : typeof report3.hint =
79552
79634
  return 2;
79553
79635
  }
79554
79636
  const redirectFlag = str8(flags["redirect-uri"]);
79555
- if (!clientId && (flags.apply === true || redirectFlag)) {
79556
- const { APPLY_NEEDS_CLIENT: APPLY_NEEDS_CLIENT2, REDIRECT_NEEDS_CLIENT: REDIRECT_NEEDS_CLIENT2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
79637
+ const { APPLY_NEEDS_CLIENT: APPLY_NEEDS_CLIENT2, REDIRECT_NEEDS_CLIENT: REDIRECT_NEEDS_CLIENT2, applyNeedsClient: applyNeedsClient2 } = await Promise.resolve().then(() => (init_cli(), cli_exports));
79638
+ const applyWantsClient = flags.apply === true && applyNeedsClient2(str8(flags.shell));
79639
+ if (!clientId && (applyWantsClient || redirectFlag)) {
79557
79640
  io.error(JSON.stringify({
79558
- error: flags.apply === true ? APPLY_NEEDS_CLIENT2 : REDIRECT_NEEDS_CLIENT2,
79641
+ error: applyWantsClient ? APPLY_NEEDS_CLIENT2 : REDIRECT_NEEDS_CLIENT2,
79559
79642
  usage: analyzeUsage
79560
79643
  }));
79561
79644
  return 2;
@@ -83,8 +83,13 @@ Each tool acts on the agent's workspace folder unless it is given a `dir`.
83
83
  **File tools, for hosts that cannot edit files.** `himi mcp --with-files` adds `himi_list_apps`,
84
84
  `himi_list_files`, `himi_read_file`, `himi_write_files` and `himi_delete_files`. The Claude Desktop
85
85
  Extension uses them, because Claude Desktop chat has no file editing of its own. They follow the
86
- hosted connector's shapes, act only inside the workspace folder (checked on the real path, so a
87
- symlink cannot lead out of it), and never touch `.git`, `node_modules` or `dist`. When the host names
86
+ hosted connector's shapes and act only inside the workspace folder, on an app root (a folder with
87
+ `himalaya.content.json`). They refuse any path through a symlink, dangling ones included, and write
88
+ without following links. They judge the deny-list in every spelling, because macOS and Windows open
89
+ `.ENV` as `.env`. They never list, read,
90
+ write or delete anything the source deny-list keeps off the machine: `.env` files, the `.himi/`
91
+ secret store, MCP host configs with their keys, key files, and `node_modules`, `dist` and `.git`
92
+ at any depth. When the host names
88
93
  no workspace, `HIMI_WORKSPACE` sets the folder. Coding agents edit files themselves, so the flag is
89
94
  off by default.
90
95
 
@@ -161,11 +161,13 @@ The plane has no headless browser (`look.unavailable: no_browser_on_plane`). Own
161
161
  `himi-brandy`, the site shell, opens the customer's live website as native-framed tabs, so its contract is a `site` block: which website, and which of its pages are tabs, drawer rows or overflow rows. `--shell brandy` derives that block into `provisioning.overlay.json`:
162
162
 
163
163
  ```bash
164
- himi site analyze --url https://owner.wixsite.com/site --client-id <id> --shell brandy # writes site.pages
164
+ himi site analyze --url https://owner.wixsite.com/site --shell brandy # writes site.pages
165
165
  # edit site.pages in .himi/business-book/provisioning.overlay.json if the map needs it
166
- himi site analyze --url https://owner.wixsite.com/site --client-id <id> --shell brandy --apply
166
+ himi site analyze --url https://owner.wixsite.com/site --shell brandy --apply
167
167
  ```
168
168
 
169
+ No `--client-id` is needed here: a site shell is visitor-only, its pages are the site itself, and its contract binds no Headless client. For an off-repo brandy app (`himi init --template brandy`), run the `--apply` inside the app folder: it writes the derived `site` block into the package's own `provisioning.json`, which the template's `config.ts` builds every tab and screen from.
170
+
169
171
  The map comes from what the analysis already reads without owner credentials, because Wix has no public menu API: the published page list (`look.sitePages`), the header menu the crawl saw (`look.navLinks`, label and href), and each page's role.
170
172
 
171
173
  - **Order and labels** follow the site's own menu. Links to other hosts, and repeat links to the same page, are dropped.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wix/himalaya-cli",
3
- "version": "0.1115.0",
3
+ "version": "0.1117.0",
4
4
  "type": "module",
5
5
  "description": "himi — the Himalaya authoring CLI. Build a Tier-5 content package on your own machine, push it to the production serve as a draft, preview it on web + Himi Player, and read worker logs back — the remote dev loop. Wraps @himalaya/serve-cli for transport.",
6
6
  "keywords": [
@@ -67,5 +67,5 @@
67
67
  "artifactId": "himalaya-cli"
68
68
  }
69
69
  },
70
- "falconPackageHash": "19c38309340f59147c2cb6f97746d920feacc058a4c256331d9f4422"
70
+ "falconPackageHash": "fc5b0cacba000b4c6e6a4a5002e19ab4d37d0560e6bd98aed8b49b13"
71
71
  }
Binary file
@@ -0,0 +1,60 @@
1
+ # __HIMI_APP_NAME__ — your website as an app
2
+
3
+ A **site shell**: your live Wix website, framed as a native app. Every tab and drawer row is one of
4
+ your site's own pages, shown in a WebView under a native tab bar, drawer and splash in your brand
5
+ colours. The site's own header, footer and Wix ads are hidden. It is the fastest way to ship an
6
+ existing website as an app, and each tab can later be replaced by a native screen.
7
+
8
+ ## Point it at your site
9
+
10
+ The whole app is the `site` block in `provisioning.json`:
11
+
12
+ ```json
13
+ {
14
+ "site": {
15
+ "origin": "https://your-account.wixsite.com/your-site",
16
+ "kind": "wix",
17
+ "pages": [
18
+ { "id": "home", "title": "Home", "path": "/", "icon": "home" },
19
+ { "id": "blog", "title": "Blog", "path": "/blog", "icon": "newspaper" },
20
+ { "id": "contact", "title": "Contact", "path": "/contact", "icon": "mail", "placement": "drawer" }
21
+ ],
22
+ "chrome": { "hideSiteHeader": true, "hideSiteFooter": true, "hideWixAds": true }
23
+ }
24
+ }
25
+ ```
26
+
27
+ The placeholder origin above is not a real site. Replace it by deriving the block from your site's
28
+ own menu, from inside this folder:
29
+
30
+ ```bash
31
+ himi site analyze --url https://your-account.wixsite.com/your-site --shell brandy --apply
32
+ ```
33
+
34
+ That writes the derived `site` block into this `provisioning.json` and your site's colours into
35
+ `tokens.json`. No `--client-id` is needed, because a site shell reads nothing through a Wix client.
36
+ To review the map first, leave out `--apply`: the block is then only written to
37
+ `.himi/business-book/provisioning.overlay.json`. You can always edit the block by hand.
38
+
39
+ - **`origin`** is your published site, `https` only. A free site's path (`/your-site`) is part of it.
40
+ - **`pages`** are paths relative to the origin. Each needs a unique `id` (`a-z`, `0-9`, `_`) and must
41
+ not use `page`, which is reserved.
42
+ - **`placement`** is `tab` (the default), `drawer` or `overflow`. At most four tabs are shown, and
43
+ `home` must be the first of them: the app opens on its first tab.
44
+ - **`icon`** is a design-system icon name (`home`, `bag`, `calendar`, `ticket`, `newspaper`, `mail`,
45
+ `info`, `tag`, `photo`, `person`, `globe`, …).
46
+ - **`chrome`** hides the site's own header (on inner pages), its footer and the Wix ads banner.
47
+ Every flag defaults to hidden.
48
+
49
+ `config.ts` validates the block whenever the app loads, so an `http` origin, a duplicate id or a map
50
+ with no tab fails `himi validate`, `himi render` and `himi push` rather than shipping.
51
+
52
+ ## Loop
53
+
54
+ ```bash
55
+ himi build && himi validate --strict # derive the screens and check them
56
+ himi render site_home # the home tab (a WebView: headless render reports it, it does not paint it)
57
+ himi push --draft # publish a draft release and open the web preview
58
+ ```
59
+
60
+ Your colours live in `tokens.json` and your name in `config.ts`.
@@ -0,0 +1,76 @@
1
+ // Your app's configuration — a SITE SHELL. Your live website is the app: every tab and drawer
2
+ // row is one of its pages, shown in a native-framed WebView under a native tab bar and drawer.
3
+ //
4
+ // Nothing here names a page. The pages come from the `site` block of ./provisioning.json, so you
5
+ // change the app by changing that block — by hand, or with
6
+ // himi site analyze --url https://<your-site> --shell brandy
7
+ // which derives it from your site's own menu. Inspect the live values any time with
8
+ // `himi context`; see `himi docs app-config`.
9
+ import {
10
+ assertSiteShellConfig,
11
+ defineApp,
12
+ siteLinkableScreens,
13
+ siteNav,
14
+ siteNavConfig,
15
+ type SiteShellConfig,
16
+ } from "@himalaya/core";
17
+ import { ConsoleTelemetrySink } from "@himalaya/telemetry-console";
18
+ import provisioning from "./provisioning.json" with { type: "json" };
19
+
20
+ /** Your website and its page map. Validated here, so a mistake in provisioning.json (an http
21
+ * origin, a duplicate page id, no tab) fails `himi validate`, `render` and `push` instead of
22
+ * shipping a broken app. */
23
+ export const site = provisioning.site as SiteShellConfig;
24
+ assertSiteShellConfig(site);
25
+ const nav = siteNav(site);
26
+ // The app opens on its first tab, and everything that names the home screen (`himi init`,
27
+ // `himi render`, agent tooling) names `site_home` — so the `home` page must BE the first tab.
28
+ if (nav.tabs[0]?.screenId !== "site_home") {
29
+ throw new Error('provisioning.json: the "home" page must be the first tab — list it first in site.pages');
30
+ }
31
+
32
+ export default defineApp({
33
+ id: "__HIMI_APP_DOTID__",
34
+ name: "__HIMI_APP_NAME__",
35
+ bundleId: "__HIMI_APP_DOTID__",
36
+ applicationId: "__HIMI_APP_DOTID__",
37
+ // Placeholders — an over-the-air app never consumes these. Leave as-is.
38
+ schemaPackage: "@example/__HIMI_APP_SLUG__-schema",
39
+ schemaPackageRange: { min: 9900, max: 9909 },
40
+ // Placeholders — only a local server would read these. Leave as-is.
41
+ ports: { devServer: 8190, web: 5390 },
42
+ // Visitor-only: your site's own pages handle anything a visitor does.
43
+ auth: { providerId: "none", config: {} },
44
+ telemetry: ConsoleTelemetrySink(),
45
+ serverUrls: {
46
+ screen: "http://localhost:8190/v1",
47
+ actions: "http://localhost:8190/v1/actions",
48
+ sync: "http://localhost:8190/v1/sync",
49
+ },
50
+ capabilities: [],
51
+ // Your brand colours paint the NATIVE chrome (tabs, drawer, splash); the site paints its pages.
52
+ designTokensPath: "./tokens.json",
53
+ // "Ready" means the first page has painted, not just the app shell — otherwise the splash
54
+ // fades into a blank WebView. It still dismisses after 5s on a slow link.
55
+ splash: {
56
+ enabled: true,
57
+ backgroundColor: "accentPrimary",
58
+ caption: "__HIMI_APP_NAME__",
59
+ minVisibleMs: 400,
60
+ maxHoldMs: 5000,
61
+ readySignal: "first_content",
62
+ },
63
+ navigation: {
64
+ // Bottom tabs on a phone, a sidebar on a larger screen; the drawer lists every page that is
65
+ // not `overflow`. At most four tabs — each keeps its page loaded while you switch.
66
+ navKind: "responsive",
67
+ scheme: "himalaya-__HIMI_APP_SLUG__",
68
+ deepLinkPatterns: [],
69
+ linkableScreens: siteLinkableScreens(site),
70
+ // A notification or app link carrying one of your site's URLs opens that page's tab.
71
+ site: siteNavConfig(site),
72
+ tabBar: { items: nav.tabs },
73
+ drawer: { items: nav.drawer },
74
+ },
75
+ entities: {},
76
+ });
@@ -0,0 +1,25 @@
1
+ // Your app's screens. Each one is a page of your website — a WebView screen built from the
2
+ // `site` block of ../provisioning.json — so this file holds no screen of its own. Change the
3
+ // pages by changing that block, never this file.
4
+ import { siteScreens } from "@himalaya/core";
5
+ import config, { site } from "../config.js";
6
+
7
+ const SCREENS = siteScreens(site);
8
+ // The home screen is always the `home` page's: `site_home`. A literal on purpose — `himi init`
9
+ // and the agent tooling read `homeScreenId` from this file as written; config.ts refuses a map
10
+ // whose first tab is anything else, so the literal and the tab bar cannot disagree.
11
+
12
+ export default {
13
+ config,
14
+ homeScreenId: "site_home",
15
+ screens: {
16
+ // A fresh copy per build: descriptors are pure functions of the site block.
17
+ exact: Object.fromEntries(Object.keys(SCREENS).map((id) => [id, () => structuredClone(SCREENS[id])])),
18
+ templated: [],
19
+ },
20
+ actions: [],
21
+ createStore: () => ({}),
22
+ // Every page is seeded, so the whole tab bar and drawer work on a cold offline start (each
23
+ // WebView then shows its own offline state until the network returns).
24
+ seed: { screens: SCREENS, data: {} },
25
+ };
@@ -0,0 +1,5 @@
1
+ {
2
+ "name": "__HIMI_APP_NAME__",
3
+ "icon": "icon.png",
4
+ "target": "prod"
5
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "title": "Brandy — your website as an app",
3
+ "description": "A site shell: the customer's live Wix website as a native app. Every tab and drawer row is one of the site's own pages in a native-framed WebView, with a native tab bar, drawer, splash and brand colours; the site's header, footer and Wix ads are hidden. The page map lives in provisioning.json (`site`), and `himi site analyze --url <site> --shell brandy` derives it from the site's own menu. Start here to ship an existing website as an app today and move tabs to native screens later.",
4
+ "tags": ["wix", "website", "site-shell", "webview", "tabs", "drawer", "no-code", "day-zero", "consumer"],
5
+ "category": "consumer"
6
+ }
Binary file
@@ -0,0 +1 @@
1
+ {"macos":false}
@@ -0,0 +1,12 @@
1
+ {
2
+ "site": {
3
+ "origin": "https://your-account.wixsite.com/your-site",
4
+ "kind": "wix",
5
+ "pages": [
6
+ { "id": "home", "title": "Home", "path": "/", "icon": "home" },
7
+ { "id": "blog", "title": "Blog", "path": "/blog", "icon": "newspaper" },
8
+ { "id": "contact", "title": "Contact", "path": "/contact", "icon": "mail", "placement": "drawer" }
9
+ ],
10
+ "chrome": { "hideSiteHeader": true, "hideSiteFooter": true, "hideWixAds": true }
11
+ }
12
+ }
@@ -0,0 +1,128 @@
1
+ {
2
+ "colors": {
3
+ "surfacePrimary": {
4
+ "light": "#FFFFFF",
5
+ "dark": "#0D1117",
6
+ "highContrast": "#000000"
7
+ },
8
+ "surfaceSecondary": {
9
+ "light": "#F4F7F8",
10
+ "dark": "#161B22",
11
+ "highContrast": "#111111"
12
+ },
13
+ "surfaceElevated": {
14
+ "light": "#FFFFFF",
15
+ "dark": "#1D2228",
16
+ "highContrast": "#111111"
17
+ },
18
+ "surfaceSubtle": {
19
+ "light": "#F8FAF9",
20
+ "dark": "#121821",
21
+ "highContrast": "#111111"
22
+ },
23
+ "surfaceTertiary": {
24
+ "light": "#E9EEF0",
25
+ "dark": "#232A32",
26
+ "highContrast": "#1A1A1A"
27
+ },
28
+ "textPrimary": {
29
+ "light": "#1F2328",
30
+ "dark": "#E6EDF3",
31
+ "highContrast": "#FFFFFF"
32
+ },
33
+ "textSecondary": {
34
+ "light": "#59636E",
35
+ "dark": "#848D98",
36
+ "highContrast": "#CCCCCC"
37
+ },
38
+ "textTertiary": {
39
+ "light": "#7D8791",
40
+ "dark": "#6E7681",
41
+ "highContrast": "#CCCCCC"
42
+ },
43
+ "textOnAccent": {
44
+ "light": "#FFFFFF",
45
+ "dark": "#FFFFFF",
46
+ "highContrast": "#000000"
47
+ },
48
+ "accentPrimary": {
49
+ "light": "#0969DA",
50
+ "dark": "#1E72C7",
51
+ "highContrast": "#58A6FF"
52
+ },
53
+ "accentSecondary": {
54
+ "light": "#087F5B",
55
+ "dark": "#1C8968",
56
+ "highContrast": "#FFFFFF"
57
+ },
58
+ "accentSurface": {
59
+ "light": "#E2EDF8",
60
+ "dark": "#0C2540",
61
+ "highContrast": "#FFFFFF"
62
+ },
63
+ "success": {
64
+ "light": "#1A7F37",
65
+ "dark": "#3FB950",
66
+ "highContrast": "#3FB950"
67
+ },
68
+ "successSurface": {
69
+ "light": "#E9F8EC",
70
+ "dark": "#12331B",
71
+ "highContrast": "#FFFFFF"
72
+ },
73
+ "warning": {
74
+ "light": "#9A6700",
75
+ "dark": "#D29922",
76
+ "highContrast": "#D29922"
77
+ },
78
+ "warningSurface": {
79
+ "light": "#FFF5D8",
80
+ "dark": "#3A2C0A",
81
+ "highContrast": "#FFFFFF"
82
+ },
83
+ "danger": {
84
+ "light": "#CF222E",
85
+ "dark": "#F85149",
86
+ "highContrast": "#FF5555"
87
+ },
88
+ "dangerSurface": {
89
+ "light": "#FFEDEA",
90
+ "dark": "#3A1513",
91
+ "highContrast": "#FFFFFF"
92
+ },
93
+ "border": {
94
+ "light": "#D8DEE4",
95
+ "dark": "#30363D",
96
+ "highContrast": "#FFFFFF"
97
+ }
98
+ },
99
+ "spacing": {
100
+ "xs": 4,
101
+ "sm": 8,
102
+ "md": 12,
103
+ "lg": 16,
104
+ "xl": 24
105
+ },
106
+ "typography": {
107
+ "title1": {
108
+ "size": 28,
109
+ "weight": 700,
110
+ "line": 1.2
111
+ },
112
+ "body": {
113
+ "size": 16,
114
+ "weight": 400,
115
+ "line": 1.5
116
+ },
117
+ "caption": {
118
+ "size": 13,
119
+ "weight": 400,
120
+ "line": 1.4
121
+ }
122
+ },
123
+ "radius": {
124
+ "sm": 4,
125
+ "md": 8,
126
+ "lg": 12
127
+ }
128
+ }