acrylblends 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -0
- package/dist/index.d.ts +45 -1
- package/dist/index.js +1980 -27
- 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 +1 -1
- package/docs/framework/workshop/README.md +138 -152
- 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 +42 -7
- package/lib/cordis/door.js +204 -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/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 +53 -23
- 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 +3 -0
- package/plugins/acryl-door/src/index.ts +166 -29
- 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/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 +26 -1
- package/plugins/extensions/resolve.ts +13 -0
- 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/snapshot.ts +275 -0
- package/plugins/openai/index.ts +64 -5
- package/plugins/openai/prompt.ts +23 -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/AGENTS.md +1 -1
- package/templates/agent/blend.yaml +4 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +1 -0
- package/templates/agent/src/app.ts +150 -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 +2 -0
- package/templates/agent/test/app.test.ts +45 -3
- 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/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/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/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/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
|
@@ -5,6 +5,8 @@
|
|
|
5
5
|
* - The drivers are `terminal` entries of the panel's `drivers` service (`pty:claude`, ...). A panel without this plugin lists none; this plugin without a panel is pending.
|
|
6
6
|
* - Transport is server-sent events down and POST up (no WebSocket): `POST /api/driver-pty/sessions`, `GET /api/driver-pty/<id>/output` (events with ids; a reconnect replays
|
|
7
7
|
* from `Last-Event-ID`, from a ring buffer of at least 64 KB), `POST .../input`, `.../resize`, `.../stop`. Same origin check and token as the panel, development only.
|
|
8
|
+
* - The routes live under the app's mount, like the panel's: `/api/driver-pty` on the app's own server, `<mount>/api/driver-pty` when a host serves the app at a prefix
|
|
9
|
+
* (the frame's `/app`). The row's `mount` config names it, and each driver carries its view (`view: { module, prefix }`) so the panel calls the mounted paths.
|
|
8
10
|
* - Every process is acquired inside one `ctx.effect`; its disposer ends the process (group), closes the streams and writes the audit line. Unloading or reloading the
|
|
9
11
|
* plugin with a session open leaves no process behind.
|
|
10
12
|
*/
|
|
@@ -16,8 +18,9 @@ import { streamSSE } from '../acryl-router/src/helpers/index.ts'
|
|
|
16
18
|
import { Acryl } from '../acryl-router/src/index.ts'
|
|
17
19
|
import type { TerminalDriver, TerminalSession } from '../agent-panel/drivers.ts'
|
|
18
20
|
import { onPath } from '../agent-panel/headless.ts'
|
|
21
|
+
import { mountedBase, mountedPrefix } from '../agent-panel/mount.ts'
|
|
19
22
|
import type {} from '../agent-panel/registry.ts'
|
|
20
|
-
import { loadNodePty, type PtySpawn } from './spawn.ts'
|
|
23
|
+
import { loadNodePty, type PtySpawn, resolveProgram } from './spawn.ts'
|
|
21
24
|
import { terminalDriver } from './terminal.ts'
|
|
22
25
|
|
|
23
26
|
export const name = 'driver-pty'
|
|
@@ -26,7 +29,7 @@ export const inject = ['drivers', 'router', 'appInstance']
|
|
|
26
29
|
export type { PtyProcess, PtySpawn } from './spawn.ts'
|
|
27
30
|
export { terminalDriver } from './terminal.ts'
|
|
28
31
|
|
|
29
|
-
const
|
|
32
|
+
const DRIVER_PATH = '/api/driver-pty'
|
|
30
33
|
const CLIENT_DIR = join(import.meta.dirname, 'client')
|
|
31
34
|
/** The least output a reconnecting page can be given again. */
|
|
32
35
|
const SCROLLBACK = 64 * 1024
|
|
@@ -36,6 +39,11 @@ export interface DriverPtyConfig {
|
|
|
36
39
|
programs?: Record<string, string[]>
|
|
37
40
|
/** The PTY implementation; default node-pty. Tests give a fake. */
|
|
38
41
|
spawn?: PtySpawn
|
|
42
|
+
/**
|
|
43
|
+
* The URL prefix the app is mounted at: empty on the app's own server, `/app` when the ACRYL frame hosts it (the same `mount` the panel takes). The
|
|
44
|
+
* terminal's routes and its browser view live under it, so the terminal belongs to the app and is served wherever the app is.
|
|
45
|
+
*/
|
|
46
|
+
mount?: string
|
|
39
47
|
}
|
|
40
48
|
|
|
41
49
|
interface Chunk {
|
|
@@ -61,6 +69,9 @@ interface Live {
|
|
|
61
69
|
|
|
62
70
|
export async function apply(ctx: Context, config: DriverPtyConfig = {}): Promise<void> {
|
|
63
71
|
if (process.env.NODE_ENV === 'production' || process.env.ACRYL_DOOR === 'off') return
|
|
72
|
+
const PREFIX = mountedPrefix(config.mount, DRIVER_PATH, 'driver-pty')
|
|
73
|
+
// The routes mount at the app's own mount and keep the fixed driver path inside (the frame's router prefixes the mount, not the routes).
|
|
74
|
+
const MOUNT = mountedBase(config.mount, 'driver-pty')
|
|
64
75
|
const root = ctx.appInstance.root
|
|
65
76
|
let spawn = config.spawn
|
|
66
77
|
if (!spawn) {
|
|
@@ -80,7 +91,15 @@ export async function apply(ctx: Context, config: DriverPtyConfig = {}): Promise
|
|
|
80
91
|
(['claude', 'codex', 'pi'] as const).filter(onPath).map((p) => [p, [] as string[]]),
|
|
81
92
|
)
|
|
82
93
|
for (const [program, args] of Object.entries(programs))
|
|
83
|
-
ctx.effect(() =>
|
|
94
|
+
ctx.effect(() =>
|
|
95
|
+
ctx.drivers.register(
|
|
96
|
+
// The name stays the program the app configured; the PTY runs its full path (Windows does not search PATH itself).
|
|
97
|
+
terminalDriver(`pty:${program}`, resolveProgram(program), args, spawn, {
|
|
98
|
+
module: `${PREFIX}/client/terminal.ts`,
|
|
99
|
+
prefix: PREFIX,
|
|
100
|
+
}),
|
|
101
|
+
),
|
|
102
|
+
)
|
|
84
103
|
|
|
85
104
|
const auditFile = join(root, '.acryl', 'audit', 'driver-pty.jsonl')
|
|
86
105
|
const audit = (entry: Record<string, unknown>): void => {
|
|
@@ -162,8 +181,8 @@ export async function apply(ctx: Context, config: DriverPtyConfig = {}): Promise
|
|
|
162
181
|
ctx.effect(() => off)
|
|
163
182
|
|
|
164
183
|
const app = new Acryl()
|
|
165
|
-
.route(`${
|
|
166
|
-
.post(`${
|
|
184
|
+
.route(`${DRIVER_PATH}/client`, clientModules({ root: CLIENT_DIR, mount: `${PREFIX}/client` }))
|
|
185
|
+
.post(`${DRIVER_PATH}/sessions`, async (c) => {
|
|
167
186
|
if (!ctx.drivers.authorized(c.req.raw.headers)) return refuse(c)
|
|
168
187
|
const body = (await c.req.json().catch(() => ({}))) as {
|
|
169
188
|
driver?: unknown
|
|
@@ -177,10 +196,18 @@ export async function apply(ctx: Context, config: DriverPtyConfig = {}): Promise
|
|
|
177
196
|
return c.json({ error: 'a terminal is already open; stop it first' }, 409)
|
|
178
197
|
const num = (value: unknown, fallback: number) =>
|
|
179
198
|
typeof value === 'number' && value >= 1 && value <= 1000 ? Math.floor(value) : fallback
|
|
180
|
-
|
|
199
|
+
let session: Live
|
|
200
|
+
try {
|
|
201
|
+
session = startSession(driver, num(body.cols, 80), num(body.rows, 24))
|
|
202
|
+
} catch (error) {
|
|
203
|
+
// The cause (a native module that cannot spawn, a program that is gone) goes to the app's log; the page gets a plain answer, as everywhere else.
|
|
204
|
+
console.error('driver-pty: the terminal could not start:', error)
|
|
205
|
+
return c.json({ error: 'the terminal could not start; the app log has the reason' }, 500)
|
|
206
|
+
}
|
|
207
|
+
live = session
|
|
181
208
|
return c.json({ id: live.id })
|
|
182
209
|
})
|
|
183
|
-
.get(`${
|
|
210
|
+
.get(`${DRIVER_PATH}/:id/output`, (c) => {
|
|
184
211
|
if (!ctx.drivers.authorized(c.req.raw.headers)) return refuse(c)
|
|
185
212
|
const current = find(c.req.param('id'))
|
|
186
213
|
if (!current) return c.json({ error: 'no such terminal' }, 404)
|
|
@@ -216,7 +243,7 @@ export async function apply(ctx: Context, config: DriverPtyConfig = {}): Promise
|
|
|
216
243
|
})
|
|
217
244
|
})
|
|
218
245
|
})
|
|
219
|
-
.post(`${
|
|
246
|
+
.post(`${DRIVER_PATH}/:id/input`, async (c) => {
|
|
220
247
|
if (!ctx.drivers.authorized(c.req.raw.headers)) return refuse(c)
|
|
221
248
|
const current = find(c.req.param('id'))
|
|
222
249
|
if (!current || current.exit) return c.json({ error: 'no such terminal' }, 404)
|
|
@@ -226,7 +253,7 @@ export async function apply(ctx: Context, config: DriverPtyConfig = {}): Promise
|
|
|
226
253
|
current.session.write(data)
|
|
227
254
|
return c.json({ ok: true })
|
|
228
255
|
})
|
|
229
|
-
.post(`${
|
|
256
|
+
.post(`${DRIVER_PATH}/:id/resize`, async (c) => {
|
|
230
257
|
if (!ctx.drivers.authorized(c.req.raw.headers)) return refuse(c)
|
|
231
258
|
const current = find(c.req.param('id'))
|
|
232
259
|
if (!current || current.exit) return c.json({ error: 'no such terminal' }, 404)
|
|
@@ -239,12 +266,12 @@ export async function apply(ctx: Context, config: DriverPtyConfig = {}): Promise
|
|
|
239
266
|
current.session.resize(cols, rows)
|
|
240
267
|
return c.json({ ok: true })
|
|
241
268
|
})
|
|
242
|
-
.post(`${
|
|
269
|
+
.post(`${DRIVER_PATH}/:id/stop`, async (c) => {
|
|
243
270
|
if (!ctx.drivers.authorized(c.req.raw.headers)) return refuse(c)
|
|
244
271
|
const current = find(c.req.param('id'))
|
|
245
272
|
if (!current) return c.json({ error: 'no such terminal' }, 404)
|
|
246
273
|
await current.dispose()
|
|
247
274
|
return c.json({ ok: true })
|
|
248
275
|
})
|
|
249
|
-
ctx.router.route('/', app)
|
|
276
|
+
ctx.router.route(MOUNT === '' ? '/' : MOUNT, app)
|
|
250
277
|
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** The one place that touches `node-pty`, a native module. Everything else works on this small seam, so tests run on a fake process. */
|
|
2
|
+
import { statSync } from 'node:fs'
|
|
3
|
+
import { delimiter, join } from 'node:path'
|
|
2
4
|
|
|
3
5
|
export interface PtyProcess {
|
|
4
6
|
readonly pid: number
|
|
@@ -16,6 +18,42 @@ export type PtySpawn = (
|
|
|
16
18
|
options: { cwd: string; env: NodeJS.ProcessEnv; cols: number; rows: number },
|
|
17
19
|
) => PtyProcess
|
|
18
20
|
|
|
21
|
+
/**
|
|
22
|
+
* The file a terminal runs: a full path. On Windows node-pty runs the file as it is given and does not search PATH - a bare `node` is "File not found" - so
|
|
23
|
+
* a bare program name is looked up once, with the platform's PATH separator and, on Windows, PATHEXT. A name that is not found comes back unchanged, and
|
|
24
|
+
* the start fails with the PTY's own error.
|
|
25
|
+
*/
|
|
26
|
+
export function resolveProgram(
|
|
27
|
+
program: string,
|
|
28
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
29
|
+
platform: NodeJS.Platform = process.platform,
|
|
30
|
+
): string {
|
|
31
|
+
if (program === '' || program.includes('/') || (platform === 'win32' && program.includes('\\')))
|
|
32
|
+
return program
|
|
33
|
+
const windows = platform === 'win32'
|
|
34
|
+
const folders = (env.PATH ?? env.Path ?? '').split(windows ? ';' : delimiter)
|
|
35
|
+
const lower = program.toLowerCase()
|
|
36
|
+
const known = (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';').filter(Boolean)
|
|
37
|
+
const suffixes = !windows
|
|
38
|
+
? ['']
|
|
39
|
+
: known.some((extension) => lower.endsWith(extension.toLowerCase()))
|
|
40
|
+
? ['']
|
|
41
|
+
: ['', ...new Set(known)]
|
|
42
|
+
for (const folder of folders) {
|
|
43
|
+
if (folder === '') continue
|
|
44
|
+
for (const suffix of suffixes) {
|
|
45
|
+
const candidate = join(folder, `${program}${suffix}`)
|
|
46
|
+
try {
|
|
47
|
+
const stat = statSync(candidate)
|
|
48
|
+
if (stat.isFile() && (windows || (stat.mode & 0o111) !== 0)) return candidate
|
|
49
|
+
} catch {
|
|
50
|
+
// Not here: the next folder, or the next extension.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return program
|
|
55
|
+
}
|
|
56
|
+
|
|
19
57
|
/** Loads node-pty. Rejects with the reason (the error and the platform), so a log says why the driver is not offered. */
|
|
20
58
|
export async function loadNodePty(): Promise<PtySpawn> {
|
|
21
59
|
try {
|
|
@@ -22,10 +22,12 @@ export function terminalDriver(
|
|
|
22
22
|
program: string,
|
|
23
23
|
args: string[],
|
|
24
24
|
spawn: PtySpawn,
|
|
25
|
+
view: TerminalDriver['view'],
|
|
25
26
|
): TerminalDriver {
|
|
26
27
|
return {
|
|
27
28
|
kind: 'terminal',
|
|
28
29
|
name,
|
|
30
|
+
view,
|
|
29
31
|
start({ cwd, env, cols, rows }): TerminalSession {
|
|
30
32
|
// The agent runs `acryl look` without the folder prefix.
|
|
31
33
|
const path = [join(cwd, '.acryl', 'bin'), env.PATH].filter(Boolean).join(delimiter)
|
|
@@ -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 =
|
|
@@ -227,6 +237,8 @@ export class Extensions extends Service {
|
|
|
227
237
|
private tail: Promise<unknown> = Promise.resolve()
|
|
228
238
|
private disposed = false
|
|
229
239
|
private readonly readOnly: boolean
|
|
240
|
+
private readonly off: ReadonlySet<string>
|
|
241
|
+
private readonly rows: ReadonlySet<string>
|
|
230
242
|
private readonly say: (line: string) => void
|
|
231
243
|
/**
|
|
232
244
|
* The context the service was constructed with. Inside a service method `this.ctx` is Cordis's shadow context, which resolves services as the
|
|
@@ -238,6 +250,8 @@ export class Extensions extends Service {
|
|
|
238
250
|
super(ctx, 'extensions')
|
|
239
251
|
this.owner = ctx
|
|
240
252
|
this.readOnly = config.readOnly === true
|
|
253
|
+
this.off = new Set(config.off ?? [])
|
|
254
|
+
this.rows = new Set(config.rows ?? [])
|
|
241
255
|
this.say = config.log ?? ((line) => console.log(line))
|
|
242
256
|
const { home, root } = ctx.appInstance
|
|
243
257
|
this.sources = (config.sources ?? [{ dir: join(home, 'extensions'), trust: 'install' }]).map(
|
|
@@ -258,8 +272,9 @@ export class Extensions extends Service {
|
|
|
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',
|
|
@@ -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(', ')}`)
|
|
@@ -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)
|
|
@@ -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
|
}
|
|
@@ -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
|