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.
Files changed (195) hide show
  1. package/dist/index.d.ts +14 -6
  2. package/dist/index.js +130 -31
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/README.md +3 -3
  5. package/docs/framework/UPSTREAM.md +1 -1
  6. package/docs/framework/agents/index.md +18 -3
  7. package/docs/framework/getting-started/basic.md +1 -2
  8. package/docs/framework/getting-started/desktop.md +3 -3
  9. package/docs/framework/guides/bring-your-own-tool.md +216 -0
  10. package/docs/framework/guides/client-modules.md +4 -3
  11. package/docs/framework/guides/create-acrylblends.md +5 -4
  12. package/docs/framework/guides/data.md +7 -6
  13. package/docs/framework/helpers/streaming.md +12 -27
  14. package/docs/framework/upstream.json +3 -3
  15. package/lib/browser/read-sse.js +84 -0
  16. package/lib/db/sqlite.d.ts +1 -1
  17. package/lib/db.d.ts +1 -84
  18. package/lib/index-BLTYsyc1.d.ts +93 -0
  19. package/lib/index-D4FAsBZu.d.ts +85 -0
  20. package/lib/kv/db.d.ts +2 -2
  21. package/lib/kv/json.d.ts +1 -1
  22. package/lib/kv/json.js +87 -24
  23. package/lib/kv.d.ts +1 -92
  24. package/lib/node-server.d.ts +3 -3
  25. package/lib/node-server.js +14 -8
  26. package/lib/read-sse.d.ts +33 -0
  27. package/lib/read-sse.js +84 -0
  28. package/package.json +5 -1
  29. package/plugins/acryl-db/plugin.json +7 -2
  30. package/plugins/acryl-db/src/conformance.ts +224 -0
  31. package/plugins/acryl-db/upstream.json +2 -2
  32. package/plugins/acryl-db-sqlite/plugin.json +4 -1
  33. package/plugins/acryl-db-sqlite/upstream.json +1 -1
  34. package/plugins/acryl-door/LICENSE +47 -0
  35. package/plugins/acryl-door/plugin.json +14 -0
  36. package/plugins/acryl-door/src/cli/acryl.ts +207 -0
  37. package/plugins/acryl-door/src/host/api.ts +9 -0
  38. package/plugins/acryl-door/src/host/audit.ts +64 -0
  39. package/plugins/acryl-door/src/host/channel.ts +142 -0
  40. package/plugins/acryl-door/src/host/chat-tools.ts +39 -0
  41. package/plugins/acryl-door/src/host/door-file.ts +94 -0
  42. package/plugins/acryl-door/src/host/door.ts +195 -0
  43. package/plugins/acryl-door/src/host/http.ts +80 -0
  44. package/plugins/acryl-door/src/host/mcp-routes.ts +83 -0
  45. package/plugins/acryl-door/src/host/operator-routes.ts +38 -0
  46. package/plugins/acryl-door/src/host/page-routes.ts +65 -0
  47. package/plugins/acryl-door/src/host/shim.ts +65 -0
  48. package/plugins/acryl-door/src/host/verbs.ts +109 -0
  49. package/plugins/acryl-door/src/index.ts +133 -0
  50. package/plugins/acryl-door/src/page/api.ts +9 -0
  51. package/plugins/acryl-door/src/page/channel.ts +135 -0
  52. package/plugins/acryl-door/src/page/driver/accessibility.ts +163 -0
  53. package/plugins/acryl-door/src/page/driver/actions.ts +186 -0
  54. package/plugins/acryl-door/src/page/driver/driver.ts +153 -0
  55. package/plugins/acryl-door/src/page/driver/ref-table.ts +78 -0
  56. package/plugins/acryl-door/src/page/driver/sensitivity.ts +49 -0
  57. package/plugins/acryl-door/src/page/driver/snapshot.ts +132 -0
  58. package/plugins/acryl-door/src/page/index.ts +40 -0
  59. package/plugins/acryl-door/src/page/indicator.ts +72 -0
  60. package/plugins/acryl-door/src/page/main.ts +7 -0
  61. package/plugins/acryl-door/src/page/protocol.ts +213 -0
  62. package/plugins/acryl-door/src/page/snapshot-text.ts +18 -0
  63. package/plugins/acryl-door/src/page/user-input.ts +24 -0
  64. package/plugins/acryl-door/upstream.json +28 -0
  65. package/plugins/acryl-inspect/upstream.json +1 -1
  66. package/plugins/acryl-kv/plugin.json +7 -2
  67. package/plugins/acryl-kv/src/conformance.ts +226 -0
  68. package/plugins/acryl-kv/upstream.json +1 -1
  69. package/plugins/acryl-kv-db/plugin.json +4 -1
  70. package/plugins/acryl-kv-db/upstream.json +1 -1
  71. package/plugins/acryl-kv-json/plugin.json +4 -1
  72. package/plugins/acryl-kv-json/src/open.ts +86 -22
  73. package/plugins/acryl-kv-json/upstream.json +2 -2
  74. package/plugins/acryl-node-server/src/adapter/listener.ts +5 -0
  75. package/plugins/acryl-node-server/src/client-modules.ts +10 -9
  76. package/plugins/acryl-node-server/upstream.json +2 -2
  77. package/plugins/acryl-router/src/browser/read-sse.ts +3 -0
  78. package/plugins/acryl-router/src/helpers/read-sse.ts +111 -0
  79. package/plugins/acryl-router/src/service.ts +3 -0
  80. package/plugins/acryl-router/upstream.json +2 -2
  81. package/plugins/acryl-router-http/upstream.json +1 -1
  82. package/plugins/acryl-state/upstream.json +1 -1
  83. package/plugins/agent-panel/client/main.ts +159 -0
  84. package/plugins/agent-panel/index.ts +232 -0
  85. package/plugins/agent-panel/plugin.json +21 -0
  86. package/plugins/builder/index.ts +2 -2
  87. package/plugins/builder/plugin.json +1 -1
  88. package/plugins/desktop/main.ts +3 -3
  89. package/plugins/desktop/plugin.json +2 -2
  90. package/plugins/desktop/upstream.json +1 -1
  91. package/plugins/desktop-web/main.ts +2 -2
  92. package/plugins/desktop-web/plugin.json +2 -2
  93. package/plugins/desktop-web/upstream.json +1 -1
  94. package/plugins/extensions/index.ts +19 -1
  95. package/plugins/extensions/plugin.json +1 -1
  96. package/plugins/extensions/resolve.ts +17 -3
  97. package/plugins/manifest/lock.ts +7 -3
  98. package/plugins/manifest/upgrade-cli.ts +1 -1
  99. package/plugins/save/cli.ts +1 -1
  100. package/plugins/save/plugin.json +1 -1
  101. package/plugins/tsconfig.json +6 -0
  102. package/shared/AGENTS.core.md +10 -10
  103. package/shared/DEFAULTS.md +45 -0
  104. package/shared/SKILL.md +14 -0
  105. package/templates/agent/AGENTS.md +1 -1
  106. package/templates/agent/README.md +6 -6
  107. package/templates/agent/blend.yaml +3 -0
  108. package/templates/agent/gitignore +1 -0
  109. package/templates/agent/package.json +8 -8
  110. package/templates/agent/src/app.ts +46 -17
  111. package/templates/agent/src/main.ts +2 -2
  112. package/templates/agent/src/routes.ts +19 -6
  113. package/templates/agent/template.json +2 -1
  114. package/templates/agent/test/agent.test.ts +11 -5
  115. package/templates/agent/test/app.test.ts +30 -9
  116. package/templates/agent/test/builder.test.ts +35 -24
  117. package/templates/agent/test/model.test.ts +25 -20
  118. package/templates/agent/tsconfig.json +48 -50
  119. package/templates/blank/AGENTS.md +1 -1
  120. package/templates/blank/README.md +2 -2
  121. package/templates/blank/gitignore +1 -0
  122. package/templates/blank/package.json +8 -8
  123. package/templates/blank/src/about.tsx +2 -2
  124. package/templates/blank/src/app.ts +29 -9
  125. package/templates/blank/src/main.ts +1 -1
  126. package/templates/blank/src/routes.ts +1 -1
  127. package/templates/blank/test/app.test.ts +29 -10
  128. package/templates/blank/tsconfig.json +1 -1
  129. package/templates/chat/AGENTS.md +16 -0
  130. package/templates/chat/blend.yaml +29 -0
  131. package/templates/chat/gitignore +6 -0
  132. package/templates/chat/migrations/001-chat.sql +12 -0
  133. package/templates/chat/package.json +33 -0
  134. package/templates/chat/src/app.ts +106 -0
  135. package/templates/chat/src/chat-app.ts +101 -0
  136. package/templates/chat/src/client/chat.tsx +115 -0
  137. package/templates/chat/src/client/tsconfig.json +26 -0
  138. package/templates/chat/src/db.ts +43 -0
  139. package/templates/chat/src/door.ts +7 -0
  140. package/templates/chat/src/llm.ts +42 -0
  141. package/templates/chat/src/main.ts +28 -0
  142. package/templates/chat/src/page.tsx +40 -0
  143. package/templates/chat/src/routes.ts +15 -0
  144. package/templates/chat/src/runtime.ts +17 -0
  145. package/templates/chat/template.json +11 -0
  146. package/templates/chat/test/chat.test.ts +198 -0
  147. package/templates/chat/test/server.test.ts +78 -0
  148. package/templates/chat/tsconfig.json +20 -0
  149. package/templates/nodejs/AGENTS.md +9 -0
  150. package/templates/nodejs/blend.yaml +26 -0
  151. package/templates/nodejs/gitignore +3 -0
  152. package/templates/nodejs/package.json +30 -0
  153. package/templates/nodejs/src/app.ts +92 -0
  154. package/templates/nodejs/src/client/counter.tsx +12 -0
  155. package/templates/nodejs/src/client/tsconfig.json +26 -0
  156. package/templates/nodejs/src/door.ts +7 -0
  157. package/templates/nodejs/src/main.ts +25 -0
  158. package/templates/nodejs/src/page.tsx +29 -0
  159. package/templates/nodejs/src/routes.ts +40 -0
  160. package/templates/nodejs/template.json +3 -0
  161. package/templates/nodejs/test/app.test.ts +78 -0
  162. package/templates/nodejs/test/server.test.ts +83 -0
  163. package/templates/nodejs/tsconfig.json +20 -0
  164. package/templates/notes/AGENTS.md +26 -0
  165. package/templates/notes/blend.yaml +29 -0
  166. package/templates/notes/gitignore +6 -0
  167. package/templates/notes/migrations/001-notes.sql +18 -0
  168. package/templates/notes/package.json +33 -0
  169. package/templates/notes/src/app.ts +106 -0
  170. package/templates/notes/src/auth.ts +57 -0
  171. package/templates/notes/src/client/counter.tsx +21 -0
  172. package/templates/notes/src/client/tsconfig.json +26 -0
  173. package/templates/notes/src/db.ts +50 -0
  174. package/templates/notes/src/door.ts +7 -0
  175. package/templates/notes/src/main.ts +28 -0
  176. package/templates/notes/src/notes-app.ts +205 -0
  177. package/templates/notes/src/routes.ts +14 -0
  178. package/templates/notes/src/runtime.ts +17 -0
  179. package/templates/notes/src/schema.ts +38 -0
  180. package/templates/notes/src/views.tsx +136 -0
  181. package/templates/notes/template.json +11 -0
  182. package/templates/notes/test/notes.test.ts +280 -0
  183. package/templates/notes/test/server.test.ts +78 -0
  184. package/templates/notes/tsconfig.json +20 -0
  185. package/templates/todo/README.md +1 -1
  186. package/templates/todo/gitignore +1 -0
  187. package/templates/todo/package.json +8 -8
  188. package/templates/todo/src/app.ts +32 -12
  189. package/templates/todo/src/main.ts +1 -1
  190. package/templates/todo/src/todo.ts +4 -4
  191. package/templates/todo/test/todo.test.ts +48 -23
  192. package/templates/todo/tsconfig.json +1 -1
  193. package/templates/todo/upstream.json +2 -2
  194. package/lib/database-D_UN6We8.d.ts +0 -44
  195. 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/cli.d.ts
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 blank Cordis app on the router, a todo list built only on the router and state, and the blank app with the offline agent. */
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>/src/plugins/<name>/`. Throws when that folder already exists. */
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`. Checks the target before writing anything.
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
- /** Copy one library plugin's source into `<app>/src/plugins/<name>/`. Throws when that folder already exists. */
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 target = join(appDir, "src", "plugins", plugin.name);
90
- if (existsSync(target)) throw new LibraryError(`src/plugins/${plugin.name} already exists; remove it first to copy a fresh one`);
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`. Checks the target before writing anything.
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 `src/plugins/desktop/main.ts`. */
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
- writeFileSync(join(target, "AGENTS.md"), `${core}${own}`.replaceAll("__APP_NAME__", name));
245
- writeFileSync(join(target, "CLAUDE.md"), "@AGENTS.md\n");
246
- return ["AGENTS.md", "CLAUDE.md"];
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 blank Cordis app on the router, a todo list built only on the router and state, and the blank app with the offline agent. */
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: { dir: { type: "string" } }
490
+ options: {
491
+ dir: { type: "string" },
492
+ suite: { type: "boolean" }
493
+ }
436
494
  });
437
- const present = (appDir, name) => existsSync(join(appDir, "src", "plugins", name));
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, "src", "plugins", "instance"));
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) => `src/plugins/${name}`).join(", ")}\n`);
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 './plugins/${plugin.name}/index.ts' and mount it in src/app.ts`}\n`);
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 `extensions/`, where the extensions plugin installs it like any other: checked, staged, started,
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 extensions/${name}\n`);
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 ${join(appDir, "extensions")}; it installs at the next start\n`);
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 = ["src/plugins/desktop/", "src/plugins/desktop-web/"];
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 src/plugins/desktop (added with "acrylblends add desktop") imports electron`;
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 extensions/
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]