pindeck 3.1.0-beta.1 → 3.2.0-beta.1
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 +105 -36
- package/cli.mjs +19 -9
- package/front.mjs +138 -43
- package/lib/agent-process.mjs +4 -1
- package/lib/agent-tools.mjs +5 -2
- package/lib/asset-roots.mjs +22 -0
- package/lib/claude.mjs +10 -2
- package/lib/deck-files.mjs +5 -3
- package/lib/deck-list.mjs +28 -5
- package/lib/deck-target.mjs +21 -3
- package/lib/eids.mjs +15 -6
- package/lib/login.mjs +15 -1
- package/lib/models.mjs +10 -4
- package/lib/pdf.mjs +18 -2
- package/lib/servers.mjs +91 -0
- package/package.json +1 -1
- package/public/bridge.js +56 -7
- package/public/editor.css +8 -2
- package/public/editor.js +190 -82
- package/public/events-worker.js +47 -0
- package/public/index.html +1 -1
- package/server.mjs +452 -155
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# PinDeck
|
|
2
2
|
|
|
3
|
-
> **macOS, and Linux as a beta · Version 3 beta preview** (`3.
|
|
3
|
+
> **macOS, and Linux as a beta · Version 3 beta preview** (`3.2.0-beta.1`). On Linux, PinDeck runs as a
|
|
4
4
|
> headless server that you use from your Mac's browser through an SSH tunnel (see
|
|
5
5
|
> [Running on a Linux server](#running-on-a-linux-server-beta)). Windows is not supported. The beta is on
|
|
6
6
|
> npm’s `beta` channel and the [`v3-beta`](https://github.com/ehabets/PinDeck/tree/v3-beta) branch; stable
|
|
@@ -42,7 +42,7 @@ Install the beta from npm:
|
|
|
42
42
|
```bash
|
|
43
43
|
npx pindeck@beta /path/to/deck
|
|
44
44
|
# Or select this exact beta:
|
|
45
|
-
npx pindeck@3.
|
|
45
|
+
npx pindeck@3.2.0-beta.1 /path/to/deck
|
|
46
46
|
```
|
|
47
47
|
|
|
48
48
|
Or run it from the `v3-beta` branch, which includes the example deck:
|
|
@@ -57,14 +57,15 @@ Use `npx pindeck@latest /path/to/deck` for the stable release; the beta channel
|
|
|
57
57
|
Releases, with notes for each beta, are on [GitHub](https://github.com/ehabets/PinDeck/releases).
|
|
58
58
|
|
|
59
59
|
Then open the login link PinDeck prints (`PinDeck: http://127.0.0.1:5180/login?code=...`). It works
|
|
60
|
-
once and logs this browser in; for a new one, run `pindeck login` (or `node front.mjs login`).
|
|
60
|
+
once and logs this browser in; for a new one, run `pindeck login` (or `node front.mjs login`). On a Mac,
|
|
61
|
+
pressing Enter in the terminal where PinDeck runs opens a new link in your default browser.
|
|
61
62
|
|
|
62
|
-
The deck is
|
|
63
|
-
one, the current folder. Several decks can share a folder, each with its own chat and undo history;
|
|
63
|
+
The deck is an `.html` file (`pindeck path/to/talk.html`) or a folder, which opens its `index.html`;
|
|
64
|
+
without one, the current folder. Several decks can share a folder, each with its own chat and undo history;
|
|
64
65
|
they share the folder's `theme.css` and `brand.css`, and the assistant edits only the deck that is open.
|
|
65
66
|
PinDeck adds element ids to the deck file when it opens it (see below), so try it on a copy of a deck
|
|
66
67
|
first. Options: `--port 5180`, `--provider claude|codex`, `--model <name>` (for this run only; otherwise the
|
|
67
|
-
|
|
68
|
+
deck's own choice, see [Models, providers and configuration](#models-providers-and-configuration)), `--asset-root <folder>` (see [Files outside the deck folder](#files-outside-the-deck-folder)),
|
|
68
69
|
`--help`. Runtime files (chat history, screenshots, backups) are kept in `~/.pindeck-v3/` (change with
|
|
69
70
|
`PINDECK_HOME`), not in the deck. `ocean-example/` is a complete example deck to start from.
|
|
70
71
|
|
|
@@ -75,9 +76,16 @@ the asset roots, and in their folders one level down (`lecture/index.html`). A d
|
|
|
75
76
|
tab, or with ⌘-click in a new one, on the same port and login, so one SSH tunnel serves them all. Each keeps
|
|
76
77
|
its own chat and undo history while it is open, and the assistant finishes a turn in a deck you leave. A
|
|
77
78
|
file PinDeck cannot open is greyed out with the reason; listing a deck changes nothing, and only opening it
|
|
78
|
-
adds its element ids.
|
|
79
|
-
|
|
80
|
-
|
|
79
|
+
adds its element ids. Symlinked decks and folders are listed too; a deck file that links to a deck in
|
|
80
|
+
another folder is greyed out (open that deck itself). Started with a folder that has no `index.html`,
|
|
81
|
+
PinDeck shows the list of its decks. A deck with no editor open for 30 minutes is closed (its undo history
|
|
82
|
+
goes, as on a restart) and opens again when you choose it. Open as many decks in as many tabs as you like:
|
|
83
|
+
the tabs of a browser share one connection for their live updates.
|
|
84
|
+
|
|
85
|
+
A deck is open in one PinDeck at a time: started again for a deck that another PinDeck (another port, or
|
|
86
|
+
`server.mjs` on its own) has open, PinDeck says where it is open and stops, so their chats and undo
|
|
87
|
+
histories never overwrite each other. `pindeck login <deck>` and `cli.mjs` find the PinDeck that serves
|
|
88
|
+
the deck, whichever port it runs on.
|
|
81
89
|
|
|
82
90
|
## Compatibility and requirements
|
|
83
91
|
|
|
@@ -220,7 +228,9 @@ and Haiku model in each family. Claude versions are compared numerically from di
|
|
|
220
228
|
a rolling family alias wins ties, avoiding duplicate entries. **Show all available models** reveals
|
|
221
229
|
the full discovered catalog; this display preference is remembered in the browser. A currently selected
|
|
222
230
|
older model remains visible and is never switched automatically by the filter.
|
|
223
|
-
**Refresh models**
|
|
231
|
+
**Refresh models** asks the backends again; otherwise the lists are shared by the decks you open for 10 minutes
|
|
232
|
+
(`models.json` in the state folder), so opening a deck starts no backend just to list models. A missing CLI,
|
|
233
|
+
missing sign-in, or discovery failure is shown in its provider group.
|
|
224
234
|
These are backend-reported choices, not a guarantee of current quota or entitlement; a later rejected
|
|
225
235
|
request reports its error. Older CLIs may expose fewer models than a provider's website.
|
|
226
236
|
|
|
@@ -234,7 +244,9 @@ additional model-generated summary. It excludes tool output, needs no extra infe
|
|
|
234
244
|
sent with the first message to the new session. Earlier messages may be omitted. **Start fresh** sends
|
|
235
245
|
no conversation digest; the assistant still receives PinDeck instructions and the current deck context.
|
|
236
246
|
|
|
237
|
-
|
|
247
|
+
Each deck keeps its own provider, model and effort with its conversation (in its `chat.json`), so a choice in
|
|
248
|
+
one deck never moves another deck's conversation to another backend. The last choice is also saved in
|
|
249
|
+
`~/.pindeck-v3/config.json` (or `$PINDECK_HOME/config.json`), as the start for decks that have none yet:
|
|
238
250
|
|
|
239
251
|
```json
|
|
240
252
|
{
|
|
@@ -244,13 +256,22 @@ Settings are saved when changed in `~/.pindeck-v3/config.json` (or `$PINDECK_HOM
|
|
|
244
256
|
}
|
|
245
257
|
```
|
|
246
258
|
|
|
247
|
-
Legacy configuration without `provider` is interpreted as Claude
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
259
|
+
Legacy configuration without `provider` is interpreted as Claude; a provider without a model gets that
|
|
260
|
+
provider's default model. Once the backend has listed its models, a saved model it no longer offers gives way
|
|
261
|
+
to its default model (the chat says so), and an effort the model does not offer to its saved or default one,
|
|
262
|
+
before the first message. config.json is read again and merged whenever a choice is saved, so decks open at
|
|
263
|
+
the same time keep each other's effort preferences. Provider-tagged chat/session records prevent one backend
|
|
264
|
+
from resuming the other's IDs. `--provider` and `--model` override the deck's settings for that run; a
|
|
265
|
+
`--provider` other than the deck's starts a new conversation (the chat says so), and the next run without it
|
|
266
|
+
is back on the deck's own choice. A later menu selection is saved. `CLAUDE_BIN` and `CODEX_BIN` override
|
|
267
|
+
executable paths. `CLAUDE_MODEL`, which PinDeck 2 read, is not used: PinDeck warns and names `--model`.
|
|
251
268
|
|
|
252
269
|
The v3 preview defaults to **`~/.pindeck-v3`**, keeping v2 settings and conversations separate. It does
|
|
253
|
-
not copy the v2 directory automatically.
|
|
270
|
+
not copy the v2 directory automatically; `config.json` and `decks/*/chat.json` can be copied over by hand
|
|
271
|
+
(asset roots too: they are in config.json). Never give both versions the same `PINDECK_HOME`: they keep files
|
|
272
|
+
of the same names in different formats and overwrite each other's settings and chats. PinDeck 3 warns at
|
|
273
|
+
start when its folder holds a chat that PinDeck 2 wrote. To run both versions at once, use another port, for
|
|
274
|
+
example:
|
|
254
275
|
|
|
255
276
|
```bash
|
|
256
277
|
node front.mjs /path/to/a/deck-copy --port 5183 --provider codex
|
|
@@ -261,16 +282,17 @@ node front.mjs /path/to/a/deck-copy --port 5183 --provider codex
|
|
|
261
282
|
Claude uses its native Read/Edit tools with PinDeck's narrow allowlist, safe mode, and no inherited
|
|
262
283
|
MCP configuration. Codex runs with a read-only sandbox, command execution disabled, and no approval
|
|
263
284
|
grants. Inherited MCP servers and plugins are disabled in the thread configuration. Codex receives
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
285
|
+
four host-controlled tools: `pindeck_read`, `pindeck_edit`, `pindeck_write` and `pindeck_command`.
|
|
286
|
+
Both assistants may edit and create the files of the deck folder: the deck file, `theme.css` and its other
|
|
287
|
+
files. Never `brand.css`, `vendor/`, `.git/`, or the other decks in the folder (its other `.html` files, and
|
|
288
|
+
subfolders that hold a deck of their own). Linked writable files and paths outside the allowed roots are
|
|
289
|
+
rejected. Commands are restricted to PinDeck's layout, shot, slides, pdf and pdf-figure helpers, executed
|
|
290
|
+
with argument arrays rather than a shell. `brand.css` also retains the server's post-turn restoration safeguard.
|
|
269
291
|
|
|
270
292
|
Codex reads text, PNG/JPEG/WebP/GIF images and screenshots through the bridge. SVG is read as text.
|
|
271
|
-
Attached PDFs are handled page by page
|
|
272
|
-
|
|
273
|
-
|
|
293
|
+
Attached PDFs are handled page by page: a rendered page image (PDFKit on macOS, Poppler on Linux), plus the
|
|
294
|
+
page's text on macOS. Audio is embedded by path and is not transcribed. Read operations have a 12 MB limit;
|
|
295
|
+
long text can be read in chunks.
|
|
274
296
|
|
|
275
297
|
### Files outside the deck folder
|
|
276
298
|
|
|
@@ -283,6 +305,8 @@ exports. Claude may read those files but does not edit them.
|
|
|
283
305
|
Symlinks: PinDeck serves a file only if, with symlinks followed, it lies inside the deck folder or an
|
|
284
306
|
asset root. A symlink in the deck that leads anywhere else is refused (403). To use a symlinked
|
|
285
307
|
library folder, add the folder it points to as an asset root; it does not have to contain the deck.
|
|
308
|
+
PinDeck never writes or deletes through a symlink out of the deck folder: with a `figures/` or `export/`
|
|
309
|
+
that links elsewhere, uploads, exports and deleting a picture's file are refused with the reason.
|
|
286
310
|
|
|
287
311
|
## Running on a Linux server (beta)
|
|
288
312
|
|
|
@@ -312,7 +336,7 @@ message twice. If PinDeck restarts, the editor asks you to log in again: run `no
|
|
|
312
336
|
path/to/deck` on the server for a new link.
|
|
313
337
|
|
|
314
338
|
Other users of a shared server cannot open the editor or read the deck through PinDeck: the token that
|
|
315
|
-
protects it is only in your browser's cookie and in PinDeck's own files in `~/.pindeck` (readable only by
|
|
339
|
+
protects it is only in your browser's cookie and in PinDeck's own files in `~/.pindeck-v3` (readable only by
|
|
316
340
|
you), never in a page or on a command line.
|
|
317
341
|
|
|
318
342
|
What works on Linux in this beta: editing by hand, speaker notes, slides, pins, the chat with Claude or Codex,
|
|
@@ -331,9 +355,12 @@ Without them the editor says what is missing (hover over "Server: Linux (beta)")
|
|
|
331
355
|
which commands are not available. LibreOffice may draw some slides a little differently from PowerPoint;
|
|
332
356
|
the pictures are references and figures, the slides themselves are rebuilt from the file. Chromium from snap may not be
|
|
333
357
|
allowed to use hidden folders in your home folder such as `~/.pindeck-v3`; if screenshots fail with it, set
|
|
334
|
-
`PINDECK_HOME` to a folder that is not hidden, or install Chrome or Chromium from
|
|
358
|
+
`PINDECK_HOME` to a folder that is not hidden (not the one PinDeck 2 uses), or install Chrome or Chromium from
|
|
359
|
+
a package. Run as root (in a container, say), PinDeck starts Chrome with `--no-sandbox`, which Chrome needs
|
|
360
|
+
there; a screenshot or PDF that fails gives Chrome's own reason.
|
|
335
361
|
|
|
336
|
-
|
|
362
|
+
One PinDeck serves all your decks on one port, and a deck can be open in only one PinDeck at a time (a
|
|
363
|
+
second one says where it is open). While PinDeck or the assistant is working on a deck, do not edit it with other tools
|
|
337
364
|
or switch git branches at the same time. Stop first, or wait until the assistant has finished. PinDeck notices
|
|
338
365
|
changes made by other programs and reloads, and it refuses Undo, Redo or an edit that would overwrite
|
|
339
366
|
them. Its checks cannot rule out every race with a program that writes at the same moment.
|
|
@@ -361,10 +388,10 @@ show and commit as usual. Clones without the filter configured simply see the fi
|
|
|
361
388
|
| Undo, redo | `⌘Z` / `⇧⌘Z` or the toolbar arrows. One history for your hand edits and the assistant's changes. |
|
|
362
389
|
| Pin elements | Press `P` (or the Pin button), click elements; each click gets a number. Click a marker to remove it, `Esc` to stop. |
|
|
363
390
|
| Ask the assistant | Write in the sidebar and refer to pins by number: "1: align with 2, make 3 smaller". The selected element is sent too, so "make this smaller" works. `Enter` sends, `Shift+Enter` adds a new line. |
|
|
364
|
-
| Visual questions | **+** in the chat, then **Screenshot**: the current slide with
|
|
365
|
-
| Review | Each answer ends with the changed files
|
|
366
|
-
| Stop | **Stop** interrupts the assistant; the conversation continues. **New chat** starts
|
|
367
|
-
| New slide | **+ New slide** above the slide list: a **content slide** (title and bullets), a **section divider**, or a **duplicate** of the current slide. It is added after the current slide, modelled on the deck's own slides
|
|
391
|
+
| Visual questions | **+** in the chat, then **Screenshot**: the current slide with the pins of that message is attached when you send. If Chrome cannot take it, the assistant is told. **Stop** during the screenshot stops at once. |
|
|
392
|
+
| Review | Each answer ends with the changed files and a **Diff** button; the most recent change still in effect also has an **Undo** button. After a restart, earlier changes keep their *undone* mark but have no Diff or Undo (the undo history ends with the run). |
|
|
393
|
+
| Stop | **Stop** interrupts the assistant; the conversation continues. **New chat** starts a new conversation; your slides, Undo and Redo stay as they are. |
|
|
394
|
+
| New slide | **+ New slide** above the slide list: a **content slide** (title and bullets), a **section divider**, or a **duplicate** of the current slide. It is added after the current slide, modelled on the deck's own slides; a new content slide or divider opens its title for typing. |
|
|
368
395
|
| Import from PowerPoint | **+ New slide** → **Import from PowerPoint…**, then choose a `.pptx` file and tick the slides you want (hidden PowerPoint slides start unticked). They are added after the current slide in this deck's own layouts, and the assistant can improve the complex ones; see [PowerPoint import](#powerpoint-import). `⌘Z` removes them again, with their figures. |
|
|
369
396
|
| Speaker notes | The **Notes** field under the slide shows the current slide's notes (the speaker view, `S`, shows them when presenting). Type to change them; they are saved when you click elsewhere or press `⌘Enter`, and `Esc` puts back what was there. A slide without notes gets them; emptying the field removes them. Notes with formatting other than line breaks are edited as HTML. Drag the line above the notes to give them more or less room (double-click it for the default size); collapse them with their header. |
|
|
370
397
|
| Move a slide | Drag it up or down in the slide list; an orange line shows where it will go. It moves with its notes and the comment above it, and `⌘Z` puts it back. Slides inside vertical stacks cannot be moved yet. |
|
|
@@ -373,10 +400,12 @@ show and commit as usual. Clones without the filter configured simply see the fi
|
|
|
373
400
|
| Attach figures and audio | **+** in the chat, then **File…**, drag and drop onto the chat or a slide, or paste an image. Files are saved in `<deck>/figures/` straight away; audio also gets a waveform SVG. Then ask the assistant, e.g. "put this figure on a new slide after 4". Removing an attachment before sending deletes the file again. |
|
|
374
401
|
| Attach a PDF | The same way: a figure or table to take over, or a paper or report with more details. PDFs stay outside the deck (in `~/.pindeck-v3`); the assistant reads them, rebuilds tables as HTML, and renders a figure (a page or a cropped part of one) into `<deck>/figures/` as PNG with `cli.mjs pdf-figure` (macOS, or Linux with Poppler). E.g. "put Fig. 3 of this paper on slide 7" or "add the results of Table 2 to slide 9". |
|
|
375
402
|
| Export PDF | **Export ▾** in the toolbar. **Handout PDF**: one page per slide, build steps shown complete. **Presentation PDF**: one page per click, so a slide with two build steps gives three pages. Files go to `<deck>/export/`. Needs Chrome or Chromium. |
|
|
376
|
-
| Navigate | Slide list, `←` `→`, or the toolbar. **Present** opens the deck in a new tab. |
|
|
403
|
+
| Navigate | Slide list, `←` `→`, or the toolbar. **Present** opens the deck in a new tab, at the current slide. |
|
|
404
|
+
| Check from a terminal | `node cli.mjs layout 3 --issues`, `slides`, `shot 3` or `pdf handout`, in the deck's folder (or with `--deck <folder or file.html>`): asks the PinDeck that serves the deck. |
|
|
377
405
|
|
|
378
406
|
You can also edit `index.html` or `theme.css` in any editor; the slide reloads, and the assistant is told
|
|
379
|
-
the files changed. CSS changes are swapped in without a reload.
|
|
407
|
+
the files changed. CSS changes are swapped in without a reload. Typing in the slide is not lost meanwhile:
|
|
408
|
+
when you finish, PinDeck refuses to save it over the other change and offers the text to copy.
|
|
380
409
|
|
|
381
410
|
## PowerPoint import
|
|
382
411
|
|
|
@@ -425,7 +454,17 @@ Without PowerPoint or Keynote (or on other systems than macOS) only the text and
|
|
|
425
454
|
* **One port, a server per deck.** `pindeck` starts the front (`front.mjs`): it holds the port, the login
|
|
426
455
|
and the deck list, and serves each open deck at `/d/<deck>/`. Every deck runs its own deck server
|
|
427
456
|
(`server.mjs`) on a private port of 127.0.0.1 with a token of its own; the front passes each request on
|
|
428
|
-
with that token. A deck's server, its assistant and its undo history are those of that deck alone.
|
|
457
|
+
with that token. A deck's server, its assistant and its undo history are those of that deck alone. The
|
|
458
|
+
editor tabs of a browser share one event stream from the front (a shared worker), since browsers open
|
|
459
|
+
only six connections to one port; if a deck server stops, the front opens the deck again and the tab
|
|
460
|
+
catches up. Each PinDeck process notes its address in `~/.pindeck-v3/servers/` for `pindeck login` and
|
|
461
|
+
`cli.mjs`, and a deck server holds its deck's `open.json`, so a deck opens in one PinDeck only.
|
|
462
|
+
* **Deck pages are sandboxed.** The deck page (in the editor's frames, and in Present) runs with a sandbox
|
|
463
|
+
policy in an origin of its own, at a per-run address that serves the deck's files and nothing else. A
|
|
464
|
+
script in a deck, a template, or written by the assistant cannot reach the login cookie, the API, the
|
|
465
|
+
chat or other decks. The editor and its bridge in the slide frame talk by `postMessage`. Headless Chrome
|
|
466
|
+
(screenshots, layout, PDFs) does not log in at all; the page it asks for is served without the sandbox,
|
|
467
|
+
which its screenshot and print modes cannot handle, only with a key made for that one run.
|
|
429
468
|
* **Element ids.** On start, the server gives every slide and every visible element a readable
|
|
430
469
|
`data-eid` attribute (`summary`, `summary/callout`, `summary/li-2`). Only the attribute is
|
|
431
470
|
inserted; the rest of the file stays as it was. A backup of the file before the first change is
|
|
@@ -461,13 +500,15 @@ Without PowerPoint or Keynote (or on other systems than macOS) only the text and
|
|
|
461
500
|
so no allow rule in your own, the deck's or a parent folder's settings widens this. Codex has no shell
|
|
462
501
|
and no file access of its own: it reads and edits through PinDeck's own tools, which apply the same
|
|
463
502
|
rules and refuse symlinks out of the deck. If `brand.css` changes during a turn anyway, the server
|
|
464
|
-
restores it and says so in the chat.
|
|
503
|
+
restores it and says so in the chat. Subfolders that hold a deck of their own count as other decks.
|
|
465
504
|
* **Undo.** The server keeps a copy of every file a change touched, from before and after it (an AI
|
|
466
505
|
turn, which may change, create or delete several files of the deck, or a hand edit). Files over 2 MB
|
|
467
506
|
are not copied: the chat says when a turn changed one, and Undo leaves it as it is. Hidden files,
|
|
468
507
|
`vendor/` and `export/` are not part of undo. Undo and redo walk that history and tell the assistant on
|
|
469
508
|
the next message. They first check that the files are still as that change left them; if something
|
|
470
509
|
else changed them since, they do nothing and say which file differs, rather than overwrite that work.
|
|
510
|
+
Decks that share a folder share its files: while another deck's assistant works there too, a turn keeps
|
|
511
|
+
only the files its own assistant said it wrote, and says which it left out.
|
|
471
512
|
* **Saving.** Every file PinDeck writes into the deck is written to a temporary file next to it and then
|
|
472
513
|
renamed into place, so other programs never see half a file. Each change from the editor carries the
|
|
473
514
|
revision of the files it was made on; if the files have changed since, the change is refused, the
|
|
@@ -480,8 +521,9 @@ Without PowerPoint or Keynote (or on other systems than macOS) only the text and
|
|
|
480
521
|
| `front.mjs` | What `pindeck` starts (127.0.0.1 only, random token per run, one-time login links): the login, the deck list and one deck server per open deck at `/d/<deck>/`; `login`. |
|
|
481
522
|
| `server.mjs` | The server for one deck: UI, deck files, events, backend session, undo, CLI endpoints. Behind the front, or on its own (`node server.mjs <deck>`). |
|
|
482
523
|
| `lib/login.mjs`, `lib/deck-target.mjs`, `lib/deck-list.mjs` | Login links and cookies, which deck a path names (and its state folder), and the decks the menu lists. |
|
|
524
|
+
| `lib/servers.mjs` | The PinDecks running for this user, and which one serves a deck (`pindeck login`, `cli.mjs`). |
|
|
483
525
|
| `cli.mjs` | `layout`, `shot`, `slides`, `pdf handout` / `pdf presentation` and `pdf-figure` commands for the backends (and for you), and `git-setup`. |
|
|
484
|
-
| `public/` | The editor page
|
|
526
|
+
| `public/` | The editor page; `bridge.js`, which is injected into the deck in edit mode (never saved to it); `events-worker.js`, the event stream the tabs of a browser share. |
|
|
485
527
|
| `lib/pptx.mjs`, `lib/zip.mjs`, `lib/xml.mjs` | Reading `.pptx` files (no dependencies): slides, text, shapes and their positions, notes. |
|
|
486
528
|
| `lib/pptx-import.mjs` | Turns PowerPoint slides into slides of the deck: figures, tables, build steps, audio, layout, HTML in the deck's style. |
|
|
487
529
|
| `lib/waveform.mjs` | Waveform SVGs for imported audio. |
|
|
@@ -511,11 +553,38 @@ Without PowerPoint or Keynote (or on other systems than macOS) only the text and
|
|
|
511
553
|
* The undo history lives on the server and starts fresh when PinDeck restarts, or when a deck closes after
|
|
512
554
|
30 minutes without an open editor.
|
|
513
555
|
* One writer at a time: PinDeck is not made for editing the same deck from two editors or tools at once.
|
|
556
|
+
* A deck's own scripts run sandboxed in the editor and in Present: they cannot use `localStorage`, cookies
|
|
557
|
+
or the editor page. A deck that needs them for its slides to work should catch the error.
|
|
558
|
+
* Decks are `.html` files; `.htm` files are not listed or opened as decks.
|
|
514
559
|
* Linux (beta): pictures of PDF pages and PowerPoint slides, and waveforms of non-WAV audio, need
|
|
515
560
|
Poppler, LibreOffice and ffmpeg on the server (see [Running on a Linux server](#running-on-a-linux-server-beta)).
|
|
516
561
|
|
|
517
562
|
## Versions
|
|
518
563
|
|
|
564
|
+
* **3.2.0-beta.1**: fixes from a code review and a round of user tests.
|
|
565
|
+
- Security: deck pages run sandboxed, so a deck's script can no longer use the API, read other decks or
|
|
566
|
+
send messages; the login link's `next` stays on PinDeck. A deck's own script cannot use `localStorage`
|
|
567
|
+
there either (it throws).
|
|
568
|
+
- Typing in a bullet that cannot be saved because the deck changed meanwhile can be copied, as other text
|
|
569
|
+
can; a deck that never chose a model keeps the one its conversation started with.
|
|
570
|
+
- Several decks in one folder: a turn no longer records or undoes another deck's files, and deleting a
|
|
571
|
+
picture keeps a file another deck uses. Subfolder decks are locked for the assistant.
|
|
572
|
+
- Each deck keeps its provider, model and effort; config.json is merged, not overwritten; unusable saved
|
|
573
|
+
settings are made usable before the first message.
|
|
574
|
+
- Many tabs on one port; a deck server that stops reconnects; `pindeck login` and `cli.mjs` find the right
|
|
575
|
+
PinDeck; a deck opens in one PinDeck only.
|
|
576
|
+
- Screenshots show the pins of their own message; Stop during a screenshot; failures reach the assistant.
|
|
577
|
+
- New chat keeps Undo; undone marks survive a restart; only the latest change offers Undo.
|
|
578
|
+
- A deck page that cannot start says why, and Send no longer hangs; typing survives an outside save; the
|
|
579
|
+
deck menu at every width; keyboard use of the model menus; Enter in a bullet leaves no copied ids.
|
|
580
|
+
- Large decks get their ids in linear time; symlinked decks; a deck that is gone; Chrome's own errors,
|
|
581
|
+
and `--no-sandbox` as root; a warning when PinDeck 2 writes to the same state folder.
|
|
582
|
+
- On a Mac, Enter in the terminal opens PinDeck in the browser.
|
|
583
|
+
- From the code review: locked paths regardless of case (macOS) and `.git/config` restored after each
|
|
584
|
+
turn; Codex reads PDFs of a deck reached through a symlink; "not signed in" only when Claude says so;
|
|
585
|
+
betas tested before they are staged; model lists shared between decks; a faster deck list; smaller
|
|
586
|
+
status updates; asset roots read alike by the front and the deck servers.
|
|
587
|
+
|
|
519
588
|
* **3.1.0-beta.1**: several decks on one port, and screenshots you can open from the chat.
|
|
520
589
|
- A deck menu: click the deck's name above the slide to open another deck from its folder or the
|
|
521
590
|
asset roots (and one folder down). Each open deck keeps its own chat, assistant session and Undo;
|
package/cli.mjs
CHANGED
|
@@ -15,14 +15,20 @@
|
|
|
15
15
|
|
|
16
16
|
node cli.mjs git-setup [deck] keep PinDeck's data-eid ids out of git (no server needed; a folder or deck file)
|
|
17
17
|
|
|
18
|
-
<slide> is a slide id (data-eid), a slide number (1-based) or "current".
|
|
18
|
+
<slide> is a slide id (data-eid), a slide number (1-based) or "current". The assistant's commands reach its
|
|
19
|
+
own deck server (DECK_EDITOR_URL); run from a terminal, they reach the PinDeck that serves the deck in the
|
|
20
|
+
current folder (its index.html), or the deck named with --deck <folder | file.html>. */
|
|
19
21
|
|
|
20
22
|
import fs from "node:fs";
|
|
21
|
-
import os from "node:os";
|
|
22
23
|
import path from "node:path";
|
|
23
24
|
import { execFileSync } from "node:child_process";
|
|
25
|
+
import { deckTarget, homeDir } from "./lib/deck-target.mjs";
|
|
26
|
+
import { serverFor } from "./lib/servers.mjs";
|
|
24
27
|
|
|
25
|
-
const
|
|
28
|
+
const args = process.argv.slice(2);
|
|
29
|
+
const deckAt = args.indexOf("--deck");
|
|
30
|
+
const deckArg = deckAt >= 0 ? args.splice(deckAt, 2)[1] : null;
|
|
31
|
+
const [cmd, ...rest] = args;
|
|
26
32
|
const arg = rest[0];
|
|
27
33
|
|
|
28
34
|
/* git-setup: a git "clean" filter strips PinDeck's data-eid attributes whenever git reads the deck's
|
|
@@ -68,13 +74,17 @@ function figureQuery(args) {
|
|
|
68
74
|
|
|
69
75
|
let url = process.env.DECK_EDITOR_URL, token = process.env.DECK_EDITOR_TOKEN;
|
|
70
76
|
if (!url || !token) {
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
// from a terminal: the deck's own server if it is open, else a front that lists it (which opens it)
|
|
78
|
+
const deck = deckTarget(deckArg || process.cwd());
|
|
79
|
+
if (!fs.existsSync(path.join(deck.dir, deck.file))) {
|
|
80
|
+
console.error(`No ${deck.file} in ${deck.dir}: run this in a deck's folder, or name the deck with --deck <folder | file.html>.`);
|
|
81
|
+
process.exit(2);
|
|
82
|
+
}
|
|
83
|
+
const found = await serverFor(homeDir(), deck);
|
|
84
|
+
if (!found) { console.error(`No running PinDeck serves ${path.join(deck.dir, deck.file)} (start it with: pindeck ${deckArg || "."}).`); process.exit(1); }
|
|
85
|
+
({ url, token } = found.own || found.front);
|
|
86
|
+
if (!found.own) url = `${url}/d/${found.key}`;
|
|
76
87
|
}
|
|
77
|
-
if (!url) { console.error("The PinDeck server is not running (start it with: pindeck <deck-folder>)."); process.exit(1); }
|
|
78
88
|
|
|
79
89
|
/* layout and shot take several slides, so the assistant needs no shell loop or pipe (which PinDeck refuses) */
|
|
80
90
|
const queries = cmd === "pdf-figure" ? [figureQuery([...rest])]
|
package/front.mjs
CHANGED
|
@@ -16,15 +16,16 @@
|
|
|
16
16
|
|
|
17
17
|
import http from "node:http";
|
|
18
18
|
import fs from "node:fs";
|
|
19
|
-
import os from "node:os";
|
|
20
19
|
import path from "node:path";
|
|
21
20
|
import crypto from "node:crypto";
|
|
22
21
|
import { spawn } from "node:child_process";
|
|
23
22
|
import { fileURLToPath } from "node:url";
|
|
24
23
|
import { isSupportedPlatform, detectCapabilities, publicCapabilities, parsePort } from "./lib/capabilities.mjs";
|
|
25
|
-
import { deckTarget, homeDir } from "./lib/deck-target.mjs";
|
|
26
|
-
import { goodHost, LOGIN_PAGE, createLogin } from "./lib/login.mjs";
|
|
24
|
+
import { deckTarget, homeDir, homeWarning } from "./lib/deck-target.mjs";
|
|
25
|
+
import { goodHost, LOGIN_PAGE, createLogin, openOnEnter } from "./lib/login.mjs";
|
|
27
26
|
import { listDecks } from "./lib/deck-list.mjs";
|
|
27
|
+
import { assetRoots } from "./lib/asset-roots.mjs";
|
|
28
|
+
import { registerServer, printLogin } from "./lib/servers.mjs";
|
|
28
29
|
import { USAGE } from "./lib/usage.mjs";
|
|
29
30
|
|
|
30
31
|
const EDITOR_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -48,38 +49,24 @@ if (!isSupportedPlatform(process.platform)) {
|
|
|
48
49
|
process.exit(0);
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
/* pindeck login: a new one-time login link from the
|
|
52
|
-
(readable only by this user). With a deck, the link opens that deck.
|
|
53
|
-
|
|
54
|
-
const SERVER_FILE = path.join(HOME_DIR, "server.json");
|
|
55
|
-
if (argv[0] === "login") {
|
|
56
|
-
const deck = argv[1] ? deckTarget(argv[1]) : null;
|
|
57
|
-
let info = null;
|
|
58
|
-
for (const f of [...(deck ? [path.join(HOME_DIR, "decks", deck.key, "server.json")] : []), SERVER_FILE]) {
|
|
59
|
-
try { info = JSON.parse(fs.readFileSync(f, "utf8")); break; } catch {}
|
|
60
|
-
}
|
|
61
|
-
if (!info) { console.error("No PinDeck is running."); process.exit(1); }
|
|
62
|
-
const next = deck && info.front ? `/d/${deck.key}/` : "/";
|
|
63
|
-
try {
|
|
64
|
-
const res = await fetch(`${info.url}/api/login-link?` + new URLSearchParams({ next }), { headers: { "x-editor-token": info.token } });
|
|
65
|
-
if (!res.ok) throw new Error(res.status);
|
|
66
|
-
console.log(`PinDeck (${deck ? path.join(deck.dir, deck.file) : info.deckDir}): ${(await res.json()).url}`);
|
|
67
|
-
process.exit(0);
|
|
68
|
-
} catch {
|
|
69
|
-
console.error(`The PinDeck at ${info.url} does not answer. Is it still running?`);
|
|
70
|
-
process.exit(1);
|
|
71
|
-
}
|
|
72
|
-
}
|
|
52
|
+
/* pindeck login: a new one-time login link from the PinDeck that serves the deck (see lib/servers.mjs), asked
|
|
53
|
+
with the token in its server file (readable only by this user). With a deck, the link opens that deck. */
|
|
54
|
+
if (argv[0] === "login") await printLogin(HOME_DIR, argv[1] ? deckTarget(argv[1]) : null);
|
|
73
55
|
|
|
74
56
|
/* The start deck, and the folders whose decks the menu offers. A folder without index.html opens the list
|
|
75
57
|
of its decks; if it has none, its deck server says why, as for any deck that cannot be opened. */
|
|
58
|
+
const homeNote = homeWarning(HOME_DIR);
|
|
59
|
+
if (homeNote) console.warn(homeNote);
|
|
76
60
|
const START = deckTarget(argv[0] || process.cwd());
|
|
77
|
-
|
|
61
|
+
if (process.env.CLAUDE_MODEL) // PinDeck 2 read it
|
|
62
|
+
console.warn(`CLAUDE_MODEL is not used by PinDeck 3: choose the model in the editor's model menu, or start with --model ${process.env.CLAUDE_MODEL}.`);
|
|
78
63
|
let fileConfig = {};
|
|
79
64
|
try { fileConfig = JSON.parse(fs.readFileSync(path.join(HOME_DIR, "config.json"), "utf8")); } catch {}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
65
|
+
if (!fileConfig || typeof fileConfig !== "object" || Array.isArray(fileConfig)) fileConfig = {}; // the deck server says why
|
|
66
|
+
// a missing asset root stops the start, before anything is written; one that goes later lists nothing
|
|
67
|
+
const givenRoots = assetRoots(assetArgs, fileConfig);
|
|
68
|
+
if (givenRoots.missing.length) { console.error(`Asset root ${givenRoots.missing[0]} is not a folder.\n\n${USAGE}`); process.exit(2); }
|
|
69
|
+
const ROOTS = [...new Set([START.dir, ...givenRoots.roots])];
|
|
83
70
|
const decksNow = () => listDecks(ROOTS);
|
|
84
71
|
const chooser = !START.named && !fs.existsSync(path.join(START.dir, START.file)) && decksNow().some((d) => !d.problem);
|
|
85
72
|
|
|
@@ -93,10 +80,10 @@ const decks = new Map(); // key -> { key, dir, file, label, proc, url, token,
|
|
|
93
80
|
const labelOf = (d) => (d.file === "index.html" ? path.basename(d.dir) : d.file);
|
|
94
81
|
let quitting = false;
|
|
95
82
|
|
|
96
|
-
/* Its output goes to this terminal, each line marked with the deck. hold(): keep the
|
|
83
|
+
/* Its output goes to this terminal, each line marked with the deck. hold(l): keep the line back for now */
|
|
97
84
|
function forward(stream, label, to, keep, hold = () => false) {
|
|
98
85
|
let rest = "";
|
|
99
|
-
const put = (l) => { keep?.(l + "\n"); if (!hold()) to.write(`[${label}] ${l}\n`); };
|
|
86
|
+
const put = (l) => { keep?.(l + "\n"); if (!hold(l)) to.write(`[${label}] ${l}\n`); };
|
|
100
87
|
stream.on("data", (chunk) => {
|
|
101
88
|
const lines = (rest + chunk).split("\n");
|
|
102
89
|
rest = lines.pop();
|
|
@@ -114,15 +101,20 @@ function openDeck(entry, start = false) {
|
|
|
114
101
|
decks.set(d.key, d);
|
|
115
102
|
// its own process group: Ctrl+C reaches the front only, which then ends every deck once
|
|
116
103
|
d.proc = spawn(process.execPath, [SERVER, entry.file === "index.html" ? entry.dir : path.join(entry.dir, entry.file),
|
|
117
|
-
"--port", "0", ...PASS], { env: { ...process.env, PINDECK_FRONT: "1"
|
|
104
|
+
"--port", "0", ...PASS], { env: { ...process.env, PINDECK_FRONT: "1", PINDECK_FRONT_URL: ORIGIN },
|
|
105
|
+
stdio: ["ignore", "pipe", "pipe", "ipc"], detached: true });
|
|
106
|
+
// what it says before it listens: its reason if it stops; the start deck's warnings once it runs
|
|
118
107
|
let said = "";
|
|
108
|
+
const held = [];
|
|
119
109
|
forward(d.proc.stdout, d.label, process.stdout);
|
|
120
|
-
forward(d.proc.stderr, d.label, process.stderr, (l) => {
|
|
110
|
+
forward(d.proc.stderr, d.label, process.stderr, (l) => { if (!d.url) said = (said + l).slice(-20000); },
|
|
111
|
+
(l) => start && !d.url && held.push(l));
|
|
121
112
|
d.exited = new Promise((r) => d.proc.once("exit", r));
|
|
122
113
|
d.ready = new Promise((resolve, reject) => {
|
|
123
114
|
d.proc.on("message", (m) => {
|
|
124
115
|
if (!m?.ready) return;
|
|
125
116
|
Object.assign(d, { url: m.ready.url, token: m.ready.token });
|
|
117
|
+
for (const l of held.splice(0)) console.error(l); // a start that went ahead anyway, as PinDeck said it before the front
|
|
126
118
|
if (!start) console.log(`[${d.label}] opened: ${path.join(d.dir, d.file)}`);
|
|
127
119
|
resolve(d);
|
|
128
120
|
});
|
|
@@ -156,14 +148,18 @@ setInterval(async () => {
|
|
|
156
148
|
|
|
157
149
|
/* The request, without the front's cookie and with the deck's token, to the deck's server; the answer
|
|
158
150
|
streamed back (event streams, media ranges and large uploads included) */
|
|
159
|
-
function proxy(req, res, d, rest) {
|
|
151
|
+
function proxy(req, res, d, rest, anonymous = false) {
|
|
160
152
|
d.last = Date.now();
|
|
161
|
-
const headers = { ...req.headers, host: new URL(d.url).host, "x-editor-token": d.token };
|
|
153
|
+
const headers = { ...req.headers, host: new URL(d.url).host, ...(anonymous ? {} : { "x-editor-token": d.token }) };
|
|
154
|
+
if (anonymous) delete headers["x-editor-token"];
|
|
162
155
|
delete headers.cookie;
|
|
163
156
|
delete headers.origin; // checked here against the front's address; the deck server checks its own
|
|
164
157
|
const up = http.request(d.url + rest, { method: req.method, headers }, (r) => {
|
|
165
158
|
res.writeHead(r.statusCode, r.headers);
|
|
166
159
|
r.pipe(res);
|
|
160
|
+
// the deck server stopped mid-answer (an event stream, say): end ours too, or the browser waits forever
|
|
161
|
+
r.on("aborted", () => res.destroy());
|
|
162
|
+
r.on("error", () => res.destroy());
|
|
167
163
|
});
|
|
168
164
|
up.on("error", () => { if (!res.headersSent) page(res, 502, "This deck stopped answering", "Reload the page to open it again."); else res.destroy(); });
|
|
169
165
|
if (rest.split("?")[0] === "/api/events") {
|
|
@@ -174,6 +170,83 @@ function proxy(req, res, d, rest) {
|
|
|
174
170
|
req.pipe(up);
|
|
175
171
|
}
|
|
176
172
|
|
|
173
|
+
/* ---- One event stream per browser -------------------------------------------------------------------
|
|
174
|
+
Browsers open at most 6 connections to one host and port, and each editor tab held one open for its deck's
|
|
175
|
+
events: a sixth tab never loaded. The tabs of a browser share one stream through a shared worker
|
|
176
|
+
(public/events-worker.js): GET /events?stream=<id> holds it, POST /events/subscribe adds a tab's deck (the
|
|
177
|
+
front opens that deck's events on its server and passes them on, marked with the tab) or removes it. If a
|
|
178
|
+
deck server stops, the deck is opened again and the tab gets its state afresh (its "hello"). */
|
|
179
|
+
const streams = new Map(); // stream id -> { res, tabs: Map(tab -> { stop() }) }
|
|
180
|
+
function eventStream(res, id) {
|
|
181
|
+
streams.get(id)?.res.end(); // the worker reconnected: its old stream is done
|
|
182
|
+
const st = { res, tabs: new Map() };
|
|
183
|
+
streams.set(id, st);
|
|
184
|
+
res.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
|
|
185
|
+
res.write("event: ready\ndata: {}\n\n");
|
|
186
|
+
const ping = setInterval(() => res.write(": ping\n\n"), 20000);
|
|
187
|
+
res.on("close", () => {
|
|
188
|
+
clearInterval(ping);
|
|
189
|
+
if (streams.get(id) === st) streams.delete(id);
|
|
190
|
+
for (const t of st.tabs.values()) t.stop();
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
function follow(st, tab, key) {
|
|
194
|
+
let stopped = false, up = null, tries = 0;
|
|
195
|
+
const send = (type, raw) => {
|
|
196
|
+
if (!stopped && !st.res.writableEnded) st.res.write(`event: deck\ndata: {"tab":${JSON.stringify(tab)},"type":${JSON.stringify(type)},"data":${raw}}\n\n`);
|
|
197
|
+
};
|
|
198
|
+
const retry = () => {
|
|
199
|
+
if (stopped || quitting) return;
|
|
200
|
+
if (++tries > 20) return send("gone", JSON.stringify({ error: "The deck stopped answering. Reload the page." }));
|
|
201
|
+
setTimeout(connect, Math.min(5000, 200 * tries));
|
|
202
|
+
};
|
|
203
|
+
async function connect() {
|
|
204
|
+
if (stopped || quitting) return;
|
|
205
|
+
let d = decks.get(key);
|
|
206
|
+
try {
|
|
207
|
+
if (!d) {
|
|
208
|
+
const entry = START.key === key && !chooser ? { key, dir: START.dir, file: START.file } : decksNow().find((e) => e.key === key);
|
|
209
|
+
if (!entry || entry.problem) return send("gone", JSON.stringify({ error: entry?.problem || "This deck is not in the deck folder or an asset root (any more)." }));
|
|
210
|
+
d = entry;
|
|
211
|
+
}
|
|
212
|
+
d = await openDeck(d);
|
|
213
|
+
} catch (e) { return send("gone", JSON.stringify({ error: e.message })); }
|
|
214
|
+
if (stopped) return;
|
|
215
|
+
up = http.get(d.url + "/api/events", { headers: { "x-editor-token": d.token } }, (r) => {
|
|
216
|
+
if (r.statusCode !== 200) { r.resume(); return retry(); }
|
|
217
|
+
d.sse++;
|
|
218
|
+
tries = 0;
|
|
219
|
+
let buf = "", ended = false;
|
|
220
|
+
r.setEncoding("utf8");
|
|
221
|
+
r.on("data", (chunk) => {
|
|
222
|
+
buf += chunk;
|
|
223
|
+
for (let i; (i = buf.indexOf("\n\n")) >= 0;) {
|
|
224
|
+
const block = buf.slice(0, i);
|
|
225
|
+
buf = buf.slice(i + 2);
|
|
226
|
+
const type = /^event: (.*)$/m.exec(block)?.[1], data = /^data: (.*)$/m.exec(block)?.[1];
|
|
227
|
+
if (type && data) send(type, data);
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
const end = () => {
|
|
231
|
+
if (ended) return;
|
|
232
|
+
ended = true;
|
|
233
|
+
d.sse--;
|
|
234
|
+
d.last = Date.now();
|
|
235
|
+
if (!stopped) { send("gone", "{}"); retry(); }
|
|
236
|
+
};
|
|
237
|
+
r.on("end", end); r.on("aborted", end); r.on("error", end); r.on("close", end);
|
|
238
|
+
});
|
|
239
|
+
up.on("error", retry);
|
|
240
|
+
}
|
|
241
|
+
connect();
|
|
242
|
+
return { stop() { stopped = true; up?.destroy(); } };
|
|
243
|
+
}
|
|
244
|
+
async function readJson(req) {
|
|
245
|
+
let s = "";
|
|
246
|
+
for await (const c of req) { s += c; if (s.length > 10000) throw new Error("body too large"); }
|
|
247
|
+
return s ? JSON.parse(s) : {};
|
|
248
|
+
}
|
|
249
|
+
|
|
177
250
|
/* ---- HTTP ------------------------------------------------------------------------------------- */
|
|
178
251
|
const esc = (s) => String(s ?? "").replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
|
|
179
252
|
const text = (res, code, s) => { res.writeHead(code, { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" }); res.end(s); };
|
|
@@ -210,12 +283,21 @@ function serveEditorFile(res, rel) {
|
|
|
210
283
|
const server = http.createServer(async (req, res) => {
|
|
211
284
|
const host = String(req.headers.host || "").toLowerCase();
|
|
212
285
|
if (!goodHost(host)) return text(res, 403, "Bad host");
|
|
213
|
-
const url = new URL(req.url, ORIGIN);
|
|
214
|
-
const p = url.pathname;
|
|
215
286
|
try {
|
|
287
|
+
const url = new URL(req.url, ORIGIN);
|
|
288
|
+
const p = url.pathname;
|
|
216
289
|
if (req.method === "GET" && p === "/login") return login(res, url);
|
|
217
290
|
// the editor's own scripts and styles hold nothing secret; the deck page loads bridge.js from here
|
|
218
291
|
if (req.method === "GET" && p.startsWith("/editor/")) return serveEditorFile(res, p.slice(8));
|
|
292
|
+
/* A deck's pages and files under /d/<deck>/v/<token>/: asked for by the deck itself, which runs sandboxed
|
|
293
|
+
without the login cookie (see VIEW in server.mjs). Passed on without the token, so the deck server's own
|
|
294
|
+
check of the path is all they get; only to a deck that is open, so they never start one. */
|
|
295
|
+
const view = /^\/d\/([\w.-]+)(\/v\/.*)$/.exec(p);
|
|
296
|
+
if (view && !authed(req)) {
|
|
297
|
+
const d = decks.get(view[1]);
|
|
298
|
+
if (!d?.url || (req.method !== "GET" && req.method !== "HEAD")) return text(res, 403, "Not logged in");
|
|
299
|
+
return proxy(req, res, d, view[2] + url.search, true);
|
|
300
|
+
}
|
|
219
301
|
if (!authed(req)) {
|
|
220
302
|
if (req.method === "GET" && /^\/(d\/[^/]+\/(index\.html)?)?$/.test(p)) {
|
|
221
303
|
res.writeHead(403, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store" });
|
|
@@ -234,6 +316,20 @@ const server = http.createServer(async (req, res) => {
|
|
|
234
316
|
if (p === "/api/ping") return json(res, 200, { ok: true, front: true });
|
|
235
317
|
if (p === "/api/login-link") return json(res, 200, { url: loginUrl(loginCode(3600000), url.searchParams.get("next") || "/") });
|
|
236
318
|
if (p === "/api/decks") return json(res, 200, { decks: deckList() });
|
|
319
|
+
if (p === "/events" && req.method === "GET") {
|
|
320
|
+
const id = url.searchParams.get("stream") || "";
|
|
321
|
+
return /^[\w-]{8,64}$/.test(id) ? eventStream(res, id) : text(res, 400, "Bad stream");
|
|
322
|
+
}
|
|
323
|
+
if (p === "/events/subscribe" && req.method === "POST") {
|
|
324
|
+
const b = await readJson(req);
|
|
325
|
+
const st = streams.get(String(b.stream));
|
|
326
|
+
const tab = String(b.tab || "");
|
|
327
|
+
if (!st || !/^\d{1,9}$/.test(tab)) return json(res, 404, { error: "No such stream." });
|
|
328
|
+
st.tabs.get(tab)?.stop();
|
|
329
|
+
st.tabs.delete(tab);
|
|
330
|
+
if (b.on && /^[\w.-]+$/.test(String(b.deck))) st.tabs.set(tab, follow(st, tab, String(b.deck)));
|
|
331
|
+
return json(res, 200, {});
|
|
332
|
+
}
|
|
237
333
|
|
|
238
334
|
const m = /^\/d\/([\w.-]+)(\/.*)?$/.exec(p);
|
|
239
335
|
if (!m) return text(res, 404, "Not found");
|
|
@@ -254,20 +350,18 @@ const server = http.createServer(async (req, res) => {
|
|
|
254
350
|
catch (e) { return page(res, 502, "PinDeck could not open this deck", `<pre>${esc(e.message)}</pre><p><a href="/">All decks</a></p>`); }
|
|
255
351
|
return proxy(req, res, d, m[2] + url.search);
|
|
256
352
|
} catch (e) {
|
|
257
|
-
|
|
353
|
+
const bad = e instanceof SyntaxError || e instanceof URIError || e.message === "body too large";
|
|
354
|
+
if (!res.headersSent) text(res, bad ? 400 : 500, String(e.message || e));
|
|
258
355
|
}
|
|
259
356
|
});
|
|
260
357
|
|
|
261
358
|
/* ---- Start ----------------------------------------------------------------------------------------
|
|
262
359
|
The port is bound first, then the start deck opens (its server checks it before writing anything). If it
|
|
263
360
|
cannot, the front says why and stops: as before the front, nothing is written. */
|
|
361
|
+
let removeServerInfo = () => {};
|
|
264
362
|
function writeServerInfo() {
|
|
265
363
|
fs.mkdirSync(HOME_DIR, { recursive: true, mode: 0o700 });
|
|
266
|
-
|
|
267
|
-
fs.chmodSync(SERVER_FILE, 0o600);
|
|
268
|
-
}
|
|
269
|
-
function removeServerInfo() {
|
|
270
|
-
try { if (JSON.parse(fs.readFileSync(SERVER_FILE, "utf8")).token === TOKEN) fs.rmSync(SERVER_FILE, { force: true }); } catch {}
|
|
364
|
+
removeServerInfo = registerServer(HOME_DIR, { url: ORIGIN, token: TOKEN, deckDir: START.dir, front: true });
|
|
271
365
|
}
|
|
272
366
|
let listening = false;
|
|
273
367
|
server.on("error", (e) => {
|
|
@@ -291,6 +385,7 @@ server.listen(PORT, "127.0.0.1", async () => {
|
|
|
291
385
|
writeServerInfo();
|
|
292
386
|
console.log(`PinDeck: ${loginUrl(loginCode(3600000))}`);
|
|
293
387
|
console.log(" (a one-time login link, valid for an hour; for a new one: pindeck login)");
|
|
388
|
+
openOnEnter(() => loginUrl(loginCode(3600000)));
|
|
294
389
|
console.log(chooser ? `Decks: ${START.dir}` : `Deck: ${START.dir}`);
|
|
295
390
|
console.log(`Server: ${CAPS.label}`);
|
|
296
391
|
for (const [name, c] of Object.entries(publicCapabilities(CAPS)))
|