yamlover 0.3.10 → 0.3.12

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.
Files changed (33) hide show
  1. package/README.md +186 -143
  2. package/bin/yamlover.js +51 -2
  3. package/dist/agent-docs/AGENTS.md +267 -0
  4. package/dist/agent-docs/CLAUDE.md +7 -0
  5. package/dist/builtin-taxonomy/$defs/.yamlover/meta.yamlover +25 -0
  6. package/dist/builtin-taxonomy/$defs/annotation +18 -0
  7. package/dist/builtin-taxonomy/$defs/board +15 -0
  8. package/dist/builtin-taxonomy/$defs/chapter +19 -0
  9. package/dist/builtin-taxonomy/$defs/chunk +6 -0
  10. package/dist/builtin-taxonomy/$defs/config +20 -0
  11. package/dist/builtin-taxonomy/$defs/fragment +44 -0
  12. package/dist/builtin-taxonomy/$defs/tag +17 -0
  13. package/dist/builtin-taxonomy/$defs/task +33 -0
  14. package/dist/builtin-taxonomy/$defs/workflow +18 -0
  15. package/dist/builtin-taxonomy/tags/.yamlover/body.yamlover +46 -0
  16. package/dist/client/assets/{decoded-M-9MjrR3.js → decoded-DVR13gJ5.js} +1 -1
  17. package/dist/client/assets/{djvu-M6ksPMNz.js → djvu-CZbDJKwH.js} +1 -1
  18. package/dist/client/assets/{docx-D4h81KWC.js → docx-BWH54ddd.js} +1 -1
  19. package/dist/client/assets/{heic-BoSpW_tk.js → heic-CV3oZ6BF.js} +1 -1
  20. package/dist/client/assets/{imagemap-CIJFPvXv.js → imagemap-DRJrF_aD.js} +1 -1
  21. package/dist/client/assets/index-CCUPHCB4.js +620 -0
  22. package/dist/client/assets/{index-DHqicFJy.css → index-D5xvCkZE.css} +1 -1
  23. package/dist/client/assets/{map-MATRMboW.js → map-Y_zmEjLV.js} +1 -1
  24. package/dist/client/assets/{paged-BQLUFBor.js → paged-DKLVDozm.js} +1 -1
  25. package/dist/client/assets/{panzoom-Do4jskMZ.js → panzoom-NbtFbptF.js} +1 -1
  26. package/dist/client/assets/{pdf-BShpl2D5.js → pdf-DM1MQkYa.js} +1 -1
  27. package/dist/client/assets/{psd-DIxfuhrV.js → psd-PXeZB_nC.js} +1 -1
  28. package/dist/client/assets/{spreadsheet-DL0NoDcm.js → spreadsheet-B-3t2XCi.js} +1 -1
  29. package/dist/client/assets/{tiff-C7zPNKoD.js → tiff-BQq-nYyI.js} +1 -1
  30. package/dist/client/index.html +2 -2
  31. package/dist/server.js +545 -339
  32. package/package.json +1 -1
  33. package/dist/client/assets/index-DZ1FuhId.js +0 -620
package/README.md CHANGED
@@ -3,15 +3,19 @@
3
3
  Browse a yamlover tree in the web browser.
4
4
 
5
5
  ```console
6
- $ npx yamlover [ROOT] [--port N] [--host ADDR] [--no-gitignore]
6
+ $ npx yamlover [ROOT] [--port N] [--headless] [--host ADDR] [--no-gitignore] [--prod]
7
7
  ```
8
8
 
9
- `ROOT` is any yamlover entity — a directory with a `.yamlover/schema.yaml`, a
10
- plain directory, or a single file (the same shapes [`walker`](../walker/)
11
- understands). It defaults to the current directory. The command starts a local
12
- web server (on `0.0.0.0` by default, so it is reachable from the network — use
13
- `--host 127.0.0.1` to keep it private) and serves a React single-page app that
14
- lets you browse the entity starting from `ROOT`.
9
+ `ROOT` is any yamlover entity — a project directory (one with a `.yamlover/`),
10
+ a plain directory, or a single file. It defaults to the current directory. The
11
+ command starts a local web server **bound to `127.0.0.1` (local only) by
12
+ default**, the safe default for a personal viewer and for the desktop wrapper
13
+ (`tools/desktop`) and serves a React single-page app that browses the entity
14
+ starting from `ROOT`. Pass `--headless` to bind `0.0.0.0` (all interfaces, e.g.
15
+ for remote access with no GUI), or `--host ADDR` for an explicit override.
16
+ `--prod` forces the prebuilt static client + bundled server (the default when
17
+ the dev sources aren't present, e.g. an installed package); without it, a
18
+ checkout runs the client from source via Vite with HMR.
15
19
 
16
20
  The page is split into two independently scrolling panes:
17
21
 
@@ -20,164 +24,206 @@ The page is split into two independently scrolling panes:
20
24
  `05-scalar-as-file` is listed and clickable). A node is *expandable* when it is
21
25
  a container with children and has no *active* renderer (see *Renderers*); the
22
26
  first three levels load expanded, and deeper branches load lazily via the
23
- chevron. Each entry is labeled by its schema `title` (or an instance `title`
24
- child) when present, otherwise by its key (objects) or `[index]` (arrays).
27
+ chevron. Each entry is labeled by its `title` when present, otherwise by its
28
+ key (objects) or `[index]` (arrays).
25
29
 
26
- Each row carries a **type/format icon** (chosen by the schema `format`, falling
27
- back to `type` — `{}`/`[]` for object/array, `"`/`#`/`◧` for
30
+ Each row carries a **type/format icon** (chosen by the node's `format`, falling
31
+ back to its type — `{}`/`[]` for object/array, `"`/`#`/`◧` for
28
32
  string/number/boolean, 📅/✉️/🔗/🖼️… for formats). The tree is agnostic to a
29
33
  node's *concrete* (how it is stored) — that shows in the node view's tags.
30
- - **Right — the selected node**, in one of four representations (tabs):
31
- - **yaml-schema** *(default)* our schema (the yamlover schema concrete) as YAML.
32
- - **yaml** — the node's value as YAML.
33
- - **json** — the node's value as JSON.
34
- - **json-schema** — the standard instance JSON Schema (every leaf `const`).
35
-
36
- All four behave **identically**: syntax-highlighted, **one level deep**, with
37
- every nested node shown as a **hyperlink** you click to descend (never inlined)
38
- `{ object with N properties }`, `[ array with M items ]`, or
39
- `< binary of N bytes >`. A registered renderer, if any, replaces the value
40
- views. The selected representation is part of the URL (`?format=`), so it is
41
- preserved as you navigate and is shareable.
42
-
43
- A binary leaf's bytes are read only when you select it and view its value: the
44
- yaml view shows a YAML `!!binary` block, json shows the `{format,size,base64}`
45
- metadata.
46
-
47
- Surfaced *stray* files (those the schema does not describe) honor `.gitignore` by
34
+ - **Right — the selected node.** A node with a registered renderer shows that
35
+ renderer's view; every node also offers a set of **data-representation tabs**,
36
+ always including:
37
+ - **yamlover** *(default)* — the node's value in yamlover (YAML-family) syntax.
38
+ - **yamlover/schema** — the node's instance schema.
39
+
40
+ plus **json5p** for a JSON-family file. All representations behave
41
+ **identically**: syntax-highlighted, **one level deep**, with every nested node
42
+ shown as a **hyperlink** you click to descend (never inlined)
43
+ `{ object with N properties }`, `[ array with M items ]`, or
44
+ `< binary of N bytes >`. The selected representation is part of the URL
45
+ (`?format=`), so it is preserved as you navigate and is shareable.
46
+
47
+ A binary leaf's bytes are read only when you select it and view its value.
48
+
49
+ The page updates **live**: an FS watcher re-indexes on external edits and pushes
50
+ what changed over an SSE stream (`/api/events`), so every surface (tree, node
51
+ view, tag pages) refreshes without a reload.
52
+
53
+ Surfaced *stray* files (those not described by the tree) honor `.gitignore` by
48
54
  default — `node_modules/`, build output, etc. are hidden. Pass `--no-gitignore`
49
- to show everything. Schema-described children are always shown.
55
+ to show everything.
50
56
 
51
57
  The browser URL is the node's path in **JSON space** — `/examples[0]/markup` —
52
- never schema space, so there are no `properties` segments, plus `?format=` for
53
- the representation. Each key is percent-encoded, so a key that itself contains a
54
- `/` (e.g. `@vitejs/plugin-react`) stays a single segment. It updates as you
55
- navigate and is shareable / back-button friendly opening a deep link expands
56
- the TOC along the path and selects (and scrolls to) the target node.
58
+ plus `?format=` for the representation. Each key is percent-encoded, so a key
59
+ that itself contains a `/` (e.g. `@vitejs/plugin-react`) stays a single segment.
60
+ It updates as you navigate and is shareable / back-button friendly — opening a
61
+ deep link expands the TOC along the path and selects (and scrolls to) the target.
57
62
 
58
63
  ## How it works
59
64
 
60
- This is a TypeScript reimplementation of the read side of [`walker`](../walker/)
61
- (concretes, `$ref`/`$defs`, inline `const`, collapsed files, expanded
62
- subdirectories), plus a small server and SPA. There is **no build step**: the
63
- launcher runs [Vite](https://vitejs.dev) in middleware mode, serving the client
64
- from source with HMR and loading the server-side materializer through Vite's
65
- `ssrLoadModule`.
66
-
67
- Leaf bytes are **read lazily**: materialization builds the tree's structure but
68
- does not read a leaf's content until that node is actually serialized. So listing
69
- or eliding a node never reads its file `value: 30` is read only when its node is
70
- shown, and a binary blob only when its base64 is requested. The materialized tree
71
- is cached per server for a short window, so a burst of clicks does not re-read the
72
- filesystem; edits show up after the window on reload.
65
+ The server is backed by the yamlover **engine** (`tools/engine`): `walkDir`
66
+ turns the directory concrete into the parser's IR, and a `Store` (a SQLite
67
+ property-graph index) holds nodes and pointer edges. The HTTP layer
68
+ (`src/server/engine-api.ts`) reads from the `Store` and emits the response
69
+ shapes the React client consumes (the `$yamloverLink` / `$yamloverBinary` /
70
+ `$yamloverMixed` markers, the schema view).
71
+
72
+ The on-disk index lives at `<root>/.yamlover/index.db`. It is a derived cache
73
+ with a persistent **file manifest** (path + hash + size + mtime): startup
74
+ re-indexes against it (an offline reconcile unchanged blobs are never re-read,
75
+ so it is cheap), and the FS watcher re-indexes on edits and broadcasts the diff
76
+ over `/api/events`.
77
+
78
+ **Long-running work runs as background tasks** (`src/server/tasks.ts`): the HTTP
79
+ server listens immediately and serves the previous index (or an empty one on a
80
+ cold start) while the initial walk and the background hasher (which fills in
81
+ content hashes for large blobs the walk no longer reads) run. Store-mutating
82
+ jobs (index, mv, paste, annotate) serialize through one writer queue; reads
83
+ never wait. Progress lands both on the console and in the web UI (SSE `task`
84
+ frames + `GET /api/tasks`).
85
+
86
+ Leaf bytes are read **lazily**: materialization builds structure but does not
87
+ read a leaf's content until that node is serialized.
88
+
89
+ ### Build
90
+
91
+ There are two run modes:
92
+
93
+ - **Dev** (a checkout): the launcher runs [Vite](https://vitejs.dev) in
94
+ middleware mode, serving the client from source with HMR and loading the
95
+ server handler through Vite's `ssrLoadModule`.
96
+ - **Prod** (`--prod`, or an installed package): the client is a prebuilt static
97
+ SPA under `dist/client` and the server handler is a single bundle at
98
+ `dist/server.js`. `scripts/build.mjs` produces both — `vite build` for the
99
+ client and `esbuild` for the server (bundling the engine, parser, and the
100
+ `ignore` / `js-yaml` / `xxhash-wasm` deps, plus the dynamically-imported
101
+ thumbnail codecs). It runs on `prepack`, so the published npm package ships
102
+ `dist/` and has **no runtime dependencies**.
73
103
 
74
104
  ### API
75
105
 
76
- All endpoints take a JSON-space `path` (default `/`); `json` and `schema` take an
77
- optional `depth` (container-nesting limit).
106
+ All endpoints take a JSON-space `path` (default `/`); the value/schema endpoints
107
+ take an optional `depth` (container-nesting limit).
78
108
 
79
109
  | endpoint | returns |
80
110
  |----------|---------|
81
- | `GET /api/info` | `{ root }` the root label (the served entity's yamlover title, else its directory name): the breadcrumb head and TOC root |
82
- | `GET /api/tree?path&depth` | the table of contents rooted at `path`, `depth` levels deep (default 3) — fetched again per branch for lazy expansion |
83
- | `GET /api/json?path&depth` | the node's value, one level deep (`depth` default 1); nested containers become link markers (add `&binary=1` for a binary leaf's base64) |
84
- | `GET /api/schema?path&depth` | the node's instance JSON Schema, one level deep, with the same link markers |
85
- | `GET /api/blob?path` | a file-backed node's **raw bytes**, with its (inferred) format as the `Content-Type` — what the image / html / pdf / djvu / markup renderers read, instead of base64 over `json` |
111
+ | `GET /api/info` | the breadcrumb head (the root label) |
112
+ | `GET /api/tree?path&depth` | the TOC subtree at `path`, `depth` levels deep (default 3) — fetched again per branch for lazy expansion |
113
+ | `GET /api/json?path&depth&binary` | the node's value, one level deep (`depth` default 1); nested containers become link markers (`&binary=1` for a binary leaf's base64) |
114
+ | `GET /api/schema?path&depth` | the node's instance schema, with the same link markers |
115
+ | `GET /api/blob?path` | a file-backed node's **raw bytes**, with its (inferred) format as the `Content-Type` |
116
+ | `GET /api/thumb?path&w&h` | a lazily-generated thumbnail of a file-backed blob |
117
+ | `GET /api/tagged?path` | the materials filed under a tag (annotations → targets) |
118
+ | `GET /api/annotations?path` | the annotations on a node |
119
+ | `GET /api/query?q&path` | the query evaluator (colon match templates) |
120
+ | `GET /api/dangling` | pointers that did not resolve at index time |
121
+ | `GET /api/events` | SSE: `{type:"diff",…}` reindex diffs + `{type:"task",…}` progress |
122
+ | `GET /api/tasks` | long-running tasks in flight (a snapshot for a fresh page) |
123
+ | `POST /api/reindex` | manual reconcile (the watcher's fallback) |
124
+ | `POST /api/paste` | clipboard paste / upload (text or files) |
125
+ | `POST /api/mv` | mediated move (surgical inbound-ref rewrite + auto-relink) |
126
+ | `POST /api/tag` | create-on-miss a tag in the taxonomy |
127
+ | `POST / DELETE /api/annotate` | add / remove an annotation on a target |
128
+ | `POST /api/fragment` | upsert a fragment (region of a target) |
129
+ | `POST /api/board` | board mutations |
130
+ | `POST /api/agent-docs` | write the LLM-agent guide (`AGENTS.md` + `CLAUDE.md`) into the root |
86
131
 
87
132
  A **link marker** — `{ "$yamloverLink": { kind, path, count|size } }` — stands in
88
133
  for a node shown only as a link (a nested container past the one-level view, or
89
134
  any binary leaf); the client renders it as a `{ object with N properties }`,
90
135
  `[ array with M items ]`, or `< binary of N bytes >` hyperlink. The same marker
91
- appears in both the value and the schema, so every representation (YAML/JSON ×
92
- data/schema) renders identically just a syntax choice over one structure. A
93
- selected binary leaf's bytes arrive as `{ "$yamloverBinary": {format,size,base64} }`.
136
+ appears in both the value and the schema, so every representation renders
137
+ identically. A selected binary leaf's bytes arrive as
138
+ `{ "$yamloverBinary": {format,size,base64} }`.
94
139
 
95
140
  ### Renderers
96
141
 
97
- A renderer is keyed by a **(type, format)** tuple the JSON-Schema `type` plus
98
- its `format` — the same key the TOC icons and the link markers carry. A node with
99
- no matching renderer falls through to the default tabbed view (YAML/JSON ×
100
- data/schema) and expands normally in the TOC.
142
+ A renderer declares an **`accepts` predicate** over a node's **type facets**
143
+ (value-type, `format`, and the keyed/ordinal capability flags) most are
144
+ `byFormat("…")`, which matches on the node's format and tolerates the other
145
+ facets (so tagging a markdown node doesn't break its markdown rendering). The
146
+ most specific matching renderer wins; a node with no match falls through to the
147
+ default data-representation tabs and expands normally in the TOC.
101
148
 
102
- The registry lives in `src/client/renderers/`. A renderer participates three ways,
103
- all on the one tuple:
149
+ The registry lives in `src/client/renderers/`. A renderer participates several
150
+ ways:
104
151
 
105
152
  - **`render`** — the full RHS page (its tab is the node's default representation).
106
- - **`renderChunk`** — its *inline* form, when embedded in another renderer's page.
107
- A renderer composes children by routing each to the renderer for the child's own
108
- tuple: the `chapter` renderer draws each of its `chunks` by delegating to that
109
- chunk's renderer (a `string`/`text/markdown` chunk → the `text` renderer; an
110
- `image/png` chunk would route to an image renderer, no change to `chapter`).
153
+ - **`renderChunk`** — its *inline* form, when embedded in another renderer's page
154
+ (e.g. the `chapter` renderer draws each chunk by delegating to that chunk's
155
+ own renderer).
111
156
  - **`tocView`** — how the node appears in the TOC: which children are navigable,
112
- whether it expands, and whether they are loaded (default: its own children,
113
- lazily loaded). A renderer can unwrap or filter `chapter` surfaces its
114
- subchapters directly (from its `children` array) and keeps its `chunks` off the
115
- tree, so the TOC reads as a table of contents.
116
-
117
- A renderer may also declare **`depth`** — the value depth `NodeView` fetches for
118
- it (default 1; `chapter` needs 2 to reach its chunk/subchapter elements).
119
-
120
- Registered today:
121
-
122
- | renderer | (type, format) | draws with |
123
- |----------|----------------|------------|
124
- | `chapter` | `object` / `x-yamlover-chapter` | numbered chunks + subchapter links |
125
- | `tag` | `object` / `x-yamlover-tag` | a hierarchy diagram: supertag (containment parent) and subtags (children) wired to the current tag |
126
- | `text` | `string` / `text/markdown` | [marked](https://marked.js.org) |
127
- | `asciidoc` | `string` / `text/asciidoc` | [@asciidoctor/core](https://asciidoctor.org) |
128
- | `image` | `binary` / `image/*` | native `<img>` |
129
- | `html` | `binary` / `text/html` | sandboxed `<iframe>` |
130
- | `pdf` | `binary` / `application/pdf` | [pdf.js](https://mozilla.github.io/pdf.js/) via [react-pdf](https://github.com/wojtekmaj/react-pdf) |
131
- | `djvu` | `binary` / `image/vnd.djvu` | [DjVu.js](https://djvu.js.org) (vendored) |
157
+ whether it expands and is loaded. A renderer can unwrap or filter (e.g.
158
+ `chapter` surfaces its subchapters and keeps its chunks off the tree).
159
+ - **`depth`** the value depth `NodeView` fetches for it (default 1).
160
+
161
+ Registered today (a representative slice — the registry is the source of truth):
162
+
163
+ | renderer | matches (format) | draws with |
164
+ |----------|------------------|------------|
165
+ | `chapter` | `x-yamlover-chapter` | numbered chunks + subchapter links |
166
+ | `tag` / `board` | `x-yamlover-tag` / `x-yamlover-board` | tag-hierarchy diagram / board (handled outside the specificity loop) |
167
+ | `task` | `x-yamlover-task` | task view |
168
+ | `markdown` / `marklower` | `text/markdown` | [marked](https://marked.js.org) |
169
+ | `asciidoc` | `text/asciidoc` | [@asciidoctor/core](https://asciidoctor.org) |
170
+ | `csv` | `text/csv`, `text/tab-separated-values` | a table |
171
+ | `plaintext` | `text/plain` | preformatted text |
172
+ | `latex` | `text/x-latex` | [KaTeX](https://katex.org) |
173
+ | `plantuml` | `text/x-plantuml` | rendered diagram |
174
+ | `map` | KML / KMZ | [Leaflet](https://leafletjs.com) |
175
+ | `image` | `image/png`, `jpeg`, `gif`, `webp`, `avif`, `svg+xml`, … | native `<img>` |
176
+ | `html` | `text/html` | sandboxed `<iframe>` |
177
+ | `pdf` | `application/pdf` | [pdf.js](https://mozilla.github.io/pdf.js/) via react-pdf |
178
+ | `djvu` | `image/vnd.djvu` | [DjVu.js](https://djvu.js.org) (vendored) |
179
+ | `epub` / `fb2` | `application/epub+zip` / FictionBook | paged e-book view |
180
+ | `docx` / `doc` / `rtf` | Word / RTF | [mammoth](https://github.com/mwilliamson/mammoth.js) etc. |
181
+ | `spreadsheet` | xlsx / xls | [SheetJS](https://sheetjs.com) |
182
+ | `psd` / `tiff` / `heic` | Photoshop / TIFF / HEIC | decoded to a canvas/image |
132
183
 
133
184
  Adding a shape is still a single registry entry.
134
185
 
186
+ Two implementation notes:
187
+
188
+ - **DjVu has no native browser support**, so it is decoded client-side by
189
+ **DjVu.js**, vendored as a prebuilt bundle at `src/client/vendor/djvu.js`.
190
+ The library is **GPL-v2** (see `src/client/vendor/README.md` for provenance);
191
+ the rest of this package is not.
192
+ - **pdf.js and DjVu.js (and several heavier codecs) reach for browser globals at
193
+ import time**, which would break the (Node/jsdom) test run, so their renderers
194
+ are **lazy-loaded** — importing the registry never pulls them in until such a
195
+ node is actually shown.
196
+
135
197
  ### File rendering and format inference
136
198
 
137
- The bottom seven renderers above turn the browser into a viewer for the common
138
- file types a tree carries. They hang off one rule, applied to any file-backed
139
- node that carries **no explicit schema `format`**: the server **infers a format
140
- from the file extension** (`formatFromExt` in `src/server/yamlover.ts`). So a
141
- stray `.pdf`, `.png`, or `.md` or one declared with only `concrete: file`/
142
- `file/binary` — renders without a `format:` line. An explicit schema `format`
143
- always wins.
199
+ The file renderers turn the browser into a viewer for the common file types a
200
+ tree carries. They hang off one rule, applied to any file-backed node that
201
+ carries **no explicit `format`**: the server **infers a format from the file
202
+ extension**. So a stray `.pdf`, `.png`, or `.md` renders without a `format:`
203
+ line. An explicit `format` always wins.
144
204
 
145
205
  Inference splits two ways by how the renderer consumes the file:
146
206
 
147
- - **Served as bytes** — images, `application/pdf`, `text/html`, `image/vnd.djvu`
148
- become `file/binary`. Their renderer points an `<img>`/`<iframe>`/pdf-loader at
149
- **`/api/blob`** (or, for DjVu, fetches the `ArrayBuffer`), so the bytes stream
150
- straight from disk with no base64 round-trip.
207
+ - **Served as bytes** — images, `application/pdf`, `text/html`, `image/vnd.djvu`,
208
+ etc. Their renderer points an `<img>`/`<iframe>`/loader at **`/api/blob`** (or
209
+ fetches the `ArrayBuffer`), so the bytes stream straight from disk with no
210
+ base64 round-trip.
151
211
  - **Read as text** — `.md`/`.adoc` keep a **string** value (the file's text),
152
- which the `text`/`asciidoc` renderer parses to HTML. (This preserves the
153
- existing "a `.md` file is a string" behavior.)
154
-
155
- Two implementation notes:
156
-
157
- - **DjVu has no native browser support**, so it is decoded client-side by
158
- **DjVu.js**, vendored as a prebuilt bundle at `src/client/vendor/djvu.js`.
159
- The library is **GPL-v2** (see `src/client/vendor/README.md` for provenance and
160
- how to regenerate); the rest of this package is not.
161
- - **pdf.js and DjVu.js reach for browser globals at import time**, which would
162
- break the (Node/jsdom) test run. So the `pdf` and `djvu` renderers are
163
- **lazy-loaded** (`React.lazy`) — importing the registry, as the TOC and the
164
- tests do, never pulls them in until a PDF/DjVu node is actually shown.
165
-
166
- `examples/18-pdf-tags` exercises this: a library of real papers stored as PDFs
167
- and one saved HTML page, each rendered in place.
212
+ which the renderer parses to HTML.
168
213
 
169
214
  ## Requirements
170
215
 
171
- - Node.js 18+
216
+ - Node.js **22+**.
172
217
 
173
- Dependencies (React, Vite, js-yaml, ignore, plus the renderer libraries
174
- `react-pdf`, `marked`, `@asciidoctor/core`) install with `npm install` in this
175
- directory; `npx yamlover` then runs `bin/yamlover.js`. DjVu.js is **vendored**
218
+ There are **no runtime dependencies**: the client deps are bundled into
219
+ `dist/client` by `vite build` and the server deps into `dist/server.js` by
220
+ esbuild at `prepack`. The `devDependencies` (React, Vite, the engine/parser, and
221
+ the renderer libraries) are build- and test-time only. DjVu.js is **vendored**
176
222
  (`src/client/vendor/djvu.js`), not an npm dependency.
177
223
 
178
224
  ## Tests
179
225
 
180
- Both sides are covered by [Vitest](https://vitest.dev) (it runs the TypeScript
226
+ This package is covered by [Vitest](https://vitest.dev) (it runs the TypeScript
181
227
  directly, no build):
182
228
 
183
229
  ```console
@@ -185,30 +231,27 @@ $ npm test # run once
185
231
  $ npm run test:watch
186
232
  ```
187
233
 
188
- - **Server** (`test/*.test.ts`, Node) against the `examples/` fixtures: path
189
- encoding round-trips (incl. keys with `/`), materialization and `$ref`/`$defs`
190
- equivalence, one-level link markers (object/array/binary) in value and schema,
191
- lazy binaries and the `!!binary` payload, the non-YAML → raw-text fallback,
192
- `buildTree` (all nodes, depth, titles), `.gitignore` filtering, and the API
193
- endpoints (`/api/info`, `/api/tree`, `/api/json`, `/api/schema`).
194
- - **Client** (`test/client/*`, jsdom + React Testing Library) — path/URL helpers
195
- and breadcrumbs, type/format icons, the renderer registry, the unified
196
- `Render` (scalars, `{ object with N properties }` / `[ array with M items ]` /
197
- `< binary of N bytes >` links, `!!binary`, YAML vs JSON), the `Tree`
198
- (selection, lazy expand, leaves), `NodeView` (markers, tab switches, binary,
199
- schema), and `App` (breadcrumb head + TOC).
234
+ This server suite is **separate** from the repository root's `npm test`, which
235
+ runs only the parser and engine suites. CI gates on the server suite, so run
236
+ both before pushing. (Run everything from the project root — see the root
237
+ `package.json`.)
200
238
 
201
239
  ## Layout
202
240
 
203
241
  ```
204
- bin/yamlover.js CLI entry — arg parsing + Vite middleware-mode server
205
- src/server/ TypeScript port of the walker read side + JSON API
206
- yamlover.ts materialize a logical tree (lazy leaves); toPlain/toSchema/buildTree; formatFromExt
207
- gitignore.ts .gitignore predicate for surfaced stray files
208
- api.ts /api/info, /api/tree, /api/json, /api/schema, /api/blob
209
- src/client/ the React SPA (tree, node view, render, icons, paths)
210
- renderers/ (type, format) registry + chapter/text/asciidoc/media/pdf/djvu
211
- vendor/djvu.js prebuilt DjVu.js bundle (GPL-v2; see vendor/README.md)
212
- test/ Vitest suite for the server logic
213
- index.html SPA shell
242
+ bin/yamlover.js CLI entry — arg parsing + dev (Vite) / prod (dist) wiring
243
+ scripts/build.mjs prod build: vite build dist/client, esbuild dist/server.js
244
+ src/server/ the engine-backed JSON API
245
+ engine-api.ts createHandlers: all /api/* routes, backed by the engine Store
246
+ embed.ts annotation / fragment / thumbnail embedding (overlay writes)
247
+ node-kind.ts node-kind classification (object|array|scalar|binary|omni|mix)
248
+ tasks.ts background task registry + SSE task frames
249
+ gitignore.ts .gitignore predicate for surfaced stray files
250
+ extract/ per-type extractors (thumbnails, fragments)
251
+ agent-docs/ the AGENTS.md / CLAUDE.md guide installed by POST /api/agent-docs
252
+ src/client/ the React SPA (tree, node view, render, icons, paths, live SSE)
253
+ renderers/ facet-predicate renderer registry + per-format renderers
254
+ vendor/djvu.js prebuilt DjVu.js bundle (GPL-v2; see vendor/README.md)
255
+ test/ Vitest suite (server logic + client components)
256
+ index.html SPA shell
214
257
  ```
package/bin/yamlover.js CHANGED
@@ -48,6 +48,20 @@ let port = 5173;
48
48
  let host = "127.0.0.1";
49
49
  let gitignore = true; // hide .gitignore'd stray files by default
50
50
  let prodFlag = false; // force production (static) mode even in the repo checkout
51
+ // URL prefix to serve the whole app under (e.g. `/demo/abc123`), so many instances can sit behind
52
+ // one host on distinct paths (the demo server). "" = served at the document root (the normal case).
53
+ // Seeded from $BASE_PATH so a shell-less image (e.g. distroless) can inject it via env without
54
+ // needing `sh -c` to expand it into a `--base-path` flag; an explicit flag below still overrides.
55
+ let basePath = process.env.BASE_PATH ?? "";
56
+ // Normalize a base path: leading `/`, no trailing `/`; `""`/`"/"` → disabled.
57
+ function normBase(s) {
58
+ let b = (s ?? "").trim();
59
+ if (b === "" || b === "/") return "";
60
+ if (!b.startsWith("/")) b = "/" + b;
61
+ if (b.endsWith("/")) b = b.slice(0, -1);
62
+ return b;
63
+ }
64
+ basePath = normBase(basePath); // normalize the $BASE_PATH seed (a flag below re-normalizes its own value)
51
65
  const argv = process.argv.slice(2);
52
66
  for (let i = 0; i < argv.length; i++) {
53
67
  const a = argv[i];
@@ -56,11 +70,14 @@ for (let i = 0; i < argv.length; i++) {
56
70
  else if (a === "--headless") host = "0.0.0.0"; // serve on all interfaces (no GUI / remote access)
57
71
  else if (a === "--host") host = argv[++i];
58
72
  else if (a.startsWith("--host=")) host = a.slice("--host=".length);
73
+ else if (a === "--base-path") basePath = normBase(argv[++i]);
74
+ else if (a.startsWith("--base-path=")) basePath = normBase(a.slice("--base-path=".length));
59
75
  else if (a === "--no-gitignore") gitignore = false;
60
76
  else if (a === "--prod") prodFlag = true;
61
77
  else if (a === "--help" || a === "-h") {
62
- console.log("usage: npx yamlover [ROOT] [--port N] [--headless] [--host ADDR] [--no-gitignore] [--prod]");
78
+ console.log("usage: npx yamlover [ROOT] [--port N] [--headless] [--host ADDR] [--base-path PREFIX] [--no-gitignore] [--prod]");
63
79
  console.log(" default: serve on 127.0.0.1 (local only); --headless serves on all interfaces");
80
+ console.log(" --base-path PREFIX: serve the whole app under PREFIX (e.g. /demo/abc) instead of /");
64
81
  process.exit(0);
65
82
  } else if (!a.startsWith("-")) rootArg = a;
66
83
  }
@@ -85,7 +102,21 @@ let handle;
85
102
  let serveClient;
86
103
 
87
104
  const server = createHttpServer((req, res) => {
88
- const url = new URL(req.url, "http://localhost");
105
+ let url = new URL(req.url, "http://localhost");
106
+ // Under `--base-path`, strip the prefix up front (and rewrite req.url) so every downstream —
107
+ // the engine API (exact `/api/...` matches), the static server, and the Vite middleware — sees
108
+ // root-relative paths and stays oblivious to the prefix. Anything outside the prefix is 404.
109
+ if (basePath) {
110
+ const p = url.pathname;
111
+ if (p === basePath || p.startsWith(basePath + "/")) {
112
+ req.url = (p.slice(basePath.length) || "/") + url.search;
113
+ url = new URL(req.url, "http://localhost");
114
+ } else {
115
+ res.statusCode = 404;
116
+ res.end("not found");
117
+ return;
118
+ }
119
+ }
89
120
  if (url.pathname.startsWith("/api/")) {
90
121
  handle(req, res, url);
91
122
  return;
@@ -193,6 +224,7 @@ if (prod) {
193
224
  try {
194
225
  let html = fs.readFileSync(indexHtmlPath, "utf-8");
195
226
  html = await vite.transformIndexHtml(url.pathname, html);
227
+ html = injectBase(html); // base-path-aware shell (no-op without --base-path)
196
228
  res.setHeader("Content-Type", "text/html; charset=utf-8");
197
229
  res.end(html);
198
230
  } catch (e) {
@@ -210,6 +242,7 @@ if (prod) {
210
242
  handle = createHandlers(dataRoot, {
211
243
  gitignore,
212
244
  watch: true, // re-index + push on external edits
245
+ ensureSettings: true, // create .yamlover/settings.yamlover with defaults if absent (so the gear opens)
213
246
  log: (line) => console.log(`yamlover ${line}`),
214
247
  });
215
248
  handle.ready.catch((e) => console.error("yamlover: indexing failed:", e));
@@ -262,9 +295,25 @@ function serveStatic(res, url, distClient, distIndex) {
262
295
  function serveIndex(res, distIndex) {
263
296
  res.setHeader("Content-Type", "text/html; charset=utf-8");
264
297
  res.setHeader("Cache-Control", "no-cache");
298
+ if (basePath) {
299
+ // The shell must learn its prefix (the client prepends it to every server URL) and its
300
+ // root-absolute asset refs must point under the prefix (the strip above maps them back).
301
+ res.end(injectBase(fs.readFileSync(distIndex, "utf-8")));
302
+ return;
303
+ }
265
304
  fs.createReadStream(distIndex).pipe(res);
266
305
  }
267
306
 
307
+ /** Make a served index.html base-path-aware: expose `window.__BASE__` for the client's URL helper
308
+ * and prefix root-absolute `src="/…"` / `href="/…"` asset refs with the base path (protocol-relative
309
+ * `//…` left alone). No-op when no base path is set. */
310
+ function injectBase(html) {
311
+ if (!basePath) return html;
312
+ html = html.replace(/((?:src|href)=")\/(?!\/)/g, `$1${basePath}/`);
313
+ const tag = `<script>window.__BASE__=${JSON.stringify(basePath)}</script>`;
314
+ return html.includes("<head>") ? html.replace("<head>", `<head>${tag}`) : tag + html;
315
+ }
316
+
268
317
  // Listen on `port`; if it is already in use, fall back to the next port (up to
269
318
  // `MAX_PORT_TRIES`), so two instances — or a leftover one — don't collide.
270
319
  const shown = host === "0.0.0.0" || host === "::" ? "localhost" : host;