acrylblends 0.7.6 → 0.7.8

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 (105) hide show
  1. package/dist/index.d.ts +1 -1
  2. package/dist/index.js +148 -24
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/README.md +1 -1
  5. package/docs/framework/UPSTREAM.md +1 -1
  6. package/docs/framework/agents/index.md +18 -3
  7. package/docs/framework/getting-started/desktop.md +2 -2
  8. package/docs/framework/guides/client-modules.md +4 -3
  9. package/docs/framework/guides/data.md +72 -8
  10. package/docs/framework/helpers/streaming.md +12 -27
  11. package/docs/framework/upstream.json +2 -2
  12. package/lib/browser/read-sse.js +84 -0
  13. package/lib/index-BLTYsyc1.d.ts +93 -0
  14. package/lib/kv/db.d.ts +12 -0
  15. package/lib/kv/db.js +57 -0
  16. package/lib/kv/json.d.ts +11 -0
  17. package/lib/kv/json.js +224 -0
  18. package/lib/kv.d.ts +2 -0
  19. package/lib/kv.js +2 -0
  20. package/lib/node-server.d.ts +3 -3
  21. package/lib/node-server.js +13 -8
  22. package/lib/read-sse.d.ts +33 -0
  23. package/lib/read-sse.js +84 -0
  24. package/lib/src-DRFVEQQb.js +90 -0
  25. package/package.json +17 -1
  26. package/plugins/acryl-db/plugin.json +1 -1
  27. package/plugins/acryl-db/upstream.json +2 -2
  28. package/plugins/acryl-db-sqlite/upstream.json +1 -1
  29. package/plugins/acryl-inspect/upstream.json +1 -1
  30. package/plugins/acryl-kv/LICENSE +47 -0
  31. package/plugins/acryl-kv/plugin.json +12 -0
  32. package/plugins/acryl-kv/src/check.ts +58 -0
  33. package/plugins/acryl-kv/src/copy.ts +39 -0
  34. package/plugins/acryl-kv/src/cordis.ts +13 -0
  35. package/plugins/acryl-kv/src/index.ts +14 -0
  36. package/plugins/acryl-kv/src/kv.ts +37 -0
  37. package/plugins/acryl-kv/upstream.json +18 -0
  38. package/plugins/acryl-kv-db/LICENSE +47 -0
  39. package/plugins/acryl-kv-db/plugin.json +13 -0
  40. package/plugins/acryl-kv-db/src/index.ts +37 -0
  41. package/plugins/acryl-kv-db/src/open.ts +80 -0
  42. package/plugins/acryl-kv-db/upstream.json +21 -0
  43. package/plugins/acryl-kv-json/LICENSE +47 -0
  44. package/plugins/acryl-kv-json/plugin.json +13 -0
  45. package/plugins/acryl-kv-json/src/index.ts +71 -0
  46. package/plugins/acryl-kv-json/src/open.ts +249 -0
  47. package/plugins/acryl-kv-json/upstream.json +22 -0
  48. package/plugins/acryl-node-server/src/client-modules.ts +10 -9
  49. package/plugins/acryl-node-server/src/index.ts +12 -1
  50. package/plugins/acryl-node-server/upstream.json +2 -2
  51. package/plugins/acryl-router/src/browser/read-sse.ts +3 -0
  52. package/plugins/acryl-router/src/helpers/read-sse.ts +111 -0
  53. package/plugins/acryl-router/upstream.json +2 -2
  54. package/plugins/acryl-router-http/upstream.json +1 -1
  55. package/plugins/acryl-state/upstream.json +1 -1
  56. package/plugins/builder/index.ts +2 -2
  57. package/plugins/builder/plugin.json +1 -1
  58. package/plugins/desktop/main.ts +3 -3
  59. package/plugins/desktop/plugin.json +2 -2
  60. package/plugins/desktop/upstream.json +1 -1
  61. package/plugins/desktop-web/main.ts +2 -2
  62. package/plugins/desktop-web/plugin.json +2 -2
  63. package/plugins/desktop-web/upstream.json +1 -1
  64. package/plugins/extensions/index.ts +19 -1
  65. package/plugins/extensions/plugin.json +1 -1
  66. package/plugins/extensions/resolve.ts +1 -1
  67. package/plugins/manifest/lock.ts +7 -3
  68. package/plugins/manifest/upgrade-cli.ts +1 -1
  69. package/plugins/save/cli.ts +1 -1
  70. package/plugins/save/plugin.json +1 -1
  71. package/plugins/tsconfig.json +2 -0
  72. package/shared/AGENTS.core.md +10 -10
  73. package/shared/DEFAULTS.md +40 -0
  74. package/shared/SKILL.md +14 -0
  75. package/templates/agent/AGENTS.md +1 -1
  76. package/templates/agent/README.md +6 -6
  77. package/templates/agent/gitignore +1 -0
  78. package/templates/agent/package.json +8 -8
  79. package/templates/agent/src/app.ts +28 -15
  80. package/templates/agent/src/main.ts +2 -2
  81. package/templates/agent/src/routes.ts +5 -5
  82. package/templates/agent/test/agent.test.ts +1 -1
  83. package/templates/agent/test/app.test.ts +1 -1
  84. package/templates/agent/test/builder.test.ts +1 -1
  85. package/templates/agent/test/model.test.ts +1 -1
  86. package/templates/agent/tsconfig.json +48 -50
  87. package/templates/blank/AGENTS.md +1 -1
  88. package/templates/blank/README.md +2 -2
  89. package/templates/blank/gitignore +1 -0
  90. package/templates/blank/package.json +8 -8
  91. package/templates/blank/src/about.tsx +2 -2
  92. package/templates/blank/src/app.ts +4 -4
  93. package/templates/blank/src/main.ts +1 -1
  94. package/templates/blank/src/routes.ts +1 -1
  95. package/templates/blank/test/app.test.ts +1 -1
  96. package/templates/blank/tsconfig.json +1 -1
  97. package/templates/todo/README.md +1 -1
  98. package/templates/todo/gitignore +1 -0
  99. package/templates/todo/package.json +8 -8
  100. package/templates/todo/src/app.ts +5 -5
  101. package/templates/todo/src/main.ts +1 -1
  102. package/templates/todo/src/todo.ts +4 -4
  103. package/templates/todo/test/todo.test.ts +1 -1
  104. package/templates/todo/tsconfig.json +1 -1
  105. package/templates/todo/upstream.json +2 -2
package/dist/index.d.ts CHANGED
@@ -102,7 +102,7 @@ export declare function libraryRoot(): string;
102
102
  export declare function readLibrary(root?: string): Map<string, LibraryPlugin>;
103
103
  /** The plugins to copy for `names`: each one after everything it requires, no duplicates. */
104
104
  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. */
105
+ /** Copy one library plugin's source into `<app>/plugins/<name>/`. Throws when that folder already exists. */
106
106
  export declare function copyPlugin(plugin: LibraryPlugin, appDir: string, root?: string): string;
107
107
  //#endregion
108
108
  //#region src/project.d.ts
package/dist/index.js CHANGED
@@ -84,10 +84,33 @@ 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,
@@ -96,8 +119,10 @@ function copyPlugin(plugin, appDir, root = libraryRoot()) {
96
119
  return !(dirname(path) === source && LIBRARY_ONLY.has(inside)) && !isOmitted(inside, plugin.omit);
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("/");
101
126
  //#endregion
102
127
  //#region src/project.ts
103
128
  /** Thrown when the target directory exists and is not empty. Nothing has been written. */
@@ -159,7 +184,7 @@ function readTemplateDesktopPath(templateDir) {
159
184
  if (!existsSync(file)) return void 0;
160
185
  return JSON.parse(readFileSync(file, "utf8")).desktopPath;
161
186
  }
162
- /** Record where the desktop window opens in package.json (`acrylblends.desktopPath`), read by `src/plugins/desktop/main.ts`. */
187
+ /** Record where the desktop window opens in package.json (`acrylblends.desktopPath`), read by `plugins/desktop/main.ts`. */
163
188
  function writeDesktopPath(appDir, path) {
164
189
  const file = join(appDir, "package.json");
165
190
  const manifest = JSON.parse(readFileSync(file, "utf8"));
@@ -241,9 +266,17 @@ function appendGitignore(appDir, lines) {
241
266
  function writeAgentFiles(sharedDir, templateDir, target, name) {
242
267
  const core = readFileSync(join(sharedDir, "AGENTS.core.md"), "utf8");
243
268
  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"];
269
+ const text = `${core}${readFileSync(join(sharedDir, "DEFAULTS.md"), "utf8")}${own}`.replaceAll("__APP_NAME__", name);
270
+ writeFileSync(join(target, "AGENTS.md"), text);
271
+ writeFileSync(join(target, "CLAUDE.md"), text);
272
+ const skill = readFileSync(join(sharedDir, "SKILL.md"), "utf8").replaceAll("__APP_NAME__", name);
273
+ const written = ["AGENTS.md", "CLAUDE.md"];
274
+ for (const folder of [".claude/skills/app-guide", ".agents/skills/app-guide"]) {
275
+ mkdirSync(join(target, folder), { recursive: true });
276
+ writeFileSync(join(target, folder, "SKILL.md"), skill);
277
+ written.push(`${folder}/SKILL.md`);
278
+ }
279
+ return written;
247
280
  }
248
281
  /**
249
282
  * Copy the framework's docs (a generated copy of ACRYL's `docs/framework`, already checked against its digest) into `<app>/docs/framework/`,
@@ -328,7 +361,7 @@ var UsageError = class extends Error {
328
361
  function parseInitOptions(argv) {
329
362
  let parsed;
330
363
  try {
331
- parsed = parse$1(argv);
364
+ parsed = parse$2(argv);
332
365
  } catch (error) {
333
366
  throw new UsageError(error.message);
334
367
  }
@@ -347,7 +380,7 @@ function parseInitOptions(argv) {
347
380
  yes: values.yes === true
348
381
  };
349
382
  }
350
- const parse$1 = (argv) => parseArgs({
383
+ const parse$2 = (argv) => parseArgs({
351
384
  args: [...argv],
352
385
  allowPositionals: true,
353
386
  strict: true,
@@ -418,7 +451,7 @@ async function init(options, io) {
418
451
  function parseAddOptions(argv) {
419
452
  let parsed;
420
453
  try {
421
- parsed = parse(argv);
454
+ parsed = parse$1(argv);
422
455
  } catch (error) {
423
456
  throw new UsageError(error.message);
424
457
  }
@@ -428,13 +461,13 @@ function parseAddOptions(argv) {
428
461
  directory: parsed.values.dir
429
462
  };
430
463
  }
431
- const parse = (argv) => parseArgs({
464
+ const parse$1 = (argv) => parseArgs({
432
465
  args: [...argv],
433
466
  allowPositionals: true,
434
467
  strict: true,
435
468
  options: { dir: { type: "string" } }
436
469
  });
437
- const present = (appDir, name) => existsSync(join(appDir, "src", "plugins", name));
470
+ const present = (appDir, name) => existsSync(join(pluginsFolder(appDir).dir, name));
438
471
  /**
439
472
  * Copy library plugins into an existing app, with the plugins they require, and add their npm packages to its package.json.
440
473
  * Refuses a plugin the app already has. Returns the exit code.
@@ -445,21 +478,26 @@ async function add(options, io) {
445
478
  io.stderr.write(`error: ${appDir} is not an app (no package.json and src/); run "acrylblends init" first\n`);
446
479
  return 1;
447
480
  }
481
+ if (pluginsFolder(appDir).legacy) io.stderr.write(`${LEGACY_WARNING}\n`);
448
482
  const external = options.names.filter((name) => /[\\/]/.test(name) || name.startsWith("."));
449
483
  for (const folder of external) {
450
484
  const code = addExternal(resolve(io.cwd, folder), appDir, io);
451
485
  if (code !== 0) return code;
452
486
  }
453
- const plain = !existsSync(join(appDir, "src", "plugins", "instance"));
487
+ const plain = !existsSync(join(pluginsFolder(appDir).dir, "instance"));
454
488
  if (plain && options.names.includes("db")) {
455
489
  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
490
  return 1;
457
491
  }
458
- const names = options.names.filter((name) => !external.includes(name)).map((name) => name === "db" ? "acryl-db-sqlite" : name).map((name) => name === "desktop" && plain ? "desktop-web" : name);
492
+ if (plain && options.names.includes("kv")) {
493
+ io.stderr.write("error: \"kv\" is the Cordis plugin for a Blend app. A plain Acryl app opens its store with openKv from \"acrylblends/kv/json\" (see docs/framework/guides/data.md#a-tiny-app-kv)\n");
494
+ return 1;
495
+ }
496
+ 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);
459
497
  if (names.length === 0) return 0;
460
498
  const already = names.filter((name) => present(appDir, name));
461
499
  if (already.length > 0) {
462
- io.stderr.write(`error: the app already has ${already.map((name) => `src/plugins/${name}`).join(", ")}\n`);
500
+ io.stderr.write(`error: the app already has ${already.map((name) => `${relative(appDir, join(pluginsFolder(appDir).dir, name)).split(sep).join("/")}`).join(", ")}\n`);
463
501
  return 1;
464
502
  }
465
503
  const toCopy = closure(names, readLibrary()).filter((plugin) => !present(appDir, plugin.name));
@@ -475,13 +513,13 @@ async function add(options, io) {
475
513
  }
476
514
  io.stdout.write(`added ${toCopy.map((plugin) => plugin.name).join(", ")} to ${appDir}\n`);
477
515
  if (addedDependencies.length > 0) io.stdout.write(`new dependencies: ${addedDependencies.join(", ")} (run npm install)\n`);
478
- 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`);
516
+ 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`);
479
517
  return 0;
480
518
  }
481
519
  const PLUGIN_NAME = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
482
520
  /**
483
521
  * Bring an external plugin folder (a package made with the builder's package_plugin, or any folder with an index.ts that exports a
484
- * Cordis plugin) into the app's `extensions/`, where the extensions plugin installs it like any other: checked, staged, started,
522
+ * Cordis plugin) into the app's `plugins/`, where the extensions plugin installs it like any other: checked, staged, started,
485
523
  * verified. Its runtime dependencies are not installed here; they are reported so the user adds them deliberately.
486
524
  */
487
525
  function addExternal(folder, appDir, io) {
@@ -504,16 +542,16 @@ function addExternal(folder, appDir, io) {
504
542
  io.stderr.write(`error: "${name}" is not a plugin name (lowercase letters, digits and dashes)\n`);
505
543
  return 1;
506
544
  }
507
- const target = join(appDir, "extensions", name);
545
+ const target = join(pluginsFolder(appDir).legacy ? join(appDir, "extensions") : join(appDir, PLUGINS_DIR), name);
508
546
  if (existsSync(target)) {
509
- io.stderr.write(`error: the app already has extensions/${name}\n`);
547
+ io.stderr.write(`error: the app already has ${relative(appDir, target).split(sep).join("/")}\n`);
510
548
  return 1;
511
549
  }
512
550
  cpSync(folder, target, {
513
551
  recursive: true,
514
552
  filter: (path) => !/[\\/](node_modules|\.git)([\\/]|$)/.test(path.slice(folder.length))
515
553
  });
516
- io.stdout.write(`added external plugin ${name} to ${join(appDir, "extensions")}; it installs at the next start\n`);
554
+ io.stdout.write(`added external plugin ${name} to ${dirname(target)}; it installs at the next start\n`);
517
555
  const dependencies = Object.keys(manifest.dependencies ?? {});
518
556
  if (dependencies.length > 0) io.stdout.write(`it uses ${dependencies.join(", ")}: add them to the app with npm install first\n`);
519
557
  return 0;
@@ -592,9 +630,14 @@ const DRIVERS = [
592
630
  const OPENERS = ["acrylblends/db/sqlite", "acryl-db-sqlite"];
593
631
  /** The desktop shell is where Electron lives (`acrylblends add desktop` copies it). */
594
632
  const ELECTRON = "electron";
595
- const ELECTRON_HOMES = ["src/plugins/desktop/", "src/plugins/desktop-web/"];
633
+ const ELECTRON_HOMES = [
634
+ "plugins/desktop/",
635
+ "plugins/desktop-web/",
636
+ "src/plugins/desktop/",
637
+ "src/plugins/desktop-web/"
638
+ ];
596
639
  /** The provider plugins themselves may import what they wrap. */
597
- const PROVIDER_HOMES = ["src/plugins/acryl-db-sqlite/"];
640
+ const PROVIDER_HOMES = ["plugins/acryl-db-sqlite/", "src/plugins/acryl-db-sqlite/"];
598
641
  const SOURCE = /\.(?:[cm]?[jt]s|[jt]sx)$/u;
599
642
  const SKIP_DIRS = /* @__PURE__ */ new Set([
600
643
  "node_modules",
@@ -607,6 +650,7 @@ const ROOTS = [
607
650
  "src",
608
651
  "test",
609
652
  "tests",
653
+ "plugins",
610
654
  "extensions"
611
655
  ];
612
656
  const matches = (specifier, name) => specifier === name || specifier.startsWith(`${name}/`);
@@ -705,7 +749,7 @@ function findViolations(appDir) {
705
749
  }
706
750
  const GUIDE = "docs/framework/guides/data.md#why-the-database-is-opened-in-one-place";
707
751
  function advice(violation, openers) {
708
- if (violation.kind === "electron") return `"${violation.specifier}" is the desktop shell's: only src/plugins/desktop (added with "acrylblends add desktop") imports electron`;
752
+ if (violation.kind === "electron") return `"${violation.specifier}" is the desktop shell's: only plugins/desktop (added with "acrylblends add desktop") imports electron`;
709
753
  const where = openers.length > 0 ? openers.join(", ") : "a file named in package.json \"acryl\": { \"opener\": \"...\" }";
710
754
  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`;
711
755
  }
@@ -791,6 +835,81 @@ function processIo() {
791
835
  };
792
836
  }
793
837
  //#endregion
838
+ //#region src/kv-import.ts
839
+ /** Parse `kv import <kv.json> <database.sqlite> [--overwrite] [--table name] [--migrations dir]`. */
840
+ function parseKvOptions(argv) {
841
+ let parsed;
842
+ try {
843
+ parsed = parse(argv);
844
+ } catch (error) {
845
+ throw new UsageError(error.message);
846
+ }
847
+ const [action, from, to, ...rest] = parsed.positionals;
848
+ if (action !== "import") throw new UsageError("kv takes one subcommand: kv import <kv.json> <database.sqlite>");
849
+ if (from === void 0 || to === void 0 || rest.length > 0) throw new UsageError("kv import takes two arguments: the JSON file and the SQLite file");
850
+ return {
851
+ from,
852
+ to,
853
+ overwrite: parsed.values.overwrite ?? false,
854
+ table: parsed.values.table,
855
+ migrations: parsed.values.migrations
856
+ };
857
+ }
858
+ const parse = (argv) => parseArgs({
859
+ args: [...argv],
860
+ allowPositionals: true,
861
+ strict: true,
862
+ options: {
863
+ overwrite: { type: "boolean" },
864
+ table: { type: "string" },
865
+ migrations: { type: "string" }
866
+ }
867
+ });
868
+ /** The framework as the app has it installed (so the code that reads the file is the code the app runs), else this package's own. */
869
+ async function framework(io, subpath, own) {
870
+ const here = createRequire(resolve(io.cwd, "package.json"));
871
+ try {
872
+ return await import(here.resolve(`acrylblends/${subpath}`));
873
+ } catch {
874
+ return await import(new URL(`../lib/${own}`, import.meta.url).href);
875
+ }
876
+ }
877
+ /**
878
+ * Import a JSON-file kv store into the `kv` table of an app's SQLite database: the move from `acryl-kv-json` to SQL. The file is not changed.
879
+ * The database is opened with the app's `migrations/` folder when it has one (a database whose schema is newer than the files given is refused),
880
+ * and the table is created when missing. Refuses to overwrite a key the table already has unless `--overwrite`.
881
+ */
882
+ async function kvImport(options, io) {
883
+ const from = resolve(io.cwd, options.from);
884
+ const to = resolve(io.cwd, options.to);
885
+ if (!existsSync(from)) {
886
+ io.stderr.write(`error: ${from} does not exist\n`);
887
+ return 1;
888
+ }
889
+ const kvCore = await framework(io, "kv", "kv.js");
890
+ const json = await framework(io, "kv/json", "kv/json.js");
891
+ const onDb = await framework(io, "kv/db", "kv/db.js");
892
+ const sqlite = await framework(io, "db/sqlite", "db/sqlite.js");
893
+ const migrations = options.migrations !== void 0 ? resolve(io.cwd, options.migrations) : existsSync(resolve(io.cwd, "migrations")) ? resolve(io.cwd, "migrations") : void 0;
894
+ const source = await json.openKv({ path: from });
895
+ try {
896
+ const database = await sqlite.openDb({
897
+ path: to,
898
+ ...migrations === void 0 ? {} : { migrations }
899
+ });
900
+ try {
901
+ const target = onDb.kvOnDb(database.db, options.table === void 0 ? {} : { table: options.table });
902
+ const result = await kvCore.copyKv(source.kv, target, { overwrite: options.overwrite });
903
+ io.stdout.write(`imported ${result.copied} ${result.copied === 1 ? "key" : "keys"} from ${options.from} into ${options.to}${options.table === void 0 ? "" : ` (table ${options.table})`}${result.overwritten > 0 ? `, ${result.overwritten} overwritten` : ""}\n`);
904
+ return 0;
905
+ } finally {
906
+ await database.close();
907
+ }
908
+ } finally {
909
+ await source.close();
910
+ }
911
+ }
912
+ //#endregion
794
913
  //#region src/cli.ts
795
914
  /** The version of this package. */
796
915
  const VERSION = createRequire(import.meta.url)("../package.json").version;
@@ -799,9 +918,10 @@ const HELP = `acrylblends ${VERSION} - scaffold Cordis apps
799
918
  Usage:
800
919
  acrylblends init [dir] [options] Create a Cordis app in an empty or new directory
801
920
  acrylblends add <plugin...> [--dir d] Copy library plugins (and what they require) into an app;
802
- a folder path brings an external plugin into its extensions/
921
+ a folder path brings an external plugin into its plugins/
803
922
  acrylblends plugins List the plugin library
804
923
  acrylblends build-client [root] [out] Compile the client modules once for production (src/client to dist/client)
924
+ 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]
805
925
  acrylblends check-deps [dir] Fail when app code imports a database driver or opener outside the file named in
806
926
  package.json "acryl": { "opener": "src/db.ts" } (a port is imported, a provider is not)
807
927
 
@@ -833,6 +953,7 @@ async function runCli(argv, io = processIo()) {
833
953
  if (command === "init") return await init(parseInitOptions(args), io);
834
954
  if (command === "add") return await add(parseAddOptions(args), io);
835
955
  if (command === "build-client") return await buildClient(parseBuildClientOptions(args), io);
956
+ if (command === "kv") return await kvImport(parseKv(args), io);
836
957
  if (command === "check-deps") return checkDeps(parseCheckDeps(args), io);
837
958
  if (command === "plugins") return listPlugins(io);
838
959
  throw new UsageError(`unknown command "${command}"`);
@@ -852,6 +973,9 @@ function parseCheckDeps(args) {
852
973
  throw new UsageError(error.message);
853
974
  }
854
975
  }
976
+ function parseKv(args) {
977
+ return parseKvOptions(args);
978
+ }
855
979
  //#endregion
856
980
  export { HELP, TEMPLATES, TargetNotEmptyError, UsageError, VERSION, add, assertTargetFree, closure, copyPlugin, copyTemplate, init, isOmitted, isTemplate, isValidName, libraryRoot, listPlugins, mergeDependencies, nameFrom, parseAddOptions, parseInitOptions, readLibrary, readTemplatePlugins, runCli, templateDir, templatesRoot };
857
981