acrylblends 1.0.2 → 1.0.3

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.
@@ -54,6 +54,7 @@ They are enabled as they are built.
54
54
  | Look up a call | [API](api/index.md), [Routing](api/routing.md), [Context](api/context.md), [Request](api/request.md), [API summary](api/summary.md) |
55
55
  | Understand the design | [Concepts](concepts/motivation.md) |
56
56
  | Copy a pattern | [Guides](guides/best-practices.md), [Validation](guides/validation.md), [Testing](guides/testing.md), [Data, migrations and choosing a store](guides/data.md), [Forms, sessions and a database](guides/forms-and-sessions.md), [Client components without a bundler](guides/client-modules.md), [Bring your own tool](guides/bring-your-own-tool.md) |
57
+ | Run the 90-minute workshop | [Workshop](workshop/README.md), and the room around it in [Facilitator notes](workshop/facilitator.md) |
57
58
  | Use Acryl inside a Cordis app | [Acryl in a Cordis app](cordis/acryl-in-a-cordis-app.md), [Plugin lifecycle](cordis/plugin-lifecycle.md), [Mounting](cordis/mounting.md) |
58
59
  | Move from the 0.3 router calls | [Migration from 0.3](cordis/migration-from-0-3.md) |
59
60
  | Know what runs in production, and what the sandbox stops | [What runs in production](guides/production.md) |
@@ -47,7 +47,7 @@ npm run dev
47
47
  ```
48
48
 
49
49
  - `add` puts the `agent-panel` row (and `driver-pty`'s after it) in `blend.yaml` before the `routes` row and registers both in `src/app.ts`. On an app without the `agent` and `extensions` rows, such as `blank`, it refuses before it copies anything and says which rows are missing.
50
- - **The panel's coding agents are yours.** `claude`, `codex` and `pi` run on the machine that runs the app, in the app folder, with the login of the person who started it: `claude` and `codex` use their own signed-in sessions, and `pi` is offered when `DEEPSEEK_API_KEY` is set or `pi` has stored a DeepSeek login (`~/.pi/agent/auth.json`). A program that is not installed, or is not logged in, is not offered; nothing is shared between machines, and a workshop laptop needs its own login. The panel's own `agent` (the offline scripted one) needs none.
50
+ - **The panel's coding agents are yours.** `claude`, `codex` and `pi` run on the machine that runs the app, in the app folder, with the login of the person who started it: `claude` and `codex` use their own signed-in sessions, and `pi` is offered when `DEEPSEEK_API_KEY` is set or `pi` has stored a DeepSeek login (`~/.pi/agent/auth.json`, read from the `HOME` of the process that runs the app: an app started under another `HOME` does not see the login, and the panel then lists `pty:pi` but no chat `pi`). A program that is not installed, or is not logged in, is not offered; nothing is shared between machines, and a workshop laptop needs its own login. The panel's own `agent` (the offline scripted one) needs none.
51
51
  - `pi` has to be an executable on the `PATH`; a shell function that wraps the binary is fine as long as the binary itself is found by `which pi`.
52
52
  - `driver-pty` brings `node-pty`, which ships prebuilt binaries for macOS, Windows and Linux. npm 11 and pnpm warn that its install script was not run (they ask you to approve dependency install scripts); you can ignore the warning. If no `pty:` terminal is offered and the app prints why at start, approve and rebuild: `npm approve-scripts node-pty`, then `npm rebuild node-pty` (pnpm: `pnpm approve-builds`).
53
53
 
@@ -142,6 +142,18 @@ CREATE TABLE tags (
142
142
  - A database whose version is newer than your files (an older build started on newer data) is refused. The app does not start.
143
143
  - A migration is plain SQL. Put the data change next to the shape change in the same file when they belong together.
144
144
 
145
+ **In an app made by `acrylblends init`** (a Cordis app) the provider is a row of `blend.yaml` (and an entry in the `registry` of `src/app.ts`, which `acrylblends add db` prints how to write):
146
+
147
+ ```yaml
148
+ - id: db
149
+ name: acryl-db-sqlite
150
+ config:
151
+ path: data/app.sqlite
152
+ migrations: migrations
153
+ ```
154
+
155
+ A relative `path` or `migrations` in a row is read from the app's folder (the one that holds `blend.yaml`), not from where the command was started. (Since 1.0.3; before it a relative path stopped the app with "no file base URL", and an absolute path, or the `db: { migrations: join(projectRoot, 'migrations') }` option of `compose` in `src/app.ts` as the notes template does, was the way.)
156
+
145
157
  To add a feature that needs a column or a table, the work is always the same three steps: write the next migration, change the store that
146
158
  reads and writes it, and change the handlers and views that use the store.
147
159
 
@@ -0,0 +1,381 @@
1
+ # From nothing to your own app: the ACRYL Blends workshop
2
+
3
+ Ninety minutes, starting from an empty folder. **Acryl** is a small web framework built on Web Standards: a handler takes a `Request` and returns a
4
+ `Response`, exactly as in the browser. **ACRYL Blends** puts that framework inside an app you own: a folder whose `blend.yaml` lists its plugins, whose
5
+ `plugins/` folder holds their code, and which runs in a browser or in a desktop window. A coding agent can change the app while it runs. By the end you
6
+ will have built an app from the blank template, added a feature to it with an agent, opened it as a desktop window, upgraded it, and published it as a
7
+ starter other people can pull from the registry.
8
+
9
+ Every command below is copy-pasteable and was run on the published packages. Each step says how long it takes; the checkpoints tell you whether you
10
+ are on time.
11
+
12
+ ## What you need (10 minutes)
13
+
14
+ | | macOS | Windows | Linux |
15
+ | --- | --- | --- | --- |
16
+ | Node.js 22.19 or newer (24 LTS or 26 tested) | `node -v` | `node -v` | `node -v` |
17
+ | npm, which comes with Node | `npm -v` | `npm -v` | `npm -v` |
18
+ | git | `git --version` | `git --version` | `git --version` |
19
+ | a coding agent, logged in | `codex login status` or `claude --version` | same | same |
20
+ | a GitHub account and the `gh` CLI, logged in | `gh auth status` | same | same |
21
+
22
+ Install what is missing: Node from <https://nodejs.org> (the LTS), git from <https://git-scm.com>, the GitHub CLI (`brew install gh`, `winget
23
+ install GitHub.cli`, or <https://cli.github.com>), and one agent with `npm i -g @openai/codex` (then `codex login` once) or Claude Code. Run
24
+ `gh auth login` once, in any repository: step 10 publishes your app with a pull request. No prior knowledge of ACRYL, Cordis or Blends is needed;
25
+ every new word is explained where it appears.
26
+
27
+ Make a folder and open a terminal in it:
28
+
29
+ ```sh
30
+ mkdir ~/acryl-workshop && cd ~/acryl-workshop
31
+ ```
32
+
33
+ ## The path
34
+
35
+ | Minutes | Step | What you get |
36
+ | --- | --- | --- |
37
+ | 0-10 | Before you start | tools installed and checked |
38
+ | 10-15 | 1. Create and run | a blank app answering in a browser |
39
+ | 15-22 | 2. Change one thing | a route of your own, live, without a restart |
40
+ | 22-30 | 3. Tests and the typecheck | the app proves itself |
41
+ | 30-38 | 4. Data: JSON, then SQLite *(optional)* | a counter kept in a file, then in SQL |
42
+ | 38-45 | 5. A desktop window | the same app as a window |
43
+ | 45-53 | 6. Pull a starter from the registry | someone else's app in your folder |
44
+ | 53-60 | 7. Create from a template | the six starters, one table, one run |
45
+ | 60-75 | 8. An agent builds a feature | your app changed by the agent, still running |
46
+ | 75-82 | 9. Upgrade | the app moved to the current framework |
47
+ | 82-90 | 10. Publish it | your app as a registry starter |
48
+
49
+ > **Checkpoint 15 minutes:** your app answers in a browser. Without that, read the "Stuck?" line of step 1.
50
+ > **Checkpoint 45 minutes:** your app is open in a desktop window. Behind? Step 4 is the optional one; skip it and carry on.
51
+ > **Checkpoint 75 minutes:** the agent's page is live. Still waiting? Give it five more minutes, then skip step 9.
52
+
53
+ ## 1. Create and run (5 minutes)
54
+
55
+ ```sh
56
+ npm create acrylblends@latest my-hero blank
57
+ cd my-hero
58
+ npm install
59
+ npm run dev
60
+ ```
61
+
62
+ You should see:
63
+
64
+ ```
65
+ created my-hero (blank) in /Users/you/acryl-workshop/my-hero: … files, plugins instance, acryl-router, acryl-node-server, manifest, save
66
+
67
+ Next:
68
+ cd my-hero
69
+ npm install
70
+ npm run dev
71
+ ```
72
+
73
+ then, from `npm run dev`:
74
+
75
+ ```
76
+ my-hero listening on http://127.0.0.1:3080 (data in /Users/you/acryl-workshop/my-hero/.acrylblends)
77
+ ```
78
+
79
+ Open that address: `{"app":"my-hero","ok":true}`. `/health` answers `{"ok":true}` too. Leave the terminal running.
80
+
81
+ **Stuck?** A busy port gets the next free one: use the address the app printed. The first `npm create` downloads the initializer, so give it a few
82
+ seconds.
83
+
84
+ ## 2. Change one thing (7 minutes)
85
+
86
+ Open `src/routes.ts`. It builds an `Acryl` app and mounts it on the router. Add one line to that chain, just above `.get('/health', …)`, and keep the
87
+ indentation of the lines around it:
88
+
89
+ ```ts
90
+ .get('/hello', (c) => c.json({ hello: 'workshop' }))
91
+ ```
92
+
93
+ Save. The terminal prints `Change detected … Restarting 'src/main.ts'`, and `http://127.0.0.1:3080/hello` answers `{"hello":"workshop"}`. No restart
94
+ command, no build step: the app watches its own source.
95
+
96
+ **Stuck?** No message means the file is not where the app watches; the message names the path it saw. A red error names the file and line.
97
+
98
+ ## 3. Tests and the typecheck (8 minutes)
99
+
100
+ Stop the server with `Ctrl-C`, then:
101
+
102
+ ```sh
103
+ npm test
104
+ npm run check
105
+ ```
106
+
107
+ `npm test` ends with `ℹ pass 5`, `ℹ fail 0`. `npm run check` prints nothing when the types are clean.
108
+
109
+ Now add a test for your route in `test/`: copy the style of the existing test file, request `/hello` and expect `{"hello":"workshop"}`. `npm test`
110
+ again; a failure names the file and line.
111
+
112
+ **Stuck?** `npm run check` complaining about your new line is the typechecker doing its job; the message says which type it expected.
113
+
114
+ ## 4. Data: JSON, then SQLite (8 minutes, optional)
115
+
116
+ An app keeps data through a **port**: a small interface a plugin provides. The smallest is `kv`, a JSON file in the app's own folder.
117
+
118
+ ```sh
119
+ npx acrylblends add acryl-kv-json
120
+ npm install
121
+ ```
122
+
123
+ `add` copies the plugin into `plugins/` and prints how to mount it. Three edits:
124
+
125
+ 1. `blend.yaml`, before `- id: server`: add the provider row (the row's id is yours to pick; the name is the plugin):
126
+
127
+ ```yaml
128
+ - id: kv
129
+ name: acryl-kv-json
130
+ ```
131
+
132
+ 2. `src/app.ts`: import the plugin and register it under that name:
133
+
134
+ ```ts
135
+ import * as kvJson from '../plugins/acryl-kv-json/src/index.ts'
136
+ ```
137
+
138
+ ```ts
139
+ export const registry = { 'acryl-router': router, 'acryl-node-server': nodeServer, 'acryl-kv-json': kvJson, routes }
140
+ ```
141
+
142
+ 3. `src/routes.ts`: declare the port and count visits in your route:
143
+
144
+ ```ts
145
+ import type {} from '../plugins/acryl-kv/src/cordis.ts'
146
+ ```
147
+
148
+ ```ts
149
+ inject: ['router', 'kv'],
150
+ ```
151
+
152
+ ```ts
153
+ .get('/hello', async (c) => {
154
+ const visits = await ctx.kv.update('visits', (n) => (n ?? 0) + 1)
155
+ return c.json({ hello: 'workshop', visits })
156
+ })
157
+ ```
158
+
159
+ `npm run dev` again and open `/hello` twice: `visits` goes 1, 2. The file is `my-hero/.acrylblends/kv.json`.
160
+
161
+ The relational port is `db`, with numbered SQL migrations:
162
+
163
+ ```sh
164
+ npx acrylblends add acryl-db-sqlite
165
+ mkdir migrations
166
+ printf 'CREATE TABLE visits (id INTEGER PRIMARY KEY, at TEXT NOT NULL);\n' > migrations/001-visits.sql
167
+ npm install
168
+ ```
169
+
170
+ A row in `blend.yaml` (`- id: db`, `name: acryl-db-sqlite`), then the migration folder in `src/app.ts`, where the app is composed: the path is set
171
+ there, not in the manifest, because a relative path in the manifest has no folder to resolve against.
172
+
173
+ ```ts
174
+ import * as dbSqlite from '../plugins/acryl-db-sqlite/src/index.ts'
175
+ // in the registry: 'acryl-db-sqlite': dbSqlite
176
+ const projectRoot = fileURLToPath(new URL('..', import.meta.url))
177
+ // in the compose options next to server:
178
+ db: { migrations: join(projectRoot, 'migrations') },
179
+ ```
180
+
181
+ Then `inject: ['router', 'kv', 'db']` with `import type {} from '../plugins/acryl-db/src/cordis.ts'`, and in the route
182
+ `await ctx.db.run('INSERT INTO visits (at) VALUES (?)', new Date().toISOString())` before the reply. The database is `my-hero/.acrylblends/data.sqlite`,
183
+ and the migrations run at start, in order.
184
+
185
+ **Stuck?** `migrations "migrations" is relative, but this app has no file base URL` means the folder was named in `blend.yaml`; move it into
186
+ `src/app.ts` as above. "no such table" means the file is not in `migrations/` at the app root.
187
+
188
+ ## 5. A desktop window (7 minutes)
189
+
190
+ ```sh
191
+ npx acrylblends add desktop
192
+ npm install
193
+ npm run desktop
194
+ ```
195
+
196
+ A window opens, showing the same routes and the same data. The app's own code never imports Electron; every Electron call lives in `plugins/desktop`,
197
+ and the window's browser data stays inside `my-hero/.acrylblends/electron`. Close the window when you are done (`Cmd-Q`, `Alt-F4`).
198
+
199
+ **Stuck?** The first `npm run desktop` downloads Electron (about 200 MB). No display (a bare server)? Skip this step; nothing later needs it.
200
+
201
+ ## 6. Pull a starter from the registry (8 minutes)
202
+
203
+ The registry is where people publish starters: <https://acrylblends.github.io/blends> (the starters written for this framework say
204
+ `runtime: acrylblends`; older ACRYL app starters are refused with a line saying so). Pull one by id, straight into a new folder:
205
+
206
+ ```sh
207
+ cd ~/acryl-workshop
208
+ npm create acrylblends@latest from-the-registry -- --from <starter-id>
209
+ cd from-the-registry
210
+ npm install
211
+ npm run dev
212
+ ```
213
+
214
+ The same command takes a git repository or a folder: `--from github:you/starter` or `--from ../a-template`. You get a complete app someone else
215
+ published, with its plugins, its dependencies and its `blend.yaml`, in your folder and yours to change. Nothing is installed for you: the starter's
216
+ `Next:` block asks for the same `npm install` as step 1.
217
+
218
+ For the workshop's own registry, name it:
219
+
220
+ ```sh
221
+ npm create acrylblends@latest from-the-workshop -- --from <id> --registry github:acryldev/workshop-2026-11-20
222
+ ```
223
+
224
+ Starters for this workshop: `acrylblends.notes-desktop` (notes with a desktop window), `acrylblends.chat`, `acrylblends.agent`.
225
+
226
+ **Stuck?** The `--` before `--from` is npm's, not ours: without it npm swallows the flag. A refused id prints its reason first (`error: acryl.gtd is an
227
+ ACRYL app starter: open it in the ACRYL app`) and then npm's own error block; read the first lines, not the last.
228
+
229
+ ## 7. Create from a template (7 minutes)
230
+
231
+ Every app begins from one of six templates, the same list in `npm create acrylblends` and `acrylblends init`:
232
+
233
+ | Template | What you get |
234
+ | --- | --- |
235
+ | `blank` | the router, the server and one route: what you just built |
236
+ | `nodejs` | a page rendered with JSX and a client component, without a bundler |
237
+ | `notes` | a multi-user notes app on the `db` port: sign-up, sessions, migrations, search, a typed client |
238
+ | `chat` | an answer streamed over server-sent events from an OpenAI-compatible API, history in SQLite |
239
+ | `todo` | a to-do list on the router and live state |
240
+ | `agent` | the blank app with an offline agent that writes and installs plugins into the running app |
241
+
242
+ Try the one with a database and accounts, in a window:
243
+
244
+ ```sh
245
+ cd ~/acryl-workshop
246
+ npm create acrylblends@latest notes-app notes
247
+ cd notes-app && npm install
248
+ npx acrylblends add desktop && npm install && npm run desktop
249
+ ```
250
+
251
+ Sign up, add a note, close the window; run `npm run dev` and the note is still there, because it lives in `notes-app/.acrylblends/data.sqlite`.
252
+
253
+ **Stuck?** The sign-in page is at `/login`; the root redirects there. Delete the folder when you are done with it.
254
+
255
+ ## 8. An agent builds a feature (15 minutes)
256
+
257
+ The agent panel turns a coding agent you already use into a chat in the corner of the page. It needs the plugins the `agent` template ships (its model,
258
+ tools and memory), so this step builds a second, small app from that template; it is the app you publish in step 10.
259
+
260
+ ```sh
261
+ cd ~/acryl-workshop
262
+ npm create acrylblends@latest agent-app agent
263
+ cd agent-app
264
+ npm install
265
+ npx acrylblends add agent-panel driver-pty
266
+ npx acrylblends add desktop
267
+ npm install
268
+ npm run dev
269
+ ```
270
+
271
+ `add agent-panel` adds the plugin rows the panel needs and registers it for you, and prints what it changed. (On a `blank` app it refuses and says
272
+ which rows are missing: that is why this step uses the `agent` template.) Open the page, click the panel button in the corner, pick `codex` (or
273
+ `claude`, or `pi` when pi has its DeepSeek login in your home folder), and ask:
274
+
275
+ > Add a page at /board that lists the app's tasks, newest first, and a test for it.
276
+
277
+ The agent reads `AGENTS.md`, reads `docs/framework` (which is in your app), edits your files, runs your tests, and the app reloads itself. It takes a
278
+ few minutes; the panel shows each step, and has a stop button. Open `/board` when it is done.
279
+
280
+ A terminal agent works too: choose `pty:codex` (or `pty:claude`, `pty:pi`) for its normal interactive session inside the app folder, where it can use
281
+ the door (`.acryl/bin/acryl`) to look at the page it built.
282
+
283
+ **Stuck?** The panel uses your own agent account: `codex login` (or `claude`) in a terminal first. If `npm install` warns that npm blocked
284
+ `node-pty`'s install script, ignore it: prebuilt binaries ship. Only if the panel lists no `pty:` terminal, run `npm approve-scripts node-pty` and
285
+ `npm rebuild node-pty` (the `add` output says the same).
286
+
287
+ ## 9. Upgrade (7 minutes)
288
+
289
+ Your app carries its framework plugins as source, so a new release moves them in place and leaves your code alone. Run the tool by its full name:
290
+ **`npx acrylblends@latest`**, because an app's own copy of `acrylblends` shadows the bare command and an older copy has no `upgrade` at all.
291
+
292
+ ```sh
293
+ cd ~/acryl-workshop/my-hero
294
+ npx acrylblends@latest upgrade --dry-run
295
+ npx acrylblends@latest upgrade
296
+ npm install && npm test
297
+ ```
298
+
299
+ The dry run lists what would be replaced; the real run replaces untouched files and stops with a diff if you edited one (move your change into a plugin
300
+ of your own, or use `--force`, which backs the edited copies up first). An app made by this version is already current, so both commands answer
301
+ `everything is up to date`: that is the mechanism working, and an app made by an earlier release lists what it would move. Your tests still pass
302
+ afterwards.
303
+
304
+ ## 10. Publish it, and the hero moment (8 minutes)
305
+
306
+ The app you publish is `agent-app`, the one the agent changed in step 8 (keep `my-hero` too: it is yours, saved the same way). It is already a
307
+ **starter**: `blend.yaml` names it, lists its rows, and `plugins/` holds its code. Two parts make it public: your own copy, then the registry.
308
+
309
+ **Your own copy first.** `npm run save` commits the app and pushes it to its remote (it refuses a file that looks like a secret, and refuses to push a
310
+ private app to a public remote). Create an empty repository on your git host, set it as `origin`, run `npm run save`.
311
+
312
+ **The registry.** Publishing is a pull request to the workshop registry, `github.com/acryldev/workshop-2026-11-20`, and it is four commands.
313
+
314
+ 1. Fork and clone it, then let its script build the entry from your app:
315
+
316
+ ```sh
317
+ cd ~/acryl-workshop
318
+ gh repo fork acryldev/workshop-2026-11-20 --clone
319
+ cd workshop-2026-11-20
320
+ node scripts/make-entry.mjs ../agent-app <your-github-login>.agent-app --description "the app I built in the workshop"
321
+ ```
322
+
323
+ `make-entry` copies the app into `blends/<your-github-login>.agent-app/` without `node_modules`, `.git` or the app's data, marks its `blend.yaml`
324
+ public and licensed under that id, and writes the `acryl.template.json` that makes it pullable. The entry is the whole app: its dependencies come with
325
+ it, so whoever pulls it gets the desktop shell without adding it again. The id is lowercase, `<login>.<name>` with letters, digits and dashes.
326
+
327
+ 2. Commit, push and open the pull request:
328
+
329
+ ```sh
330
+ git add blends && git commit -m "add <your-github-login>.agent-app"
331
+ git push
332
+ gh pr create --fill --repo acryldev/workshop-2026-11-20
333
+ ```
334
+
335
+ 3. A check runs on the pull request, and when it passes the pull request is merged: your starter is listed, usually well under two minutes. If it
336
+ refuses, it comments what to change; fix, commit, push again and it re-checks. To take an app down, open a pull request that removes the folder:
337
+ `git rm -r blends/<your-github-login>.agent-app`.
338
+
339
+ Anyone can then start from it, in the browser or in a window:
340
+
341
+ ```sh
342
+ npm create acrylblends@latest my-app -- --from <your-github-login>.agent-app --registry github:acryldev/workshop-2026-11-20
343
+ ```
344
+
345
+ **Stuck?** The check's comment is the answer: read it, fix, push again. An id with a capital or a dot inside a part is refused by `make-entry`; use
346
+ lowercase `<login>.<name>`. `gh: not found` means the GitHub CLI is not installed, and `gh auth status` saying not logged in means `gh auth login`.
347
+
348
+ > **The hero moment.** An app you created, with a page an agent wrote for it, running as a desktop window, saved in your own repository, and published
349
+ > as a starter in the registry so the next person can pull it with one command. Along the way you built a second app from blank, a route, a test, a JSON
350
+ > store and a SQLite database. Note what you did not write: the router, the server, the JSON file, the SQLite migrations, the window, the page.
351
+
352
+ ## Where next
353
+
354
+ - **Read the docs you already have**: every app carries `docs/framework`, the same pages as <https://acrylblends.github.io/docs>. Start with
355
+ `cordis/acryl-in-a-cordis-app.md`, then `guides/data.md`, then `guides/production.md` (what runs in production, and what does not).
356
+ - **Swap a provider by editing one row**: SQLite to Postgres (`npx acrylblends add acryl-db-postgres`), the browser to a window (`desktop`), one agent
357
+ to another (the panel's driver list).
358
+ - **Teach your agent**: `AGENTS.md` in your app is what it reads first; add your team's conventions there and the next run follows them.
359
+ - **Bring your own tool**: <https://acrylblends.github.io/docs/guides/bring-your-own-tool> wraps an outside library as a provider of a port.
360
+
361
+ ## Browser only (WebBoxes.ai) TODO(M9)
362
+
363
+ No local install? The same app, and the same agent, will run as a hosted workspace: open it in a browser, pick a starter, and work in the same three
364
+ folders. Nothing to install, nothing to keep on your machine.
365
+
366
+ The hosted workspace is milestone M9 and is due before the workshop, so this section is a stub until it is live: write the address, the sign-in it
367
+ needs, and the two or three clicks that replace steps 1-2 here, then let the local steps stand for anyone who wants the app on their own disk.
368
+
369
+ ---
370
+
371
+ ## If something goes wrong
372
+
373
+ | Symptom | Do |
374
+ | --- | --- |
375
+ | `npm create` fails or hangs | check the network; the facilitator has prepared apps |
376
+ | A port is taken | use the address the app printed |
377
+ | The agent cannot log in | `codex login` / `claude` in a terminal, then retry in the panel |
378
+ | You want to start over | delete your app folder and repeat step 1; nothing lives outside it |
379
+ | Anything else | the **Stuck?** line of the step, then your facilitator |
380
+
381
+ Behind at a checkpoint? Skip step 4 (data) first, then the second app in step 7. Steps 1-3, 5, 8-10 are the spine.
@@ -0,0 +1,107 @@
1
+ # Facilitator notes: the 90-minute ACRYL Blends workshop
2
+
3
+ Read the attendee handout first (`docs/framework/workshop/README.md`); this page is the room around it: what to put on each machine, what actually breaks, and
4
+ where to cut when the clock runs. Every timing and every failure below was measured on the published `acrylblends` 1.0.0 during the readiness walk
5
+ (`docs/plans/workshop-readiness.md`).
6
+
7
+ ## Before the room: put this on every machine
8
+
9
+ | What | Why | How |
10
+ | --- | --- | --- |
11
+ | Node.js 22.19 or newer (24 LTS or 26 tested), npm 11 | the app runs TypeScript directly, no build step | `node -v`; if missing, the LTS from nodejs.org or `brew install node` (Node 26 today, which is tested) |
12
+ | git | the registry block clones the registry | `git --version` |
13
+ | A warm npm cache | the first `npm create` downloads the initializer, `npm install` downloads Electron | create one app and add the desktop shell once per machine (below) |
14
+ | A coding agent, logged in | step 7 drives Codex or Claude Code through the panel | `codex login status` or run `claude` once |
15
+ | `~/acryl-workshop/prepared/` | the no-network fallback and the reset | copy the two prepared apps (below) |
16
+ | A GitHub account with `gh auth login` | step 10 forks the workshop registry and opens a pull request | `gh auth status` |
17
+ | A device with `git` and a text editor | the edits in steps 2, 4 and 8 are small but real | `git --version` |
18
+
19
+ Warm-up, once per machine (this is also the exact smoke test that the room works):
20
+
21
+ ```sh
22
+ mkdir -p ~/acryl-workshop && cd ~/acryl-workshop
23
+ npm create acrylblends@latest warmup notes
24
+ cd warmup && npm install && npm test
25
+ npx acrylblends add desktop && npm install
26
+ cd ~/acryl-workshop && cp -R warmup prepared-notes && rm -rf warmup
27
+ ```
28
+
29
+ That leaves `prepared-notes` with everything installed, including Electron.
30
+
31
+ ## Timings, checkpoints, and what to make optional
32
+
33
+ The handout's own timings, as measured on the published 1.0.2 (macOS, cold npm cache in a throwaway home):
34
+
35
+ | Minutes | Step | Measured | If late |
36
+ | --- | --- | --- | --- |
37
+ | 0-10 | installs and checks | seconds each | never cut |
38
+ | 10-15 | create and run the blank app | create 9 s cold (2 s warm), install 2 s, boot 2 s | never cut |
39
+ | 15-22 | change one route | reload is instant | never cut |
40
+ | 22-30 | tests and the typecheck | test 0.5-2 s, check 2.5-4 s | keep; cut the "write a test" exercise |
41
+ | 30-38 | data: JSON then SQLite (**optional**) | two adds plus edits, ~8 min | **cut first** |
42
+ | 38-45 | the desktop window | add 2 s, install 3 s, first Electron boot ~20 s | pre-warm Electron on every machine or demo it |
43
+ | 45-53 | pull a starter | create 4 s, install 3 s, runs at once | keep; it is the fastest win in the room |
44
+ | 53-60 | create from a template | create + install + add desktop 4 s | drop the second app |
45
+ | 60-75 | the agent builds a feature | set-up 4 s; codex wrote a page and tests in about 7 minutes | never cut; demo it if the room is behind |
46
+ | 75-82 | upgrade | seconds: a fresh app answers "everything is up to date" | keep; it is a 2-minute talk |
47
+ | 82-90 | publish and the hero moment | the registry steps are seconds; the PR is talk | always keep at least the hero talk |
48
+
49
+ Checkpoints in the handout, and what to look for as you walk the room: **15 minutes** every screen shows `{"app":"my-hero","ok":true}`; **45 minutes**
50
+ every screen has the app in a desktop window; **75 minutes** every screen has `/board` open with the agent's page. A machine behind at a checkpoint is
51
+ what the optional column is for, and the handout names the same choices so nobody has to guess.
52
+
53
+ ## What breaks in the room, and what to say
54
+
55
+ These are the defects found by the walk. They are known; say so rather than letting people think they broke something.
56
+
57
+ - **The data step is the slow one.** Wiring `kv` then `db` by hand is about ten minutes of edits for a newcomer (two rows, two registry lines, two
58
+ type imports, the migrations folder). It is the block the handout names optional, and the one to help with first.
59
+ - **A relative migrations path in `blend.yaml` fails.** `migrations: migrations` in the row stops the app with "this app has no file base URL to
60
+ resolve it from". The folder is named in `src/app.ts`'s compose options instead; the handout teaches that form and the stuck line names the error.
61
+ - **`pi` shows up only when the app can see its login.** 1.0.2 offers pi as a chat driver when `DEEPSEEK_API_KEY` is exported or `~/.pi/agent/auth.json`
62
+ holds a deepseek entry, read from the HOME the app runs in. On a normal attendee machine it is there; in a run with a moved HOME it is not (and pi
63
+ would fail on its first message anyway). `pty:pi` needs only the program.
64
+ - **The agent run takes minutes, not seconds.** Codex wrote the page and its tests in about seven; tell the room to start it and keep talking.
65
+ - **Publishing.** The workshop registry is `github.com/acryldev/workshop-2026-11-20`, public and open. Attendees fork it, run
66
+ `node scripts/make-entry.mjs ../agent-app <login>.agent-app --description "..."`, push and open a pull request; a check merges it, usually well under
67
+ two minutes, and a refusal comes as a comment. Taking an app down is a pull request that removes its folder. Pulling is
68
+ `--from <id> --registry github:acryldev/workshop-2026-11-20` (rehearsed on 1.0.2: create 12 s, install 3 s, runs at once).
69
+ - **Browser-only attendees (TODO(M9)).** The handout's hosted-workspace stub is empty until M9 is live; until then a browser-only attendee can follow
70
+ along but not create an app.
71
+ - **node-pty's install script (C5).** `npm install` warns that npm 11 blocked `node-pty`'s script. The terminal drivers still list; the workshop does
72
+ not open one.
73
+
74
+ ## The no-network fallback
75
+
76
+ If the venue has no reliable network:
77
+
78
+ 1. Every machine gets `prepared-notes` and a prepared agent app from the sheet above, so steps 1-6 skip creation and installs.
79
+ 2. Step 8 uses the cloned registry and the installed `blends-core`; no network needed.
80
+ 3. Step 7 needs the agent's own model, so it cannot run offline. Demo it from your machine on a tethered connection, or record it (a 4-minute
81
+ screen recording of the codex run is enough).
82
+ 4. Keep one machine on a phone hotspot for `npm create` failures.
83
+
84
+ ## Demo script for the agent block (5 minutes, if you demo instead of hands-on)
85
+
86
+ 1. On the prepared agent app: `npm run dev`, open the page, click the panel button.
87
+ 2. Ask: "Add a route GET /agent-built to src/routes.ts that returns {\"built\": true}, then run the tests and tell me what happened."
88
+ 3. Narrate while it runs: the agent reads `AGENTS.md`, reads `docs/framework`, edits `src/routes.ts`, runs the tests; the terminal prints
89
+ `Change detected … Restarting`; reload `/agent-built` and show `{"built":true}`.
90
+ 4. Point at the panel's stop button and the audit line the panel writes under `.acrylblends/audit/`.
91
+
92
+ ## Resetting between sessions
93
+
94
+ Everything a person makes lives inside their app folder and `~/acryl-workshop`. To reset a machine:
95
+
96
+ ```sh
97
+ rm -rf ~/acryl-workshop/[a-z]*-app ~/acryl-workshop/registry
98
+ cp -R ~/acryl-workshop/prepared/* ~/acryl-workshop/ # if you keep a prepared/ folder
99
+ ```
100
+
101
+ Nothing is installed globally except the agent and Bun; neither needs cleaning between sessions.
102
+
103
+ ## What is still open
104
+
105
+ - Starter pull (B1), the panel's rows (C1) and the upgrade command in the docs (B2) are with the framework team; when they land, the handout's
106
+ TODO lines become normal steps and this page's "what breaks" list shrinks.
107
+ - The Windows and Linux runs of the attendee handout are being replayed separately; add what differs to the handout's OS column rather than here.
@@ -15,6 +15,10 @@ import z from "@deepseek-ai/schemastery";
15
15
  */
16
16
  /** Stable Cordis plugin name, equal to the package name and the Loader row id. */
17
17
  const name = "acryl-db-sqlite";
18
+ /** The project folder of an acrylblends app (`appInstance.root`), when the instance has one: where a relative path in a row's config is read from, since such an app has no Loader base URL. */
19
+ function appRoot(value) {
20
+ return typeof value === "object" && value !== null && "root" in value && typeof value.root === "string" ? value.root : void 0;
21
+ }
18
22
  function isAppHome(value) {
19
23
  return typeof value === "object" && value !== null && "home" in value && typeof value.home === "string";
20
24
  }
@@ -26,12 +30,16 @@ const Config = z.object({
26
30
  });
27
31
  /** The file name used in the app home when `path` is not configured. */
28
32
  const DEFAULT_FILE = "data.sqlite";
29
- /** A configured path as a file path: absolute as given, relative to the Loader's base URL (the app) otherwise. */
33
+ /** A configured path as a file path: absolute as given, relative to the Loader's base URL (the app) otherwise, or to the app's project folder (`appInstance.root`) where there is no base URL. */
30
34
  function resolveFromApp(ctx, path, option) {
31
35
  if (path.startsWith("file:")) return fileURLToPath(path);
32
36
  if (isAbsolute(path)) return path;
33
37
  const base = ctx.baseUrl;
34
- if (base === void 0 || !base.startsWith("file:")) throw new Error(`acryl-db-sqlite: ${option} ${JSON.stringify(path)} is relative, but this app has no file base URL to resolve it from; give an absolute path`);
38
+ if (base === void 0 || !base.startsWith("file:")) {
39
+ const root = appRoot(ctx.get("appInstance"));
40
+ if (root !== void 0) return resolve(root, path);
41
+ throw new Error(`acryl-db-sqlite: ${option} ${JSON.stringify(path)} is relative, but this app has no file base URL to resolve it from; give an absolute path`);
42
+ }
35
43
  return resolve(base.endsWith("/") ? fileURLToPath(base) : dirname(fileURLToPath(base)), path);
36
44
  }
37
45
  async function apply(ctx, rawConfig = {}) {
@@ -19,12 +19,20 @@ function isAppHome(value) {
19
19
  const Config = z.object({ path: z.string().description("The JSON file, absolute or relative to the app. Defaults to kv.json in the ACRYL app home; required where there is none.") });
20
20
  /** The file name used in the app home when `path` is not configured. */
21
21
  const DEFAULT_FILE = "kv.json";
22
- /** A configured path as a file path: absolute as given, relative to the Loader's base URL (the app) otherwise. */
22
+ /** The project folder of an acrylblends app (`appInstance.root`), when the instance has one: where a relative path in a row's config is read from, since such an app has no Loader base URL. */
23
+ function appRoot(value) {
24
+ return typeof value === "object" && value !== null && "root" in value && typeof value.root === "string" ? value.root : void 0;
25
+ }
26
+ /** A configured path as a file path: absolute as given, relative to the Loader's base URL (the app) otherwise, or to the app's project folder (`appInstance.root`) where there is no base URL. */
23
27
  function resolveFromApp(ctx, path) {
24
28
  if (path.startsWith("file:")) return fileURLToPath(path);
25
29
  if (isAbsolute(path)) return path;
26
30
  const base = ctx.baseUrl;
27
- if (base === void 0 || !base.startsWith("file:")) throw new Error(`acryl-kv-json: path ${JSON.stringify(path)} is relative, but this app has no file base URL to resolve it from; give an absolute path`);
31
+ if (base === void 0 || !base.startsWith("file:")) {
32
+ const root = appRoot(ctx.get("appInstance"));
33
+ if (root !== void 0) return resolve(root, path);
34
+ throw new Error(`acryl-kv-json: path ${JSON.stringify(path)} is relative, but this app has no file base URL to resolve it from; give an absolute path`);
35
+ }
28
36
  return resolve(base.endsWith("/") ? fileURLToPath(base) : dirname(fileURLToPath(base)), path);
29
37
  }
30
38
  async function apply(ctx, rawConfig = {}) {
@@ -1,4 +1,4 @@
1
- import { c as Request, i as createAdaptorServer, l as RequestError, n as UpgradeEmitter, r as UpgradeHub, t as testWebSocket } from "../test-websocket-ZXHNHQSU.js";
1
+ import { c as Request, i as createAdaptorServer, l as RequestError, n as UpgradeEmitter, r as UpgradeHub, t as testWebSocket } from "../test-websocket-D8WWqE5G.js";
2
2
  import { t as serveStatic } from "../serve-static-3uo_9rjF.js";
3
3
  import { Service } from "@deepseek-ai/cordis";
4
4
  import z from "@deepseek-ai/schemastery";
@@ -1,4 +1,4 @@
1
- import { n as UpgradeEmitter, r as UpgradeHub } from "../test-websocket-ZXHNHQSU.js";
1
+ import { n as UpgradeEmitter, r as UpgradeHub } from "../test-websocket-D8WWqE5G.js";
2
2
  import "./node-server.js";
3
3
  import { Readable } from "node:stream";
4
4
  import { Service } from "@deepseek-ai/cordis";
@@ -1,4 +1,4 @@
1
- import { a as serve, i as createAdaptorServer, l as RequestError, n as UpgradeEmitter, o as upgradeWebSocket, r as UpgradeHub, s as getRequestListener, t as testWebSocket } from "./test-websocket-ZXHNHQSU.js";
1
+ import { a as serve, i as createAdaptorServer, l as RequestError, n as UpgradeEmitter, o as upgradeWebSocket, r as UpgradeHub, s as getRequestListener, t as testWebSocket } from "./test-websocket-D8WWqE5G.js";
2
2
  import { t as serveStatic } from "./serve-static-3uo_9rjF.js";
3
3
  import { getConnInfo } from "./node-server/conninfo.js";
4
4
  import { n as clientModules, t as buildClientModules } from "./client-modules-CLvWOjbd.js";
@@ -226,8 +226,7 @@ const readBodyBufferedBeforeDisconnect = (incoming, chunks) => {
226
226
  else if (incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey] !== void 0 && incoming.readableLength !== incomingWithRecovery[bodyBufferedLengthBeforeDisconnectKey]) result = newBodyUnusableError();
227
227
  else {
228
228
  const bodyChunks = chunks ?? [];
229
- const chunk = incoming.read();
230
- if (chunk !== null) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding));
229
+ for (let chunk = incoming.read(); chunk !== null; chunk = incoming.read()) bodyChunks.push(toBufferChunk(chunk, incoming.readableEncoding));
231
230
  const buffer = bodyChunks.length === 1 ? bodyChunks[0] : Buffer.concat(bodyChunks);
232
231
  result = buffer;
233
232
  const contentLength = incoming.headers["content-length"];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "acrylblends",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "The Acryl web framework (import { Acryl } from \"acrylblends\") and the acrylblends command that scaffolds Cordis apps.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -8,7 +8,7 @@
8
8
  "@deepseek-ai/cordis": "4.0.4",
9
9
  "@deepseek-ai/schemastery": "3.18.4"
10
10
  },
11
- "mount": "mount it after instance: root.plugin(dbSqlite, { migrations: 'migrations' }) with the migrations/ folder (001-name.sql, ...) next to src/; then a plugin that declares inject: ['db'] calls await ctx.db.all('SELECT ...', value)",
11
+ "mount": "mount it after instance, either as a row in blend.yaml (`- id: db` / `name: acryl-db-sqlite` / `config: { migrations: migrations }`, registered in src/app.ts as 'acryl-db-sqlite') or as root.plugin(dbSqlite, { migrations: 'migrations' }); a relative path is read from the app's folder, and the migrations/ folder (001-name.sql, ...) sits next to src/. Then a plugin that declares inject: ['db'] calls await ctx.db.all('SELECT ...', value)",
12
12
  "omit": ["**/*.test.*", "**/*.spec.*", "**/*.case.ts"],
13
13
  "imports": {
14
14
  "#acryl-db-sqlite": "acryl-db-sqlite/src/index.ts"