acrylblends 0.7.8 → 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 (141) hide show
  1. package/dist/index.d.ts +13 -5
  2. package/dist/index.js +72 -13
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/README.md +2 -2
  5. package/docs/framework/UPSTREAM.md +1 -1
  6. package/docs/framework/getting-started/basic.md +1 -2
  7. package/docs/framework/getting-started/desktop.md +1 -1
  8. package/docs/framework/guides/bring-your-own-tool.md +216 -0
  9. package/docs/framework/guides/create-acrylblends.md +5 -4
  10. package/docs/framework/guides/data.md +2 -1
  11. package/docs/framework/upstream.json +3 -3
  12. package/lib/node-server.js +1 -0
  13. package/package.json +1 -1
  14. package/plugins/acryl-db/plugin.json +6 -1
  15. package/plugins/acryl-db/src/conformance.ts +224 -0
  16. package/plugins/acryl-db/upstream.json +1 -1
  17. package/plugins/acryl-db-sqlite/plugin.json +4 -1
  18. package/plugins/acryl-db-sqlite/upstream.json +1 -1
  19. package/plugins/acryl-door/LICENSE +47 -0
  20. package/plugins/acryl-door/plugin.json +14 -0
  21. package/plugins/acryl-door/src/cli/acryl.ts +207 -0
  22. package/plugins/acryl-door/src/host/api.ts +9 -0
  23. package/plugins/acryl-door/src/host/audit.ts +64 -0
  24. package/plugins/acryl-door/src/host/channel.ts +142 -0
  25. package/plugins/acryl-door/src/host/chat-tools.ts +39 -0
  26. package/plugins/acryl-door/src/host/door-file.ts +94 -0
  27. package/plugins/acryl-door/src/host/door.ts +195 -0
  28. package/plugins/acryl-door/src/host/http.ts +80 -0
  29. package/plugins/acryl-door/src/host/mcp-routes.ts +83 -0
  30. package/plugins/acryl-door/src/host/operator-routes.ts +38 -0
  31. package/plugins/acryl-door/src/host/page-routes.ts +65 -0
  32. package/plugins/acryl-door/src/host/shim.ts +65 -0
  33. package/plugins/acryl-door/src/host/verbs.ts +109 -0
  34. package/plugins/acryl-door/src/index.ts +133 -0
  35. package/plugins/acryl-door/src/page/api.ts +9 -0
  36. package/plugins/acryl-door/src/page/channel.ts +135 -0
  37. package/plugins/acryl-door/src/page/driver/accessibility.ts +163 -0
  38. package/plugins/acryl-door/src/page/driver/actions.ts +186 -0
  39. package/plugins/acryl-door/src/page/driver/driver.ts +153 -0
  40. package/plugins/acryl-door/src/page/driver/ref-table.ts +78 -0
  41. package/plugins/acryl-door/src/page/driver/sensitivity.ts +49 -0
  42. package/plugins/acryl-door/src/page/driver/snapshot.ts +132 -0
  43. package/plugins/acryl-door/src/page/index.ts +40 -0
  44. package/plugins/acryl-door/src/page/indicator.ts +72 -0
  45. package/plugins/acryl-door/src/page/main.ts +7 -0
  46. package/plugins/acryl-door/src/page/protocol.ts +213 -0
  47. package/plugins/acryl-door/src/page/snapshot-text.ts +18 -0
  48. package/plugins/acryl-door/src/page/user-input.ts +24 -0
  49. package/plugins/acryl-door/upstream.json +28 -0
  50. package/plugins/acryl-inspect/upstream.json +1 -1
  51. package/plugins/acryl-kv/plugin.json +6 -1
  52. package/plugins/acryl-kv/src/conformance.ts +226 -0
  53. package/plugins/acryl-kv/upstream.json +1 -1
  54. package/plugins/acryl-kv-db/plugin.json +4 -1
  55. package/plugins/acryl-kv-db/upstream.json +1 -1
  56. package/plugins/acryl-kv-json/plugin.json +4 -1
  57. package/plugins/acryl-kv-json/upstream.json +1 -1
  58. package/plugins/acryl-node-server/src/adapter/listener.ts +5 -0
  59. package/plugins/acryl-node-server/upstream.json +2 -2
  60. package/plugins/acryl-router/src/service.ts +3 -0
  61. package/plugins/acryl-router/upstream.json +2 -2
  62. package/plugins/acryl-router-http/upstream.json +1 -1
  63. package/plugins/acryl-state/upstream.json +1 -1
  64. package/plugins/agent-panel/client/main.ts +159 -0
  65. package/plugins/agent-panel/index.ts +232 -0
  66. package/plugins/agent-panel/plugin.json +21 -0
  67. package/plugins/desktop/upstream.json +1 -1
  68. package/plugins/desktop-web/upstream.json +1 -1
  69. package/plugins/extensions/resolve.ts +16 -2
  70. package/plugins/tsconfig.json +6 -0
  71. package/shared/DEFAULTS.md +11 -6
  72. package/shared/SKILL.md +1 -1
  73. package/templates/agent/blend.yaml +3 -0
  74. package/templates/agent/src/app.ts +18 -2
  75. package/templates/agent/src/routes.ts +14 -1
  76. package/templates/agent/template.json +2 -1
  77. package/templates/agent/test/agent.test.ts +10 -4
  78. package/templates/agent/test/app.test.ts +29 -8
  79. package/templates/agent/test/builder.test.ts +34 -23
  80. package/templates/agent/test/model.test.ts +24 -19
  81. package/templates/blank/src/app.ts +25 -5
  82. package/templates/blank/test/app.test.ts +28 -9
  83. package/templates/chat/AGENTS.md +16 -0
  84. package/templates/chat/blend.yaml +29 -0
  85. package/templates/chat/gitignore +6 -0
  86. package/templates/chat/migrations/001-chat.sql +12 -0
  87. package/templates/chat/package.json +33 -0
  88. package/templates/chat/src/app.ts +106 -0
  89. package/templates/chat/src/chat-app.ts +101 -0
  90. package/templates/chat/src/client/chat.tsx +115 -0
  91. package/templates/chat/src/client/tsconfig.json +26 -0
  92. package/templates/chat/src/db.ts +43 -0
  93. package/templates/chat/src/door.ts +7 -0
  94. package/templates/chat/src/llm.ts +42 -0
  95. package/templates/chat/src/main.ts +28 -0
  96. package/templates/chat/src/page.tsx +40 -0
  97. package/templates/chat/src/routes.ts +15 -0
  98. package/templates/chat/src/runtime.ts +17 -0
  99. package/templates/chat/template.json +11 -0
  100. package/templates/chat/test/chat.test.ts +198 -0
  101. package/templates/chat/test/server.test.ts +78 -0
  102. package/templates/chat/tsconfig.json +20 -0
  103. package/templates/nodejs/AGENTS.md +9 -0
  104. package/templates/nodejs/blend.yaml +26 -0
  105. package/templates/nodejs/gitignore +3 -0
  106. package/templates/nodejs/package.json +30 -0
  107. package/templates/nodejs/src/app.ts +92 -0
  108. package/templates/nodejs/src/client/counter.tsx +12 -0
  109. package/templates/nodejs/src/client/tsconfig.json +26 -0
  110. package/templates/nodejs/src/door.ts +7 -0
  111. package/templates/nodejs/src/main.ts +25 -0
  112. package/templates/nodejs/src/page.tsx +29 -0
  113. package/templates/nodejs/src/routes.ts +40 -0
  114. package/templates/nodejs/template.json +3 -0
  115. package/templates/nodejs/test/app.test.ts +78 -0
  116. package/templates/nodejs/test/server.test.ts +83 -0
  117. package/templates/nodejs/tsconfig.json +20 -0
  118. package/templates/notes/AGENTS.md +26 -0
  119. package/templates/notes/blend.yaml +29 -0
  120. package/templates/notes/gitignore +6 -0
  121. package/templates/notes/migrations/001-notes.sql +18 -0
  122. package/templates/notes/package.json +33 -0
  123. package/templates/notes/src/app.ts +106 -0
  124. package/templates/notes/src/auth.ts +57 -0
  125. package/templates/notes/src/client/counter.tsx +21 -0
  126. package/templates/notes/src/client/tsconfig.json +26 -0
  127. package/templates/notes/src/db.ts +50 -0
  128. package/templates/notes/src/door.ts +7 -0
  129. package/templates/notes/src/main.ts +28 -0
  130. package/templates/notes/src/notes-app.ts +205 -0
  131. package/templates/notes/src/routes.ts +14 -0
  132. package/templates/notes/src/runtime.ts +17 -0
  133. package/templates/notes/src/schema.ts +38 -0
  134. package/templates/notes/src/views.tsx +136 -0
  135. package/templates/notes/template.json +11 -0
  136. package/templates/notes/test/notes.test.ts +280 -0
  137. package/templates/notes/test/server.test.ts +78 -0
  138. package/templates/notes/tsconfig.json +20 -0
  139. package/templates/todo/src/app.ts +27 -7
  140. package/templates/todo/test/todo.test.ts +47 -22
  141. package/templates/todo/upstream.json +1 -1
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
  /**
@@ -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";
@@ -116,7 +116,7 @@ function copyPlugin(plugin, appDir, root = libraryRoot()) {
116
116
  recursive: true,
117
117
  filter: (path) => {
118
118
  const inside = path.slice(source.length + 1);
119
- 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;
120
120
  }
121
121
  });
122
122
  if (!folder.legacy) recordLibraryCopy(folder.dir, plugin.name);
@@ -124,6 +124,10 @@ function copyPlugin(plugin, appDir, root = libraryRoot()) {
124
124
  }
125
125
  const relativeName = (appDir, path) => path.slice(appDir.length + 1).split(sep).join("/");
126
126
  //#endregion
127
+ //#region src/version.ts
128
+ /** The version of this package. */
129
+ const VERSION = createRequire(import.meta.url)("../package.json").version;
130
+ //#endregion
127
131
  //#region src/project.ts
128
132
  /** Thrown when the target directory exists and is not empty. Nothing has been written. */
129
133
  var TargetNotEmptyError = class extends Error {
@@ -148,7 +152,7 @@ function assertTargetFree(target) {
148
152
  if (!statSync(target).isDirectory() || readdirSync(target).length > 0) throw new TargetNotEmptyError(target);
149
153
  }
150
154
  /**
151
- * 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.
152
156
  *
153
157
  * @returns the copied files, relative to `target`
154
158
  */
@@ -168,7 +172,7 @@ function copyTemplate(source, target, name) {
168
172
  continue;
169
173
  }
170
174
  const text = readFileSync(sourcePath, "utf8");
171
- 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}`));
172
176
  else cpSync(sourcePath, targetPath);
173
177
  written.push(`${prefix}${targetName}`);
174
178
  }
@@ -214,6 +218,19 @@ function mergeDependencies(appDir, dependencies) {
214
218
  writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
215
219
  return added;
216
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
+ }
217
234
  /** Declare a file as an opener in package.json (`"acryl": { "opener": [...] }`): the one place that may import a database provider. */
218
235
  function mergeOpener(appDir, opener) {
219
236
  const file = join(appDir, "package.json");
@@ -306,8 +323,11 @@ function copyFrameworkDocs(source, target) {
306
323
  }
307
324
  //#endregion
308
325
  //#region src/templates.ts
309
- /** 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. */
310
327
  const TEMPLATES = [
328
+ "nodejs",
329
+ "notes",
330
+ "chat",
311
331
  "blank",
312
332
  "todo",
313
333
  "agent"
@@ -427,6 +447,7 @@ async function init(options, io) {
427
447
  const plugins = closure([...readTemplatePlugins(templateDir(template)), ...options.desktop ? ["desktop"] : []], readLibrary());
428
448
  for (const plugin of plugins) {
429
449
  copyPlugin(plugin, directory);
450
+ if (plugin.imports) mergeImports(directory, plugin.imports, "plugins");
430
451
  mergeDependencies(directory, plugin.dependencies);
431
452
  mergeTooling(directory, plugin);
432
453
  }
@@ -447,7 +468,7 @@ async function init(options, io) {
447
468
  }
448
469
  //#endregion
449
470
  //#region src/add.ts
450
- /** Parse `add <plugin...> [--dir <app>]`. */
471
+ /** Parse `add <plugin...> [--dir <app>] [--suite]`. */
451
472
  function parseAddOptions(argv) {
452
473
  let parsed;
453
474
  try {
@@ -458,14 +479,18 @@ function parseAddOptions(argv) {
458
479
  if (parsed.positionals.length === 0) throw new UsageError("add needs at least one plugin name (see \"acrylblends plugins\")");
459
480
  return {
460
481
  names: parsed.positionals,
461
- directory: parsed.values.dir
482
+ directory: parsed.values.dir,
483
+ suite: parsed.values.suite === true
462
484
  };
463
485
  }
464
486
  const parse$1 = (argv) => parseArgs({
465
487
  args: [...argv],
466
488
  allowPositionals: true,
467
489
  strict: true,
468
- options: { dir: { type: "string" } }
490
+ options: {
491
+ dir: { type: "string" },
492
+ suite: { type: "boolean" }
493
+ }
469
494
  });
470
495
  const present = (appDir, name) => existsSync(join(pluginsFolder(appDir).dir, name));
471
496
  /**
@@ -495,7 +520,7 @@ async function add(options, io) {
495
520
  }
496
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);
497
522
  if (names.length === 0) return 0;
498
- const already = names.filter((name) => present(appDir, name));
523
+ const already = options.suite ? [] : names.filter((name) => present(appDir, name));
499
524
  if (already.length > 0) {
500
525
  io.stderr.write(`error: the app already has ${already.map((name) => `${relative(appDir, join(pluginsFolder(appDir).dir, name)).split(sep).join("/")}`).join(", ")}\n`);
501
526
  return 1;
@@ -504,6 +529,7 @@ async function add(options, io) {
504
529
  const addedDependencies = [];
505
530
  for (const plugin of toCopy) {
506
531
  copyPlugin(plugin, appDir);
532
+ if (plugin.imports) mergeImports(appDir, plugin.imports, relative(appDir, pluginsFolder(appDir).dir).split(sep).join("/"));
507
533
  addedDependencies.push(...mergeDependencies(appDir, plugin.dependencies));
508
534
  const tooling = mergeTooling(appDir, plugin);
509
535
  appendGitignore(appDir, plugin.gitignore);
@@ -511,11 +537,45 @@ async function add(options, io) {
511
537
  addedDependencies.push(...tooling.devDependencies);
512
538
  if (tooling.scripts.length > 0) io.stdout.write(`new scripts: ${tooling.scripts.map((name) => `npm run ${name}`).join(", ")}\n`);
513
539
  }
540
+ if (options.suite) {
541
+ const refused = copySuites(names, appDir, addedDependencies, io);
542
+ if (refused !== 0) return refused;
543
+ }
514
544
  io.stdout.write(`added ${toCopy.map((plugin) => plugin.name).join(", ")} to ${appDir}\n`);
515
545
  if (addedDependencies.length > 0) io.stdout.write(`new dependencies: ${addedDependencies.join(", ")} (run npm install)\n`);
516
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`);
517
547
  return 0;
518
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);
577
+ return 0;
578
+ }
519
579
  const PLUGIN_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
520
580
  /**
521
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
@@ -911,14 +971,13 @@ async function kvImport(options, io) {
911
971
  }
912
972
  //#endregion
913
973
  //#region src/cli.ts
914
- /** The version of this package. */
915
- const VERSION = createRequire(import.meta.url)("../package.json").version;
916
974
  const HELP = `acrylblends ${VERSION} - scaffold Cordis apps
917
975
 
918
976
  Usage:
919
977
  acrylblends init [dir] [options] Create a Cordis app in an empty or new directory
920
- acrylblends add <plugin...> [--dir d] Copy library plugins (and what they require) into an app;
921
- a folder path brings an external plugin into its plugins/
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)
922
981
  acrylblends plugins List the plugin library
923
982
  acrylblends build-client [root] [out] Compile the client modules once for production (src/client to dist/client)
924
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]