@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.
- package/dist/commands/demo-artifact.js +609 -0
- package/dist/commands/demo.js +305 -0
- package/dist/commands/share.js +184 -0
- package/dist/commands/upgrade.js +67 -0
- package/dist/index.js +38 -0
- package/dist/version.js +1 -1
- package/package.json +3 -3
|
@@ -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 →</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">⚙</span>your terminal<small>the agent</small></div>
|
|
324
|
+
<span class="wire">⇄</span>
|
|
325
|
+
<div class="node"><span class="ic">☁</span>relay<small>routes events</small></div>
|
|
326
|
+
<span class="wire">⇄</span>
|
|
327
|
+
<div class="node"><span class="ic">▦</span>this page<small>the pane</small></div>
|
|
328
|
+
</div>
|
|
329
|
+
<p>
|
|
330
|
+
You ran <code>pane demo</code> — 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">✓ 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 →</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 →</button>
|
|
371
|
+
<div id="echo" hidden>
|
|
372
|
+
<div class="badge" style="margin-top:16px">✓ 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 <id></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 <id> --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, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
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
|
+
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.
|
|
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.
|
|
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.
|
|
51
|
+
"vitest": "^4.1.8"
|
|
52
52
|
}
|
|
53
53
|
}
|