acrylblends 1.0.10 → 1.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.
Files changed (252) hide show
  1. package/README.md +9 -0
  2. package/dist/index.d.ts +60 -2
  3. package/dist/index.js +1992 -33
  4. package/dist/index.js.map +1 -1
  5. package/docs/framework/agents/index.md +1 -1
  6. package/docs/framework/api/summary.md +1 -1
  7. package/docs/framework/getting-started/desktop.md +23 -4
  8. package/docs/framework/guides/create-acrylblends.md +1 -1
  9. package/docs/framework/guides/production.md +20 -0
  10. package/docs/framework/workshop/README.md +174 -285
  11. package/docs/framework/workshop/facilitator.md +81 -90
  12. package/legacy/README.md +9 -0
  13. package/legacy/agent/157ce3db1112/src/app.ts +98 -0
  14. package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
  15. package/legacy/agent/24c681a00fca/src/app.ts +160 -0
  16. package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
  17. package/legacy/agent/2e790da977b2/src/app.ts +98 -0
  18. package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
  19. package/legacy/agent/6307045545dc/src/app.ts +168 -0
  20. package/legacy/agent/6307045545dc/src/routes.ts +83 -0
  21. package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
  22. package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
  23. package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
  24. package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
  25. package/legacy/agent/74ece148269c/src/app.ts +168 -0
  26. package/legacy/agent/74ece148269c/src/routes.ts +93 -0
  27. package/legacy/agent/855359304707/src/app.ts +168 -0
  28. package/legacy/agent/855359304707/src/routes.ts +126 -0
  29. package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
  30. package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
  31. package/legacy/agent/c2f30958043b/src/app.ts +98 -0
  32. package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
  33. package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
  34. package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
  35. package/legacy/blank/aea589283445/src/routes.ts +16 -0
  36. package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
  37. package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
  38. package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
  39. package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
  40. package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
  41. package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
  42. package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
  43. package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
  44. package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
  45. package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
  46. package/legacy/layouts.json +524 -0
  47. package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
  48. package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
  49. package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
  50. package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
  51. package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
  52. package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
  53. package/legacy/notes/4edc36949c15/src/app.ts +107 -0
  54. package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
  55. package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
  56. package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
  57. package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
  58. package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
  59. package/legacy/todo/02e613b3625c/src/app.ts +71 -0
  60. package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
  61. package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
  62. package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
  63. package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
  64. package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
  65. package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
  66. package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
  67. package/legacy/todo/8e64211312f3/src/app.ts +71 -0
  68. package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
  69. package/legacy/todo/b79078a87565/src/app.ts +71 -0
  70. package/legacy/todo/b79078a87565/src/todo.ts +125 -0
  71. package/lib/{audit-Dw-siYTp.d.ts → audit-Bok_UNCn.d.ts} +41 -6
  72. package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
  73. package/lib/cordis/door.d.ts +44 -7
  74. package/lib/cordis/door.js +214 -60
  75. package/lib/cordis/inspect.js +1 -1
  76. package/lib/cordis/node-server.d.ts +101 -3
  77. package/lib/cordis/node-server.js +3 -3
  78. package/lib/cordis/router-http.d.ts +7 -1
  79. package/lib/cordis/router-http.js +44 -3
  80. package/lib/cordis/router.d.ts +1 -1
  81. package/lib/cordis/router.js +1 -1
  82. package/lib/cordis/state.d.ts +3 -2
  83. package/lib/cordis/state.js +371 -7
  84. package/lib/door/host.d.ts +2 -2
  85. package/lib/door/host.js +1 -1
  86. package/lib/door/page.d.ts +20 -1
  87. package/lib/door/page.js +75 -14
  88. package/lib/door/protocol.d.ts +1 -1
  89. package/lib/drivers.d.ts +10 -1
  90. package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
  91. package/lib/node-server.d.ts +7 -1
  92. package/lib/node-server.js +2 -2
  93. package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
  94. package/lib/{snapshot-text-BfEbEOjA.js → snapshot-text-CXTnjxGx.js} +121 -9
  95. package/lib/src-CtkIX_Gg.js +427 -0
  96. package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
  97. package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
  98. package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
  99. package/package.json +2 -1
  100. package/plugins/acryl-door/src/host/channel.ts +124 -9
  101. package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
  102. package/plugins/acryl-door/src/host/door-file.ts +7 -2
  103. package/plugins/acryl-door/src/host/door.ts +58 -23
  104. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  105. package/plugins/acryl-door/src/host/page-socket.ts +18 -8
  106. package/plugins/acryl-door/src/host/verbs.ts +3 -0
  107. package/plugins/acryl-door/src/index.ts +166 -29
  108. package/plugins/acryl-door/src/page/base.ts +32 -0
  109. package/plugins/acryl-door/src/page/channel.ts +11 -1
  110. package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
  111. package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
  112. package/plugins/acryl-door/src/page/driver/driver.ts +49 -7
  113. package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
  114. package/plugins/acryl-door/src/page/index.ts +46 -2
  115. package/plugins/acryl-door/src/page/indicator.ts +14 -6
  116. package/plugins/acryl-door/src/page/outline.ts +56 -0
  117. package/plugins/acryl-door/src/page/protocol.ts +4 -0
  118. package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
  119. package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
  120. package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
  121. package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
  122. package/plugins/acryl-node-server/src/client-modules.ts +7 -1
  123. package/plugins/acryl-node-server/src/index.ts +304 -16
  124. package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
  125. package/plugins/acryl-page/plugin.json +12 -0
  126. package/plugins/acryl-page/src/index.ts +163 -0
  127. package/plugins/acryl-router/src/service.ts +77 -14
  128. package/plugins/acryl-router-http/src/index.ts +15 -1
  129. package/plugins/acryl-router-http/src/serve.ts +31 -0
  130. package/plugins/acryl-state/plugin.json +4 -3
  131. package/plugins/acryl-state/src/client-source.ts +244 -0
  132. package/plugins/acryl-state/src/service.ts +33 -6
  133. package/plugins/acryl-state/src/socket.ts +96 -0
  134. package/plugins/acryl-web-server/plugin.json +14 -0
  135. package/plugins/acryl-web-server/src/index.ts +164 -0
  136. package/plugins/agent/agent.ts +38 -7
  137. package/plugins/agent/index.ts +1 -1
  138. package/plugins/agent/model.ts +10 -2
  139. package/plugins/agent/tools.ts +9 -3
  140. package/plugins/agent-panel/client/describe.ts +57 -0
  141. package/plugins/agent-panel/client/driver-pick.ts +22 -0
  142. package/plugins/agent-panel/client/frame.ts +51 -0
  143. package/plugins/agent-panel/client/main.ts +343 -81
  144. package/plugins/agent-panel/drivers.ts +7 -2
  145. package/plugins/agent-panel/headless.ts +5 -2
  146. package/plugins/agent-panel/index.ts +187 -61
  147. package/plugins/agent-panel/mount.ts +27 -0
  148. package/plugins/agent-panel/visitors.ts +231 -0
  149. package/plugins/desktop/frame.ts +231 -0
  150. package/plugins/desktop/launch.ts +44 -9
  151. package/plugins/desktop/main.ts +100 -32
  152. package/plugins/desktop/window.ts +21 -4
  153. package/plugins/driver-pty/client/terminal.ts +4 -1
  154. package/plugins/driver-pty/index.ts +38 -11
  155. package/plugins/driver-pty/spawn.ts +38 -0
  156. package/plugins/driver-pty/terminal.ts +2 -0
  157. package/plugins/extensions/index.ts +26 -1
  158. package/plugins/extensions/resolve.ts +13 -0
  159. package/plugins/feedback/client.d.ts +22 -0
  160. package/plugins/feedback/client.js +471 -0
  161. package/plugins/feedback/index.ts +143 -0
  162. package/plugins/feedback/plugin.json +11 -0
  163. package/plugins/feedback/store.ts +307 -0
  164. package/plugins/home/index.ts +122 -0
  165. package/plugins/home/plugin.json +11 -0
  166. package/plugins/instance/index.ts +12 -3
  167. package/plugins/manifest/app-plugins.ts +86 -0
  168. package/plugins/manifest/index.ts +12 -2
  169. package/plugins/manifest/snapshot.ts +275 -0
  170. package/plugins/openai/index.ts +64 -5
  171. package/plugins/openai/prompt.ts +23 -0
  172. package/shared/AGENTS.core.md +36 -16
  173. package/shared/skills/acryl-add-ui/SKILL.md +19 -0
  174. package/shared/skills/acryl-app/SKILL.md +13 -0
  175. package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
  176. package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
  177. package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
  178. package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
  179. package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
  180. package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
  181. package/shared/skills/cordis-plugin/SKILL.md +26 -0
  182. package/templates/agent/AGENTS.md +4 -2
  183. package/templates/agent/blend.yaml +4 -0
  184. package/templates/agent/gitignore +1 -0
  185. package/templates/agent/package.json +1 -0
  186. package/templates/agent/src/app.ts +150 -33
  187. package/templates/agent/src/frame.ts +17 -0
  188. package/templates/agent/src/main.ts +5 -3
  189. package/templates/agent/src/routes.ts +73 -21
  190. package/templates/agent/template.json +2 -0
  191. package/templates/agent/test/app.test.ts +83 -5
  192. package/templates/blank/AGENTS.md +3 -2
  193. package/templates/blank/blend.yaml +11 -0
  194. package/templates/blank/gitignore +1 -0
  195. package/templates/blank/package.json +1 -0
  196. package/templates/blank/src/app.ts +100 -29
  197. package/templates/blank/src/door.ts +7 -0
  198. package/templates/blank/src/first-page.ts +5 -0
  199. package/templates/blank/src/frame.ts +17 -0
  200. package/templates/blank/src/routes.ts +65 -8
  201. package/templates/blank/template.json +14 -1
  202. package/templates/blank/test/app.test.ts +103 -16
  203. package/templates/chat/blend.yaml +2 -1
  204. package/templates/chat/gitignore +1 -0
  205. package/templates/chat/package.json +1 -0
  206. package/templates/chat/src/app.ts +72 -22
  207. package/templates/chat/src/chat-app.ts +11 -2
  208. package/templates/chat/src/client/chat.tsx +4 -2
  209. package/templates/chat/src/frame.ts +17 -0
  210. package/templates/chat/src/main.ts +1 -6
  211. package/templates/chat/src/page.tsx +6 -4
  212. package/templates/chat/src/routes.ts +14 -4
  213. package/templates/chat/src/runtime.ts +9 -3
  214. package/templates/chat/template.json +3 -1
  215. package/templates/chat/test/chat.test.ts +46 -1
  216. package/templates/chat/test/server.test.ts +17 -3
  217. package/templates/nodejs/blend.yaml +2 -1
  218. package/templates/nodejs/gitignore +1 -0
  219. package/templates/nodejs/package.json +1 -0
  220. package/templates/nodejs/src/app.ts +76 -21
  221. package/templates/nodejs/src/frame.ts +17 -0
  222. package/templates/nodejs/src/main.ts +1 -6
  223. package/templates/nodejs/src/page.tsx +3 -3
  224. package/templates/nodejs/src/routes.ts +32 -13
  225. package/templates/nodejs/template.json +3 -1
  226. package/templates/nodejs/test/app.test.ts +38 -3
  227. package/templates/nodejs/test/server.test.ts +17 -3
  228. package/templates/notes/blend.yaml +2 -1
  229. package/templates/notes/gitignore +1 -0
  230. package/templates/notes/package.json +1 -0
  231. package/templates/notes/src/app.ts +72 -22
  232. package/templates/notes/src/frame.ts +17 -0
  233. package/templates/notes/src/main.ts +1 -6
  234. package/templates/notes/src/notes-app.ts +24 -17
  235. package/templates/notes/src/routes.ts +11 -4
  236. package/templates/notes/src/runtime.ts +9 -3
  237. package/templates/notes/src/views.tsx +26 -17
  238. package/templates/notes/template.json +3 -1
  239. package/templates/notes/test/notes.test.ts +39 -1
  240. package/templates/notes/test/server.test.ts +17 -3
  241. package/templates/todo/blend.yaml +3 -0
  242. package/templates/todo/gitignore +1 -0
  243. package/templates/todo/package.json +1 -0
  244. package/templates/todo/src/app.ts +81 -27
  245. package/templates/todo/src/door.ts +7 -0
  246. package/templates/todo/src/frame.ts +17 -0
  247. package/templates/todo/src/page.ts +13 -6
  248. package/templates/todo/src/todo.ts +18 -6
  249. package/templates/todo/template.json +11 -1
  250. package/templates/todo/test/todo.test.ts +67 -9
  251. package/lib/src-LX0E4ec4.js +0 -117
  252. package/shared/SKILL.md +0 -14
@@ -1,108 +1,99 @@
1
- # Facilitator notes: the 90-minute ACRYL Blends workshop
1
+ # Facilitator notes: building inside the ACRYL frame
2
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`).
3
+ Read the handout first (`docs/framework/workshop/README.md`); this page is the room around it. The workshop changed shape again for the frame: the
4
+ app is created, then **opened inside ACRYL's IDE** (projects on the left, the app's page in the canvas, files/changes/terminals on the right), a
5
+ chat model works on the app from inside the frame, and coding agents run in terminal tabs in the same folder. The data layer, the desktop window and
6
+ the panel-first flow of the previous handout are gone.
7
+
8
+ The handout is written for **acrylblends 1.2.0 or newer** (the release that carries the frame). A room on an older release is on the previous handout;
9
+ check with `npx acrylblends@latest --version` before the day.
6
10
 
7
11
  ## Before the room: put this on every machine
8
12
 
9
13
  | What | Why | How |
10
14
  | --- | --- | --- |
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 |
15
+ | The preflight passing | it checks Node, npm, git, an agent with its login or key, a free port, and on Windows PowerShell and long paths | `npx acrylblends@latest preflight` ends with `Ready for the workshop.` |
16
+ | Node.js 22.19 or newer (24 and 26 tested) | the app runs TypeScript directly | `node -v`; the app's `scripts/check-node.cjs` refuses an older Node with a clear line |
17
+ | git, and a GitHub account with `gh auth login` | the first `npm run dev` makes the folder a git repository (Files and Changes need one); the last block forks the registry | `git --version`, `gh auth status` |
18
+ | One coding agent, logged in | block 5 starts it in a terminal tab; `claude`, `codex` and `pi` all worked | `codex login` once, or run `claude` once, or pi with `DEEPSEEK_API_KEY` in the terminal that starts `npm run dev` |
19
+ | One model key | block 3 adds it in Settings > Models; without it the chat shows a connect card | a DeepSeek key from <https://platform.deepseek.com>, or a Claude/ChatGPT subscription through Sign in with an account |
20
+ | A prepared app with `npm install` done | the first install brings the frame and the chat engine (about 500 MB together); a room on one Wi-Fi should not do it 20 times | run the handout's block 1 once per machine and keep the folder as `~/acryl-workshop/prepared/` |
21
+ | A warm npm cache | the first install dominates block 1 | the prepared app does it |
22
+
23
+ **The agent setting that decides the day.** The agents are configured to act rather than describe. Check on every machine that a plain request
24
+ produces files, not a plan: ask the prepared app's chat "add a line to the app's page" and watch the Changes panel. If it only describes, the model
25
+ is waiting for permission; the handout's sentence is "do it now".
26
+
27
+ **Agent-specific checks.** Codex answering `model_not_found` means the account does not have the default model: pick one it supports (`/model` in
28
+ codex, or `model = "..."` in `~/.codex/config.toml`). pi with DeepSeek needs `DEEPSEEK_API_KEY` in the environment of the terminal that runs `npm
29
+ run dev` (the terminal tabs inherit it); the preflight reports `PASS agent pi (DeepSeek key found)`. The package is
30
+ `@earendil-works/pi-coding-agent`.
31
+
32
+ ## Timings, from the rehearsal (this Mac, packed CLI, isolated HOME)
33
+
34
+ | Minutes | Block | Measured | If late |
36
35
  | --- | --- | --- | --- |
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
- - **An agent with its own settings acts differently.** Skills, hooks and instructions in a person's own Codex or Claude configuration travel into the panel's run: on the owner's laptop Codex stopped to "present a design for approval" instead of building. Word every task for the agent as "do it now, without asking for approval", and for a demo machine start the agent from a clean configuration (a fresh `CODEX_HOME`, no personal skills).
58
- - **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
59
- type imports, the migrations folder). It is the block the handout names optional, and the one to help with first.
60
- - **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
61
- 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.
62
- - **`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`
63
- 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
64
- would fail on its first message anyway). `pty:pi` needs only the program.
65
- - **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.
66
- - **Publishing.** The workshop registry is `github.com/acryldev/workshop-2026-11-20`, public and open. Attendees fork it, run
67
- `node scripts/make-entry.mjs ../agent-app <login>.agent-app --description "..."`, push and open a pull request; a check merges it, usually well under
68
- two minutes, and a refusal comes as a comment. Taking an app down is a pull request that removes its folder. Pulling is
69
- `--from <id> --registry github:acryldev/workshop-2026-11-20` (rehearsed on 1.0.2: create 12 s, install 3 s, runs at once).
70
- - **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
71
- along but not create an app.
72
- - **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
73
- not open one.
36
+ | 0-10 | preflight and setup | preflight ~15 s; the prepared app skips create and install | never cut |
37
+ | 10-20 | create and open the frame | the first `npm install` dominates (the frame and the chat engine); the frame's first page is up in a few seconds after that | use the prepared app and open its frame |
38
+ | 20-25 | the first change at /app | the Files view opens `src/routes.ts`; stop and `npm run dev` reconnects the frame in a few seconds; the page shows the edit | cut `npm test` to the take-home |
39
+ | 25-30 | Settings > Models | the key saves with Apply (`Saved DeepSeek (deepseek-official).`); the page uses it after a reload | paste the key in the first-run card instead, Save and continue |
40
+ | 30-50 | chat builds the board | the chat writes into the app folder and Changes fills; the restart loop shows it at /app/board | if the model is slow, cut the board to one column |
41
+ | 50-65 | a coding agent in a terminal | the agent tab starts in the app folder (its prompt is `my-app`); `.acryl/bin/acryl look` prints the page in the frame's tile | demo one agent and let the room follow |
42
+ | 65-80 | upgrade an older app | `npx acrylblends@latest upgrade` reports every file it changed (the `frame` line is the new layout), then `npm install` and `npm run dev` | keep the report, skip the second app |
43
+ | 80-90 | publish (take-home) | the registry check merges in about two minutes | leave it as the take-home |
44
+
45
+ What the rehearsal verified end to end on this Mac: a packed CLI in an isolated HOME, the app opened by the frame with the app's page in a tab
46
+ (the first-run tile), the Preview Notice dismissed once, Settings > Models saving a key and the chat sending with it (against a local mock), a
47
+ terminal tab running commands in the app folder, the app's page edited and shown after a restart, the door seeing the page (`acryl look`), and an
48
+ app made by the previous release upgraded and opened by the frame.
49
+
50
+ ## What to say in the room
51
+
52
+ - **The frame is the product.** The point is not "an app with a chat sidebar"; it is that ACRYL's IDE hosts the app you are building: the app is
53
+ the page in the middle, its folder is the workspace, and the agents work in that folder.
54
+ - **Settings > Models is the key's home.** The key goes to the frame's own credentials store, never the app folder or `.acrylblends` files. The
55
+ first-run card writes the same store; either path is fine.
56
+ - **The loop is stop, start, look.** A source change needs `npm run dev` again; the port is stable per app, so the frame reconnects on the same
57
+ address and the app's tab is still there. If a chat tab comes to the front after the restart, press the `my-app` tab.
58
+ - **Two kinds of agent.** The chat (DSH's own, in the frame) and the coding agents in terminal tabs are different hands on the same folder; show
59
+ both and let people pick.
60
+ - **The door.** `.acryl/bin/acryl look` prints the page with refs, `act` clicks and types. In the frame the page is the tile, and the door sees
61
+ it. This is how an agent checks its own work.
62
+ - **Production is unchanged.** The frame, the chat and the agents are development tools; `NODE_ENV=production npm start` serves the app alone.
63
+
64
+ ## Windows
65
+
66
+ Windows PowerShell 5.1 has no `&&` (two lines, or `;`); PowerShell sets variables with `$env:NAME="value"`; cmd.exe's `Ctrl-C` asks `Terminate
67
+ batch job (Y/N)?` (answer `Y`); cmd.exe does not expand `~` (use `%USERPROFILE%`). `npm install` needs the "Desktop development with C++" workload
68
+ of Visual Studio Build Tools for the terminal package; the preflight notes it.
74
69
 
75
70
  ## The no-network fallback
76
71
 
77
- If the venue has no reliable network:
78
-
79
- 1. Every machine gets `prepared-notes` and a prepared agent app from the sheet above, so steps 1-6 skip creation and installs.
80
- 2. Step 8 uses the cloned registry and the installed `blends-core`; no network needed.
81
- 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
82
- screen recording of the codex run is enough).
83
- 4. Keep one machine on a phone hotspot for `npm create` failures.
72
+ 1. Prepared apps with `npm install` done cover blocks 1, 2 and the frame; the models (chat and agents) still need a connection or a demo.
73
+ 2. Block 7's registry needs GitHub; if the venue is offline, publish from your machine on a hotspot at the end, or show the recording.
74
+ 3. Keep one phone hotspot for the room.
84
75
 
85
- ## Demo script for the agent block (5 minutes, if you demo instead of hands-on)
76
+ ## Demo script (five minutes, if the room is behind)
86
77
 
87
- 1. On the prepared agent app: `npm run dev`, open the page, click the panel button.
88
- 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."
89
- 3. Narrate while it runs: the agent reads `AGENTS.md`, reads `docs/framework`, edits `src/routes.ts`, runs the tests; the terminal prints
90
- `Change detected … Restarting`; reload `/agent-built` and show `{"built":true}`.
91
- 4. Point at the panel's stop button and the audit line the panel writes under `.acrylblends/audit/`.
78
+ 1. Prepared app, frame open on the app's page: press `+`, open **AcrylDSH Chat**, ask for a sentence on the page. Narrate the Changes panel and
79
+ the restart loop.
80
+ 2. Open a **files** tab in the right panel and show the edit the model made.
81
+ 3. `+` → **Pi** (or Claude): the agent starts in the app folder; ask it for a route; run `.acryl/bin/acryl look` in that terminal and show the
82
+ page's snapshot.
83
+ 4. Settings > Models: show where the key lives and that it is not in the app folder.
84
+ 5. Show the registry listing with an app already published.
92
85
 
93
86
  ## Resetting between sessions
94
87
 
95
- Everything a person makes lives inside their app folder and `~/acryl-workshop`. To reset a machine:
96
-
97
- ```sh
98
- rm -rf ~/acryl-workshop/[a-z]*-app ~/acryl-workshop/registry
99
- cp -R ~/acryl-workshop/prepared/* ~/acryl-workshop/ # if you keep a prepared/ folder
100
- ```
101
-
102
- Nothing is installed globally except the agent and Bun; neither needs cleaning between sessions.
88
+ Everything a person makes lives inside their app folder and `~/acryl-workshop`. To reset a machine, delete the app folders and copy the prepared one
89
+ back; nothing is installed globally except the agent, `gh` and Node. An app's data lives in its `.acrylblends/` (gitignored); the model key lives in
90
+ that home's credentials store, so a fresh copy starts without it.
103
91
 
104
92
  ## What is still open
105
93
 
106
- - 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
107
- TODO lines become normal steps and this page's "what breaks" list shrinks.
108
- - 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.
94
+ - The frame's chat tab takes the focus when it binds its session after a restart; the app's tab is one click away and the guide says so.
95
+ - The frame's address is stable across restarts: the launcher keeps the token in the app home (owner-only, atomic) and reuses it, so an open page
96
+ comes back by reloading its URL even if the browser lost its cookie. `npx acrylblends open` prints the address of the running frame;
97
+ `npx acrylblends open --new` rotates it (the old address stops working at once and the new one is printed). The frame-side banner that names the
98
+ command is still to come (after the 120 gate work).
99
+ - The one-click WebBoxes.ai deploy and the browser-only section stay TODO(M9) until that workspace is live.
@@ -0,0 +1,9 @@
1
+ # The scaffolds published acrylblends releases shipped
2
+
3
+ Generated by `scripts/build-legacy-layouts.mjs`; do not edit by hand. Each folder is a template, each hash under it one distinct scaffold (deduplicated by
4
+ content hash across releases), and `layouts.json` maps every published release that shipped templates (0.3.0 through 1.1.0, 40 releases)
5
+ to the layouts it shipped.
6
+
7
+ `acrylblends upgrade` compares an app's scaffold files with these bytes: a file the app has not edited is brought up to date, an edited one is refused (or
8
+ replaced with --force after a backup). Releases before 0.7.8 wrote src/main.ts importing './plugins/...', which cannot start; their layouts are kept so the
9
+ upgrade can say so precisely instead of writing a frame such a release's app cannot run.
@@ -0,0 +1,98 @@
1
+ import { join } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
4
+ import * as nodeServer from './plugins/acryl-node-server/src/index.ts'
5
+ import * as router from './plugins/acryl-router/src/index.ts'
6
+ import * as state from './plugins/acryl-state/src/index.ts'
7
+ import { agent, memory, model, tools } from './plugins/agent/index.ts'
8
+ import { builder } from './plugins/builder/index.ts'
9
+ import { extensions } from './plugins/extensions/index.ts'
10
+ import { type AppInstance, instance } from './plugins/instance/index.ts'
11
+ import {
12
+ blueprintsIn,
13
+ compose,
14
+ loadManifest,
15
+ type ParameterValue,
16
+ } from './plugins/manifest/index.ts'
17
+ import { type OpenAIConfig, openai } from './plugins/openai/index.ts'
18
+ import { sandboxPlugin } from './plugins/sandbox/index.ts'
19
+ import { save } from './plugins/save/index.ts'
20
+ import { tasks } from './plugins/tasks/index.ts'
21
+ import { routes } from './routes.ts'
22
+
23
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
24
+ export const registry = {
25
+ tasks,
26
+ model,
27
+ tools,
28
+ memory,
29
+ agent,
30
+ 'acryl-router': router,
31
+ 'acryl-state': state,
32
+ extensions,
33
+ builder,
34
+ save,
35
+ sandbox: sandboxPlugin,
36
+ routes,
37
+ 'acryl-node-server': nodeServer,
38
+ }
39
+
40
+ /** The manifest next to this app's source. */
41
+ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
42
+
43
+ /**
44
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
45
+ * `createApp` and the tests read it the same way.
46
+ */
47
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
48
+ return loadManifest(MANIFEST, {
49
+ known: Object.keys(registry),
50
+ getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
51
+ ...(values ? { values } : {}),
52
+ })
53
+ }
54
+
55
+ export interface App {
56
+ readonly root: Context
57
+ readonly url: string
58
+ stop(): Promise<void>
59
+ }
60
+
61
+ /**
62
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's own `extensions/`
63
+ * folder is trusted to install at start, and is where the builder writes.
64
+ */
65
+ export async function createApp(
66
+ chosen: AppInstance,
67
+ options: {
68
+ values?: Readonly<Record<string, ParameterValue>>
69
+ /** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
70
+ model?: OpenAIConfig
71
+ } = {},
72
+ ): Promise<App> {
73
+ const manifest = readManifest(options.values)
74
+ const extensionsDir = join(chosen.root, 'extensions')
75
+ const root = new Context()
76
+ const first = root.plugin(instance, chosen)
77
+ await first.await()
78
+ const fibers: Fiber[] = [
79
+ first,
80
+ // The `model` row mounts the scripted model, or the real one when a model is configured.
81
+ ...(await compose(root, manifest, options.model ? { ...registry, model: openai } : registry, {
82
+ ...(options.model ? { model: { ...options.model } } : {}),
83
+ // Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
84
+ tasks: { manifest: `sha256:${manifest.digest}` },
85
+ // The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
86
+ extensions: { sources: [{ dir: extensionsDir, trust: 'install' }] },
87
+ builder: { dir: extensionsDir },
88
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
89
+ })),
90
+ ]
91
+ return {
92
+ root,
93
+ url: root.nodeServer.url,
94
+ async stop() {
95
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
96
+ },
97
+ }
98
+ }
@@ -0,0 +1,63 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import { Acryl } from './plugins/acryl-router/src/index.ts'
3
+ import { bodyLimit } from './plugins/acryl-router/src/middleware/index.ts'
4
+ import type {} from './plugins/agent/index.ts'
5
+ import type {} from './plugins/extensions/index.ts'
6
+ import type {} from './plugins/tasks/index.ts'
7
+
8
+ export const APP_NAME = '__APP_NAME__'
9
+
10
+ /**
11
+ * The app's own routes on the router: `POST /ask` for the agent; `GET /extensions` and `GET /tasks` to observe what it did (every run is a
12
+ * durable task owning one task per tool call); `POST /tasks/cancel`. What changes is also streamed: `GET /api/state/tasks/events` and
13
+ * `GET /api/state/extensions/events`. The mount is removed when the plugin is disposed.
14
+ */
15
+ export const routes = {
16
+ name: 'routes',
17
+ inject: ['router', 'agent', 'extensions', 'tasks'],
18
+ apply(ctx: Context) {
19
+ // A body here is a name, an id or a question; anything bigger is refused before it is read.
20
+ const json = async (c: {
21
+ req: { json(): Promise<unknown> }
22
+ }): Promise<Record<string, unknown>> => {
23
+ const body = await c.req.json().catch(() => undefined)
24
+ return typeof body === 'object' && body !== null ? (body as Record<string, unknown>) : {}
25
+ }
26
+ const app = new Acryl()
27
+ .use(bodyLimit({ maxSize: 64 * 1024 }))
28
+ .get('/', (c) => c.json({ app: APP_NAME, ok: true }))
29
+ .get('/health', (c) => c.json({ ok: true }))
30
+ .get('/extensions', (c) =>
31
+ c.json({
32
+ installed: ctx.extensions.list(),
33
+ notInstalled: ctx.extensions.pending(),
34
+ waitingForApproval: ctx.extensions.approvals(),
35
+ }),
36
+ )
37
+ // The owner's decision on an install that is waiting (the `approval` parameter in blend.yaml).
38
+ for (const [path, decide] of [
39
+ ['/extensions/approve', (name: string) => ctx.extensions.approve(name)],
40
+ ['/extensions/deny', (name: string) => ctx.extensions.deny(name)],
41
+ ] as const) {
42
+ app.post(path, async (c) => {
43
+ const { name } = await json(c)
44
+ if (typeof name !== 'string') return c.json({ error: 'name must be a string' }, 400)
45
+ return c.json(await decide(name))
46
+ })
47
+ }
48
+ app
49
+ .get('/tasks', (c) => c.json(ctx.tasks.list().slice(0, 100)))
50
+ .post('/tasks/cancel', async (c) => {
51
+ const { id } = await json(c)
52
+ if (typeof id !== 'string' || !ctx.tasks.get(id))
53
+ return c.json({ error: 'no such task' }, 404)
54
+ return c.json({ cancelled: ctx.tasks.cancel(id) })
55
+ })
56
+ .post('/ask', async (c) => {
57
+ const { question } = await json(c)
58
+ if (typeof question !== 'string') return c.json({ error: 'question must be a string' }, 400)
59
+ return c.json(await ctx.agent.run(question))
60
+ })
61
+ ctx.router.route('/', app)
62
+ },
63
+ }
@@ -0,0 +1,160 @@
1
+ import { existsSync } from 'node:fs'
2
+ import { join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
5
+ import * as door from '../plugins/acryl-door/src/index.ts'
6
+ import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
7
+ import * as router from '../plugins/acryl-router/src/index.ts'
8
+ import * as state from '../plugins/acryl-state/src/index.ts'
9
+ import { agent, memory, model, tools } from '../plugins/agent/index.ts'
10
+ import * as panel from '../plugins/agent-panel/index.ts'
11
+ import { builder } from '../plugins/builder/index.ts'
12
+ import { extensions } from '../plugins/extensions/index.ts'
13
+ import { type AppInstance, instance } from '../plugins/instance/index.ts'
14
+ import {
15
+ blueprintsIn,
16
+ compose,
17
+ loadManifest,
18
+ type ParameterValue,
19
+ } from '../plugins/manifest/index.ts'
20
+ import { type OpenAIConfig, openai } from '../plugins/openai/index.ts'
21
+ import { sandboxPlugin } from '../plugins/sandbox/index.ts'
22
+ import { save } from '../plugins/save/index.ts'
23
+ import { tasks } from '../plugins/tasks/index.ts'
24
+ import { routes } from './routes.ts'
25
+
26
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
27
+ export const registry = {
28
+ tasks,
29
+ model,
30
+ tools,
31
+ memory,
32
+ agent,
33
+ 'acryl-router': router,
34
+ 'acryl-state': state,
35
+ extensions,
36
+ builder,
37
+ save,
38
+ sandbox: sandboxPlugin,
39
+ routes,
40
+ 'acryl-node-server': nodeServer,
41
+ 'agent-panel': panel,
42
+ 'acryl-door': door,
43
+ }
44
+
45
+ /** The manifest next to this app's source. */
46
+ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
47
+
48
+ /**
49
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
50
+ * `createApp` and the tests read it the same way.
51
+ */
52
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
53
+ return loadManifest(MANIFEST, {
54
+ known: Object.keys(registry),
55
+ getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
56
+ ...(values ? { values } : {}),
57
+ })
58
+ }
59
+
60
+ export interface App {
61
+ readonly root: Context
62
+ /** The address the server listens on; empty when the app was made with `listen: false`. */
63
+ readonly url: string
64
+ stop(): Promise<void>
65
+ }
66
+
67
+ /** The development rows production leaves out, and what each one gives a visitor. */
68
+ const DEVELOPMENT_ROWS: Record<string, string> = {
69
+ agent: 'POST /ask runs the agent, which spends the model key and calls its tools',
70
+ builder: 'the agent can write plugin source into plugins/',
71
+ extensions:
72
+ 'a plugin installed live runs in this process, and POST /extensions/approve decides installs',
73
+ }
74
+
75
+ /**
76
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The app's `plugins/`
77
+ * folder is trusted to install at start, and is where the builder writes.
78
+ */
79
+ export async function createApp(
80
+ chosen: AppInstance,
81
+ options: {
82
+ values?: Readonly<Record<string, ParameterValue>>
83
+ /** A real model (any OpenAI-compatible API) in place of the offline scripted one. */
84
+ model?: OpenAIConfig
85
+ /** Start the HTTP server (default). Tests pass `false` and call `app.root.router.request(path)`: it needs no port, so it also runs where a sandbox forbids listening. */
86
+ listen?: boolean
87
+ } = {},
88
+ ): Promise<App> {
89
+ const read = readManifest(options.values)
90
+ // Rows left out: the server (and so the door, which needs it) when the app is reached in process; the door in production and with ACRYL_DOOR=off (one process at a time
91
+ // serves a folder's door, so the tests that start several apps set it).
92
+ const listen = options.listen !== false
93
+ const leaveOut = new Set<string>()
94
+ if (!listen) leaveOut.add('acryl-node-server').add('acryl-door')
95
+ if (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off')
96
+ leaveOut.add('acryl-door')
97
+ // Production: the rows that give a visitor power over the running app or the owner's model key are left out too. The agent (`POST /ask` spends the model key and runs tools),
98
+ // the builder and the extensions (live plugin install runs code in this process, and `POST /extensions/approve` decides it) are development tools. `ACRYL_PRODUCTION_KEEP`
99
+ // names the ones to keep (`agent,builder,extensions`); each one kept is announced at start, because none of them is behind a login.
100
+ if (process.env.NODE_ENV === 'production') {
101
+ const keep = new Set(
102
+ (process.env.ACRYL_PRODUCTION_KEEP ?? '')
103
+ .split(',')
104
+ .map((name) => name.trim())
105
+ .filter((name) => name !== ''),
106
+ )
107
+ for (const name of keep)
108
+ if (!(name in DEVELOPMENT_ROWS))
109
+ console.warn(
110
+ `WARNING: ACRYL_PRODUCTION_KEEP names "${name}", which is not one of ${Object.keys(DEVELOPMENT_ROWS).join(', ')}`,
111
+ )
112
+ for (const [row, danger] of Object.entries(DEVELOPMENT_ROWS)) {
113
+ if (keep.has(row))
114
+ console.warn(
115
+ `WARNING: the "${row}" row runs in production because ACRYL_PRODUCTION_KEEP names it: ${danger}. Nothing here is behind a login; put the app behind your own.`,
116
+ )
117
+ else leaveOut.add(row)
118
+ }
119
+ }
120
+ const manifest = {
121
+ ...read,
122
+ plugins: read.plugins.map((row) => (leaveOut.has(row.use) ? { ...row, disabled: true } : row)),
123
+ }
124
+ const extensionsDir = join(chosen.root, 'plugins')
125
+ // One release of grace: an app made before 0.7.8 kept what the agent wrote in `extensions/`. It is still installed, with a warning.
126
+ const legacyDir = join(chosen.root, 'extensions')
127
+ const legacy = existsSync(legacyDir)
128
+ if (legacy)
129
+ console.warn(
130
+ 'warning: extensions/ is the old folder for plugins the agent writes; move them to plugins/ ("git mv extensions/<name> plugins/<name>"). extensions/ is read for this release only.',
131
+ )
132
+ const root = new Context()
133
+ const first = root.plugin(instance, chosen)
134
+ await first.await()
135
+ const fibers: Fiber[] = [
136
+ first,
137
+ // The `model` row mounts the scripted model, or the real one when a model is configured.
138
+ ...(await compose(root, manifest, options.model ? { ...registry, model: openai } : registry, {
139
+ ...(options.model ? { model: { ...options.model } } : {}),
140
+ // Runtime values the manifest cannot hold: the manifest's own digest (every task records it), and who may install.
141
+ tasks: { manifest: `sha256:${manifest.digest}` },
142
+ // The `approval` parameter in blend.yaml (the extensions row) turns this source into one that asks.
143
+ extensions: {
144
+ sources: [
145
+ { dir: extensionsDir, trust: 'install' },
146
+ ...(legacy ? [{ dir: legacyDir, trust: 'install' as const }] : []),
147
+ ],
148
+ },
149
+ builder: { dir: extensionsDir },
150
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0, hostname: chosen.host },
151
+ })),
152
+ ]
153
+ return {
154
+ root,
155
+ url: listen ? root.nodeServer.url : '',
156
+ async stop() {
157
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
158
+ },
159
+ }
160
+ }