acrylblends 0.7.7 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +14 -6
- package/dist/index.js +130 -31
- package/dist/index.js.map +1 -1
- package/docs/framework/README.md +3 -3
- package/docs/framework/UPSTREAM.md +1 -1
- package/docs/framework/agents/index.md +18 -3
- package/docs/framework/getting-started/basic.md +1 -2
- package/docs/framework/getting-started/desktop.md +3 -3
- package/docs/framework/guides/bring-your-own-tool.md +216 -0
- package/docs/framework/guides/client-modules.md +4 -3
- package/docs/framework/guides/create-acrylblends.md +5 -4
- package/docs/framework/guides/data.md +7 -6
- package/docs/framework/helpers/streaming.md +12 -27
- package/docs/framework/upstream.json +3 -3
- package/lib/browser/read-sse.js +84 -0
- package/lib/db/sqlite.d.ts +1 -1
- package/lib/db.d.ts +1 -84
- package/lib/index-BLTYsyc1.d.ts +93 -0
- package/lib/index-D4FAsBZu.d.ts +85 -0
- package/lib/kv/db.d.ts +2 -2
- package/lib/kv/json.d.ts +1 -1
- package/lib/kv/json.js +87 -24
- package/lib/kv.d.ts +1 -92
- package/lib/node-server.d.ts +3 -3
- package/lib/node-server.js +14 -8
- package/lib/read-sse.d.ts +33 -0
- package/lib/read-sse.js +84 -0
- package/package.json +5 -1
- package/plugins/acryl-db/plugin.json +7 -2
- package/plugins/acryl-db/src/conformance.ts +224 -0
- package/plugins/acryl-db/upstream.json +2 -2
- package/plugins/acryl-db-sqlite/plugin.json +4 -1
- package/plugins/acryl-db-sqlite/upstream.json +1 -1
- package/plugins/acryl-door/LICENSE +47 -0
- package/plugins/acryl-door/plugin.json +14 -0
- package/plugins/acryl-door/src/cli/acryl.ts +207 -0
- package/plugins/acryl-door/src/host/api.ts +9 -0
- package/plugins/acryl-door/src/host/audit.ts +64 -0
- package/plugins/acryl-door/src/host/channel.ts +142 -0
- package/plugins/acryl-door/src/host/chat-tools.ts +39 -0
- package/plugins/acryl-door/src/host/door-file.ts +94 -0
- package/plugins/acryl-door/src/host/door.ts +195 -0
- package/plugins/acryl-door/src/host/http.ts +80 -0
- package/plugins/acryl-door/src/host/mcp-routes.ts +83 -0
- package/plugins/acryl-door/src/host/operator-routes.ts +38 -0
- package/plugins/acryl-door/src/host/page-routes.ts +65 -0
- package/plugins/acryl-door/src/host/shim.ts +65 -0
- package/plugins/acryl-door/src/host/verbs.ts +109 -0
- package/plugins/acryl-door/src/index.ts +133 -0
- package/plugins/acryl-door/src/page/api.ts +9 -0
- package/plugins/acryl-door/src/page/channel.ts +135 -0
- package/plugins/acryl-door/src/page/driver/accessibility.ts +163 -0
- package/plugins/acryl-door/src/page/driver/actions.ts +186 -0
- package/plugins/acryl-door/src/page/driver/driver.ts +153 -0
- package/plugins/acryl-door/src/page/driver/ref-table.ts +78 -0
- package/plugins/acryl-door/src/page/driver/sensitivity.ts +49 -0
- package/plugins/acryl-door/src/page/driver/snapshot.ts +132 -0
- package/plugins/acryl-door/src/page/index.ts +40 -0
- package/plugins/acryl-door/src/page/indicator.ts +72 -0
- package/plugins/acryl-door/src/page/main.ts +7 -0
- package/plugins/acryl-door/src/page/protocol.ts +213 -0
- package/plugins/acryl-door/src/page/snapshot-text.ts +18 -0
- package/plugins/acryl-door/src/page/user-input.ts +24 -0
- package/plugins/acryl-door/upstream.json +28 -0
- package/plugins/acryl-inspect/upstream.json +1 -1
- package/plugins/acryl-kv/plugin.json +7 -2
- package/plugins/acryl-kv/src/conformance.ts +226 -0
- package/plugins/acryl-kv/upstream.json +1 -1
- package/plugins/acryl-kv-db/plugin.json +4 -1
- package/plugins/acryl-kv-db/upstream.json +1 -1
- package/plugins/acryl-kv-json/plugin.json +4 -1
- package/plugins/acryl-kv-json/src/open.ts +86 -22
- package/plugins/acryl-kv-json/upstream.json +2 -2
- package/plugins/acryl-node-server/src/adapter/listener.ts +5 -0
- package/plugins/acryl-node-server/src/client-modules.ts +10 -9
- package/plugins/acryl-node-server/upstream.json +2 -2
- package/plugins/acryl-router/src/browser/read-sse.ts +3 -0
- package/plugins/acryl-router/src/helpers/read-sse.ts +111 -0
- package/plugins/acryl-router/src/service.ts +3 -0
- package/plugins/acryl-router/upstream.json +2 -2
- package/plugins/acryl-router-http/upstream.json +1 -1
- package/plugins/acryl-state/upstream.json +1 -1
- package/plugins/agent-panel/client/main.ts +159 -0
- package/plugins/agent-panel/index.ts +232 -0
- package/plugins/agent-panel/plugin.json +21 -0
- package/plugins/builder/index.ts +2 -2
- package/plugins/builder/plugin.json +1 -1
- package/plugins/desktop/main.ts +3 -3
- package/plugins/desktop/plugin.json +2 -2
- package/plugins/desktop/upstream.json +1 -1
- package/plugins/desktop-web/main.ts +2 -2
- package/plugins/desktop-web/plugin.json +2 -2
- package/plugins/desktop-web/upstream.json +1 -1
- package/plugins/extensions/index.ts +19 -1
- package/plugins/extensions/plugin.json +1 -1
- package/plugins/extensions/resolve.ts +17 -3
- package/plugins/manifest/lock.ts +7 -3
- package/plugins/manifest/upgrade-cli.ts +1 -1
- package/plugins/save/cli.ts +1 -1
- package/plugins/save/plugin.json +1 -1
- package/plugins/tsconfig.json +6 -0
- package/shared/AGENTS.core.md +10 -10
- package/shared/DEFAULTS.md +45 -0
- package/shared/SKILL.md +14 -0
- package/templates/agent/AGENTS.md +1 -1
- package/templates/agent/README.md +6 -6
- package/templates/agent/blend.yaml +3 -0
- package/templates/agent/gitignore +1 -0
- package/templates/agent/package.json +8 -8
- package/templates/agent/src/app.ts +46 -17
- package/templates/agent/src/main.ts +2 -2
- package/templates/agent/src/routes.ts +19 -6
- package/templates/agent/template.json +2 -1
- package/templates/agent/test/agent.test.ts +11 -5
- package/templates/agent/test/app.test.ts +30 -9
- package/templates/agent/test/builder.test.ts +35 -24
- package/templates/agent/test/model.test.ts +25 -20
- package/templates/agent/tsconfig.json +48 -50
- package/templates/blank/AGENTS.md +1 -1
- package/templates/blank/README.md +2 -2
- package/templates/blank/gitignore +1 -0
- package/templates/blank/package.json +8 -8
- package/templates/blank/src/about.tsx +2 -2
- package/templates/blank/src/app.ts +29 -9
- package/templates/blank/src/main.ts +1 -1
- package/templates/blank/src/routes.ts +1 -1
- package/templates/blank/test/app.test.ts +29 -10
- package/templates/blank/tsconfig.json +1 -1
- package/templates/chat/AGENTS.md +16 -0
- package/templates/chat/blend.yaml +29 -0
- package/templates/chat/gitignore +6 -0
- package/templates/chat/migrations/001-chat.sql +12 -0
- package/templates/chat/package.json +33 -0
- package/templates/chat/src/app.ts +106 -0
- package/templates/chat/src/chat-app.ts +101 -0
- package/templates/chat/src/client/chat.tsx +115 -0
- package/templates/chat/src/client/tsconfig.json +26 -0
- package/templates/chat/src/db.ts +43 -0
- package/templates/chat/src/door.ts +7 -0
- package/templates/chat/src/llm.ts +42 -0
- package/templates/chat/src/main.ts +28 -0
- package/templates/chat/src/page.tsx +40 -0
- package/templates/chat/src/routes.ts +15 -0
- package/templates/chat/src/runtime.ts +17 -0
- package/templates/chat/template.json +11 -0
- package/templates/chat/test/chat.test.ts +198 -0
- package/templates/chat/test/server.test.ts +78 -0
- package/templates/chat/tsconfig.json +20 -0
- package/templates/nodejs/AGENTS.md +9 -0
- package/templates/nodejs/blend.yaml +26 -0
- package/templates/nodejs/gitignore +3 -0
- package/templates/nodejs/package.json +30 -0
- package/templates/nodejs/src/app.ts +92 -0
- package/templates/nodejs/src/client/counter.tsx +12 -0
- package/templates/nodejs/src/client/tsconfig.json +26 -0
- package/templates/nodejs/src/door.ts +7 -0
- package/templates/nodejs/src/main.ts +25 -0
- package/templates/nodejs/src/page.tsx +29 -0
- package/templates/nodejs/src/routes.ts +40 -0
- package/templates/nodejs/template.json +3 -0
- package/templates/nodejs/test/app.test.ts +78 -0
- package/templates/nodejs/test/server.test.ts +83 -0
- package/templates/nodejs/tsconfig.json +20 -0
- package/templates/notes/AGENTS.md +26 -0
- package/templates/notes/blend.yaml +29 -0
- package/templates/notes/gitignore +6 -0
- package/templates/notes/migrations/001-notes.sql +18 -0
- package/templates/notes/package.json +33 -0
- package/templates/notes/src/app.ts +106 -0
- package/templates/notes/src/auth.ts +57 -0
- package/templates/notes/src/client/counter.tsx +21 -0
- package/templates/notes/src/client/tsconfig.json +26 -0
- package/templates/notes/src/db.ts +50 -0
- package/templates/notes/src/door.ts +7 -0
- package/templates/notes/src/main.ts +28 -0
- package/templates/notes/src/notes-app.ts +205 -0
- package/templates/notes/src/routes.ts +14 -0
- package/templates/notes/src/runtime.ts +17 -0
- package/templates/notes/src/schema.ts +38 -0
- package/templates/notes/src/views.tsx +136 -0
- package/templates/notes/template.json +11 -0
- package/templates/notes/test/notes.test.ts +280 -0
- package/templates/notes/test/server.test.ts +78 -0
- package/templates/notes/tsconfig.json +20 -0
- package/templates/todo/README.md +1 -1
- package/templates/todo/gitignore +1 -0
- package/templates/todo/package.json +8 -8
- package/templates/todo/src/app.ts +32 -12
- package/templates/todo/src/main.ts +1 -1
- package/templates/todo/src/todo.ts +4 -4
- package/templates/todo/test/todo.test.ts +48 -23
- package/templates/todo/tsconfig.json +1 -1
- package/templates/todo/upstream.json +2 -2
- package/lib/database-D_UN6We8.d.ts +0 -44
- package/lib/kv-DWDm0Lja.d.ts +0 -39
package/dist/index.d.ts
CHANGED
|
@@ -18,8 +18,10 @@ interface Io {
|
|
|
18
18
|
interface AddOptions {
|
|
19
19
|
readonly names: readonly string[];
|
|
20
20
|
readonly directory: string | undefined;
|
|
21
|
+
/** Also copy the port's conformance suite (`src/conformance.ts`) and the `vitest` it runs on: what a provider of your own must pass. */
|
|
22
|
+
readonly suite: boolean;
|
|
21
23
|
}
|
|
22
|
-
/** Parse `add <plugin...> [--dir <app>]`. */
|
|
24
|
+
/** Parse `add <plugin...> [--dir <app>] [--suite]`. */
|
|
23
25
|
export declare function parseAddOptions(argv: readonly string[]): AddOptions;
|
|
24
26
|
/**
|
|
25
27
|
* Copy library plugins into an existing app, with the plugins they require, and add their npm packages to its package.json.
|
|
@@ -29,9 +31,11 @@ export declare function add(options: AddOptions, io: Io): Promise<number>;
|
|
|
29
31
|
/** `acrylblends plugins`: the library, one line per plugin. */
|
|
30
32
|
export declare function listPlugins(io: Io): number;
|
|
31
33
|
//#endregion
|
|
32
|
-
//#region src/
|
|
34
|
+
//#region src/version.d.ts
|
|
33
35
|
/** The version of this package. */
|
|
34
36
|
export declare const VERSION: string;
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/cli.d.ts
|
|
35
39
|
export declare const HELP: string;
|
|
36
40
|
/**
|
|
37
41
|
* Run the CLI and return the exit code: 0 done, 1 failed or refused, 2 a usage error. Never calls `process.exit`.
|
|
@@ -39,8 +43,8 @@ export declare const HELP: string;
|
|
|
39
43
|
export declare function runCli(argv: readonly string[], io?: Io): Promise<number>;
|
|
40
44
|
//#endregion
|
|
41
45
|
//#region src/templates.d.ts
|
|
42
|
-
/** The templates: a
|
|
43
|
-
export declare const TEMPLATES: readonly ["blank", "todo", "agent"];
|
|
46
|
+
/** The templates, all Cordis apps: a web app with a page and a client component (nodejs), a notes app and a chat app on the db port (notes, chat), a blank app on the router, a todo list on the router and state, and the blank app with the offline agent. */
|
|
47
|
+
export declare const TEMPLATES: readonly ["nodejs", "notes", "chat", "blank", "todo", "agent"];
|
|
44
48
|
type Template = (typeof TEMPLATES)[number];
|
|
45
49
|
export declare function isTemplate(value: string): value is Template;
|
|
46
50
|
/**
|
|
@@ -86,6 +90,10 @@ interface LibraryPlugin {
|
|
|
86
90
|
readonly scripts?: Readonly<Record<string, string>>;
|
|
87
91
|
/** Lines the app's `.gitignore` needs for this plugin (added when missing, if the app has a `.gitignore`). */
|
|
88
92
|
readonly gitignore?: readonly string[];
|
|
93
|
+
/** The port's conformance suite, a path inside the plugin: shipped in the package, but copied into an app only by `acrylblends add <plugin> --suite` (it needs vitest). */
|
|
94
|
+
readonly suite?: string;
|
|
95
|
+
/** `package.json` "imports" entries the app gets (`#acryl-kv`): a path below the plugins folder. A plugin of your own imports the port by that name, in tests and the type-check as well as when installed live. */
|
|
96
|
+
readonly imports?: Readonly<Record<string, string>>;
|
|
89
97
|
/** How to mount it, shown after `acrylblends add`. */
|
|
90
98
|
readonly mount?: string;
|
|
91
99
|
/**
|
|
@@ -102,7 +110,7 @@ export declare function libraryRoot(): string;
|
|
|
102
110
|
export declare function readLibrary(root?: string): Map<string, LibraryPlugin>;
|
|
103
111
|
/** The plugins to copy for `names`: each one after everything it requires, no duplicates. */
|
|
104
112
|
export declare function closure(names: readonly string[], library: Map<string, LibraryPlugin>): LibraryPlugin[];
|
|
105
|
-
/** Copy one library plugin's source into `<app>/
|
|
113
|
+
/** Copy one library plugin's source into `<app>/plugins/<name>/`. Throws when that folder already exists. */
|
|
106
114
|
export declare function copyPlugin(plugin: LibraryPlugin, appDir: string, root?: string): string;
|
|
107
115
|
//#endregion
|
|
108
116
|
//#region src/project.d.ts
|
|
@@ -118,7 +126,7 @@ export declare function nameFrom(directory: string): string;
|
|
|
118
126
|
/** Refuse a target that exists and is not an empty directory. A missing target is fine: it is created. */
|
|
119
127
|
export declare function assertTargetFree(target: string): void;
|
|
120
128
|
/**
|
|
121
|
-
* Copy a template into `target`, replacing `__APP_NAME__` with `name
|
|
129
|
+
* Copy a template into `target`, replacing `__APP_NAME__` with `name` and `__ACRYLBLENDS_RANGE__` with the range of this release. Checks the target before writing anything.
|
|
122
130
|
*
|
|
123
131
|
* @returns the copied files, relative to `target`
|
|
124
132
|
*/
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { copyFileSync, cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { parseArgs } from "node:util";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -84,20 +84,49 @@ function closure(names, library) {
|
|
|
84
84
|
for (const name of names) visit(name, []);
|
|
85
85
|
return ordered;
|
|
86
86
|
}
|
|
87
|
-
/**
|
|
87
|
+
/** The one folder every plugin of an app lives in, library copies and the ones an agent writes. */
|
|
88
|
+
const PLUGINS_DIR = "plugins";
|
|
89
|
+
/** Where apps made before 0.7.8 kept library copies; still understood for one release. */
|
|
90
|
+
const LEGACY_PLUGINS_DIR = join("src", "plugins");
|
|
91
|
+
/** The list of library copies in the plugins folder; the extensions plugin installs every other folder there. */
|
|
92
|
+
const LIBRARY_LIST = "library.json";
|
|
93
|
+
/** The plugins folder of an app: `plugins/`, or `src/plugins/` for an app that still has the old layout (and no `plugins/`). */
|
|
94
|
+
function pluginsFolder(appDir) {
|
|
95
|
+
const legacy = existsSync(join(appDir, LEGACY_PLUGINS_DIR)) && !existsSync(join(appDir, "plugins"));
|
|
96
|
+
return {
|
|
97
|
+
dir: join(appDir, legacy ? LEGACY_PLUGINS_DIR : PLUGINS_DIR),
|
|
98
|
+
legacy
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
/** The warning for an app with the old layout. */
|
|
102
|
+
const LEGACY_WARNING = "warning: this app keeps its plugins in src/plugins/ (and extensions/); plugins now live in one folder, plugins/. Move them with \"git mv src/plugins plugins\", fix the paths in tsconfig.json, package.json and src/, and see docs/framework/agents/index.md. The old layout is read for this release only.";
|
|
103
|
+
function recordLibraryCopy(folder, name) {
|
|
104
|
+
const file = join(folder, LIBRARY_LIST);
|
|
105
|
+
const names = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")).plugins : [];
|
|
106
|
+
if (!names.includes(name)) names.push(name);
|
|
107
|
+
writeFileSync(file, `${JSON.stringify({ plugins: names.sort() }, null, 2)}\n`);
|
|
108
|
+
}
|
|
109
|
+
/** Copy one library plugin's source into `<app>/plugins/<name>/`. Throws when that folder already exists. */
|
|
88
110
|
function copyPlugin(plugin, appDir, root = libraryRoot()) {
|
|
89
|
-
const
|
|
90
|
-
|
|
111
|
+
const folder = pluginsFolder(appDir);
|
|
112
|
+
const target = join(folder.dir, plugin.name);
|
|
113
|
+
if (existsSync(target)) throw new LibraryError(`${relativeName(appDir, target)} already exists; remove it first to copy a fresh one`);
|
|
91
114
|
const source = join(root, plugin.name);
|
|
92
115
|
cpSync(source, target, {
|
|
93
116
|
recursive: true,
|
|
94
117
|
filter: (path) => {
|
|
95
118
|
const inside = path.slice(source.length + 1);
|
|
96
|
-
return !(dirname(path) === source && LIBRARY_ONLY.has(inside)) && !isOmitted(inside, plugin.omit);
|
|
119
|
+
return !(dirname(path) === source && LIBRARY_ONLY.has(inside)) && !isOmitted(inside, plugin.omit) && inside.split(sep).join("/") !== plugin.suite;
|
|
97
120
|
}
|
|
98
121
|
});
|
|
122
|
+
if (!folder.legacy) recordLibraryCopy(folder.dir, plugin.name);
|
|
99
123
|
return target;
|
|
100
124
|
}
|
|
125
|
+
const relativeName = (appDir, path) => path.slice(appDir.length + 1).split(sep).join("/");
|
|
126
|
+
//#endregion
|
|
127
|
+
//#region src/version.ts
|
|
128
|
+
/** The version of this package. */
|
|
129
|
+
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
101
130
|
//#endregion
|
|
102
131
|
//#region src/project.ts
|
|
103
132
|
/** Thrown when the target directory exists and is not empty. Nothing has been written. */
|
|
@@ -123,7 +152,7 @@ function assertTargetFree(target) {
|
|
|
123
152
|
if (!statSync(target).isDirectory() || readdirSync(target).length > 0) throw new TargetNotEmptyError(target);
|
|
124
153
|
}
|
|
125
154
|
/**
|
|
126
|
-
* Copy a template into `target`, replacing `__APP_NAME__` with `name
|
|
155
|
+
* Copy a template into `target`, replacing `__APP_NAME__` with `name` and `__ACRYLBLENDS_RANGE__` with the range of this release. Checks the target before writing anything.
|
|
127
156
|
*
|
|
128
157
|
* @returns the copied files, relative to `target`
|
|
129
158
|
*/
|
|
@@ -143,7 +172,7 @@ function copyTemplate(source, target, name) {
|
|
|
143
172
|
continue;
|
|
144
173
|
}
|
|
145
174
|
const text = readFileSync(sourcePath, "utf8");
|
|
146
|
-
if (text.includes("__APP_NAME__")) writeFileSync(targetPath, text.replaceAll("__APP_NAME__", name));
|
|
175
|
+
if (text.includes("__APP_NAME__") || text.includes("__ACRYLBLENDS_RANGE__")) writeFileSync(targetPath, text.replaceAll("__APP_NAME__", name).replaceAll("__ACRYLBLENDS_RANGE__", `^${VERSION}`));
|
|
147
176
|
else cpSync(sourcePath, targetPath);
|
|
148
177
|
written.push(`${prefix}${targetName}`);
|
|
149
178
|
}
|
|
@@ -159,7 +188,7 @@ function readTemplateDesktopPath(templateDir) {
|
|
|
159
188
|
if (!existsSync(file)) return void 0;
|
|
160
189
|
return JSON.parse(readFileSync(file, "utf8")).desktopPath;
|
|
161
190
|
}
|
|
162
|
-
/** Record where the desktop window opens in package.json (`acrylblends.desktopPath`), read by `
|
|
191
|
+
/** Record where the desktop window opens in package.json (`acrylblends.desktopPath`), read by `plugins/desktop/main.ts`. */
|
|
163
192
|
function writeDesktopPath(appDir, path) {
|
|
164
193
|
const file = join(appDir, "package.json");
|
|
165
194
|
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
|
@@ -189,6 +218,19 @@ function mergeDependencies(appDir, dependencies) {
|
|
|
189
218
|
writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
190
219
|
return added;
|
|
191
220
|
}
|
|
221
|
+
/** Add `package.json` "imports" entries (`#acryl-kv`: `./plugins/acryl-kv/src/index.ts`); an entry the app already has is kept. Returns the names added. */
|
|
222
|
+
function mergeImports(appDir, imports, pluginsDir) {
|
|
223
|
+
const file = join(appDir, "package.json");
|
|
224
|
+
const manifest = JSON.parse(readFileSync(file, "utf8"));
|
|
225
|
+
const current = manifest.imports ?? {};
|
|
226
|
+
const added = Object.keys(imports).filter((name) => !(name in current));
|
|
227
|
+
manifest.imports = {
|
|
228
|
+
...Object.fromEntries(Object.entries(imports).map(([name, path]) => [name, `./${pluginsDir}/${path}`])),
|
|
229
|
+
...current
|
|
230
|
+
};
|
|
231
|
+
writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
232
|
+
return added;
|
|
233
|
+
}
|
|
192
234
|
/** Declare a file as an opener in package.json (`"acryl": { "opener": [...] }`): the one place that may import a database provider. */
|
|
193
235
|
function mergeOpener(appDir, opener) {
|
|
194
236
|
const file = join(appDir, "package.json");
|
|
@@ -241,9 +283,17 @@ function appendGitignore(appDir, lines) {
|
|
|
241
283
|
function writeAgentFiles(sharedDir, templateDir, target, name) {
|
|
242
284
|
const core = readFileSync(join(sharedDir, "AGENTS.core.md"), "utf8");
|
|
243
285
|
const own = existsSync(join(templateDir, "AGENTS.md")) ? readFileSync(join(templateDir, "AGENTS.md"), "utf8") : "";
|
|
244
|
-
|
|
245
|
-
writeFileSync(join(target, "
|
|
246
|
-
|
|
286
|
+
const text = `${core}${readFileSync(join(sharedDir, "DEFAULTS.md"), "utf8")}${own}`.replaceAll("__APP_NAME__", name);
|
|
287
|
+
writeFileSync(join(target, "AGENTS.md"), text);
|
|
288
|
+
writeFileSync(join(target, "CLAUDE.md"), text);
|
|
289
|
+
const skill = readFileSync(join(sharedDir, "SKILL.md"), "utf8").replaceAll("__APP_NAME__", name);
|
|
290
|
+
const written = ["AGENTS.md", "CLAUDE.md"];
|
|
291
|
+
for (const folder of [".claude/skills/app-guide", ".agents/skills/app-guide"]) {
|
|
292
|
+
mkdirSync(join(target, folder), { recursive: true });
|
|
293
|
+
writeFileSync(join(target, folder, "SKILL.md"), skill);
|
|
294
|
+
written.push(`${folder}/SKILL.md`);
|
|
295
|
+
}
|
|
296
|
+
return written;
|
|
247
297
|
}
|
|
248
298
|
/**
|
|
249
299
|
* Copy the framework's docs (a generated copy of ACRYL's `docs/framework`, already checked against its digest) into `<app>/docs/framework/`,
|
|
@@ -273,8 +323,11 @@ function copyFrameworkDocs(source, target) {
|
|
|
273
323
|
}
|
|
274
324
|
//#endregion
|
|
275
325
|
//#region src/templates.ts
|
|
276
|
-
/** The templates: a
|
|
326
|
+
/** The templates, all Cordis apps: a web app with a page and a client component (nodejs), a notes app and a chat app on the db port (notes, chat), a blank app on the router, a todo list on the router and state, and the blank app with the offline agent. */
|
|
277
327
|
const TEMPLATES = [
|
|
328
|
+
"nodejs",
|
|
329
|
+
"notes",
|
|
330
|
+
"chat",
|
|
278
331
|
"blank",
|
|
279
332
|
"todo",
|
|
280
333
|
"agent"
|
|
@@ -394,6 +447,7 @@ async function init(options, io) {
|
|
|
394
447
|
const plugins = closure([...readTemplatePlugins(templateDir(template)), ...options.desktop ? ["desktop"] : []], readLibrary());
|
|
395
448
|
for (const plugin of plugins) {
|
|
396
449
|
copyPlugin(plugin, directory);
|
|
450
|
+
if (plugin.imports) mergeImports(directory, plugin.imports, "plugins");
|
|
397
451
|
mergeDependencies(directory, plugin.dependencies);
|
|
398
452
|
mergeTooling(directory, plugin);
|
|
399
453
|
}
|
|
@@ -414,7 +468,7 @@ async function init(options, io) {
|
|
|
414
468
|
}
|
|
415
469
|
//#endregion
|
|
416
470
|
//#region src/add.ts
|
|
417
|
-
/** Parse `add <plugin...> [--dir <app>]`. */
|
|
471
|
+
/** Parse `add <plugin...> [--dir <app>] [--suite]`. */
|
|
418
472
|
function parseAddOptions(argv) {
|
|
419
473
|
let parsed;
|
|
420
474
|
try {
|
|
@@ -425,16 +479,20 @@ function parseAddOptions(argv) {
|
|
|
425
479
|
if (parsed.positionals.length === 0) throw new UsageError("add needs at least one plugin name (see \"acrylblends plugins\")");
|
|
426
480
|
return {
|
|
427
481
|
names: parsed.positionals,
|
|
428
|
-
directory: parsed.values.dir
|
|
482
|
+
directory: parsed.values.dir,
|
|
483
|
+
suite: parsed.values.suite === true
|
|
429
484
|
};
|
|
430
485
|
}
|
|
431
486
|
const parse$1 = (argv) => parseArgs({
|
|
432
487
|
args: [...argv],
|
|
433
488
|
allowPositionals: true,
|
|
434
489
|
strict: true,
|
|
435
|
-
options: {
|
|
490
|
+
options: {
|
|
491
|
+
dir: { type: "string" },
|
|
492
|
+
suite: { type: "boolean" }
|
|
493
|
+
}
|
|
436
494
|
});
|
|
437
|
-
const present = (appDir, name) => existsSync(join(appDir,
|
|
495
|
+
const present = (appDir, name) => existsSync(join(pluginsFolder(appDir).dir, name));
|
|
438
496
|
/**
|
|
439
497
|
* Copy library plugins into an existing app, with the plugins they require, and add their npm packages to its package.json.
|
|
440
498
|
* Refuses a plugin the app already has. Returns the exit code.
|
|
@@ -445,12 +503,13 @@ async function add(options, io) {
|
|
|
445
503
|
io.stderr.write(`error: ${appDir} is not an app (no package.json and src/); run "acrylblends init" first\n`);
|
|
446
504
|
return 1;
|
|
447
505
|
}
|
|
506
|
+
if (pluginsFolder(appDir).legacy) io.stderr.write(`${LEGACY_WARNING}\n`);
|
|
448
507
|
const external = options.names.filter((name) => /[\\/]/.test(name) || name.startsWith("."));
|
|
449
508
|
for (const folder of external) {
|
|
450
509
|
const code = addExternal(resolve(io.cwd, folder), appDir, io);
|
|
451
510
|
if (code !== 0) return code;
|
|
452
511
|
}
|
|
453
|
-
const plain = !existsSync(join(appDir, "
|
|
512
|
+
const plain = !existsSync(join(pluginsFolder(appDir).dir, "instance"));
|
|
454
513
|
if (plain && options.names.includes("db")) {
|
|
455
514
|
io.stderr.write("error: \"db\" is the Cordis plugin for a Blend app. A plain Acryl app opens its database with openDb from \"acrylblends/db/sqlite\" (see docs/framework/guides/data.md)\n");
|
|
456
515
|
return 1;
|
|
@@ -461,15 +520,16 @@ async function add(options, io) {
|
|
|
461
520
|
}
|
|
462
521
|
const names = options.names.filter((name) => !external.includes(name)).map((name) => name === "db" ? "acryl-db-sqlite" : name).map((name) => name === "kv" ? "acryl-kv-json" : name).map((name) => name === "desktop" && plain ? "desktop-web" : name);
|
|
463
522
|
if (names.length === 0) return 0;
|
|
464
|
-
const already = names.filter((name) => present(appDir, name));
|
|
523
|
+
const already = options.suite ? [] : names.filter((name) => present(appDir, name));
|
|
465
524
|
if (already.length > 0) {
|
|
466
|
-
io.stderr.write(`error: the app already has ${already.map((name) =>
|
|
525
|
+
io.stderr.write(`error: the app already has ${already.map((name) => `${relative(appDir, join(pluginsFolder(appDir).dir, name)).split(sep).join("/")}`).join(", ")}\n`);
|
|
467
526
|
return 1;
|
|
468
527
|
}
|
|
469
528
|
const toCopy = closure(names, readLibrary()).filter((plugin) => !present(appDir, plugin.name));
|
|
470
529
|
const addedDependencies = [];
|
|
471
530
|
for (const plugin of toCopy) {
|
|
472
531
|
copyPlugin(plugin, appDir);
|
|
532
|
+
if (plugin.imports) mergeImports(appDir, plugin.imports, relative(appDir, pluginsFolder(appDir).dir).split(sep).join("/"));
|
|
473
533
|
addedDependencies.push(...mergeDependencies(appDir, plugin.dependencies));
|
|
474
534
|
const tooling = mergeTooling(appDir, plugin);
|
|
475
535
|
appendGitignore(appDir, plugin.gitignore);
|
|
@@ -477,15 +537,49 @@ async function add(options, io) {
|
|
|
477
537
|
addedDependencies.push(...tooling.devDependencies);
|
|
478
538
|
if (tooling.scripts.length > 0) io.stdout.write(`new scripts: ${tooling.scripts.map((name) => `npm run ${name}`).join(", ")}\n`);
|
|
479
539
|
}
|
|
540
|
+
if (options.suite) {
|
|
541
|
+
const refused = copySuites(names, appDir, addedDependencies, io);
|
|
542
|
+
if (refused !== 0) return refused;
|
|
543
|
+
}
|
|
480
544
|
io.stdout.write(`added ${toCopy.map((plugin) => plugin.name).join(", ")} to ${appDir}\n`);
|
|
481
545
|
if (addedDependencies.length > 0) io.stdout.write(`new dependencies: ${addedDependencies.join(", ")} (run npm install)\n`);
|
|
482
|
-
for (const plugin of toCopy.filter((plugin) => names.includes(plugin.name))) io.stdout.write(`\n${plugin.name}: ${plugin.mount ?? `import it from '
|
|
546
|
+
for (const plugin of toCopy.filter((plugin) => names.includes(plugin.name))) io.stdout.write(`\n${plugin.name}: ${plugin.mount ?? `import it from '../plugins/${plugin.name}/index.ts' and mount it in src/app.ts`}\n`);
|
|
547
|
+
return 0;
|
|
548
|
+
}
|
|
549
|
+
/** The vitest the suites run on (the version this package is tested with). */
|
|
550
|
+
const SUITE_RUNNER = { vitest: "5.0.3" };
|
|
551
|
+
/**
|
|
552
|
+
* Copy the conformance suite of each named port next to its source (`plugins/<name>/src/conformance.ts`), and add `vitest` to the app's development
|
|
553
|
+
* dependencies. The suite is left out of a normal copy because an app that only uses a port has no use for it. Returns the exit code: 1 when a plugin has none.
|
|
554
|
+
*/
|
|
555
|
+
function copySuites(names, appDir, added, io) {
|
|
556
|
+
const copies = [];
|
|
557
|
+
const ports = [...new Set(names.map((name) => name.replace(/^(acryl-(?:kv|db))(?:-.*)?$/u, "$1")))];
|
|
558
|
+
const library = readLibrary();
|
|
559
|
+
for (const name of ports) {
|
|
560
|
+
const suite = library.get(name)?.suite;
|
|
561
|
+
const from = suite === void 0 ? "" : join(libraryRoot(), name, suite);
|
|
562
|
+
if (suite === void 0 || !existsSync(from)) {
|
|
563
|
+
io.stderr.write(`error: ${name} has no conformance suite (the ports db and kv do)\n`);
|
|
564
|
+
return 1;
|
|
565
|
+
}
|
|
566
|
+
copies.push({
|
|
567
|
+
from,
|
|
568
|
+
to: join(pluginsFolder(appDir).dir, name, suite)
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
for (const { from, to } of copies) {
|
|
572
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
573
|
+
copyFileSync(from, to);
|
|
574
|
+
io.stdout.write(`added the conformance suite ${relative(appDir, to).split(sep).join("/")}\n`);
|
|
575
|
+
}
|
|
576
|
+
added.push(...mergeTooling(appDir, { devDependencies: SUITE_RUNNER }).devDependencies);
|
|
483
577
|
return 0;
|
|
484
578
|
}
|
|
485
579
|
const PLUGIN_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
486
580
|
/**
|
|
487
581
|
* Bring an external plugin folder (a package made with the builder's package_plugin, or any folder with an index.ts that exports a
|
|
488
|
-
* Cordis plugin) into the app's `
|
|
582
|
+
* Cordis plugin) into the app's `plugins/`, where the extensions plugin installs it like any other: checked, staged, started,
|
|
489
583
|
* verified. Its runtime dependencies are not installed here; they are reported so the user adds them deliberately.
|
|
490
584
|
*/
|
|
491
585
|
function addExternal(folder, appDir, io) {
|
|
@@ -508,16 +602,16 @@ function addExternal(folder, appDir, io) {
|
|
|
508
602
|
io.stderr.write(`error: "${name}" is not a plugin name (lowercase letters, digits and dashes)\n`);
|
|
509
603
|
return 1;
|
|
510
604
|
}
|
|
511
|
-
const target = join(appDir, "extensions", name);
|
|
605
|
+
const target = join(pluginsFolder(appDir).legacy ? join(appDir, "extensions") : join(appDir, PLUGINS_DIR), name);
|
|
512
606
|
if (existsSync(target)) {
|
|
513
|
-
io.stderr.write(`error: the app already has
|
|
607
|
+
io.stderr.write(`error: the app already has ${relative(appDir, target).split(sep).join("/")}\n`);
|
|
514
608
|
return 1;
|
|
515
609
|
}
|
|
516
610
|
cpSync(folder, target, {
|
|
517
611
|
recursive: true,
|
|
518
612
|
filter: (path) => !/[\\/](node_modules|\.git)([\\/]|$)/.test(path.slice(folder.length))
|
|
519
613
|
});
|
|
520
|
-
io.stdout.write(`added external plugin ${name} to ${
|
|
614
|
+
io.stdout.write(`added external plugin ${name} to ${dirname(target)}; it installs at the next start\n`);
|
|
521
615
|
const dependencies = Object.keys(manifest.dependencies ?? {});
|
|
522
616
|
if (dependencies.length > 0) io.stdout.write(`it uses ${dependencies.join(", ")}: add them to the app with npm install first\n`);
|
|
523
617
|
return 0;
|
|
@@ -596,9 +690,14 @@ const DRIVERS = [
|
|
|
596
690
|
const OPENERS = ["acrylblends/db/sqlite", "acryl-db-sqlite"];
|
|
597
691
|
/** The desktop shell is where Electron lives (`acrylblends add desktop` copies it). */
|
|
598
692
|
const ELECTRON = "electron";
|
|
599
|
-
const ELECTRON_HOMES = [
|
|
693
|
+
const ELECTRON_HOMES = [
|
|
694
|
+
"plugins/desktop/",
|
|
695
|
+
"plugins/desktop-web/",
|
|
696
|
+
"src/plugins/desktop/",
|
|
697
|
+
"src/plugins/desktop-web/"
|
|
698
|
+
];
|
|
600
699
|
/** The provider plugins themselves may import what they wrap. */
|
|
601
|
-
const PROVIDER_HOMES = ["src/plugins/acryl-db-sqlite/"];
|
|
700
|
+
const PROVIDER_HOMES = ["plugins/acryl-db-sqlite/", "src/plugins/acryl-db-sqlite/"];
|
|
602
701
|
const SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/u;
|
|
603
702
|
const SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
604
703
|
"node_modules",
|
|
@@ -611,6 +710,7 @@ const ROOTS = [
|
|
|
611
710
|
"src",
|
|
612
711
|
"test",
|
|
613
712
|
"tests",
|
|
713
|
+
"plugins",
|
|
614
714
|
"extensions"
|
|
615
715
|
];
|
|
616
716
|
const matches = (specifier, name) => specifier === name || specifier.startsWith(`${name}/`);
|
|
@@ -709,7 +809,7 @@ function findViolations(appDir) {
|
|
|
709
809
|
}
|
|
710
810
|
const GUIDE = "docs/framework/guides/data.md#why-the-database-is-opened-in-one-place";
|
|
711
811
|
function advice(violation, openers) {
|
|
712
|
-
if (violation.kind === "electron") return `"${violation.specifier}" is the desktop shell's: only
|
|
812
|
+
if (violation.kind === "electron") return `"${violation.specifier}" is the desktop shell's: only plugins/desktop (added with "acrylblends add desktop") imports electron`;
|
|
713
813
|
const where = openers.length > 0 ? openers.join(", ") : "a file named in package.json \"acryl\": { \"opener\": \"...\" }";
|
|
714
814
|
return violation.kind === "driver" ? `"${violation.specifier}" is a database driver; take a Db (import type { Db } from "acrylblends/db") as an argument and let ${where} open the database` : `"${violation.specifier}" opens a database, and only ${where} may; take a Db (import type { Db } from "acrylblends/db") as an argument`;
|
|
715
815
|
}
|
|
@@ -871,14 +971,13 @@ async function kvImport(options, io) {
|
|
|
871
971
|
}
|
|
872
972
|
//#endregion
|
|
873
973
|
//#region src/cli.ts
|
|
874
|
-
/** The version of this package. */
|
|
875
|
-
const VERSION = createRequire(import.meta.url)("../package.json").version;
|
|
876
974
|
const HELP = `acrylblends ${VERSION} - scaffold Cordis apps
|
|
877
975
|
|
|
878
976
|
Usage:
|
|
879
977
|
acrylblends init [dir] [options] Create a Cordis app in an empty or new directory
|
|
880
|
-
acrylblends add <plugin...> [--dir d] Copy library plugins (and what they require) into an app;
|
|
881
|
-
a folder path brings an external plugin into its
|
|
978
|
+
acrylblends add <plugin...> [--dir d] [--suite] Copy library plugins (and what they require) into an app;
|
|
979
|
+
a folder path brings an external plugin into its plugins/;
|
|
980
|
+
--suite also copies the port's conformance suite and vitest (add kv --suite)
|
|
882
981
|
acrylblends plugins List the plugin library
|
|
883
982
|
acrylblends build-client [root] [out] Compile the client modules once for production (src/client to dist/client)
|
|
884
983
|
acrylblends kv import <kv.json> <db> Move a JSON-file kv store into the kv table of a SQLite database [--overwrite] [--table t] [--migrations dir]
|