acrylblends 1.0.1 → 1.0.3
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 +19 -15
- package/bin/acrylblends.js +9 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +136 -5
- package/dist/index.js.map +1 -1
- package/docs/framework/README.md +1 -0
- package/docs/framework/agents/index.md +18 -0
- package/docs/framework/cordis/migration-to-1-0.md +4 -3
- package/docs/framework/getting-started/desktop.md +20 -0
- package/docs/framework/guides/create-acrylblends.md +21 -0
- package/docs/framework/guides/data.md +12 -0
- package/docs/framework/guides/production.md +11 -1
- package/docs/framework/workshop/README.md +381 -0
- package/docs/framework/workshop/facilitator.md +107 -0
- package/lib/cordis/db-sqlite.js +10 -2
- package/lib/cordis/kv-json.js +10 -2
- package/lib/cordis/node-server.js +1 -1
- package/lib/cordis/router-http.js +1 -1
- package/lib/node-server.js +1 -1
- package/lib/{test-websocket-ZXHNHQSU.js → test-websocket-D8WWqE5G.js} +1 -2
- package/package.json +1 -1
- package/plugins/acryl-db-sqlite/plugin.json +1 -1
- package/plugins/acryl-db-sqlite/src/index.ts +8 -1
- package/plugins/acryl-kv-json/plugin.json +1 -1
- package/plugins/acryl-kv-json/src/index.ts +8 -1
- package/plugins/acryl-node-server/src/adapter/request.ts +3 -2
- package/plugins/agent-panel/headless.ts +24 -2
- package/plugins/desktop/launch.ts +48 -0
- package/plugins/desktop/plugin.json +1 -1
- package/plugins/desktop-web/launch.ts +48 -0
- package/plugins/desktop-web/plugin.json +1 -1
- package/templates/chat/AGENTS.md +1 -1
- package/templates/chat/src/access.ts +110 -0
- package/templates/chat/src/chat-app.ts +7 -1
- package/templates/chat/src/routes.ts +5 -1
- package/templates/chat/test/chat.test.ts +65 -0
- package/templates/notes/test/notes.test.ts +1 -1
- package/templates/todo/src/main.ts +2 -0
package/docs/framework/README.md
CHANGED
|
@@ -54,6 +54,7 @@ They are enabled as they are built.
|
|
|
54
54
|
| Look up a call | [API](api/index.md), [Routing](api/routing.md), [Context](api/context.md), [Request](api/request.md), [API summary](api/summary.md) |
|
|
55
55
|
| Understand the design | [Concepts](concepts/motivation.md) |
|
|
56
56
|
| Copy a pattern | [Guides](guides/best-practices.md), [Validation](guides/validation.md), [Testing](guides/testing.md), [Data, migrations and choosing a store](guides/data.md), [Forms, sessions and a database](guides/forms-and-sessions.md), [Client components without a bundler](guides/client-modules.md), [Bring your own tool](guides/bring-your-own-tool.md) |
|
|
57
|
+
| Run the 90-minute workshop | [Workshop](workshop/README.md), and the room around it in [Facilitator notes](workshop/facilitator.md) |
|
|
57
58
|
| Use Acryl inside a Cordis app | [Acryl in a Cordis app](cordis/acryl-in-a-cordis-app.md), [Plugin lifecycle](cordis/plugin-lifecycle.md), [Mounting](cordis/mounting.md) |
|
|
58
59
|
| Move from the 0.3 router calls | [Migration from 0.3](cordis/migration-from-0-3.md) |
|
|
59
60
|
| Know what runs in production, and what the sandbox stops | [What runs in production](guides/production.md) |
|
|
@@ -33,6 +33,24 @@ The `agent` template (a Cordis app; `acryl-app` is its old name and still works)
|
|
|
33
33
|
|
|
34
34
|
See [Acryl in a Cordis app](../cordis/acryl-in-a-cordis-app.md), [Plugin lifecycle](../cordis/plugin-lifecycle.md) and [Mounting](../cordis/mounting.md).
|
|
35
35
|
|
|
36
|
+
## The agent panel and the terminals
|
|
37
|
+
|
|
38
|
+
The panel is a chat button on every page of the running app, and `driver-pty` adds real terminals to it. Both are **development only** (see [what runs in production](../guides/production.md)). They need the rows of the `agent` template (`agent` and `extensions`, with the `model`, `tools` and `memory` the agent brings), so start from that template:
|
|
39
|
+
|
|
40
|
+
```sh
|
|
41
|
+
npm create acrylblends@latest my-app agent
|
|
42
|
+
cd my-app
|
|
43
|
+
npm install
|
|
44
|
+
npx acrylblends add agent-panel driver-pty
|
|
45
|
+
npm install
|
|
46
|
+
npm run dev
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
- `add` puts the `agent-panel` row (and `driver-pty`'s after it) in `blend.yaml` before the `routes` row and registers both in `src/app.ts`. On an app without the `agent` and `extensions` rows, such as `blank`, it refuses before it copies anything and says which rows are missing.
|
|
50
|
+
- **The panel's coding agents are yours.** `claude`, `codex` and `pi` run on the machine that runs the app, in the app folder, with the login of the person who started it: `claude` and `codex` use their own signed-in sessions, and `pi` is offered when `DEEPSEEK_API_KEY` is set or `pi` has stored a DeepSeek login (`~/.pi/agent/auth.json`, read from the `HOME` of the process that runs the app: an app started under another `HOME` does not see the login, and the panel then lists `pty:pi` but no chat `pi`). A program that is not installed, or is not logged in, is not offered; nothing is shared between machines, and a workshop laptop needs its own login. The panel's own `agent` (the offline scripted one) needs none.
|
|
51
|
+
- `pi` has to be an executable on the `PATH`; a shell function that wraps the binary is fine as long as the binary itself is found by `which pi`.
|
|
52
|
+
- `driver-pty` brings `node-pty`, which ships prebuilt binaries for macOS, Windows and Linux. npm 11 and pnpm warn that its install script was not run (they ask you to approve dependency install scripts); you can ignore the warning. If no `pty:` terminal is offered and the app prints why at start, approve and rebuild: `npm approve-scripts node-pty`, then `npm rebuild node-pty` (pnpm: `pnpm approve-builds`).
|
|
53
|
+
|
|
36
54
|
## Checking that an agent can use the docs
|
|
37
55
|
|
|
38
56
|
The repository has a manual gate: it creates a fresh app, starts a headless coding agent inside it with a one-line task (for example "add a /customers/count route with a test"),
|
|
@@ -7,12 +7,13 @@
|
|
|
7
7
|
An app made by `acrylblends init` carries copies of the framework plugins (`acryl-router`, `acryl-node-server`, `acryl-door`, `extensions`, ...) in `plugins/`. `acrylblends upgrade` brings them to the version of the installed package:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
|
-
|
|
11
|
-
npx acrylblends upgrade
|
|
12
|
-
npx acrylblends upgrade # do it
|
|
10
|
+
npx acrylblends@latest upgrade --dry-run # what would change, and the diff of every file you edited
|
|
11
|
+
npx acrylblends@latest upgrade # do it
|
|
13
12
|
npm install && npm run check && npm test
|
|
14
13
|
```
|
|
15
14
|
|
|
15
|
+
**Write `@latest`.** A bare `npx acrylblends upgrade` runs the `acrylblends` the app has installed, and an app made before 1.0 has a 0.x copy with no `upgrade` verb. It does not upgrade anything: depending on the platform and the version it prints its old help and exits 0 (so nothing happens and nothing says so), or answers "unknown command" and exits 2. `npx acrylblends@latest` fetches the current package and runs that. (After the first upgrade the app depends on 1.x, and the bare form works too.)
|
|
16
|
+
|
|
16
17
|
- A file nobody edited is replaced. A file the app edited stops the upgrade: nothing is changed, the diff of what would be lost is shown, and the command exits 1. Move your change out of the plugin (a plugin of your own in `plugins/<name>/` is never touched), or run `npx acrylblends upgrade --force`, which saves the edited versions in `.acrylblends/upgrade-backup/<time>/` before replacing them.
|
|
17
18
|
- `--diff` prints the diff of every changed file, not only the edited ones. `acrylblends upgrade acryl-router extensions` upgrades only those plugins.
|
|
18
19
|
- A file of your own inside a library plugin folder (a note, a test) is kept and reported. A plugin that a newer version needs and the app lacks is added.
|
|
@@ -36,6 +36,26 @@ In a Blend app, the same two commands (`npx acrylblends add desktop`, `npm insta
|
|
|
36
36
|
`npm install` does not download Electron's binary (Electron 44 has no install script). The first `npm run desktop` does: about 100 MB, once per machine, then it is cached. Behind a proxy or mirror set `ELECTRON_MIRROR`
|
|
37
37
|
(and `ELECTRON_CACHE` to reuse a download); if the download is blocked, run `npx install-electron` where the mirror is reachable.
|
|
38
38
|
|
|
39
|
+
## Linux: Ubuntu 23.10 and later
|
|
40
|
+
|
|
41
|
+
Ubuntu 23.10 and later restrict unprivileged user namespaces (`kernel.apparmor_restrict_unprivileged_userns=1`). Chromium's sandbox needs them, and an Electron unpacked by npm into `node_modules` has no AppArmor profile, so Electron stops at the start with "The SUID sandbox helper binary was found, but is not configured correctly" before the app runs.
|
|
42
|
+
|
|
43
|
+
`npm run desktop` starts through `plugins/desktop/launch.ts`, which reads that sysctl. **Only when it is `1` on Linux** it starts Electron with `--no-sandbox` and prints one line saying so; on macOS, on Windows and on any Linux without the restriction Electron starts with its sandbox, as it always does. The window's renderer is still locked down (context isolation, no Node in the page, web security); what is lost is Chromium's process sandbox around it.
|
|
44
|
+
|
|
45
|
+
To keep the sandbox, give Electron an AppArmor profile that allows `userns`, as the VS Code and Chrome `.deb` packages do. Save this as `/etc/apparmor.d/my-app-electron` (the path is the one in your app's `node_modules`), then `sudo apparmor_parser -r /etc/apparmor.d/my-app-electron`:
|
|
46
|
+
|
|
47
|
+
```text
|
|
48
|
+
abi <abi/4.0>,
|
|
49
|
+
include <tunables/global>
|
|
50
|
+
|
|
51
|
+
profile my-app-electron /path/to/my-app/node_modules/electron/dist/electron flags=(unconfined) {
|
|
52
|
+
userns,
|
|
53
|
+
include if exists <local/my-app-electron>
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The launcher then still sees the sysctl as `1` and passes `--no-sandbox`; set `ACRYLBLENDS_DESKTOP_SANDBOX=1` to start with the sandbox anyway.
|
|
58
|
+
|
|
39
59
|
## A different start page
|
|
40
60
|
|
|
41
61
|
The window opens on `/`. To open another path, set it in `package.json`: `"acrylblends": { "desktopPath": "/todo" }`.
|
|
@@ -54,6 +54,8 @@ bun create acrylblends@latest my-app --template nodejs
|
|
|
54
54
|
| `[directory]` | The folder to create. It must not exist or must be empty. Asked for when it is missing. | `my-app` |
|
|
55
55
|
| `[source]` | A template from outside the package: `github:owner/repo[#ref]`, `@scope/name[@version]` or a packed `.tgz` file. See [Templates from a repository or a package](#templates-from-a-repository-or-a-package). | `github:acme/starter` |
|
|
56
56
|
| `--template <template>` | One of `blank`, `nodejs`, `notes`, `chat`, `todo`, `agent` (the same list as `acrylblends init --template`; `acryl-app` is accepted as `agent`). Skips the template question; a bare name after the directory means the same. | `--template notes` |
|
|
57
|
+
| `--from <id \| source>` | A starter from the registry by its id (`acrylblends.notes-desktop`), or any source above. Only starters that declare `runtime: acrylblends` are made; an ACRYL app starter is refused. See [Starters from the registry](#starters-from-the-registry). | `-- --from acrylblends.notes-desktop` |
|
|
58
|
+
| `--registry <source>` | Read the ids from this registry instead of the public one: `github:owner/repo` or a folder, with an `index.json` (at its root or in `registry/`). | `-- --from login.thing --registry github:acryldev/workshop-2026-11-20` |
|
|
57
59
|
| `--install` | Install the dependencies with the package manager after creating the project. For a template from a source, the install scripts it will run are listed first. | `--install` |
|
|
58
60
|
| `--pm <packageManager>` | The package manager for `--install`: `npm`, `pnpm`, `yarn` or `bun`. Defaults to the one that started the initializer. | `--pm pnpm` |
|
|
59
61
|
| `--yes`, `-y` | Ask nothing: use `nodejs` unless `--template` is given, and the directory `acryl-app`. | `-y` |
|
|
@@ -61,6 +63,25 @@ bun create acrylblends@latest my-app --template nodejs
|
|
|
61
63
|
|
|
62
64
|
The templates are inside the package, so creating a project needs no network except for `--install`.
|
|
63
65
|
|
|
66
|
+
## Starters from the registry
|
|
67
|
+
|
|
68
|
+
The [registry](https://acrylblends.github.io/blends) lists starters people published. Each is a whole app (`blend.yaml`, `plugins/`, `src/`, `package.json`, `README.md`) that other people can start from, and its index says which runtime it is for:
|
|
69
|
+
|
|
70
|
+
```sh
|
|
71
|
+
npm create acrylblends@latest my-notes -- --from acrylblends.notes-desktop
|
|
72
|
+
cd my-notes
|
|
73
|
+
npm install
|
|
74
|
+
npm run dev
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- npm needs the `--` before `--from`; with `pnpm`, `yarn` and `bun` it is not needed.
|
|
78
|
+
- A starter with `runtime: acrylblends` is an `acrylblends` app: this command downloads its folder, gives the manifest to the new app (`metadata.id: acrylblends.my-notes`, `visibility: private`; the starter's id and licence stay the starter's), checks it for anything that looks like a secret, and writes it. Nothing in it is installed or run: `npm install` is yours to run, and the dependencies of the starter (a desktop starter brings `electron`) are in its `package.json`.
|
|
79
|
+
- A starter for the ACRYL app (`runtime: acryl`, the older `acryl.*` entries) is refused with one line that says so, because its plugins run only inside the ACRYL app. Nothing is written.
|
|
80
|
+
- An unknown id lists the starters this command can make.
|
|
81
|
+
- The ids come from `https://acrylblends.github.io/registry/index.json`. `--registry github:owner/repo` reads another registry with the same layout (`index.json` and `blends/<id>/`), a team's own or a workshop's; `--registry ./folder` reads one on disk.
|
|
82
|
+
|
|
83
|
+
To publish a starter of your own, see [the registry README](https://github.com/acrylblends/acrylblends.github.io/tree/main/registry): the folder of your app under `blends/<id>/` with an `acryl.template.json`, in a pull request.
|
|
84
|
+
|
|
64
85
|
## Templates from a repository or a package
|
|
65
86
|
|
|
66
87
|
A second argument after the directory makes the app from someone else's template:
|
|
@@ -142,6 +142,18 @@ CREATE TABLE tags (
|
|
|
142
142
|
- A database whose version is newer than your files (an older build started on newer data) is refused. The app does not start.
|
|
143
143
|
- A migration is plain SQL. Put the data change next to the shape change in the same file when they belong together.
|
|
144
144
|
|
|
145
|
+
**In an app made by `acrylblends init`** (a Cordis app) the provider is a row of `blend.yaml` (and an entry in the `registry` of `src/app.ts`, which `acrylblends add db` prints how to write):
|
|
146
|
+
|
|
147
|
+
```yaml
|
|
148
|
+
- id: db
|
|
149
|
+
name: acryl-db-sqlite
|
|
150
|
+
config:
|
|
151
|
+
path: data/app.sqlite
|
|
152
|
+
migrations: migrations
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
A relative `path` or `migrations` in a row is read from the app's folder (the one that holds `blend.yaml`), not from where the command was started. (Since 1.0.3; before it a relative path stopped the app with "no file base URL", and an absolute path, or the `db: { migrations: join(projectRoot, 'migrations') }` option of `compose` in `src/app.ts` as the notes template does, was the way.)
|
|
156
|
+
|
|
145
157
|
To add a feature that needs a column or a table, the work is always the same three steps: write the next migration, change the store that
|
|
146
158
|
reads and writes it, and change the handlers and views that use the store.
|
|
147
159
|
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
| **The agent panel** (`agent-panel`) | **experimental**: a script tag on every HTML page and `/api/agent-panel/*` | does nothing (no tag, no routes). `ACRYL_DOOR=off` too |
|
|
12
12
|
| **The PTY driver** (`driver-pty`) | **experimental**: terminal drivers in the panel, `/api/driver-pty/*` | does nothing. `ACRYL_DOOR=off` too |
|
|
13
13
|
| **Live plugin install and the agent** (`extensions`, `builder`, `agent`: `POST /ask`) | **experimental**: install, update, remove and reload plugins while the app runs; the agent builds them | left out: the `nodejs`, `notes` and `chat` templates drop the `extensions` row, and the `agent` template drops `agent`, `builder` and `extensions` (since 1.0.1) unless `ACRYL_PRODUCTION_KEEP` names them |
|
|
14
|
+
| **The chat template's model endpoint** (`POST /api/chat`) | answers anyone | **closed** (503) unless `CHAT_ACCESS_TOKEN` is set, or `ACRYL_PRODUCTION_KEEP=chat` makes it public with a warning and a per-address limit (since 1.0.2) |
|
|
14
15
|
| `acrylblends/bun` | **experimental**: one smoke gate runs it on Bun | the same |
|
|
15
16
|
|
|
16
17
|
Experimental means: it works and is gated, its interface may still change in a minor version, and it is not for the public internet. The door and the panel are loopback and own-page only, with a per-run secret and an audit log; that is a convenience fence for a developer's machine, not an authentication system.
|
|
@@ -22,6 +23,15 @@ The `agent` template mounts an agent behind `POST /ask` that spends the model ke
|
|
|
22
23
|
- To keep a row anyway, name it: `ACRYL_PRODUCTION_KEEP=agent,builder,extensions` (a comma list). Each row kept prints a `WARNING` line at start saying what it gives a visitor. Put your own login in front before you do.
|
|
23
24
|
- **An app made by 1.0.0 keeps its own `src/app.ts` and `src/routes.ts`**, which `acrylblends upgrade` never touches (they are your files). Until you change them: deploy with the `agent`, `builder` and `extensions` rows set to `disabled: true` in `blend.yaml`, or start from another template, or copy the new `src/app.ts` and `src/routes.ts` of a fresh `acrylblends init my-app --template agent` over yours.
|
|
24
25
|
|
|
26
|
+
## The `chat` template in production
|
|
27
|
+
|
|
28
|
+
The chat template answers with the owner's model key, so anyone who can reach it spends that key. Since 1.0.2, with `NODE_ENV=production` it is **closed**: every route but `GET /health` answers 503 until you choose one of:
|
|
29
|
+
|
|
30
|
+
- `CHAT_ACCESS_TOKEN=<a long random string>`: the browser asks for a login and takes any user name with the token as the password. Share the token with the people who may use the chat.
|
|
31
|
+
- `ACRYL_PRODUCTION_KEEP=chat`: the chat is public, with a `WARNING` line at start and a limit of `CHAT_RATE_PER_MINUTE` messages a minute from one address (default 20; behind a proxy you run, set `TRUST_PROXY=1` so the limit counts the visitor, not the proxy). Prefer your own login in front.
|
|
32
|
+
|
|
33
|
+
An app made by 1.0.0 or 1.0.1 keeps its own `src/routes.ts`: copy `src/access.ts`, `src/chat-app.ts` and `src/routes.ts` from a fresh `acrylblends init my-app --template chat`, or put a login in front.
|
|
34
|
+
|
|
25
35
|
## What live plugin install does and does not promise
|
|
26
36
|
|
|
27
37
|
Installing a plugin (`ctx.extensions.install`, the door's `plugin add`, the builder's `install_plugin`) is a **correctness check, not a security boundary**:
|
|
@@ -41,6 +51,6 @@ Installing a plugin (`ctx.extensions.install`, the door's `plugin add`, the buil
|
|
|
41
51
|
## Checklist for a deployment
|
|
42
52
|
|
|
43
53
|
1. `NODE_ENV=production` (the process manager or the Dockerfile sets it; the templates' Dockerfile does).
|
|
44
|
-
2. With the `agent` template, leave `ACRYL_PRODUCTION_KEEP` unset (1.0.1 and later), or remove the agent rows.
|
|
54
|
+
2. With the `agent` template, leave `ACRYL_PRODUCTION_KEEP` unset (1.0.1 and later), or remove the agent rows. With the `chat` template, set `CHAT_ACCESS_TOKEN` (1.0.2 and later) or put your own login in front.
|
|
45
55
|
3. Do not set `allowInProduction` on the door unless the host is a single-user desktop app.
|
|
46
56
|
4. Keep `ACRYL_DOOR=off` in any environment where an agent should not be able to see the page.
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
# From nothing to your own app: the ACRYL Blends workshop
|
|
2
|
+
|
|
3
|
+
Ninety minutes, starting from an empty folder. **Acryl** is a small web framework built on Web Standards: a handler takes a `Request` and returns a
|
|
4
|
+
`Response`, exactly as in the browser. **ACRYL Blends** puts that framework inside an app you own: a folder whose `blend.yaml` lists its plugins, whose
|
|
5
|
+
`plugins/` folder holds their code, and which runs in a browser or in a desktop window. A coding agent can change the app while it runs. By the end you
|
|
6
|
+
will have built an app from the blank template, added a feature to it with an agent, opened it as a desktop window, upgraded it, and published it as a
|
|
7
|
+
starter other people can pull from the registry.
|
|
8
|
+
|
|
9
|
+
Every command below is copy-pasteable and was run on the published packages. Each step says how long it takes; the checkpoints tell you whether you
|
|
10
|
+
are on time.
|
|
11
|
+
|
|
12
|
+
## What you need (10 minutes)
|
|
13
|
+
|
|
14
|
+
| | macOS | Windows | Linux |
|
|
15
|
+
| --- | --- | --- | --- |
|
|
16
|
+
| Node.js 22.19 or newer (24 LTS or 26 tested) | `node -v` | `node -v` | `node -v` |
|
|
17
|
+
| npm, which comes with Node | `npm -v` | `npm -v` | `npm -v` |
|
|
18
|
+
| git | `git --version` | `git --version` | `git --version` |
|
|
19
|
+
| a coding agent, logged in | `codex login status` or `claude --version` | same | same |
|
|
20
|
+
| a GitHub account and the `gh` CLI, logged in | `gh auth status` | same | same |
|
|
21
|
+
|
|
22
|
+
Install what is missing: Node from <https://nodejs.org> (the LTS), git from <https://git-scm.com>, the GitHub CLI (`brew install gh`, `winget
|
|
23
|
+
install GitHub.cli`, or <https://cli.github.com>), and one agent with `npm i -g @openai/codex` (then `codex login` once) or Claude Code. Run
|
|
24
|
+
`gh auth login` once, in any repository: step 10 publishes your app with a pull request. No prior knowledge of ACRYL, Cordis or Blends is needed;
|
|
25
|
+
every new word is explained where it appears.
|
|
26
|
+
|
|
27
|
+
Make a folder and open a terminal in it:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
mkdir ~/acryl-workshop && cd ~/acryl-workshop
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## The path
|
|
34
|
+
|
|
35
|
+
| Minutes | Step | What you get |
|
|
36
|
+
| --- | --- | --- |
|
|
37
|
+
| 0-10 | Before you start | tools installed and checked |
|
|
38
|
+
| 10-15 | 1. Create and run | a blank app answering in a browser |
|
|
39
|
+
| 15-22 | 2. Change one thing | a route of your own, live, without a restart |
|
|
40
|
+
| 22-30 | 3. Tests and the typecheck | the app proves itself |
|
|
41
|
+
| 30-38 | 4. Data: JSON, then SQLite *(optional)* | a counter kept in a file, then in SQL |
|
|
42
|
+
| 38-45 | 5. A desktop window | the same app as a window |
|
|
43
|
+
| 45-53 | 6. Pull a starter from the registry | someone else's app in your folder |
|
|
44
|
+
| 53-60 | 7. Create from a template | the six starters, one table, one run |
|
|
45
|
+
| 60-75 | 8. An agent builds a feature | your app changed by the agent, still running |
|
|
46
|
+
| 75-82 | 9. Upgrade | the app moved to the current framework |
|
|
47
|
+
| 82-90 | 10. Publish it | your app as a registry starter |
|
|
48
|
+
|
|
49
|
+
> **Checkpoint 15 minutes:** your app answers in a browser. Without that, read the "Stuck?" line of step 1.
|
|
50
|
+
> **Checkpoint 45 minutes:** your app is open in a desktop window. Behind? Step 4 is the optional one; skip it and carry on.
|
|
51
|
+
> **Checkpoint 75 minutes:** the agent's page is live. Still waiting? Give it five more minutes, then skip step 9.
|
|
52
|
+
|
|
53
|
+
## 1. Create and run (5 minutes)
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
npm create acrylblends@latest my-hero blank
|
|
57
|
+
cd my-hero
|
|
58
|
+
npm install
|
|
59
|
+
npm run dev
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
You should see:
|
|
63
|
+
|
|
64
|
+
```
|
|
65
|
+
created my-hero (blank) in /Users/you/acryl-workshop/my-hero: … files, plugins instance, acryl-router, acryl-node-server, manifest, save
|
|
66
|
+
|
|
67
|
+
Next:
|
|
68
|
+
cd my-hero
|
|
69
|
+
npm install
|
|
70
|
+
npm run dev
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
then, from `npm run dev`:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
my-hero listening on http://127.0.0.1:3080 (data in /Users/you/acryl-workshop/my-hero/.acrylblends)
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Open that address: `{"app":"my-hero","ok":true}`. `/health` answers `{"ok":true}` too. Leave the terminal running.
|
|
80
|
+
|
|
81
|
+
**Stuck?** A busy port gets the next free one: use the address the app printed. The first `npm create` downloads the initializer, so give it a few
|
|
82
|
+
seconds.
|
|
83
|
+
|
|
84
|
+
## 2. Change one thing (7 minutes)
|
|
85
|
+
|
|
86
|
+
Open `src/routes.ts`. It builds an `Acryl` app and mounts it on the router. Add one line to that chain, just above `.get('/health', …)`, and keep the
|
|
87
|
+
indentation of the lines around it:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
.get('/hello', (c) => c.json({ hello: 'workshop' }))
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Save. The terminal prints `Change detected … Restarting 'src/main.ts'`, and `http://127.0.0.1:3080/hello` answers `{"hello":"workshop"}`. No restart
|
|
94
|
+
command, no build step: the app watches its own source.
|
|
95
|
+
|
|
96
|
+
**Stuck?** No message means the file is not where the app watches; the message names the path it saw. A red error names the file and line.
|
|
97
|
+
|
|
98
|
+
## 3. Tests and the typecheck (8 minutes)
|
|
99
|
+
|
|
100
|
+
Stop the server with `Ctrl-C`, then:
|
|
101
|
+
|
|
102
|
+
```sh
|
|
103
|
+
npm test
|
|
104
|
+
npm run check
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`npm test` ends with `ℹ pass 5`, `ℹ fail 0`. `npm run check` prints nothing when the types are clean.
|
|
108
|
+
|
|
109
|
+
Now add a test for your route in `test/`: copy the style of the existing test file, request `/hello` and expect `{"hello":"workshop"}`. `npm test`
|
|
110
|
+
again; a failure names the file and line.
|
|
111
|
+
|
|
112
|
+
**Stuck?** `npm run check` complaining about your new line is the typechecker doing its job; the message says which type it expected.
|
|
113
|
+
|
|
114
|
+
## 4. Data: JSON, then SQLite (8 minutes, optional)
|
|
115
|
+
|
|
116
|
+
An app keeps data through a **port**: a small interface a plugin provides. The smallest is `kv`, a JSON file in the app's own folder.
|
|
117
|
+
|
|
118
|
+
```sh
|
|
119
|
+
npx acrylblends add acryl-kv-json
|
|
120
|
+
npm install
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
`add` copies the plugin into `plugins/` and prints how to mount it. Three edits:
|
|
124
|
+
|
|
125
|
+
1. `blend.yaml`, before `- id: server`: add the provider row (the row's id is yours to pick; the name is the plugin):
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
- id: kv
|
|
129
|
+
name: acryl-kv-json
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
2. `src/app.ts`: import the plugin and register it under that name:
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
import * as kvJson from '../plugins/acryl-kv-json/src/index.ts'
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
export const registry = { 'acryl-router': router, 'acryl-node-server': nodeServer, 'acryl-kv-json': kvJson, routes }
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
3. `src/routes.ts`: declare the port and count visits in your route:
|
|
143
|
+
|
|
144
|
+
```ts
|
|
145
|
+
import type {} from '../plugins/acryl-kv/src/cordis.ts'
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
```ts
|
|
149
|
+
inject: ['router', 'kv'],
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
.get('/hello', async (c) => {
|
|
154
|
+
const visits = await ctx.kv.update('visits', (n) => (n ?? 0) + 1)
|
|
155
|
+
return c.json({ hello: 'workshop', visits })
|
|
156
|
+
})
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
`npm run dev` again and open `/hello` twice: `visits` goes 1, 2. The file is `my-hero/.acrylblends/kv.json`.
|
|
160
|
+
|
|
161
|
+
The relational port is `db`, with numbered SQL migrations:
|
|
162
|
+
|
|
163
|
+
```sh
|
|
164
|
+
npx acrylblends add acryl-db-sqlite
|
|
165
|
+
mkdir migrations
|
|
166
|
+
printf 'CREATE TABLE visits (id INTEGER PRIMARY KEY, at TEXT NOT NULL);\n' > migrations/001-visits.sql
|
|
167
|
+
npm install
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
A row in `blend.yaml` (`- id: db`, `name: acryl-db-sqlite`), then the migration folder in `src/app.ts`, where the app is composed: the path is set
|
|
171
|
+
there, not in the manifest, because a relative path in the manifest has no folder to resolve against.
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import * as dbSqlite from '../plugins/acryl-db-sqlite/src/index.ts'
|
|
175
|
+
// in the registry: 'acryl-db-sqlite': dbSqlite
|
|
176
|
+
const projectRoot = fileURLToPath(new URL('..', import.meta.url))
|
|
177
|
+
// in the compose options next to server:
|
|
178
|
+
db: { migrations: join(projectRoot, 'migrations') },
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Then `inject: ['router', 'kv', 'db']` with `import type {} from '../plugins/acryl-db/src/cordis.ts'`, and in the route
|
|
182
|
+
`await ctx.db.run('INSERT INTO visits (at) VALUES (?)', new Date().toISOString())` before the reply. The database is `my-hero/.acrylblends/data.sqlite`,
|
|
183
|
+
and the migrations run at start, in order.
|
|
184
|
+
|
|
185
|
+
**Stuck?** `migrations "migrations" is relative, but this app has no file base URL` means the folder was named in `blend.yaml`; move it into
|
|
186
|
+
`src/app.ts` as above. "no such table" means the file is not in `migrations/` at the app root.
|
|
187
|
+
|
|
188
|
+
## 5. A desktop window (7 minutes)
|
|
189
|
+
|
|
190
|
+
```sh
|
|
191
|
+
npx acrylblends add desktop
|
|
192
|
+
npm install
|
|
193
|
+
npm run desktop
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
A window opens, showing the same routes and the same data. The app's own code never imports Electron; every Electron call lives in `plugins/desktop`,
|
|
197
|
+
and the window's browser data stays inside `my-hero/.acrylblends/electron`. Close the window when you are done (`Cmd-Q`, `Alt-F4`).
|
|
198
|
+
|
|
199
|
+
**Stuck?** The first `npm run desktop` downloads Electron (about 200 MB). No display (a bare server)? Skip this step; nothing later needs it.
|
|
200
|
+
|
|
201
|
+
## 6. Pull a starter from the registry (8 minutes)
|
|
202
|
+
|
|
203
|
+
The registry is where people publish starters: <https://acrylblends.github.io/blends> (the starters written for this framework say
|
|
204
|
+
`runtime: acrylblends`; older ACRYL app starters are refused with a line saying so). Pull one by id, straight into a new folder:
|
|
205
|
+
|
|
206
|
+
```sh
|
|
207
|
+
cd ~/acryl-workshop
|
|
208
|
+
npm create acrylblends@latest from-the-registry -- --from <starter-id>
|
|
209
|
+
cd from-the-registry
|
|
210
|
+
npm install
|
|
211
|
+
npm run dev
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
The same command takes a git repository or a folder: `--from github:you/starter` or `--from ../a-template`. You get a complete app someone else
|
|
215
|
+
published, with its plugins, its dependencies and its `blend.yaml`, in your folder and yours to change. Nothing is installed for you: the starter's
|
|
216
|
+
`Next:` block asks for the same `npm install` as step 1.
|
|
217
|
+
|
|
218
|
+
For the workshop's own registry, name it:
|
|
219
|
+
|
|
220
|
+
```sh
|
|
221
|
+
npm create acrylblends@latest from-the-workshop -- --from <id> --registry github:acryldev/workshop-2026-11-20
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
Starters for this workshop: `acrylblends.notes-desktop` (notes with a desktop window), `acrylblends.chat`, `acrylblends.agent`.
|
|
225
|
+
|
|
226
|
+
**Stuck?** The `--` before `--from` is npm's, not ours: without it npm swallows the flag. A refused id prints its reason first (`error: acryl.gtd is an
|
|
227
|
+
ACRYL app starter: open it in the ACRYL app`) and then npm's own error block; read the first lines, not the last.
|
|
228
|
+
|
|
229
|
+
## 7. Create from a template (7 minutes)
|
|
230
|
+
|
|
231
|
+
Every app begins from one of six templates, the same list in `npm create acrylblends` and `acrylblends init`:
|
|
232
|
+
|
|
233
|
+
| Template | What you get |
|
|
234
|
+
| --- | --- |
|
|
235
|
+
| `blank` | the router, the server and one route: what you just built |
|
|
236
|
+
| `nodejs` | a page rendered with JSX and a client component, without a bundler |
|
|
237
|
+
| `notes` | a multi-user notes app on the `db` port: sign-up, sessions, migrations, search, a typed client |
|
|
238
|
+
| `chat` | an answer streamed over server-sent events from an OpenAI-compatible API, history in SQLite |
|
|
239
|
+
| `todo` | a to-do list on the router and live state |
|
|
240
|
+
| `agent` | the blank app with an offline agent that writes and installs plugins into the running app |
|
|
241
|
+
|
|
242
|
+
Try the one with a database and accounts, in a window:
|
|
243
|
+
|
|
244
|
+
```sh
|
|
245
|
+
cd ~/acryl-workshop
|
|
246
|
+
npm create acrylblends@latest notes-app notes
|
|
247
|
+
cd notes-app && npm install
|
|
248
|
+
npx acrylblends add desktop && npm install && npm run desktop
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Sign up, add a note, close the window; run `npm run dev` and the note is still there, because it lives in `notes-app/.acrylblends/data.sqlite`.
|
|
252
|
+
|
|
253
|
+
**Stuck?** The sign-in page is at `/login`; the root redirects there. Delete the folder when you are done with it.
|
|
254
|
+
|
|
255
|
+
## 8. An agent builds a feature (15 minutes)
|
|
256
|
+
|
|
257
|
+
The agent panel turns a coding agent you already use into a chat in the corner of the page. It needs the plugins the `agent` template ships (its model,
|
|
258
|
+
tools and memory), so this step builds a second, small app from that template; it is the app you publish in step 10.
|
|
259
|
+
|
|
260
|
+
```sh
|
|
261
|
+
cd ~/acryl-workshop
|
|
262
|
+
npm create acrylblends@latest agent-app agent
|
|
263
|
+
cd agent-app
|
|
264
|
+
npm install
|
|
265
|
+
npx acrylblends add agent-panel driver-pty
|
|
266
|
+
npx acrylblends add desktop
|
|
267
|
+
npm install
|
|
268
|
+
npm run dev
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
`add agent-panel` adds the plugin rows the panel needs and registers it for you, and prints what it changed. (On a `blank` app it refuses and says
|
|
272
|
+
which rows are missing: that is why this step uses the `agent` template.) Open the page, click the panel button in the corner, pick `codex` (or
|
|
273
|
+
`claude`, or `pi` when pi has its DeepSeek login in your home folder), and ask:
|
|
274
|
+
|
|
275
|
+
> Add a page at /board that lists the app's tasks, newest first, and a test for it.
|
|
276
|
+
|
|
277
|
+
The agent reads `AGENTS.md`, reads `docs/framework` (which is in your app), edits your files, runs your tests, and the app reloads itself. It takes a
|
|
278
|
+
few minutes; the panel shows each step, and has a stop button. Open `/board` when it is done.
|
|
279
|
+
|
|
280
|
+
A terminal agent works too: choose `pty:codex` (or `pty:claude`, `pty:pi`) for its normal interactive session inside the app folder, where it can use
|
|
281
|
+
the door (`.acryl/bin/acryl`) to look at the page it built.
|
|
282
|
+
|
|
283
|
+
**Stuck?** The panel uses your own agent account: `codex login` (or `claude`) in a terminal first. If `npm install` warns that npm blocked
|
|
284
|
+
`node-pty`'s install script, ignore it: prebuilt binaries ship. Only if the panel lists no `pty:` terminal, run `npm approve-scripts node-pty` and
|
|
285
|
+
`npm rebuild node-pty` (the `add` output says the same).
|
|
286
|
+
|
|
287
|
+
## 9. Upgrade (7 minutes)
|
|
288
|
+
|
|
289
|
+
Your app carries its framework plugins as source, so a new release moves them in place and leaves your code alone. Run the tool by its full name:
|
|
290
|
+
**`npx acrylblends@latest`**, because an app's own copy of `acrylblends` shadows the bare command and an older copy has no `upgrade` at all.
|
|
291
|
+
|
|
292
|
+
```sh
|
|
293
|
+
cd ~/acryl-workshop/my-hero
|
|
294
|
+
npx acrylblends@latest upgrade --dry-run
|
|
295
|
+
npx acrylblends@latest upgrade
|
|
296
|
+
npm install && npm test
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
The dry run lists what would be replaced; the real run replaces untouched files and stops with a diff if you edited one (move your change into a plugin
|
|
300
|
+
of your own, or use `--force`, which backs the edited copies up first). An app made by this version is already current, so both commands answer
|
|
301
|
+
`everything is up to date`: that is the mechanism working, and an app made by an earlier release lists what it would move. Your tests still pass
|
|
302
|
+
afterwards.
|
|
303
|
+
|
|
304
|
+
## 10. Publish it, and the hero moment (8 minutes)
|
|
305
|
+
|
|
306
|
+
The app you publish is `agent-app`, the one the agent changed in step 8 (keep `my-hero` too: it is yours, saved the same way). It is already a
|
|
307
|
+
**starter**: `blend.yaml` names it, lists its rows, and `plugins/` holds its code. Two parts make it public: your own copy, then the registry.
|
|
308
|
+
|
|
309
|
+
**Your own copy first.** `npm run save` commits the app and pushes it to its remote (it refuses a file that looks like a secret, and refuses to push a
|
|
310
|
+
private app to a public remote). Create an empty repository on your git host, set it as `origin`, run `npm run save`.
|
|
311
|
+
|
|
312
|
+
**The registry.** Publishing is a pull request to the workshop registry, `github.com/acryldev/workshop-2026-11-20`, and it is four commands.
|
|
313
|
+
|
|
314
|
+
1. Fork and clone it, then let its script build the entry from your app:
|
|
315
|
+
|
|
316
|
+
```sh
|
|
317
|
+
cd ~/acryl-workshop
|
|
318
|
+
gh repo fork acryldev/workshop-2026-11-20 --clone
|
|
319
|
+
cd workshop-2026-11-20
|
|
320
|
+
node scripts/make-entry.mjs ../agent-app <your-github-login>.agent-app --description "the app I built in the workshop"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
`make-entry` copies the app into `blends/<your-github-login>.agent-app/` without `node_modules`, `.git` or the app's data, marks its `blend.yaml`
|
|
324
|
+
public and licensed under that id, and writes the `acryl.template.json` that makes it pullable. The entry is the whole app: its dependencies come with
|
|
325
|
+
it, so whoever pulls it gets the desktop shell without adding it again. The id is lowercase, `<login>.<name>` with letters, digits and dashes.
|
|
326
|
+
|
|
327
|
+
2. Commit, push and open the pull request:
|
|
328
|
+
|
|
329
|
+
```sh
|
|
330
|
+
git add blends && git commit -m "add <your-github-login>.agent-app"
|
|
331
|
+
git push
|
|
332
|
+
gh pr create --fill --repo acryldev/workshop-2026-11-20
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
3. A check runs on the pull request, and when it passes the pull request is merged: your starter is listed, usually well under two minutes. If it
|
|
336
|
+
refuses, it comments what to change; fix, commit, push again and it re-checks. To take an app down, open a pull request that removes the folder:
|
|
337
|
+
`git rm -r blends/<your-github-login>.agent-app`.
|
|
338
|
+
|
|
339
|
+
Anyone can then start from it, in the browser or in a window:
|
|
340
|
+
|
|
341
|
+
```sh
|
|
342
|
+
npm create acrylblends@latest my-app -- --from <your-github-login>.agent-app --registry github:acryldev/workshop-2026-11-20
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
**Stuck?** The check's comment is the answer: read it, fix, push again. An id with a capital or a dot inside a part is refused by `make-entry`; use
|
|
346
|
+
lowercase `<login>.<name>`. `gh: not found` means the GitHub CLI is not installed, and `gh auth status` saying not logged in means `gh auth login`.
|
|
347
|
+
|
|
348
|
+
> **The hero moment.** An app you created, with a page an agent wrote for it, running as a desktop window, saved in your own repository, and published
|
|
349
|
+
> as a starter in the registry so the next person can pull it with one command. Along the way you built a second app from blank, a route, a test, a JSON
|
|
350
|
+
> store and a SQLite database. Note what you did not write: the router, the server, the JSON file, the SQLite migrations, the window, the page.
|
|
351
|
+
|
|
352
|
+
## Where next
|
|
353
|
+
|
|
354
|
+
- **Read the docs you already have**: every app carries `docs/framework`, the same pages as <https://acrylblends.github.io/docs>. Start with
|
|
355
|
+
`cordis/acryl-in-a-cordis-app.md`, then `guides/data.md`, then `guides/production.md` (what runs in production, and what does not).
|
|
356
|
+
- **Swap a provider by editing one row**: SQLite to Postgres (`npx acrylblends add acryl-db-postgres`), the browser to a window (`desktop`), one agent
|
|
357
|
+
to another (the panel's driver list).
|
|
358
|
+
- **Teach your agent**: `AGENTS.md` in your app is what it reads first; add your team's conventions there and the next run follows them.
|
|
359
|
+
- **Bring your own tool**: <https://acrylblends.github.io/docs/guides/bring-your-own-tool> wraps an outside library as a provider of a port.
|
|
360
|
+
|
|
361
|
+
## Browser only (WebBoxes.ai) TODO(M9)
|
|
362
|
+
|
|
363
|
+
No local install? The same app, and the same agent, will run as a hosted workspace: open it in a browser, pick a starter, and work in the same three
|
|
364
|
+
folders. Nothing to install, nothing to keep on your machine.
|
|
365
|
+
|
|
366
|
+
The hosted workspace is milestone M9 and is due before the workshop, so this section is a stub until it is live: write the address, the sign-in it
|
|
367
|
+
needs, and the two or three clicks that replace steps 1-2 here, then let the local steps stand for anyone who wants the app on their own disk.
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## If something goes wrong
|
|
372
|
+
|
|
373
|
+
| Symptom | Do |
|
|
374
|
+
| --- | --- |
|
|
375
|
+
| `npm create` fails or hangs | check the network; the facilitator has prepared apps |
|
|
376
|
+
| A port is taken | use the address the app printed |
|
|
377
|
+
| The agent cannot log in | `codex login` / `claude` in a terminal, then retry in the panel |
|
|
378
|
+
| You want to start over | delete your app folder and repeat step 1; nothing lives outside it |
|
|
379
|
+
| Anything else | the **Stuck?** line of the step, then your facilitator |
|
|
380
|
+
|
|
381
|
+
Behind at a checkpoint? Skip step 4 (data) first, then the second app in step 7. Steps 1-3, 5, 8-10 are the spine.
|