acrylblends 1.1.0 → 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 (241) hide show
  1. package/README.md +9 -0
  2. package/dist/index.d.ts +45 -1
  3. package/dist/index.js +1980 -27
  4. package/dist/index.js.map +1 -1
  5. package/docs/framework/agents/index.md +1 -1
  6. package/docs/framework/getting-started/desktop.md +23 -4
  7. package/docs/framework/guides/create-acrylblends.md +1 -1
  8. package/docs/framework/workshop/README.md +138 -152
  9. package/docs/framework/workshop/facilitator.md +69 -101
  10. package/legacy/README.md +9 -0
  11. package/legacy/agent/157ce3db1112/src/app.ts +98 -0
  12. package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
  13. package/legacy/agent/24c681a00fca/src/app.ts +160 -0
  14. package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
  15. package/legacy/agent/2e790da977b2/src/app.ts +98 -0
  16. package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
  17. package/legacy/agent/6307045545dc/src/app.ts +168 -0
  18. package/legacy/agent/6307045545dc/src/routes.ts +83 -0
  19. package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
  20. package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
  21. package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
  22. package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
  23. package/legacy/agent/74ece148269c/src/app.ts +168 -0
  24. package/legacy/agent/74ece148269c/src/routes.ts +93 -0
  25. package/legacy/agent/855359304707/src/app.ts +168 -0
  26. package/legacy/agent/855359304707/src/routes.ts +126 -0
  27. package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
  28. package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
  29. package/legacy/agent/c2f30958043b/src/app.ts +98 -0
  30. package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
  31. package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
  32. package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
  33. package/legacy/blank/aea589283445/src/routes.ts +16 -0
  34. package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
  35. package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
  36. package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
  37. package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
  38. package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
  39. package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
  40. package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
  41. package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
  42. package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
  43. package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
  44. package/legacy/layouts.json +524 -0
  45. package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
  46. package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
  47. package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
  48. package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
  49. package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
  50. package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
  51. package/legacy/notes/4edc36949c15/src/app.ts +107 -0
  52. package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
  53. package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
  54. package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
  55. package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
  56. package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
  57. package/legacy/todo/02e613b3625c/src/app.ts +71 -0
  58. package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
  59. package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
  60. package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
  61. package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
  62. package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
  63. package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
  64. package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
  65. package/legacy/todo/8e64211312f3/src/app.ts +71 -0
  66. package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
  67. package/legacy/todo/b79078a87565/src/app.ts +71 -0
  68. package/legacy/todo/b79078a87565/src/todo.ts +125 -0
  69. package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
  70. package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
  71. package/lib/cordis/door.d.ts +42 -7
  72. package/lib/cordis/door.js +204 -60
  73. package/lib/cordis/inspect.js +1 -1
  74. package/lib/cordis/node-server.d.ts +101 -3
  75. package/lib/cordis/node-server.js +3 -3
  76. package/lib/cordis/router-http.d.ts +7 -1
  77. package/lib/cordis/router-http.js +44 -3
  78. package/lib/cordis/router.d.ts +1 -1
  79. package/lib/cordis/router.js +1 -1
  80. package/lib/cordis/state.js +1 -1
  81. package/lib/door/host.d.ts +2 -2
  82. package/lib/door/host.js +1 -1
  83. package/lib/door/page.d.ts +10 -1
  84. package/lib/door/page.js +46 -7
  85. package/lib/door/protocol.d.ts +1 -1
  86. package/lib/drivers.d.ts +10 -1
  87. package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
  88. package/lib/node-server.d.ts +7 -1
  89. package/lib/node-server.js +2 -2
  90. package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
  91. package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
  92. package/lib/src-CtkIX_Gg.js +427 -0
  93. package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
  94. package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
  95. package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
  96. package/package.json +2 -1
  97. package/plugins/acryl-door/src/host/channel.ts +86 -9
  98. package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
  99. package/plugins/acryl-door/src/host/door-file.ts +7 -2
  100. package/plugins/acryl-door/src/host/door.ts +53 -23
  101. package/plugins/acryl-door/src/host/page-routes.ts +3 -1
  102. package/plugins/acryl-door/src/host/page-socket.ts +18 -8
  103. package/plugins/acryl-door/src/host/verbs.ts +3 -0
  104. package/plugins/acryl-door/src/index.ts +166 -29
  105. package/plugins/acryl-door/src/page/base.ts +32 -0
  106. package/plugins/acryl-door/src/page/channel.ts +3 -1
  107. package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
  108. package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
  109. package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
  110. package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
  111. package/plugins/acryl-door/src/page/index.ts +41 -2
  112. package/plugins/acryl-door/src/page/outline.ts +56 -0
  113. package/plugins/acryl-door/src/page/protocol.ts +4 -0
  114. package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
  115. package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
  116. package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
  117. package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
  118. package/plugins/acryl-node-server/src/client-modules.ts +7 -1
  119. package/plugins/acryl-node-server/src/index.ts +304 -16
  120. package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
  121. package/plugins/acryl-page/plugin.json +12 -0
  122. package/plugins/acryl-page/src/index.ts +163 -0
  123. package/plugins/acryl-router/src/service.ts +77 -14
  124. package/plugins/acryl-router-http/src/index.ts +15 -1
  125. package/plugins/acryl-router-http/src/serve.ts +31 -0
  126. package/plugins/acryl-web-server/plugin.json +14 -0
  127. package/plugins/acryl-web-server/src/index.ts +164 -0
  128. package/plugins/agent/agent.ts +38 -7
  129. package/plugins/agent/index.ts +1 -1
  130. package/plugins/agent/model.ts +10 -2
  131. package/plugins/agent/tools.ts +9 -3
  132. package/plugins/agent-panel/client/describe.ts +57 -0
  133. package/plugins/agent-panel/client/driver-pick.ts +22 -0
  134. package/plugins/agent-panel/client/frame.ts +51 -0
  135. package/plugins/agent-panel/client/main.ts +238 -26
  136. package/plugins/agent-panel/drivers.ts +7 -2
  137. package/plugins/agent-panel/headless.ts +5 -2
  138. package/plugins/agent-panel/index.ts +146 -57
  139. package/plugins/agent-panel/mount.ts +27 -0
  140. package/plugins/agent-panel/visitors.ts +231 -0
  141. package/plugins/desktop/frame.ts +231 -0
  142. package/plugins/desktop/launch.ts +44 -9
  143. package/plugins/desktop/main.ts +100 -32
  144. package/plugins/desktop/window.ts +21 -4
  145. package/plugins/driver-pty/client/terminal.ts +4 -1
  146. package/plugins/driver-pty/index.ts +38 -11
  147. package/plugins/driver-pty/spawn.ts +38 -0
  148. package/plugins/driver-pty/terminal.ts +2 -0
  149. package/plugins/extensions/index.ts +26 -1
  150. package/plugins/extensions/resolve.ts +13 -0
  151. package/plugins/feedback/client.d.ts +22 -0
  152. package/plugins/feedback/client.js +471 -0
  153. package/plugins/feedback/index.ts +143 -0
  154. package/plugins/feedback/plugin.json +11 -0
  155. package/plugins/feedback/store.ts +307 -0
  156. package/plugins/home/index.ts +122 -0
  157. package/plugins/home/plugin.json +11 -0
  158. package/plugins/instance/index.ts +6 -0
  159. package/plugins/manifest/app-plugins.ts +86 -0
  160. package/plugins/manifest/index.ts +12 -2
  161. package/plugins/manifest/snapshot.ts +275 -0
  162. package/plugins/openai/index.ts +64 -5
  163. package/plugins/openai/prompt.ts +23 -0
  164. package/shared/AGENTS.core.md +36 -16
  165. package/shared/skills/acryl-add-ui/SKILL.md +19 -0
  166. package/shared/skills/acryl-app/SKILL.md +13 -0
  167. package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
  168. package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
  169. package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
  170. package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
  171. package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
  172. package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
  173. package/shared/skills/cordis-plugin/SKILL.md +26 -0
  174. package/templates/agent/AGENTS.md +1 -1
  175. package/templates/agent/blend.yaml +4 -0
  176. package/templates/agent/gitignore +1 -0
  177. package/templates/agent/package.json +1 -0
  178. package/templates/agent/src/app.ts +150 -33
  179. package/templates/agent/src/frame.ts +17 -0
  180. package/templates/agent/src/main.ts +5 -3
  181. package/templates/agent/src/routes.ts +38 -19
  182. package/templates/agent/template.json +2 -0
  183. package/templates/agent/test/app.test.ts +45 -3
  184. package/templates/blank/AGENTS.md +3 -2
  185. package/templates/blank/blend.yaml +11 -0
  186. package/templates/blank/gitignore +1 -0
  187. package/templates/blank/package.json +1 -0
  188. package/templates/blank/src/app.ts +100 -29
  189. package/templates/blank/src/door.ts +7 -0
  190. package/templates/blank/src/first-page.ts +5 -0
  191. package/templates/blank/src/frame.ts +17 -0
  192. package/templates/blank/src/routes.ts +65 -8
  193. package/templates/blank/template.json +14 -1
  194. package/templates/blank/test/app.test.ts +103 -16
  195. package/templates/chat/blend.yaml +2 -1
  196. package/templates/chat/gitignore +1 -0
  197. package/templates/chat/package.json +1 -0
  198. package/templates/chat/src/app.ts +72 -22
  199. package/templates/chat/src/chat-app.ts +11 -2
  200. package/templates/chat/src/client/chat.tsx +4 -2
  201. package/templates/chat/src/frame.ts +17 -0
  202. package/templates/chat/src/page.tsx +6 -4
  203. package/templates/chat/src/routes.ts +14 -4
  204. package/templates/chat/src/runtime.ts +9 -3
  205. package/templates/chat/template.json +3 -1
  206. package/templates/chat/test/chat.test.ts +46 -1
  207. package/templates/chat/test/server.test.ts +17 -3
  208. package/templates/nodejs/blend.yaml +2 -1
  209. package/templates/nodejs/gitignore +1 -0
  210. package/templates/nodejs/package.json +1 -0
  211. package/templates/nodejs/src/app.ts +76 -21
  212. package/templates/nodejs/src/frame.ts +17 -0
  213. package/templates/nodejs/src/page.tsx +3 -3
  214. package/templates/nodejs/src/routes.ts +32 -13
  215. package/templates/nodejs/template.json +3 -1
  216. package/templates/nodejs/test/app.test.ts +38 -3
  217. package/templates/nodejs/test/server.test.ts +17 -3
  218. package/templates/notes/blend.yaml +2 -1
  219. package/templates/notes/gitignore +1 -0
  220. package/templates/notes/package.json +1 -0
  221. package/templates/notes/src/app.ts +72 -22
  222. package/templates/notes/src/frame.ts +17 -0
  223. package/templates/notes/src/notes-app.ts +24 -17
  224. package/templates/notes/src/routes.ts +11 -4
  225. package/templates/notes/src/runtime.ts +9 -3
  226. package/templates/notes/src/views.tsx +26 -17
  227. package/templates/notes/template.json +3 -1
  228. package/templates/notes/test/notes.test.ts +39 -1
  229. package/templates/notes/test/server.test.ts +17 -3
  230. package/templates/todo/blend.yaml +3 -0
  231. package/templates/todo/gitignore +1 -0
  232. package/templates/todo/package.json +1 -0
  233. package/templates/todo/src/app.ts +81 -27
  234. package/templates/todo/src/door.ts +7 -0
  235. package/templates/todo/src/frame.ts +17 -0
  236. package/templates/todo/src/page.ts +13 -6
  237. package/templates/todo/src/todo.ts +18 -6
  238. package/templates/todo/template.json +11 -1
  239. package/templates/todo/test/todo.test.ts +67 -9
  240. package/lib/src-LX0E4ec4.js +0 -117
  241. package/shared/SKILL.md +0 -14
@@ -0,0 +1,61 @@
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 { type AppInstance, instance } from '../plugins/instance/index.ts'
7
+ import {
8
+ blueprintsIn,
9
+ compose,
10
+ loadManifest,
11
+ type ParameterValue,
12
+ } from '../plugins/manifest/index.ts'
13
+ import { routes } from './routes.ts'
14
+
15
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
16
+ export const registry = { 'acryl-router': router, 'acryl-node-server': nodeServer, routes }
17
+
18
+ /** The manifest next to this app's source. */
19
+ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
20
+
21
+ /**
22
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
23
+ * `createApp` and the tests read it the same way.
24
+ */
25
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
26
+ return loadManifest(MANIFEST, {
27
+ known: Object.keys(registry),
28
+ getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
29
+ ...(values ? { values } : {}),
30
+ })
31
+ }
32
+
33
+ export interface App {
34
+ readonly root: Context
35
+ readonly url: string
36
+ stop(): Promise<void>
37
+ }
38
+
39
+ /** The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. */
40
+ export async function createApp(
41
+ chosen: AppInstance,
42
+ options: { values?: Readonly<Record<string, ParameterValue>> } = {},
43
+ ): Promise<App> {
44
+ const manifest = readManifest(options.values)
45
+ const root = new Context()
46
+ const first = root.plugin(instance, chosen)
47
+ await first.await()
48
+ const fibers: Fiber[] = [
49
+ first,
50
+ ...(await compose(root, manifest, registry, {
51
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 },
52
+ })),
53
+ ]
54
+ return {
55
+ root,
56
+ url: root.nodeServer.url,
57
+ async stop() {
58
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
59
+ },
60
+ }
61
+ }
@@ -0,0 +1,37 @@
1
+ import { createRequire } from 'node:module'
2
+ import { dirname, join } from 'node:path'
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import { Acryl } from '../plugins/acryl-router/src/index.ts'
5
+ import { clientModules } from '../plugins/acryl-node-server/src/client-modules.ts'
6
+ import { aboutPage, counterPage } from './page.tsx'
7
+
8
+ /** The browser builds of the DOM renderer and the typed client, shipped by the `acrylblends` package this app depends on. */
9
+ const runtimeDir = join(
10
+ dirname(createRequire(import.meta.url).resolve('acrylblends/package.json')),
11
+ 'lib',
12
+ 'browser',
13
+ )
14
+
15
+ export const APP_NAME = '__APP_NAME__'
16
+
17
+ /** The routes, in a file that does not import the Node adapter's server, so browser code can import the type: `import type { AppType } from '../routes.ts'`. */
18
+ export const pages = new Acryl()
19
+ .get('/', (c) => c.json({ app: APP_NAME, ok: true }))
20
+ .get('/health', (c) => c.json({ ok: true }))
21
+ .get('/about', (c) => c.html(aboutPage()))
22
+ .get('/counter', (c) => c.html(counterPage()))
23
+
24
+ export type AppType = typeof pages
25
+
26
+ /**
27
+ * The app's own plugin: the routes above, and the client components without a bundler (`src/client` is compiled when the browser asks; `npm run build`
28
+ * compiles it once for production, and `NODE_ENV=production` then serves only `dist/client`). The mounts are removed when the plugin is disposed.
29
+ */
30
+ export const routes = {
31
+ name: 'routes',
32
+ inject: ['router'],
33
+ apply(ctx: Context) {
34
+ ctx.router.route('/', pages)
35
+ ctx.router.route('/client', clientModules({ root: 'src/client', compiled: 'dist/client', runtimeDir }))
36
+ },
37
+ }
@@ -0,0 +1,95 @@
1
+ import { join } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
4
+ import * as door from '../plugins/acryl-door/src/index.ts'
5
+ import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
6
+ import * as router from '../plugins/acryl-router/src/index.ts'
7
+ import { extensions } from '../plugins/extensions/index.ts'
8
+ import { type AppInstance, instance } from '../plugins/instance/index.ts'
9
+ import {
10
+ blueprintsIn,
11
+ compose,
12
+ loadManifest,
13
+ type ParameterValue,
14
+ } from '../plugins/manifest/index.ts'
15
+ import { doorEnabled } from './door.ts'
16
+ import { routes } from './routes.ts'
17
+
18
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
19
+ export const registry = {
20
+ 'acryl-router': router,
21
+ 'acryl-node-server': nodeServer,
22
+ 'acryl-door': door,
23
+ extensions,
24
+ routes,
25
+ }
26
+
27
+ const projectRoot = fileURLToPath(new URL('..', import.meta.url))
28
+
29
+ /** The manifest next to this app's source. */
30
+ export const MANIFEST = join(projectRoot, 'blend.yaml')
31
+
32
+ /**
33
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
34
+ * `createApp` and the tests read it the same way.
35
+ */
36
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
37
+ return loadManifest(MANIFEST, {
38
+ known: Object.keys(registry),
39
+ getBlueprint: blueprintsIn(join(projectRoot, 'blueprints')),
40
+ ...(values ? { values } : {}),
41
+ })
42
+ }
43
+
44
+ /** The manifest without some rows: without its server (and so its door) the app is reached in process, through `root.router.request`; the door is also left out in production. */
45
+ function without<M extends { plugins: readonly { use: string }[] }>(
46
+ manifest: M,
47
+ ...uses: string[]
48
+ ): M {
49
+ return {
50
+ ...manifest,
51
+ plugins: manifest.plugins.map((row) =>
52
+ uses.includes(row.use) ? { ...row, disabled: true } : row,
53
+ ),
54
+ }
55
+ }
56
+
57
+ export interface App {
58
+ readonly root: Context
59
+ /** The address the server listens on; empty when the app was made with `listen: false`. */
60
+ readonly url: string
61
+ stop(): Promise<void>
62
+ }
63
+
64
+ /** The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. */
65
+ export async function createApp(
66
+ chosen: AppInstance,
67
+ options: { values?: Readonly<Record<string, ParameterValue>>; listen?: boolean } = {},
68
+ ): Promise<App> {
69
+ const read = readManifest(options.values)
70
+ const listen = options.listen
71
+ // Rows left out: the server (and the door, which needs it) when the app is reached in process; the door when it is switched off; live plugin installs in production.
72
+ const leaveOut = new Set<string>()
73
+ if (listen === false) leaveOut.add('acryl-node-server').add('acryl-door')
74
+ if (!doorEnabled()) leaveOut.add('acryl-door')
75
+ if (process.env.NODE_ENV === 'production') leaveOut.add('extensions')
76
+ const manifest = without(read, ...leaveOut)
77
+ const root = new Context()
78
+ const first = root.plugin(instance, chosen)
79
+ await first.await()
80
+ const fibers: Fiber[] = [
81
+ first,
82
+ ...(await compose(root, manifest, registry, {
83
+ // What is installed while the app runs lives in plugins/, next to the plugins copied from the library (plugins/library.json lists those, so they are not installed twice).
84
+ extensions: { sources: [{ dir: join(projectRoot, 'plugins'), trust: 'install' }] },
85
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 },
86
+ })),
87
+ ]
88
+ return {
89
+ root,
90
+ url: listen === false ? '' : root.nodeServer.url,
91
+ async stop() {
92
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
93
+ },
94
+ }
95
+ }
@@ -0,0 +1,40 @@
1
+ import { createRequire } from 'node:module'
2
+ import { dirname, join } from 'node:path'
3
+ import type { Context } from '@deepseek-ai/cordis'
4
+ import { clientModules } from '../plugins/acryl-node-server/src/client-modules.ts'
5
+ import { Acryl } from '../plugins/acryl-router/src/index.ts'
6
+ import { aboutPage, counterPage } from './page.tsx'
7
+
8
+ /** The browser builds of the DOM renderer and the typed client, shipped by the `acrylblends` package this app depends on. */
9
+ const runtimeDir = join(
10
+ dirname(createRequire(import.meta.url).resolve('acrylblends/package.json')),
11
+ 'lib',
12
+ 'browser',
13
+ )
14
+
15
+ export const APP_NAME = '__APP_NAME__'
16
+
17
+ /** The routes, in a file that does not import the Node adapter's server, so browser code can import the type: `import type { AppType } from '../routes.ts'`. */
18
+ export const pages = new Acryl()
19
+ .get('/', (c) => c.json({ app: APP_NAME, ok: true }))
20
+ .get('/health', (c) => c.json({ ok: true }))
21
+ .get('/about', (c) => c.html(aboutPage()))
22
+ .get('/counter', (c) => c.html(counterPage()))
23
+
24
+ export type AppType = typeof pages
25
+
26
+ /**
27
+ * The app's own plugin: the routes above, and the client components without a bundler (`src/client` is compiled when the browser asks; `npm run build`
28
+ * compiles it once for production, and `NODE_ENV=production` then serves only `dist/client`). The mounts are removed when the plugin is disposed.
29
+ */
30
+ export const routes = {
31
+ name: 'routes',
32
+ inject: ['router'],
33
+ apply(ctx: Context) {
34
+ ctx.router.route('/', pages)
35
+ ctx.router.route(
36
+ '/client',
37
+ clientModules({ root: 'src/client', compiled: 'dist/client', runtimeDir }),
38
+ )
39
+ },
40
+ }
@@ -0,0 +1,107 @@
1
+ import { isAbsolute, join, resolve } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
4
+ import * as db from '../plugins/acryl-db-sqlite/src/index.ts'
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 { extensions } from '../plugins/extensions/index.ts'
9
+ import { type AppInstance, instance } from '../plugins/instance/index.ts'
10
+ import {
11
+ blueprintsIn,
12
+ compose,
13
+ loadManifest,
14
+ type ParameterValue,
15
+ } from '../plugins/manifest/index.ts'
16
+ import { doorEnabled } from './door.ts'
17
+ import { routes } from './routes.ts'
18
+
19
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
20
+ export const registry = {
21
+ 'acryl-router': router,
22
+ 'acryl-db-sqlite': db,
23
+ 'acryl-node-server': nodeServer,
24
+ 'acryl-door': door,
25
+ extensions,
26
+ routes,
27
+ }
28
+
29
+ const projectRoot = fileURLToPath(new URL('..', import.meta.url))
30
+
31
+ /** The manifest next to this app's source. */
32
+ export const MANIFEST = join(projectRoot, 'blend.yaml')
33
+
34
+ /** The manifest, resolved. `createApp` and the tests read it the same way. */
35
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
36
+ return loadManifest(MANIFEST, {
37
+ known: Object.keys(registry),
38
+ getBlueprint: blueprintsIn(join(projectRoot, 'blueprints')),
39
+ ...(values ? { values } : {}),
40
+ })
41
+ }
42
+
43
+ /** The manifest without some rows: without its server (and so its door) the app is reached in process, through `root.router.request`; the door is also left out in production. */
44
+ function without<M extends { plugins: readonly { use: string }[] }>(
45
+ manifest: M,
46
+ ...uses: string[]
47
+ ): M {
48
+ return {
49
+ ...manifest,
50
+ plugins: manifest.plugins.map((row) =>
51
+ uses.includes(row.use) ? { ...row, disabled: true } : row,
52
+ ),
53
+ }
54
+ }
55
+
56
+ export interface App {
57
+ readonly root: Context
58
+ /** The address the server listens on; empty when the app was made with `listen: false`. */
59
+ readonly url: string
60
+ stop(): Promise<void>
61
+ }
62
+
63
+ export interface AppOptions {
64
+ readonly values?: Readonly<Record<string, ParameterValue>>
65
+ /** The SQLite file, relative to the app folder; an absolute path is used as it is (tests give a file in a throwaway home). Default: `data/notes.sqlite`. */
66
+ readonly database?: string
67
+ /** 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. */
68
+ readonly listen?: boolean
69
+ }
70
+
71
+ /**
72
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The `db` row opens the database and applies
73
+ * `migrations/`; the one place that names the SQLite provider is this file (`acrylblends check-deps`), so a different provider is a change here.
74
+ */
75
+ export async function createApp(chosen: AppInstance, options: AppOptions = {}): Promise<App> {
76
+ const read = readManifest(options.values)
77
+ const listen = options.listen
78
+ // Rows left out: the server (and the door, which needs it) when the app is reached in process; the door when it is switched off; live plugin installs in production.
79
+ const leaveOut = new Set<string>()
80
+ if (listen === false) leaveOut.add('acryl-node-server').add('acryl-door')
81
+ if (!doorEnabled()) leaveOut.add('acryl-door')
82
+ if (process.env.NODE_ENV === 'production') leaveOut.add('extensions')
83
+ const manifest = without(read, ...leaveOut)
84
+ const file = options.database ?? 'data/notes.sqlite'
85
+ const root = new Context()
86
+ const first = root.plugin(instance, chosen)
87
+ await first.await()
88
+ const fibers: Fiber[] = [
89
+ first,
90
+ ...(await compose(root, manifest, registry, {
91
+ db: {
92
+ path: isAbsolute(file) ? file : resolve(projectRoot, file),
93
+ migrations: join(projectRoot, 'migrations'),
94
+ },
95
+ // What is installed while the app runs lives in plugins/, next to the plugins copied from the library (plugins/library.json lists those, so they are not installed twice).
96
+ extensions: { sources: [{ dir: join(projectRoot, 'plugins'), trust: 'install' }] },
97
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 },
98
+ })),
99
+ ]
100
+ return {
101
+ root,
102
+ url: listen === false ? '' : root.nodeServer.url,
103
+ async stop() {
104
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
105
+ },
106
+ }
107
+ }
@@ -0,0 +1,14 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type {} from '../plugins/acryl-db/src/cordis.ts'
3
+ import { createApp } from './notes-app.ts'
4
+ import { createClient } from './runtime.ts'
5
+
6
+ /** The app's own plugin: it builds the app on the `db` port and mounts it on the router. The mount is removed when the plugin is disposed. */
7
+ export const routes = {
8
+ name: 'routes',
9
+ inject: ['router', 'db'],
10
+ apply(ctx: Context) {
11
+ ctx.router.route('/', createApp(ctx.db))
12
+ ctx.router.route('/client', createClient())
13
+ },
14
+ }
@@ -0,0 +1,72 @@
1
+ import { isAbsolute, join, resolve } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
4
+ import * as db from '../plugins/acryl-db-sqlite/src/index.ts'
5
+ import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
6
+ import * as router from '../plugins/acryl-router/src/index.ts'
7
+ import { type AppInstance, instance } from '../plugins/instance/index.ts'
8
+ import {
9
+ blueprintsIn,
10
+ compose,
11
+ loadManifest,
12
+ type ParameterValue,
13
+ } from '../plugins/manifest/index.ts'
14
+ import { routes } from './routes.ts'
15
+
16
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
17
+ export const registry = { 'acryl-router': router, 'acryl-db-sqlite': db, 'acryl-node-server': nodeServer, routes }
18
+
19
+ const projectRoot = fileURLToPath(new URL('..', import.meta.url))
20
+
21
+ /** The manifest next to this app's source. */
22
+ export const MANIFEST = join(projectRoot, 'blend.yaml')
23
+
24
+ /** The manifest, resolved. `createApp` and the tests read it the same way. */
25
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
26
+ return loadManifest(MANIFEST, {
27
+ known: Object.keys(registry),
28
+ getBlueprint: blueprintsIn(join(projectRoot, 'blueprints')),
29
+ ...(values ? { values } : {}),
30
+ })
31
+ }
32
+
33
+ export interface App {
34
+ readonly root: Context
35
+ readonly url: string
36
+ stop(): Promise<void>
37
+ }
38
+
39
+ export interface AppOptions {
40
+ readonly values?: Readonly<Record<string, ParameterValue>>
41
+ /** The SQLite file, relative to the app folder; `:memory:` for a database that lives and dies with the process (tests). Default: ${'data/notes.sqlite'}. */
42
+ readonly database?: string
43
+ }
44
+
45
+ /**
46
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The `db` row opens the database and applies
47
+ * `migrations/`; the one place that names the SQLite provider is this file (`acrylblends check-deps`), so a different provider is a change here.
48
+ */
49
+ export async function createApp(chosen: AppInstance, options: AppOptions = {}): Promise<App> {
50
+ const manifest = readManifest(options.values)
51
+ const file = options.database ?? 'data/notes.sqlite'
52
+ const root = new Context()
53
+ const first = root.plugin(instance, chosen)
54
+ await first.await()
55
+ const fibers: Fiber[] = [
56
+ first,
57
+ ...(await compose(root, manifest, registry, {
58
+ db: {
59
+ path: file === ':memory:' || isAbsolute(file) ? file : resolve(projectRoot, file),
60
+ migrations: join(projectRoot, 'migrations'),
61
+ },
62
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 },
63
+ })),
64
+ ]
65
+ return {
66
+ root,
67
+ url: root.nodeServer.url,
68
+ async stop() {
69
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
70
+ },
71
+ }
72
+ }
@@ -0,0 +1,12 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type {} from '../plugins/acryl-db/src/cordis.ts'
3
+ import { createApp } from './notes-app.ts'
4
+
5
+ /** The app's own plugin: it builds the app on the `db` port and mounts it on the router. The mount is removed when the plugin is disposed. */
6
+ export const routes = {
7
+ name: 'routes',
8
+ inject: ['router', 'db'],
9
+ apply(ctx: Context) {
10
+ ctx.router.route('/', createApp(ctx.db))
11
+ },
12
+ }
@@ -0,0 +1,106 @@
1
+ import { isAbsolute, join, resolve } from 'node:path'
2
+ import { fileURLToPath } from 'node:url'
3
+ import { Context, type Fiber } from '@deepseek-ai/cordis'
4
+ import * as db from '../plugins/acryl-db-sqlite/src/index.ts'
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 { type AppInstance, instance } from '../plugins/instance/index.ts'
9
+ import {
10
+ blueprintsIn,
11
+ compose,
12
+ loadManifest,
13
+ type ParameterValue,
14
+ } from '../plugins/manifest/index.ts'
15
+ import { doorEnabled } from './door.ts'
16
+ import { routes } from './routes.ts'
17
+
18
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
19
+ export const registry = {
20
+ 'acryl-router': router,
21
+ 'acryl-db-sqlite': db,
22
+ 'acryl-node-server': nodeServer,
23
+ 'acryl-door': door,
24
+ routes,
25
+ }
26
+
27
+ const projectRoot = fileURLToPath(new URL('..', import.meta.url))
28
+
29
+ /** The manifest next to this app's source. */
30
+ export const MANIFEST = join(projectRoot, 'blend.yaml')
31
+
32
+ /** The manifest, resolved. `createApp` and the tests read it the same way. */
33
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
34
+ return loadManifest(MANIFEST, {
35
+ known: Object.keys(registry),
36
+ getBlueprint: blueprintsIn(join(projectRoot, 'blueprints')),
37
+ ...(values ? { values } : {}),
38
+ })
39
+ }
40
+
41
+ /** The manifest without some rows: without its server (and so its door) the app is reached in process, through `root.router.request`; the door is also left out in production. */
42
+ function without<M extends { plugins: readonly { use: string }[] }>(
43
+ manifest: M,
44
+ ...uses: string[]
45
+ ): M {
46
+ return {
47
+ ...manifest,
48
+ plugins: manifest.plugins.map((row) =>
49
+ uses.includes(row.use) ? { ...row, disabled: true } : row,
50
+ ),
51
+ }
52
+ }
53
+
54
+ export interface App {
55
+ readonly root: Context
56
+ /** The address the server listens on; empty when the app was made with `listen: false`. */
57
+ readonly url: string
58
+ stop(): Promise<void>
59
+ }
60
+
61
+ export interface AppOptions {
62
+ readonly values?: Readonly<Record<string, ParameterValue>>
63
+ /** The SQLite file, relative to the app folder; an absolute path is used as it is (tests give a file in a throwaway home). Default: `data/notes.sqlite`. */
64
+ readonly database?: string
65
+ /** 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. */
66
+ readonly listen?: boolean
67
+ }
68
+
69
+ /**
70
+ * The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. The `db` row opens the database and applies
71
+ * `migrations/`; the one place that names the SQLite provider is this file (`acrylblends check-deps`), so a different provider is a change here.
72
+ */
73
+ export async function createApp(chosen: AppInstance, options: AppOptions = {}): Promise<App> {
74
+ const manifest = readManifest(options.values)
75
+ const listen = options.listen
76
+ const file = options.database ?? 'data/notes.sqlite'
77
+ const root = new Context()
78
+ const first = root.plugin(instance, chosen)
79
+ await first.await()
80
+ const fibers: Fiber[] = [
81
+ first,
82
+ ...(await compose(
83
+ root,
84
+ listen === false
85
+ ? without(manifest, 'acryl-node-server', 'acryl-door')
86
+ : doorEnabled()
87
+ ? manifest
88
+ : without(manifest, 'acryl-door'),
89
+ registry,
90
+ {
91
+ db: {
92
+ path: isAbsolute(file) ? file : resolve(projectRoot, file),
93
+ migrations: join(projectRoot, 'migrations'),
94
+ },
95
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 },
96
+ },
97
+ )),
98
+ ]
99
+ return {
100
+ root,
101
+ url: listen === false ? '' : root.nodeServer.url,
102
+ async stop() {
103
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
104
+ },
105
+ }
106
+ }
@@ -0,0 +1,14 @@
1
+ import type { Context } from '@deepseek-ai/cordis'
2
+ import type {} from '../plugins/acryl-db/src/cordis.ts'
3
+ import { createApp } from './notes-app.ts'
4
+ import { createClient } from './runtime.ts'
5
+
6
+ /** The app's own plugin: it builds the app on the `db` port and mounts it on the router. The mount is removed when the plugin is disposed. */
7
+ export const routes = {
8
+ name: 'routes',
9
+ inject: ['router', 'db'],
10
+ apply(ctx: Context) {
11
+ ctx.router.route('/', createApp(ctx.db))
12
+ ctx.router.route('/client', createClient())
13
+ },
14
+ }
@@ -0,0 +1,71 @@
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 { type AppInstance, instance } from './plugins/instance/index.ts'
8
+ import {
9
+ blueprintsIn,
10
+ compose,
11
+ loadManifest,
12
+ type ParameterValue,
13
+ } from './plugins/manifest/index.ts'
14
+ import * as todo from './todo.ts'
15
+
16
+ /** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
17
+ export const registry = {
18
+ 'acryl-router': router,
19
+ 'acryl-state': state,
20
+ todo,
21
+ 'acryl-node-server': nodeServer,
22
+ }
23
+
24
+ export const APP_NAME = '__APP_NAME__'
25
+
26
+ /** The manifest next to this app's source. */
27
+ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'blend.yaml')
28
+
29
+ /**
30
+ * The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
31
+ * `createApp` and the tests read it the same way.
32
+ */
33
+ export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
34
+ return loadManifest(MANIFEST, {
35
+ known: Object.keys(registry),
36
+ getBlueprint: blueprintsIn(join(fileURLToPath(new URL('..', import.meta.url)), 'blueprints')),
37
+ ...(values ? { values } : {}),
38
+ })
39
+ }
40
+
41
+ export interface App {
42
+ readonly root: Context
43
+ readonly url: string
44
+ stop(): Promise<void>
45
+ }
46
+
47
+ /** The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. */
48
+ export async function createApp(
49
+ chosen: AppInstance,
50
+ options: { values?: Readonly<Record<string, ParameterValue>> } = {},
51
+ ): Promise<App> {
52
+ const manifest = readManifest(options.values)
53
+ const root = new Context()
54
+ const first = root.plugin(instance, chosen)
55
+ await first.await()
56
+ const fibers: Fiber[] = [
57
+ first,
58
+ ...(await compose(root, manifest, registry, {
59
+ // Runtime values the manifest cannot hold: where the list is kept (in the app's home) and where the server listens.
60
+ todo: { file: join(chosen.home, 'todo.json') },
61
+ server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 },
62
+ })),
63
+ ]
64
+ return {
65
+ root,
66
+ url: root.nodeServer.url,
67
+ async stop() {
68
+ for (const fiber of [...fibers].reverse()) await fiber.dispose()
69
+ },
70
+ }
71
+ }