lavish-axi 0.1.59 → 0.1.60
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 +29 -22
- package/dist/chrome-client.js +140 -9
- package/dist/chrome.css +20 -0
- package/dist/cli.mjs +921 -104
- package/package.json +1 -1
- package/plugin.json +1 -1
- package/skills/lavish/SKILL.md +11 -65
package/README.md
CHANGED
|
@@ -53,7 +53,7 @@ npx skills add kunchenguid/lavish-axi --skill lavish
|
|
|
53
53
|
|
|
54
54
|
That is the entire setup - no npm install needed.
|
|
55
55
|
The skill teaches your agent to run Lavish through `npx -y lavish-axi`, so the CLI comes along on demand.
|
|
56
|
-
|
|
56
|
+
It stays a short stub and sends the agent to `npx -y lavish-axi --help`, `design`, and `playbook` for current instructions, so an installed copy cannot go stale against a newer CLI.
|
|
57
57
|
Its frontmatter also includes Hermes Agent metadata, so Hermes-compatible harnesses can categorize and surface it as a first-class productivity skill.
|
|
58
58
|
This installs the public `lavish` skill.
|
|
59
59
|
The repository also contains an internal `lavish-design` brand skill for maintainers; default `npx skills add ... --list` and skills.sh discovery hide it unless `INSTALL_INTERNAL_SKILLS=1` is set.
|
|
@@ -184,11 +184,14 @@ pnpm link
|
|
|
184
184
|
- **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.
|
|
185
185
|
- **Export and sharing** - `lavish-axi export` writes `<name>.export.html` by inlining local assets only, stripping the annotation SDK, and leaving remote CDN/font references as links that still need network access.
|
|
186
186
|
`lavish-axi share` publishes the same local-inlined HTML to [ht-ml.app](https://ht-ml.app), a third-party hosting service not part of Lavish.
|
|
187
|
-
Publishing sends the artifact to ht-ml.app's servers, public by default, or private and password-protected with `--password
|
|
187
|
+
Publishing sends the artifact to ht-ml.app's servers, public by default, or private and password-protected with `--private` (Lavish generates the password and returns it once, in the command output and in the browser publish dialog) or `--password <pw>` (one you supply, never echoed back). A generated password is a shared secret: give it to whoever should read the page, and expect it to appear in your agent's transcript, because the agent has to relay it. Lavish does not store it.
|
|
188
|
+
The response includes a secret `update_key` shown once. Keep it to republish the same URL later with `--site <site_id> --update-key <key>`, which replaces the HTML in place and leaves the password alone unless you pass `--private` (rotate to a new generated one) or `--password <pw>` (set one). Locking a page that was public is not instant - it can keep answering from ht-ml.app's CDN cache for minutes after the password is set - while a page that was already private has no such cached copy.
|
|
189
|
+
A page's password cannot be removed: ht-ml.app accepts a request to clear one and silently ignores it, so Lavish offers no way to make a private page public rather than telling you it did something the host did not do. Republish as a new page if you need a public URL.
|
|
190
|
+
ht-ml.app has no delete endpoint, so `--unpublish --site <site_id> --update-key <key>` replaces the page with a short placeholder and locks it behind a discarded random password rather than removing it: the URL still resolves and the host still holds what was published. The content swap is immediate, but the lock is not: a page that was public can keep serving the placeholder to uncredentialed visitors from ht-ml.app's CDN cache for minutes afterwards. The `update_key` plus `--private` republishes real content behind a new password you can share.
|
|
188
191
|
Bundling never fetches remote URLs, Lavish itself does not set a CSP, local reads stay confined and size-capped, and absolute `file://` paths outside safe inlined asset references are redacted before output.
|
|
189
192
|
Per-asset and per-bundle inline caps default to 10 MB and 25 MB, overridable with `LAVISH_AXI_EXPORT_MAX_ASSET_BYTES` and `LAVISH_AXI_EXPORT_MAX_BUNDLE_BYTES`.
|
|
190
193
|
Unresolved local assets or export notices such as author-set CSP meta tags and redacted file URLs are surfaced in command or browser output.
|
|
191
|
-
Use `--token` or `LAVISH_AXI_HTML_APP_TOKEN` for an optional bearer token; set `LAVISH_AXI_HTML_APP_API_URL` to override the ht-ml.app API base and point `share` at a backend you control (see [Self-hosting the share backend](docs/self-hosting-share.md) for the contract it must implement).
|
|
194
|
+
Use `--token` or `LAVISH_AXI_HTML_APP_TOKEN` for an optional bearer token when publishing a new page (a republish or `--unpublish` authorizes with the `update_key` instead and rejects `--token`); set `LAVISH_AXI_HTML_APP_API_URL` to override the ht-ml.app API base and point `share` at a backend you control (see [Self-hosting the share backend](docs/self-hosting-share.md) for the contract it must implement).
|
|
192
195
|
- **Live reload** - Lavish watches the HTML artifact file by default and preserves review context across reloads: the artifact iframe scroll position, an open annotation card's unsent text, and answers to `data-lavish-question` controls (application-owned form state is left alone). Unsent annotation text also survives a full reload of the review page itself. While a queued layout-issue batch is outstanding, closely spaced saves coalesce so one batch of fixes costs one refresh. 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">`.
|
|
193
196
|
If the element an unsent annotation was attached to is gone from the artifact for good, Lavish cannot reopen that card, so it writes your text into the conversation panel under **Unsent annotation** - selectable, never written over anything you have typed, and kept there across reloads; no note is ever dropped to make room for a newer one, and a note the browser refuses to store says so where it is shown.
|
|
194
197
|
- **Feedback controls** - Native controls (radios, checkboxes, inputs, selects, buttons, labels, disclosure summaries, contenteditable) are interactive automatically, so they do not need `data-lavish-action`.
|
|
@@ -237,27 +240,27 @@ pnpm link
|
|
|
237
240
|
- **Local-first state** - Session state stays under `~/.lavish-axi/` by default, or `LAVISH_AXI_STATE_DIR` when set.
|
|
238
241
|
- **Diagnostic viewports** - `LAVISH_AXI_DIAGNOSTIC_VIEWPORTS` sets which viewport classes the layout-issue inbox tracks (`mobile`, `compact`, `desktop`; comma-separated, default all). Warnings whose class leaves the set are marked obsolete with an explicit reason instead of silently reading as fixed.
|
|
239
242
|
- **Server port** - Set `LAVISH_AXI_PORT` to choose the server port; it defaults to `4387`.
|
|
240
|
-
- **Network binding** -
|
|
241
|
-
- **Allowed hosts** - To defend against DNS rebinding, the server rejects (`403`) any request whose `Host` header is missing or not one it answers to:
|
|
243
|
+
- **Network binding** - With Tailscale running, the review server automatically listens only on loopback (`127.0.0.1`) and this machine's Tailscale IPv4 address - never on `0.0.0.0` or every interface. The generated session link uses the machine's MagicDNS name, so it is the phone-ready URL to open from another device on the same tailnet. When Tailscale is absent or down, Lavish silently falls back to loopback-only and prints no phone URL. If Tailscale is running but MagicDNS is unavailable or its address cannot be bound after brief retries, Lavish visibly warns that phone access is unavailable and remains loopback-only. Binding beyond loopback exposes an unauthenticated server that can read and serve arbitrary local files to devices that can reach it, so the tailnet should be trusted. Any explicit `LAVISH_AXI_HOST` overrides automatic Tailscale binding; wildcard values are restricted to loopback, while a non-wildcard value selects that one concrete bind address. `LAVISH_AXI_LINK_HOST` controls the link host when automatic Tailscale binding is disabled.
|
|
244
|
+
- **Allowed hosts** - To defend against DNS rebinding, the server rejects (`403`) any request whose `Host` header is missing or not one it answers to: loopback names plus the concrete Tailscale IPv4 address and MagicDNS name when the Tailscale listener is successfully bound. If you configure a reverse proxy or another intentional hostname, list it in `LAVISH_AXI_ALLOWED_HOSTS` (whitespace-separated). Behind a reverse proxy, the forwarded `X-Forwarded-Host` is validated against the same list, so add the public hostname there and have the proxy send it together with `X-Forwarded-Proto`. Set `LAVISH_AXI_ALLOWED_HOSTS` to `*` to disable the check entirely, only when the server sits behind your own authentication or proxy. Mutating routes also reject a present foreign `Origin` or `Referer` (`403`); header-less CLI control requests remain allowed where supported.
|
|
242
245
|
- **Browser opening** - Set `LAVISH_AXI_NO_OPEN=1`, equivalent to `--no-open`, to create or resume a session without launching a browser window.
|
|
243
246
|
|
|
244
247
|
## CLI Reference
|
|
245
248
|
|
|
246
|
-
| Command | Description
|
|
247
|
-
| ------------------------------- |
|
|
248
|
-
| `lavish-axi` | Show current sessions and usage guidance.
|
|
249
|
-
| `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater.
|
|
250
|
-
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. Unresolved layout issues from earlier in the session are preserved. Refuses to reopen a session the user explicitly ended from the browser unless `--reopen` is passed.
|
|
251
|
-
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback or ends the session; detected layout issues wait in the user's Layout issues inbox and arrive only when queued. Leave no-timeout polls running, or re-run them if interrupted. Codex guidance keeps polls attached to the active turn. On `status: ended`, stop polling and do not reopen uninvited.
|
|
252
|
-
| `lavish-axi end <html-file>` | End a session as the agent; unlike a user-initiated end from the browser, this still allows a plain reopen later.
|
|
253
|
-
| `lavish-axi export <html-file>` | Write a portable copy of the artifact: one HTML file with its local assets inlined, so it opens with no server and no sibling files. Remote CDN/font references are left as links.
|
|
254
|
-
| `lavish-axi share <html-file>` | Publish the artifact (local assets inlined) to [ht-ml.app](https://ht-ml.app), a third-party host not part of Lavish, and print a visitable URL plus a secret update key; shares are public by default,
|
|
255
|
-
| `lavish-axi stop` | Shut down the background server.
|
|
256
|
-
| `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook; agents must open each matching playbook before writing HTML.
|
|
257
|
-
| `lavish-axi design` | Show agent-facing design guidance, including optional CDN snippets and the whiteboard (Mermaid) opt-in snippet.
|
|
258
|
-
| `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, OpenCode, and GitHub Copilot CLI; restart the agent session afterward.
|
|
259
|
-
| `lavish-axi setup plugin` | Register the installed package as an [Agent Plugin](https://agent-plugins.org) in VS Code, Cursor, and GitHub Copilot CLI; opt-in, idempotent, no marketplace involved. Reload each client afterward.
|
|
260
|
-
| `lavish-axi server` | Run the local Lavish Editor server.
|
|
249
|
+
| Command | Description |
|
|
250
|
+
| ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
251
|
+
| `lavish-axi` | Show current sessions and usage guidance. |
|
|
252
|
+
| `lavish-axi update` | Check for or apply the latest npm release through the AXI SDK self-updater. |
|
|
253
|
+
| `lavish-axi <html-file>` | Open or resume a Lavish Editor session, with the open-time layout gate enabled by default. Unresolved layout issues from earlier in the session are preserved. Refuses to reopen a session the user explicitly ended from the browser unless `--reopen` is passed. |
|
|
254
|
+
| `lavish-axi poll <html-file>` | Long-poll until the user sends feedback or ends the session; detected layout issues wait in the user's Layout issues inbox and arrive only when queued. Leave no-timeout polls running, or re-run them if interrupted. Codex guidance keeps polls attached to the active turn. On `status: ended`, stop polling and do not reopen uninvited. |
|
|
255
|
+
| `lavish-axi end <html-file>` | End a session as the agent; unlike a user-initiated end from the browser, this still allows a plain reopen later. |
|
|
256
|
+
| `lavish-axi export <html-file>` | Write a portable copy of the artifact: one HTML file with its local assets inlined, so it opens with no server and no sibling files. Remote CDN/font references are left as links. |
|
|
257
|
+
| `lavish-axi share <html-file>` | Publish the artifact (local assets inlined) to [ht-ml.app](https://ht-ml.app), a third-party host not part of Lavish, and print a visitable URL plus a secret update key; shares are public by default, `--private` locks one behind a generated password, and the same command republishes or unpublishes an existing page with `--site`/`--update-key`. |
|
|
258
|
+
| `lavish-axi stop` | Shut down the background server. |
|
|
259
|
+
| `lavish-axi playbook [id]` | List focused artifact guidance or show one playbook; agents must open each matching playbook before writing HTML. |
|
|
260
|
+
| `lavish-axi design` | Show agent-facing design guidance, including optional CDN snippets and the whiteboard (Mermaid) opt-in snippet. |
|
|
261
|
+
| `lavish-axi setup hooks` | Install or repair optional SessionStart hooks for Claude Code, Codex, OpenCode, and GitHub Copilot CLI; restart the agent session afterward. |
|
|
262
|
+
| `lavish-axi setup plugin` | Register the installed package as an [Agent Plugin](https://agent-plugins.org) in VS Code, Cursor, and GitHub Copilot CLI; opt-in, idempotent, no marketplace involved. Reload each client afterward. |
|
|
263
|
+
| `lavish-axi server` | Run the local Lavish Editor server. |
|
|
261
264
|
|
|
262
265
|
Known playbook IDs: `diagram`, `table`, `comparison`, `plan`, `code`, `input`, `slides`.
|
|
263
266
|
One artifact often combines several playbooks, such as a plan that includes a comparison and a diagram, so agents must match against each `use_when` trigger and open every matching playbook before writing HTML.
|
|
@@ -272,8 +275,12 @@ For flows, architecture, state, or sequence diagrams, open the diagram playbook
|
|
|
272
275
|
| `lavish-axi <html-file>` | `--reopen` | Reopen a session the user explicitly ended from the browser; without it, a plain open refuses and explains why instead of reopening uninvited. |
|
|
273
276
|
| `lavish-axi update` | `--check` | Report current vs latest npm version without installing an update. |
|
|
274
277
|
| `lavish-axi export` | `--out <path>` | Write the export to a specific path instead of `<name>.export.html` next to the source. |
|
|
275
|
-
| `lavish-axi share` | `--
|
|
276
|
-
| `lavish-axi share` | `--
|
|
278
|
+
| `lavish-axi share` | `--private` | Make the page private behind a password Lavish generates and returns once; hand it to viewers as a shared secret. |
|
|
279
|
+
| `lavish-axi share` | `--password <pw>` | Make the third-party ht-ml.app page private with a password you supply; viewers must supply the password. An empty or whitespace-only value (an unquoted `$PW` that is unset) is refused rather than publishing a public page. |
|
|
280
|
+
| `lavish-axi share` | `--site <site_id>` | Republish an existing page in place (with `--update-key`) instead of creating a new one; the URL does not change. |
|
|
281
|
+
| `lavish-axi share` | `--update-key <key>` | The secret returned when the page was published; required to republish or unpublish it. |
|
|
282
|
+
| `lavish-axi share` | `--unpublish` | Replace a published page with a locked placeholder (ht-ml.app cannot delete); takes `--site` and `--update-key` and no file. |
|
|
283
|
+
| `lavish-axi share` | `--token <t>` | Attach an optional bearer token (`LAVISH_AXI_HTML_APP_TOKEN`) when creating a page; never required, and rejected on a republish or `--unpublish`, where the `update_key` is the credential. |
|
|
277
284
|
| `lavish-axi poll` | `--agent-reply "..."` | Show the agent's reply in the existing browser chat, conclude delivered work, and return presence to waiting before polling again. |
|
|
278
285
|
| `lavish-axi poll` | `--timeout-ms <ms>` | Test/debug escape hatch only; agents should normally omit it and leave the long poll running. |
|
|
279
286
|
| `lavish-axi stop` | `--port <port>` | Shut down a server running on a non-default port. |
|
package/dist/chrome-client.js
CHANGED
|
@@ -133,8 +133,18 @@ const shareStatus = /** @type {HTMLDivElement} */ (document.getElementById("shar
|
|
|
133
133
|
const shareResult = /** @type {HTMLDivElement} */ (document.getElementById("shareResult"));
|
|
134
134
|
const shareUrlInput = /** @type {HTMLInputElement} */ (document.getElementById("shareUrl"));
|
|
135
135
|
const shareUpdateKeyInput = /** @type {HTMLInputElement} */ (document.getElementById("shareUpdateKey"));
|
|
136
|
+
const shareGenerateInput = /** @type {HTMLInputElement} */ (document.getElementById("shareGenerate"));
|
|
137
|
+
const sharePasswordResult = /** @type {HTMLLabelElement} */ (document.getElementById("sharePasswordResult"));
|
|
138
|
+
const shareUrlResult = /** @type {HTMLLabelElement} */ (document.getElementById("shareUrlResult"));
|
|
139
|
+
const shareUpdateKeyResult = /** @type {HTMLLabelElement} */ (document.getElementById("shareUpdateKeyResult"));
|
|
140
|
+
const shareUpdateKeyNote = /** @type {HTMLParagraphElement} */ (document.getElementById("shareUpdateKeyNote"));
|
|
141
|
+
const shareSiteIdResult = /** @type {HTMLLabelElement} */ (document.getElementById("shareSiteIdResult"));
|
|
142
|
+
const shareSiteIdInput = /** @type {HTMLInputElement} */ (document.getElementById("shareSiteId"));
|
|
143
|
+
const sharePasswordOutput = /** @type {HTMLInputElement} */ (document.getElementById("sharePasswordOut"));
|
|
144
|
+
const copySharePasswordButton = /** @type {HTMLButtonElement} */ (document.getElementById("copySharePassword"));
|
|
136
145
|
const copyShareUrlButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyShareUrl"));
|
|
137
146
|
const copyUpdateKeyButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyUpdateKey"));
|
|
147
|
+
const copyShareSiteIdButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyShareSiteId"));
|
|
138
148
|
const endButton = /** @type {HTMLButtonElement} */ (document.getElementById("end"));
|
|
139
149
|
const copyPathButton = /** @type {HTMLButtonElement} */ (document.getElementById("copyPath"));
|
|
140
150
|
const copyHint = /** @type {HTMLSpanElement} */ (document.getElementById("copyHint"));
|
|
@@ -1898,16 +1908,68 @@ async function exportArtifact() {
|
|
|
1898
1908
|
}
|
|
1899
1909
|
}
|
|
1900
1910
|
|
|
1911
|
+
// ONE owner for the whole result panel. Every path that renders an outcome - dialog open, a
|
|
1912
|
+
// successful publish, a retry after any failure, an indeterminate report, an incomplete 200 -
|
|
1913
|
+
// routes through here, because a row shown or hidden by one path and reset by another is how a
|
|
1914
|
+
// retry after a failed publish came to display "Published" with the once-only update_key still
|
|
1915
|
+
// hidden. Each row is derived from the value it would show, so nothing can be half-rendered.
|
|
1916
|
+
function renderShareResult({ url = "", siteId = "", password = "", updateKey = "" } = {}) {
|
|
1917
|
+
shareUrlInput.value = url;
|
|
1918
|
+
shareUrlResult.hidden = !url;
|
|
1919
|
+
// A self-hosted backend may not return one, and an empty box with a copy button is worse than
|
|
1920
|
+
// no row. Never derive it from the URL: that shape belongs to the backend, not Lavish.
|
|
1921
|
+
shareSiteIdInput.value = siteId;
|
|
1922
|
+
shareSiteIdResult.hidden = !siteId;
|
|
1923
|
+
sharePasswordOutput.value = password;
|
|
1924
|
+
sharePasswordResult.hidden = !password;
|
|
1925
|
+
shareUpdateKeyInput.value = updateKey;
|
|
1926
|
+
shareUpdateKeyResult.hidden = !updateKey;
|
|
1927
|
+
// The note's own copy tells the user to republish with `--site <site id> --update-key <key>`,
|
|
1928
|
+
// so it may only appear when BOTH halves of that credential are on screen. An update key with
|
|
1929
|
+
// no usable site id cannot update anything, and the status line says so instead.
|
|
1930
|
+
shareUpdateKeyNote.hidden = !(updateKey && siteId);
|
|
1931
|
+
shareResult.hidden = !(url || siteId || password || updateKey);
|
|
1932
|
+
// Returned so every sentence in the status line is derived from what was actually rendered.
|
|
1933
|
+
// Copy stamped from the request instead has promised rows the panel does not contain.
|
|
1934
|
+
return { url, siteId, password, updateKey };
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// Wording shared with the CLI's next_step for the same condition, so the two surfaces cannot
|
|
1938
|
+
// drift into describing the same dead end differently.
|
|
1939
|
+
const NO_SITE_ID_WARNING =
|
|
1940
|
+
" The host did not return a site id Lavish can use, and --site is half the republish credential, so this page can NEVER be republished or unpublished even though its update key is in hand.";
|
|
1941
|
+
|
|
1942
|
+
// What the page is gated behind, said only in terms of what the panel can show. A password the
|
|
1943
|
+
// user typed is never echoed by the server, so pointing at a row that was not rendered is the
|
|
1944
|
+
// same confidently-wrong sentence this feature exists to avoid.
|
|
1945
|
+
function publishedVisibilityText(isPublic, rendered, publicText) {
|
|
1946
|
+
if (isPublic) return publicText;
|
|
1947
|
+
return rendered.password ? "behind the password below" : "behind the password you supplied";
|
|
1948
|
+
}
|
|
1949
|
+
|
|
1901
1950
|
function openShareDialog() {
|
|
1902
1951
|
closeMenus();
|
|
1903
1952
|
shareDialog.hidden = false;
|
|
1904
1953
|
shareStatus.textContent = "";
|
|
1905
1954
|
shareStatus.classList.remove("error");
|
|
1906
|
-
|
|
1955
|
+
renderShareResult();
|
|
1956
|
+
shareGenerateInput.checked = false;
|
|
1907
1957
|
sharePasswordInput.value = "";
|
|
1958
|
+
syncSharePasswordInput();
|
|
1908
1959
|
sharePasswordInput.focus();
|
|
1909
1960
|
}
|
|
1910
1961
|
|
|
1962
|
+
// A generated password and a typed one are the same field to the server, so the checkbox owns
|
|
1963
|
+
// the input rather than the two racing to decide what gets published.
|
|
1964
|
+
function syncSharePasswordInput() {
|
|
1965
|
+
const generating = shareGenerateInput.checked;
|
|
1966
|
+
sharePasswordInput.disabled = generating;
|
|
1967
|
+
sharePasswordInput.placeholder = generating
|
|
1968
|
+
? "Lavish will generate one when you publish"
|
|
1969
|
+
: "Leave blank for a public page";
|
|
1970
|
+
if (generating) sharePasswordInput.value = "";
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1911
1973
|
function closeShareDialog() {
|
|
1912
1974
|
shareDialog.hidden = true;
|
|
1913
1975
|
}
|
|
@@ -1920,24 +1982,83 @@ async function copyToButton(value, button, label) {
|
|
|
1920
1982
|
}, 1200);
|
|
1921
1983
|
}
|
|
1922
1984
|
|
|
1985
|
+
function reportIndeterminatePublish(data) {
|
|
1986
|
+
const rendered = renderShareResult({ password: data.password || "" });
|
|
1987
|
+
shareStatus.classList.add("error");
|
|
1988
|
+
const reason = data.error ? data.error + " " : "";
|
|
1989
|
+
const visibility = publishedVisibilityText(data.public, rendered, "PUBLIC - anyone with the link could read it");
|
|
1990
|
+
shareStatus.textContent =
|
|
1991
|
+
reason +
|
|
1992
|
+
"ht-ml.app may or may not have published this page, so treat the outcome as unknown. If it did publish, the page is live " +
|
|
1993
|
+
visibility +
|
|
1994
|
+
", and its URL and update key were lost with the failed response, so it can never be republished or unpublished. Publishing again creates a SECOND page rather than replacing it." +
|
|
1995
|
+
(rendered.password ? " Copy the password now - it is shown once here and Lavish does not store it." : "");
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// An incomplete 200 is NOT an unknown outcome: the host answered, so the page landed. Whatever
|
|
1999
|
+
// fields did arrive are rendered, because a url with no update_key names a live, public-by-default
|
|
2000
|
+
// page whose only write credential is gone - and saying "may or may not" there would throw away
|
|
2001
|
+
// the address Lavish is holding.
|
|
2002
|
+
function reportIncompletePublish(data) {
|
|
2003
|
+
const rendered = renderShareResult({
|
|
2004
|
+
url: data.url || "",
|
|
2005
|
+
siteId: data.site_id || "",
|
|
2006
|
+
password: data.password || "",
|
|
2007
|
+
updateKey: data.update_key || "",
|
|
2008
|
+
});
|
|
2009
|
+
shareStatus.classList.add("error");
|
|
2010
|
+
const visibility = publishedVisibilityText(data.public, rendered, "PUBLIC - anyone with the link can read it");
|
|
2011
|
+
const updateKeyNote = !rendered.updateKey
|
|
2012
|
+
? "No update key came back, and ht-ml.app issues one only once and has no delete, so this page can never be republished or unpublished. "
|
|
2013
|
+
: rendered.siteId
|
|
2014
|
+
? "Copy the update key below - it is issued once. "
|
|
2015
|
+
: "Copy the update key below - it is issued once, though" + NO_SITE_ID_WARNING.slice(1) + " ";
|
|
2016
|
+
shareStatus.textContent =
|
|
2017
|
+
"ht-ml.app accepted this publish, so the page IS live and " +
|
|
2018
|
+
visibility +
|
|
2019
|
+
", but its response was malformed and Lavish could not read the whole result back. " +
|
|
2020
|
+
(rendered.url ? "Its address is below. " : "The response carried no URL, so Lavish cannot show the address. ") +
|
|
2021
|
+
updateKeyNote +
|
|
2022
|
+
"Publishing again creates a SECOND page rather than replacing it." +
|
|
2023
|
+
(rendered.password ? " Copy the password now - it is shown once here and Lavish does not store it." : "");
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1923
2026
|
async function publishShare(event) {
|
|
1924
2027
|
event.preventDefault();
|
|
1925
2028
|
sharePublishButton.disabled = true;
|
|
1926
2029
|
shareStatus.classList.remove("error");
|
|
1927
2030
|
shareStatus.textContent = "Publishing to ht-ml.app...";
|
|
1928
|
-
|
|
1929
|
-
const
|
|
1930
|
-
const
|
|
2031
|
+
renderShareResult();
|
|
2032
|
+
const generating = shareGenerateInput.checked;
|
|
2033
|
+
const password = generating ? "" : sharePasswordInput.value.trim();
|
|
2034
|
+
const passwordProtected = generating || Boolean(password);
|
|
1931
2035
|
try {
|
|
1932
2036
|
const response = await fetch("/api/" + key + "/share", {
|
|
1933
2037
|
method: "POST",
|
|
1934
2038
|
headers: { "content-type": "application/json" },
|
|
1935
|
-
body: JSON.stringify(password ? { password } : {}),
|
|
2039
|
+
body: JSON.stringify(generating ? { generate_password: true } : password ? { password } : {}),
|
|
1936
2040
|
});
|
|
1937
2041
|
const data = await response.json();
|
|
1938
|
-
if (!response.ok)
|
|
1939
|
-
|
|
1940
|
-
|
|
2042
|
+
if (!response.ok) {
|
|
2043
|
+
// Only a host rejection proves nothing was published. On anything else the page may already
|
|
2044
|
+
// be live, and a password minted for this request is the one thing that could still open it,
|
|
2045
|
+
// so it is shown here rather than dying with the failed response.
|
|
2046
|
+
if (data.outcome === "published-incomplete") {
|
|
2047
|
+
reportIncompletePublish(data);
|
|
2048
|
+
return;
|
|
2049
|
+
}
|
|
2050
|
+
if (data.outcome === "indeterminate") {
|
|
2051
|
+
reportIndeterminatePublish(data);
|
|
2052
|
+
return;
|
|
2053
|
+
}
|
|
2054
|
+
throw new Error(data.error || "publish failed");
|
|
2055
|
+
}
|
|
2056
|
+
const rendered = renderShareResult({
|
|
2057
|
+
url: data.url || "",
|
|
2058
|
+
siteId: data.site_id || "",
|
|
2059
|
+
password: data.password || "",
|
|
2060
|
+
updateKey: data.update_key || "",
|
|
2061
|
+
});
|
|
1941
2062
|
const unresolvedAssets = Array.isArray(data.unresolved_local_assets) ? data.unresolved_local_assets : [];
|
|
1942
2063
|
const notices = Array.isArray(data.notices) ? data.notices : [];
|
|
1943
2064
|
const warningCount = unresolvedAssets.length;
|
|
@@ -1951,10 +2072,16 @@ async function publishShare(event) {
|
|
|
1951
2072
|
: passwordProtected
|
|
1952
2073
|
? "Published. This page is PASSWORD-PROTECTED; viewers also need the password."
|
|
1953
2074
|
: "Published. Anyone with the link can view this page.";
|
|
1954
|
-
|
|
2075
|
+
if (rendered.updateKey && !rendered.siteId) shareStatus.textContent += NO_SITE_ID_WARNING;
|
|
2076
|
+
if (rendered.password) {
|
|
2077
|
+
shareStatus.textContent += " Copy the password now - it is shown once here and Lavish does not store it.";
|
|
2078
|
+
}
|
|
1955
2079
|
shareUrlInput.focus();
|
|
1956
2080
|
shareUrlInput.select();
|
|
1957
2081
|
} catch (error) {
|
|
2082
|
+
// Deliberately does NOT clear the panel. It is already cleared before the fetch, so the only
|
|
2083
|
+
// thing this could reach is a result the success path already rendered - and wiping that
|
|
2084
|
+
// destroys the once-issued update_key of a page that definitely published.
|
|
1958
2085
|
shareStatus.classList.add("error");
|
|
1959
2086
|
shareStatus.textContent = error instanceof Error ? error.message : String(error);
|
|
1960
2087
|
} finally {
|
|
@@ -3087,6 +3214,10 @@ shareDialog.addEventListener("click", (event) => {
|
|
|
3087
3214
|
});
|
|
3088
3215
|
copyShareUrlButton.onclick = () => copyToButton(shareUrlInput.value, copyShareUrlButton, "Copy URL");
|
|
3089
3216
|
copyUpdateKeyButton.onclick = () => copyToButton(shareUpdateKeyInput.value, copyUpdateKeyButton, "Copy key");
|
|
3217
|
+
copySharePasswordButton.onclick = () =>
|
|
3218
|
+
copyToButton(sharePasswordOutput.value, copySharePasswordButton, "Copy password");
|
|
3219
|
+
copyShareSiteIdButton.onclick = () => copyToButton(shareSiteIdInput.value, copyShareSiteIdButton, "Copy site ID");
|
|
3220
|
+
shareGenerateInput.onchange = syncSharePasswordInput;
|
|
3090
3221
|
endButton.onclick = () => {
|
|
3091
3222
|
closeMenus();
|
|
3092
3223
|
endSession();
|
package/dist/chrome.css
CHANGED
|
@@ -803,6 +803,23 @@ body.lavish {
|
|
|
803
803
|
.share-grid input::placeholder {
|
|
804
804
|
color: var(--fg-label);
|
|
805
805
|
}
|
|
806
|
+
.share-grid .share-check {
|
|
807
|
+
grid-template-columns: auto 1fr;
|
|
808
|
+
grid-auto-flow: column;
|
|
809
|
+
align-items: center;
|
|
810
|
+
gap: var(--space-6);
|
|
811
|
+
color: var(--fg-muted);
|
|
812
|
+
font-size: var(--text-sm);
|
|
813
|
+
font-weight: var(--w-regular);
|
|
814
|
+
letter-spacing: 0;
|
|
815
|
+
text-transform: none;
|
|
816
|
+
cursor: pointer;
|
|
817
|
+
}
|
|
818
|
+
.share-grid .share-check input {
|
|
819
|
+
width: auto;
|
|
820
|
+
padding: 0;
|
|
821
|
+
accent-color: var(--accent);
|
|
822
|
+
}
|
|
806
823
|
.share-status {
|
|
807
824
|
min-height: 20px;
|
|
808
825
|
margin-top: var(--space-8);
|
|
@@ -824,6 +841,9 @@ body.lavish {
|
|
|
824
841
|
.share-result[hidden] {
|
|
825
842
|
display: none;
|
|
826
843
|
}
|
|
844
|
+
.share-result label[hidden] {
|
|
845
|
+
display: none;
|
|
846
|
+
}
|
|
827
847
|
.share-copy-row {
|
|
828
848
|
display: flex;
|
|
829
849
|
gap: var(--space-6);
|