@wix/himalaya-cli 0.1116.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 +145 -69
- package/guides/himi-mcp.md +7 -2
- package/package.json +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -36089,10 +36089,10 @@ var init_render_fonts = __esm({
|
|
|
36089
36089
|
|
|
36090
36090
|
// ../../core/dev-server/src/wix-cache-policy.ts
|
|
36091
36091
|
function clampRule(rule, denyPrefixes, maxTTL) {
|
|
36092
|
-
const
|
|
36092
|
+
const denied2 = denyPrefixes.some((p) => rule.pathPrefix.startsWith(p));
|
|
36093
36093
|
return {
|
|
36094
36094
|
...rule,
|
|
36095
|
-
cacheable: rule.cacheable && !
|
|
36095
|
+
cacheable: rule.cacheable && !denied2,
|
|
36096
36096
|
ttl: Math.min(rule.ttl, maxTTL)
|
|
36097
36097
|
};
|
|
36098
36098
|
}
|
|
@@ -53854,7 +53854,8 @@ var init_cli_bridge = __esm({
|
|
|
53854
53854
|
});
|
|
53855
53855
|
|
|
53856
53856
|
// src/mcp/file-tools.ts
|
|
53857
|
-
import { lstatSync as lstatSync12, mkdirSync as mkdirSync26, readdirSync as readdirSync27, readFileSync as readFileSync59, realpathSync as realpathSync7, rmSync as rmSync12, statSync as statSync13,
|
|
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";
|
|
53858
53859
|
import { dirname as dirname30, isAbsolute as isAbsolute5, join as join53, relative as relative11, resolve as resolve45, sep as sep14 } from "node:path";
|
|
53859
53860
|
import * as z from "zod";
|
|
53860
53861
|
function within(root, p) {
|
|
@@ -53871,37 +53872,99 @@ function realish(p) {
|
|
|
53871
53872
|
}
|
|
53872
53873
|
return join53(realpathSync7(cur), ...tail);
|
|
53873
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
|
+
}
|
|
53874
53887
|
async function confinedAppDir(args, deps) {
|
|
53875
53888
|
const root = resolve45(await deps.defaultDir());
|
|
53876
53889
|
const dir = typeof args.dir === "string" && args.dir ? resolve45(root, args.dir) : root;
|
|
53877
53890
|
if (!existsSync54(root)) return refuse4("no_workspace", `the workspace folder ${root} does not exist`);
|
|
53878
|
-
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
|
+
}
|
|
53879
53899
|
return dir;
|
|
53880
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
|
+
}
|
|
53881
53921
|
function confinedPath(appDir3, rel) {
|
|
53882
53922
|
if (typeof rel !== "string" || !rel.trim()) return { error: "a path is required" };
|
|
53883
53923
|
if (rel.includes("\0")) return { error: "a path may not contain NUL" };
|
|
53884
53924
|
if (isAbsolute5(rel)) return { error: `"${rel}" is absolute \u2014 give a path inside the app folder` };
|
|
53885
53925
|
const abs = resolve45(appDir3, rel);
|
|
53926
|
+
if (!within(appDir3, abs) || abs === appDir3) return { error: `"${rel}" is outside the app folder` };
|
|
53886
53927
|
const real = realish(abs);
|
|
53887
53928
|
const base = existsSync54(appDir3) ? realpathSync7(appDir3) : realish(appDir3);
|
|
53888
53929
|
if (!within(base, real) || real === base) return { error: `"${rel}" is outside the app folder` };
|
|
53889
|
-
const
|
|
53890
|
-
if (
|
|
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` };
|
|
53891
53933
|
return abs;
|
|
53892
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
|
+
}
|
|
53893
53953
|
function listFiles(appDir3) {
|
|
53894
53954
|
const files = [];
|
|
53895
53955
|
const walk2 = (d) => {
|
|
53956
|
+
const parentRel = relative11(appDir3, d);
|
|
53896
53957
|
for (const e of readdirSync27(d, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
53897
|
-
if (RESERVED.has(e.name)) continue;
|
|
53898
53958
|
if (e.isSymbolicLink()) continue;
|
|
53899
53959
|
const p = join53(d, e.name);
|
|
53900
53960
|
if (e.isDirectory()) {
|
|
53961
|
+
if (isDeniedSourceDir(e.name, parentRel) || isDeniedSourceDir(e.name.toLowerCase(), parentRel)) continue;
|
|
53901
53962
|
if (!walk2(p)) return false;
|
|
53902
53963
|
} else if (e.isFile()) {
|
|
53964
|
+
const rel = relative11(appDir3, p);
|
|
53965
|
+
if (isDeniedSourcePath(rel) || isDeniedSourcePath(rel.toLowerCase())) continue;
|
|
53903
53966
|
if (files.length >= MAX_LISTED) return false;
|
|
53904
|
-
files.push({ path:
|
|
53967
|
+
files.push({ path: rel.split(sep14).join("/"), bytes: statSync13(p).size });
|
|
53905
53968
|
}
|
|
53906
53969
|
}
|
|
53907
53970
|
return true;
|
|
@@ -53931,7 +53994,7 @@ function fileTools() {
|
|
|
53931
53994
|
{
|
|
53932
53995
|
name: "himi_list_files",
|
|
53933
53996
|
title: "List app files",
|
|
53934
|
-
description: `Every file in an app folder with its size
|
|
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).`,
|
|
53935
53998
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
53936
53999
|
inputSchema: z.object({ dir: DIR }),
|
|
53937
54000
|
handler: async (a, d) => {
|
|
@@ -53980,14 +54043,20 @@ function fileTools() {
|
|
|
53980
54043
|
else if (typeof f.content !== "string") refused2.push({ path: f.path, error: "content must be text" });
|
|
53981
54044
|
else if (Buffer.byteLength(f.content) > MAX_FILE_BYTES) refused2.push({ path: f.path, error: `over the ${MAX_FILE_BYTES}-byte limit` });
|
|
53982
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" });
|
|
53983
54047
|
else planned.push({ abs: p, rel: String(f.path), content: f.content });
|
|
53984
54048
|
}
|
|
53985
54049
|
if (refused2.length) return { ...ok({ ok: false, error: "refused", written: [], refused: refused2 }), isError: true };
|
|
54050
|
+
const written = [];
|
|
53986
54051
|
for (const f of planned) {
|
|
53987
|
-
|
|
53988
|
-
|
|
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
|
+
}
|
|
53989
54058
|
}
|
|
53990
|
-
return ok({ ok: true, dir: app, written
|
|
54059
|
+
return ok({ ok: true, dir: app, written });
|
|
53991
54060
|
}
|
|
53992
54061
|
},
|
|
53993
54062
|
{
|
|
@@ -54001,28 +54070,32 @@ function fileTools() {
|
|
|
54001
54070
|
if (typeof app !== "string") return app;
|
|
54002
54071
|
const paths = Array.isArray(a.paths) ? a.paths : [];
|
|
54003
54072
|
const planned = [];
|
|
54073
|
+
const deleted = [];
|
|
54004
54074
|
const refused2 = [];
|
|
54005
54075
|
for (const rel of paths) {
|
|
54006
54076
|
const p = confinedPath(app, rel);
|
|
54007
54077
|
if (typeof p !== "string") refused2.push({ path: rel, error: p.error });
|
|
54008
54078
|
else if (!existsSync54(p)) refused2.push({ path: rel, error: "no such file" });
|
|
54009
54079
|
else if (!lstatSync12(p).isFile()) refused2.push({ path: rel, error: "not a regular file" });
|
|
54010
|
-
else planned.
|
|
54080
|
+
else if (!planned.includes(p)) {
|
|
54081
|
+
planned.push(p);
|
|
54082
|
+
deleted.push(rel);
|
|
54083
|
+
}
|
|
54011
54084
|
}
|
|
54012
54085
|
if (refused2.length) return { ...ok({ ok: false, error: "refused", deleted: [], refused: refused2 }), isError: true };
|
|
54013
54086
|
for (const p of planned) rmSync12(p);
|
|
54014
|
-
return ok({ ok: true, dir: app, deleted
|
|
54087
|
+
return ok({ ok: true, dir: app, deleted });
|
|
54015
54088
|
}
|
|
54016
54089
|
}
|
|
54017
54090
|
];
|
|
54018
54091
|
}
|
|
54019
|
-
var MAX_FILE_BYTES, MAX_FILES_PER_WRITE, MAX_LISTED,
|
|
54092
|
+
var MAX_FILE_BYTES, MAX_FILES_PER_WRITE, MAX_LISTED, CONTENT_MARKER, ok, refuse4, DIR, WRITE_ANN;
|
|
54020
54093
|
var init_file_tools = __esm({
|
|
54021
54094
|
"src/mcp/file-tools.ts"() {
|
|
54095
|
+
init_source_deny();
|
|
54022
54096
|
MAX_FILE_BYTES = 1024 * 1024;
|
|
54023
54097
|
MAX_FILES_PER_WRITE = 100;
|
|
54024
54098
|
MAX_LISTED = 2e3;
|
|
54025
|
-
RESERVED = /* @__PURE__ */ new Set([".git", "node_modules", "dist"]);
|
|
54026
54099
|
CONTENT_MARKER = "himalaya.content.json";
|
|
54027
54100
|
ok = (o) => ({ content: [{ type: "text", text: JSON.stringify(o, null, 2) }] });
|
|
54028
54101
|
refuse4 = (error, message) => ({ content: [{ type: "text", text: JSON.stringify({ ok: false, error, message }, null, 2) }], isError: true });
|
|
@@ -54533,7 +54606,7 @@ __export(native_decode_exports, {
|
|
|
54533
54606
|
runNativeDecode: () => runNativeDecode
|
|
54534
54607
|
});
|
|
54535
54608
|
import { execFileSync as execFileSync5, spawnSync as spawnSync2 } from "node:child_process";
|
|
54536
|
-
import { existsSync as existsSync55, mkdtempSync as mkdtempSync2, readFileSync as readFileSync61, rmSync as rmSync13, writeFileSync as
|
|
54609
|
+
import { existsSync as existsSync55, mkdtempSync as mkdtempSync2, readFileSync as readFileSync61, rmSync as rmSync13, writeFileSync as writeFileSync21 } from "node:fs";
|
|
54537
54610
|
import { tmpdir as tmpdir3 } from "node:os";
|
|
54538
54611
|
import { join as join54 } from "node:path";
|
|
54539
54612
|
import { fileURLToPath as fileURLToPath22 } from "node:url";
|
|
@@ -54667,7 +54740,7 @@ function runNativeDecode(options) {
|
|
|
54667
54740
|
if (typeof descriptor.descriptorJson !== "string") {
|
|
54668
54741
|
throw new Error(`native decode cannot judge ${descriptor.screenId}: the release builder did not return its exact baked JSON bytes`);
|
|
54669
54742
|
}
|
|
54670
|
-
|
|
54743
|
+
writeFileSync21(join54(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
|
|
54671
54744
|
}
|
|
54672
54745
|
options.progress?.("building cached Swift oracle");
|
|
54673
54746
|
execFileSync5("swift", ["build", "--package-path", options.packageDir, "--product", "himi-decode-oracle"], {
|
|
@@ -54705,7 +54778,7 @@ function runAndroidNativeDecode(options) {
|
|
|
54705
54778
|
if (typeof descriptor.descriptorJson !== "string") {
|
|
54706
54779
|
throw new Error(`native decode cannot judge ${descriptor.screenId}: the release builder did not return its exact baked JSON bytes`);
|
|
54707
54780
|
}
|
|
54708
|
-
|
|
54781
|
+
writeFileSync21(join54(descriptorDir, `${encodeURIComponent(descriptor.screenId)}.json`), descriptor.descriptorJson);
|
|
54709
54782
|
}
|
|
54710
54783
|
options.progress?.(`running real Kotlin decoder over ${options.descriptors.length} screen${options.descriptors.length === 1 ? "" : "s"}`);
|
|
54711
54784
|
execFileSync5(options.gradlew, [
|
|
@@ -54789,7 +54862,7 @@ __export(browser_authoring_exports, {
|
|
|
54789
54862
|
sdkSubpathReader: () => sdkSubpathReader
|
|
54790
54863
|
});
|
|
54791
54864
|
import { createServer as createServer5 } from "node:http";
|
|
54792
|
-
import { createHash as createHash29, randomBytes as
|
|
54865
|
+
import { createHash as createHash29, randomBytes as randomBytes5 } from "node:crypto";
|
|
54793
54866
|
import { existsSync as existsSync56, readdirSync as readdirSync28, readFileSync as readFileSync62, realpathSync as realpathSync8, statSync as statSync14 } from "node:fs";
|
|
54794
54867
|
import { join as join55, posix as posix2, relative as relative12, sep as sep15 } from "node:path";
|
|
54795
54868
|
function humanMs(ms) {
|
|
@@ -54887,7 +54960,7 @@ function sdkSubpathReader(resolve54) {
|
|
|
54887
54960
|
function encodeJob(job) {
|
|
54888
54961
|
return Buffer.from(JSON.stringify(job), "utf8").toString("base64");
|
|
54889
54962
|
}
|
|
54890
|
-
function mintToken2(payload, nonce =
|
|
54963
|
+
function mintToken2(payload, nonce = randomBytes5(16).toString("hex")) {
|
|
54891
54964
|
const digest2 = createHash29("sha256").update(payload, "utf8").digest("base64url");
|
|
54892
54965
|
return `${nonce}.${digest2}`;
|
|
54893
54966
|
}
|
|
@@ -55050,7 +55123,7 @@ __export(coverage_exports, {
|
|
|
55050
55123
|
suppressedNativeKeys: () => suppressedNativeKeys,
|
|
55051
55124
|
trendAndStore: () => trendAndStore
|
|
55052
55125
|
});
|
|
55053
|
-
import { existsSync as existsSync57, mkdirSync as mkdirSync27, readFileSync as readFileSync63, writeFileSync as
|
|
55126
|
+
import { existsSync as existsSync57, mkdirSync as mkdirSync27, readFileSync as readFileSync63, writeFileSync as writeFileSync22 } from "node:fs";
|
|
55054
55127
|
import { dirname as dirname31, join as join56 } from "node:path";
|
|
55055
55128
|
function suppressedNativeKeys(oracle) {
|
|
55056
55129
|
return [...oracle.ios ? [] : ["native-decode|ios"], ...oracle.android ? [] : ["native-decode|android"]];
|
|
@@ -55244,7 +55317,7 @@ function trendAndStore(dir, entries, suppressed) {
|
|
|
55244
55317
|
const stored = [.../* @__PURE__ */ new Set([...current, ...(previous ?? []).filter((key2) => (suppressed ?? []).includes(key2))])];
|
|
55245
55318
|
try {
|
|
55246
55319
|
mkdirSync27(dirname31(p), { recursive: true });
|
|
55247
|
-
|
|
55320
|
+
writeFileSync22(p, JSON.stringify({ keys: stored, ts: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
55248
55321
|
} catch {
|
|
55249
55322
|
return null;
|
|
55250
55323
|
}
|
|
@@ -55289,7 +55362,7 @@ function diffAndStoreObservations(dir, current) {
|
|
|
55289
55362
|
}
|
|
55290
55363
|
try {
|
|
55291
55364
|
mkdirSync27(dirname31(p), { recursive: true });
|
|
55292
|
-
|
|
55365
|
+
writeFileSync22(p, JSON.stringify({ screens: current, ts: (/* @__PURE__ */ new Date()).toISOString() }, null, 2));
|
|
55293
55366
|
} catch {
|
|
55294
55367
|
return null;
|
|
55295
55368
|
}
|
|
@@ -55900,8 +55973,8 @@ ${releaseId}`;
|
|
|
55900
55973
|
const pin = resolvePin(request, url);
|
|
55901
55974
|
let bytes2 = null;
|
|
55902
55975
|
if (pin) {
|
|
55903
|
-
const
|
|
55904
|
-
if (
|
|
55976
|
+
const denied2 = await authorizePinnedRead(request, url, app, pin);
|
|
55977
|
+
if (denied2) return { ok: false, response: denied2 };
|
|
55905
55978
|
const m = await pinnedManifest(app, pin);
|
|
55906
55979
|
if (!m) return { ok: false, response: json4(404, { error: "release_not_found", app, releaseId: pin }) };
|
|
55907
55980
|
bytes2 = m.appConfigHash ? await pinnedBody(app, m.appConfigHash, () => store.getAppConfig(app)) : null;
|
|
@@ -55924,7 +55997,7 @@ ${releaseId}`;
|
|
|
55924
55997
|
return new Response(body, { status: 200, headers: { "content-type": JSON_CT2, etag, "cache-control": cache, ...vary } });
|
|
55925
55998
|
}
|
|
55926
55999
|
async function authorizePinnedRead(request, url, app, releaseId, screenId) {
|
|
55927
|
-
const
|
|
56000
|
+
const denied2 = await checkPinnedRead({
|
|
55928
56001
|
secret: opts.previewTokenSecret,
|
|
55929
56002
|
token: resolvePreviewToken(request, url),
|
|
55930
56003
|
app,
|
|
@@ -55934,7 +56007,7 @@ ${releaseId}`;
|
|
|
55934
56007
|
authorizePreview: opts.authorizePreview,
|
|
55935
56008
|
authorizeFollow: opts.authorizeFollow
|
|
55936
56009
|
});
|
|
55937
|
-
return
|
|
56010
|
+
return denied2 && json4(denied2.status, denied2.body);
|
|
55938
56011
|
}
|
|
55939
56012
|
async function honourRingPin(request) {
|
|
55940
56013
|
const kept = { request, dropped: false };
|
|
@@ -56385,8 +56458,8 @@ ${releaseId}`;
|
|
|
56385
56458
|
const app = cfgMatch?.[1] ?? resolveApp(request, url);
|
|
56386
56459
|
const pin = resolvePin(request, url);
|
|
56387
56460
|
if (app && pin) {
|
|
56388
|
-
const
|
|
56389
|
-
if (
|
|
56461
|
+
const denied2 = await authorizePinnedRead(request, url, app, pin);
|
|
56462
|
+
if (denied2) return denied2;
|
|
56390
56463
|
}
|
|
56391
56464
|
if (path2 === "/v1/template-source") {
|
|
56392
56465
|
if (!app) return json4(400, { error: "missing_app" });
|
|
@@ -56469,8 +56542,8 @@ ${releaseId}`;
|
|
|
56469
56542
|
if (!app) return json4(400, { error: "missing_app" });
|
|
56470
56543
|
let m;
|
|
56471
56544
|
if (pin) {
|
|
56472
|
-
const
|
|
56473
|
-
if (
|
|
56545
|
+
const denied2 = await authorizePinnedRead(request, url, app, pin);
|
|
56546
|
+
if (denied2) return denied2;
|
|
56474
56547
|
m = await pinnedManifest(app, pin);
|
|
56475
56548
|
if (!m) return json4(404, { error: "release_not_found", app, releaseId: pin });
|
|
56476
56549
|
} else {
|
|
@@ -56656,8 +56729,8 @@ ${releaseId}`;
|
|
|
56656
56729
|
const screenId = screenMatch[1];
|
|
56657
56730
|
if (!app || !isSafeSegment(screenId)) return json4(404, { error: "screen_not_found", screenId });
|
|
56658
56731
|
if (pin) {
|
|
56659
|
-
const
|
|
56660
|
-
if (
|
|
56732
|
+
const denied2 = await authorizePinnedRead(request, url, app, pin, screenId);
|
|
56733
|
+
if (denied2) return denied2;
|
|
56661
56734
|
const m2 = await pinnedManifest(app, pin);
|
|
56662
56735
|
if (!m2) return json4(404, { error: "release_not_found", app, releaseId: pin });
|
|
56663
56736
|
const meta2 = m2.screens[screenId];
|
|
@@ -57450,7 +57523,7 @@ __export(run_watch_exports, {
|
|
|
57450
57523
|
watchedContentDir: () => watchedContentDir,
|
|
57451
57524
|
writeRunRecord: () => writeRunRecord
|
|
57452
57525
|
});
|
|
57453
|
-
import { closeSync as
|
|
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";
|
|
57454
57527
|
import { randomUUID as randomUUID4 } from "node:crypto";
|
|
57455
57528
|
import { extname as extname4, join as join59, resolve as resolve47, sep as sep16 } from "node:path";
|
|
57456
57529
|
function diffReleases(prev, next) {
|
|
@@ -57585,9 +57658,9 @@ function writeRunRecord(dir, record2) {
|
|
|
57585
57658
|
if (!root) return false;
|
|
57586
57659
|
const tmp = join59(root, `.run.json.${process.pid}.${randomUUID4()}.tmp`);
|
|
57587
57660
|
try {
|
|
57588
|
-
|
|
57661
|
+
writeFileSync23(tmp, `${JSON.stringify(record2, null, 2)}
|
|
57589
57662
|
`, { flag: "wx" });
|
|
57590
|
-
|
|
57663
|
+
renameSync7(tmp, join59(root, "run.json"));
|
|
57591
57664
|
} catch (e) {
|
|
57592
57665
|
rmSync15(tmp, { force: true });
|
|
57593
57666
|
throw e;
|
|
@@ -57644,12 +57717,12 @@ function readConfined(root, rel) {
|
|
|
57644
57717
|
}
|
|
57645
57718
|
const canonicalRoot = realpathSync9(root);
|
|
57646
57719
|
if (!realpathSync9(at2).startsWith(canonicalRoot + sep16)) return null;
|
|
57647
|
-
const fd =
|
|
57720
|
+
const fd = openSync4(at2, constants2.O_RDONLY | (constants2.O_NOFOLLOW ?? 0));
|
|
57648
57721
|
try {
|
|
57649
57722
|
if (!fstatSync(fd).isFile()) return null;
|
|
57650
57723
|
return readFileSync67(fd);
|
|
57651
57724
|
} finally {
|
|
57652
|
-
|
|
57725
|
+
closeSync5(fd);
|
|
57653
57726
|
}
|
|
57654
57727
|
} catch {
|
|
57655
57728
|
return null;
|
|
@@ -58871,7 +58944,7 @@ __export(version_exports, {
|
|
|
58871
58944
|
setAndroidVersion: () => setAndroidVersion,
|
|
58872
58945
|
setIosVersion: () => setIosVersion
|
|
58873
58946
|
});
|
|
58874
|
-
import { readFileSync as readFileSync72, writeFileSync as
|
|
58947
|
+
import { readFileSync as readFileSync72, writeFileSync as writeFileSync24 } from "node:fs";
|
|
58875
58948
|
function readIosVersion(projectYml) {
|
|
58876
58949
|
const marketing = matchScalar(projectYml, "CFBundleShortVersionString");
|
|
58877
58950
|
const build2 = matchScalar(projectYml, "CFBundleVersion");
|
|
@@ -58936,8 +59009,8 @@ function bumpVersion(app, opts) {
|
|
|
58936
59009
|
marketing: opts.marketing ?? current.marketing,
|
|
58937
59010
|
build: opts.build === "increment" || opts.build === void 0 ? current.build + (opts.build === "increment" ? 1 : 0) : opts.build
|
|
58938
59011
|
};
|
|
58939
|
-
|
|
58940
|
-
|
|
59012
|
+
writeFileSync24(iosPath, setIosVersion(iosYml, next));
|
|
59013
|
+
writeFileSync24(gradlePath, setAndroidVersion(gradle, next));
|
|
58941
59014
|
return next;
|
|
58942
59015
|
}
|
|
58943
59016
|
var init_version = __esm({
|
|
@@ -59080,7 +59153,7 @@ var init_asc_status = __esm({
|
|
|
59080
59153
|
});
|
|
59081
59154
|
|
|
59082
59155
|
// ../../core/functions-secrets/src/vault.ts
|
|
59083
|
-
import { createCipheriv, createDecipheriv, randomBytes as
|
|
59156
|
+
import { createCipheriv, createDecipheriv, randomBytes as randomBytes6 } from "node:crypto";
|
|
59084
59157
|
import { mkdir, readFile as readFile2, rename, writeFile as writeFile2 } from "node:fs/promises";
|
|
59085
59158
|
import { dirname as dirname34 } from "node:path";
|
|
59086
59159
|
function createJsonSecretStore(filePath) {
|
|
@@ -59138,7 +59211,7 @@ function createSecretVault(options) {
|
|
|
59138
59211
|
environment: input.environment,
|
|
59139
59212
|
name: input.name,
|
|
59140
59213
|
version: input.version,
|
|
59141
|
-
iv:
|
|
59214
|
+
iv: randomBytes6(12).toString("base64url"),
|
|
59142
59215
|
ciphertext: "",
|
|
59143
59216
|
authTag: ""
|
|
59144
59217
|
};
|
|
@@ -59500,7 +59573,7 @@ __export(notify_exports, {
|
|
|
59500
59573
|
runNotify: () => runNotify,
|
|
59501
59574
|
saveAdminSession: () => saveAdminSession
|
|
59502
59575
|
});
|
|
59503
|
-
import { chmodSync as chmodSync3, mkdirSync as mkdirSync30, readFileSync as readFileSync76, writeFileSync as
|
|
59576
|
+
import { chmodSync as chmodSync3, mkdirSync as mkdirSync30, readFileSync as readFileSync76, writeFileSync as writeFileSync25 } from "node:fs";
|
|
59504
59577
|
import { homedir as homedir6 } from "node:os";
|
|
59505
59578
|
import { dirname as dirname35, join as join62 } from "node:path";
|
|
59506
59579
|
function defaultSessionsPath() {
|
|
@@ -59517,7 +59590,7 @@ function saveAdminSession(adminUrl, session, path2 = defaultSessionsPath()) {
|
|
|
59517
59590
|
const file = loadSessionsFile(path2);
|
|
59518
59591
|
file.sessions = { ...file.sessions, [adminUrl]: session };
|
|
59519
59592
|
mkdirSync30(dirname35(path2), { recursive: true, mode: 448 });
|
|
59520
|
-
|
|
59593
|
+
writeFileSync25(path2, JSON.stringify(file, null, 2) + "\n", { mode: 384 });
|
|
59521
59594
|
try {
|
|
59522
59595
|
chmodSync3(dirname35(path2), 448);
|
|
59523
59596
|
chmodSync3(path2, 384);
|
|
@@ -63276,7 +63349,7 @@ var init_brand_font = __esm({
|
|
|
63276
63349
|
});
|
|
63277
63350
|
|
|
63278
63351
|
// src/site-analyze/write-artifacts.ts
|
|
63279
|
-
import { mkdirSync as mkdirSync31, writeFileSync as
|
|
63352
|
+
import { mkdirSync as mkdirSync31, writeFileSync as writeFileSync26 } from "node:fs";
|
|
63280
63353
|
import { join as join63 } from "node:path";
|
|
63281
63354
|
function attachVeloSources(book, sources, put) {
|
|
63282
63355
|
const byPath = new Map((sources ?? []).map((s) => [s.path, s.text]));
|
|
@@ -63438,7 +63511,7 @@ async function writeBusinessBook(result, opts) {
|
|
|
63438
63511
|
const put = (rel, body) => {
|
|
63439
63512
|
const p = join63(out2, rel);
|
|
63440
63513
|
mkdirSync31(join63(p, ".."), { recursive: true });
|
|
63441
|
-
|
|
63514
|
+
writeFileSync26(p, body);
|
|
63442
63515
|
files.push(rel);
|
|
63443
63516
|
};
|
|
63444
63517
|
const book = result.book;
|
|
@@ -63531,7 +63604,7 @@ var init_customer_build = __esm({
|
|
|
63531
63604
|
});
|
|
63532
63605
|
|
|
63533
63606
|
// src/site-analyze/apply.ts
|
|
63534
|
-
import { existsSync as existsSync65, readFileSync as readFileSync77, writeFileSync as
|
|
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";
|
|
63535
63608
|
import { dirname as dirname36, isAbsolute as isAbsolute7, join as join64, resolve as resolve51, sep as sep17 } from "node:path";
|
|
63536
63609
|
import { spawnSync as spawnSync4 } from "node:child_process";
|
|
63537
63610
|
function unpairedRedirectNote(overlay) {
|
|
@@ -63683,10 +63756,10 @@ function buildMergedContract(options) {
|
|
|
63683
63756
|
const body = `${JSON.stringify(merged, null, 2)}
|
|
63684
63757
|
`;
|
|
63685
63758
|
const path2 = join64(packageDir, "provisioning.contract.json");
|
|
63686
|
-
|
|
63759
|
+
writeFileSync27(path2, body);
|
|
63687
63760
|
const localPath = join64(appDir3, LOCAL_CONTRACT_REL);
|
|
63688
63761
|
mkdirSync32(dirname36(localPath), { recursive: true });
|
|
63689
|
-
|
|
63762
|
+
writeFileSync27(localPath, body);
|
|
63690
63763
|
staged.push(localPath);
|
|
63691
63764
|
return { path: path2, staged };
|
|
63692
63765
|
}
|
|
@@ -63785,10 +63858,10 @@ async function applyOverlays(input) {
|
|
|
63785
63858
|
const cur = JSON.parse(readFileSync77(pkgProv, "utf8"));
|
|
63786
63859
|
const merged = deepMerge4(cur, overlay);
|
|
63787
63860
|
packageFaq = merged.faq;
|
|
63788
|
-
|
|
63861
|
+
writeFileSync27(pkgProv, JSON.stringify(merged, null, 2) + "\n");
|
|
63789
63862
|
wrote.push(pkgProv);
|
|
63790
63863
|
} else {
|
|
63791
|
-
|
|
63864
|
+
writeFileSync27(pkgProv, JSON.stringify(overlay, null, 2) + "\n");
|
|
63792
63865
|
wrote.push(pkgProv);
|
|
63793
63866
|
}
|
|
63794
63867
|
const pkgTokens = join64(cwd, "tokens.json");
|
|
@@ -63814,7 +63887,7 @@ async function applyOverlays(input) {
|
|
|
63814
63887
|
if (primary && glow && typeof glow.color === "string") glow.color = primary;
|
|
63815
63888
|
stripBrandFromIdiom(merged);
|
|
63816
63889
|
}
|
|
63817
|
-
|
|
63890
|
+
writeFileSync27(pkgTokens, JSON.stringify(merged, null, 2) + "\n");
|
|
63818
63891
|
wrote.push(pkgTokens);
|
|
63819
63892
|
}
|
|
63820
63893
|
const homeAction = homeRefreshActionId(shell);
|
|
@@ -63844,7 +63917,7 @@ async function applyOverlays(input) {
|
|
|
63844
63917
|
}
|
|
63845
63918
|
}
|
|
63846
63919
|
if (next !== prev) {
|
|
63847
|
-
|
|
63920
|
+
writeFileSync27(homeSrc, next);
|
|
63848
63921
|
wrote.push(homeSrc);
|
|
63849
63922
|
}
|
|
63850
63923
|
if (patched.unparsed) {
|
|
@@ -68600,7 +68673,7 @@ __export(site_create_exports, {
|
|
|
68600
68673
|
runSiteList: () => runSiteList,
|
|
68601
68674
|
writeProvisioningBinding: () => writeProvisioningBinding
|
|
68602
68675
|
});
|
|
68603
|
-
import { existsSync as existsSync66, readFileSync as readFileSync79, writeFileSync as
|
|
68676
|
+
import { existsSync as existsSync66, readFileSync as readFileSync79, writeFileSync as writeFileSync28 } from "node:fs";
|
|
68604
68677
|
import { resolve as resolvePath2 } from "node:path";
|
|
68605
68678
|
function oauthClientName(projectName) {
|
|
68606
68679
|
return `${projectName} client`;
|
|
@@ -68749,7 +68822,7 @@ async function runSiteCreate(opts) {
|
|
|
68749
68822
|
if (!wantOauthApp) return { ...base, nextSteps: nextSteps(metaSiteId) };
|
|
68750
68823
|
return { ...base, clientId, redirectUri, nextSteps: nextSteps(metaSiteId, clientId) };
|
|
68751
68824
|
}
|
|
68752
|
-
function writeProvisioningBinding(target, binding2, fs = { existsSync: existsSync66, readFileSync: readFileSync79, writeFileSync:
|
|
68825
|
+
function writeProvisioningBinding(target, binding2, fs = { existsSync: existsSync66, readFileSync: readFileSync79, writeFileSync: writeFileSync28 }) {
|
|
68753
68826
|
const msid = binding2.siteId;
|
|
68754
68827
|
const path2 = resolvePath2(target);
|
|
68755
68828
|
const committed = committedShellProvisioning(path2);
|
|
@@ -69422,7 +69495,7 @@ var init_context = __esm({
|
|
|
69422
69495
|
});
|
|
69423
69496
|
|
|
69424
69497
|
// src/cli.ts
|
|
69425
|
-
import { mkdtempSync as mkdtempSync4, readFileSync as readFileSync80, mkdirSync as mkdirSync33, writeFileSync as
|
|
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";
|
|
69426
69499
|
import { execFile, execFileSync as execFileSync7, spawnSync as spawnSync5, spawn as spawn7 } from "node:child_process";
|
|
69427
69500
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
69428
69501
|
import { createHash as createHash31, randomUUID as randomUUID7 } from "node:crypto";
|
|
@@ -71872,9 +71945,12 @@ var USAGE = `himi \u2014 the Himalaya authoring CLI (remote dev loop)
|
|
|
71872
71945
|
file is not the plane's per-release archive.
|
|
71873
71946
|
himi share --file <package.himi> [--expires-at <ISO>] | revoke --app <app> <shareId>
|
|
71874
71947
|
publish a content-addressed package link for a released app
|
|
71875
|
-
himi mcp [plugin --out <dir>]
|
|
71948
|
+
himi mcp [plugin --out <dir>] [--with-files]
|
|
71949
|
+
the himi tools as a local MCP server over stdio, for coding
|
|
71876
71950
|
agents (Claude Code, Cursor, Codex, VS Code). \`himi init\`
|
|
71877
|
-
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).
|
|
71878
71954
|
himi run [<package.himi|dir|url>] [--browser] [--watch [--idle-exit <min>]] [--lan] [--port <n>] [--chrome preview|app] [--screen <id>] [--surface <id>] [--no-open]
|
|
71879
71955
|
serve a package on localhost + open it in a browser. No serve
|
|
71880
71956
|
plane, no dev-server. With no argument it packs the current
|
|
@@ -72475,7 +72551,7 @@ function readUpdateCache(path2) {
|
|
|
72475
72551
|
function writeUpdateCache(path2, cache) {
|
|
72476
72552
|
try {
|
|
72477
72553
|
mkdirSync33(dirname37(path2), { recursive: true, mode: 448 });
|
|
72478
|
-
|
|
72554
|
+
writeFileSync29(path2, JSON.stringify(cache, null, 2) + "\n", { mode: 384 });
|
|
72479
72555
|
} catch {
|
|
72480
72556
|
}
|
|
72481
72557
|
}
|
|
@@ -72506,7 +72582,7 @@ function globalPrefixWritable() {
|
|
|
72506
72582
|
const root = globalNodeModulesRoot();
|
|
72507
72583
|
if (!root) return false;
|
|
72508
72584
|
try {
|
|
72509
|
-
accessSync(root,
|
|
72585
|
+
accessSync(root, constants3.W_OK);
|
|
72510
72586
|
return true;
|
|
72511
72587
|
} catch {
|
|
72512
72588
|
return false;
|
|
@@ -72617,7 +72693,7 @@ function acquireUpdateLock(cachePath, now, hooks = {}) {
|
|
|
72617
72693
|
};
|
|
72618
72694
|
const tryCreate = (path2) => {
|
|
72619
72695
|
try {
|
|
72620
|
-
|
|
72696
|
+
writeFileSync29(path2, mine, { flag: "wx", mode: 384 });
|
|
72621
72697
|
return true;
|
|
72622
72698
|
} catch {
|
|
72623
72699
|
return false;
|
|
@@ -73205,7 +73281,7 @@ function executableOnPath(name, env = process.env) {
|
|
|
73205
73281
|
const candidate = join65(dir, `${name}${suffix}`);
|
|
73206
73282
|
try {
|
|
73207
73283
|
if (!statSync19(candidate).isFile()) continue;
|
|
73208
|
-
if (process.platform !== "win32") accessSync(candidate,
|
|
73284
|
+
if (process.platform !== "win32") accessSync(candidate, constants3.X_OK);
|
|
73209
73285
|
return true;
|
|
73210
73286
|
} catch {
|
|
73211
73287
|
}
|
|
@@ -73332,7 +73408,7 @@ function readConfigSource(dir) {
|
|
|
73332
73408
|
}
|
|
73333
73409
|
function writePngFile(path2, base64) {
|
|
73334
73410
|
mkdirSync33(dirname37(path2), { recursive: true });
|
|
73335
|
-
|
|
73411
|
+
writeFileSync29(path2, Buffer.from(base64, "base64"));
|
|
73336
73412
|
return path2;
|
|
73337
73413
|
}
|
|
73338
73414
|
async function seedAgentFilesFor(dir, flags, mode = "init") {
|
|
@@ -74955,7 +75031,7 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
|
|
|
74955
75031
|
const files = himiPluginFiles2(skills.skillMd);
|
|
74956
75032
|
for (const f of files) {
|
|
74957
75033
|
mkdirSync33(dirname37(join65(dir, f.path)), { recursive: true });
|
|
74958
|
-
|
|
75034
|
+
writeFileSync29(join65(dir, f.path), f.text);
|
|
74959
75035
|
}
|
|
74960
75036
|
print({ ok: true, dir, files: files.map((f) => f.path), next: [`claude plugin validate ${dir}`, `claude --plugin-dir ${dir}`] });
|
|
74961
75037
|
return 0;
|
|
@@ -74981,7 +75057,7 @@ ${doNow.map((l) => ` ${l}`).join("\n")}`);
|
|
|
74981
75057
|
const out2 = resolve53(str8(flags.out) ?? join65(dir, ".himi", "making-of.html"));
|
|
74982
75058
|
const { html, plan } = buildReel2(dir);
|
|
74983
75059
|
mkdirSync33(dirname37(out2), { recursive: true });
|
|
74984
|
-
|
|
75060
|
+
writeFileSync29(out2, html);
|
|
74985
75061
|
const { pathToFileURL: pathToFileURL9 } = await import("node:url");
|
|
74986
75062
|
const url2 = pathToFileURL9(out2).href;
|
|
74987
75063
|
const open2 = flags["no-open"] !== true;
|
|
@@ -77201,9 +77277,9 @@ default (no --glyph): ${DEFAULT_GLYPH2} \u2014 a mark that claims nothing, so th
|
|
|
77201
77277
|
const { describeChanges: describeChanges2, headline: headline2 } = await Promise.resolve().then(() => (init_build_changes(), build_changes_exports));
|
|
77202
77278
|
const changes = describeChanges2(live2, join65(next, running.app), diff);
|
|
77203
77279
|
const retired = `${live2}.retired-${Date.now()}`;
|
|
77204
|
-
|
|
77280
|
+
renameSync8(live2, retired);
|
|
77205
77281
|
try {
|
|
77206
|
-
|
|
77282
|
+
renameSync8(join65(next, running.app), live2);
|
|
77207
77283
|
} catch {
|
|
77208
77284
|
cpSync3(join65(next, running.app), live2, { recursive: true });
|
|
77209
77285
|
}
|
package/guides/himi-mcp.md
CHANGED
|
@@ -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
|
|
87
|
-
|
|
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
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wix/himalaya-cli",
|
|
3
|
-
"version": "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": "
|
|
70
|
+
"falconPackageHash": "fc5b0cacba000b4c6e6a4a5002e19ab4d37d0560e6bd98aed8b49b13"
|
|
71
71
|
}
|