lavish-axi 0.1.83 → 0.1.84
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 +134 -75
- package/package.json +1 -1
- package/plugin.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -21,7 +21,7 @@ import { access, readFile as readFile6, writeFile as writeFile4 } from "node:fs/
|
|
|
21
21
|
import { get as httpGet2 } from "node:http";
|
|
22
22
|
import { isIP as isIP4 } from "node:net";
|
|
23
23
|
import os4 from "node:os";
|
|
24
|
-
import
|
|
24
|
+
import path9 from "node:path";
|
|
25
25
|
import { fileURLToPath as fileURLToPath4 } from "node:url";
|
|
26
26
|
import { AxiError, installSessionStartHooks, RESERVED_COMMANDS, runAxiCli } from "axi-sdk-js";
|
|
27
27
|
|
|
@@ -4427,7 +4427,7 @@ function htmlAppRequestError(message, details = {}) {
|
|
|
4427
4427
|
if (status !== void 0) Object.defineProperty(error, "status", { value: status, enumerable: true });
|
|
4428
4428
|
return error;
|
|
4429
4429
|
}
|
|
4430
|
-
async function requestHtmlApp({ method, path:
|
|
4430
|
+
async function requestHtmlApp({ method, path: path10, body, bearer, options, env, action = "publish" }) {
|
|
4431
4431
|
const apiUrl = (options.apiUrl ? String(options.apiUrl).replace(/\/+$/, "") : "") || htmlAppApiUrl(env);
|
|
4432
4432
|
const fetchImpl = options.fetch || fetch;
|
|
4433
4433
|
const headers = { "content-type": "application/json", "user-agent": "lavish-axi" };
|
|
@@ -4437,7 +4437,7 @@ async function requestHtmlApp({ method, path: path9, body, bearer, options, env,
|
|
|
4437
4437
|
let response;
|
|
4438
4438
|
let text;
|
|
4439
4439
|
try {
|
|
4440
|
-
response = await fetchImpl(`${apiUrl}${
|
|
4440
|
+
response = await fetchImpl(`${apiUrl}${path10}`, {
|
|
4441
4441
|
method,
|
|
4442
4442
|
headers,
|
|
4443
4443
|
body: JSON.stringify(body),
|
|
@@ -4704,10 +4704,10 @@ function writeTextFileAtomically(file, content, operations = {}) {
|
|
|
4704
4704
|
const rename3 = operations.renameSync || renameSync;
|
|
4705
4705
|
const remove = operations.rmSync || rmSync;
|
|
4706
4706
|
const chmod2 = operations.chmodSync || chmodSync;
|
|
4707
|
-
const
|
|
4707
|
+
const stat4 = operations.statSync || statSync;
|
|
4708
4708
|
let mode;
|
|
4709
4709
|
try {
|
|
4710
|
-
mode =
|
|
4710
|
+
mode = stat4(file).mode & 511;
|
|
4711
4711
|
} catch {
|
|
4712
4712
|
}
|
|
4713
4713
|
try {
|
|
@@ -4829,11 +4829,11 @@ function attrValue(attrs, name) {
|
|
|
4829
4829
|
import crypto6 from "node:crypto";
|
|
4830
4830
|
import { EventEmitter } from "node:events";
|
|
4831
4831
|
import { existsSync as existsSync2 } from "node:fs";
|
|
4832
|
-
import { readFile as readFile5, realpath as
|
|
4832
|
+
import { readFile as readFile5, realpath as realpath4 } from "node:fs/promises";
|
|
4833
4833
|
import { createServer, get as httpGet } from "node:http";
|
|
4834
4834
|
import { isIP as isIP3 } from "node:net";
|
|
4835
4835
|
import { homedir } from "node:os";
|
|
4836
|
-
import
|
|
4836
|
+
import path8 from "node:path";
|
|
4837
4837
|
import { fileURLToPath as fileURLToPath3 } from "node:url";
|
|
4838
4838
|
import chokidar from "chokidar";
|
|
4839
4839
|
import express from "express";
|
|
@@ -5925,15 +5925,15 @@ function createArtifactSdk(deriveQueueKey, isNativeInteractive = isNativeInterac
|
|
|
5925
5925
|
return node.parentElement || document.body;
|
|
5926
5926
|
}
|
|
5927
5927
|
function nodePath(node, root) {
|
|
5928
|
-
const
|
|
5928
|
+
const path10 = [];
|
|
5929
5929
|
let current = node;
|
|
5930
5930
|
while (current && current !== root) {
|
|
5931
5931
|
const parentNode = current.parentNode;
|
|
5932
5932
|
if (!parentNode) break;
|
|
5933
|
-
|
|
5933
|
+
path10.unshift([...parentNode.childNodes].indexOf(current));
|
|
5934
5934
|
current = parentNode;
|
|
5935
5935
|
}
|
|
5936
|
-
return
|
|
5936
|
+
return path10;
|
|
5937
5937
|
}
|
|
5938
5938
|
function rangeBoundary(node, offset) {
|
|
5939
5939
|
const el = closestElement(node);
|
|
@@ -8459,10 +8459,53 @@ function isIncompleteRunningTailscaleStatus(raw) {
|
|
|
8459
8459
|
return addresses.some((address) => typeof address === "string" && isIP2(address.trim()) === 4);
|
|
8460
8460
|
}
|
|
8461
8461
|
|
|
8462
|
+
// src/artifact-path.js
|
|
8463
|
+
import { realpath as realpath2, stat as stat2 } from "node:fs/promises";
|
|
8464
|
+
import path5 from "node:path";
|
|
8465
|
+
var ARTIFACT_HTML_ERROR = "Lavish Editor expects an HTML file";
|
|
8466
|
+
function isHtmlPath(file) {
|
|
8467
|
+
const lower = String(file || "").toLowerCase();
|
|
8468
|
+
return lower.endsWith(".html") || lower.endsWith(".htm");
|
|
8469
|
+
}
|
|
8470
|
+
function artifactTreeRoot(file) {
|
|
8471
|
+
return path5.dirname(file);
|
|
8472
|
+
}
|
|
8473
|
+
var ArtifactPathError = class extends Error {
|
|
8474
|
+
/** @type {"VALIDATION_ERROR"} */
|
|
8475
|
+
code = "VALIDATION_ERROR";
|
|
8476
|
+
/** @type {400} */
|
|
8477
|
+
statusCode = 400;
|
|
8478
|
+
/**
|
|
8479
|
+
* @param {string} [message]
|
|
8480
|
+
*/
|
|
8481
|
+
constructor(message = ARTIFACT_HTML_ERROR) {
|
|
8482
|
+
super(message);
|
|
8483
|
+
this.name = "ArtifactPathError";
|
|
8484
|
+
}
|
|
8485
|
+
};
|
|
8486
|
+
function isArtifactPathError(error) {
|
|
8487
|
+
return error instanceof Error && error.name === "ArtifactPathError";
|
|
8488
|
+
}
|
|
8489
|
+
async function resolveAllowedArtifactFile(file) {
|
|
8490
|
+
const requested = typeof file === "string" ? file : "";
|
|
8491
|
+
if (!isHtmlPath(requested)) {
|
|
8492
|
+
throw new ArtifactPathError();
|
|
8493
|
+
}
|
|
8494
|
+
const canonical = await realpath2(path5.resolve(requested));
|
|
8495
|
+
if (!isHtmlPath(canonical)) {
|
|
8496
|
+
throw new ArtifactPathError();
|
|
8497
|
+
}
|
|
8498
|
+
const info = await stat2(canonical);
|
|
8499
|
+
if (!info.isFile()) {
|
|
8500
|
+
throw new ArtifactPathError();
|
|
8501
|
+
}
|
|
8502
|
+
return canonical;
|
|
8503
|
+
}
|
|
8504
|
+
|
|
8462
8505
|
// src/session-store.js
|
|
8463
8506
|
import crypto4 from "node:crypto";
|
|
8464
|
-
import { readFile as readFile3, realpath as
|
|
8465
|
-
import
|
|
8507
|
+
import { readFile as readFile3, realpath as realpath3, writeFile as writeFile2 } from "node:fs/promises";
|
|
8508
|
+
import path6 from "node:path";
|
|
8466
8509
|
|
|
8467
8510
|
// src/async-mutex.js
|
|
8468
8511
|
var AsyncMutex = class {
|
|
@@ -9118,8 +9161,8 @@ var SessionStore = class {
|
|
|
9118
9161
|
}
|
|
9119
9162
|
};
|
|
9120
9163
|
async function canonicalFile(file) {
|
|
9121
|
-
const absolute =
|
|
9122
|
-
return
|
|
9164
|
+
const absolute = path6.resolve(file);
|
|
9165
|
+
return realpath3(absolute);
|
|
9123
9166
|
}
|
|
9124
9167
|
function sessionKey(file) {
|
|
9125
9168
|
return crypto4.createHash("sha256").update(file).digest("hex").slice(0, 16);
|
|
@@ -9359,8 +9402,8 @@ function generateSharePassword() {
|
|
|
9359
9402
|
|
|
9360
9403
|
// src/attachment-store.js
|
|
9361
9404
|
import crypto5 from "node:crypto";
|
|
9362
|
-
import { chmod, mkdir as mkdir3, readdir, readFile as readFile4, rename as rename2, rm as rm2, stat as
|
|
9363
|
-
import
|
|
9405
|
+
import { chmod, mkdir as mkdir3, readdir, readFile as readFile4, rename as rename2, rm as rm2, stat as stat3, utimes, writeFile as writeFile3 } from "node:fs/promises";
|
|
9406
|
+
import path7 from "node:path";
|
|
9364
9407
|
var KEY_RE2 = /^[0-9a-f]{16}$/;
|
|
9365
9408
|
var ID_RE = /^[0-9a-f]{64}\.(png|jpg|webp)$/;
|
|
9366
9409
|
var MIME_BY_EXT = { png: "image/png", jpg: "image/jpeg", webp: "image/webp" };
|
|
@@ -9388,10 +9431,10 @@ function isValidAttachmentId(id) {
|
|
|
9388
9431
|
return ID_RE.test(String(id || ""));
|
|
9389
9432
|
}
|
|
9390
9433
|
function attachmentsDir(stateDir2, key) {
|
|
9391
|
-
return
|
|
9434
|
+
return path7.join(stateDir2, "attachments", String(key));
|
|
9392
9435
|
}
|
|
9393
9436
|
function attachmentFile(stateDir2, key, id) {
|
|
9394
|
-
return
|
|
9437
|
+
return path7.join(attachmentsDir(stateDir2, key), id);
|
|
9395
9438
|
}
|
|
9396
9439
|
function resolveAttachmentConfig(env = process.env) {
|
|
9397
9440
|
const maxDiskBytes = diskCapEnv(env.LAVISH_AXI_MAX_ATTACHMENT_DISK_MB);
|
|
@@ -9526,7 +9569,7 @@ function buildMetadata(id, file, mime, bytes, dims) {
|
|
|
9526
9569
|
}
|
|
9527
9570
|
async function pathExists(file) {
|
|
9528
9571
|
try {
|
|
9529
|
-
await
|
|
9572
|
+
await stat3(file);
|
|
9530
9573
|
return true;
|
|
9531
9574
|
} catch (error) {
|
|
9532
9575
|
if (error && error.code === "ENOENT") return false;
|
|
@@ -9566,7 +9609,7 @@ async function writeFileAtomically2(file, content) {
|
|
|
9566
9609
|
}
|
|
9567
9610
|
}
|
|
9568
9611
|
async function ensureAttachmentDir(stateDir2, key) {
|
|
9569
|
-
const root =
|
|
9612
|
+
const root = path7.join(stateDir2, "attachments");
|
|
9570
9613
|
const dir = attachmentsDir(stateDir2, key);
|
|
9571
9614
|
await mkdir3(dir, { recursive: true, mode: ATTACHMENT_DIR_MODE });
|
|
9572
9615
|
for (const target of [root, dir]) {
|
|
@@ -9647,7 +9690,7 @@ async function resolveAttachment(stateDir2, key, id) {
|
|
|
9647
9690
|
const file = attachmentFile(stateDir2, key, id);
|
|
9648
9691
|
let info;
|
|
9649
9692
|
try {
|
|
9650
|
-
info = await
|
|
9693
|
+
info = await stat3(file);
|
|
9651
9694
|
} catch (error) {
|
|
9652
9695
|
if (error && error.code === "ENOENT") return null;
|
|
9653
9696
|
throw error;
|
|
@@ -9661,7 +9704,7 @@ async function statAttachmentForServe(stateDir2, key, id) {
|
|
|
9661
9704
|
if (!isValidAttachmentKey(key) || !isValidAttachmentId(id)) return null;
|
|
9662
9705
|
const file = attachmentFile(stateDir2, key, id);
|
|
9663
9706
|
try {
|
|
9664
|
-
const info = await
|
|
9707
|
+
const info = await stat3(file);
|
|
9665
9708
|
if (!info.isFile()) return null;
|
|
9666
9709
|
} catch (error) {
|
|
9667
9710
|
if (error && error.code === "ENOENT") return null;
|
|
@@ -9690,20 +9733,20 @@ async function removeAttachment(stateDir2, key, id) {
|
|
|
9690
9733
|
}
|
|
9691
9734
|
}
|
|
9692
9735
|
async function listAttachments(stateDir2) {
|
|
9693
|
-
const root =
|
|
9736
|
+
const root = path7.join(stateDir2, "attachments");
|
|
9694
9737
|
const sessionDirs = await readdirSafe(root);
|
|
9695
9738
|
const files = [];
|
|
9696
9739
|
for (const dirent of sessionDirs) {
|
|
9697
9740
|
if (!dirent.isDirectory() || !isValidAttachmentKey(dirent.name)) continue;
|
|
9698
|
-
const dir =
|
|
9741
|
+
const dir = path7.join(root, dirent.name);
|
|
9699
9742
|
for (const entry of await readdirSafe(dir)) {
|
|
9700
9743
|
if (!entry.isFile() || !isValidAttachmentId(entry.name)) continue;
|
|
9701
|
-
const filePath =
|
|
9744
|
+
const filePath = path7.join(dir, entry.name);
|
|
9702
9745
|
try {
|
|
9703
|
-
const info = await
|
|
9746
|
+
const info = await stat3(filePath);
|
|
9704
9747
|
let sidecarBytes = 0;
|
|
9705
9748
|
try {
|
|
9706
|
-
sidecarBytes = (await
|
|
9749
|
+
sidecarBytes = (await stat3(sidecarPath(filePath))).size;
|
|
9707
9750
|
} catch {
|
|
9708
9751
|
}
|
|
9709
9752
|
files.push({
|
|
@@ -9780,19 +9823,19 @@ async function sweepAttachments(stateDir2, options = {}) {
|
|
|
9780
9823
|
const orphans = await reapOrphanFiles(stateDir2, now);
|
|
9781
9824
|
deleted += orphans.deleted;
|
|
9782
9825
|
freedBytes += orphans.freedBytes;
|
|
9783
|
-
await pruneEmptyDirs(
|
|
9826
|
+
await pruneEmptyDirs(path7.join(stateDir2, "attachments"));
|
|
9784
9827
|
return { deleted, freedBytes };
|
|
9785
9828
|
}
|
|
9786
9829
|
async function committedChargedBytes(stateDir2) {
|
|
9787
|
-
const root =
|
|
9830
|
+
const root = path7.join(stateDir2, "attachments");
|
|
9788
9831
|
let total = 0;
|
|
9789
9832
|
for (const dirent of await readdirSafe(root)) {
|
|
9790
9833
|
if (!dirent.isDirectory() || !isValidAttachmentKey(dirent.name)) continue;
|
|
9791
|
-
const dir =
|
|
9834
|
+
const dir = path7.join(root, dirent.name);
|
|
9792
9835
|
for (const entry of await readdirSafe(dir)) {
|
|
9793
9836
|
if (!entry.isFile()) continue;
|
|
9794
9837
|
try {
|
|
9795
|
-
total += allocatedBytes((await
|
|
9838
|
+
total += allocatedBytes((await stat3(path7.join(dir, entry.name))).size);
|
|
9796
9839
|
} catch {
|
|
9797
9840
|
}
|
|
9798
9841
|
}
|
|
@@ -9800,28 +9843,28 @@ async function committedChargedBytes(stateDir2) {
|
|
|
9800
9843
|
return total;
|
|
9801
9844
|
}
|
|
9802
9845
|
async function reapOrphanFiles(stateDir2, now) {
|
|
9803
|
-
const root =
|
|
9846
|
+
const root = path7.join(stateDir2, "attachments");
|
|
9804
9847
|
let deleted = 0;
|
|
9805
9848
|
let freedBytes = 0;
|
|
9806
9849
|
for (const dirent of await readdirSafe(root)) {
|
|
9807
9850
|
if (!dirent.isDirectory() || !isValidAttachmentKey(dirent.name)) continue;
|
|
9808
|
-
const dir =
|
|
9851
|
+
const dir = path7.join(root, dirent.name);
|
|
9809
9852
|
const entries = await readdirSafe(dir);
|
|
9810
9853
|
const present = new Set(entries.filter((e) => e.isFile()).map((e) => e.name));
|
|
9811
9854
|
for (const entry of entries) {
|
|
9812
9855
|
if (!entry.isFile()) continue;
|
|
9813
|
-
const filePath =
|
|
9856
|
+
const filePath = path7.join(dir, entry.name);
|
|
9814
9857
|
const sidecarMatch = SIDECAR_ORPHAN_RE.exec(entry.name);
|
|
9815
9858
|
try {
|
|
9816
9859
|
if (TEMP_FILE_RE.test(entry.name)) {
|
|
9817
|
-
const info = await
|
|
9860
|
+
const info = await stat3(filePath);
|
|
9818
9861
|
if (now - info.mtimeMs <= ATTACHMENT_TEMP_GRACE_MS) continue;
|
|
9819
9862
|
if (await removeFile(filePath)) {
|
|
9820
9863
|
deleted += 1;
|
|
9821
9864
|
freedBytes += info.size;
|
|
9822
9865
|
}
|
|
9823
9866
|
} else if (sidecarMatch && !present.has(sidecarMatch[1])) {
|
|
9824
|
-
const info = await
|
|
9867
|
+
const info = await stat3(filePath);
|
|
9825
9868
|
if (await removeFile(filePath)) {
|
|
9826
9869
|
deleted += 1;
|
|
9827
9870
|
freedBytes += info.size;
|
|
@@ -9854,7 +9897,7 @@ async function removeFile(file) {
|
|
|
9854
9897
|
async function pruneEmptyDirs(root) {
|
|
9855
9898
|
for (const dirent of await readdirSafe(root)) {
|
|
9856
9899
|
if (!dirent.isDirectory()) continue;
|
|
9857
|
-
const dir =
|
|
9900
|
+
const dir = path7.join(root, dirent.name);
|
|
9858
9901
|
try {
|
|
9859
9902
|
if ((await readdir(dir)).length === 0) await rm2(dir, { recursive: true, force: true });
|
|
9860
9903
|
} catch {
|
|
@@ -10233,7 +10276,7 @@ async function serve({
|
|
|
10233
10276
|
if (pendingAfterRestore) events.emit("feedback", key);
|
|
10234
10277
|
await publishChatSync(key);
|
|
10235
10278
|
}
|
|
10236
|
-
const whiteboardStateRoot =
|
|
10279
|
+
const whiteboardStateRoot = path8.dirname(stateFile2);
|
|
10237
10280
|
const allowedHostnames = buildAllowedHostnames({
|
|
10238
10281
|
host: requestedListenHosts[0],
|
|
10239
10282
|
hosts: [...requestedListenHosts, ...listenHosts],
|
|
@@ -10265,6 +10308,11 @@ async function serve({
|
|
|
10265
10308
|
workingUrl: workingUrlFor(req, { includeSessionPath: false })
|
|
10266
10309
|
});
|
|
10267
10310
|
}
|
|
10311
|
+
function refuseNonHtmlArtifact(session, res) {
|
|
10312
|
+
if (isHtmlPath(session.file)) return false;
|
|
10313
|
+
res.status(403).send("Forbidden");
|
|
10314
|
+
return true;
|
|
10315
|
+
}
|
|
10268
10316
|
if (!allowAnyHostname) {
|
|
10269
10317
|
app.use((req, res, next) => {
|
|
10270
10318
|
const requestHost = { host: req.headers.host, forwardedHost: req.headers["x-forwarded-host"] };
|
|
@@ -10295,7 +10343,7 @@ async function serve({
|
|
|
10295
10343
|
next();
|
|
10296
10344
|
});
|
|
10297
10345
|
const attachmentConfig = resolveAttachmentConfig();
|
|
10298
|
-
const attachmentStateRoot =
|
|
10346
|
+
const attachmentStateRoot = path8.dirname(stateFile2);
|
|
10299
10347
|
const defaultJsonParser = express.json({ limit: "2mb" });
|
|
10300
10348
|
const whiteboardJsonParser = express.json({ limit: "20mb" });
|
|
10301
10349
|
app.use((req, res, next) => {
|
|
@@ -10318,7 +10366,7 @@ async function serve({
|
|
|
10318
10366
|
app: "lavish-axi",
|
|
10319
10367
|
version,
|
|
10320
10368
|
state_id: serverStateId,
|
|
10321
|
-
state_dir:
|
|
10369
|
+
state_dir: path8.dirname(stateFile2),
|
|
10322
10370
|
// Where this process is listening and every address it was asked to serve (bound or still
|
|
10323
10371
|
// retrying). The CLI uses both to find one daemon per port whatever host it was configured
|
|
10324
10372
|
// with, and to tell a same-port daemon at another address apart from this one.
|
|
@@ -10346,7 +10394,7 @@ async function serve({
|
|
|
10346
10394
|
});
|
|
10347
10395
|
app.post("/api/sessions", async (req, res, next) => {
|
|
10348
10396
|
try {
|
|
10349
|
-
const file = await
|
|
10397
|
+
const file = await resolveAllowedArtifactFile(req.body?.file);
|
|
10350
10398
|
const key = sessionKey(file);
|
|
10351
10399
|
const reopen = Boolean(req.body.reopen);
|
|
10352
10400
|
const existing = await store.findByKey(key);
|
|
@@ -10378,6 +10426,14 @@ async function serve({
|
|
|
10378
10426
|
...networkWarningField()
|
|
10379
10427
|
});
|
|
10380
10428
|
} catch (error) {
|
|
10429
|
+
if (isArtifactPathError(error)) {
|
|
10430
|
+
res.status(error.statusCode).json({
|
|
10431
|
+
status: "error",
|
|
10432
|
+
code: error.code,
|
|
10433
|
+
error: error.message
|
|
10434
|
+
});
|
|
10435
|
+
return;
|
|
10436
|
+
}
|
|
10381
10437
|
next(error);
|
|
10382
10438
|
}
|
|
10383
10439
|
});
|
|
@@ -10806,8 +10862,9 @@ async function serve({
|
|
|
10806
10862
|
res.status(404).json({ error: "session not found" });
|
|
10807
10863
|
return;
|
|
10808
10864
|
}
|
|
10865
|
+
if (refuseNonHtmlArtifact(session, res)) return;
|
|
10809
10866
|
const source = await readFile5(session.file, "utf8");
|
|
10810
|
-
const root =
|
|
10867
|
+
const root = path8.dirname(session.file);
|
|
10811
10868
|
const { html, warnings } = await buildSelfContainedHtml(source, {
|
|
10812
10869
|
baseDir: root,
|
|
10813
10870
|
confineDir: root,
|
|
@@ -10834,11 +10891,12 @@ async function serve({
|
|
|
10834
10891
|
res.status(404).json({ error: "session not found" });
|
|
10835
10892
|
return;
|
|
10836
10893
|
}
|
|
10894
|
+
if (refuseNonHtmlArtifact(session, res)) return;
|
|
10837
10895
|
const body = req.body || {};
|
|
10838
10896
|
const generatePassword = body.generate_password === true;
|
|
10839
10897
|
const password = generatePassword ? generateSharePassword() : optionalBodyString(body.password);
|
|
10840
10898
|
const source = await readFile5(session.file, "utf8");
|
|
10841
|
-
const root =
|
|
10899
|
+
const root = path8.dirname(session.file);
|
|
10842
10900
|
const { html, warnings } = await buildSelfContainedHtml(source, {
|
|
10843
10901
|
baseDir: root,
|
|
10844
10902
|
confineDir: root,
|
|
@@ -10909,6 +10967,7 @@ async function serve({
|
|
|
10909
10967
|
return;
|
|
10910
10968
|
}
|
|
10911
10969
|
const session = chromeLoad.session;
|
|
10970
|
+
if (refuseNonHtmlArtifact(session, res)) return;
|
|
10912
10971
|
await watchSession(session, watchers, events, logEvent, reloadDebounceMs);
|
|
10913
10972
|
const artifactHtml = await readFile5(session.file, "utf8").catch(() => "");
|
|
10914
10973
|
const { faviconTag, title } = extractArtifactHead(artifactHtml);
|
|
@@ -10986,6 +11045,7 @@ async function serve({
|
|
|
10986
11045
|
sendSessionNotFound(req, res);
|
|
10987
11046
|
return;
|
|
10988
11047
|
}
|
|
11048
|
+
if (refuseNonHtmlArtifact(beforeRead.session, res)) return;
|
|
10989
11049
|
if (!beforeRead.valid) {
|
|
10990
11050
|
res.status(409).type("html").send(
|
|
10991
11051
|
"<!doctype html><title>Artifact load expired</title><p>This artifact load is no longer current. Reload Lavish to continue.</p>"
|
|
@@ -11015,7 +11075,8 @@ async function serve({
|
|
|
11015
11075
|
sendSessionNotFound(req, res);
|
|
11016
11076
|
return;
|
|
11017
11077
|
}
|
|
11018
|
-
|
|
11078
|
+
if (refuseNonHtmlArtifact(session, res)) return;
|
|
11079
|
+
const root = artifactTreeRoot(session.file);
|
|
11019
11080
|
const file = await resolveArtifactAsset(root, assetPath);
|
|
11020
11081
|
if (!file) {
|
|
11021
11082
|
res.status(403).send("Forbidden");
|
|
@@ -11122,6 +11183,7 @@ data: ${JSON.stringify({ reason: "server-restarted" })}
|
|
|
11122
11183
|
res.status(404).json({ error: "session not found" });
|
|
11123
11184
|
return;
|
|
11124
11185
|
}
|
|
11186
|
+
if (refuseNonHtmlArtifact(session, res)) return;
|
|
11125
11187
|
const html = await readFile5(session.file, "utf8").catch(() => "");
|
|
11126
11188
|
const sources = extractMermaidSources(html).map(({ index, source }) => ({
|
|
11127
11189
|
index,
|
|
@@ -11837,14 +11899,14 @@ function optionalBodyString(value) {
|
|
|
11837
11899
|
return trimmed || void 0;
|
|
11838
11900
|
}
|
|
11839
11901
|
async function resolveArtifactAsset(root, assetPath) {
|
|
11840
|
-
const file =
|
|
11841
|
-
const relative =
|
|
11842
|
-
if (relative.startsWith("..") ||
|
|
11902
|
+
const file = path8.resolve(root, assetPath);
|
|
11903
|
+
const relative = path8.relative(root, file);
|
|
11904
|
+
if (relative.startsWith("..") || path8.isAbsolute(relative)) {
|
|
11843
11905
|
return null;
|
|
11844
11906
|
}
|
|
11845
11907
|
let real;
|
|
11846
11908
|
try {
|
|
11847
|
-
real = await
|
|
11909
|
+
real = await realpath4(file);
|
|
11848
11910
|
} catch (error) {
|
|
11849
11911
|
if (error?.code === "ENOENT" || error?.code === "ENOTDIR") {
|
|
11850
11912
|
return file;
|
|
@@ -11853,12 +11915,12 @@ async function resolveArtifactAsset(root, assetPath) {
|
|
|
11853
11915
|
}
|
|
11854
11916
|
let realRoot;
|
|
11855
11917
|
try {
|
|
11856
|
-
realRoot = await
|
|
11918
|
+
realRoot = await realpath4(root);
|
|
11857
11919
|
} catch {
|
|
11858
|
-
realRoot =
|
|
11920
|
+
realRoot = path8.resolve(root);
|
|
11859
11921
|
}
|
|
11860
|
-
const relativeReal =
|
|
11861
|
-
if (relativeReal === ".." || relativeReal.startsWith(`..${
|
|
11922
|
+
const relativeReal = path8.relative(realRoot, real);
|
|
11923
|
+
if (relativeReal === ".." || relativeReal.startsWith(`..${path8.sep}`) || path8.isAbsolute(relativeReal)) {
|
|
11862
11924
|
return null;
|
|
11863
11925
|
}
|
|
11864
11926
|
return real;
|
|
@@ -11894,7 +11956,7 @@ async function resolveWatchTarget(session) {
|
|
|
11894
11956
|
const html = await readFile5(session.file, "utf8");
|
|
11895
11957
|
if (hasLiveReloadRootOptIn(html)) {
|
|
11896
11958
|
return {
|
|
11897
|
-
path:
|
|
11959
|
+
path: path8.dirname(session.file),
|
|
11898
11960
|
scope: "directory",
|
|
11899
11961
|
options: {
|
|
11900
11962
|
...baseOptions,
|
|
@@ -12348,9 +12410,9 @@ var HttpTelemetryClient = class {
|
|
|
12348
12410
|
if (!trimmed) return;
|
|
12349
12411
|
this.send(trimmed, eventURL(this.app, trimmed), fields);
|
|
12350
12412
|
}
|
|
12351
|
-
pageview(
|
|
12413
|
+
pageview(path10, fields = {}) {
|
|
12352
12414
|
if (this.closed) return;
|
|
12353
|
-
this.send("", normalizePagePath(
|
|
12415
|
+
this.send("", normalizePagePath(path10), fields);
|
|
12354
12416
|
}
|
|
12355
12417
|
async close(timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS) {
|
|
12356
12418
|
this.closed = true;
|
|
@@ -12425,8 +12487,8 @@ function eventURL(app, name) {
|
|
|
12425
12487
|
if (!name) return `app://${app}`;
|
|
12426
12488
|
return `app://${app}/${name.replace(/\./g, "/")}`;
|
|
12427
12489
|
}
|
|
12428
|
-
function normalizePagePath(
|
|
12429
|
-
const trimmed = String(
|
|
12490
|
+
function normalizePagePath(path10) {
|
|
12491
|
+
const trimmed = String(path10 || "").trim();
|
|
12430
12492
|
if (!trimmed) return "/";
|
|
12431
12493
|
return trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
12432
12494
|
}
|
|
@@ -12468,7 +12530,7 @@ var AGENT_REPLY_INPUT_LIMIT_BYTES = AGENT_REPLY_JSON_LIMIT_BYTES - AGENT_REPLY_J
|
|
|
12468
12530
|
var AGENT_REPLY_LIMIT_LABEL = "2 MB JSON request limit";
|
|
12469
12531
|
var POLL_STATE_HEADER = "lavish-poll-state";
|
|
12470
12532
|
var CODEX_POLL_WAKE_PATH_GUIDANCE = "Codex detected: completed background tasks may not resume Codex automatically, so keep the poll attached to the active turn.";
|
|
12471
|
-
var VERSION = "0.1.
|
|
12533
|
+
var VERSION = "0.1.84";
|
|
12472
12534
|
function detectInvokingAgent(env = process.env) {
|
|
12473
12535
|
return ["CODEX_SANDBOX", "CODEX_THREAD_ID"].some((key) => Object.hasOwn(env, key)) ? "codex" : "generic";
|
|
12474
12536
|
}
|
|
@@ -12946,8 +13008,8 @@ async function exportCommand(args) {
|
|
|
12946
13008
|
}
|
|
12947
13009
|
await assertHtmlFile(file);
|
|
12948
13010
|
const absolute = await canonicalFile(file);
|
|
12949
|
-
const root =
|
|
12950
|
-
const output =
|
|
13011
|
+
const root = path9.dirname(absolute);
|
|
13012
|
+
const output = path9.resolve(flagValue(args, "--out") || path9.join(root, exportFileName(absolute)));
|
|
12951
13013
|
const source = await readFile6(absolute, "utf8");
|
|
12952
13014
|
const { html, warnings } = await buildSelfContainedHtml(source, {
|
|
12953
13015
|
baseDir: root,
|
|
@@ -13002,7 +13064,7 @@ async function shareCommand(args) {
|
|
|
13002
13064
|
}
|
|
13003
13065
|
await assertHtmlFile(request.file);
|
|
13004
13066
|
const absolute = await canonicalFile(request.file);
|
|
13005
|
-
const root =
|
|
13067
|
+
const root = path9.dirname(absolute);
|
|
13006
13068
|
const source = await readFile6(absolute, "utf8");
|
|
13007
13069
|
const { html, warnings } = await buildSelfContainedHtml(source, {
|
|
13008
13070
|
baseDir: root,
|
|
@@ -13425,7 +13487,7 @@ async function setupPluginCommand() {
|
|
|
13425
13487
|
const manifest = readPluginManifest(pluginRoot);
|
|
13426
13488
|
if (!manifest) {
|
|
13427
13489
|
throw new AxiError("No plugin.json found in the lavish-axi package", "SERVER_ERROR", [
|
|
13428
|
-
`Expected a manifest at ${
|
|
13490
|
+
`Expected a manifest at ${path9.join(pluginRoot, "plugin.json")}`,
|
|
13429
13491
|
"Reinstall lavish-axi, or run `npm run build:plugin` when working from a source checkout"
|
|
13430
13492
|
]);
|
|
13431
13493
|
}
|
|
@@ -13449,7 +13511,7 @@ function collapseHome(target) {
|
|
|
13449
13511
|
}
|
|
13450
13512
|
function registerVsCodePlugin(pluginRoot, pluginName) {
|
|
13451
13513
|
const settingsFile = resolveVsCodeSettingsFile(process.env, resolveHookHomeDir());
|
|
13452
|
-
const settingsDir =
|
|
13514
|
+
const settingsDir = path9.dirname(settingsFile);
|
|
13453
13515
|
const hasSettingsFile = existsSync3(settingsFile);
|
|
13454
13516
|
if (!hasSettingsFile && !existsSync3(settingsDir)) {
|
|
13455
13517
|
return { client: "vscode", status: "absent", detail: "no VS Code user configuration found" };
|
|
@@ -13478,7 +13540,7 @@ function registerVsCodePlugin(pluginRoot, pluginName) {
|
|
|
13478
13540
|
return { client: "vscode", status: "registered", detail: collapseHome(settingsFile) };
|
|
13479
13541
|
}
|
|
13480
13542
|
function registerCursorPlugin(pluginRoot, pluginName) {
|
|
13481
|
-
const cursorDir =
|
|
13543
|
+
const cursorDir = path9.join(resolveHookHomeDir(), ".cursor");
|
|
13482
13544
|
if (!existsSync3(cursorDir)) {
|
|
13483
13545
|
return { client: "cursor", status: "absent", detail: "no ~/.cursor directory found" };
|
|
13484
13546
|
}
|
|
@@ -13551,9 +13613,9 @@ function parseCopilotPluginRecords(output) {
|
|
|
13551
13613
|
}
|
|
13552
13614
|
}
|
|
13553
13615
|
function installedCopilotPluginSourcePath(pluginName) {
|
|
13554
|
-
const configDir = process.env.COPILOT_HOME ||
|
|
13616
|
+
const configDir = process.env.COPILOT_HOME || path9.join(resolveHookHomeDir(), ".copilot");
|
|
13555
13617
|
try {
|
|
13556
|
-
const config = JSON.parse(readFileSync2(
|
|
13618
|
+
const config = JSON.parse(readFileSync2(path9.join(configDir, "config.json"), "utf8"));
|
|
13557
13619
|
const record = Array.isArray(config.installedPlugins) ? config.installedPlugins.find((candidate) => candidate?.name === pluginName) : null;
|
|
13558
13620
|
return record ? copilotPluginSourcePath(record) : null;
|
|
13559
13621
|
} catch {
|
|
@@ -13578,7 +13640,7 @@ function copilotPluginSourcePath(record) {
|
|
|
13578
13640
|
continue;
|
|
13579
13641
|
}
|
|
13580
13642
|
}
|
|
13581
|
-
if (
|
|
13643
|
+
if (path9.isAbsolute(candidate)) return candidate;
|
|
13582
13644
|
}
|
|
13583
13645
|
return null;
|
|
13584
13646
|
}
|
|
@@ -13586,14 +13648,14 @@ function sameResolvedPath(left, right) {
|
|
|
13586
13648
|
try {
|
|
13587
13649
|
return realpathSync2(left) === realpathSync2(right);
|
|
13588
13650
|
} catch {
|
|
13589
|
-
return
|
|
13651
|
+
return path9.resolve(left) === path9.resolve(right);
|
|
13590
13652
|
}
|
|
13591
13653
|
}
|
|
13592
13654
|
function resolveHookHomeDir(env = process.env, fallback = os4.homedir()) {
|
|
13593
13655
|
return env.HOME || fallback;
|
|
13594
13656
|
}
|
|
13595
13657
|
function resolveCopilotHookDir(env = process.env, homeDir = resolveHookHomeDir(env)) {
|
|
13596
|
-
return
|
|
13658
|
+
return path9.join(env.COPILOT_HOME || path9.join(homeDir, ".copilot"), "hooks");
|
|
13597
13659
|
}
|
|
13598
13660
|
function createCopilotCliAmbientContextScript(command = "lavish-axi") {
|
|
13599
13661
|
return [
|
|
@@ -13642,9 +13704,9 @@ function installCopilotCliSessionStartHook({
|
|
|
13642
13704
|
timeoutSec = 10,
|
|
13643
13705
|
onError = void 0
|
|
13644
13706
|
} = {}) {
|
|
13645
|
-
const target =
|
|
13707
|
+
const target = path9.join(hookDir, "lavish-axi.json");
|
|
13646
13708
|
try {
|
|
13647
|
-
mkdirSync2(
|
|
13709
|
+
mkdirSync2(path9.dirname(target), { recursive: true });
|
|
13648
13710
|
const current = existsSync3(target) ? JSON.parse(readFileSync2(target, "utf8")) : {};
|
|
13649
13711
|
const [updated, changed] = computeCopilotCliHookUpdate(
|
|
13650
13712
|
current,
|
|
@@ -13707,9 +13769,6 @@ async function assertHtmlFile(file) {
|
|
|
13707
13769
|
]);
|
|
13708
13770
|
}
|
|
13709
13771
|
}
|
|
13710
|
-
function isHtmlPath(file) {
|
|
13711
|
-
return file.toLowerCase().endsWith(".html") || file.toLowerCase().endsWith(".htm");
|
|
13712
|
-
}
|
|
13713
13772
|
var HEALTH_PROBE_TIMEOUT_MS = 500;
|
|
13714
13773
|
var DEFAULT_HEALTH_TIMEOUT_MS = 2e3;
|
|
13715
13774
|
var MAX_HEALTH_BODY_BYTES = 256 * 1024;
|
|
@@ -13922,7 +13981,7 @@ function serverReplacementReason(currentVersion, healthBody, forceRestart = fals
|
|
|
13922
13981
|
}
|
|
13923
13982
|
function shouldForceRestartForLocalBuild(executablePath, sourceServerExists = localSourceServerExists()) {
|
|
13924
13983
|
const localBuildEntry = fileURLToPath4(new URL("../dist/cli.mjs", import.meta.url));
|
|
13925
|
-
return sourceServerExists &&
|
|
13984
|
+
return sourceServerExists && path9.resolve(executablePath) === path9.resolve(localBuildEntry);
|
|
13926
13985
|
}
|
|
13927
13986
|
function localSourceServerExists() {
|
|
13928
13987
|
return existsSync3(fileURLToPath4(new URL("../src/server.js", import.meta.url)));
|
package/package.json
CHANGED
package/plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
|
|
3
3
|
"name": "lavish-axi",
|
|
4
|
-
"version": "0.1.
|
|
4
|
+
"version": "0.1.84",
|
|
5
5
|
"description": "HTML is the new markdown. Lavish is the new editor for your HTML artifacts.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Kun Chen",
|