lavish-axi 0.1.11 → 0.1.13
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/README.md +8 -7
- package/dist/chrome-client.js +58 -5
- package/dist/cli.mjs +109 -28
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -96,9 +96,10 @@ pnpm link
|
|
|
96
96
|
|
|
97
97
|
- **File-path identity** - Sessions are keyed by the canonical HTML file path, so agents do not need opaque IDs.
|
|
98
98
|
- **Portable artifacts** - The artifact runs in an iframe while Lavish injects a small SDK for annotations, snapshots, and feedback controls. Lavish does not inject any design system, so the saved HTML file renders identically whether you open it through `lavish-axi` or directly in a browser. Run `lavish-axi design` for a copy-pasteable CDN snippet to opt in to Tailwind CSS v4 + DaisyUI v5, or use any other design system (or none).
|
|
99
|
+
- **Local assets** - Copy local images, CSS, fonts, and scripts next to the HTML artifact and reference them with relative paths from that directory; root-prefixed paths such as `/assets/logo.png` will not resolve through Lavish's artifact route.
|
|
99
100
|
- **Live reload** - Lavish watches the HTML artifact file by default. To also reload on sibling asset changes, add `data-lavish-live-reload-root` to the root element or `<meta name="lavish-live-reload" content="root">`.
|
|
100
101
|
- **Feedback controls** - Mark buttons, choices, and other interactive elements with `data-lavish-action` so Lavish does not annotate them, then call `window.lavish.queuePrompt()` or `window.lavish.sendQueuedPrompts()` from the control handler.
|
|
101
|
-
- **Agent presence** - The browser shows when no agent is listening,
|
|
102
|
+
- **Agent presence** - The browser shows when no agent is listening, keeps queued feedback for the next successful `lavish-axi poll` send even across reloads, and only blocks sending while the agent is working on delivered feedback.
|
|
102
103
|
- **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
|
|
103
104
|
- **Local-first state** - Session state stays under `.lavish-axi/` in the workspace.
|
|
104
105
|
|
|
@@ -118,12 +119,12 @@ Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `diff`, `input`, `
|
|
|
118
119
|
|
|
119
120
|
### Flags
|
|
120
121
|
|
|
121
|
-
| Command | Flag | Description
|
|
122
|
-
| ------------------------ | --------------------- |
|
|
123
|
-
| `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window.
|
|
124
|
-
| `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat before polling again.
|
|
125
|
-
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it.
|
|
126
|
-
| `lavish-axi server` | `--verbose` | Log session and watcher events to stderr; can also be enabled with `LAVISH_AXI_DEBUG=1`. |
|
|
122
|
+
| Command | Flag | Description |
|
|
123
|
+
| ------------------------ | --------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
124
|
+
| `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
|
|
125
|
+
| `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat before polling again. |
|
|
126
|
+
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it. |
|
|
127
|
+
| `lavish-axi server` | `--verbose` | Log session and watcher events to stderr; can also be enabled with `LAVISH_AXI_DEBUG=1`. Detached server output is appended to `~/.lavish-axi/server.log` (or `LAVISH_AXI_STATE_DIR/server.log`) for startup and crash diagnostics. |
|
|
127
128
|
|
|
128
129
|
## Development
|
|
129
130
|
|
package/dist/chrome-client.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const sessionDataElement = document.getElementById("lavish-session");
|
|
4
4
|
const sessionData = JSON.parse(sessionDataElement?.textContent || "{}");
|
|
5
5
|
const key = String(sessionData.key || "");
|
|
6
|
+
const queueStorageKey = "lavish-axi:queued:" + key;
|
|
6
7
|
const initialChat = Array.isArray(sessionData.initialChat) ? sessionData.initialChat : [];
|
|
7
8
|
|
|
8
9
|
const frame = /** @type {HTMLIFrameElement} */ (document.getElementById("artifact"));
|
|
@@ -16,11 +17,13 @@ const filePathInput = /** @type {HTMLInputElement} */ (document.getElementById("
|
|
|
16
17
|
const copyPathButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyPath"));
|
|
17
18
|
const presenceBanner = /** @type {HTMLDivElement} */ (document.getElementById("presenceBanner"));
|
|
18
19
|
|
|
19
|
-
const queued =
|
|
20
|
+
const queued = loadQueuedPrompts();
|
|
20
21
|
let annotation = true;
|
|
21
22
|
let agentPresence = "waiting";
|
|
22
23
|
let pendingSnapshot = "";
|
|
23
24
|
let workingBubble = null;
|
|
25
|
+
let submitQueuedPromise = null;
|
|
26
|
+
let submitQueuedAgain = false;
|
|
24
27
|
|
|
25
28
|
function escapeHtml(value) {
|
|
26
29
|
return String(value).replace(
|
|
@@ -36,6 +39,27 @@ function escapeHtml(value) {
|
|
|
36
39
|
);
|
|
37
40
|
}
|
|
38
41
|
|
|
42
|
+
function loadQueuedPrompts() {
|
|
43
|
+
try {
|
|
44
|
+
const parsed = JSON.parse(sessionStorage.getItem(queueStorageKey) || "[]");
|
|
45
|
+
return Array.isArray(parsed) ? parsed.filter((prompt) => prompt && typeof prompt === "object") : [];
|
|
46
|
+
} catch {
|
|
47
|
+
return [];
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function persistQueuedPrompts() {
|
|
52
|
+
try {
|
|
53
|
+
if (queued.length) {
|
|
54
|
+
sessionStorage.setItem(queueStorageKey, JSON.stringify(queued));
|
|
55
|
+
} else {
|
|
56
|
+
sessionStorage.removeItem(queueStorageKey);
|
|
57
|
+
}
|
|
58
|
+
} catch {
|
|
59
|
+
// The in-memory queue still works if browser storage is unavailable.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
39
63
|
function render() {
|
|
40
64
|
annotationPills.innerHTML = queued
|
|
41
65
|
.map(
|
|
@@ -105,6 +129,7 @@ function setAgentPresence(state) {
|
|
|
105
129
|
function removeQueuedPrompt(index, event) {
|
|
106
130
|
if (event) event.stopPropagation();
|
|
107
131
|
queued.splice(index, 1);
|
|
132
|
+
persistQueuedPrompts();
|
|
108
133
|
render();
|
|
109
134
|
}
|
|
110
135
|
|
|
@@ -118,6 +143,7 @@ function sendQueued() {
|
|
|
118
143
|
const text = chatInput.value.trim();
|
|
119
144
|
if (text) {
|
|
120
145
|
queued.push({ uid: "", prompt: text, selector: "", tag: "message", text: "Freeform message" });
|
|
146
|
+
persistQueuedPrompts();
|
|
121
147
|
addChat("user", text);
|
|
122
148
|
chatInput.value = "";
|
|
123
149
|
render();
|
|
@@ -128,14 +154,40 @@ function sendQueued() {
|
|
|
128
154
|
}
|
|
129
155
|
|
|
130
156
|
async function submitQueued() {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
157
|
+
if (submitQueuedPromise) {
|
|
158
|
+
submitQueuedAgain = true;
|
|
159
|
+
return submitQueuedPromise;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
let succeeded = false;
|
|
163
|
+
submitQueuedPromise = submitQueuedOnce();
|
|
164
|
+
try {
|
|
165
|
+
const result = await submitQueuedPromise;
|
|
166
|
+
succeeded = true;
|
|
167
|
+
return result;
|
|
168
|
+
} finally {
|
|
169
|
+
submitQueuedPromise = null;
|
|
170
|
+
const shouldSubmitAgain = submitQueuedAgain;
|
|
171
|
+
submitQueuedAgain = false;
|
|
172
|
+
if (succeeded && shouldSubmitAgain && queued.length) submitQueued();
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
async function submitQueuedOnce() {
|
|
177
|
+
const prompts = queued.slice();
|
|
178
|
+
const response = await fetch("/api/" + key + "/prompts", {
|
|
135
179
|
method: "POST",
|
|
136
180
|
headers: { "content-type": "application/json" },
|
|
137
181
|
body: JSON.stringify({ prompts, domSnapshot: pendingSnapshot }),
|
|
138
182
|
});
|
|
183
|
+
if (!response.ok) throw new Error("failed to submit queued prompts");
|
|
184
|
+
for (const prompt of prompts) {
|
|
185
|
+
const index = queued.indexOf(prompt);
|
|
186
|
+
if (index !== -1) queued.splice(index, 1);
|
|
187
|
+
}
|
|
188
|
+
persistQueuedPrompts();
|
|
189
|
+
render();
|
|
190
|
+
if (agentPresence === "listening") setAgentPresence("working");
|
|
139
191
|
}
|
|
140
192
|
|
|
141
193
|
async function endSession() {
|
|
@@ -185,6 +237,7 @@ window.addEventListener("message", (event) => {
|
|
|
185
237
|
const msg = event.data || {};
|
|
186
238
|
if (msg.type === "lavish:queuePrompt") {
|
|
187
239
|
queued.push(msg.prompt);
|
|
240
|
+
persistQueuedPrompts();
|
|
188
241
|
render();
|
|
189
242
|
}
|
|
190
243
|
if (msg.type === "lavish:snapshot") {
|
package/dist/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.js
|
|
4
4
|
import { spawn, spawnSync } from "node:child_process";
|
|
5
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
5
|
+
import { closeSync, existsSync, openSync, readFileSync } from "node:fs";
|
|
6
6
|
import { access } from "node:fs/promises";
|
|
7
7
|
import os2 from "node:os";
|
|
8
8
|
import path4 from "node:path";
|
|
@@ -472,12 +472,16 @@ function createDesignOutput() {
|
|
|
472
472
|
import { mkdir } from "node:fs/promises";
|
|
473
473
|
import os from "node:os";
|
|
474
474
|
import path from "node:path";
|
|
475
|
+
var LOOPBACK_HOST = "127.0.0.1";
|
|
475
476
|
function stateDir() {
|
|
476
477
|
return process.env.LAVISH_AXI_STATE_DIR || path.join(os.homedir(), ".lavish-axi");
|
|
477
478
|
}
|
|
478
479
|
function stateFile() {
|
|
479
480
|
return path.join(stateDir(), "state.json");
|
|
480
481
|
}
|
|
482
|
+
function serverLogFile() {
|
|
483
|
+
return path.join(stateDir(), "server.log");
|
|
484
|
+
}
|
|
481
485
|
async function ensureStateDir() {
|
|
482
486
|
await mkdir(stateDir(), { recursive: true });
|
|
483
487
|
}
|
|
@@ -1179,7 +1183,7 @@ var designAssetUrls = {
|
|
|
1179
1183
|
type: "application/javascript"
|
|
1180
1184
|
}
|
|
1181
1185
|
};
|
|
1182
|
-
async function serve({ port, stateFile: stateFile2, version = "", debug = false, log = null }) {
|
|
1186
|
+
async function serve({ port, stateFile: stateFile2, version = "", debug = false, log = null, pollHeartbeatMs = 15e3 }) {
|
|
1183
1187
|
const app = express();
|
|
1184
1188
|
const store = new SessionStore(stateFile2);
|
|
1185
1189
|
const events = new EventEmitter();
|
|
@@ -1191,6 +1195,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1191
1195
|
const writeLog = typeof log === "function" ? log : (line) => process.stderr.write(`${line}
|
|
1192
1196
|
`);
|
|
1193
1197
|
const logEvent = verbose ? (line) => writeLog(`[lavish] ${line}`) : null;
|
|
1198
|
+
let publicPort = port;
|
|
1194
1199
|
app.use(express.json({ limit: "2mb" }));
|
|
1195
1200
|
app.get("/health", (req, res) => {
|
|
1196
1201
|
res.json({ ok: true, app: "lavish-axi", version });
|
|
@@ -1207,7 +1212,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1207
1212
|
try {
|
|
1208
1213
|
const file = await canonicalFile(req.body.file);
|
|
1209
1214
|
const key = sessionKey(file);
|
|
1210
|
-
const url = `http
|
|
1215
|
+
const url = `http://${LOOPBACK_HOST}:${publicPort}/session/${key}`;
|
|
1211
1216
|
const existing = await store.findByKey(key);
|
|
1212
1217
|
const session = await store.upsertSession(file, url);
|
|
1213
1218
|
if (existing?.status === "ended") {
|
|
@@ -1222,6 +1227,14 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1222
1227
|
});
|
|
1223
1228
|
app.get("/api/poll", async (req, res, next) => {
|
|
1224
1229
|
try {
|
|
1230
|
+
let handleRespondError = function(error) {
|
|
1231
|
+
if (streamHeartbeat) {
|
|
1232
|
+
cleanup();
|
|
1233
|
+
if (!res.writableEnded) res.destroy(error);
|
|
1234
|
+
return;
|
|
1235
|
+
}
|
|
1236
|
+
next(error);
|
|
1237
|
+
};
|
|
1225
1238
|
const file = await canonicalFile(String(req.query.file || ""));
|
|
1226
1239
|
const key = sessionKey(file);
|
|
1227
1240
|
const timeoutMs = req.query.timeoutMs === void 0 ? null : Math.max(0, Math.min(Number(req.query.timeoutMs || 0), 2147483647));
|
|
@@ -1231,34 +1244,49 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1231
1244
|
res.json(immediate);
|
|
1232
1245
|
return;
|
|
1233
1246
|
}
|
|
1247
|
+
const streamHeartbeat = timeoutMs === null;
|
|
1248
|
+
let heartbeat = null;
|
|
1249
|
+
if (streamHeartbeat) {
|
|
1250
|
+
res.status(200).type("application/json");
|
|
1251
|
+
res.write(" ");
|
|
1252
|
+
heartbeat = setInterval(() => {
|
|
1253
|
+
if (!res.writableEnded) res.write(" ");
|
|
1254
|
+
}, pollHeartbeatMs);
|
|
1255
|
+
heartbeat.unref?.();
|
|
1256
|
+
}
|
|
1234
1257
|
setPollActive(key, activePolls, deliveredFeedback, events, true);
|
|
1235
|
-
const timer = timeoutMs === null ? null : setTimeout(() => respond().catch(
|
|
1258
|
+
const timer = timeoutMs === null ? null : setTimeout(() => respond().catch(handleRespondError), timeoutMs);
|
|
1236
1259
|
let cleaned = false;
|
|
1237
1260
|
let responding = false;
|
|
1238
1261
|
const cleanup = () => {
|
|
1239
1262
|
if (cleaned) return;
|
|
1240
1263
|
cleaned = true;
|
|
1241
1264
|
if (timer) clearTimeout(timer);
|
|
1265
|
+
if (heartbeat) clearInterval(heartbeat);
|
|
1242
1266
|
events.off("feedback", onFeedback);
|
|
1243
1267
|
events.off("ended", onFeedback);
|
|
1244
1268
|
setPollActive(key, activePolls, deliveredFeedback, events, false);
|
|
1245
1269
|
};
|
|
1246
1270
|
const respond = async () => {
|
|
1247
|
-
if (responding || res.
|
|
1271
|
+
if (responding || res.writableEnded) return;
|
|
1248
1272
|
responding = true;
|
|
1249
1273
|
try {
|
|
1250
1274
|
const result = await store.takeFeedback(key);
|
|
1251
1275
|
if (result.status === "feedback") markFeedbackDelivered(key, activePolls, deliveredFeedback, events);
|
|
1252
|
-
|
|
1276
|
+
if (streamHeartbeat) {
|
|
1277
|
+
res.end(JSON.stringify(result));
|
|
1278
|
+
} else {
|
|
1279
|
+
res.json(result);
|
|
1280
|
+
}
|
|
1253
1281
|
} finally {
|
|
1254
1282
|
cleanup();
|
|
1255
1283
|
}
|
|
1256
1284
|
};
|
|
1257
1285
|
const onFeedback = (changedKey) => {
|
|
1258
|
-
if (changedKey !== key || res.
|
|
1286
|
+
if (changedKey !== key || res.writableEnded) {
|
|
1259
1287
|
return;
|
|
1260
1288
|
}
|
|
1261
|
-
respond().catch(
|
|
1289
|
+
respond().catch(handleRespondError);
|
|
1262
1290
|
};
|
|
1263
1291
|
events.on("feedback", onFeedback);
|
|
1264
1292
|
events.on("ended", onFeedback);
|
|
@@ -1361,7 +1389,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1361
1389
|
res.status(403).send("Forbidden");
|
|
1362
1390
|
return;
|
|
1363
1391
|
}
|
|
1364
|
-
res.sendFile(file);
|
|
1392
|
+
res.sendFile(file, { dotfiles: "allow" });
|
|
1365
1393
|
} catch (error) {
|
|
1366
1394
|
next(error);
|
|
1367
1395
|
}
|
|
@@ -1452,8 +1480,9 @@ data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, del
|
|
|
1452
1480
|
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
1453
1481
|
});
|
|
1454
1482
|
const httpServer = await new Promise((resolve) => {
|
|
1455
|
-
const s = app.listen(port,
|
|
1483
|
+
const s = app.listen(port, LOOPBACK_HOST, () => resolve(s));
|
|
1456
1484
|
});
|
|
1485
|
+
publicPort = httpServer.address().port;
|
|
1457
1486
|
let shuttingDown = false;
|
|
1458
1487
|
function shutdown() {
|
|
1459
1488
|
if (shuttingDown) return;
|
|
@@ -1793,7 +1822,7 @@ function normalizePagePath(path5) {
|
|
|
1793
1822
|
// src/cli.js
|
|
1794
1823
|
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "server", "playbook", "design"]);
|
|
1795
1824
|
var DESCRIPTION = "Lavish Editor helps agents turn rich HTML artifacts into collaborative human review surfaces. Whenever you are about to give user a complex response that will be easier to understand via a rich / interactive page, consider using Lavish Editor. First generate an interactive HTML artifact according to user request, then run `lavish-axi <html-file>` so the user can visually review it, annotate elements or selected text, queue prompts, and send feedback back through `lavish-axi poll`.";
|
|
1796
|
-
var VERSION = "0.1.
|
|
1825
|
+
var VERSION = "0.1.13";
|
|
1797
1826
|
async function run(argv) {
|
|
1798
1827
|
await ensureStateDir();
|
|
1799
1828
|
const normalizedArgv = normalizeArgv(argv);
|
|
@@ -1883,6 +1912,7 @@ function createHomeOutput({ bin, sessions, includeSessions = true }) {
|
|
|
1883
1912
|
help: [
|
|
1884
1913
|
"Run `lavish-axi <html-file>` to open or resume a Lavish Editor session",
|
|
1885
1914
|
"Unless the user specifies another location, create HTML artifacts in the current working directory under `.lavish/`",
|
|
1915
|
+
"Lavish serves the html file through a local express.js server. If your html needs to reference other filesystem assets such as images, CSS, fonts, and local scripts, copy them into the same directory as the HTML file, then reference them with relative paths from that directory. Never prepend `/` to those asset paths - root paths won't work",
|
|
1886
1916
|
"Run `lavish-axi poll <html-file>` to wait for user feedback",
|
|
1887
1917
|
"Run `lavish-axi end <html-file>` to end a session",
|
|
1888
1918
|
"Run `lavish-axi playbook <playbook_id>` for focused artifact guidance",
|
|
@@ -1948,7 +1978,10 @@ async function pollCommand(args) {
|
|
|
1948
1978
|
}
|
|
1949
1979
|
const timeoutMs = flagValue(args, "--timeout-ms");
|
|
1950
1980
|
const timeoutQuery = timeoutMs ? `&timeoutMs=${encodeURIComponent(timeoutMs)}` : "";
|
|
1951
|
-
const response = await fetchJson(`${baseUrl}/api/poll?file=${encodeURIComponent(absolute)}${timeoutQuery}
|
|
1981
|
+
const response = await fetchJson(`${baseUrl}/api/poll?file=${encodeURIComponent(absolute)}${timeoutQuery}`, {
|
|
1982
|
+
retries: 3,
|
|
1983
|
+
retryDelayMs: 500
|
|
1984
|
+
});
|
|
1952
1985
|
return createPollOutput({ file: absolute, response });
|
|
1953
1986
|
}
|
|
1954
1987
|
function createPollOutput({ file, response }) {
|
|
@@ -2017,7 +2050,7 @@ function isHtmlPath(file) {
|
|
|
2017
2050
|
}
|
|
2018
2051
|
async function ensureServer({ forceRestart = false } = {}) {
|
|
2019
2052
|
const port = defaultPort();
|
|
2020
|
-
const baseUrl = `http
|
|
2053
|
+
const baseUrl = `http://${LOOPBACK_HOST}:${port}`;
|
|
2021
2054
|
const existing = await fetchHealth(baseUrl);
|
|
2022
2055
|
if (existing && !shouldRestartServer(VERSION, existing, forceRestart)) {
|
|
2023
2056
|
return baseUrl;
|
|
@@ -2106,36 +2139,84 @@ function killProcessOnPort(port) {
|
|
|
2106
2139
|
async function startServer(port) {
|
|
2107
2140
|
await ensureStateDir();
|
|
2108
2141
|
const entry = resolveServerEntry();
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
}
|
|
2114
|
-
|
|
2142
|
+
let logFd = null;
|
|
2143
|
+
try {
|
|
2144
|
+
logFd = openSync(serverLogFile(), "a");
|
|
2145
|
+
} catch {
|
|
2146
|
+
}
|
|
2147
|
+
try {
|
|
2148
|
+
const child = spawn(process.execPath, [entry, "server", "--port", String(port)], createServerSpawnOptions(logFd));
|
|
2149
|
+
child.unref();
|
|
2150
|
+
} finally {
|
|
2151
|
+
if (logFd !== null) closeSync(logFd);
|
|
2152
|
+
}
|
|
2115
2153
|
}
|
|
2116
2154
|
function resolveServerEntry() {
|
|
2117
2155
|
const binEntry = fileURLToPath(new URL("../bin/lavish-axi.js", import.meta.url));
|
|
2118
2156
|
if (existsSync(binEntry)) return binEntry;
|
|
2119
2157
|
return fileURLToPath(import.meta.url);
|
|
2120
2158
|
}
|
|
2121
|
-
|
|
2122
|
-
const
|
|
2159
|
+
function createServerSpawnOptions(logFd = null) {
|
|
2160
|
+
const stdio = (
|
|
2161
|
+
/** @type {import("node:child_process").StdioOptions} */
|
|
2162
|
+
logFd === null ? "ignore" : ["ignore", logFd, logFd]
|
|
2163
|
+
);
|
|
2164
|
+
return {
|
|
2165
|
+
detached: true,
|
|
2166
|
+
stdio,
|
|
2167
|
+
env: { ...process.env, LAVISH_AXI_NO_OPEN: "1" }
|
|
2168
|
+
};
|
|
2169
|
+
}
|
|
2170
|
+
async function fetchJson(url, { retries = 0, retryDelayMs = 250 } = {}) {
|
|
2171
|
+
let response;
|
|
2172
|
+
for (let attempt = 0; attempt <= retries; attempt += 1) {
|
|
2173
|
+
try {
|
|
2174
|
+
response = await fetch(url);
|
|
2175
|
+
break;
|
|
2176
|
+
} catch (error) {
|
|
2177
|
+
if (error instanceof AxiError) throw error;
|
|
2178
|
+
if (attempt >= retries) throw serverConnectionError();
|
|
2179
|
+
await delay(retryDelayMs);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
if (!response) throw serverConnectionError();
|
|
2123
2183
|
if (!response.ok) {
|
|
2124
2184
|
throw new AxiError(`Lavish Editor request failed: ${response.status}`, "SERVER_ERROR");
|
|
2125
2185
|
}
|
|
2126
|
-
|
|
2186
|
+
try {
|
|
2187
|
+
return await response.json();
|
|
2188
|
+
} catch {
|
|
2189
|
+
throw pollResponseInterruptedError();
|
|
2190
|
+
}
|
|
2127
2191
|
}
|
|
2128
2192
|
async function postJson(url, body) {
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2193
|
+
let response;
|
|
2194
|
+
try {
|
|
2195
|
+
response = await fetch(url, {
|
|
2196
|
+
method: "POST",
|
|
2197
|
+
headers: { "content-type": "application/json" },
|
|
2198
|
+
body: JSON.stringify(body)
|
|
2199
|
+
});
|
|
2200
|
+
} catch {
|
|
2201
|
+
throw serverConnectionError();
|
|
2202
|
+
}
|
|
2134
2203
|
if (!response.ok) {
|
|
2135
2204
|
throw new AxiError(`Lavish Editor request failed: ${response.status}`, "SERVER_ERROR");
|
|
2136
2205
|
}
|
|
2137
2206
|
return response.json();
|
|
2138
2207
|
}
|
|
2208
|
+
function serverConnectionError() {
|
|
2209
|
+
return new AxiError("Lavish Editor server connection failed", "SERVER_ERROR", [
|
|
2210
|
+
"Run `lavish-axi server --verbose` or inspect `~/.lavish-axi/server.log` (`LAVISH_AXI_STATE_DIR/server.log` when set) for server startup or crash diagnostics",
|
|
2211
|
+
"Re-run the last `lavish-axi poll <html-file>` command after the server is healthy"
|
|
2212
|
+
]);
|
|
2213
|
+
}
|
|
2214
|
+
function pollResponseInterruptedError() {
|
|
2215
|
+
return new AxiError("Lavish Editor poll response was interrupted", "SERVER_ERROR", [
|
|
2216
|
+
"Run `lavish-axi server --verbose` or inspect `~/.lavish-axi/server.log` (`LAVISH_AXI_STATE_DIR/server.log` when set) for server startup or crash diagnostics",
|
|
2217
|
+
"Re-run the last `lavish-axi poll <html-file>` command after the server is healthy"
|
|
2218
|
+
]);
|
|
2219
|
+
}
|
|
2139
2220
|
function flagValue(args, flag) {
|
|
2140
2221
|
const index = args.indexOf(flag);
|
|
2141
2222
|
if (index === -1) {
|
|
@@ -2192,7 +2273,7 @@ Show a copy-pasteable CDN snippet for Tailwind CSS browser runtime v4 + DaisyUI
|
|
|
2192
2273
|
`,
|
|
2193
2274
|
server: `Usage: lavish-axi server [--port 4387] [--verbose]
|
|
2194
2275
|
|
|
2195
|
-
Run the local Lavish Editor server. Pass --verbose (or set LAVISH_AXI_DEBUG=1) to log session and watcher events to stderr.
|
|
2276
|
+
Run the local Lavish Editor server. Pass --verbose (or set LAVISH_AXI_DEBUG=1) to log session and watcher events to stderr. Detached server output is appended to ~/.lavish-axi/server.log, or LAVISH_AXI_STATE_DIR/server.log when set, for startup and crash diagnostics.
|
|
2196
2277
|
`
|
|
2197
2278
|
};
|
|
2198
2279
|
|