acrylblends 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.d.ts +47 -2
- package/dist/index.js +2017 -32
- package/dist/index.js.map +1 -1
- package/docs/framework/agents/index.md +1 -1
- package/docs/framework/getting-started/desktop.md +23 -4
- package/docs/framework/guides/create-acrylblends.md +5 -3
- package/docs/framework/workshop/README.md +139 -153
- package/docs/framework/workshop/facilitator.md +69 -101
- package/legacy/README.md +9 -0
- package/legacy/agent/157ce3db1112/src/app.ts +98 -0
- package/legacy/agent/157ce3db1112/src/routes.ts +63 -0
- package/legacy/agent/24c681a00fca/src/app.ts +160 -0
- package/legacy/agent/24c681a00fca/src/routes.ts +83 -0
- package/legacy/agent/2e790da977b2/src/app.ts +98 -0
- package/legacy/agent/2e790da977b2/src/routes.ts +63 -0
- package/legacy/agent/6307045545dc/src/app.ts +168 -0
- package/legacy/agent/6307045545dc/src/routes.ts +83 -0
- package/legacy/agent/6a5fb9c15c53/src/app.ts +158 -0
- package/legacy/agent/6a5fb9c15c53/src/routes.ts +83 -0
- package/legacy/agent/71fb54a5a942/src/app.ts +111 -0
- package/legacy/agent/71fb54a5a942/src/routes.ts +63 -0
- package/legacy/agent/74ece148269c/src/app.ts +168 -0
- package/legacy/agent/74ece148269c/src/routes.ts +93 -0
- package/legacy/agent/855359304707/src/app.ts +168 -0
- package/legacy/agent/855359304707/src/routes.ts +126 -0
- package/legacy/agent/b5ef3c786ae4/src/app.ts +127 -0
- package/legacy/agent/b5ef3c786ae4/src/routes.ts +76 -0
- package/legacy/agent/c2f30958043b/src/app.ts +98 -0
- package/legacy/agent/c2f30958043b/src/routes.ts +53 -0
- package/legacy/blank/5a91c558af22/src/routes.ts +18 -0
- package/legacy/blank/9c650cf52bf3/src/routes.ts +14 -0
- package/legacy/blank/aea589283445/src/routes.ts +16 -0
- package/legacy/blank/b2cbb759c889/src/routes.ts +16 -0
- package/legacy/blank/d6d371c9a65e/src/routes.ts +18 -0
- package/legacy/chat/7076cc821e2c/src/app.ts +72 -0
- package/legacy/chat/7076cc821e2c/src/routes.ts +13 -0
- package/legacy/chat/7dbe6d5170df/src/app.ts +107 -0
- package/legacy/chat/7dbe6d5170df/src/routes.ts +19 -0
- package/legacy/chat/a9a9ab44c58e/src/app.ts +107 -0
- package/legacy/chat/a9a9ab44c58e/src/routes.ts +15 -0
- package/legacy/chat/f20ad9a51b06/src/app.ts +106 -0
- package/legacy/chat/f20ad9a51b06/src/routes.ts +15 -0
- package/legacy/layouts.json +524 -0
- package/legacy/nodejs/1d477f37aaf4/src/app.ts +92 -0
- package/legacy/nodejs/1d477f37aaf4/src/routes.ts +40 -0
- package/legacy/nodejs/b1bd9462496c/src/app.ts +61 -0
- package/legacy/nodejs/b1bd9462496c/src/routes.ts +37 -0
- package/legacy/nodejs/d5bab75076ef/src/app.ts +95 -0
- package/legacy/nodejs/d5bab75076ef/src/routes.ts +40 -0
- package/legacy/notes/4edc36949c15/src/app.ts +107 -0
- package/legacy/notes/4edc36949c15/src/routes.ts +14 -0
- package/legacy/notes/6cab2d09747c/src/app.ts +72 -0
- package/legacy/notes/6cab2d09747c/src/routes.ts +12 -0
- package/legacy/notes/7ca2dfed623a/src/app.ts +106 -0
- package/legacy/notes/7ca2dfed623a/src/routes.ts +14 -0
- package/legacy/todo/02e613b3625c/src/app.ts +71 -0
- package/legacy/todo/02e613b3625c/src/todo.ts +123 -0
- package/legacy/todo/45694a8d9e15/src/app.ts +71 -0
- package/legacy/todo/45694a8d9e15/src/todo.ts +96 -0
- package/legacy/todo/5923d3a6099f/src/app.ts +71 -0
- package/legacy/todo/5923d3a6099f/src/todo.ts +125 -0
- package/legacy/todo/849a18d66dc1/src/app.ts +91 -0
- package/legacy/todo/849a18d66dc1/src/todo.ts +125 -0
- package/legacy/todo/8e64211312f3/src/app.ts +71 -0
- package/legacy/todo/8e64211312f3/src/todo.ts +125 -0
- package/legacy/todo/b79078a87565/src/app.ts +71 -0
- package/legacy/todo/b79078a87565/src/todo.ts +125 -0
- package/lib/{audit-BFpULBCj.d.ts → audit-Bok_UNCn.d.ts} +26 -3
- package/lib/{client-modules-drj4mjhj.js → client-modules-Bkdr83jO.js} +1 -1
- package/lib/cordis/door.d.ts +87 -9
- package/lib/cordis/door.js +324 -60
- package/lib/cordis/inspect.js +1 -1
- package/lib/cordis/node-server.d.ts +101 -3
- package/lib/cordis/node-server.js +3 -3
- package/lib/cordis/router-http.d.ts +7 -1
- package/lib/cordis/router-http.js +44 -3
- package/lib/cordis/router.d.ts +1 -1
- package/lib/cordis/router.js +1 -1
- package/lib/cordis/state.js +1 -1
- package/lib/door/host.d.ts +2 -2
- package/lib/door/host.js +1 -1
- package/lib/door/page.d.ts +10 -1
- package/lib/door/page.js +46 -7
- package/lib/door/protocol.d.ts +1 -1
- package/lib/drivers.d.ts +10 -1
- package/lib/{index-DMOtA-NJ.d.ts → index-DGq7H0lb.d.ts} +11 -1
- package/lib/node-server.d.ts +7 -1
- package/lib/node-server.js +2 -2
- package/lib/{protocol-a2aqrp2X.d.ts → protocol-BeIYCgTo.d.ts} +4 -0
- package/lib/{snapshot-text-BwM6T2nG.js → snapshot-text-CXTnjxGx.js} +87 -7
- package/lib/src-CtkIX_Gg.js +427 -0
- package/lib/{src-BTBm4OPD.js → src-iBJ2ysV9.js} +77 -14
- package/lib/{test-websocket-BLeEvybj.js → test-websocket-CQOSvBth.js} +17 -7
- package/lib/{test-websocket-DyeS50Tr.d.ts → test-websocket-GUTAkFQT.d.ts} +3 -1
- package/package.json +2 -1
- package/plugins/acryl-door/plugin.json +3 -0
- package/plugins/acryl-door/src/cli/acryl.ts +63 -6
- package/plugins/acryl-door/src/host/channel.ts +86 -9
- package/plugins/acryl-door/src/host/chat-tools.ts +8 -6
- package/plugins/acryl-door/src/host/door-file.ts +7 -2
- package/plugins/acryl-door/src/host/door.ts +128 -24
- package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
- package/plugins/acryl-door/src/host/page-routes.ts +3 -1
- package/plugins/acryl-door/src/host/page-socket.ts +18 -8
- package/plugins/acryl-door/src/host/verbs.ts +14 -1
- package/plugins/acryl-door/src/index.ts +168 -30
- package/plugins/acryl-door/src/page/base.ts +32 -0
- package/plugins/acryl-door/src/page/channel.ts +3 -1
- package/plugins/acryl-door/src/page/driver/accessibility.ts +11 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +4 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +32 -4
- package/plugins/acryl-door/src/page/driver/snapshot.ts +5 -2
- package/plugins/acryl-door/src/page/index.ts +41 -2
- package/plugins/acryl-door/src/page/outline.ts +56 -0
- package/plugins/acryl-door/src/page/protocol.ts +4 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +2 -1
- package/plugins/acryl-node-server/src/adapter/listener.ts +4 -0
- package/plugins/acryl-node-server/src/adapter/raw.ts +18 -0
- package/plugins/acryl-node-server/src/adapter/websocket.ts +15 -9
- package/plugins/acryl-node-server/src/client-modules.ts +7 -1
- package/plugins/acryl-node-server/src/index.ts +304 -16
- package/plugins/acryl-node-server/src/upgrade-hub.ts +2 -2
- package/plugins/acryl-page/plugin.json +12 -0
- package/plugins/acryl-page/src/index.ts +163 -0
- package/plugins/acryl-router/src/service.ts +77 -14
- package/plugins/acryl-router-http/src/index.ts +15 -1
- package/plugins/acryl-router-http/src/serve.ts +31 -0
- package/plugins/acryl-web-server/plugin.json +14 -0
- package/plugins/acryl-web-server/src/index.ts +164 -0
- package/plugins/agent/agent.ts +38 -7
- package/plugins/agent/index.ts +1 -1
- package/plugins/agent/model.ts +10 -2
- package/plugins/agent/tools.ts +9 -3
- package/plugins/agent-panel/client/describe.ts +57 -0
- package/plugins/agent-panel/client/driver-pick.ts +22 -0
- package/plugins/agent-panel/client/frame.ts +51 -0
- package/plugins/agent-panel/client/main.ts +238 -26
- package/plugins/agent-panel/drivers.ts +7 -2
- package/plugins/agent-panel/headless.ts +5 -2
- package/plugins/agent-panel/index.ts +146 -57
- package/plugins/agent-panel/mount.ts +27 -0
- package/plugins/agent-panel/visitors.ts +231 -0
- package/plugins/builder/index.ts +13 -35
- package/plugins/builder/package.ts +186 -0
- package/plugins/builder/plugin.json +3 -1
- package/plugins/desktop/frame.ts +231 -0
- package/plugins/desktop/launch.ts +44 -9
- package/plugins/desktop/main.ts +100 -32
- package/plugins/desktop/window.ts +21 -4
- package/plugins/driver-pty/client/terminal.ts +4 -1
- package/plugins/driver-pty/index.ts +38 -11
- package/plugins/driver-pty/spawn.ts +38 -0
- package/plugins/driver-pty/terminal.ts +2 -0
- package/plugins/extensions/index.ts +47 -18
- package/plugins/extensions/plugin-store.ts +27 -0
- package/plugins/extensions/plugin.json +1 -1
- package/plugins/extensions/resolve.ts +13 -0
- package/plugins/extensions/stage.ts +15 -5
- package/plugins/feedback/client.d.ts +22 -0
- package/plugins/feedback/client.js +471 -0
- package/plugins/feedback/index.ts +143 -0
- package/plugins/feedback/plugin.json +11 -0
- package/plugins/feedback/store.ts +307 -0
- package/plugins/home/index.ts +122 -0
- package/plugins/home/plugin.json +11 -0
- package/plugins/instance/index.ts +6 -0
- package/plugins/manifest/app-plugins.ts +86 -0
- package/plugins/manifest/index.ts +12 -2
- package/plugins/manifest/plugin.json +1 -1
- package/plugins/manifest/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -0
- package/plugins/plugin-share/README.md +51 -0
- package/plugins/plugin-share/index.ts +467 -0
- package/plugins/plugin-share/plugin.json +15 -0
- package/plugins/plugin-share/publish.ts +65 -0
- package/plugins/plugin-share/pull-check.ts +129 -0
- package/plugins/plugin-share/pulled-records.ts +70 -0
- package/shared/AGENTS.core.md +36 -16
- package/shared/skills/acryl-add-ui/SKILL.md +19 -0
- package/shared/skills/acryl-app/SKILL.md +13 -0
- package/shared/skills/acryl-build-plugin/SKILL.md +22 -0
- package/shared/skills/acryl-change-plugin/SKILL.md +14 -0
- package/shared/skills/acryl-diagnose-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-fix-plugin/SKILL.md +13 -0
- package/shared/skills/acryl-remove-plugin/SKILL.md +12 -0
- package/shared/skills/acryl-share-plugin/SKILL.md +16 -0
- package/shared/skills/cordis-plugin/SKILL.md +26 -0
- package/templates/agent/.gitattributes +2 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/blend.yaml +6 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +154 -33
- package/templates/agent/src/frame.ts +17 -0
- package/templates/agent/src/main.ts +5 -3
- package/templates/agent/src/routes.ts +38 -19
- package/templates/agent/template.json +3 -0
- package/templates/agent/test/app.test.ts +45 -3
- package/templates/blank/.gitattributes +2 -0
- package/templates/blank/AGENTS.md +3 -2
- package/templates/blank/blend.yaml +11 -0
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +1 -0
- package/templates/blank/src/app.ts +100 -29
- package/templates/blank/src/door.ts +7 -0
- package/templates/blank/src/first-page.ts +5 -0
- package/templates/blank/src/frame.ts +17 -0
- package/templates/blank/src/routes.ts +65 -8
- package/templates/blank/template.json +14 -1
- package/templates/blank/test/app.test.ts +103 -16
- package/templates/chat/.gitattributes +2 -0
- package/templates/chat/blend.yaml +2 -1
- package/templates/chat/gitignore +1 -0
- package/templates/chat/package.json +1 -0
- package/templates/chat/src/app.ts +72 -22
- package/templates/chat/src/chat-app.ts +11 -2
- package/templates/chat/src/client/chat.tsx +4 -2
- package/templates/chat/src/frame.ts +17 -0
- package/templates/chat/src/page.tsx +6 -4
- package/templates/chat/src/routes.ts +14 -4
- package/templates/chat/src/runtime.ts +9 -3
- package/templates/chat/template.json +3 -1
- package/templates/chat/test/chat.test.ts +46 -1
- package/templates/chat/test/server.test.ts +17 -3
- package/templates/nodejs/.gitattributes +2 -0
- package/templates/nodejs/blend.yaml +2 -1
- package/templates/nodejs/gitignore +1 -0
- package/templates/nodejs/package.json +1 -0
- package/templates/nodejs/src/app.ts +76 -21
- package/templates/nodejs/src/frame.ts +17 -0
- package/templates/nodejs/src/page.tsx +3 -3
- package/templates/nodejs/src/routes.ts +32 -13
- package/templates/nodejs/template.json +3 -1
- package/templates/nodejs/test/app.test.ts +38 -3
- package/templates/nodejs/test/server.test.ts +17 -3
- package/templates/notes/.gitattributes +2 -0
- package/templates/notes/blend.yaml +2 -1
- package/templates/notes/gitignore +1 -0
- package/templates/notes/package.json +1 -0
- package/templates/notes/src/app.ts +72 -22
- package/templates/notes/src/frame.ts +17 -0
- package/templates/notes/src/notes-app.ts +24 -17
- package/templates/notes/src/routes.ts +11 -4
- package/templates/notes/src/runtime.ts +9 -3
- package/templates/notes/src/views.tsx +26 -17
- package/templates/notes/template.json +3 -1
- package/templates/notes/test/notes.test.ts +39 -1
- package/templates/notes/test/server.test.ts +17 -3
- package/templates/todo/.gitattributes +2 -0
- package/templates/todo/blend.yaml +3 -0
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +1 -0
- package/templates/todo/src/app.ts +81 -27
- package/templates/todo/src/door.ts +7 -0
- package/templates/todo/src/frame.ts +17 -0
- package/templates/todo/src/page.ts +13 -6
- package/templates/todo/src/todo.ts +18 -6
- package/templates/todo/template.json +11 -1
- package/templates/todo/test/todo.test.ts +67 -9
- package/lib/src-LX0E4ec4.js +0 -117
- 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
|
+
}
|