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
|
@@ -31,8 +31,8 @@ import { type Context, type Fiber, Service } from '@deepseek-ai/cordis'
|
|
|
31
31
|
import type {} from '../instance/index.ts'
|
|
32
32
|
import type { Sandbox } from '../sandbox/index.ts'
|
|
33
33
|
import { appendLedger, type LedgerEntry, readLedger } from './ledger.ts'
|
|
34
|
-
import {
|
|
35
|
-
import {
|
|
34
|
+
import { type PluginStore, pluginVersion } from './plugin-store.ts'
|
|
35
|
+
import { folderPluginStore } from './stage.ts'
|
|
36
36
|
|
|
37
37
|
/** A fiber's lifecycle state, by name (Cordis's own enum is a `const enum`, which does not exist at run time). */
|
|
38
38
|
export type ExtensionState = 'pending' | 'loading' | 'active' | 'failed' | 'disposed' | 'unloading'
|
|
@@ -83,6 +83,16 @@ export interface ExtensionsConfig {
|
|
|
83
83
|
readOnly?: boolean
|
|
84
84
|
/** Where the read-only start says what it did (the console by default). */
|
|
85
85
|
log?: (line: string) => void
|
|
86
|
+
/**
|
|
87
|
+
* Plugins of the sources that are switched off (Settings → Plugins writes this list into `blend.yaml`): they are not started, in development or
|
|
88
|
+
* production, until taken off the list. Their folders stay as they are.
|
|
89
|
+
*/
|
|
90
|
+
off?: readonly string[]
|
|
91
|
+
/**
|
|
92
|
+
* Plugins of the sources that `blend.yaml` mounts as rows of their own (the app composes them in its row order, with their row's config): they
|
|
93
|
+
* are not extensions, so they are neither found nor started here, and installing one is refused with that reason.
|
|
94
|
+
*/
|
|
95
|
+
rows?: readonly string[]
|
|
86
96
|
}
|
|
87
97
|
|
|
88
98
|
const READ_ONLY =
|
|
@@ -214,7 +224,6 @@ interface StateService {
|
|
|
214
224
|
|
|
215
225
|
export class Extensions extends Service {
|
|
216
226
|
readonly sources: readonly ExtensionSource[]
|
|
217
|
-
readonly stageRoot: string
|
|
218
227
|
readonly stateFile: string
|
|
219
228
|
readonly ledgerFile: string
|
|
220
229
|
/** Settles when the startup pass is done. */
|
|
@@ -227,17 +236,23 @@ export class Extensions extends Service {
|
|
|
227
236
|
private tail: Promise<unknown> = Promise.resolve()
|
|
228
237
|
private disposed = false
|
|
229
238
|
private readonly readOnly: boolean
|
|
239
|
+
private readonly off: ReadonlySet<string>
|
|
240
|
+
private readonly rows: ReadonlySet<string>
|
|
230
241
|
private readonly say: (line: string) => void
|
|
231
242
|
/**
|
|
232
243
|
* The context the service was constructed with. Inside a service method `this.ctx` is Cordis's shadow context, which resolves services as the
|
|
233
244
|
* service's own fiber, so a plugin mounted from it would read `ctx.router` from the installer's fiber and fail. Extensions mount from this one.
|
|
234
245
|
*/
|
|
235
246
|
private readonly owner: Context
|
|
247
|
+
private readonly store: PluginStore
|
|
236
248
|
|
|
237
|
-
constructor(ctx: Context, config: ExtensionsConfig
|
|
249
|
+
constructor(ctx: Context, config: ExtensionsConfig, store: PluginStore) {
|
|
238
250
|
super(ctx, 'extensions')
|
|
239
251
|
this.owner = ctx
|
|
252
|
+
this.store = store
|
|
240
253
|
this.readOnly = config.readOnly === true
|
|
254
|
+
this.off = new Set(config.off ?? [])
|
|
255
|
+
this.rows = new Set(config.rows ?? [])
|
|
241
256
|
this.say = config.log ?? ((line) => console.log(line))
|
|
242
257
|
const { home, root } = ctx.appInstance
|
|
243
258
|
this.sources = (config.sources ?? [{ dir: join(home, 'extensions'), trust: 'install' }]).map(
|
|
@@ -246,20 +261,20 @@ export class Extensions extends Service {
|
|
|
246
261
|
trust: config.approval === true && source.trust === 'install' ? 'ask' : source.trust,
|
|
247
262
|
}),
|
|
248
263
|
)
|
|
249
|
-
this.stageRoot = join(home, 'extensions-staged')
|
|
250
264
|
this.stateFile = join(home, 'extensions.json')
|
|
251
265
|
this.ledgerFile = join(home, 'extensions-ledger.jsonl')
|
|
252
266
|
// Effects run their disposers newest first: settle pending operations, then dispose every mounted extension, then drop the hook.
|
|
253
267
|
// Staged copies live in the app's home, outside the app: their bare imports are resolved from the app. A read-only start imports the folders where they are (inside the app).
|
|
254
268
|
if (!this.readOnly)
|
|
255
269
|
ctx.effect(
|
|
256
|
-
() =>
|
|
270
|
+
() => this.store.resolveImportsFrom(root),
|
|
257
271
|
'extensions: resolve staged imports from the app',
|
|
258
272
|
)
|
|
259
273
|
ctx.effect(
|
|
260
274
|
() => async () => {
|
|
275
|
+
// A fiber already being disposed (an app reload) can return nothing here, not a promise.
|
|
261
276
|
for (const entry of [...this.installed.values()].reverse())
|
|
262
|
-
await entry.fiber.dispose().catch(() => undefined)
|
|
277
|
+
await Promise.resolve(entry.fiber.dispose()).catch(() => undefined)
|
|
263
278
|
this.installed.clear()
|
|
264
279
|
},
|
|
265
280
|
'extensions: dispose mounted extensions',
|
|
@@ -302,7 +317,7 @@ export class Extensions extends Service {
|
|
|
302
317
|
this.stale.add(entry.name)
|
|
303
318
|
continue
|
|
304
319
|
}
|
|
305
|
-
if (
|
|
320
|
+
if (pluginVersion(entry.source) !== entry.version)
|
|
306
321
|
results.push(await this.installNow(entry.source))
|
|
307
322
|
}
|
|
308
323
|
for (const found of this.discover()) {
|
|
@@ -350,7 +365,7 @@ export class Extensions extends Service {
|
|
|
350
365
|
}
|
|
351
366
|
}
|
|
352
367
|
this.requests.delete(name)
|
|
353
|
-
if (!existsSync(request.dir) ||
|
|
368
|
+
if (!existsSync(request.dir) || pluginVersion(request.dir) !== request.version) {
|
|
354
369
|
return {
|
|
355
370
|
ok: false,
|
|
356
371
|
stage: 'approval',
|
|
@@ -417,6 +432,8 @@ export class Extensions extends Service {
|
|
|
417
432
|
entry.isDirectory() &&
|
|
418
433
|
NAME.test(entry.name) &&
|
|
419
434
|
!library.has(entry.name) &&
|
|
435
|
+
!this.off.has(entry.name) &&
|
|
436
|
+
!this.rows.has(entry.name) &&
|
|
420
437
|
ENTRIES.some((file) => existsSync(join(dir, file)))
|
|
421
438
|
) {
|
|
422
439
|
if (!found.some((other) => other.name === entry.name))
|
|
@@ -442,6 +459,10 @@ export class Extensions extends Service {
|
|
|
442
459
|
errors.push(
|
|
443
460
|
`the folder name "${name}" must be lowercase letters, digits and dashes; it is the extension's name`,
|
|
444
461
|
)
|
|
462
|
+
if (this.rows.has(name))
|
|
463
|
+
errors.push(
|
|
464
|
+
`"${name}" is a row of blend.yaml, so the app starts it, in its row order and with its row's config; it is not installed here. Change its folder and the running app loads it again (npm run dev); npm test checks it`,
|
|
465
|
+
)
|
|
445
466
|
if (!existsSync(dir) || !statSync(dir).isDirectory()) errors.push(`"${dir}" is not a folder`)
|
|
446
467
|
else if (!ENTRIES.some((file) => existsSync(join(dir, file))))
|
|
447
468
|
errors.push(`"${dir}" has no ${ENTRIES.join(', ')}`)
|
|
@@ -596,7 +617,7 @@ export class Extensions extends Service {
|
|
|
596
617
|
const { name, dir, errors } = this.check(source)
|
|
597
618
|
if (errors.length > 0) return { ok: false, stage: 'check', name, errors, next: NOTHING }
|
|
598
619
|
const previous = this.installed.get(name)
|
|
599
|
-
const version =
|
|
620
|
+
const version = pluginVersion(dir)
|
|
600
621
|
const running =
|
|
601
622
|
previous && previous.version === version && STATES[previous.fiber.state] !== 'failed'
|
|
602
623
|
// A source that asks: this exact version must have been approved. Anything else waits, and the ledger says so.
|
|
@@ -627,12 +648,12 @@ export class Extensions extends Service {
|
|
|
627
648
|
}
|
|
628
649
|
|
|
629
650
|
// Import the new version while the old one keeps running: a syntax error or a missing export changes nothing.
|
|
630
|
-
const staged =
|
|
651
|
+
const staged = this.store.put(name, dir)
|
|
631
652
|
let module: PluginModule
|
|
632
653
|
try {
|
|
633
654
|
module = await this.load(staged.dir, name)
|
|
634
655
|
} catch (error) {
|
|
635
|
-
discard(staged
|
|
656
|
+
this.store.discard(staged)
|
|
636
657
|
this.record({ kind: 'refused', name, version: staged.version, error: message(error) })
|
|
637
658
|
return {
|
|
638
659
|
ok: false,
|
|
@@ -659,7 +680,7 @@ export class Extensions extends Service {
|
|
|
659
680
|
: undefined
|
|
660
681
|
if (failure) {
|
|
661
682
|
await mounted.fiber.dispose().catch(() => undefined)
|
|
662
|
-
discard(staged
|
|
683
|
+
this.store.discard(staged)
|
|
663
684
|
const error = failure.error
|
|
664
685
|
this.record({ kind: 'refused', name, version: staged.version, error })
|
|
665
686
|
let restored = false
|
|
@@ -696,7 +717,7 @@ export class Extensions extends Service {
|
|
|
696
717
|
})
|
|
697
718
|
this.stale.delete(name)
|
|
698
719
|
this.saveState()
|
|
699
|
-
|
|
720
|
+
this.store.keepOnly(name, staged)
|
|
700
721
|
this.record({ kind: action, name, version: staged.version })
|
|
701
722
|
this.ctx.emit('extensions/changed', name, action)
|
|
702
723
|
return {
|
|
@@ -716,7 +737,7 @@ export class Extensions extends Service {
|
|
|
716
737
|
this.installed.delete(name)
|
|
717
738
|
this.stale.delete(name)
|
|
718
739
|
this.saveState()
|
|
719
|
-
|
|
740
|
+
this.store.keepOnly(name)
|
|
720
741
|
this.record({ kind: 'removed', name, version: entry.version })
|
|
721
742
|
this.ctx.emit('extensions/changed', name, 'removed')
|
|
722
743
|
return { ok: true, name }
|
|
@@ -741,7 +762,7 @@ export class Extensions extends Service {
|
|
|
741
762
|
this.installed.set(folder.name, {
|
|
742
763
|
name: folder.name,
|
|
743
764
|
source: folder.dir,
|
|
744
|
-
version:
|
|
765
|
+
version: pluginVersion(folder.dir),
|
|
745
766
|
dir: folder.dir,
|
|
746
767
|
module,
|
|
747
768
|
fiber: mounted.fiber,
|
|
@@ -788,6 +809,10 @@ export class Extensions extends Service {
|
|
|
788
809
|
summary.failed.push({ name: 'extensions.json', error: message(error) })
|
|
789
810
|
}
|
|
790
811
|
for (const [name, record] of Object.entries(records)) {
|
|
812
|
+
// Switched off: not started, and dropped from the record; switched on again, its folder is found and installed as new.
|
|
813
|
+
if (this.off.has(name)) continue
|
|
814
|
+
// Now a row of blend.yaml: the app starts it, and the record of its install is dropped.
|
|
815
|
+
if (this.rows.has(name)) continue
|
|
791
816
|
try {
|
|
792
817
|
// What was installed before was approved then: an unchanged source starts again without asking.
|
|
793
818
|
this.approved.set(name, record.version)
|
|
@@ -800,7 +825,7 @@ export class Extensions extends Service {
|
|
|
800
825
|
// The source is gone: the last installed version still runs, and is reported as stale.
|
|
801
826
|
summary.stale.push(name)
|
|
802
827
|
this.stale.add(name)
|
|
803
|
-
if (!
|
|
828
|
+
if (!this.store.holds(record)) {
|
|
804
829
|
summary.failed.push({ name, error: 'neither the source nor the staged copy exists' })
|
|
805
830
|
continue
|
|
806
831
|
}
|
|
@@ -844,7 +869,11 @@ export const extensions = {
|
|
|
844
869
|
name: 'extensions',
|
|
845
870
|
inject: ['appInstance'],
|
|
846
871
|
async apply(ctx: Context, config: ExtensionsConfig) {
|
|
847
|
-
const service = new Extensions(
|
|
872
|
+
const service = new Extensions(
|
|
873
|
+
ctx,
|
|
874
|
+
config ?? {},
|
|
875
|
+
folderPluginStore(join(ctx.appInstance.home, 'extensions-staged')),
|
|
876
|
+
)
|
|
848
877
|
await service.ready
|
|
849
878
|
},
|
|
850
879
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where installed versions of a plugin are kept, as the install transaction needs it. The transaction (`index.ts`) owns this port and says nothing about folders,
|
|
3
|
+
* copies or URLs; `stage.ts` is the adapter that keeps them on disk (Clean Architecture: a boundary owned by the use case; the layout is a detail).
|
|
4
|
+
*/
|
|
5
|
+
import { digestFolder } from '@webboxes/blends-core'
|
|
6
|
+
|
|
7
|
+
/** One kept version of a plugin. `dir` is what the transaction imports it from. */
|
|
8
|
+
export interface StoredPlugin {
|
|
9
|
+
readonly dir: string
|
|
10
|
+
readonly version: string
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface PluginStore {
|
|
14
|
+
/** Keep this source as a version of `name` and return it. An unchanged source returns the version already kept. */
|
|
15
|
+
put(name: string, source: string): StoredPlugin
|
|
16
|
+
/** Whether this version, recorded earlier, is still here. */
|
|
17
|
+
holds(plugin: StoredPlugin): boolean
|
|
18
|
+
/** Drop a version that was refused. */
|
|
19
|
+
discard(plugin: StoredPlugin): void
|
|
20
|
+
/** Drop every version of `name` but `plugin`; all of them when none is given. */
|
|
21
|
+
keepOnly(name: string, plugin?: StoredPlugin): void
|
|
22
|
+
/** Let kept plugins import what the app at `appRoot` provides, until the returned function is called. */
|
|
23
|
+
resolveImportsFrom(appRoot: string): () => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** A short content digest of a plugin's folder: an unchanged source keeps its version. */
|
|
27
|
+
export const pluginVersion = (dir: string): string => digestFolder(dir, 12)
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
"provides": ["extensions"],
|
|
7
7
|
"dependencies": {
|
|
8
8
|
"@deepseek-ai/cordis": "4.0.4",
|
|
9
|
-
"@webboxes/blends-core": "0.
|
|
9
|
+
"@webboxes/blends-core": "0.5.0"
|
|
10
10
|
},
|
|
11
11
|
"mount": "mount it after instance: root.plugin(extensions, { sources: [{ dir: join(root, 'plugins'), trust: 'install' }] })"
|
|
12
12
|
}
|
|
@@ -101,6 +101,19 @@ export function routerAliases(): Record<string, string> {
|
|
|
101
101
|
// outside the app folder, where Node would not find that entry.
|
|
102
102
|
aliases[folder.name] ??= entry
|
|
103
103
|
aliases[`#${folder.name}`] = entry
|
|
104
|
+
// Its other top-level modules by subpath, as its package exports them (`acryl-node-server/client-modules`): the door imports one before any extension is installed.
|
|
105
|
+
for (const file of readdirSync(fileURLToPath(new URL(`${folder.name}/src/`, plugins)))) {
|
|
106
|
+
if (
|
|
107
|
+
!file.endsWith('.ts') ||
|
|
108
|
+
file === 'index.ts' ||
|
|
109
|
+
file.endsWith('.test.ts') ||
|
|
110
|
+
file.endsWith('.d.ts')
|
|
111
|
+
)
|
|
112
|
+
continue
|
|
113
|
+
aliases[`${folder.name}/${file.slice(0, -3)}`] ??= fileURLToPath(
|
|
114
|
+
new URL(`${folder.name}/src/${file}`, plugins),
|
|
115
|
+
)
|
|
116
|
+
}
|
|
104
117
|
const suite = fileURLToPath(new URL(`${folder.name}/src/conformance.ts`, plugins))
|
|
105
118
|
if (existsSync(suite)) aliases[`#${folder.name}/conformance`] = suite
|
|
106
119
|
}
|
|
@@ -7,10 +7,9 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import { cpSync, existsSync, mkdirSync, readdirSync, renameSync, rmSync } from 'node:fs'
|
|
9
9
|
import { basename, join } from 'node:path'
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
export const hashFolder = (dir: string, length = 12): string => digestFolder(dir, length)
|
|
10
|
+
import { FOLDER_DIGEST_SKIPPED } from '@webboxes/blends-core'
|
|
11
|
+
import { type PluginStore, pluginVersion } from './plugin-store.ts'
|
|
12
|
+
import { resolveFromApp, routerAliases } from './resolve.ts'
|
|
14
13
|
|
|
15
14
|
/**
|
|
16
15
|
* Whether `node --watch` runs this process. It watches every module the process has imported, and keeps watching the paths of the runs before; a staged copy that is deleted
|
|
@@ -40,7 +39,7 @@ export function stage(
|
|
|
40
39
|
stageRoot: string,
|
|
41
40
|
name: string,
|
|
42
41
|
): { dir: string; version: string } {
|
|
43
|
-
const version =
|
|
42
|
+
const version = pluginVersion(source)
|
|
44
43
|
const existing = staged(stageRoot, name, version)
|
|
45
44
|
if (existing !== undefined) return { dir: existing, version }
|
|
46
45
|
const stamp = String(Date.now()).padStart(15, '0')
|
|
@@ -70,3 +69,14 @@ export function prune(stageRoot: string, name: string, keep: readonly string[]):
|
|
|
70
69
|
if (!keep.includes(path)) rmSync(path, { recursive: true, force: true })
|
|
71
70
|
}
|
|
72
71
|
}
|
|
72
|
+
|
|
73
|
+
/** The plugin store of an app: its versions as staged copies under `stageRoot`. */
|
|
74
|
+
export function folderPluginStore(stageRoot: string): PluginStore {
|
|
75
|
+
return {
|
|
76
|
+
put: (name, source) => stage(source, stageRoot, name),
|
|
77
|
+
holds: ({ dir }) => existsSync(dir),
|
|
78
|
+
discard: ({ dir }) => discard(dir),
|
|
79
|
+
keepOnly: (name, plugin) => prune(stageRoot, name, plugin ? [plugin.dir] : []),
|
|
80
|
+
resolveImportsFrom: (appRoot) => resolveFromApp(stageRoot, appRoot, routerAliases()),
|
|
81
|
+
}
|
|
82
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Types for the browser client served as-is (client.js): the Host reads it as text and appends its start call; tests import the exported helpers.
|
|
2
|
+
|
|
3
|
+
/** The ACRYL frame's view handle, or undefined when the page is not in the frame's canvas. */
|
|
4
|
+
export function frameViewOf(win: {
|
|
5
|
+
readonly parent: unknown
|
|
6
|
+
}): { readonly mode: 'build' | 'preview' } | undefined
|
|
7
|
+
|
|
8
|
+
/** A short name for a picked element: what it says it is, else its heading, else the first words it shows, else its tag. */
|
|
9
|
+
export function elementLabel(element: Element): string
|
|
10
|
+
|
|
11
|
+
/** The stored size of a screenshot: the whole view, downscaled to `maxSide`. */
|
|
12
|
+
export function shotSize(
|
|
13
|
+
width: number,
|
|
14
|
+
height: number,
|
|
15
|
+
maxSide?: number,
|
|
16
|
+
): { width: number; height: number }
|
|
17
|
+
|
|
18
|
+
/** The largest screenshot the page sends (decoded bytes); the Host refuses more than 512 KB. */
|
|
19
|
+
export const SHOT_MAX_BYTES: number
|
|
20
|
+
|
|
21
|
+
/** Whether this browser can capture the current tab (Chromium only; Firefox and Safari would offer the whole screen). */
|
|
22
|
+
export function canCaptureTab(userAgent: string, hasDisplayCapture: boolean): boolean
|