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
|
@@ -1,22 +1,39 @@
|
|
|
1
1
|
import { join } from 'node:path'
|
|
2
2
|
import { fileURLToPath } from 'node:url'
|
|
3
3
|
import { Context, type Fiber } from '@deepseek-ai/cordis'
|
|
4
|
+
import * as door from '../plugins/acryl-door/src/index.ts'
|
|
4
5
|
import * as nodeServer from '../plugins/acryl-node-server/src/index.ts'
|
|
5
6
|
import * as router from '../plugins/acryl-router/src/index.ts'
|
|
7
|
+
import { extensions } from '../plugins/extensions/index.ts'
|
|
8
|
+
import * as home from '../plugins/home/index.ts'
|
|
6
9
|
import { type AppInstance, instance } from '../plugins/instance/index.ts'
|
|
7
10
|
import {
|
|
11
|
+
appPluginNames,
|
|
8
12
|
blueprintsIn,
|
|
9
13
|
compose,
|
|
14
|
+
loadAppPlugins,
|
|
10
15
|
loadManifest,
|
|
11
16
|
type ParameterValue,
|
|
12
17
|
} from '../plugins/manifest/index.ts'
|
|
13
|
-
import {
|
|
18
|
+
import { doorEnabled } from './door.ts'
|
|
19
|
+
import { FRAME_MOUNT_PREFIX, routes } from './routes.ts'
|
|
14
20
|
|
|
15
|
-
/** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
|
|
16
|
-
export const registry = {
|
|
21
|
+
/** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them, and adds the app's own plugins from `plugins/` by name. */
|
|
22
|
+
export const registry = {
|
|
23
|
+
'acryl-router': router,
|
|
24
|
+
'acryl-node-server': nodeServer,
|
|
25
|
+
'acryl-door': door,
|
|
26
|
+
home,
|
|
27
|
+
extensions,
|
|
28
|
+
routes,
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const projectRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
17
32
|
|
|
18
33
|
/** The manifest next to this app's source. */
|
|
19
|
-
export const MANIFEST = join(
|
|
34
|
+
export const MANIFEST = join(projectRoot, 'blend.yaml')
|
|
35
|
+
/** The app's plugins: the library copies `registry` imports, and the app's own plugins (a feature each), which `blend.yaml` mounts by their folder name. */
|
|
36
|
+
const PLUGINS_DIR = join(projectRoot, 'plugins')
|
|
20
37
|
|
|
21
38
|
/**
|
|
22
39
|
* The app's manifest, resolved. A Blend finds its Blueprint in `blueprints/<id>.yaml`; an app that is itself a Blueprint never looks.
|
|
@@ -24,18 +41,21 @@ export const MANIFEST = join(fileURLToPath(new URL('..', import.meta.url)), 'ble
|
|
|
24
41
|
*/
|
|
25
42
|
export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
|
|
26
43
|
return loadManifest(MANIFEST, {
|
|
27
|
-
known: Object.keys(registry),
|
|
28
|
-
getBlueprint: blueprintsIn(join(
|
|
44
|
+
known: [...Object.keys(registry), ...appPluginNames(PLUGINS_DIR)],
|
|
45
|
+
getBlueprint: blueprintsIn(join(projectRoot, 'blueprints')),
|
|
29
46
|
...(values ? { values } : {}),
|
|
30
47
|
})
|
|
31
48
|
}
|
|
32
49
|
|
|
33
|
-
/** The manifest without its server
|
|
34
|
-
function
|
|
50
|
+
/** 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. */
|
|
51
|
+
function without<M extends { plugins: readonly { use: string }[] }>(
|
|
52
|
+
manifest: M,
|
|
53
|
+
...uses: string[]
|
|
54
|
+
): M {
|
|
35
55
|
return {
|
|
36
56
|
...manifest,
|
|
37
57
|
plugins: manifest.plugins.map((row) =>
|
|
38
|
-
row.use
|
|
58
|
+
uses.includes(row.use) ? { ...row, disabled: true } : row,
|
|
39
59
|
),
|
|
40
60
|
}
|
|
41
61
|
}
|
|
@@ -47,30 +67,81 @@ export interface App {
|
|
|
47
67
|
stop(): Promise<void>
|
|
48
68
|
}
|
|
49
69
|
|
|
50
|
-
|
|
51
|
-
|
|
70
|
+
export interface AppOptions {
|
|
71
|
+
readonly values?: Readonly<Record<string, ParameterValue>>
|
|
72
|
+
/** The path the app is mounted at; absent on its own server, where it keeps the root, and {@link FRAME_MOUNT_PREFIX} in the frame. */
|
|
73
|
+
readonly prefix?: string
|
|
74
|
+
/** 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. */
|
|
75
|
+
readonly listen?: boolean
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The rows the frame host provides: the app contributes only the plugins that are its own. */
|
|
79
|
+
const HOST_ROWS = [
|
|
80
|
+
'instance',
|
|
81
|
+
'acryl-router',
|
|
82
|
+
'acryl-node-server',
|
|
83
|
+
'acryl-web-server',
|
|
84
|
+
'acryl-page',
|
|
85
|
+
'manifest',
|
|
86
|
+
'save',
|
|
87
|
+
'acryl-door',
|
|
88
|
+
]
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* The app's rows, composed into `ctx`: the app's own server calls this from `createApp`, and the generated frame layer mounts it as one row, so the frame
|
|
92
|
+
* runs the app exactly as the app's own server does (same rows, same order, same config). `host: true` leaves the infrastructure rows to the frame host,
|
|
93
|
+
* which is where the door lives too, and mounts the app at {@link FRAME_MOUNT_PREFIX}.
|
|
94
|
+
*/
|
|
95
|
+
export async function composeInto(
|
|
96
|
+
ctx: Context,
|
|
52
97
|
chosen: AppInstance,
|
|
53
|
-
options: {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
98
|
+
options: AppOptions & { readonly host?: boolean } = {},
|
|
99
|
+
): Promise<Fiber[]> {
|
|
100
|
+
const read = readManifest(options.values)
|
|
101
|
+
const own = options.host !== true && options.listen !== false
|
|
102
|
+
// The frame host mounts the app under its own prefix; the app's own server keeps the root.
|
|
103
|
+
const prefix = options.prefix ?? (options.host === true ? FRAME_MOUNT_PREFIX : undefined)
|
|
104
|
+
// Rows left out: the server (and the door, which needs it) when the app is reached in process or inside the frame host; the door when it is switched
|
|
105
|
+
// off.
|
|
106
|
+
const leaveOut = new Set<string>(options.host === true ? HOST_ROWS : [])
|
|
107
|
+
if (!own) leaveOut.add('acryl-node-server').add('acryl-door')
|
|
108
|
+
if (!doorEnabled()) leaveOut.add('acryl-door')
|
|
109
|
+
const manifest = without(read, ...leaveOut)
|
|
110
|
+
// The rows that name one of the app's own plugin folders (a feature: plugins/<name>/ and its row) rather than a registry import. Each one that runs
|
|
111
|
+
// is imported here; all of them, off or on, belong to their row, so the extensions row never starts them a second time.
|
|
112
|
+
const ownRows = [
|
|
113
|
+
...new Set(read.plugins.map((row) => row.use).filter((use) => !Object.hasOwn(registry, use))),
|
|
114
|
+
]
|
|
115
|
+
const ownPlugins = await loadAppPlugins(
|
|
116
|
+
PLUGINS_DIR,
|
|
117
|
+
ownRows.filter((use) => manifest.plugins.some((row) => row.use === use && !row.disabled)),
|
|
118
|
+
)
|
|
119
|
+
return compose(
|
|
120
|
+
ctx,
|
|
121
|
+
manifest,
|
|
122
|
+
{ ...registry, ...ownPlugins },
|
|
123
|
+
{
|
|
124
|
+
// The routes row mounts the pages (and the client modules) at the prefix the frame host chose.
|
|
125
|
+
routes: { ...(prefix === undefined ? {} : { prefix }) },
|
|
126
|
+
// The app's own plugins live in plugins/, next to the plugins copied from the library (plugins/library.json lists those, so they are not installed twice).
|
|
127
|
+
// In production they start read-only from where they are deployed: the app keeps its features, and nothing can be installed while it runs.
|
|
128
|
+
extensions: {
|
|
129
|
+
...(process.env.NODE_ENV === 'production' ? { readOnly: true } : {}),
|
|
130
|
+
sources: [{ dir: PLUGINS_DIR, trust: 'install' }],
|
|
131
|
+
rows: ownRows,
|
|
132
|
+
},
|
|
133
|
+
// Only the app's own server listens; the frame host owns the server and its port.
|
|
134
|
+
...(own ? { server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 } } : {}),
|
|
135
|
+
},
|
|
136
|
+
)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. */
|
|
140
|
+
export async function createApp(chosen: AppInstance, options: AppOptions = {}): Promise<App> {
|
|
60
141
|
const root = new Context()
|
|
61
142
|
const first = root.plugin(instance, chosen)
|
|
62
143
|
await first.await()
|
|
63
|
-
const fibers: Fiber[] = [
|
|
64
|
-
first,
|
|
65
|
-
...(await compose(
|
|
66
|
-
root,
|
|
67
|
-
options.listen === false ? withoutServer(manifest) : manifest,
|
|
68
|
-
registry,
|
|
69
|
-
{
|
|
70
|
-
server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 },
|
|
71
|
-
},
|
|
72
|
-
)),
|
|
73
|
-
]
|
|
144
|
+
const fibers: Fiber[] = [first, ...(await composeInto(root, chosen, options))]
|
|
74
145
|
return {
|
|
75
146
|
root,
|
|
76
147
|
url: options.listen === false ? '' : root.nodeServer.url,
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The door (`plugins/acryl-door`) lets an agent working in this folder look at the running page and act on it, through `.acryl/bin/acryl`. It is on in development
|
|
3
|
+
* and off in production (`NODE_ENV=production`) and when `ACRYL_DOOR=off`: `src/app.ts` leaves its row out then, and the pages leave its script tag out. One process
|
|
4
|
+
* at a time serves a folder's door, so the tests that start the real server set `ACRYL_DOOR=off` and a second `npm start` beside a running one does not fail on it.
|
|
5
|
+
*/
|
|
6
|
+
export const doorEnabled = (): boolean =>
|
|
7
|
+
process.env.NODE_ENV !== 'production' && process.env.ACRYL_DOOR !== 'off'
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The sentence the first screen carries. The desktop door's frame probe imports this same value, so the page's text and the probe's expectation can never
|
|
3
|
+
* drift apart again (the probe outlived the JSON root it was written for by one template change).
|
|
4
|
+
*/
|
|
5
|
+
export const FIRST_PAGE_MARKER = 'This is the page the ACRYL frame opens first'
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The app as the ACRYL frame hosts it (`npm run dev`): the generated layer mounts this row, which claims {@link FRAME_MOUNT_PREFIX} on the frame's web
|
|
3
|
+
* server. The app's own rows (the generated `app-plugins` row, which calls `composeInto`) mount the app at that prefix, so its pages and its client
|
|
4
|
+
* component answer at `/app`. Nothing here runs in production: `npm start` composes the app on its own server (no `routerHttp` service, so this row is not
|
|
5
|
+
* mounted there), where the app keeps the root as it always did.
|
|
6
|
+
*/
|
|
7
|
+
import type { Context } from '@deepseek-ai/cordis'
|
|
8
|
+
import { FRAME_MOUNT_PREFIX } from './routes.ts'
|
|
9
|
+
|
|
10
|
+
export const name = 'app-routes'
|
|
11
|
+
|
|
12
|
+
/** The frame always has it: its layer carries the router and the web adapter. */
|
|
13
|
+
export const inject = ['routerHttp']
|
|
14
|
+
|
|
15
|
+
export function apply(ctx: Context): void {
|
|
16
|
+
ctx.routerHttp.mount(FRAME_MOUNT_PREFIX)
|
|
17
|
+
}
|
|
@@ -1,18 +1,75 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
2
|
import { Acryl } from '../plugins/acryl-router/src/index.ts'
|
|
3
|
+
import { type Home, renderWidgets } from '../plugins/home/index.ts'
|
|
3
4
|
import { aboutPage } from './about.tsx'
|
|
5
|
+
import { FIRST_PAGE_MARKER } from './first-page.ts'
|
|
4
6
|
|
|
5
7
|
export const APP_NAME = '__APP_NAME__'
|
|
6
8
|
|
|
7
|
-
/**
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
/**
|
|
10
|
+
* The only place the app's mount prefix is named. On its own server (production, `npm start`) the app mounts at the root and
|
|
11
|
+
* its public URLs are `/`, `/about`, `/health`; when the ACRYL frame hosts it (`npm run dev`) the routes mount here, and the
|
|
12
|
+
* frame opens its tile at `/app/`. Every page link and asset URL this template writes is derived from the prefix it is
|
|
13
|
+
* mounted at, so the same source serves both.
|
|
14
|
+
*/
|
|
15
|
+
export const FRAME_MOUNT_PREFIX = '/app'
|
|
16
|
+
|
|
17
|
+
/** The scaffold's name comes from the folder, so the first screen escapes it like any other user text. */
|
|
18
|
+
const escapeHtml = (text: string): string =>
|
|
19
|
+
text
|
|
20
|
+
.replaceAll('&', '&')
|
|
21
|
+
.replaceAll('<', '<')
|
|
22
|
+
.replaceAll('>', '>')
|
|
23
|
+
.replaceAll('"', '"')
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* The first screen: the page a browser gets at the root, which is what the ACRYL frame's first tile shows. A blank app whose root answered JSON to
|
|
27
|
+
* every client gave the tile Chrome's JSON viewer, nearly invisible in the frame's dark surface (owner review, 2026-11-02); the root now answers a page
|
|
28
|
+
* to a browser and the JSON hello to everything else, the way the agent template does. The About link is relative, so it works under the frame's /app
|
|
29
|
+
* mount and on the app's own server alike.
|
|
30
|
+
*/
|
|
31
|
+
const homePage = (name: string, widgets: string): string => `<!doctype html>
|
|
32
|
+
<html lang="en">
|
|
33
|
+
<head>
|
|
34
|
+
<meta charset="utf-8">
|
|
35
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
36
|
+
<title>${escapeHtml(name)}</title>
|
|
37
|
+
<style>:root{color-scheme:light dark;background:Canvas;color:CanvasText}body{margin:2rem auto;max-width:44rem;padding:0 1rem;font:15px/1.55 system-ui,-apple-system,"Segoe UI",sans-serif}h1{font-size:1.6rem;margin:0 0 .5rem}a{color:#5468ff}code{font:.9em ui-monospace,"SF Mono",Menlo,monospace}</style>
|
|
38
|
+
</head>
|
|
39
|
+
<body>
|
|
40
|
+
<main>
|
|
41
|
+
<h1>${escapeHtml(name)}</h1>
|
|
42
|
+
<p>Your app is running. ${FIRST_PAGE_MARKER}; edit <code>src/routes.ts</code> to change it.</p>
|
|
43
|
+
<p><a href="./about">About</a></p>
|
|
44
|
+
${widgets}
|
|
45
|
+
</main>
|
|
46
|
+
</body>
|
|
47
|
+
</html>`
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The app's own pages and routes, mountable at any prefix: the standalone server mounts it at the root, the frame host at {@link FRAME_MOUNT_PREFIX}.
|
|
51
|
+
* The first screen shows the widgets of the app's plugins (`ctx.home`), read at each request, so a plugin switched on or off shows at the next load.
|
|
52
|
+
*/
|
|
53
|
+
export function makeApp(home: Home): Acryl {
|
|
54
|
+
return (
|
|
55
|
+
new Acryl()
|
|
56
|
+
// A browser gets the first screen; an API client gets the JSON hello.
|
|
57
|
+
.get('/', async (c) =>
|
|
58
|
+
(c.req.header('accept') ?? '').includes('text/html')
|
|
59
|
+
? c.html(homePage(APP_NAME, await renderWidgets(home)))
|
|
60
|
+
: c.json({ app: APP_NAME, ok: true }),
|
|
61
|
+
)
|
|
14
62
|
.get('/health', (c) => c.json({ ok: true }))
|
|
15
63
|
.get('/about', (c) => c.html(aboutPage()))
|
|
16
|
-
|
|
64
|
+
)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** The app's own routes: an `App` mounted on the router at `options.prefix` (empty or absent: the root). The mount is removed when this plugin is disposed. */
|
|
68
|
+
export const routes = {
|
|
69
|
+
name: 'routes',
|
|
70
|
+
inject: ['router', 'home'],
|
|
71
|
+
apply(ctx: Context, options: { readonly prefix?: string } = {}) {
|
|
72
|
+
const prefix = options.prefix ?? ''
|
|
73
|
+
ctx.router.route(prefix === '' ? '/' : prefix, makeApp(ctx.home))
|
|
17
74
|
},
|
|
18
75
|
}
|
|
@@ -1 +1,14 @@
|
|
|
1
|
-
{
|
|
1
|
+
{
|
|
2
|
+
"plugins": [
|
|
3
|
+
"instance",
|
|
4
|
+
"acryl-router",
|
|
5
|
+
"acryl-node-server",
|
|
6
|
+
"manifest",
|
|
7
|
+
"save",
|
|
8
|
+
"home",
|
|
9
|
+
"extensions",
|
|
10
|
+
"acryl-door",
|
|
11
|
+
"acryl-web-server",
|
|
12
|
+
"acryl-page"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -6,8 +6,8 @@ import { join } from 'node:path'
|
|
|
6
6
|
import { test } from 'node:test'
|
|
7
7
|
import { fileURLToPath } from 'node:url'
|
|
8
8
|
import { pinnedInstance, withPort } from '../plugins/instance/index.ts'
|
|
9
|
-
import { type App, createApp } from '../src/app.ts'
|
|
10
|
-
import { APP_NAME } from '../src/routes.ts'
|
|
9
|
+
import { type App, type AppOptions, createApp } from '../src/app.ts'
|
|
10
|
+
import { APP_NAME, FRAME_MOUNT_PREFIX } from '../src/routes.ts'
|
|
11
11
|
|
|
12
12
|
/** Throwaway homes live inside the project (`.acrylblends/` is ignored by git): a sandboxed agent may not write to the system temp folder. */
|
|
13
13
|
const scratch = join(fileURLToPath(new URL('..', import.meta.url)), '.acrylblends', 'test')
|
|
@@ -16,9 +16,10 @@ mkdirSync(scratch, { recursive: true })
|
|
|
16
16
|
const projectRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
17
17
|
|
|
18
18
|
/** Boot the app in a throwaway home on any free port, run the test, always stop it and remove the home. */
|
|
19
|
-
async function withApp(run: (app: App) => Promise<void
|
|
19
|
+
async function withApp(run: (app: App) => Promise<void>, options: AppOptions = {}) {
|
|
20
20
|
const home = await mkdtemp(join(scratch, 'app-test-'))
|
|
21
21
|
const app = await createApp(withPort(pinnedInstance(home, { root: projectRoot }), 0), {
|
|
22
|
+
...options,
|
|
22
23
|
listen: false,
|
|
23
24
|
})
|
|
24
25
|
try {
|
|
@@ -29,11 +30,20 @@ async function withApp(run: (app: App) => Promise<void>) {
|
|
|
29
30
|
}
|
|
30
31
|
}
|
|
31
32
|
|
|
32
|
-
test('serves the index route', () =>
|
|
33
|
+
test('serves the index route: the first screen to a browser, the JSON hello to an API client', () =>
|
|
33
34
|
withApp(async (app) => {
|
|
34
|
-
const
|
|
35
|
-
assert.equal(
|
|
36
|
-
assert.deepEqual(await
|
|
35
|
+
const api = await app.root.router.request(`/`)
|
|
36
|
+
assert.equal(api.status, 200)
|
|
37
|
+
assert.deepEqual(await api.json(), { app: APP_NAME, ok: true })
|
|
38
|
+
const page = await app.root.router.request(`/`, {
|
|
39
|
+
headers: { accept: 'text/html,application/xhtml+xml,*/*;q=0.8' },
|
|
40
|
+
})
|
|
41
|
+
assert.equal(page.status, 200)
|
|
42
|
+
assert.match(page.headers.get('content-type') ?? '', /text\/html/)
|
|
43
|
+
const html = await page.text()
|
|
44
|
+
// The ACRYL frame's first tile asks for a page: the root must never hand it JSON (Chrome's viewer is nearly invisible in the dark frame).
|
|
45
|
+
assert.ok(html.includes(`<h1>${APP_NAME}</h1>`), 'the page names the app')
|
|
46
|
+
assert.ok(html.includes('href="./about"'), 'the page links to About relatively')
|
|
37
47
|
}))
|
|
38
48
|
|
|
39
49
|
test('renders a .tsx page, and an error in it names the .tsx line', () =>
|
|
@@ -59,11 +69,78 @@ test('answers health checks and 404 for unknown paths', () =>
|
|
|
59
69
|
await missing.body?.cancel()
|
|
60
70
|
}))
|
|
61
71
|
|
|
72
|
+
test('the frame mounts the same app under /app; the app on its own server keeps its old URLs', async () => {
|
|
73
|
+
// Standalone (production, `npm start`): the public URLs do not move.
|
|
74
|
+
await withApp(async (app) => {
|
|
75
|
+
for (const path of ['/', '/about', '/health']) {
|
|
76
|
+
const response = await app.root.router.request(path)
|
|
77
|
+
assert.equal(response.status, 200, path)
|
|
78
|
+
await response.body?.cancel()
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
// The frame composes the same blend.yaml rows at the one prefix the app names; src/frame.ts only claims that prefix on the frame's web server.
|
|
82
|
+
const frame = await import('../src/frame.ts')
|
|
83
|
+
assert.deepEqual([...frame.inject], ['routerHttp'])
|
|
84
|
+
await withApp(
|
|
85
|
+
async (app) => {
|
|
86
|
+
for (const path of ['', '/about', '/health']) {
|
|
87
|
+
const response = await app.root.router.request(`${FRAME_MOUNT_PREFIX}${path}`)
|
|
88
|
+
assert.equal(response.status, 200, path)
|
|
89
|
+
await response.body?.cancel()
|
|
90
|
+
}
|
|
91
|
+
const missing = await app.root.router.request(`${FRAME_MOUNT_PREFIX}/nope`)
|
|
92
|
+
assert.equal(missing.status, 404)
|
|
93
|
+
await missing.body?.cancel()
|
|
94
|
+
},
|
|
95
|
+
{ prefix: FRAME_MOUNT_PREFIX },
|
|
96
|
+
)
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
test('a plugin puts its widget on the first screen, and the widget leaves with the plugin', () =>
|
|
100
|
+
withApp(async (app) => {
|
|
101
|
+
const page = async () =>
|
|
102
|
+
(await app.root.router.request('/', { headers: { accept: 'text/html' } })).text()
|
|
103
|
+
const feature = app.root.plugin({
|
|
104
|
+
name: 'hello-widget',
|
|
105
|
+
inject: ['home'],
|
|
106
|
+
apply(ctx: typeof app.root) {
|
|
107
|
+
ctx.effect(() =>
|
|
108
|
+
ctx.home.widget({
|
|
109
|
+
id: 'hello-widget',
|
|
110
|
+
title: 'Hello',
|
|
111
|
+
render: () => '<p id="hello">hi</p>',
|
|
112
|
+
}),
|
|
113
|
+
)
|
|
114
|
+
},
|
|
115
|
+
})
|
|
116
|
+
await feature.await()
|
|
117
|
+
assert.match(
|
|
118
|
+
await page(),
|
|
119
|
+
/<section class="widget" data-widget="hello-widget">[\s\S]*<p id="hello">hi<\/p>/,
|
|
120
|
+
)
|
|
121
|
+
await feature.dispose()
|
|
122
|
+
assert.doesNotMatch(await page(), /data-widget="hello-widget"/)
|
|
123
|
+
}))
|
|
124
|
+
|
|
62
125
|
/** A sandbox may forbid listening on a port (Codex's workspace-write does); the one test that starts the real server is skipped there, and says so. */
|
|
126
|
+
/**
|
|
127
|
+
* Whether this environment forbids listening at all: only a sandbox's refusal (EPERM/EACCES) means the real-server test must skip. A loaded
|
|
128
|
+
* machine can fail a bind transiently, so that is retried briefly; any other outcome lets the real test run, where a host that truly cannot
|
|
129
|
+
* listen fails loudly instead of hiding behind a skip.
|
|
130
|
+
*/
|
|
63
131
|
const canListen = await new Promise<boolean>((done) => {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
132
|
+
let attempts = 0
|
|
133
|
+
const probeOnce = (): void => {
|
|
134
|
+
attempts += 1
|
|
135
|
+
const probe = createServer()
|
|
136
|
+
probe.once('error', (error: NodeJS.ErrnoException) => {
|
|
137
|
+
if (error.code === 'EPERM' || error.code === 'EACCES') done(false)
|
|
138
|
+
else if (attempts < 3) setTimeout(probeOnce, 100)
|
|
139
|
+
else done(true)
|
|
140
|
+
})
|
|
141
|
+
probe.listen(0, '127.0.0.1', () => probe.close(() => done(true)))
|
|
142
|
+
}
|
|
143
|
+
probeOnce()
|
|
67
144
|
})
|
|
68
145
|
const skip = canListen ? false : 'this environment does not allow listening on a port'
|
|
69
146
|
if (!canListen)
|
|
@@ -72,19 +149,29 @@ if (!canListen)
|
|
|
72
149
|
)
|
|
73
150
|
|
|
74
151
|
test('stops listening when stopped', { skip }, async () => {
|
|
152
|
+
// One process at a time serves a folder's door, and in the frame the frame's own door serves this folder: this server starts without one.
|
|
153
|
+
const door = process.env.ACRYL_DOOR
|
|
154
|
+
process.env.ACRYL_DOOR = 'off'
|
|
75
155
|
const home = await mkdtemp(join(scratch, 'app-test-'))
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
156
|
+
try {
|
|
157
|
+
const app = await createApp(withPort(pinnedInstance(home, { root: projectRoot }), 0))
|
|
158
|
+
await app.stop()
|
|
159
|
+
await assert.rejects(fetch(`${app.url}/health`))
|
|
160
|
+
} finally {
|
|
161
|
+
if (door === undefined) delete process.env.ACRYL_DOOR
|
|
162
|
+
else process.env.ACRYL_DOOR = door
|
|
163
|
+
await rm(home, { recursive: true, force: true })
|
|
164
|
+
}
|
|
80
165
|
})
|
|
81
166
|
|
|
82
167
|
test('the manifest decides what is mounted: a parameter changes the server, an unknown value is refused', async () => {
|
|
83
168
|
const { readManifest } = await import('../src/app.ts')
|
|
84
169
|
const manifest = readManifest()
|
|
170
|
+
// The template's rows, in their order; a feature's row (plugins/<name>/) may sit between them.
|
|
171
|
+
const library = ['router', 'home', 'routes', 'extensions', 'server', 'acryl-door']
|
|
85
172
|
assert.deepEqual(
|
|
86
|
-
manifest.plugins.map((row) => row.id),
|
|
87
|
-
|
|
173
|
+
manifest.plugins.map((row) => row.id).filter((id) => library.includes(id)),
|
|
174
|
+
library,
|
|
88
175
|
)
|
|
89
176
|
assert.throws(() => readManifest({ nothing: 1 }), /parameter-override-unknown/)
|
|
90
177
|
})
|
|
@@ -20,7 +20,8 @@ spec:
|
|
|
20
20
|
name: acryl-db-sqlite
|
|
21
21
|
- id: routes
|
|
22
22
|
name: routes
|
|
23
|
-
#
|
|
23
|
+
# Plugin folders without a row of their own (a live install with the door's `plugin add`); a folder with a row is the app's and starts by it.
|
|
24
|
+
# Read-only when NODE_ENV=production.
|
|
24
25
|
- id: extensions
|
|
25
26
|
name: extensions
|
|
26
27
|
- id: server
|
package/templates/chat/gitignore
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"check": "tsc --noEmit && tsc --noEmit -p src/client && acrylblends check-deps",
|
|
18
18
|
"build": "acrylblends build-client",
|
|
19
19
|
"lock": "node plugins/manifest/lock.ts",
|
|
20
|
+
"snapshot": "node plugins/manifest/snapshot.ts",
|
|
20
21
|
"save": "node plugins/save/cli.ts",
|
|
21
22
|
"upgrade": "node plugins/manifest/upgrade-cli.ts"
|
|
22
23
|
},
|
|
@@ -8,13 +8,15 @@ import * as router from '../plugins/acryl-router/src/index.ts'
|
|
|
8
8
|
import { extensions } from '../plugins/extensions/index.ts'
|
|
9
9
|
import { type AppInstance, instance } from '../plugins/instance/index.ts'
|
|
10
10
|
import {
|
|
11
|
+
appPluginNames,
|
|
11
12
|
blueprintsIn,
|
|
12
13
|
compose,
|
|
14
|
+
loadAppPlugins,
|
|
13
15
|
loadManifest,
|
|
14
16
|
type ParameterValue,
|
|
15
17
|
} from '../plugins/manifest/index.ts'
|
|
16
18
|
import { doorEnabled } from './door.ts'
|
|
17
|
-
import { routes } from './routes.ts'
|
|
19
|
+
import { FRAME_MOUNT_PREFIX, routes } from './routes.ts'
|
|
18
20
|
|
|
19
21
|
/** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
|
|
20
22
|
export const registry = {
|
|
@@ -30,11 +32,13 @@ const projectRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
|
30
32
|
|
|
31
33
|
/** The manifest next to this app's source. */
|
|
32
34
|
export const MANIFEST = join(projectRoot, 'blend.yaml')
|
|
35
|
+
/** The app's plugins: the library copies `registry` imports, and the app's own plugins (a feature each), which `blend.yaml` mounts by their folder name. */
|
|
36
|
+
const PLUGINS_DIR = join(projectRoot, 'plugins')
|
|
33
37
|
|
|
34
38
|
/** The manifest, resolved. `createApp` and the tests read it the same way. */
|
|
35
39
|
export function readManifest(values?: Readonly<Record<string, ParameterValue>>) {
|
|
36
40
|
return loadManifest(MANIFEST, {
|
|
37
|
-
known: Object.keys(registry),
|
|
41
|
+
known: [...Object.keys(registry), ...appPluginNames(PLUGINS_DIR)],
|
|
38
42
|
getBlueprint: blueprintsIn(join(projectRoot, 'blueprints')),
|
|
39
43
|
...(values ? { values } : {}),
|
|
40
44
|
})
|
|
@@ -64,42 +68,88 @@ export interface AppOptions {
|
|
|
64
68
|
readonly values?: Readonly<Record<string, ParameterValue>>
|
|
65
69
|
/** 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/chat.sqlite`. */
|
|
66
70
|
readonly database?: string
|
|
71
|
+
/** The path the app is mounted at; absent on its own server, where it keeps the root, and {@link FRAME_MOUNT_PREFIX} in the frame. */
|
|
72
|
+
readonly prefix?: string
|
|
67
73
|
/** 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
74
|
readonly listen?: boolean
|
|
69
75
|
}
|
|
70
76
|
|
|
77
|
+
/** The rows the frame host provides: the app contributes only the plugins that are its own. */
|
|
78
|
+
const HOST_ROWS = [
|
|
79
|
+
'instance',
|
|
80
|
+
'acryl-router',
|
|
81
|
+
'acryl-node-server',
|
|
82
|
+
'acryl-web-server',
|
|
83
|
+
'acryl-page',
|
|
84
|
+
'manifest',
|
|
85
|
+
'save',
|
|
86
|
+
'acryl-door',
|
|
87
|
+
]
|
|
88
|
+
|
|
71
89
|
/**
|
|
72
|
-
* The app
|
|
73
|
-
*
|
|
90
|
+
* The app's rows, composed into `ctx`: the app's own server calls this from `createApp`, and the generated frame layer mounts it as one row, so the frame
|
|
91
|
+
* runs the app exactly as the app's own server does (same rows, same order, same config). `host: true` leaves the infrastructure rows to the frame host,
|
|
92
|
+
* which is where the door lives too, and mounts the app at {@link FRAME_MOUNT_PREFIX}. The `db` row opens the database and applies `migrations/`; the one
|
|
93
|
+
* place that names the SQLite provider is this file (`acrylblends check-deps`), so a different provider is a change here.
|
|
74
94
|
*/
|
|
75
|
-
export async function
|
|
95
|
+
export async function composeInto(
|
|
96
|
+
ctx: Context,
|
|
97
|
+
chosen: AppInstance,
|
|
98
|
+
options: AppOptions & { readonly host?: boolean } = {},
|
|
99
|
+
): Promise<Fiber[]> {
|
|
76
100
|
const read = readManifest(options.values)
|
|
77
|
-
const
|
|
78
|
-
//
|
|
79
|
-
const
|
|
80
|
-
|
|
101
|
+
const own = options.host !== true && options.listen !== false
|
|
102
|
+
// The frame host mounts the app under its own prefix; the app's own server keeps the root.
|
|
103
|
+
const prefix = options.prefix ?? (options.host === true ? FRAME_MOUNT_PREFIX : undefined)
|
|
104
|
+
// Rows left out: the server (and the door, which needs it) when the app is reached in process or inside the frame host; the door when it is switched
|
|
105
|
+
// off.
|
|
106
|
+
const leaveOut = new Set<string>(options.host === true ? HOST_ROWS : [])
|
|
107
|
+
if (!own) leaveOut.add('acryl-node-server').add('acryl-door')
|
|
81
108
|
if (!doorEnabled()) leaveOut.add('acryl-door')
|
|
82
|
-
if (process.env.NODE_ENV === 'production') leaveOut.add('extensions')
|
|
83
109
|
const manifest = without(read, ...leaveOut)
|
|
110
|
+
// The rows that name one of the app's own plugin folders (a feature: plugins/<name>/ and its row) rather than a registry import. Each one that runs
|
|
111
|
+
// is imported here; all of them, off or on, belong to their row, so the extensions row never starts them a second time.
|
|
112
|
+
const ownRows = [
|
|
113
|
+
...new Set(read.plugins.map((row) => row.use).filter((use) => !Object.hasOwn(registry, use))),
|
|
114
|
+
]
|
|
115
|
+
const ownPlugins = await loadAppPlugins(
|
|
116
|
+
PLUGINS_DIR,
|
|
117
|
+
ownRows.filter((use) => manifest.plugins.some((row) => row.use === use && !row.disabled)),
|
|
118
|
+
)
|
|
84
119
|
const file = options.database ?? 'data/chat.sqlite'
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
...(await compose(root, manifest, registry, {
|
|
120
|
+
return compose(
|
|
121
|
+
ctx,
|
|
122
|
+
manifest,
|
|
123
|
+
{ ...registry, ...ownPlugins },
|
|
124
|
+
{
|
|
91
125
|
db: {
|
|
92
126
|
path: isAbsolute(file) ? file : resolve(projectRoot, file),
|
|
93
127
|
migrations: join(projectRoot, 'migrations'),
|
|
94
128
|
},
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
129
|
+
// The routes row mounts the app (and its client modules) at the prefix the frame host chose.
|
|
130
|
+
routes: { ...(prefix === undefined ? {} : { prefix }) },
|
|
131
|
+
// The app's own plugins live in plugins/, next to the plugins copied from the library (plugins/library.json lists those, so they are not installed twice).
|
|
132
|
+
// In production they start read-only from where they are deployed: the app keeps its features, and nothing can be installed while it runs.
|
|
133
|
+
extensions: {
|
|
134
|
+
...(process.env.NODE_ENV === 'production' ? { readOnly: true } : {}),
|
|
135
|
+
sources: [{ dir: PLUGINS_DIR, trust: 'install' }],
|
|
136
|
+
rows: ownRows,
|
|
137
|
+
},
|
|
138
|
+
// Only the app's own server listens; the frame host owns the server and its port.
|
|
139
|
+
...(own ? { server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 } } : {}),
|
|
140
|
+
},
|
|
141
|
+
)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. */
|
|
145
|
+
export async function createApp(chosen: AppInstance, options: AppOptions = {}): Promise<App> {
|
|
146
|
+
const root = new Context()
|
|
147
|
+
const first = root.plugin(instance, chosen)
|
|
148
|
+
await first.await()
|
|
149
|
+
const fibers: Fiber[] = [first, ...(await composeInto(root, chosen, options))]
|
|
100
150
|
return {
|
|
101
151
|
root,
|
|
102
|
-
url: listen === false ? '' : root.nodeServer.url,
|
|
152
|
+
url: options.listen === false ? '' : root.nodeServer.url,
|
|
103
153
|
async stop() {
|
|
104
154
|
for (const fiber of [...fibers].reverse()) await fiber.dispose()
|
|
105
155
|
},
|