glimpse-cli 0.0.1 → 0.2.0
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/cli.mjs +370 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/daemon-main.mjs +295 -0
- package/dist/daemon-main.mjs.map +1 -0
- package/dist/glimpse-adapter-COgj6E-W.mjs +34 -0
- package/dist/glimpse-adapter-COgj6E-W.mjs.map +1 -0
- package/docs/PRD.md +356 -0
- package/docs/npm-staged-trusted-publishing.md +44 -0
- package/examples/01_prompt.sh +60 -0
- package/examples/02_counter.sh +88 -0
- package/examples/02b_counter_w_state.sh +105 -0
- package/examples/03_watch.sh +75 -0
- package/package.json +43 -7
- package/README.md +0 -45
package/docs/PRD.md
ADDED
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
# glimpse-cli PRD
|
|
2
|
+
|
|
3
|
+
## Purpose
|
|
4
|
+
|
|
5
|
+
`glimpse-cli` is a command-line wrapper around the Glimpse micro-UI runtime. It lets scripts and agents open native UI windows, collect user input, and update existing windows over time without embedding Glimpse runtime code directly in every script.
|
|
6
|
+
|
|
7
|
+
macOS is the first supported implementation target, but the command and data model should remain platform-neutral.
|
|
8
|
+
|
|
9
|
+
## Implementation Stack
|
|
10
|
+
|
|
11
|
+
v1 should use a minimal dependency policy while avoiding custom implementations for well-solved infrastructure.
|
|
12
|
+
|
|
13
|
+
- Runtime/tooling: Bun with TypeScript.
|
|
14
|
+
- Package identity: publish as `glimpse-cli` with binary name `glimpse`.
|
|
15
|
+
- Binary goal: keep the code compatible with `bun build --compile` so `glimpse` can become a standalone binary.
|
|
16
|
+
- CLI parser: `commander`.
|
|
17
|
+
- JSON validation: `valibot` for command inputs, IPC payloads, command results, and persisted daemon state.
|
|
18
|
+
- Logging: `evlog` for structured daemon/CLI diagnostics.
|
|
19
|
+
- UI runtime: `glimpseui` from npm. Direct Glimpse imports should be isolated behind an internal adapter module so runtime package/API changes do not leak into command logic.
|
|
20
|
+
- File watching: start with built-in file watching for single-file HTML watch mode. If reliability issues appear or directory watching is needed, prefer `@parcel/watcher` over `chokidar`. Avoid `chokidar` by default due to prior memory-leak concerns and because v1 does not need glob-heavy watcher features. Before adopting `@parcel/watcher`, verify compatibility with `bun build --compile` because it uses native components.
|
|
21
|
+
- IPC: built-in Unix domain sockets via `node:net` on macOS, using newline-delimited JSON for request/response framing. IPC methods mirror CLI command names to keep command parsing and daemon dispatch simple.
|
|
22
|
+
- IDs: built-in `node:crypto` UUIDs.
|
|
23
|
+
- URL/DNS checks: built-in `node:url`, `node:dns/promises`, and `node:net`.
|
|
24
|
+
- Duration parsing: small local helper for `ms`, `s`, and `m` suffixes.
|
|
25
|
+
|
|
26
|
+
Avoid additional dependencies in v1 unless they remove substantial complexity or address a proven reliability issue.
|
|
27
|
+
|
|
28
|
+
Recommended source layout:
|
|
29
|
+
|
|
30
|
+
```text
|
|
31
|
+
src/
|
|
32
|
+
cli.ts
|
|
33
|
+
daemon-main.ts
|
|
34
|
+
daemon/
|
|
35
|
+
daemon.ts
|
|
36
|
+
window-registry.ts
|
|
37
|
+
event-queue.ts
|
|
38
|
+
commands/
|
|
39
|
+
open.ts
|
|
40
|
+
prompt.ts
|
|
41
|
+
ipc/
|
|
42
|
+
client.ts
|
|
43
|
+
server.ts
|
|
44
|
+
protocol.ts
|
|
45
|
+
platform/
|
|
46
|
+
paths.ts
|
|
47
|
+
url-policy.ts
|
|
48
|
+
runtime/
|
|
49
|
+
glimpse-adapter.ts
|
|
50
|
+
schemas/
|
|
51
|
+
command-results.ts
|
|
52
|
+
ipc.ts
|
|
53
|
+
utils/
|
|
54
|
+
duration.ts
|
|
55
|
+
json.ts
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The layout may be collapsed during early implementation if a separate file would add ceremony without clarity.
|
|
59
|
+
|
|
60
|
+
Testing uses Bun's built-in test runner (`bun test`). Prioritize unit tests for duration parsing, JSON/Valibot schemas, URL trust policy, CSP generation, event queue semantics, IPC protocol framing, daemon startup path/lock behavior, and command parser smoke tests. Native window tests can start as manual or minimal smoke tests.
|
|
61
|
+
|
|
62
|
+
## v1 Commands
|
|
63
|
+
|
|
64
|
+
- `glimpse prompt <html-source>`: one-shot foreground interaction.
|
|
65
|
+
- `glimpse open <html-source>`: open persistent daemon-owned Window.
|
|
66
|
+
- `glimpse open --url <url>`: open URL Source.
|
|
67
|
+
- `glimpse set-html -w <ref> <html-source>`: replace Window HTML.
|
|
68
|
+
- `glimpse navigate -w <ref> --url <url>`: navigate Window to URL Source.
|
|
69
|
+
- `glimpse send -w <ref> --type <type> (--data <json>|--data-file <path|->|--text <text>)`: send structured Page Message.
|
|
70
|
+
- `glimpse eval -w <ref> <js>`: execute raw JavaScript and return JSON-serializable result.
|
|
71
|
+
- `glimpse wait -w <ref> [--type <type>] [--timeout <duration>]`: block until one matching event and consume it.
|
|
72
|
+
- `glimpse read -w <ref> [--type <type>]`: non-blocking event consume.
|
|
73
|
+
- `glimpse events -w <ref>` / `glimpse peek -w <ref>`: inspect queued unconsumed events without consuming.
|
|
74
|
+
- `glimpse close -w <ref> [--force]`: close one Window.
|
|
75
|
+
- `glimpse close --all [--force]`: close all Windows.
|
|
76
|
+
- `glimpse list [--include-closed]`: list open Windows, optionally recent closed tombstones.
|
|
77
|
+
|
|
78
|
+
No general v1 command mutates Window options after opening.
|
|
79
|
+
|
|
80
|
+
## HTML and URL Sources
|
|
81
|
+
|
|
82
|
+
HTML Sources:
|
|
83
|
+
|
|
84
|
+
- positional file path
|
|
85
|
+
- `-` for stdin
|
|
86
|
+
- `--html <literal>`
|
|
87
|
+
|
|
88
|
+
HTML is read once by default. `--watch` makes the daemon watch file-based HTML and fully reload Window HTML on changes. Watchers are daemon-owned and live only as long as their Window.
|
|
89
|
+
|
|
90
|
+
URL Sources require explicit `--url`; positional URLs are not supported in v1.
|
|
91
|
+
|
|
92
|
+
Existing Windows move to URL Sources via `navigate`, not `set-html`.
|
|
93
|
+
|
|
94
|
+
## Persistent Windows and Daemon
|
|
95
|
+
|
|
96
|
+
Persistent Windows are owned by an Ephemeral Daemon:
|
|
97
|
+
|
|
98
|
+
- starts on first persistent `open`
|
|
99
|
+
- exits after last Window closes
|
|
100
|
+
- may remain alive for up to 30 seconds to serve final close events
|
|
101
|
+
- macOS IPC uses local same-user Unix domain socket
|
|
102
|
+
- daemon discovery uses predictable per-user socket path plus state files under `$TMPDIR/glimpse-cli-$UID/` (`daemon.sock`, `daemon.json`) and an atomic startup lock directory named `daemon.lock`
|
|
103
|
+
- daemon startup uses detached child process launch followed by socket `ping` polling until ready or a 5 second startup timeout
|
|
104
|
+
- concurrent CLI connections are allowed
|
|
105
|
+
- mutations and event queue consumption are serialized per Window
|
|
106
|
+
|
|
107
|
+
`prompt` is standalone foreground by default and does not require the daemon.
|
|
108
|
+
|
|
109
|
+
## Window References
|
|
110
|
+
|
|
111
|
+
Every persistent Window gets an opaque `windowId`.
|
|
112
|
+
|
|
113
|
+
Users may optionally assign a unique active Window Name:
|
|
114
|
+
|
|
115
|
+
```fish
|
|
116
|
+
glimpse open ./status.html --name build
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
Commands address Windows with only:
|
|
120
|
+
|
|
121
|
+
```fish
|
|
122
|
+
-w, --window <ref>
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`<ref>` may be a Window ID or Window Name.
|
|
126
|
+
|
|
127
|
+
Window Names are case-sensitive, script-safe identifiers and must not use reserved Window ID prefixes. Closed Window names can be reused immediately; close-event tombstones remain addressable only by old Window ID.
|
|
128
|
+
|
|
129
|
+
`open --name <name>` fails if the name is in use unless `--replace` is passed. Replacement closes the existing Window, discards its event queue, stops watchers, creates a fresh Window with a new ID, and reserves the name during the operation.
|
|
130
|
+
|
|
131
|
+
## Command Results
|
|
132
|
+
|
|
133
|
+
Commands default to JSON output.
|
|
134
|
+
|
|
135
|
+
Success:
|
|
136
|
+
|
|
137
|
+
```json
|
|
138
|
+
{ "ok": true }
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Failure:
|
|
142
|
+
|
|
143
|
+
```json
|
|
144
|
+
{
|
|
145
|
+
"ok": false,
|
|
146
|
+
"error": {
|
|
147
|
+
"code": "window_not_found",
|
|
148
|
+
"message": "Window build is not open."
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
Prompt cancellation/window close is not an error. Explicit in-page cancellation uses a reserved Prompt Result:
|
|
154
|
+
|
|
155
|
+
```json
|
|
156
|
+
{ "ok": true, "result": { "type": "prompt.canceled" } }
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Prompt Window closure without a page-submitted value uses a synthesized Prompt Result:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{ "ok": true, "result": { "type": "window.closed" } }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Timeouts are technical failures.
|
|
166
|
+
|
|
167
|
+
Exit codes:
|
|
168
|
+
|
|
169
|
+
- `0`: success
|
|
170
|
+
- `1`: runtime or command failure with JSON error
|
|
171
|
+
- `2`: usage or validation error
|
|
172
|
+
|
|
173
|
+
## Prompt Semantics
|
|
174
|
+
|
|
175
|
+
`prompt` opens a temporary Window, waits for first page-to-CLI message, prints raw submitted value as `result`, closes, and exits. Explicit in-page cancel controls should submit `{ "type": "prompt.canceled" }`. Prompt Window closure without a page-submitted value returns `{ "type": "window.closed" }`. Prompt content should not submit raw `null`; in v1, raw `null` is treated as Window closure.
|
|
176
|
+
|
|
177
|
+
`prompt --timeout <duration>` fails with `timeout` if no result arrives.
|
|
178
|
+
|
|
179
|
+
Remote URL prompts require both `--allow-remote` and `--allow-bridge` because prompts need the page bridge.
|
|
180
|
+
|
|
181
|
+
## Window Events
|
|
182
|
+
|
|
183
|
+
Persistent Window page-to-CLI messages become queued Window Events.
|
|
184
|
+
|
|
185
|
+
Event commands in v1 require `-w`.
|
|
186
|
+
|
|
187
|
+
Event behavior:
|
|
188
|
+
|
|
189
|
+
- `wait` consumes one matching event.
|
|
190
|
+
- `read` consumes one matching event if present.
|
|
191
|
+
- `events`/`peek` inspect currently queued unconsumed events only.
|
|
192
|
+
- unfiltered `wait`/`read` return oldest queued event, including system events.
|
|
193
|
+
- filtered `wait`/`read` consume only first matching event and preserve non-matching events.
|
|
194
|
+
- `read` with no matching event returns `{ "ok": true, "event": null }`.
|
|
195
|
+
- `wait` blocks indefinitely unless `--timeout` is provided.
|
|
196
|
+
|
|
197
|
+
Queue size:
|
|
198
|
+
|
|
199
|
+
- default 1000 events per Window
|
|
200
|
+
- configurable up to 20000 events
|
|
201
|
+
- overflow drops oldest droppable event and reports `glimpse.error`
|
|
202
|
+
|
|
203
|
+
Untyped page payloads are wrapped as event type `json`.
|
|
204
|
+
|
|
205
|
+
System prefixes are reserved; page attempts to use them produce `glimpse.error`.
|
|
206
|
+
|
|
207
|
+
System events include:
|
|
208
|
+
|
|
209
|
+
- `window.ready`
|
|
210
|
+
- `window.closed`
|
|
211
|
+
- `window.navigated`
|
|
212
|
+
- `html.reloaded`
|
|
213
|
+
- `glimpse.error`
|
|
214
|
+
|
|
215
|
+
`window.closed` remains readable for up to 30 seconds after closure.
|
|
216
|
+
|
|
217
|
+
## Page Messages and Eval
|
|
218
|
+
|
|
219
|
+
`send` sends structured JSON Page Messages to a Window bridge.
|
|
220
|
+
|
|
221
|
+
Rules:
|
|
222
|
+
|
|
223
|
+
- explicit `--type` required
|
|
224
|
+
- `--data` is JSON-only
|
|
225
|
+
- `--text` sends string data
|
|
226
|
+
- `--data-file <path|->` supports large/generated JSON
|
|
227
|
+
- success means delivered to the webview bridge, not handled by app code
|
|
228
|
+
- v1 has listener-style delivery only, no built-in request/response protocol
|
|
229
|
+
|
|
230
|
+
`eval` executes raw JS. It is enabled by default for local CLI use because the command name is explicit. It returns JSON-serializable result values.
|
|
231
|
+
|
|
232
|
+
Bridge-dependent commands fail clearly against Windows without bridge.
|
|
233
|
+
|
|
234
|
+
## Window Options
|
|
235
|
+
|
|
236
|
+
CLI stays close to the Glimpse JavaScript SDK.
|
|
237
|
+
|
|
238
|
+
- JS SDK option names map to kebab-case CLI flags.
|
|
239
|
+
- SDK options can also be supplied as JSON object using SDK camelCase keys.
|
|
240
|
+
- explicit CLI flags override JSON options, including nested option flags.
|
|
241
|
+
- options files are file-based only in v1; they do not read from stdin.
|
|
242
|
+
|
|
243
|
+
Example:
|
|
244
|
+
|
|
245
|
+
```fish
|
|
246
|
+
glimpse open ./ui.html \
|
|
247
|
+
--click-through \
|
|
248
|
+
--follow-cursor \
|
|
249
|
+
--follow-mode spring \
|
|
250
|
+
--cursor-offset 20,-20 \
|
|
251
|
+
--options-json '{"width":400,"height":300}'
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
## URL Security Policy
|
|
255
|
+
|
|
256
|
+
Trusted local URL Sources are allowed by default with bridge enabled.
|
|
257
|
+
|
|
258
|
+
Trusted local URLs:
|
|
259
|
+
|
|
260
|
+
- loopback hosts
|
|
261
|
+
- `file:` URLs
|
|
262
|
+
- `.localhost` hostnames only when they resolve to loopback addresses
|
|
263
|
+
|
|
264
|
+
Trusted URL algorithm:
|
|
265
|
+
|
|
266
|
+
1. Parse URL.
|
|
267
|
+
2. If protocol is `file:`, trusted.
|
|
268
|
+
3. If protocol is not `http:` or `https:`, untrusted.
|
|
269
|
+
4. If hostname is `localhost`, trusted.
|
|
270
|
+
5. If hostname is loopback IP (`127.0.0.0/8` or `::1`), trusted.
|
|
271
|
+
6. If hostname ends with `.localhost`, resolve DNS once.
|
|
272
|
+
7. `.localhost` is trusted only if every resolved address is loopback.
|
|
273
|
+
8. Everything else is remote/untrusted.
|
|
274
|
+
|
|
275
|
+
Trust is checked before loading/navigation. Redirects or in-window navigations to untrusted URLs are blocked unless remote loading is explicitly allowed.
|
|
276
|
+
|
|
277
|
+
Remote URLs:
|
|
278
|
+
|
|
279
|
+
- blocked by default
|
|
280
|
+
- `--allow-remote` permits loading remote content in read-only/no-bridge mode
|
|
281
|
+
- `--allow-bridge` additionally permits bridge injection
|
|
282
|
+
- `--allow-bridge` alone does not permit remote loading
|
|
283
|
+
|
|
284
|
+
Remote bridged Windows allow `send` and `eval` only after explicit remote loading and bridge allowance.
|
|
285
|
+
|
|
286
|
+
Links opened externally are handled outside the Window.
|
|
287
|
+
|
|
288
|
+
## HTML CSP Policy
|
|
289
|
+
|
|
290
|
+
Local and inline HTML Sources receive a default Content Security Policy that blocks remote subresources unless relaxed.
|
|
291
|
+
|
|
292
|
+
Default CSP intent:
|
|
293
|
+
|
|
294
|
+
- allow local/inline micro-UI needs
|
|
295
|
+
- allow loopback resources
|
|
296
|
+
- allow literal `localhost`
|
|
297
|
+
- allow broad `.localhost` resources for portless/local-dev workflows
|
|
298
|
+
- block arbitrary remote subresources
|
|
299
|
+
|
|
300
|
+
Initial v1 default CSP:
|
|
301
|
+
|
|
302
|
+
```text
|
|
303
|
+
default-src 'self' data: blob:;
|
|
304
|
+
img-src 'self' data: blob: http://localhost:* https://localhost:* http://127.0.0.1:* https://127.0.0.1:* http://*.localhost:* https://*.localhost:*;
|
|
305
|
+
style-src 'self' 'unsafe-inline' data:;
|
|
306
|
+
script-src 'self' 'unsafe-inline' blob:;
|
|
307
|
+
connect-src 'self' http://localhost:* https://localhost:* ws://localhost:* wss://localhost:* http://127.0.0.1:* https://127.0.0.1:* ws://127.0.0.1:* wss://127.0.0.1:* http://*.localhost:* https://*.localhost:* ws://*.localhost:* wss://*.localhost:*;
|
|
308
|
+
font-src 'self' data:;
|
|
309
|
+
media-src 'self' data: blob:;
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
The implementation may adjust this exact string if WKWebView behavior requires changes while preserving the stated policy intent.
|
|
313
|
+
|
|
314
|
+
Escape hatches:
|
|
315
|
+
|
|
316
|
+
- `--allow-remote-resources`
|
|
317
|
+
- explicit `--csp <policy>`
|
|
318
|
+
|
|
319
|
+
No `--no-csp` in v1.
|
|
320
|
+
|
|
321
|
+
CSP is part of Window creation policy and persists across `set-html` and watch reloads.
|
|
322
|
+
|
|
323
|
+
`navigate` uses URL Source policy, not HTML Source CSP.
|
|
324
|
+
|
|
325
|
+
## Listing
|
|
326
|
+
|
|
327
|
+
`list` succeeds even when daemon is not running:
|
|
328
|
+
|
|
329
|
+
```json
|
|
330
|
+
{
|
|
331
|
+
"ok": true,
|
|
332
|
+
"daemon": { "running": false },
|
|
333
|
+
"windows": []
|
|
334
|
+
}
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
Open Window entries should include state, event queue size, source metadata, and bridge/security metadata.
|
|
338
|
+
|
|
339
|
+
`--include-closed` includes recently closed entries with expiry time.
|
|
340
|
+
|
|
341
|
+
## Closure
|
|
342
|
+
|
|
343
|
+
`close -w <ref>` fails on missing/stale Window by default.
|
|
344
|
+
|
|
345
|
+
Normal closure emits `window.closed` and keeps tombstone for up to 30 seconds.
|
|
346
|
+
|
|
347
|
+
Forced closure discards event queues and tombstones, stops watchers, and lets daemon exit immediately when no Windows remain.
|
|
348
|
+
|
|
349
|
+
`close --all` supports normal and forced closure.
|
|
350
|
+
|
|
351
|
+
## Deferred Ideas
|
|
352
|
+
|
|
353
|
+
Tracked as beans:
|
|
354
|
+
|
|
355
|
+
- `glimpse-cli-pj3z`: Evaluate seamless HMR for watched Glimpse windows.
|
|
356
|
+
- `glimpse-cli-zbfv`: Evaluate global event awaiting across windows.
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# npm staged trusted publishing
|
|
2
|
+
|
|
3
|
+
This package uses [`danielroe/uppt`](https://github.com/danielroe/uppt) for npm Trusted Publishing plus staged publishing.
|
|
4
|
+
|
|
5
|
+
## One-time setup
|
|
6
|
+
|
|
7
|
+
`glimpse-cli` must exist on npm before its Trusted Publisher can be configured. If it is not published yet, create the placeholder package:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx --yes setup-npm-trusted-publish glimpse-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Then open:
|
|
14
|
+
|
|
15
|
+
```text
|
|
16
|
+
https://www.npmjs.com/package/glimpse-cli/access
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
Configure the Trusted Publisher:
|
|
20
|
+
|
|
21
|
+
- Provider: GitHub Actions
|
|
22
|
+
- Repository: `bjesuiter/glimpse-cli`
|
|
23
|
+
- Workflow filename: `release.yml`
|
|
24
|
+
- Environment name: `npm`
|
|
25
|
+
- Allowed action: `npm stage publish`
|
|
26
|
+
|
|
27
|
+
Also configure GitHub:
|
|
28
|
+
|
|
29
|
+
1. Create a GitHub environment named `npm`.
|
|
30
|
+
2. In repo Settings → Actions → General → Workflow permissions, enable “Allow GitHub Actions to create and approve pull requests”.
|
|
31
|
+
|
|
32
|
+
Prefer stage-only npm access so CI can submit a release candidate, but a maintainer still approves the final publication with 2FA.
|
|
33
|
+
|
|
34
|
+
## Release flow
|
|
35
|
+
|
|
36
|
+
1. Use conventional commits on `main`.
|
|
37
|
+
2. On push to `main`, `uppt/pr` opens or updates a draft `release/vX.Y.Z` PR.
|
|
38
|
+
3. Merge the release PR.
|
|
39
|
+
4. `uppt/release` creates the tag and GitHub Release, then dispatches `release.yml` on the tag.
|
|
40
|
+
5. `uppt/pack` creates the tarball from a Bun-checked build artifact.
|
|
41
|
+
6. `uppt/publish` runs `npm stage publish` with OIDC.
|
|
42
|
+
7. Approve or reject the staged package on npmjs.com or with an interactive npm CLI session.
|
|
43
|
+
|
|
44
|
+
No `NPM_TOKEN` secret is required. The publish job uses GitHub Actions OIDC via `permissions.id-token: write`.
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Self-contained glimpse-cli example: one-shot prompt window.
|
|
5
|
+
# Run from the repo root with: ./examples/01_prompt.sh
|
|
6
|
+
|
|
7
|
+
GLIMPSE="${GLIMPSE:-bun src/cli.ts}"
|
|
8
|
+
|
|
9
|
+
HTML=$(cat <<'HTML'
|
|
10
|
+
<!doctype html>
|
|
11
|
+
<html lang="en">
|
|
12
|
+
<head>
|
|
13
|
+
<meta charset="utf-8" />
|
|
14
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
15
|
+
<title>Glimpse Prompt</title>
|
|
16
|
+
<style>
|
|
17
|
+
:root { color-scheme: dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
18
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: linear-gradient(135deg, #111827, #312e81); color: #f8fafc; }
|
|
19
|
+
form { width: min(420px, calc(100vw - 32px)); padding: 28px; border: 1px solid rgba(199, 210, 254, .35); border-radius: 24px; background: rgba(15, 23, 42, .82); box-shadow: 0 24px 80px rgba(0, 0, 0, .45); }
|
|
20
|
+
h1 { margin: 0 0 8px; font-size: 22px; }
|
|
21
|
+
p { margin: 0 0 18px; color: #cbd5e1; line-height: 1.45; }
|
|
22
|
+
label { display: block; margin-bottom: 8px; color: #c4b5fd; font-size: 13px; font-weight: 700; }
|
|
23
|
+
input { box-sizing: border-box; width: 100%; border: 1px solid rgba(199, 210, 254, .35); border-radius: 14px; padding: 13px 14px; color: #f8fafc; background: rgba(2, 6, 23, .65); font: inherit; outline: none; }
|
|
24
|
+
input:focus { border-color: #818cf8; box-shadow: 0 0 0 4px rgba(129, 140, 248, .18); }
|
|
25
|
+
.actions { display: flex; gap: 10px; margin-top: 16px; }
|
|
26
|
+
button { flex: 1; border: 0; border-radius: 14px; padding: 13px 14px; color: #f8fafc; background: #4f46e5; font-weight: 800; cursor: pointer; }
|
|
27
|
+
button.secondary { background: rgba(100, 116, 139, .55); }
|
|
28
|
+
button:hover { filter: brightness(1.12); }
|
|
29
|
+
small { display: block; margin-top: 14px; color: #94a3b8; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<form id="prompt-form">
|
|
34
|
+
<h1>Quick prompt</h1>
|
|
35
|
+
<p>Submit a short value back to the shell. Cancel and window close return explicit result objects.</p>
|
|
36
|
+
<label for="answer">What should Glimpse say?</label>
|
|
37
|
+
<input id="answer" name="answer" value="Hello from Glimpse" autofocus />
|
|
38
|
+
<div class="actions">
|
|
39
|
+
<button type="button" class="secondary" id="cancel">Cancel</button>
|
|
40
|
+
<button type="submit">Submit</button>
|
|
41
|
+
</div>
|
|
42
|
+
<small>Try editing the text, then press Enter.</small>
|
|
43
|
+
</form>
|
|
44
|
+
<script>
|
|
45
|
+
const form = document.querySelector('#prompt-form');
|
|
46
|
+
const answer = document.querySelector('#answer');
|
|
47
|
+
form.addEventListener('submit', event => {
|
|
48
|
+
event.preventDefault();
|
|
49
|
+
window.glimpse?.send?.({ answer: answer.value, submittedAt: new Date().toISOString() });
|
|
50
|
+
});
|
|
51
|
+
document.querySelector('#cancel').addEventListener('click', () => window.glimpse?.send?.({ type: 'prompt.canceled' }));
|
|
52
|
+
answer.select();
|
|
53
|
+
</script>
|
|
54
|
+
</body>
|
|
55
|
+
</html>
|
|
56
|
+
HTML
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
echo "Opening prompt window..."
|
|
60
|
+
$GLIMPSE prompt --width 540 --height 390 --title "Glimpse Prompt" --html "$HTML"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Self-contained glimpse-cli example: an interactive counter window.
|
|
5
|
+
# Uses one discrete event per button click.
|
|
6
|
+
# Run from the repo root with: ./examples/02_counter.sh
|
|
7
|
+
|
|
8
|
+
GLIMPSE="${GLIMPSE:-bun src/cli.ts}"
|
|
9
|
+
WINDOW_NAME="glimpse-counter-example"
|
|
10
|
+
|
|
11
|
+
HTML=$(cat <<'HTML'
|
|
12
|
+
<!doctype html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8" />
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
17
|
+
<title>Glimpse Counter</title>
|
|
18
|
+
<style>
|
|
19
|
+
:root { color-scheme: dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
20
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
21
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; overflow-x: hidden; background: radial-gradient(circle at top, #334155, #020617 65%); color: #f8fafc; }
|
|
22
|
+
main { width: min(360px, calc(100vw - 32px)); padding: 28px; border: 1px solid rgba(148, 163, 184, .35); border-radius: 24px; background: rgba(15, 23, 42, .78); box-shadow: 0 24px 80px rgba(0, 0, 0, .45); text-align: center; }
|
|
23
|
+
h1 { margin: 0 0 12px; font-size: 18px; font-weight: 650; color: #cbd5e1; }
|
|
24
|
+
output { display: block; margin: 18px 0 24px; font-size: 72px; font-weight: 800; line-height: 1; letter-spacing: -0.06em; }
|
|
25
|
+
.buttons { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
|
26
|
+
button { border: 0; border-radius: 14px; padding: 14px 12px; color: #f8fafc; background: #2563eb; font-size: 18px; font-weight: 750; cursor: pointer; }
|
|
27
|
+
button:hover { filter: brightness(1.12); }
|
|
28
|
+
button.secondary { background: rgba(100, 116, 139, .55); }
|
|
29
|
+
p { margin: 18px 0 0; color: #94a3b8; font-size: 13px; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<main>
|
|
34
|
+
<h1>Interactive Glimpse Counter</h1>
|
|
35
|
+
<output id="count">0</output>
|
|
36
|
+
<div class="buttons">
|
|
37
|
+
<button id="dec" class="secondary">−</button>
|
|
38
|
+
<button id="reset" class="secondary">0</button>
|
|
39
|
+
<button id="inc">+</button>
|
|
40
|
+
</div>
|
|
41
|
+
<p>Each click is sent back as one discrete CLI event.</p>
|
|
42
|
+
</main>
|
|
43
|
+
<script>
|
|
44
|
+
let count = 0;
|
|
45
|
+
const output = document.querySelector('#count');
|
|
46
|
+
function render() { output.textContent = String(count); }
|
|
47
|
+
let seq = 0;
|
|
48
|
+
function emit(action) {
|
|
49
|
+
seq += 1;
|
|
50
|
+
window.glimpse?.send?.({ type: 'counter.changed', action, count, seq });
|
|
51
|
+
}
|
|
52
|
+
document.querySelector('#inc').addEventListener('click', () => { count += 1; render(); emit('increment'); });
|
|
53
|
+
document.querySelector('#dec').addEventListener('click', () => { count -= 1; render(); emit('decrement'); });
|
|
54
|
+
document.querySelector('#reset').addEventListener('click', () => { count = 0; render(); emit('reset'); });
|
|
55
|
+
render();
|
|
56
|
+
</script>
|
|
57
|
+
</body>
|
|
58
|
+
</html>
|
|
59
|
+
HTML
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
open_json=$($GLIMPSE open --name "$WINDOW_NAME" --replace --width 380 --height 430 --title "Glimpse Counter" --html "$HTML")
|
|
63
|
+
echo "$open_json"
|
|
64
|
+
WINDOW_REF=$(printf '%s' "$open_json" | sed -n 's/.*"windowId":"\([^"]*\)".*/\1/p')
|
|
65
|
+
if [[ -z "$WINDOW_REF" ]]; then
|
|
66
|
+
echo "Failed to open counter window." >&2
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "Opened window '$WINDOW_NAME' ($WINDOW_REF)."
|
|
71
|
+
echo "Waiting for discrete counter events. Press Ctrl-C to stop listening; close the window when done."
|
|
72
|
+
|
|
73
|
+
# Block until the next event instead of polling with repeated CLI invocations.
|
|
74
|
+
# Waiting without --type lets the same loop handle both button clicks and the
|
|
75
|
+
# window.closed system event.
|
|
76
|
+
while true; do
|
|
77
|
+
event_json=$($GLIMPSE wait -w "$WINDOW_REF")
|
|
78
|
+
|
|
79
|
+
if [[ "$event_json" == *'"type":"window.closed"'* ]]; then
|
|
80
|
+
echo "$event_json"
|
|
81
|
+
echo "Window closed; stopping listener."
|
|
82
|
+
break
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
if [[ "$event_json" == *'"type":"counter.changed"'* ]]; then
|
|
86
|
+
echo "$event_json"
|
|
87
|
+
fi
|
|
88
|
+
done
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Self-contained glimpse-cli example: an interactive counter window.
|
|
5
|
+
# Demonstrates a state-snapshot pattern for consumers that prefer latest-state reconciliation.
|
|
6
|
+
# Run from the repo root with: ./examples/02b_counter_w_state.sh
|
|
7
|
+
|
|
8
|
+
GLIMPSE="${GLIMPSE:-bun src/cli.ts}"
|
|
9
|
+
WINDOW_NAME="glimpse-counter-example"
|
|
10
|
+
|
|
11
|
+
HTML=$(cat <<'HTML'
|
|
12
|
+
<!doctype html>
|
|
13
|
+
<html lang="en">
|
|
14
|
+
<head>
|
|
15
|
+
<meta charset="utf-8" />
|
|
16
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
17
|
+
<title>Glimpse Counter</title>
|
|
18
|
+
<style>
|
|
19
|
+
:root { color-scheme: dark; font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
20
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
21
|
+
body { margin: 0; min-height: 100vh; display: grid; place-items: center; overflow-x: hidden; background: radial-gradient(circle at top, #334155, #020617 65%); color: #f8fafc; }
|
|
22
|
+
main { width: min(360px, calc(100vw - 32px)); padding: 28px; border: 1px solid rgba(148, 163, 184, .35); border-radius: 24px; background: rgba(15, 23, 42, .78); box-shadow: 0 24px 80px rgba(0, 0, 0, .45); text-align: center; }
|
|
23
|
+
h1 { margin: 0 0 12px; font-size: 18px; font-weight: 650; color: #cbd5e1; }
|
|
24
|
+
output { display: block; margin: 18px 0 24px; font-size: 72px; font-weight: 800; line-height: 1; letter-spacing: -0.06em; }
|
|
25
|
+
.buttons { display: grid; grid-template-columns: repeat(3, 1fr); gap: 10px; }
|
|
26
|
+
button { border: 0; border-radius: 14px; padding: 14px 12px; color: #f8fafc; background: #2563eb; font-size: 18px; font-weight: 750; cursor: pointer; }
|
|
27
|
+
button:hover { filter: brightness(1.12); }
|
|
28
|
+
button.secondary { background: rgba(100, 116, 139, .55); }
|
|
29
|
+
p { margin: 18px 0 0; color: #94a3b8; font-size: 13px; }
|
|
30
|
+
</style>
|
|
31
|
+
</head>
|
|
32
|
+
<body>
|
|
33
|
+
<main>
|
|
34
|
+
<h1>Interactive Glimpse Counter</h1>
|
|
35
|
+
<output id="count">0</output>
|
|
36
|
+
<div class="buttons">
|
|
37
|
+
<button id="dec" class="secondary">−</button>
|
|
38
|
+
<button id="reset" class="secondary">0</button>
|
|
39
|
+
<button id="inc">+</button>
|
|
40
|
+
</div>
|
|
41
|
+
<p>Clicks are sent back, then latest state is repeated briefly.</p>
|
|
42
|
+
</main>
|
|
43
|
+
<script>
|
|
44
|
+
let count = 0;
|
|
45
|
+
const output = document.querySelector('#count');
|
|
46
|
+
function render() { output.textContent = String(count); }
|
|
47
|
+
let seq = 0;
|
|
48
|
+
let snapshotRetries = 0;
|
|
49
|
+
function emit(action) {
|
|
50
|
+
seq += 1;
|
|
51
|
+
snapshotRetries = 10;
|
|
52
|
+
window.glimpse?.send?.({ type: 'counter.changed', action, count, seq });
|
|
53
|
+
}
|
|
54
|
+
// Reliable state channel: clicks can be bursty and shell polling is coarse.
|
|
55
|
+
// Repeat each latest snapshot briefly, then stop so the CLI queue cannot
|
|
56
|
+
// grow forever while waiting for the window to close.
|
|
57
|
+
setInterval(() => {
|
|
58
|
+
if (snapshotRetries > 0) {
|
|
59
|
+
snapshotRetries -= 1;
|
|
60
|
+
window.glimpse?.send?.({ type: 'counter.snapshot', count, seq });
|
|
61
|
+
}
|
|
62
|
+
}, 100);
|
|
63
|
+
document.querySelector('#inc').addEventListener('click', () => { count += 1; render(); emit('increment'); });
|
|
64
|
+
document.querySelector('#dec').addEventListener('click', () => { count -= 1; render(); emit('decrement'); });
|
|
65
|
+
document.querySelector('#reset').addEventListener('click', () => { count = 0; render(); emit('reset'); });
|
|
66
|
+
render();
|
|
67
|
+
</script>
|
|
68
|
+
</body>
|
|
69
|
+
</html>
|
|
70
|
+
HTML
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
open_json=$($GLIMPSE open --name "$WINDOW_NAME" --replace --width 380 --height 430 --title "Glimpse Counter" --html "$HTML")
|
|
74
|
+
echo "$open_json"
|
|
75
|
+
WINDOW_REF=$(printf '%s' "$open_json" | sed -n 's/.*"windowId":"\([^"]*\)".*/\1/p')
|
|
76
|
+
if [[ -z "$WINDOW_REF" ]]; then
|
|
77
|
+
echo "Failed to open counter window." >&2
|
|
78
|
+
exit 1
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
echo "Opened window '$WINDOW_NAME' ($WINDOW_REF)."
|
|
82
|
+
echo "Waiting for counter state snapshots. Press Ctrl-C to stop listening; close the window when done."
|
|
83
|
+
|
|
84
|
+
# Block until the next event instead of polling with repeated CLI invocations.
|
|
85
|
+
# This variant shows latest-state reconciliation: the page emits counter.changed
|
|
86
|
+
# per click, then repeats counter.snapshot briefly so consumers can observe the
|
|
87
|
+
# newest state without processing every click as a durable command.
|
|
88
|
+
last_seq=0
|
|
89
|
+
while true; do
|
|
90
|
+
event_json=$($GLIMPSE wait -w "$WINDOW_REF")
|
|
91
|
+
|
|
92
|
+
if [[ "$event_json" == *'"type":"window.closed"'* ]]; then
|
|
93
|
+
echo "$event_json"
|
|
94
|
+
echo "Window closed; stopping listener."
|
|
95
|
+
break
|
|
96
|
+
fi
|
|
97
|
+
|
|
98
|
+
if [[ "$event_json" == *'"type":"counter.snapshot"'* ]]; then
|
|
99
|
+
seq=$(printf '%s' "$event_json" | sed -n 's/.*"seq":\([0-9][0-9]*\).*/\1/p')
|
|
100
|
+
if [[ -n "$seq" && "$seq" -gt "$last_seq" ]]; then
|
|
101
|
+
echo "$event_json"
|
|
102
|
+
last_seq="$seq"
|
|
103
|
+
fi
|
|
104
|
+
fi
|
|
105
|
+
done
|