@paneui/cli 0.0.11 → 0.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,609 @@
1
+ // The `pane demo` tutorial artifact — a self-contained HTML page that teaches
2
+ // Pane *by being a pane*. Authored as a string constant (OPEN-1 fallback (a)):
3
+ // the CLI ships independently of the relay and talks to a remote relay over
4
+ // HTTP, so the artifact has to travel inside the `POST /v1/panes` create body
5
+ // (`template.source`). A relay static asset would mean fetching the artifact
6
+ // *back* from the relay before sending it on — circular, network-dependent,
7
+ // and broken offline. A build-time string is the single source the CLI demo
8
+ // uses today; a future landing-page mount can inline the same HTML from this
9
+ // module (it's a plain ESM export).
10
+ //
11
+ // Constraints (the content-frame CSP, src/bridge/routes.ts):
12
+ // default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline';
13
+ // img-src data: attachment:; connect-src 'none'
14
+ // => fully self-contained: inline CSS + inline JS, NO external resources
15
+ // (no CDN, no remote fonts, no fetch). Animation is CSS keyframes + the
16
+ // Web Animations API only.
17
+ //
18
+ // Runtime surface used (confirmed against
19
+ // src/bridge/client/runtime.client.ts): pane.emit(type, data?), pane.on(type,
20
+ // handler), pane.inputData, pane.ready. Nothing else.
21
+ //
22
+ // Event schema (the contract `pane demo` registers — kept here so the artifact
23
+ // and its schema stay in one file and cannot drift):
24
+ // demo.start page {} — Scene 1 "Show me how"
25
+ // demo.hello page {} — Scene 3 "Click me" (the proof)
26
+ // demo.form page { name, choice } — Scene 4 structured data
27
+ // demo.advance agent { scene, note? } — drive the next scene in
28
+ // demo.echo agent { received } — reflect the form payload back
29
+ // demo.done agent {} — render the final CTA
30
+ /**
31
+ * The tutorial event schema, in the legacy `{ events: { type: { payload,
32
+ * emittedBy } } }` shape (still fully supported; see skills/pane/SKILL.md).
33
+ * Exported so the demo command and its tests reference the one definition.
34
+ */
35
+ export const DEMO_EVENT_SCHEMA = {
36
+ events: {
37
+ "demo.start": {
38
+ emittedBy: ["page"],
39
+ payload: { type: "object", additionalProperties: false },
40
+ },
41
+ "demo.hello": {
42
+ emittedBy: ["page"],
43
+ payload: { type: "object", additionalProperties: false },
44
+ },
45
+ "demo.form": {
46
+ emittedBy: ["page"],
47
+ payload: {
48
+ type: "object",
49
+ properties: {
50
+ name: { type: "string", maxLength: 80 },
51
+ choice: { type: "string", enum: ["build", "explore", "watch"] },
52
+ },
53
+ required: ["choice"],
54
+ additionalProperties: false,
55
+ },
56
+ },
57
+ "demo.advance": {
58
+ emittedBy: ["agent"],
59
+ payload: {
60
+ type: "object",
61
+ properties: {
62
+ scene: { type: "integer" },
63
+ note: { type: "string" },
64
+ },
65
+ required: ["scene"],
66
+ additionalProperties: false,
67
+ },
68
+ },
69
+ "demo.echo": {
70
+ emittedBy: ["agent"],
71
+ payload: {
72
+ type: "object",
73
+ properties: { received: { type: "object" } },
74
+ required: ["received"],
75
+ additionalProperties: false,
76
+ },
77
+ },
78
+ "demo.done": {
79
+ emittedBy: ["agent"],
80
+ payload: { type: "object", additionalProperties: false },
81
+ },
82
+ },
83
+ };
84
+ /** The tab title for the demo pane. */
85
+ export const DEMO_TITLE = "Pane — the 60-second tour";
86
+ /** The auto-created template's name (inline-form `--name`). */
87
+ export const DEMO_TEMPLATE_NAME = "Pane demo tour";
88
+ // The artifact HTML. One document, no external resources. The mode switch
89
+ // (`live` | `simulated`) is read from inputData so a future landing-page
90
+ // mount can drop the same blob in `simulated` mode without a rewrite — in
91
+ // LIVE mode (the only mode this PR ships) the real agent loop drives every
92
+ // agent event over the relay; SIMULATED is a deliberate no-op placeholder
93
+ // here (the scripted-echo path is a fast-follow, OPEN-3).
94
+ export const DEMO_ARTIFACT_HTML = `<!doctype html>
95
+ <html lang="en">
96
+ <head>
97
+ <meta charset="utf-8" />
98
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
99
+ <title>Pane — the 60-second tour</title>
100
+ <style>
101
+ :root {
102
+ --bg: #0b0d12;
103
+ --panel: #141821;
104
+ --ink: #e7ebf3;
105
+ --muted: #97a1b5;
106
+ --line: #232a37;
107
+ --accent: #6ea8fe;
108
+ --accent-2: #8b78ff;
109
+ --ok: #46d39a;
110
+ --radius: 14px;
111
+ --font: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica,
112
+ Arial, sans-serif;
113
+ --mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
114
+ }
115
+ * { box-sizing: border-box; }
116
+ html, body { height: 100%; }
117
+ body {
118
+ margin: 0;
119
+ background:
120
+ radial-gradient(1100px 600px at 50% -10%, #1a2030 0%, var(--bg) 60%);
121
+ color: var(--ink);
122
+ font-family: var(--font);
123
+ -webkit-font-smoothing: antialiased;
124
+ display: flex;
125
+ align-items: center;
126
+ justify-content: center;
127
+ min-height: 100%;
128
+ padding: 24px;
129
+ line-height: 1.5;
130
+ }
131
+ .stage {
132
+ width: 100%;
133
+ max-width: 560px;
134
+ background: var(--panel);
135
+ border: 1px solid var(--line);
136
+ border-radius: var(--radius);
137
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.4);
138
+ overflow: hidden;
139
+ }
140
+ .progress {
141
+ display: flex;
142
+ gap: 6px;
143
+ padding: 16px 22px 0;
144
+ }
145
+ .progress i {
146
+ flex: 1;
147
+ height: 3px;
148
+ border-radius: 3px;
149
+ background: var(--line);
150
+ transition: background 0.4s ease;
151
+ }
152
+ .progress i.on { background: linear-gradient(90deg, var(--accent), var(--accent-2)); }
153
+ .body { padding: 26px 30px 30px; }
154
+ .scene { display: none; }
155
+ .scene.active { display: block; }
156
+ .kicker {
157
+ font-size: 12px;
158
+ letter-spacing: 0.14em;
159
+ text-transform: uppercase;
160
+ color: var(--muted);
161
+ margin: 0 0 10px;
162
+ }
163
+ h1 {
164
+ font-size: 24px;
165
+ line-height: 1.25;
166
+ margin: 0 0 12px;
167
+ letter-spacing: -0.01em;
168
+ }
169
+ p { margin: 0 0 14px; color: #c7cedd; }
170
+ p.lead { color: var(--ink); font-size: 16px; }
171
+ .muted { color: var(--muted); font-size: 14px; }
172
+ button.cta {
173
+ appearance: none;
174
+ border: 0;
175
+ cursor: pointer;
176
+ font: inherit;
177
+ font-weight: 600;
178
+ color: #0b0d12;
179
+ background: linear-gradient(90deg, var(--accent), var(--accent-2));
180
+ padding: 12px 20px;
181
+ border-radius: 10px;
182
+ margin-top: 6px;
183
+ transition: transform 0.12s ease, box-shadow 0.12s ease, opacity 0.2s ease;
184
+ box-shadow: 0 6px 18px rgba(110, 168, 254, 0.28);
185
+ }
186
+ button.cta:hover { transform: translateY(-1px); box-shadow: 0 10px 26px rgba(110, 168, 254, 0.38); }
187
+ button.cta:active { transform: translateY(0); }
188
+ button.cta:disabled { opacity: 0.55; cursor: default; transform: none; box-shadow: none; }
189
+ .diagram {
190
+ display: flex;
191
+ align-items: center;
192
+ justify-content: space-between;
193
+ gap: 8px;
194
+ margin: 6px 0 18px;
195
+ padding: 18px 14px;
196
+ border: 1px solid var(--line);
197
+ border-radius: 12px;
198
+ background: #10141d;
199
+ }
200
+ .node {
201
+ flex: 1;
202
+ text-align: center;
203
+ font-size: 13px;
204
+ color: var(--ink);
205
+ padding: 12px 6px;
206
+ border: 1px solid var(--line);
207
+ border-radius: 10px;
208
+ background: #161b26;
209
+ }
210
+ .node .ic { font-size: 20px; display: block; margin-bottom: 4px; }
211
+ .node small { display: block; color: var(--muted); font-size: 11px; margin-top: 2px; }
212
+ .wire { color: var(--accent); font-size: 18px; opacity: 0.7; }
213
+ .field { display: block; margin: 0 0 14px; }
214
+ .field span { display: block; font-size: 13px; color: var(--muted); margin-bottom: 6px; }
215
+ input[type="text"] {
216
+ width: 100%;
217
+ font: inherit;
218
+ color: var(--ink);
219
+ background: #0e121a;
220
+ border: 1px solid var(--line);
221
+ border-radius: 9px;
222
+ padding: 11px 12px;
223
+ outline: none;
224
+ }
225
+ input[type="text"]:focus { border-color: var(--accent); }
226
+ .choices { display: flex; gap: 8px; flex-wrap: wrap; }
227
+ .choices label {
228
+ flex: 1;
229
+ min-width: 120px;
230
+ border: 1px solid var(--line);
231
+ border-radius: 9px;
232
+ padding: 11px 12px;
233
+ cursor: pointer;
234
+ background: #0e121a;
235
+ transition: border-color 0.15s ease, background 0.15s ease;
236
+ }
237
+ .choices label.sel { border-color: var(--accent); background: #141b2b; }
238
+ .choices input { position: absolute; opacity: 0; pointer-events: none; }
239
+ .choices b { display: block; font-size: 14px; }
240
+ .choices em { display: block; font-style: normal; color: var(--muted); font-size: 12px; }
241
+ pre {
242
+ margin: 0 0 14px;
243
+ padding: 14px 16px;
244
+ background: #0e121a;
245
+ border: 1px solid var(--line);
246
+ border-radius: 10px;
247
+ font-family: var(--mono);
248
+ font-size: 13px;
249
+ color: #cdd6e6;
250
+ overflow-x: auto;
251
+ white-space: pre-wrap;
252
+ word-break: break-word;
253
+ }
254
+ pre .k { color: var(--accent); }
255
+ pre .s { color: var(--ok); }
256
+ .badge {
257
+ display: inline-flex;
258
+ align-items: center;
259
+ gap: 7px;
260
+ font-size: 13px;
261
+ color: var(--ok);
262
+ border: 1px solid rgba(70, 211, 154, 0.35);
263
+ background: rgba(70, 211, 154, 0.08);
264
+ padding: 6px 11px;
265
+ border-radius: 999px;
266
+ margin: 0 0 14px;
267
+ }
268
+ .point {
269
+ border-left: 2px solid var(--accent);
270
+ padding: 2px 0 2px 12px;
271
+ margin: 0 0 8px;
272
+ color: var(--ink);
273
+ }
274
+ .log { list-style: none; margin: 0 0 16px; padding: 0; }
275
+ .log li {
276
+ display: flex;
277
+ gap: 10px;
278
+ align-items: baseline;
279
+ padding: 9px 12px;
280
+ border: 1px solid var(--line);
281
+ border-radius: 9px;
282
+ margin-bottom: 7px;
283
+ background: #0e121a;
284
+ font-size: 13px;
285
+ }
286
+ .log code { font-family: var(--mono); color: var(--accent); }
287
+ .log .who { color: var(--muted); font-size: 11px; margin-left: auto; }
288
+ .anim-in { animation: rise 0.5s cubic-bezier(0.2, 0.7, 0.2, 1) both; }
289
+ @keyframes rise {
290
+ from { opacity: 0; transform: translateY(10px); }
291
+ to { opacity: 1; transform: translateY(0); }
292
+ }
293
+ @media (prefers-reduced-motion: reduce) {
294
+ .anim-in { animation: none; }
295
+ * { transition: none !important; }
296
+ }
297
+ </style>
298
+ </head>
299
+ <body>
300
+ <main class="stage" role="application" aria-label="Pane tutorial">
301
+ <div class="progress" aria-hidden="true">
302
+ <i data-step="1"></i><i data-step="2"></i><i data-step="3"></i>
303
+ <i data-step="4"></i><i data-step="5"></i><i data-step="6"></i>
304
+ </div>
305
+ <div class="body">
306
+ <!-- Scene 1 — Hook -->
307
+ <section class="scene" data-scene="1">
308
+ <p class="kicker">A pane</p>
309
+ <h1>You're looking at a pane.</h1>
310
+ <p class="lead">
311
+ An agent just handed you this UI — by URL, nothing installed. Whatever
312
+ you do here turns into structured data the agent reads back.
313
+ </p>
314
+ <p class="muted">Let's prove it in about a minute.</p>
315
+ <button class="cta" id="b-start" type="button">Show me how &rarr;</button>
316
+ </section>
317
+
318
+ <!-- Scene 2 — The model -->
319
+ <section class="scene" data-scene="2">
320
+ <p class="kicker">The round-trip</p>
321
+ <h1>How it works</h1>
322
+ <div class="diagram" aria-hidden="true">
323
+ <div class="node"><span class="ic">&#9881;</span>your terminal<small>the agent</small></div>
324
+ <span class="wire">&#8644;</span>
325
+ <div class="node"><span class="ic">&#9729;</span>relay<small>routes events</small></div>
326
+ <span class="wire">&#8644;</span>
327
+ <div class="node"><span class="ic">&#9638;</span>this page<small>the pane</small></div>
328
+ </div>
329
+ <p>
330
+ You ran <code>pane demo</code> &mdash; that command is acting as your
331
+ agent right now. It's watching this session over a WebSocket.
332
+ </p>
333
+ <button class="cta" id="b-hello" type="button">Click me</button>
334
+ </section>
335
+
336
+ <!-- Scene 3 — First emit (the proof) -->
337
+ <section class="scene" data-scene="3">
338
+ <div class="badge">&#10003; Your agent just received your click.</div>
339
+ <h1>That landed in two places.</h1>
340
+ <p class="point">Look at your terminal — it printed the same event.</p>
341
+ <p>
342
+ The click became a <code>demo.hello</code> event, streamed to the
343
+ agent, which streamed a reply back to redraw this page. No polling, no
344
+ refresh.
345
+ </p>
346
+ <p class="muted">Now the interesting part: typed, validated data.</p>
347
+ <button class="cta" id="b-to-form" type="button">Next &rarr;</button>
348
+ </section>
349
+
350
+ <!-- Scene 4 — Structured data -->
351
+ <section class="scene" data-scene="4">
352
+ <p class="kicker">Structured data</p>
353
+ <h1>Interactions aren't clicks — they're data.</h1>
354
+ <label class="field">
355
+ <span>Your name (optional)</span>
356
+ <input type="text" id="f-name" autocomplete="off" maxlength="80"
357
+ placeholder="e.g. Sam" />
358
+ </label>
359
+ <div class="field">
360
+ <span>What brought you here?</span>
361
+ <div class="choices" id="f-choices">
362
+ <label data-choice="build"><input type="radio" name="choice" value="build" />
363
+ <b>Build</b><em>wire it into an agent</em></label>
364
+ <label data-choice="explore"><input type="radio" name="choice" value="explore" />
365
+ <b>Explore</b><em>just looking</em></label>
366
+ <label data-choice="watch"><input type="radio" name="choice" value="watch" />
367
+ <b>Watch</b><em>show me more</em></label>
368
+ </div>
369
+ </div>
370
+ <button class="cta" id="b-form" type="button" disabled>Send it &rarr;</button>
371
+ <div id="echo" hidden>
372
+ <div class="badge" style="margin-top:16px">&#10003; Your agent received:</div>
373
+ <pre id="echo-pre"></pre>
374
+ <p class="muted">
375
+ That's the exact payload — typed and validated by the relay before
376
+ the agent ever saw it.
377
+ </p>
378
+ </div>
379
+ </section>
380
+
381
+ <!-- Scene 5 — State / the log -->
382
+ <section class="scene" data-scene="5">
383
+ <p class="kicker">The event log</p>
384
+ <h1>Everything you did is a log.</h1>
385
+ <p>
386
+ A pane is an append-only event log your agent can read at any time.
387
+ Here's yours so far:
388
+ </p>
389
+ <ul class="log" id="log"></ul>
390
+ <p class="muted">
391
+ From your terminal that's just:
392
+ <code style="font-family:var(--mono);color:var(--accent)">pane show &lt;id&gt;</code>
393
+ </p>
394
+ </section>
395
+
396
+ <!-- Scene 6 — Now you -->
397
+ <section class="scene" data-scene="6">
398
+ <p class="kicker">Your turn</p>
399
+ <h1>That's the whole idea.</h1>
400
+ <p>An agent hands a human a UI, gets structured data back. To build one:</p>
401
+ <pre><span class="k">pane</span> create \\
402
+ --template ./form.html --name "My form" \\
403
+ --event-schema ./schema.json
404
+ <span class="k">pane</span> watch &lt;id&gt; --type form.submitted</pre>
405
+ <p>
406
+ The full guide is in the skill:
407
+ <code style="font-family:var(--mono);color:var(--accent)">pane skill show</code>.
408
+ </p>
409
+ <p class="muted">Now go hand one to a human.</p>
410
+ </section>
411
+ </div>
412
+ </main>
413
+
414
+ <script>
415
+ (function () {
416
+ var reduce = false;
417
+ try {
418
+ reduce = window.matchMedia &&
419
+ window.matchMedia("(prefers-reduced-motion: reduce)").matches;
420
+ } catch (e) { reduce = false; }
421
+
422
+ var scenes = {};
423
+ var nodes = document.querySelectorAll(".scene");
424
+ for (var i = 0; i < nodes.length; i++) {
425
+ scenes[nodes[i].getAttribute("data-scene")] = nodes[i];
426
+ }
427
+ var steps = document.querySelectorAll(".progress i");
428
+ var current = 0;
429
+
430
+ function setProgress(n) {
431
+ for (var i = 0; i < steps.length; i++) {
432
+ var step = Number(steps[i].getAttribute("data-step"));
433
+ if (step <= n) steps[i].classList.add("on");
434
+ else steps[i].classList.remove("on");
435
+ }
436
+ }
437
+
438
+ // Show a scene by number. Scenes are interaction-driven: scene 1 is shown
439
+ // immediately; every later scene is revealed by an agent reply (demo.advance
440
+ // / demo.echo / demo.done), never by the page on its own.
441
+ function show(n) {
442
+ if (n === current) return;
443
+ var el = scenes[String(n)];
444
+ if (!el) return;
445
+ var prev = current ? scenes[String(current)] : null;
446
+ if (prev) prev.classList.remove("active");
447
+ el.classList.add("active");
448
+ current = n;
449
+ setProgress(n);
450
+ if (!reduce && el.animate) {
451
+ try {
452
+ el.animate(
453
+ [
454
+ { opacity: 0, transform: "translateY(10px)" },
455
+ { opacity: 1, transform: "translateY(0)" },
456
+ ],
457
+ { duration: 460, easing: "cubic-bezier(0.2,0.7,0.2,1)" }
458
+ );
459
+ } catch (e) { /* WAAPI unavailable — the scene still shows */ }
460
+ }
461
+ var focusable = el.querySelector("button.cta:not([disabled]), input");
462
+ if (focusable && focusable.focus) {
463
+ try { focusable.focus(); } catch (e) { /* ignore */ }
464
+ }
465
+ }
466
+
467
+ function esc(s) {
468
+ return String(s)
469
+ .replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
470
+ }
471
+
472
+ // Render the form payload the agent echoed back, as pretty (highlighted)
473
+ // JSON. Only fields we understand are rendered — never the raw envelope.
474
+ function renderEcho(received) {
475
+ var obj = received && typeof received === "object" ? received : {};
476
+ var parts = [];
477
+ parts.push("{");
478
+ var keys = ["name", "choice"];
479
+ var shown = [];
480
+ for (var i = 0; i < keys.length; i++) {
481
+ var k = keys[i];
482
+ if (Object.prototype.hasOwnProperty.call(obj, k)) {
483
+ shown.push(
484
+ ' <span class="k">"' + esc(k) + '"</span>: ' +
485
+ '<span class="s">' + JSON.stringify(obj[k]) + "</span>"
486
+ );
487
+ }
488
+ }
489
+ parts.push(shown.join(",\\n"));
490
+ parts.push("}");
491
+ document.getElementById("echo-pre").innerHTML = parts.join("\\n");
492
+ document.getElementById("echo").hidden = false;
493
+ }
494
+
495
+ // Render the event log for scene 5 from pane.state — the user's own emits.
496
+ var EVENT_LABEL = {
497
+ "demo.start": "opened the tour",
498
+ "demo.hello": "clicked the button",
499
+ "demo.form": "submitted the form",
500
+ };
501
+ function renderLog() {
502
+ var log = document.getElementById("log");
503
+ if (!log) return;
504
+ log.innerHTML = "";
505
+ var events = [];
506
+ try { events = pane.state.events || []; } catch (e) { events = []; }
507
+ var any = false;
508
+ for (var i = 0; i < events.length; i++) {
509
+ var t = events[i].type;
510
+ if (!Object.prototype.hasOwnProperty.call(EVENT_LABEL, t)) continue;
511
+ any = true;
512
+ var li = document.createElement("li");
513
+ var code = document.createElement("code");
514
+ code.textContent = t;
515
+ var label = document.createElement("span");
516
+ label.textContent = EVENT_LABEL[t];
517
+ var who = document.createElement("span");
518
+ who.className = "who";
519
+ who.textContent = "you";
520
+ li.appendChild(code);
521
+ li.appendChild(label);
522
+ li.appendChild(who);
523
+ log.appendChild(li);
524
+ }
525
+ if (!any) {
526
+ var li2 = document.createElement("li");
527
+ li2.textContent = "Your events will appear here.";
528
+ log.appendChild(li2);
529
+ }
530
+ }
531
+
532
+ // --- wiring -----------------------------------------------------------
533
+
534
+ // Scene 1 -> demo.start. The agent replies demo.advance{scene:2}.
535
+ document.getElementById("b-start").addEventListener("click", function () {
536
+ var b = this;
537
+ b.disabled = true;
538
+ pane.emit("demo.start", {})["catch"](function () { b.disabled = false; });
539
+ });
540
+
541
+ // Scene 2 -> demo.hello (the proof). The agent replies demo.advance{scene:3}.
542
+ document.getElementById("b-hello").addEventListener("click", function () {
543
+ var b = this;
544
+ b.disabled = true;
545
+ pane.emit("demo.hello", {})["catch"](function () { b.disabled = false; });
546
+ });
547
+
548
+ // Scene 3 -> reveal the form. This is a local navigation step (no round-trip
549
+ // needed to read the form), so it just shows scene 4.
550
+ document.getElementById("b-to-form").addEventListener("click", function () {
551
+ show(4);
552
+ });
553
+
554
+ // Scene 4 — the structured-data form. choice is required; name optional.
555
+ var choiceWrap = document.getElementById("f-choices");
556
+ var formBtn = document.getElementById("b-form");
557
+ var picked = null;
558
+ choiceWrap.addEventListener("change", function (e) {
559
+ var label = e.target.closest ? e.target.closest("label") : null;
560
+ var labels = choiceWrap.querySelectorAll("label");
561
+ for (var i = 0; i < labels.length; i++) labels[i].classList.remove("sel");
562
+ if (label) {
563
+ label.classList.add("sel");
564
+ picked = label.getAttribute("data-choice");
565
+ formBtn.disabled = false;
566
+ }
567
+ });
568
+ formBtn.addEventListener("click", function () {
569
+ if (!picked) return;
570
+ var name = document.getElementById("f-name").value.trim();
571
+ var data = { choice: picked };
572
+ if (name) data.name = name;
573
+ formBtn.disabled = true;
574
+ // The agent replies demo.echo{received} (then walks scenes 5 + 6).
575
+ pane.emit("demo.form", data)["catch"](function () {
576
+ formBtn.disabled = false;
577
+ });
578
+ });
579
+
580
+ // --- agent-driven scene changes --------------------------------------
581
+
582
+ pane.on("demo.advance", function (ev) {
583
+ var scene = ev && ev.data && Number(ev.data.scene);
584
+ if (scene === 5) renderLog();
585
+ if (scene >= 1 && scene <= 6) show(scene);
586
+ });
587
+
588
+ pane.on("demo.echo", function (ev) {
589
+ var received = ev && ev.data ? ev.data.received : null;
590
+ renderEcho(received);
591
+ });
592
+
593
+ pane.on("demo.done", function () {
594
+ show(6);
595
+ });
596
+
597
+ // First paint. mode is read for forward-compat (a future landing-page mount
598
+ // can pass { mode: "simulated" } and replay a canned trace through the same
599
+ // render path) but LIVE is the only behaviour this build ships.
600
+ pane.ready.then(function () {
601
+ show(1);
602
+ });
603
+ // pane.ready resolves on the init frame; show scene 1 eagerly too so the
604
+ // page is never blank if init is momentarily delayed.
605
+ if (!current) show(1);
606
+ })();
607
+ </script>
608
+ </body>
609
+ </html>`;
@@ -0,0 +1,305 @@
1
+ // `pane demo` — a self-teaching tutorial pane.
2
+ //
3
+ // One command: create a pane with the bundled tutorial artifact, open (or
4
+ // print) its URL, then run a tiny built-in agent loop in this same process
5
+ // that watches the session and reacts to each human interaction with the
6
+ // matching agent event. Every received event is echoed to the terminal as it
7
+ // lands, so the user sees their click in BOTH places — the pane redraws AND
8
+ // their terminal prints the same event. That round-trip IS the lesson, and a
9
+ // successful demo doubles as an end-to-end smoke test of the install (auth,
10
+ // relay reachability, WebSocket, a real event round-trip).
11
+ //
12
+ // Run-to-completion: the loop walks Scenes 1-6, sends demo.done, prints the
13
+ // "build your own" snippet, and exits 0. The pane is created with a short TTL
14
+ // (the relay's sweeper reclaims it) and best-effort DELETEd on exit.
15
+ import { openStream, PaneClient, } from "@paneui/core";
16
+ import { spawn } from "node:child_process";
17
+ import { platform } from "node:os";
18
+ import { assertKnownFlags } from "../argv.js";
19
+ import { resolveConfig } from "../config.js";
20
+ import { fail, failFromError } from "../output.js";
21
+ import { VERSION } from "../version.js";
22
+ import { DEMO_ARTIFACT_HTML, DEMO_EVENT_SCHEMA, DEMO_TEMPLATE_NAME, DEMO_TITLE, } from "./demo-artifact.js";
23
+ const KNOWN_FLAGS = ["ttl"];
24
+ // --no-open is the documented spelling; the parser stores it as the boolean
25
+ // flag "no-open" (a leading `--no-` is NOT auto-negated by this CLI's parser,
26
+ // so we read the literal flag name).
27
+ const KNOWN_BOOLS = ["no-open", "json"];
28
+ // The default pane TTL for the demo: long enough to read through the tour at a
29
+ // relaxed pace, short enough that an abandoned demo is reclaimed promptly. The
30
+ // relay clamps this to its own MAX_TTL_SECONDS regardless.
31
+ const DEMO_TTL_SECONDS = 900;
32
+ export const demoHelp = `pane demo — take the 60-second guided tour
33
+
34
+ Usage:
35
+ pane demo [options]
36
+
37
+ Creates a short-lived pane with the built-in tutorial artifact, opens its URL
38
+ in your browser (or prints it if none is available), then runs a tiny agent
39
+ loop right here in your terminal that watches the session and reacts to each
40
+ thing you do in the pane. Every event you trigger is echoed to this terminal
41
+ as it arrives — so you see your click land in BOTH places at once.
42
+
43
+ It's also a full smoke test: if your install is healthy, the tour completes;
44
+ if auth, the relay, or the WebSocket is broken, it fails loudly at the exact
45
+ step that's wrong.
46
+
47
+ The loop runs to completion (Scenes 1-6), then prints a "build your own"
48
+ snippet and exits 0. The demo pane is created with a short TTL and deleted on
49
+ exit.
50
+
51
+ Options:
52
+ --ttl <seconds> Demo pane time-to-live (default ${DEMO_TTL_SECONDS}). The relay
53
+ clamps to its configured maximum.
54
+ --no-open Don't try to open a browser — just print the URL. Implied
55
+ on headless / SSH sessions where no opener is found.
56
+ --url <url> Relay base URL (overrides PANE_URL).
57
+ --api-key <key> Agent API key (overrides PANE_API_KEY).
58
+ -h, --help Show this help.
59
+
60
+ Run it right after 'pane agent register' to confirm everything works.`;
61
+ export function demoReactions(humanEventType, humanData) {
62
+ switch (humanEventType) {
63
+ case "demo.start":
64
+ return [{ type: "demo.advance", data: { scene: 2 } }];
65
+ case "demo.hello":
66
+ return [
67
+ {
68
+ type: "demo.advance",
69
+ data: {
70
+ scene: 3,
71
+ note: "received your click — printed in your terminal",
72
+ },
73
+ },
74
+ ];
75
+ case "demo.form": {
76
+ const received = humanData && typeof humanData === "object"
77
+ ? humanData
78
+ : {};
79
+ return [
80
+ { type: "demo.echo", data: { received } },
81
+ { type: "demo.advance", data: { scene: 5 }, delayMs: 1200 },
82
+ { type: "demo.done", data: {}, delayMs: 2400 },
83
+ ];
84
+ }
85
+ default:
86
+ return [];
87
+ }
88
+ }
89
+ /** The human event types the demo loop reacts to (its terminal one is demo.form). */
90
+ const HUMAN_EVENT_TYPES = new Set(["demo.start", "demo.hello", "demo.form"]);
91
+ /** The "build your own" snippet printed on completion. */
92
+ function buildYourOwnSnippet() {
93
+ return [
94
+ "",
95
+ "That's the round-trip. To hand your own UI to a human:",
96
+ "",
97
+ " pane create \\",
98
+ ' --template ./form.html --name "My form" \\',
99
+ " --event-schema ./schema.json",
100
+ " pane watch <id> --type form.submitted",
101
+ "",
102
+ "Full guide: pane skill show",
103
+ "Docs: https://paneui.com",
104
+ "",
105
+ ].join("\n");
106
+ }
107
+ /**
108
+ * Best-effort open a URL in the user's default browser. Returns true if an
109
+ * opener was spawned, false if none is available (headless / unknown platform)
110
+ * or the spawn failed. Never throws — the tour works headless either way.
111
+ */
112
+ export function openInBrowser(url) {
113
+ // No env-var gating here: on headless / CI boxes the platform opener simply
114
+ // isn't installed (or errors), the spawn failure is swallowed below, and the
115
+ // caller falls back to printing the URL. (`--no-open` is handled upstream.)
116
+ const p = platform();
117
+ let cmd;
118
+ let args;
119
+ if (p === "darwin") {
120
+ cmd = "open";
121
+ args = [url];
122
+ }
123
+ else if (p === "win32") {
124
+ // `start` is a cmd builtin; the empty "" is the (ignored) window title.
125
+ cmd = "cmd";
126
+ args = ["/c", "start", "", url];
127
+ }
128
+ else {
129
+ // Linux / BSD: xdg-open is the de-facto opener. On a headless box it
130
+ // won't exist; the spawn error is swallowed and we fall back to print.
131
+ cmd = "xdg-open";
132
+ args = [url];
133
+ }
134
+ try {
135
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
136
+ let failed = false;
137
+ child.on("error", () => {
138
+ failed = true;
139
+ });
140
+ child.unref();
141
+ // `spawn` reports a missing binary asynchronously via the 'error' event,
142
+ // so we can't know synchronously whether xdg-open exists. We optimistically
143
+ // report true; the printed URL below is always shown regardless, so a
144
+ // silent opener failure still leaves the user a working link.
145
+ return !failed;
146
+ }
147
+ catch {
148
+ return false;
149
+ }
150
+ }
151
+ export async function runDemo(args) {
152
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane demo");
153
+ let ttl = DEMO_TTL_SECONDS;
154
+ const ttlRaw = args.flags.get("ttl");
155
+ if (ttlRaw !== undefined) {
156
+ const t = Number(ttlRaw);
157
+ if (!Number.isInteger(t) || t <= 0) {
158
+ fail("--ttl must be a positive integer", "invalid_args");
159
+ }
160
+ ttl = t;
161
+ }
162
+ // Resolve config once and build the client from it — the agent loop below
163
+ // needs the API key directly (for the WS token), and makeClient would
164
+ // re-resolve the same config (extra disk read + parse) to no benefit.
165
+ const cfg = resolveConfig(args);
166
+ const client = new PaneClient({
167
+ url: cfg.url,
168
+ apiKey: cfg.apiKey,
169
+ cliVersion: VERSION,
170
+ });
171
+ // 1. Create the demo pane with the bundled artifact + its event schema.
172
+ let created;
173
+ try {
174
+ created = await client.createPane({
175
+ template: {
176
+ name: DEMO_TEMPLATE_NAME,
177
+ type: "html-inline",
178
+ source: DEMO_ARTIFACT_HTML,
179
+ event_schema: DEMO_EVENT_SCHEMA,
180
+ },
181
+ title: DEMO_TITLE,
182
+ ttl,
183
+ participants: { humans: 1 },
184
+ });
185
+ }
186
+ catch (e) {
187
+ failFromError(e);
188
+ }
189
+ const paneId = created.pane_id;
190
+ const humanUrl = created.urls.humans[0];
191
+ // 2. Open (or print) the URL. The loop runs either way, so headless / SSH
192
+ // works — we just skip the browser launch.
193
+ const wantOpen = !args.bools.has("no-open");
194
+ const out = process.stdout;
195
+ out.write(`\nPane demo — your 60-second tour is ready.\n\n`);
196
+ if (humanUrl) {
197
+ out.write(` ${humanUrl}\n\n`);
198
+ if (wantOpen && openInBrowser(humanUrl)) {
199
+ out.write(`Opening it in your browser…\n`);
200
+ }
201
+ else {
202
+ out.write(`Open this link to start the tour.\n`);
203
+ }
204
+ }
205
+ else {
206
+ out.write(`(No human URL was minted — check your relay configuration.)\n`);
207
+ }
208
+ out.write(`\nWatching the pane — your clicks will print here as they land:\n\n`);
209
+ // 3. Run the agent loop: watch the stream, react to each human event,
210
+ // echo every received event to the terminal, and finish on demo.done.
211
+ await runDemoLoop({
212
+ wsBaseUrl: client.wsBaseUrl,
213
+ paneId,
214
+ token: cfg.apiKey,
215
+ sendEvent: (type, data) => client.sendEvent(paneId, { type, data }).then(() => undefined),
216
+ deletePane: () => client.deletePane(paneId).catch(() => undefined),
217
+ write: (s) => {
218
+ out.write(s);
219
+ },
220
+ });
221
+ }
222
+ export function runDemoLoop(deps) {
223
+ const schedule = deps.schedule ?? ((fn, ms) => void setTimeout(fn, ms));
224
+ const open = deps.openStreamImpl ?? openStream;
225
+ return new Promise((resolve) => {
226
+ let settled = false;
227
+ // Late-bound so `finish` (defined before the stream is opened) can close
228
+ // it; `open()` returns synchronously below and every handler that calls
229
+ // `finish` fires asynchronously, so the binding is always set by then.
230
+ const ref = {};
231
+ // Track whether demo.done was actually sent, so an early stream close
232
+ // (TTL / human shut the tab) is reported as "before completion" rather
233
+ // than a successful finish.
234
+ let doneSent = false;
235
+ const finish = async () => {
236
+ if (settled)
237
+ return;
238
+ settled = true;
239
+ try {
240
+ ref.handle?.close();
241
+ }
242
+ catch {
243
+ /* ignore */
244
+ }
245
+ await deps.deletePane();
246
+ resolve();
247
+ };
248
+ // Dispatch a single agent reaction, honouring its delay. demo.done is the
249
+ // terminal event — once it's been emitted we wrap up.
250
+ const dispatch = (r) => {
251
+ const send = () => {
252
+ if (settled)
253
+ return;
254
+ deps
255
+ .sendEvent(r.type, r.data)
256
+ .then(() => {
257
+ if (r.type === "demo.done") {
258
+ doneSent = true;
259
+ deps.write(buildYourOwnSnippet());
260
+ void finish();
261
+ }
262
+ })
263
+ .catch((e) => {
264
+ deps.write(`\n[demo] failed to send ${r.type}: ${e instanceof Error ? e.message : String(e)}\n`);
265
+ void finish();
266
+ });
267
+ };
268
+ if (r.delayMs && r.delayMs > 0)
269
+ schedule(send, r.delayMs);
270
+ else
271
+ send();
272
+ };
273
+ ref.handle = open({ wsBaseUrl: deps.wsBaseUrl, paneId: deps.paneId, token: deps.token }, {
274
+ onEvent: (event) => {
275
+ if (settled)
276
+ return;
277
+ // Only react to (and echo) the human's own interactions. The agent's
278
+ // own replies stream back too — echoing those would double-print and
279
+ // re-trigger reactions.
280
+ if (!HUMAN_EVENT_TYPES.has(event.type))
281
+ return;
282
+ deps.write(` ← ${event.type} ${JSON.stringify(event.data ?? {})}\n`);
283
+ for (const r of demoReactions(event.type, event.data))
284
+ dispatch(r);
285
+ },
286
+ onClose: () => {
287
+ // If the pane closed before demo.done (e.g. TTL or the human shut the
288
+ // tab), still resolve cleanly — the loop is run-to-completion but a
289
+ // dropped human is a valid end, not a crash.
290
+ if (!doneSent) {
291
+ deps.write(`\n[demo] session closed before completion.\n`);
292
+ }
293
+ void finish();
294
+ },
295
+ onRelayError: (err) => {
296
+ deps.write(`\n[demo] relay error: ${err.message ?? err.code ?? "unknown"}\n`);
297
+ void finish();
298
+ },
299
+ onError: (err) => {
300
+ deps.write(`\n[demo] stream error: ${err.message}\n`);
301
+ void finish();
302
+ },
303
+ });
304
+ });
305
+ }
@@ -0,0 +1,184 @@
1
+ // `pane share <pane-id>` — manage identity sharing on a pane.
2
+ //
3
+ // pane share <pane-id> --email <addr> [--role participant|viewer]
4
+ // Invite a human by email (upsert). Role defaults to participant
5
+ // (read + emit); viewer is read-only.
6
+ // pane share <pane-id> --mode <invite-only|link|public>
7
+ // Set the pane-id (/p/<pane-id>) access mode. Convenience aliases:
8
+ // --public (= public), --link (= link), --invite-only (= invite_only).
9
+ // Token (/s/<token>) links are independent and keep working in every
10
+ // mode.
11
+ // pane share <pane-id> --list
12
+ // Show the pane's access_mode + every grant.
13
+ // pane share <pane-id> --revoke <grant-id>
14
+ // Remove one grant (idempotent).
15
+ //
16
+ // One verb per invocation. Output is machine-readable JSON on stdout; errors
17
+ // are `{"error":{"code","message"}}` on stderr with a non-zero exit.
18
+ import { assertKnownFlags } from "../argv.js";
19
+ import { makeClient } from "../config.js";
20
+ import { printJson, fail, failFromError } from "../output.js";
21
+ // Value-flags this command accepts (in addition to the global --url/--api-key/
22
+ // --profile). Boolean flags (--public/--link/--invite-only/--list) are
23
+ // registered in index.ts's BOOLEAN_FLAGS so the parser doesn't swallow the
24
+ // next token.
25
+ const VALUE_FLAGS = ["email", "role", "revoke", "mode"];
26
+ const BOOL_FLAGS = ["public", "link", "invite-only", "list"];
27
+ // Map a --mode value (or one of the convenience boolean aliases) to the wire
28
+ // AccessMode. Accepts both hyphenated ("invite-only") and underscore
29
+ // ("invite_only") spellings for --mode.
30
+ function parseAccessMode(value) {
31
+ switch (value) {
32
+ case "invite_only":
33
+ case "invite-only":
34
+ return "invite_only";
35
+ case "link":
36
+ return "link";
37
+ case "public":
38
+ return "public";
39
+ default:
40
+ return null;
41
+ }
42
+ }
43
+ export const shareHelp = `pane share — manage identity sharing on a pane
44
+
45
+ A pane has two layered share mechanisms on top of participant tokens:
46
+ - access mode: governs the pane-id (/p/<pane-id>) path. One of:
47
+ invite-only only invited people (after login) can open it.
48
+ link anyone with the /p URL opens it read-only, no login
49
+ (the default; not discoverable).
50
+ public anyone opens it read-only, no login (may be listed later).
51
+ - invitation: specific humans (by email) get a grant. A 'participant'
52
+ grant can read AND emit page events; a 'viewer' grant is
53
+ read-only. A pending invite binds to the human on their
54
+ first magic-link login.
55
+
56
+ Token (/s/<token>) links are independent of the access mode and keep working
57
+ in every mode until explicitly revoked.
58
+
59
+ Usage:
60
+ pane share <pane-id> --email <addr> [--role participant|viewer]
61
+ pane share <pane-id> --mode <invite-only|link|public>
62
+ pane share <pane-id> --public | --link | --invite-only
63
+ pane share <pane-id> --list
64
+ pane share <pane-id> --revoke <grant-id>
65
+
66
+ Verbs (exactly one per call):
67
+ --email <addr> Invite a human by email (upsert). --role defaults
68
+ to 'participant'. Re-inviting the same address
69
+ updates the role in place. Returns the grant
70
+ { id, human_id, invite_email, role, accepted_at }.
71
+ --mode <mode> Set the /p access mode (invite-only|link|public).
72
+ Returns { pane_id, access_mode }.
73
+ --public Alias for --mode public.
74
+ --link Alias for --mode link.
75
+ --invite-only Alias for --mode invite-only.
76
+ --list Show { pane_id, access_mode, items: [grant...] }.
77
+ --revoke <grant-id> Remove one grant. Idempotent (unknown id still OK).
78
+
79
+ Options:
80
+ --role <participant|viewer> Role for --email (default participant).
81
+ --url <url> Relay base URL (overrides PANE_URL).
82
+ --api-key <key> Agent API key (overrides PANE_API_KEY).
83
+ -h, --help Show this help.
84
+
85
+ Output: stdout is machine-readable JSON.`;
86
+ export async function runShare(args) {
87
+ if (args.bools.has("help")) {
88
+ process.stdout.write(shareHelp + "\n");
89
+ return;
90
+ }
91
+ assertKnownFlags(args, VALUE_FLAGS, BOOL_FLAGS, "pane share");
92
+ // positionals[0] is the verb slot in the dispatcher's view, but `share` is a
93
+ // flat top-level command: positionals[0] is the pane id.
94
+ const paneId = args.positionals[0];
95
+ if (!paneId) {
96
+ fail("missing <pane-id> — usage: pane share <pane-id> --email <addr> | --mode <invite-only|link|public> | --list | --revoke <grant-id>", "invalid_args");
97
+ }
98
+ // Determine which verb was requested; reject ambiguous combinations so the
99
+ // caller's intent is never guessed. The three access-mode aliases
100
+ // (--public / --link / --invite-only) and the explicit --mode all collapse
101
+ // to the single "set access mode" verb.
102
+ const hasEmail = args.flags.has("email");
103
+ const hasMode = args.flags.has("mode");
104
+ const hasPublic = args.bools.has("public");
105
+ const hasLink = args.bools.has("link");
106
+ const hasInviteOnly = args.bools.has("invite-only");
107
+ const hasList = args.bools.has("list");
108
+ const hasRevoke = args.flags.has("revoke");
109
+ const modeAliasCount = (hasMode ? 1 : 0) +
110
+ (hasPublic ? 1 : 0) +
111
+ (hasLink ? 1 : 0) +
112
+ (hasInviteOnly ? 1 : 0);
113
+ const hasModeVerb = modeAliasCount > 0;
114
+ const verbCount = (hasEmail ? 1 : 0) +
115
+ (hasModeVerb ? 1 : 0) +
116
+ (hasList ? 1 : 0) +
117
+ (hasRevoke ? 1 : 0);
118
+ if (verbCount === 0) {
119
+ fail("missing verb — pass exactly one of --email <addr>, --mode <invite-only|link|public> (or --public/--link/--invite-only), --list, --revoke <grant-id>", "invalid_args");
120
+ }
121
+ if (verbCount > 1 || modeAliasCount > 1) {
122
+ fail("ambiguous — pass exactly one of --email, --mode/--public/--link/--invite-only, --list, --revoke", "invalid_args");
123
+ }
124
+ const client = makeClient(args);
125
+ try {
126
+ if (hasList) {
127
+ const res = await client.listGrants(paneId);
128
+ printJson(res);
129
+ return;
130
+ }
131
+ if (hasModeVerb) {
132
+ // Resolve the access mode from --mode or one of the boolean aliases.
133
+ let mode;
134
+ if (hasMode) {
135
+ const raw = args.flags.get("mode");
136
+ if (!raw) {
137
+ fail("missing <mode> — usage: pane share <pane-id> --mode <invite-only|link|public>", "invalid_args");
138
+ }
139
+ mode = parseAccessMode(raw);
140
+ if (!mode) {
141
+ fail(`invalid --mode '${raw}' — expected 'invite-only', 'link', or 'public'`, "invalid_args");
142
+ }
143
+ }
144
+ else if (hasPublic) {
145
+ mode = "public";
146
+ }
147
+ else if (hasLink) {
148
+ mode = "link";
149
+ }
150
+ else {
151
+ mode = "invite_only";
152
+ }
153
+ const res = await client.setPaneVisibility(paneId, mode);
154
+ printJson(res);
155
+ return;
156
+ }
157
+ if (hasRevoke) {
158
+ const grantId = args.flags.get("revoke");
159
+ if (!grantId) {
160
+ fail("missing <grant-id> — usage: pane share <pane-id> --revoke <grant-id>", "invalid_args");
161
+ }
162
+ await client.revokeGrant(paneId, grantId);
163
+ printJson({ pane_id: paneId, grant_id: grantId, revoked: true });
164
+ return;
165
+ }
166
+ // hasEmail
167
+ const email = args.flags.get("email");
168
+ if (!email) {
169
+ fail("missing <addr> — usage: pane share <pane-id> --email <addr>", "invalid_args");
170
+ }
171
+ const role = args.flags.get("role");
172
+ if (role !== undefined && role !== "participant" && role !== "viewer") {
173
+ fail(`invalid --role '${role}' — expected 'participant' or 'viewer'`, "invalid_args");
174
+ }
175
+ const res = await client.createGrant(paneId, {
176
+ email: email,
177
+ ...(role ? { role: role } : {}),
178
+ });
179
+ printJson(res);
180
+ }
181
+ catch (e) {
182
+ failFromError(e);
183
+ }
184
+ }
@@ -0,0 +1,67 @@
1
+ // `pane upgrade <pane-id>` — re-pin a live pane to another version of its
2
+ // template, swapping design + content in place (#267).
3
+ import { assertKnownFlags } from "../argv.js";
4
+ import { makeClient } from "../config.js";
5
+ import { printJson, fail, failFromError } from "../output.js";
6
+ const KNOWN_FLAGS = ["template-version"];
7
+ const KNOWN_BOOLS = ["force"];
8
+ export const upgradeHelp = `pane upgrade — re-pin a live pane to another template version
9
+
10
+ Usage:
11
+ pane upgrade <pane-id> [--template-version <n>] [--force]
12
+
13
+ Re-points an existing, live pane at a different version of the SAME template
14
+ (POST /v1/panes/:id/upgrade). This swaps the pane's HTML (design) and its
15
+ event/input/record schemas (content contract) in place — the human keeps the
16
+ same URL, no new pane is created. Use it after appending a new template
17
+ version with 'pane template version <id|slug> --template ...'.
18
+
19
+ Events already on disk are never rewritten — each keeps the template version
20
+ it was authored under, so the prior history still renders.
21
+
22
+ By default the relay runs a strict schema-compat gate: if the target version's
23
+ schema narrows the pane's current one (a removed collection, a newly-required
24
+ field, a tightened type), the upgrade is refused with a
25
+ 'schema_incompatible_upgrade' error whose details.breaks lists what would
26
+ break. Pass --force to apply the upgrade anyway, accepting that events written
27
+ under the old schema may no longer validate.
28
+
29
+ Note: the re-pin takes effect on the relay immediately and emits a
30
+ 'system.template.updated' event, but an already-open pane tab is not
31
+ force-reloaded in v1 — the new version renders the next time the URL is loaded.
32
+
33
+ Options:
34
+ --template-version <n> Target version number. Defaults to the template's
35
+ latest version.
36
+ --force Override the strict schema-compat gate (compat=force).
37
+ --url <url> Relay base URL (overrides PANE_URL).
38
+ --api-key <key> Agent API key (overrides PANE_API_KEY).
39
+ -h, --help Show this help.
40
+
41
+ Output (stdout, JSON):
42
+ { pane_id, template_version_id, template_version, upgraded, breaks, compat }`;
43
+ export async function runUpgrade(args) {
44
+ assertKnownFlags(args, KNOWN_FLAGS, KNOWN_BOOLS, "pane upgrade");
45
+ const paneId = args.positionals[0];
46
+ if (!paneId)
47
+ fail("missing <pane-id>", "invalid_args");
48
+ const opts = {};
49
+ const versionRaw = args.flags.get("template-version");
50
+ if (versionRaw !== undefined) {
51
+ const version = Number(versionRaw);
52
+ if (!Number.isInteger(version) || version < 1) {
53
+ fail("--template-version must be a positive integer", "invalid_args");
54
+ }
55
+ opts.template_version = version;
56
+ }
57
+ if (args.bools.has("force"))
58
+ opts.compat = "force";
59
+ const client = makeClient(args);
60
+ try {
61
+ const res = await client.upgradePane(paneId, opts);
62
+ printJson(res);
63
+ }
64
+ catch (e) {
65
+ failFromError(e);
66
+ }
67
+ }
package/dist/index.js CHANGED
@@ -31,7 +31,9 @@ import { runState, stateHelp } from "./commands/state.js";
31
31
  import { runSend, sendHelp } from "./commands/send.js";
32
32
  import { runWatch, watchHelp } from "./commands/watch.js";
33
33
  import { runDelete, deleteHelp } from "./commands/delete.js";
34
+ import { runUpgrade, upgradeHelp } from "./commands/upgrade.js";
34
35
  import { runParticipant, participantHelp } from "./commands/participant.js";
36
+ import { runShare, shareHelp } from "./commands/share.js";
35
37
  import { runTemplate, artifactHelp } from "./commands/template.js";
36
38
  import { runAgent, agentHelp } from "./commands/agent.js";
37
39
  import { runKey, keyHelp } from "./commands/key.js";
@@ -44,6 +46,7 @@ import { runRecords, recordsHelp } from "./commands/records.js";
44
46
  import { runTemplateRecords, templateRecordsHelp, } from "./commands/template-records.js";
45
47
  import { runQuery, queryHelp } from "./commands/query.js";
46
48
  import { runTrash, trashHelp } from "./commands/trash.js";
49
+ import { runDemo, demoHelp } from "./commands/demo.js";
47
50
  import { VERSION } from "./version.js";
48
51
  import { PaneApiError } from "@paneui/core";
49
52
  import { failUpgradeRequired } from "./output.js";
@@ -60,10 +63,21 @@ Pane commands (operate on the core noun — a live UI channel):
60
63
  send <id> Emit an agent event into a pane.
61
64
  watch <id> Stream a pane's events as JSON-lines on stdout.
62
65
  delete <id> Close/delete a pane (DELETE /v1/panes/:id).
66
+ upgrade <id> Re-pin a live pane to another version of its template —
67
+ swap design + content in place, same URL (--template-version
68
+ <n>, --force to override the schema-compat gate).
63
69
  participant Manage participant URLs on an existing pane
64
70
  <list|new|revoke> (list | mint a fresh URL | revoke one URL).
71
+ share <id> Share a pane by identity: invite humans by email
72
+ (--email, with --role participant|viewer), set the /p
73
+ access mode (--mode invite-only|link|public, or the
74
+ aliases --public/--link/--invite-only), list grants
75
+ (--list), or revoke one (--revoke <grant-id>).
65
76
 
66
77
  Other noun groups:
78
+ demo Take the 60-second guided tour — creates a tutorial pane,
79
+ opens it, and runs a live agent loop in your terminal.
80
+ Doubles as an end-to-end smoke test of your install.
67
81
  template Reusable, versioned UI templates
68
82
  (create | version | update | search | list | show | delete).
69
83
  template-records Owner-curated content scoped to a Template head
@@ -127,6 +141,18 @@ const BOOLEAN_FLAGS = new Set([
127
141
  "print-key",
128
142
  "yes",
129
143
  "plain",
144
+ // `pane demo --no-open`: skip the browser launch. Stored as the literal
145
+ // `no-open` boolean (the parser does not auto-negate `--no-` prefixes).
146
+ "no-open",
147
+ // `pane share` access-mode aliases + list verb — registered here so the
148
+ // parser treats them as flags, not value-flags that would swallow the next
149
+ // token. (The full --mode <value> is a value-flag, handled in share.ts.)
150
+ "public",
151
+ "link",
152
+ "invite-only",
153
+ "list",
154
+ // `pane upgrade --force`: override the strict schema-compat gate.
155
+ "force",
130
156
  ]);
131
157
  async function main() {
132
158
  const rawArgv = process.argv.slice(2);
@@ -162,7 +188,9 @@ async function main() {
162
188
  send: sendHelp,
163
189
  watch: watchHelp,
164
190
  delete: deleteHelp,
191
+ upgrade: upgradeHelp,
165
192
  participant: participantHelp,
193
+ share: shareHelp,
166
194
  // Other noun groups.
167
195
  template: artifactHelp,
168
196
  key: keyHelp,
@@ -176,6 +204,7 @@ async function main() {
176
204
  "template-records": templateRecordsHelp,
177
205
  query: queryHelp,
178
206
  trash: trashHelp,
207
+ demo: demoHelp,
179
208
  };
180
209
  if (!(noun in helps)) {
181
210
  process.stderr.write(JSON.stringify({
@@ -214,9 +243,15 @@ async function main() {
214
243
  case "delete":
215
244
  await runDelete(args);
216
245
  break;
246
+ case "upgrade":
247
+ await runUpgrade(args);
248
+ break;
217
249
  case "participant":
218
250
  await runParticipant(args);
219
251
  break;
252
+ case "share":
253
+ await runShare(args);
254
+ break;
220
255
  // Other noun groups.
221
256
  case "template":
222
257
  await runTemplate(args);
@@ -254,6 +289,9 @@ async function main() {
254
289
  case "trash":
255
290
  await runTrash(args);
256
291
  break;
292
+ case "demo":
293
+ await runDemo(args);
294
+ break;
257
295
  }
258
296
  }
259
297
  main().catch((err) => {
package/dist/version.js CHANGED
@@ -8,4 +8,4 @@
8
8
  // Keep this in lockstep with packages/cli/package.json's `version` field;
9
9
  // they're consulted in different places (here for the runtime header,
10
10
  // package.json for npm publish + dependency resolution).
11
- export const VERSION = "0.0.11";
11
+ export const VERSION = "0.0.13";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@paneui/cli",
3
- "version": "0.0.11",
3
+ "version": "0.0.13",
4
4
  "description": "Command-line client for the Pane relay: create panes, inspect state, send and watch events.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -41,13 +41,13 @@
41
41
  "test:unit": "vitest run"
42
42
  },
43
43
  "dependencies": {
44
- "@paneui/core": "^0.0.11",
44
+ "@paneui/core": "^0.0.13",
45
45
  "qrcode-terminal": "^0.12.0"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@types/node": "^25.9.1",
49
49
  "@types/qrcode-terminal": "^0.12.2",
50
50
  "typescript": "^6.0.3",
51
- "vitest": "^4.1.6"
51
+ "vitest": "^4.1.8"
52
52
  }
53
53
  }