lavish-axi 0.1.16 → 0.1.18
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 +6 -2
- package/dist/cli.mjs +135 -18
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Lavish Editor opens agent-generated HTML files in a local browser, lets you pinp
|
|
|
35
35
|
|
|
36
36
|
- **Local only** - Work with your local HTML artifacts with a local CLI. Zero cloud dependency.
|
|
37
37
|
- **Human-AI collaboration** - Annotate elements, selected text ranges, and send messages to the agent without leaving Lavish Editor.
|
|
38
|
-
- **Battery included** - Lavish Editor teaches your agent good visualization for common use cases such as
|
|
38
|
+
- **Battery included** - Lavish Editor teaches your agent good visualization for common use cases such as product or technical plans, design explorations and more out of the box.
|
|
39
39
|
|
|
40
40
|
Lavish Editor is an [AXI](https://axi.md), which means -
|
|
41
41
|
|
|
@@ -48,7 +48,7 @@ Lavish Editor is an [AXI](https://axi.md), which means -
|
|
|
48
48
|
Just tell your agent:
|
|
49
49
|
|
|
50
50
|
```sh
|
|
51
|
-
Use `npx lavish-axi` to write a technical plan for what we discussed.
|
|
51
|
+
Use `npx lavish-axi` to write a product or technical plan for what we discussed.
|
|
52
52
|
```
|
|
53
53
|
|
|
54
54
|
## Install
|
|
@@ -101,6 +101,8 @@ pnpm link
|
|
|
101
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.
|
|
102
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.
|
|
103
103
|
- **Precise targets** - Text annotations include selected text plus range anchors, so agents are not limited to whole-element selectors.
|
|
104
|
+
- **Server cleanup** - The detached server stops after the last session ends when nothing is connected, or after `LAVISH_AXI_IDLE_TIMEOUT_MS` (default 30 minutes) with no browser or poll connections.
|
|
105
|
+
Set `LAVISH_AXI_IDLE_TIMEOUT_MS=0` or `off` to disable idle self-shutdown.
|
|
104
106
|
- **Local-first state** - Session state stays under `.lavish-axi/` in the workspace.
|
|
105
107
|
|
|
106
108
|
## CLI Reference
|
|
@@ -111,6 +113,7 @@ pnpm link
|
|
|
111
113
|
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session. |
|
|
112
114
|
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback or ends the session. |
|
|
113
115
|
| `lavish-axi end <html-file>` | End a session. |
|
|
116
|
+
| `lavish-axi stop` | Shut down the background server. |
|
|
114
117
|
| `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook. |
|
|
115
118
|
| `lavish-axi design` | Show CDN snippet + DaisyUI component reference (opt-in). |
|
|
116
119
|
| `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, and OpenCode; restart the agent session afterward. |
|
|
@@ -125,6 +128,7 @@ Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `diff`, `input`, `
|
|
|
125
128
|
| `lavish-axi <html-file>` | `--no-open` | Ensure the server/session exists without opening another browser window. |
|
|
126
129
|
| `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat before polling again. |
|
|
127
130
|
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it. |
|
|
131
|
+
| `lavish-axi stop` | `--port <port>` | Shut down a server running on a non-default port. |
|
|
128
132
|
| `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. |
|
|
129
133
|
|
|
130
134
|
## Development
|
package/dist/cli.mjs
CHANGED
|
@@ -577,31 +577,28 @@ var PLAYBOOKS = [
|
|
|
577
577
|
},
|
|
578
578
|
{
|
|
579
579
|
id: "plan",
|
|
580
|
-
use_when: "Explain a technical plan before implementation",
|
|
580
|
+
use_when: "Explain a product or technical plan before implementation",
|
|
581
581
|
choose: [
|
|
582
582
|
"Use this when the user needs to inspect a feature approach before implementation begins.",
|
|
583
|
-
"Use it when
|
|
584
|
-
"Use a lighter comparison or diagram playbook when the plan is only a small design choice."
|
|
583
|
+
"Use it when the user explicitly asked for a PRD, technical design, implementation plan or proposal.",
|
|
584
|
+
"Use a lighter comparison or diagram playbook when the plan is only a single small design choice."
|
|
585
585
|
],
|
|
586
586
|
structure: [
|
|
587
|
-
"Start with the
|
|
588
|
-
"
|
|
589
|
-
"
|
|
587
|
+
"Start with the goal, the current state, and desired behavior.",
|
|
588
|
+
"Then describe a proposed approach, focusing on high level decisions.",
|
|
589
|
+
"At the end, list any risks you see, and open questions you have, and follow the 'comparison' playbook to provide options for the user to choose from."
|
|
590
590
|
],
|
|
591
591
|
design_rules: [
|
|
592
592
|
"Verify each claim against the codebase before presenting it as fact.",
|
|
593
|
-
"
|
|
594
|
-
"
|
|
593
|
+
"When discussing frontend experiences, prefer visually mocking the experience under a consistent design system as the real product over describing it with text.",
|
|
594
|
+
"The plan needs to be self-contained enough that another developer can read it and fully implement the proposal."
|
|
595
595
|
],
|
|
596
596
|
pitfalls: [
|
|
597
|
-
"Do not
|
|
598
|
-
"Do not
|
|
597
|
+
"Do not leave resolved open questions in the artifact. Update existing content to reflect the decision and remove the open question.",
|
|
598
|
+
"Do not only focus on ambiguous decisions and omit the actual proposal.",
|
|
599
599
|
"Do not omit failure modes, migration concerns, or backwards compatibility questions."
|
|
600
600
|
],
|
|
601
|
-
lavish_notes: [
|
|
602
|
-
"A Lavish plan should make uncertainties easy to annotate before code exists.",
|
|
603
|
-
"Use controls for scope choices so the user can queue a precise implementation direction."
|
|
604
|
-
]
|
|
601
|
+
lavish_notes: ["A Lavish plan should make a plan and its uncertainties easy to annotate before code exists."]
|
|
605
602
|
},
|
|
606
603
|
{
|
|
607
604
|
id: "diff",
|
|
@@ -1198,7 +1195,24 @@ var designAssetUrls = {
|
|
|
1198
1195
|
type: "application/javascript"
|
|
1199
1196
|
}
|
|
1200
1197
|
};
|
|
1201
|
-
|
|
1198
|
+
var DEFAULT_IDLE_TIMEOUT_MS = 30 * 6e4;
|
|
1199
|
+
function resolveIdleTimeoutMs(env = process.env) {
|
|
1200
|
+
const raw = env.LAVISH_AXI_IDLE_TIMEOUT_MS?.trim();
|
|
1201
|
+
if (raw === void 0 || raw === "") return DEFAULT_IDLE_TIMEOUT_MS;
|
|
1202
|
+
if (raw === "0" || raw.toLowerCase() === "off") return null;
|
|
1203
|
+
const value = Number(raw);
|
|
1204
|
+
if (!Number.isFinite(value) || value <= 0) return DEFAULT_IDLE_TIMEOUT_MS;
|
|
1205
|
+
return value;
|
|
1206
|
+
}
|
|
1207
|
+
async function serve({
|
|
1208
|
+
port,
|
|
1209
|
+
stateFile: stateFile2,
|
|
1210
|
+
version = "",
|
|
1211
|
+
debug = false,
|
|
1212
|
+
log = null,
|
|
1213
|
+
pollHeartbeatMs = 15e3,
|
|
1214
|
+
idleTimeoutMs = resolveIdleTimeoutMs()
|
|
1215
|
+
}) {
|
|
1202
1216
|
const app = express();
|
|
1203
1217
|
const store = new SessionStore(stateFile2);
|
|
1204
1218
|
const events = new EventEmitter();
|
|
@@ -1270,6 +1284,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1270
1284
|
heartbeat.unref?.();
|
|
1271
1285
|
}
|
|
1272
1286
|
setPollActive(key, activePolls, deliveredFeedback, events, true);
|
|
1287
|
+
refreshIdleTimer();
|
|
1273
1288
|
const timer = timeoutMs === null ? null : setTimeout(() => respond().catch(handleRespondError), timeoutMs);
|
|
1274
1289
|
let cleaned = false;
|
|
1275
1290
|
let responding = false;
|
|
@@ -1281,6 +1296,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1281
1296
|
events.off("feedback", onFeedback);
|
|
1282
1297
|
events.off("ended", onFeedback);
|
|
1283
1298
|
setPollActive(key, activePolls, deliveredFeedback, events, false);
|
|
1299
|
+
refreshIdleTimer();
|
|
1284
1300
|
};
|
|
1285
1301
|
const respond = async () => {
|
|
1286
1302
|
if (responding || res.writableEnded) return;
|
|
@@ -1329,6 +1345,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1329
1345
|
clearFeedbackDelivery(req.params.key, activePolls, deliveredFeedback, events);
|
|
1330
1346
|
events.emit("ended", req.params.key);
|
|
1331
1347
|
res.json({ status: "ended" });
|
|
1348
|
+
await shutdownIfNoLiveSessions();
|
|
1332
1349
|
} catch (error) {
|
|
1333
1350
|
next(error);
|
|
1334
1351
|
}
|
|
@@ -1355,6 +1372,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1355
1372
|
clearFeedbackDelivery(key, activePolls, deliveredFeedback, events);
|
|
1356
1373
|
events.emit("ended", key);
|
|
1357
1374
|
res.json({ status: "ended" });
|
|
1375
|
+
await shutdownIfNoLiveSessions();
|
|
1358
1376
|
} catch (error) {
|
|
1359
1377
|
next(error);
|
|
1360
1378
|
}
|
|
@@ -1417,6 +1435,7 @@ async function serve({ port, stateFile: stateFile2, version = "", debug = false,
|
|
|
1417
1435
|
connection: "keep-alive"
|
|
1418
1436
|
});
|
|
1419
1437
|
sseClients.add(res);
|
|
1438
|
+
refreshIdleTimer();
|
|
1420
1439
|
const session = await store.findByKey(req.params.key);
|
|
1421
1440
|
const sendReload = (key) => {
|
|
1422
1441
|
if (key === req.params.key) {
|
|
@@ -1457,6 +1476,7 @@ data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, del
|
|
|
1457
1476
|
events.off("reload", sendReload);
|
|
1458
1477
|
events.off("agent-reply", sendAgentReply);
|
|
1459
1478
|
events.off("agent-presence", sendPresence);
|
|
1479
|
+
refreshIdleTimer();
|
|
1460
1480
|
});
|
|
1461
1481
|
} catch (error) {
|
|
1462
1482
|
next(error);
|
|
@@ -1502,6 +1522,10 @@ data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, del
|
|
|
1502
1522
|
function shutdown() {
|
|
1503
1523
|
if (shuttingDown) return;
|
|
1504
1524
|
shuttingDown = true;
|
|
1525
|
+
if (idleTimer) {
|
|
1526
|
+
clearTimeout(idleTimer);
|
|
1527
|
+
idleTimer = null;
|
|
1528
|
+
}
|
|
1505
1529
|
for (const res of sseClients) {
|
|
1506
1530
|
try {
|
|
1507
1531
|
res.write("event: chrome-reload\ndata: {}\n\n");
|
|
@@ -1520,6 +1544,35 @@ data: ${JSON.stringify({ state: computePresence(req.params.key, activePolls, del
|
|
|
1520
1544
|
httpServer.closeAllConnections();
|
|
1521
1545
|
}
|
|
1522
1546
|
}
|
|
1547
|
+
let idleTimer = null;
|
|
1548
|
+
function refreshIdleTimer() {
|
|
1549
|
+
if (idleTimer) {
|
|
1550
|
+
clearTimeout(idleTimer);
|
|
1551
|
+
idleTimer = null;
|
|
1552
|
+
}
|
|
1553
|
+
if (shuttingDown || idleTimeoutMs == null) return;
|
|
1554
|
+
if (sseClients.size > 0 || activePolls.size > 0) return;
|
|
1555
|
+
idleTimer = setTimeout(() => {
|
|
1556
|
+
idleTimer = null;
|
|
1557
|
+
if (!shuttingDown && sseClients.size === 0 && activePolls.size === 0) {
|
|
1558
|
+
logEvent?.(`idle for ${idleTimeoutMs}ms with no connections, shutting down`);
|
|
1559
|
+
shutdown();
|
|
1560
|
+
}
|
|
1561
|
+
}, idleTimeoutMs);
|
|
1562
|
+
idleTimer.unref?.();
|
|
1563
|
+
}
|
|
1564
|
+
async function shutdownIfNoLiveSessions() {
|
|
1565
|
+
if (sseClients.size > 0 || activePolls.size > 0) return;
|
|
1566
|
+
try {
|
|
1567
|
+
const sessions = await store.listSessions();
|
|
1568
|
+
if (sessions.every((session) => session.status === "ended")) {
|
|
1569
|
+
logEvent?.("last open session ended with no live connections, shutting down");
|
|
1570
|
+
setImmediate(shutdown);
|
|
1571
|
+
}
|
|
1572
|
+
} catch {
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
refreshIdleTimer();
|
|
1523
1576
|
return {
|
|
1524
1577
|
port: httpServer.address().port,
|
|
1525
1578
|
close: async () => {
|
|
@@ -1835,9 +1888,9 @@ function normalizePagePath(path5) {
|
|
|
1835
1888
|
}
|
|
1836
1889
|
|
|
1837
1890
|
// src/cli.js
|
|
1838
|
-
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "server", "playbook", "design", "setup"]);
|
|
1891
|
+
var COMMANDS = /* @__PURE__ */ new Set(["open", "poll", "end", "stop", "server", "playbook", "design", "setup"]);
|
|
1839
1892
|
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`.";
|
|
1840
|
-
var VERSION = "0.1.
|
|
1893
|
+
var VERSION = "0.1.18";
|
|
1841
1894
|
async function run(argv) {
|
|
1842
1895
|
await ensureStateDir();
|
|
1843
1896
|
const normalizedArgv = normalizeArgv(argv);
|
|
@@ -1865,6 +1918,7 @@ async function run(argv) {
|
|
|
1865
1918
|
open: openCommand,
|
|
1866
1919
|
poll: pollCommand,
|
|
1867
1920
|
end: endCommand,
|
|
1921
|
+
stop: stopCommand,
|
|
1868
1922
|
playbook: playbookCommand,
|
|
1869
1923
|
design: designCommand,
|
|
1870
1924
|
setup: setupCommand,
|
|
@@ -1930,9 +1984,10 @@ function createHomeOutput({ bin, sessions, includeSessions = true }) {
|
|
|
1930
1984
|
"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",
|
|
1931
1985
|
"Run `lavish-axi poll <html-file>` to wait for user feedback",
|
|
1932
1986
|
"Run `lavish-axi end <html-file>` to end a session",
|
|
1987
|
+
"Run `lavish-axi stop` to shut down the background server (it also self-stops when idle or after the last session ends with nothing connected)",
|
|
1933
1988
|
"Run `lavish-axi playbook <playbook_id>` for focused artifact guidance",
|
|
1934
1989
|
DESIGN_SYSTEM_HINT,
|
|
1935
|
-
"Use lavish-axi when the user asks for a visual artifact, HTML explainer, interactive prototype, review surface, technical plan, comparison, report, or browser-based feedback loop"
|
|
1990
|
+
"Use lavish-axi when the user asks for a visual artifact, HTML explainer, interactive prototype, review surface, product or technical plan, comparison, report, or browser-based feedback loop"
|
|
1936
1991
|
]
|
|
1937
1992
|
};
|
|
1938
1993
|
}
|
|
@@ -2031,6 +2086,35 @@ async function endCommand(args) {
|
|
|
2031
2086
|
const response = await postJson(`${baseUrl}/api/end`, { file: absolute });
|
|
2032
2087
|
return { session: { file: absolute, status: response.status || "ended" } };
|
|
2033
2088
|
}
|
|
2089
|
+
async function stopCommand(args) {
|
|
2090
|
+
const port = Number(flagValue(args, "--port") || defaultPort());
|
|
2091
|
+
const baseUrl = `http://${LOOPBACK_HOST}:${port}`;
|
|
2092
|
+
return shutdownServerOnPort(port, { baseUrl, currentVersion: VERSION });
|
|
2093
|
+
}
|
|
2094
|
+
async function shutdownServerOnPort(port, {
|
|
2095
|
+
baseUrl = `http://${LOOPBACK_HOST}:${port}`,
|
|
2096
|
+
currentVersion = VERSION,
|
|
2097
|
+
fetchHealth: healthFetcher = fetchHealth,
|
|
2098
|
+
requestShutdown: shutdownRequester = requestShutdown,
|
|
2099
|
+
waitForPortFree: portFreeWaiter = waitForPortFree,
|
|
2100
|
+
killProcessOnPort: portKiller = killProcessOnPort,
|
|
2101
|
+
processMatchesLavish = processOnPortMatchesLavish
|
|
2102
|
+
} = {}) {
|
|
2103
|
+
const health = await healthFetcher(baseUrl);
|
|
2104
|
+
if (!health) {
|
|
2105
|
+
return { server: { status: "not-running", port } };
|
|
2106
|
+
}
|
|
2107
|
+
if (!await canControlServerOnPort(port, health, processMatchesLavish)) {
|
|
2108
|
+
return { server: { status: "not-lavish", port } };
|
|
2109
|
+
}
|
|
2110
|
+
await shutdownRequester(baseUrl);
|
|
2111
|
+
let freed = await portFreeWaiter(baseUrl, 3e3);
|
|
2112
|
+
if (!freed && shouldKillProcessOnPort(currentVersion, health)) {
|
|
2113
|
+
portKiller(port);
|
|
2114
|
+
freed = await portFreeWaiter(baseUrl, 3e3);
|
|
2115
|
+
}
|
|
2116
|
+
return { server: { status: freed ? "stopped" : "stopping", port } };
|
|
2117
|
+
}
|
|
2034
2118
|
async function playbookCommand(args) {
|
|
2035
2119
|
return createPlaybookOutput(args);
|
|
2036
2120
|
}
|
|
@@ -2094,6 +2178,11 @@ async function ensureServer({ forceRestart = false } = {}) {
|
|
|
2094
2178
|
return baseUrl;
|
|
2095
2179
|
}
|
|
2096
2180
|
if (existing) {
|
|
2181
|
+
if (!await canControlServerOnPort(port, existing, processOnPortMatchesLavish)) {
|
|
2182
|
+
throw new AxiError(`Port ${port} is occupied by a non-Lavish server`, "SERVER_ERROR", [
|
|
2183
|
+
`Stop the process using port ${port}, or set LAVISH_AXI_PORT to another port`
|
|
2184
|
+
]);
|
|
2185
|
+
}
|
|
2097
2186
|
await requestShutdown(baseUrl);
|
|
2098
2187
|
const freed = await waitForPortFree(baseUrl, 2e3);
|
|
2099
2188
|
if (!freed) {
|
|
@@ -2135,6 +2224,12 @@ function shouldKillProcessOnPort(currentVersion, healthBody) {
|
|
|
2135
2224
|
if (healthBody.app !== "lavish-axi") return false;
|
|
2136
2225
|
return healthBody.version !== currentVersion;
|
|
2137
2226
|
}
|
|
2227
|
+
async function canControlServerOnPort(port, healthBody, processMatchesLavish) {
|
|
2228
|
+
if (!healthBody || typeof healthBody !== "object") return false;
|
|
2229
|
+
if (healthBody.app === "lavish-axi") return true;
|
|
2230
|
+
if (typeof healthBody.version === "string" && healthBody.version !== "") return false;
|
|
2231
|
+
return processMatchesLavish(port);
|
|
2232
|
+
}
|
|
2138
2233
|
async function fetchHealth(baseUrl) {
|
|
2139
2234
|
try {
|
|
2140
2235
|
const response = await fetch(`${baseUrl}/health`);
|
|
@@ -2174,6 +2269,23 @@ function killProcessOnPort(port) {
|
|
|
2174
2269
|
} catch {
|
|
2175
2270
|
}
|
|
2176
2271
|
}
|
|
2272
|
+
function processOnPortMatchesLavish(port) {
|
|
2273
|
+
try {
|
|
2274
|
+
const pids = spawnSync("lsof", ["-t", `-iTCP:${port}`, "-sTCP:LISTEN"], { encoding: "utf8" });
|
|
2275
|
+
if (pids.status !== 0) return false;
|
|
2276
|
+
for (const line of pids.stdout.split("\n")) {
|
|
2277
|
+
const pid = Number(line.trim());
|
|
2278
|
+
if (!Number.isInteger(pid) || pid <= 0 || pid === process.pid) continue;
|
|
2279
|
+
const command = spawnSync("ps", ["-p", String(pid), "-o", "command="], { encoding: "utf8" });
|
|
2280
|
+
if (command.status === 0 && /lavish-axi/.test(command.stdout)) {
|
|
2281
|
+
return true;
|
|
2282
|
+
}
|
|
2283
|
+
}
|
|
2284
|
+
} catch {
|
|
2285
|
+
return false;
|
|
2286
|
+
}
|
|
2287
|
+
return false;
|
|
2288
|
+
}
|
|
2177
2289
|
async function startServer(port) {
|
|
2178
2290
|
await ensureStateDir();
|
|
2179
2291
|
const entry = resolveServerEntry();
|
|
@@ -2275,6 +2387,7 @@ Usage:
|
|
|
2275
2387
|
lavish-axi <html-file>
|
|
2276
2388
|
lavish-axi poll <html-file> [--agent-reply "..."]
|
|
2277
2389
|
lavish-axi end <html-file>
|
|
2390
|
+
lavish-axi stop
|
|
2278
2391
|
lavish-axi playbook [playbook_id]
|
|
2279
2392
|
lavish-axi design
|
|
2280
2393
|
lavish-axi setup hooks
|
|
@@ -2296,6 +2409,10 @@ This command long-polls indefinitely for queued user prompts, then returns them
|
|
|
2296
2409
|
end: `Usage: lavish-axi end <html-file>
|
|
2297
2410
|
|
|
2298
2411
|
End a Lavish Editor session.
|
|
2412
|
+
`,
|
|
2413
|
+
stop: `Usage: lavish-axi stop [--port <port>]
|
|
2414
|
+
|
|
2415
|
+
Shut down the background Lavish Editor server. The server also stops itself when no browser or poll has been connected for a while (LAVISH_AXI_IDLE_TIMEOUT_MS, default 30m) and immediately when the last session ends with nothing connected.
|
|
2299
2416
|
`,
|
|
2300
2417
|
playbook: `Usage: lavish-axi playbook [playbook_id]
|
|
2301
2418
|
|