acrylblends 0.7.7 → 0.8.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/dist/index.d.ts +14 -6
- package/dist/index.js +130 -31
- package/dist/index.js.map +1 -1
- package/docs/framework/README.md +3 -3
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/agents/index.md +18 -3
- package/docs/framework/getting-started/basic.md +1 -2
- package/docs/framework/getting-started/desktop.md +3 -3
- package/docs/framework/guides/bring-your-own-tool.md +216 -0
- package/docs/framework/guides/client-modules.md +4 -3
- package/docs/framework/guides/create-acrylblends.md +5 -4
- package/docs/framework/guides/data.md +7 -6
- package/docs/framework/helpers/streaming.md +12 -27
- package/docs/framework/upstream.json +3 -3
- package/lib/browser/read-sse.js +84 -0
- package/lib/db/sqlite.d.ts +1 -1
- package/lib/db.d.ts +1 -84
- package/lib/index-BLTYsyc1.d.ts +93 -0
- package/lib/index-D4FAsBZu.d.ts +85 -0
- package/lib/kv/db.d.ts +2 -2
- package/lib/kv/json.d.ts +1 -1
- package/lib/kv/json.js +87 -24
- package/lib/kv.d.ts +1 -92
- package/lib/node-server.d.ts +3 -3
- package/lib/node-server.js +14 -8
- package/lib/read-sse.d.ts +33 -0
- package/lib/read-sse.js +84 -0
- package/package.json +5 -1
- package/plugins/acryl-db/plugin.json +7 -2
- package/plugins/acryl-db/src/conformance.ts +224 -0
- package/plugins/acryl-db/upstream.json +2 -2
- package/plugins/acryl-db-sqlite/plugin.json +4 -1
- package/plugins/acryl-db-sqlite/upstream.json +1 -1
- package/plugins/acryl-door/LICENSE +47 -0
- package/plugins/acryl-door/plugin.json +14 -0
- package/plugins/acryl-door/src/cli/acryl.ts +207 -0
- package/plugins/acryl-door/src/host/api.ts +9 -0
- package/plugins/acryl-door/src/host/audit.ts +64 -0
- package/plugins/acryl-door/src/host/channel.ts +142 -0
- package/plugins/acryl-door/src/host/chat-tools.ts +39 -0
- package/plugins/acryl-door/src/host/door-file.ts +94 -0
- package/plugins/acryl-door/src/host/door.ts +195 -0
- package/plugins/acryl-door/src/host/http.ts +80 -0
- package/plugins/acryl-door/src/host/mcp-routes.ts +83 -0
- package/plugins/acryl-door/src/host/operator-routes.ts +38 -0
- package/plugins/acryl-door/src/host/page-routes.ts +65 -0
- package/plugins/acryl-door/src/host/shim.ts +65 -0
- package/plugins/acryl-door/src/host/verbs.ts +109 -0
- package/plugins/acryl-door/src/index.ts +133 -0
- package/plugins/acryl-door/src/page/api.ts +9 -0
- package/plugins/acryl-door/src/page/channel.ts +135 -0
- package/plugins/acryl-door/src/page/driver/accessibility.ts +163 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +186 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +153 -0
- package/plugins/acryl-door/src/page/driver/ref-table.ts +78 -0
- package/plugins/acryl-door/src/page/driver/sensitivity.ts +49 -0
- package/plugins/acryl-door/src/page/driver/snapshot.ts +132 -0
- package/plugins/acryl-door/src/page/index.ts +40 -0
- package/plugins/acryl-door/src/page/indicator.ts +72 -0
- package/plugins/acryl-door/src/page/main.ts +7 -0
- package/plugins/acryl-door/src/page/protocol.ts +213 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +18 -0
- package/plugins/acryl-door/src/page/user-input.ts +24 -0
- package/plugins/acryl-door/upstream.json +28 -0
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-kv/plugin.json +7 -2
- package/plugins/acryl-kv/src/conformance.ts +226 -0
- package/plugins/acryl-kv/upstream.json +1 -1
- package/plugins/acryl-kv-db/plugin.json +4 -1
- package/plugins/acryl-kv-db/upstream.json +1 -1
- package/plugins/acryl-kv-json/plugin.json +4 -1
- package/plugins/acryl-kv-json/src/open.ts +86 -22
- package/plugins/acryl-kv-json/upstream.json +2 -2
- package/plugins/acryl-node-server/src/adapter/listener.ts +5 -0
- package/plugins/acryl-node-server/src/client-modules.ts +10 -9
- package/plugins/acryl-node-server/upstream.json +2 -2
- package/plugins/acryl-router/src/browser/read-sse.ts +3 -0
- package/plugins/acryl-router/src/helpers/read-sse.ts +111 -0
- package/plugins/acryl-router/src/service.ts +3 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/agent-panel/client/main.ts +159 -0
- package/plugins/agent-panel/index.ts +232 -0
- package/plugins/agent-panel/plugin.json +21 -0
- package/plugins/builder/index.ts +2 -2
- package/plugins/builder/plugin.json +1 -1
- package/plugins/desktop/main.ts +3 -3
- package/plugins/desktop/plugin.json +2 -2
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/main.ts +2 -2
- package/plugins/desktop-web/plugin.json +2 -2
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/extensions/index.ts +19 -1
- package/plugins/extensions/plugin.json +1 -1
- package/plugins/extensions/resolve.ts +17 -3
- package/plugins/manifest/lock.ts +7 -3
- package/plugins/manifest/upgrade-cli.ts +1 -1
- package/plugins/save/cli.ts +1 -1
- package/plugins/save/plugin.json +1 -1
- package/plugins/tsconfig.json +6 -0
- package/shared/AGENTS.core.md +10 -10
- package/shared/DEFAULTS.md +45 -0
- package/shared/SKILL.md +14 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/README.md +6 -6
- package/templates/agent/blend.yaml +3 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +8 -8
- package/templates/agent/src/app.ts +46 -17
- package/templates/agent/src/main.ts +2 -2
- package/templates/agent/src/routes.ts +19 -6
- package/templates/agent/template.json +2 -1
- package/templates/agent/test/agent.test.ts +11 -5
- package/templates/agent/test/app.test.ts +30 -9
- package/templates/agent/test/builder.test.ts +35 -24
- package/templates/agent/test/model.test.ts +25 -20
- package/templates/agent/tsconfig.json +48 -50
- package/templates/blank/AGENTS.md +1 -1
- package/templates/blank/README.md +2 -2
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +8 -8
- package/templates/blank/src/about.tsx +2 -2
- package/templates/blank/src/app.ts +29 -9
- package/templates/blank/src/main.ts +1 -1
- package/templates/blank/src/routes.ts +1 -1
- package/templates/blank/test/app.test.ts +29 -10
- package/templates/blank/tsconfig.json +1 -1
- package/templates/chat/AGENTS.md +16 -0
- package/templates/chat/blend.yaml +29 -0
- package/templates/chat/gitignore +6 -0
- package/templates/chat/migrations/001-chat.sql +12 -0
- package/templates/chat/package.json +33 -0
- package/templates/chat/src/app.ts +106 -0
- package/templates/chat/src/chat-app.ts +101 -0
- package/templates/chat/src/client/chat.tsx +115 -0
- package/templates/chat/src/client/tsconfig.json +26 -0
- package/templates/chat/src/db.ts +43 -0
- package/templates/chat/src/door.ts +7 -0
- package/templates/chat/src/llm.ts +42 -0
- package/templates/chat/src/main.ts +28 -0
- package/templates/chat/src/page.tsx +40 -0
- package/templates/chat/src/routes.ts +15 -0
- package/templates/chat/src/runtime.ts +17 -0
- package/templates/chat/template.json +11 -0
- package/templates/chat/test/chat.test.ts +198 -0
- package/templates/chat/test/server.test.ts +78 -0
- package/templates/chat/tsconfig.json +20 -0
- package/templates/nodejs/AGENTS.md +9 -0
- package/templates/nodejs/blend.yaml +26 -0
- package/templates/nodejs/gitignore +3 -0
- package/templates/nodejs/package.json +30 -0
- package/templates/nodejs/src/app.ts +92 -0
- package/templates/nodejs/src/client/counter.tsx +12 -0
- package/templates/nodejs/src/client/tsconfig.json +26 -0
- package/templates/nodejs/src/door.ts +7 -0
- package/templates/nodejs/src/main.ts +25 -0
- package/templates/nodejs/src/page.tsx +29 -0
- package/templates/nodejs/src/routes.ts +40 -0
- package/templates/nodejs/template.json +3 -0
- package/templates/nodejs/test/app.test.ts +78 -0
- package/templates/nodejs/test/server.test.ts +83 -0
- package/templates/nodejs/tsconfig.json +20 -0
- package/templates/notes/AGENTS.md +26 -0
- package/templates/notes/blend.yaml +29 -0
- package/templates/notes/gitignore +6 -0
- package/templates/notes/migrations/001-notes.sql +18 -0
- package/templates/notes/package.json +33 -0
- package/templates/notes/src/app.ts +106 -0
- package/templates/notes/src/auth.ts +57 -0
- package/templates/notes/src/client/counter.tsx +21 -0
- package/templates/notes/src/client/tsconfig.json +26 -0
- package/templates/notes/src/db.ts +50 -0
- package/templates/notes/src/door.ts +7 -0
- package/templates/notes/src/main.ts +28 -0
- package/templates/notes/src/notes-app.ts +205 -0
- package/templates/notes/src/routes.ts +14 -0
- package/templates/notes/src/runtime.ts +17 -0
- package/templates/notes/src/schema.ts +38 -0
- package/templates/notes/src/views.tsx +136 -0
- package/templates/notes/template.json +11 -0
- package/templates/notes/test/notes.test.ts +280 -0
- package/templates/notes/test/server.test.ts +78 -0
- package/templates/notes/tsconfig.json +20 -0
- package/templates/todo/README.md +1 -1
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +8 -8
- package/templates/todo/src/app.ts +32 -12
- package/templates/todo/src/main.ts +1 -1
- package/templates/todo/src/todo.ts +4 -4
- package/templates/todo/test/todo.test.ts +48 -23
- package/templates/todo/tsconfig.json +1 -1
- package/templates/todo/upstream.json +2 -2
- package/lib/database-D_UN6We8.d.ts +0 -44
- package/lib/kv-DWDm0Lja.d.ts +0 -39
|
@@ -1,19 +1,26 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
2
3
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
-
import {
|
|
4
|
+
import { createServer } from 'node:net'
|
|
4
5
|
import { join } from 'node:path'
|
|
5
6
|
import { test } from 'node:test'
|
|
6
7
|
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { pinnedInstance, withPort } from '../plugins/instance/index.ts'
|
|
7
9
|
import { type App, createApp } from '../src/app.ts'
|
|
8
|
-
import { pinnedInstance, withPort } from '../src/plugins/instance/index.ts'
|
|
9
10
|
import { APP_NAME } from '../src/routes.ts'
|
|
10
11
|
|
|
12
|
+
/** Throwaway homes live inside the project (`.acrylblends/` is ignored by git): a sandboxed agent may not write to the system temp folder. */
|
|
13
|
+
const scratch = join(fileURLToPath(new URL('..', import.meta.url)), '.acrylblends', 'test')
|
|
14
|
+
mkdirSync(scratch, { recursive: true })
|
|
15
|
+
|
|
11
16
|
const projectRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
12
17
|
|
|
13
18
|
/** Boot the app in a throwaway home on any free port, run the test, always stop it and remove the home. */
|
|
14
19
|
async function withApp(run: (app: App) => Promise<void>) {
|
|
15
|
-
const home = await mkdtemp(join(
|
|
16
|
-
const app = await createApp(withPort(pinnedInstance(home, { root: projectRoot }), 0)
|
|
20
|
+
const home = await mkdtemp(join(scratch, 'app-test-'))
|
|
21
|
+
const app = await createApp(withPort(pinnedInstance(home, { root: projectRoot }), 0), {
|
|
22
|
+
listen: false,
|
|
23
|
+
})
|
|
17
24
|
try {
|
|
18
25
|
await run(app)
|
|
19
26
|
} finally {
|
|
@@ -24,21 +31,35 @@ async function withApp(run: (app: App) => Promise<void>) {
|
|
|
24
31
|
|
|
25
32
|
test('serves the index route', () =>
|
|
26
33
|
withApp(async (app) => {
|
|
27
|
-
const response = await
|
|
34
|
+
const response = await app.root.router.request(`/`)
|
|
28
35
|
assert.equal(response.status, 200)
|
|
29
36
|
assert.deepEqual(await response.json(), { app: APP_NAME, ok: true })
|
|
30
37
|
}))
|
|
31
38
|
|
|
32
39
|
test('answers health checks and 404 for unknown paths', () =>
|
|
33
40
|
withApp(async (app) => {
|
|
34
|
-
assert.equal((await
|
|
35
|
-
const missing = await
|
|
41
|
+
assert.equal((await app.root.router.request(`/health`)).status, 200)
|
|
42
|
+
const missing = await app.root.router.request(`/nope`)
|
|
36
43
|
assert.equal(missing.status, 404)
|
|
37
44
|
await missing.body?.cancel()
|
|
38
45
|
}))
|
|
39
46
|
|
|
40
|
-
|
|
41
|
-
|
|
47
|
+
/** 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. */
|
|
48
|
+
const canListen = await new Promise<boolean>((done) => {
|
|
49
|
+
const probe = createServer()
|
|
50
|
+
probe.once('error', () => done(false))
|
|
51
|
+
probe.listen(0, '127.0.0.1', () => probe.close(() => done(true)))
|
|
52
|
+
})
|
|
53
|
+
const skip = canListen ? false : 'this environment does not allow listening on a port'
|
|
54
|
+
if (!canListen)
|
|
55
|
+
console.log(
|
|
56
|
+
`skipped: the test that starts the real server (${skip}); every other test ran in process`,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
test('stops listening when stopped', { skip }, async () => {
|
|
60
|
+
const home = await mkdtemp(join(scratch, 'app-test-'))
|
|
61
|
+
// The door is left out (ACRYL_DOOR=off): one process at a time serves a folder's door.
|
|
62
|
+
process.env.ACRYL_DOOR = 'off'
|
|
42
63
|
const app = await createApp(withPort(pinnedInstance(home, { root: projectRoot }), 0))
|
|
43
64
|
await app.stop()
|
|
44
65
|
await assert.rejects(fetch(`${app.url}/health`))
|
|
@@ -1,18 +1,23 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
2
3
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
-
import { tmpdir } from 'node:os'
|
|
4
4
|
import { join } from 'node:path'
|
|
5
5
|
import { test } from 'node:test'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { pinnedInstance, withPort } from '../plugins/instance/index.ts'
|
|
6
8
|
import { createApp } from '../src/app.ts'
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
/** Throwaway homes live inside the project (`.acrylblends/` is ignored by git): a sandboxed agent may not write to the system temp folder. */
|
|
11
|
+
const scratch = join(fileURLToPath(new URL('..', import.meta.url)), '.acrylblends', 'test')
|
|
12
|
+
mkdirSync(scratch, { recursive: true })
|
|
8
13
|
|
|
9
14
|
/** The agent extends the running app through POST /ask; a restart keeps what it built. */
|
|
10
15
|
test('the agent builds, updates, protects and removes a plugin in the running app, and a restart keeps it', async () => {
|
|
11
|
-
const project = await mkdtemp(join(
|
|
16
|
+
const project = await mkdtemp(join(scratch, 'builder-project-'))
|
|
12
17
|
const chosen = withPort(pinnedInstance(join(project, '.acrylblends'), { root: project }), 0)
|
|
13
|
-
let app = await createApp(chosen)
|
|
18
|
+
let app = await createApp(chosen, { listen: false })
|
|
14
19
|
const ask = async (question: string) => {
|
|
15
|
-
const response = await
|
|
20
|
+
const response = await app.root.router.request(`/ask`, {
|
|
16
21
|
method: 'POST',
|
|
17
22
|
headers: { 'content-type': 'application/json' },
|
|
18
23
|
body: JSON.stringify({ question }),
|
|
@@ -20,7 +25,7 @@ test('the agent builds, updates, protects and removes a plugin in the running ap
|
|
|
20
25
|
return ((await response.json()) as { output: string }).output
|
|
21
26
|
}
|
|
22
27
|
const hello = async () => {
|
|
23
|
-
const response = await
|
|
28
|
+
const response = await app.root.router.request(`/hello`)
|
|
24
29
|
return response.status === 200
|
|
25
30
|
? ((await response.json()) as { text: string }).text
|
|
26
31
|
: response.status
|
|
@@ -36,14 +41,14 @@ test('the agent builds, updates, protects and removes a plugin in the running ap
|
|
|
36
41
|
// The broken version stays on disk, so put the working one back before restarting.
|
|
37
42
|
await ask('update route hello /hello Bonjour')
|
|
38
43
|
await app.stop()
|
|
39
|
-
app = await createApp(chosen)
|
|
44
|
+
app = await createApp(chosen, { listen: false })
|
|
40
45
|
assert.equal(await hello(), 'Bonjour')
|
|
41
46
|
|
|
42
47
|
assert.equal(await ask('remove plugin hello'), 'hello: done.')
|
|
43
48
|
assert.equal(await hello(), 404)
|
|
44
49
|
|
|
45
50
|
// Every run is a durable task that owns its tool calls, and the records survived the restart.
|
|
46
|
-
const records = (await (await
|
|
51
|
+
const records = (await (await app.root.router.request(`/tasks`)).json()) as Array<{
|
|
47
52
|
kind: string
|
|
48
53
|
status: string
|
|
49
54
|
parent?: string
|
|
@@ -63,12 +68,12 @@ test('the agent builds, updates, protects and removes a plugin in the running ap
|
|
|
63
68
|
})
|
|
64
69
|
|
|
65
70
|
test('with approval on, an install waits for the owner; approving it makes it live', async () => {
|
|
66
|
-
const project = await mkdtemp(join(
|
|
71
|
+
const project = await mkdtemp(join(scratch, 'approval-project-'))
|
|
67
72
|
const chosen = withPort(pinnedInstance(join(project, '.acrylblends'), { root: project }), 0)
|
|
68
|
-
const app = await createApp(chosen, { values: { approval: true } })
|
|
73
|
+
const app = await createApp(chosen, { values: { approval: true }, listen: false })
|
|
69
74
|
const post = async (path: string, body: unknown) =>
|
|
70
75
|
(await (
|
|
71
|
-
await
|
|
76
|
+
await app.root.router.request(`${path}`, {
|
|
72
77
|
method: 'POST',
|
|
73
78
|
headers: { 'content-type': 'application/json' },
|
|
74
79
|
body: JSON.stringify(body),
|
|
@@ -79,8 +84,8 @@ test('with approval on, an install waits for the owner; approving it makes it li
|
|
|
79
84
|
(await post('/ask', { question: 'create route hello /hello Hello' })).output,
|
|
80
85
|
"hello: waiting for the owner's approval.",
|
|
81
86
|
)
|
|
82
|
-
assert.equal((await
|
|
83
|
-
const listed = (await (await
|
|
87
|
+
assert.equal((await app.root.router.request(`/hello`)).status, 404)
|
|
88
|
+
const listed = (await (await app.root.router.request(`/extensions`)).json()) as {
|
|
84
89
|
waitingForApproval: Array<{ name: string }>
|
|
85
90
|
}
|
|
86
91
|
assert.deepEqual(
|
|
@@ -91,8 +96,10 @@ test('with approval on, an install waits for the owner; approving it makes it li
|
|
|
91
96
|
((await post('/extensions/approve', { name: 'hello' })) as { state?: string }).state,
|
|
92
97
|
'active',
|
|
93
98
|
)
|
|
94
|
-
assert.deepEqual(await (await
|
|
95
|
-
const tasks = (await (await
|
|
99
|
+
assert.deepEqual(await (await app.root.router.request(`/hello`)).json(), { text: 'Hello' })
|
|
100
|
+
const tasks = (await (await app.root.router.request(`/tasks`)).json()) as Array<{
|
|
101
|
+
manifest?: string
|
|
102
|
+
}>
|
|
96
103
|
assert.ok(
|
|
97
104
|
tasks.length > 0 && tasks.every((task) => /^sha256:[0-9a-f]{64}$/.test(task.manifest ?? '')),
|
|
98
105
|
)
|
|
@@ -103,13 +110,13 @@ test('with approval on, an install waits for the owner; approving it makes it li
|
|
|
103
110
|
})
|
|
104
111
|
|
|
105
112
|
test('the agent builds a sandboxed tool, calls it, and the sandbox refuses a tool that reads outside its folder', async () => {
|
|
106
|
-
const project = await mkdtemp(join(
|
|
113
|
+
const project = await mkdtemp(join(scratch, 'tool-project-'))
|
|
107
114
|
const chosen = withPort(pinnedInstance(join(project, '.acrylblends'), { root: project }), 0)
|
|
108
|
-
const app = await createApp(chosen)
|
|
115
|
+
const app = await createApp(chosen, { listen: false })
|
|
109
116
|
const ask = async (question: string) =>
|
|
110
117
|
(
|
|
111
118
|
(await (
|
|
112
|
-
await
|
|
119
|
+
await app.root.router.request(`/ask`, {
|
|
113
120
|
method: 'POST',
|
|
114
121
|
headers: { 'content-type': 'application/json' },
|
|
115
122
|
body: JSON.stringify({ question }),
|
|
@@ -121,7 +128,7 @@ test('the agent builds a sandboxed tool, calls it, and the sandbox refuses a too
|
|
|
121
128
|
assert.equal(await ask('run tool triple 14'), '{"n":42}')
|
|
122
129
|
const refused = await ask('spy tool snoop')
|
|
123
130
|
assert.match(refused, /^Refused: its check failed: denied:/)
|
|
124
|
-
const listed = (await (await
|
|
131
|
+
const listed = (await (await app.root.router.request(`/extensions`)).json()) as {
|
|
125
132
|
installed: Array<{ name: string }>
|
|
126
133
|
}
|
|
127
134
|
assert.deepEqual(
|
|
@@ -135,13 +142,13 @@ test('the agent builds a sandboxed tool, calls it, and the sandbox refuses a too
|
|
|
135
142
|
})
|
|
136
143
|
|
|
137
144
|
test('what happens is streamed from the state cells: the run as a task, and the install as an extension', async () => {
|
|
138
|
-
const project = await mkdtemp(join(
|
|
145
|
+
const project = await mkdtemp(join(scratch, 'events-project-'))
|
|
139
146
|
const chosen = withPort(pinnedInstance(join(project, '.acrylblends'), { root: project }), 0)
|
|
140
|
-
const app = await createApp(chosen)
|
|
147
|
+
const app = await createApp(chosen, { listen: false })
|
|
141
148
|
const controller = new AbortController()
|
|
142
149
|
/** Follow one server-sent stream: its text so far. */
|
|
143
150
|
const follow = async (path: string) => {
|
|
144
|
-
const stream = await
|
|
151
|
+
const stream = await app.root.router.request(`${path}`, { signal: controller.signal })
|
|
145
152
|
assert.equal(stream.status, 200, path)
|
|
146
153
|
const reader = stream.body!.getReader()
|
|
147
154
|
const decoder = new TextDecoder()
|
|
@@ -153,12 +160,12 @@ test('what happens is streamed from the state cells: the run as a task, and the
|
|
|
153
160
|
seen.text += decoder.decode(value, { stream: true })
|
|
154
161
|
}
|
|
155
162
|
})().catch(() => undefined)
|
|
156
|
-
return { seen, pump }
|
|
163
|
+
return { seen, pump, reader }
|
|
157
164
|
}
|
|
158
165
|
try {
|
|
159
166
|
const tasksStream = await follow('/api/state/tasks/events')
|
|
160
167
|
const extensionsStream = await follow('/api/state/extensions/events')
|
|
161
|
-
await
|
|
168
|
+
await app.root.router.request(`/ask`, {
|
|
162
169
|
method: 'POST',
|
|
163
170
|
headers: { 'content-type': 'application/json' },
|
|
164
171
|
body: JSON.stringify({ question: 'create route hello /hello Hello' }),
|
|
@@ -185,7 +192,11 @@ test('what happens is streamed from the state cells: the run as a task, and the
|
|
|
185
192
|
arrived(extensionsStream.seen, /"name":"hello"[^\n]*"state":"active"/),
|
|
186
193
|
'the extension is reported',
|
|
187
194
|
)
|
|
195
|
+
// In process there is no socket to close: the readers are cancelled, which ends the streams and lets the pumps finish.
|
|
188
196
|
controller.abort()
|
|
197
|
+
await Promise.all([tasksStream.reader.cancel(), extensionsStream.reader.cancel()]).catch(
|
|
198
|
+
() => undefined,
|
|
199
|
+
)
|
|
189
200
|
await Promise.all([tasksStream.pump, extensionsStream.pump])
|
|
190
201
|
} finally {
|
|
191
202
|
await app.stop()
|
|
@@ -1,16 +1,20 @@
|
|
|
1
1
|
import assert from 'node:assert/strict'
|
|
2
|
+
import { mkdirSync } from 'node:fs'
|
|
2
3
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
3
|
-
import { createServer } from 'node:http'
|
|
4
|
-
import type { AddressInfo } from 'node:net'
|
|
5
|
-
import { tmpdir } from 'node:os'
|
|
6
4
|
import { join } from 'node:path'
|
|
7
5
|
import { test } from 'node:test'
|
|
6
|
+
import { fileURLToPath } from 'node:url'
|
|
7
|
+
import { pinnedInstance, withPort } from '../plugins/instance/index.ts'
|
|
8
8
|
import { createApp } from '../src/app.ts'
|
|
9
|
-
|
|
9
|
+
|
|
10
|
+
/** Throwaway homes live inside the project (`.acrylblends/` is ignored by git): a sandboxed agent may not write to the system temp folder. */
|
|
11
|
+
const scratch = join(fileURLToPath(new URL('..', import.meta.url)), '.acrylblends', 'test')
|
|
12
|
+
mkdirSync(scratch, { recursive: true })
|
|
10
13
|
|
|
11
14
|
/** A plugin no scripted command knows: the model writes it from scratch. */
|
|
12
15
|
const COUNTER = `import type { Context } from '@deepseek-ai/cordis'
|
|
13
16
|
import { Acryl } from 'acrylblends'
|
|
17
|
+
|
|
14
18
|
export const name = 'counter'
|
|
15
19
|
export const inject = ['router']
|
|
16
20
|
export function apply(ctx: Context) {
|
|
@@ -45,42 +49,43 @@ async function modelApi() {
|
|
|
45
49
|
},
|
|
46
50
|
{ content: 'I built a counter: GET /count.' },
|
|
47
51
|
]
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
})
|
|
57
|
-
await new Promise<void>((done) => server.listen(0, '127.0.0.1', done))
|
|
52
|
+
// The model API is answered in process: the global fetch is wrapped for the model's address, so no port is needed.
|
|
53
|
+
const realFetch = globalThis.fetch
|
|
54
|
+
const url = 'http://model.test/v1'
|
|
55
|
+
globalThis.fetch = async (input, init) => {
|
|
56
|
+
const target = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url
|
|
57
|
+
if (!target.startsWith(url)) return realFetch(input, init)
|
|
58
|
+
return Response.json({ choices: [{ message: replies.shift() ?? { content: 'no more' } }] })
|
|
59
|
+
}
|
|
58
60
|
return {
|
|
59
|
-
url
|
|
60
|
-
close:
|
|
61
|
+
url,
|
|
62
|
+
close: async () => {
|
|
63
|
+
globalThis.fetch = realFetch
|
|
64
|
+
},
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
test('with a real model, the agent writes a new Cordis plugin and the running app serves it', async () => {
|
|
65
69
|
const api = await modelApi()
|
|
66
|
-
const project = await mkdtemp(join(
|
|
70
|
+
const project = await mkdtemp(join(scratch, 'model-project-'))
|
|
67
71
|
const app = await createApp(
|
|
68
72
|
withPort(pinnedInstance(join(project, '.acrylblends'), { root: project }), 0),
|
|
69
73
|
{
|
|
70
74
|
model: { baseUrl: api.url, model: 'test' },
|
|
75
|
+
listen: false,
|
|
71
76
|
},
|
|
72
77
|
)
|
|
73
78
|
try {
|
|
74
79
|
const answer = await (
|
|
75
|
-
await
|
|
80
|
+
await app.root.router.request(`/ask`, {
|
|
76
81
|
method: 'POST',
|
|
77
82
|
headers: { 'content-type': 'application/json' },
|
|
78
83
|
body: JSON.stringify({ question: 'add a counter route' }),
|
|
79
84
|
})
|
|
80
85
|
).json()
|
|
81
86
|
assert.equal((answer as { output: string }).output, 'I built a counter: GET /count.')
|
|
82
|
-
assert.deepEqual(await (await
|
|
83
|
-
assert.deepEqual(await (await
|
|
87
|
+
assert.deepEqual(await (await app.root.router.request(`/count`)).json(), { count: 1 })
|
|
88
|
+
assert.deepEqual(await (await app.root.router.request(`/count`)).json(), { count: 2 })
|
|
84
89
|
} finally {
|
|
85
90
|
await app.stop()
|
|
86
91
|
await api.close()
|
|
@@ -13,58 +13,56 @@
|
|
|
13
13
|
"skipLibCheck": true,
|
|
14
14
|
"types": ["node"],
|
|
15
15
|
"paths": {
|
|
16
|
-
"acrylblends": ["./
|
|
17
|
-
"acrylblends/conninfo": ["./
|
|
18
|
-
"acrylblends/http-exception": ["./
|
|
19
|
-
"acrylblends/types": ["./
|
|
20
|
-
"acrylblends/client": ["./
|
|
21
|
-
"acrylblends/basic-auth": ["./
|
|
22
|
-
"acrylblends/bearer-auth": ["./
|
|
23
|
-
"acrylblends/body-limit": ["./
|
|
24
|
-
"acrylblends/combine": ["./
|
|
25
|
-
"acrylblends/compress": ["./
|
|
26
|
-
"acrylblends/context-storage": ["./
|
|
27
|
-
"acrylblends/cors": ["./
|
|
28
|
-
"acrylblends/csrf": ["./
|
|
29
|
-
"acrylblends/etag": ["./
|
|
30
|
-
"acrylblends/ip-restriction": ["./
|
|
31
|
-
"acrylblends/jwk": ["./
|
|
32
|
-
"acrylblends/jwt": ["./
|
|
33
|
-
"acrylblends/language": ["./
|
|
34
|
-
"acrylblends/logger": ["./
|
|
35
|
-
"acrylblends/loopback-only": ["./
|
|
36
|
-
"acrylblends/method-not-allowed": ["./
|
|
37
|
-
"acrylblends/method-override": ["./
|
|
38
|
-
"acrylblends/powered-by": ["./
|
|
39
|
-
"acrylblends/pretty-json": ["./
|
|
40
|
-
"acrylblends/request-id": ["./
|
|
41
|
-
"acrylblends/secure-headers": ["./
|
|
42
|
-
"acrylblends/serve-static": ["./
|
|
43
|
-
"acrylblends/timeout": ["./
|
|
44
|
-
"acrylblends/timing": ["./
|
|
45
|
-
"acrylblends/trailing-slash": ["./
|
|
46
|
-
"acrylblends/accepts": ["./
|
|
47
|
-
"acrylblends/cookie": ["./
|
|
48
|
-
"acrylblends/factory": ["./
|
|
49
|
-
"acrylblends/html": ["./
|
|
50
|
-
"acrylblends/route": ["./
|
|
51
|
-
"acrylblends/schema-validator": ["./
|
|
52
|
-
"acrylblends/streaming": ["./
|
|
53
|
-
"acrylblends/testing": ["./
|
|
54
|
-
"acrylblends/validator": ["./
|
|
55
|
-
"acrylblends/ws": ["./
|
|
56
|
-
"acryl-router": ["./
|
|
57
|
-
"acryl-router/middleware": ["./
|
|
58
|
-
"acryl-router/helpers": ["./
|
|
59
|
-
"acrylblends/jsx": ["./
|
|
60
|
-
"acrylblends/jsx/jsx-runtime": ["./
|
|
61
|
-
"acrylblends/jsx/jsx-dev-runtime": [
|
|
62
|
-
|
|
63
|
-
],
|
|
64
|
-
"acrylblends/jsx/streaming": ["./src/plugins/acryl-router/src/core/jsx/streaming.ts"]
|
|
16
|
+
"acrylblends": ["./plugins/acryl-router/src/index.ts"],
|
|
17
|
+
"acrylblends/conninfo": ["./plugins/acryl-router/src/index.ts"],
|
|
18
|
+
"acrylblends/http-exception": ["./plugins/acryl-router/src/index.ts"],
|
|
19
|
+
"acrylblends/types": ["./plugins/acryl-router/src/index.ts"],
|
|
20
|
+
"acrylblends/client": ["./plugins/acryl-router/src/client.ts"],
|
|
21
|
+
"acrylblends/basic-auth": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
22
|
+
"acrylblends/bearer-auth": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
23
|
+
"acrylblends/body-limit": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
24
|
+
"acrylblends/combine": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
25
|
+
"acrylblends/compress": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
26
|
+
"acrylblends/context-storage": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
27
|
+
"acrylblends/cors": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
28
|
+
"acrylblends/csrf": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
29
|
+
"acrylblends/etag": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
30
|
+
"acrylblends/ip-restriction": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
31
|
+
"acrylblends/jwk": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
32
|
+
"acrylblends/jwt": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
33
|
+
"acrylblends/language": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
34
|
+
"acrylblends/logger": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
35
|
+
"acrylblends/loopback-only": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
36
|
+
"acrylblends/method-not-allowed": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
37
|
+
"acrylblends/method-override": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
38
|
+
"acrylblends/powered-by": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
39
|
+
"acrylblends/pretty-json": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
40
|
+
"acrylblends/request-id": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
41
|
+
"acrylblends/secure-headers": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
42
|
+
"acrylblends/serve-static": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
43
|
+
"acrylblends/timeout": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
44
|
+
"acrylblends/timing": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
45
|
+
"acrylblends/trailing-slash": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
46
|
+
"acrylblends/accepts": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
47
|
+
"acrylblends/cookie": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
48
|
+
"acrylblends/factory": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
49
|
+
"acrylblends/html": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
50
|
+
"acrylblends/route": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
51
|
+
"acrylblends/schema-validator": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
52
|
+
"acrylblends/streaming": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
53
|
+
"acrylblends/testing": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
54
|
+
"acrylblends/validator": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
55
|
+
"acrylblends/ws": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
56
|
+
"acryl-router": ["./plugins/acryl-router/src/index.ts"],
|
|
57
|
+
"acryl-router/middleware": ["./plugins/acryl-router/src/middleware/index.ts"],
|
|
58
|
+
"acryl-router/helpers": ["./plugins/acryl-router/src/helpers/index.ts"],
|
|
59
|
+
"acrylblends/jsx": ["./plugins/acryl-router/src/core/jsx/index.ts"],
|
|
60
|
+
"acrylblends/jsx/jsx-runtime": ["./plugins/acryl-router/src/core/jsx/jsx-runtime.ts"],
|
|
61
|
+
"acrylblends/jsx/jsx-dev-runtime": ["./plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts"],
|
|
62
|
+
"acrylblends/jsx/streaming": ["./plugins/acryl-router/src/core/jsx/streaming.ts"]
|
|
65
63
|
},
|
|
66
64
|
"jsx": "react-jsx",
|
|
67
65
|
"jsxImportSource": "#acryl-jsx"
|
|
68
66
|
},
|
|
69
|
-
"include": ["src", "test", "
|
|
67
|
+
"include": ["src", "test", "plugins"]
|
|
70
68
|
}
|
|
@@ -2,4 +2,4 @@
|
|
|
2
2
|
## This app (blank)
|
|
3
3
|
|
|
4
4
|
`src/routes.ts` is the app's own plugin: add routes to its `App`. `blend.yaml` lists the plugins that are mounted. `acrylblends add <plugin>` copies
|
|
5
|
-
another library plugin (for example `acryl-state`) into `
|
|
5
|
+
another library plugin (for example `acryl-state`) into `plugins/`.
|
|
@@ -10,13 +10,13 @@ npm test
|
|
|
10
10
|
npm run check # type-check
|
|
11
11
|
```
|
|
12
12
|
|
|
13
|
-
- `src/main.ts` chooses where the app lives (`
|
|
13
|
+
- `src/main.ts` chooses where the app lives (`plugins/instance`) and starts it.
|
|
14
14
|
- `blend.yaml` is the app as data: which plugins it mounts, in what order, with what config and parameters. Switch
|
|
15
15
|
one off with `disabled: true`. `npm run lock` writes `blend.lock.json`: the manifest's digest and a digest of
|
|
16
16
|
every plugin folder, so a diff of the lock shows what changed.
|
|
17
17
|
- `src/app.ts` holds the registry of plugins the code provides, and mounts the manifest's rows.
|
|
18
18
|
- `src/routes.ts` is your own plugin: it builds an `App` (`import { App } from './plugins/acryl-router/src/index.ts'`) and mounts it with `ctx.router.route(prefix, app)`. Routes added to the app after it is mounted are not served. A page mounted at `/x` answers `/x`, not `/x/` (trailing slashes are strict). The router API is documented at https://acrylblends.github.io/docs/framework/the-acryl-framework and in `docs/framework` of the acrylblends repository.
|
|
19
|
-
- `
|
|
19
|
+
- `plugins/` holds the plugins copied from the ACRYLBLENDS library. They are yours now; edit them freely.
|
|
20
20
|
Add more with `acrylblends add <plugin>`.
|
|
21
21
|
|
|
22
22
|
`npm run save -- "message"` commits the app with git (and pushes when it has a remote). It refuses a file that looks like
|
|
@@ -4,20 +4,20 @@
|
|
|
4
4
|
"private": true,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"imports": {
|
|
7
|
-
"#acryl-jsx/jsx-runtime": "./
|
|
8
|
-
"#acryl-jsx/jsx-dev-runtime": "./
|
|
7
|
+
"#acryl-jsx/jsx-runtime": "./plugins/acryl-router/src/core/jsx/jsx-runtime.ts",
|
|
8
|
+
"#acryl-jsx/jsx-dev-runtime": "./plugins/acryl-router/src/core/jsx/jsx-dev-runtime.ts"
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
11
|
"node": ">=22.19.0"
|
|
12
12
|
},
|
|
13
13
|
"scripts": {
|
|
14
|
-
"start": "node --import ./
|
|
15
|
-
"dev": "node --watch --import ./
|
|
16
|
-
"test": "node --import ./
|
|
14
|
+
"start": "node --import ./plugins/acryl-node-server/src/register.ts src/main.ts",
|
|
15
|
+
"dev": "node --watch --import ./plugins/acryl-node-server/src/register.ts src/main.ts",
|
|
16
|
+
"test": "node --import ./plugins/acryl-node-server/src/register.ts --test \"test/**/*.test.{ts,tsx}\"",
|
|
17
17
|
"check": "tsc --noEmit",
|
|
18
|
-
"lock": "node
|
|
19
|
-
"save": "node
|
|
20
|
-
"upgrade": "node
|
|
18
|
+
"lock": "node plugins/manifest/lock.ts",
|
|
19
|
+
"save": "node plugins/save/cli.ts",
|
|
20
|
+
"upgrade": "node plugins/manifest/upgrade-cli.ts"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
23
|
"typescript": "6.0.3"
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/** A page written in JSX: the scripts in package.json run Node.js with the vendored register hook (
|
|
1
|
+
/** A page written in JSX: the scripts in package.json run Node.js with the vendored register hook (plugins/acryl-node-server/src/register.ts), which compiles .tsx with this app's own typescript. */
|
|
2
2
|
const About = () => (
|
|
3
3
|
<main>
|
|
4
4
|
<h1>About</h1>
|
|
5
|
-
<p>Rendered on the server with the JSX runtime in
|
|
5
|
+
<p>Rendered on the server with the JSX runtime in plugins/acryl-router.</p>
|
|
6
6
|
</main>
|
|
7
7
|
)
|
|
8
8
|
|
|
@@ -1,15 +1,15 @@
|
|
|
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 nodeServer from '
|
|
5
|
-
import * as router from '
|
|
6
|
-
import { type AppInstance, instance } from '
|
|
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
7
|
import {
|
|
8
8
|
blueprintsIn,
|
|
9
9
|
compose,
|
|
10
10
|
loadManifest,
|
|
11
11
|
type ParameterValue,
|
|
12
|
-
} from '
|
|
12
|
+
} from '../plugins/manifest/index.ts'
|
|
13
13
|
import { routes } from './routes.ts'
|
|
14
14
|
|
|
15
15
|
/** The plugins this app's code provides. `blend.yaml` chooses, orders and configures them. */
|
|
@@ -30,8 +30,19 @@ export function readManifest(values?: Readonly<Record<string, ParameterValue>>)
|
|
|
30
30
|
})
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/** The manifest without its server row: the app is then reached in process, through `root.router.request`. */
|
|
34
|
+
function withoutServer<M extends { plugins: readonly { use: string }[] }>(manifest: M): M {
|
|
35
|
+
return {
|
|
36
|
+
...manifest,
|
|
37
|
+
plugins: manifest.plugins.map((row) =>
|
|
38
|
+
row.use === 'acryl-node-server' ? { ...row, disabled: true } : row,
|
|
39
|
+
),
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
export interface App {
|
|
34
44
|
readonly root: Context
|
|
45
|
+
/** The address the server listens on; empty when the app was made with `listen: false`. */
|
|
35
46
|
readonly url: string
|
|
36
47
|
stop(): Promise<void>
|
|
37
48
|
}
|
|
@@ -39,7 +50,11 @@ export interface App {
|
|
|
39
50
|
/** The app: `instance` first (where it lives, chosen at run time), then the manifest's rows in order. */
|
|
40
51
|
export async function createApp(
|
|
41
52
|
chosen: AppInstance,
|
|
42
|
-
options: {
|
|
53
|
+
options: {
|
|
54
|
+
values?: Readonly<Record<string, ParameterValue>>
|
|
55
|
+
/** 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. */
|
|
56
|
+
listen?: boolean
|
|
57
|
+
} = {},
|
|
43
58
|
): Promise<App> {
|
|
44
59
|
const manifest = readManifest(options.values)
|
|
45
60
|
const root = new Context()
|
|
@@ -47,13 +62,18 @@ export async function createApp(
|
|
|
47
62
|
await first.await()
|
|
48
63
|
const fibers: Fiber[] = [
|
|
49
64
|
first,
|
|
50
|
-
...(await compose(
|
|
51
|
-
|
|
52
|
-
|
|
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
|
+
)),
|
|
53
73
|
]
|
|
54
74
|
return {
|
|
55
75
|
root,
|
|
56
|
-
url: root.nodeServer.url,
|
|
76
|
+
url: options.listen === false ? '' : root.nodeServer.url,
|
|
57
77
|
async stop() {
|
|
58
78
|
for (const fiber of [...fibers].reverse()) await fiber.dispose()
|
|
59
79
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url'
|
|
2
|
+
import { selectInstance } from '../plugins/instance/index.ts'
|
|
2
3
|
import { createApp } from './app.ts'
|
|
3
|
-
import { selectInstance } from './plugins/instance/index.ts'
|
|
4
4
|
import { APP_NAME } from './routes.ts'
|
|
5
5
|
|
|
6
6
|
// The composition root: the one place that reads the environment (through selectInstance) and chooses where the app lives.
|