acrylblends 0.11.0 → 1.0.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 (93) hide show
  1. package/README.md +131 -53
  2. package/dist/index.d.ts +29 -2
  3. package/dist/index.js +450 -69
  4. package/dist/index.js.map +1 -1
  5. package/docs/framework/README.md +5 -3
  6. package/docs/framework/agents/index.md +1 -1
  7. package/docs/framework/api/acryl.md +5 -17
  8. package/docs/framework/api/context.md +12 -61
  9. package/docs/framework/api/presets.md +1 -1
  10. package/docs/framework/api/request.md +3 -3
  11. package/docs/framework/api/summary.md +1 -1
  12. package/docs/framework/concepts/web-standard.md +6 -16
  13. package/docs/framework/cordis/acryl-in-a-cordis-app.md +13 -8
  14. package/docs/framework/cordis/migration-from-0-4.md +1 -1
  15. package/docs/framework/cordis/migration-to-1-0.md +45 -0
  16. package/docs/framework/cordis/node-server.md +2 -2
  17. package/docs/framework/getting-started/basic.md +15 -10
  18. package/docs/framework/getting-started/desktop.md +1 -1
  19. package/docs/framework/getting-started/other-runtimes.md +10 -14
  20. package/docs/framework/guides/create-acrylblends.md +4 -2
  21. package/docs/framework/guides/data.md +1 -1
  22. package/docs/framework/guides/faq.md +1 -1
  23. package/docs/framework/guides/jsx-dom.md +1 -1
  24. package/docs/framework/guides/production.md +43 -0
  25. package/docs/framework/guides/rpc.md +3 -11
  26. package/docs/framework/helpers/adapter.md +13 -43
  27. package/docs/framework/helpers/conninfo.md +1 -36
  28. package/docs/framework/helpers/streaming.md +1 -1
  29. package/docs/framework/helpers/websocket.md +4 -11
  30. package/docs/framework/middleware/builtin/basic-auth.md +1 -1
  31. package/docs/framework/middleware/builtin/cache.md +2 -22
  32. package/docs/framework/middleware/builtin/compress.md +1 -1
  33. package/docs/framework/middleware/builtin/context-storage.md +10 -14
  34. package/docs/framework/middleware/builtin/cors.md +1 -2
  35. package/docs/framework/middleware/builtin/ip-restriction.md +1 -1
  36. package/docs/framework/middleware/builtin/jsx-renderer.md +1 -1
  37. package/docs/framework/middleware/builtin/logger.md +1 -1
  38. package/docs/framework/middleware/builtin/loopback-only.md +1 -1
  39. package/docs/framework/middleware/builtin/mount.md +1 -1
  40. package/docs/framework/middleware/builtin/request-id.md +13 -20
  41. package/docs/framework/middleware/builtin/timing.md +1 -2
  42. package/lib/cordis/door.d.ts +5 -0
  43. package/lib/cordis/door.js +8 -4
  44. package/lib/cordis/inspect.js +1 -1
  45. package/lib/cordis/router-http.js +1 -1
  46. package/lib/cordis/router.d.ts +1 -2
  47. package/lib/cordis/router.js +2 -3
  48. package/lib/cordis/state.js +1 -1
  49. package/lib/door/host.js +1 -1
  50. package/lib/drivers.d.ts +66 -0
  51. package/lib/drivers.js +16 -0
  52. package/lib/index.d.ts +1 -2
  53. package/lib/index.js +1 -2
  54. package/lib/{snapshot-text-yaWlezbd.js → snapshot-text-DmE0YfeZ.js} +2 -2
  55. package/lib/{src-DNzw0Qsb.js → src-2Z4mJg0x.js} +0 -1
  56. package/package.json +5 -121
  57. package/plugins/acryl-door/src/index.ts +8 -0
  58. package/plugins/acryl-router/src/index.ts +0 -1
  59. package/plugins/agent-panel/drivers.ts +1 -16
  60. package/plugins/agent-panel/index.ts +1 -1
  61. package/plugins/agent-panel/registry.ts +16 -0
  62. package/plugins/driver-acp/README.md +53 -0
  63. package/plugins/driver-acp/chat.ts +258 -0
  64. package/plugins/driver-acp/connection.ts +239 -0
  65. package/plugins/driver-acp/index.ts +65 -0
  66. package/plugins/driver-acp/plugin.json +12 -0
  67. package/plugins/driver-acp/provenance.json +20 -0
  68. package/plugins/driver-pty/index.ts +1 -0
  69. package/shared/library-history.json +464 -0
  70. package/docs/framework/getting-started/aws-lambda.md +0 -115
  71. package/docs/framework/getting-started/cloudflare-pages.md +0 -53
  72. package/docs/framework/getting-started/cloudflare-workers.md +0 -67
  73. package/docs/framework/getting-started/lambda-edge.md +0 -41
  74. package/docs/framework/getting-started/netlify.md +0 -25
  75. package/docs/framework/getting-started/service-worker.md +0 -33
  76. package/docs/framework/getting-started/vercel.md +0 -28
  77. package/lib/aws-lambda.d.ts +0 -324
  78. package/lib/aws-lambda.js +0 -357
  79. package/lib/cloudflare-pages.d.ts +0 -58
  80. package/lib/cloudflare-pages.js +0 -78
  81. package/lib/cloudflare-workers.d.ts +0 -21
  82. package/lib/cloudflare-workers.js +0 -80
  83. package/lib/deprecated-BP4fPt38.js +0 -10
  84. package/lib/deprecated-CTy4N7ng.d.ts +0 -12
  85. package/lib/lambda-edge.d.ts +0 -92
  86. package/lib/lambda-edge.js +0 -105
  87. package/lib/netlify.d.ts +0 -29
  88. package/lib/netlify.js +0 -30
  89. package/lib/service-worker.d.ts +0 -48
  90. package/lib/service-worker.js +0 -38
  91. package/lib/vercel.d.ts +0 -10
  92. package/lib/vercel.js +0 -9
  93. package/plugins/acryl-router/src/deprecated.ts +0 -15
package/dist/index.js CHANGED
@@ -1,10 +1,143 @@
1
1
  import { createRequire } from "node:module";
2
- import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync } from "node:fs";
2
+ import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { basename, dirname, join, posix, relative, resolve, sep } from "node:path";
4
4
  import { parseArgs } from "node:util";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { createHash } from "node:crypto";
6
7
  import { spawn } from "node:child_process";
7
8
  import { createInterface } from "node:readline/promises";
9
+ //#region src/templates.ts
10
+ /** 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. */
11
+ const TEMPLATES = [
12
+ "blank",
13
+ "nodejs",
14
+ "notes",
15
+ "chat",
16
+ "todo",
17
+ "agent"
18
+ ];
19
+ /** One line per template: the single list `acrylblends init` and `create-acrylblends` both show (`create-acrylblends` tests that its own copy equals this one). */
20
+ const TEMPLATE_SUMMARY = {
21
+ blank: "the router, a server and one route: the smallest app",
22
+ nodejs: "a web app with a page and a client component",
23
+ notes: "a multi-user notes app on the db port (sign-up, log-in, sessions)",
24
+ chat: "a chat app that streams an answer over server-sent events",
25
+ todo: "a to-do list on the router and state",
26
+ agent: "the blank app with an offline agent that builds plugins into the running app"
27
+ };
28
+ /** Names that were templates once and still work: `create-acrylblends --template acryl-app` made the agent app. */
29
+ const TEMPLATE_ALIASES = { "acryl-app": "agent" };
30
+ function isTemplate(value) {
31
+ return TEMPLATES.includes(value);
32
+ }
33
+ /** The template a name means: itself, or what an old name stands for; undefined for anything else. */
34
+ function resolveTemplate(value) {
35
+ return isTemplate(value) ? value : TEMPLATE_ALIASES[value];
36
+ }
37
+ /**
38
+ * The `templates/` folder of this package. The module sits at different depths in source (`src/`) and in the build
39
+ * (`dist/`), so walk up until a `templates` folder holds the `blank` template.
40
+ */
41
+ function templatesRoot() {
42
+ let dir = dirname(fileURLToPath(import.meta.url));
43
+ for (;;) {
44
+ const candidate = join(dir, "templates");
45
+ if (existsSync(join(candidate, "blank"))) return candidate;
46
+ const parent = dirname(dir);
47
+ if (parent === dir) throw new Error("acrylblends cannot find its templates folder");
48
+ dir = parent;
49
+ }
50
+ }
51
+ /** Absolute path of a template folder. */
52
+ function templateDir(template) {
53
+ return join(templatesRoot(), template);
54
+ }
55
+ /** The text every generated app's `AGENTS.md` starts with (`shared/`, beside `templates/`). */
56
+ function sharedDir() {
57
+ return join(dirname(templatesRoot()), "shared");
58
+ }
59
+ /**
60
+ * The framework's docs, as ACRYL wrote them: `docs/framework` of the repository, or the copy `prepack` puts inside the package. The module sits at
61
+ * different depths in source and in the build, so walk up until a `docs/framework` folder holds the README.
62
+ */
63
+ function frameworkDocsRoot() {
64
+ let dir = dirname(fileURLToPath(import.meta.url));
65
+ for (;;) {
66
+ const candidate = join(dir, "docs", "framework");
67
+ if (existsSync(join(candidate, "README.md"))) return candidate;
68
+ const parent = dirname(dir);
69
+ if (parent === dir) throw new Error("acrylblends cannot find the framework docs (docs/framework)");
70
+ dir = parent;
71
+ }
72
+ }
73
+ //#endregion
74
+ //#region src/version.ts
75
+ /** The version of this package. */
76
+ const VERSION = createRequire(import.meta.url)("../package.json").version;
77
+ //#endregion
78
+ //#region src/lock.ts
79
+ /**
80
+ * What an app knows about the library plugins it carries: `plugins/library.lock.json` records, for every library plugin copied into the app, the version it came from and a hash of each file
81
+ * as it was copied. `acrylblends upgrade` reads it to tell a file nobody touched (safe to replace) from one the app edited (never replaced without `--force`). An app made before the lock existed
82
+ * has none; for it `upgrade` falls back to the hashes of the released versions (`shared/library-history.json`, made by `scripts/build-library-history.mjs`).
83
+ */
84
+ const LOCK_FILE = "library.lock.json";
85
+ /** A file's identity: a hash of its text with line endings normalized (a checkout may have changed them), cut to 20 hex digits. */
86
+ function digest(content) {
87
+ const text = typeof content === "string" ? content : content.toString("utf8");
88
+ return createHash("sha256").update(text.replace(/\r\n/gu, "\n")).digest("hex").slice(0, 20);
89
+ }
90
+ /** Every file below `dir`, as `relative/path` (forward slashes) to its {@link digest}. */
91
+ function hashFolder(dir) {
92
+ const files = {};
93
+ const walk = (folder, inside) => {
94
+ for (const entry of readdirSync(folder).sort()) {
95
+ const path = join(folder, entry);
96
+ const name = inside === "" ? entry : `${inside}/${entry}`;
97
+ if (statSync(path).isDirectory()) walk(path, name);
98
+ else files[name] = digest(readFileSync(path));
99
+ }
100
+ };
101
+ if (existsSync(dir)) walk(dir, "");
102
+ return files;
103
+ }
104
+ function readLock(pluginsDir) {
105
+ const file = join(pluginsDir, LOCK_FILE);
106
+ if (!existsSync(file)) return {
107
+ version: 1,
108
+ plugins: {}
109
+ };
110
+ try {
111
+ return {
112
+ version: 1,
113
+ plugins: JSON.parse(readFileSync(file, "utf8")).plugins ?? {}
114
+ };
115
+ } catch {
116
+ return {
117
+ version: 1,
118
+ plugins: {}
119
+ };
120
+ }
121
+ }
122
+ /** Record that `name` was copied from this version of the library, with the hashes of the files as they now are. */
123
+ function recordLock(pluginsDir, name, files) {
124
+ const lock = readLock(pluginsDir);
125
+ lock.plugins[name] = {
126
+ from: VERSION,
127
+ files
128
+ };
129
+ const sorted = Object.fromEntries(Object.entries(lock.plugins).sort(([a], [b]) => a.localeCompare(b)));
130
+ writeFileSync(join(pluginsDir, LOCK_FILE), `${JSON.stringify({
131
+ version: 1,
132
+ plugins: sorted
133
+ }, null, 2)}\n`);
134
+ }
135
+ function readHistory() {
136
+ const file = join(sharedDir(), "library-history.json");
137
+ if (!existsSync(file)) return {};
138
+ return JSON.parse(readFileSync(file, "utf8"));
139
+ }
140
+ //#endregion
8
141
  //#region src/rewrite.ts
9
142
  function entryTable(library) {
10
143
  const table = /* @__PURE__ */ new Map();
@@ -154,6 +287,7 @@ function pluginsFolder(appDir) {
154
287
  }
155
288
  /** The warning for an app with the old layout. */
156
289
  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.";
290
+ /** List `name` among the library copies of a plugins folder (`library.json`). */
157
291
  function recordLibraryCopy(folder, name) {
158
292
  const file = join(folder, LIBRARY_LIST);
159
293
  const names = existsSync(file) ? JSON.parse(readFileSync(file, "utf8")).plugins : [];
@@ -177,7 +311,10 @@ function copyPlugin(plugin, appDir, root = libraryRoot()) {
177
311
  }
178
312
  });
179
313
  if (entries) rewriteTree(join(target, "src"), plugin.name, "src", entries);
180
- if (!folder.legacy) recordLibraryCopy(folder.dir, plugin.name);
314
+ if (!folder.legacy) {
315
+ recordLibraryCopy(folder.dir, plugin.name);
316
+ recordLock(folder.dir, plugin.name, hashFolder(target));
317
+ }
181
318
  return target;
182
319
  }
183
320
  /** Rewrite the `acryl-*` imports of every TypeScript file below `dir` (the plugin's `src/`) to relative paths. */
@@ -194,10 +331,6 @@ function copyRewritten(from, to, plugin, inside, root = libraryRoot()) {
194
331
  }
195
332
  const relativeName = (appDir, path) => path.slice(appDir.length + 1).split(sep).join("/");
196
333
  //#endregion
197
- //#region src/version.ts
198
- /** The version of this package. */
199
- const VERSION = createRequire(import.meta.url)("../package.json").version;
200
- //#endregion
201
334
  //#region src/project.ts
202
335
  /** Thrown when the target directory exists and is not empty. Nothing has been written. */
203
336
  var TargetNotEmptyError = class extends Error {
@@ -392,56 +525,6 @@ function copyFrameworkDocs(source, target) {
392
525
  return written;
393
526
  }
394
527
  //#endregion
395
- //#region src/templates.ts
396
- /** 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. */
397
- const TEMPLATES = [
398
- "nodejs",
399
- "notes",
400
- "chat",
401
- "blank",
402
- "todo",
403
- "agent"
404
- ];
405
- function isTemplate(value) {
406
- return TEMPLATES.includes(value);
407
- }
408
- /**
409
- * The `templates/` folder of this package. The module sits at different depths in source (`src/`) and in the build
410
- * (`dist/`), so walk up until a `templates` folder holds the `blank` template.
411
- */
412
- function templatesRoot() {
413
- let dir = dirname(fileURLToPath(import.meta.url));
414
- for (;;) {
415
- const candidate = join(dir, "templates");
416
- if (existsSync(join(candidate, "blank"))) return candidate;
417
- const parent = dirname(dir);
418
- if (parent === dir) throw new Error("acrylblends cannot find its templates folder");
419
- dir = parent;
420
- }
421
- }
422
- /** Absolute path of a template folder. */
423
- function templateDir(template) {
424
- return join(templatesRoot(), template);
425
- }
426
- /** The text every generated app's `AGENTS.md` starts with (`shared/`, beside `templates/`). */
427
- function sharedDir() {
428
- return join(dirname(templatesRoot()), "shared");
429
- }
430
- /**
431
- * The framework's docs, as ACRYL wrote them: `docs/framework` of the repository, or the copy `prepack` puts inside the package. The module sits at
432
- * different depths in source and in the build, so walk up until a `docs/framework` folder holds the README.
433
- */
434
- function frameworkDocsRoot() {
435
- let dir = dirname(fileURLToPath(import.meta.url));
436
- for (;;) {
437
- const candidate = join(dir, "docs", "framework");
438
- if (existsSync(join(candidate, "README.md"))) return candidate;
439
- const parent = dirname(dir);
440
- if (parent === dir) throw new Error("acrylblends cannot find the framework docs (docs/framework)");
441
- dir = parent;
442
- }
443
- }
444
- //#endregion
445
528
  //#region src/init.ts
446
529
  /** A command line mistake. The CLI prints it with the help text and exits 2. */
447
530
  var UsageError = class extends Error {
@@ -451,26 +534,27 @@ var UsageError = class extends Error {
451
534
  function parseInitOptions(argv) {
452
535
  let parsed;
453
536
  try {
454
- parsed = parse$2(argv);
537
+ parsed = parse$3(argv);
455
538
  } catch (error) {
456
539
  throw new UsageError(error.message);
457
540
  }
458
541
  const { values, positionals } = parsed;
459
542
  if (positionals.length > 1) throw new UsageError("init takes at most one directory");
460
543
  if (values.agent && values["no-agent"]) throw new UsageError("use --agent or --no-agent, not both");
461
- if (values.template !== void 0 && !isTemplate(values.template)) throw new UsageError(`unknown template "${values.template}" (${TEMPLATES.join(", ")})`);
544
+ const named = values.template === void 0 ? void 0 : resolveTemplate(values.template);
545
+ if (values.template !== void 0 && named === void 0) throw new UsageError(`unknown template "${values.template}" (${TEMPLATES.join(", ")})`);
462
546
  const alias = values.agent ? "agent" : values["no-agent"] ? "blank" : void 0;
463
- if (values.template !== void 0 && alias !== void 0 && values.template !== alias) throw new UsageError(`--template ${values.template} contradicts --${alias === "agent" ? "agent" : "no-agent"}`);
547
+ if (named !== void 0 && alias !== void 0 && named !== alias) throw new UsageError(`--template ${values.template} contradicts --${alias === "agent" ? "agent" : "no-agent"}`);
464
548
  return {
465
549
  directory: positionals[0],
466
550
  name: values.name,
467
- template: values.template ?? alias,
551
+ template: named ?? alias,
468
552
  install: values.install,
469
553
  desktop: values.desktop === true,
470
554
  yes: values.yes === true
471
555
  };
472
556
  }
473
- const parse$2 = (argv) => parseArgs({
557
+ const parse$3 = (argv) => parseArgs({
474
558
  args: [...argv],
475
559
  allowPositionals: true,
476
560
  strict: true,
@@ -489,9 +573,10 @@ const parse$2 = (argv) => parseArgs({
489
573
  });
490
574
  const yes = (answer) => ["y", "yes"].includes(answer.trim().toLowerCase());
491
575
  async function askTemplate(ask) {
492
- const answer = (await ask(`Template (${TEMPLATES.join(", ")}) [blank]: `)).trim().toLowerCase();
576
+ const answer = (await ask(`Templates:\n${TEMPLATES.map((name) => ` ${name.padEnd(7)} ${TEMPLATE_SUMMARY[name]}`).join("\n")}\nTemplate [blank]: `)).trim().toLowerCase();
493
577
  if (answer === "") return "blank";
494
- if (isTemplate(answer)) return answer;
578
+ const chosen = resolveTemplate(answer);
579
+ if (chosen !== void 0) return chosen;
495
580
  throw new UsageError(`unknown template "${answer}" (${TEMPLATES.join(", ")})`);
496
581
  }
497
582
  /** Scaffold a Cordis app. Returns the exit code: 0 done, 1 refused or failed. Never overwrites anything. */
@@ -542,7 +627,7 @@ async function init(options, io) {
542
627
  function parseAddOptions(argv) {
543
628
  let parsed;
544
629
  try {
545
- parsed = parse$1(argv);
630
+ parsed = parse$2(argv);
546
631
  } catch (error) {
547
632
  throw new UsageError(error.message);
548
633
  }
@@ -553,7 +638,7 @@ function parseAddOptions(argv) {
553
638
  suite: parsed.values.suite === true
554
639
  };
555
640
  }
556
- const parse$1 = (argv) => parseArgs({
641
+ const parse$2 = (argv) => parseArgs({
557
642
  args: [...argv],
558
643
  allowPositionals: true,
559
644
  strict: true,
@@ -976,7 +1061,7 @@ function processIo() {
976
1061
  function parseKvOptions(argv) {
977
1062
  let parsed;
978
1063
  try {
979
- parsed = parse(argv);
1064
+ parsed = parse$1(argv);
980
1065
  } catch (error) {
981
1066
  throw new UsageError(error.message);
982
1067
  }
@@ -991,7 +1076,7 @@ function parseKvOptions(argv) {
991
1076
  migrations: parsed.values.migrations
992
1077
  };
993
1078
  }
994
- const parse = (argv) => parseArgs({
1079
+ const parse$1 = (argv) => parseArgs({
995
1080
  args: [...argv],
996
1081
  allowPositionals: true,
997
1082
  strict: true,
@@ -1046,6 +1131,295 @@ async function kvImport(options, io) {
1046
1131
  }
1047
1132
  }
1048
1133
  //#endregion
1134
+ //#region src/diff.ts
1135
+ function ops(a, b) {
1136
+ if (a.length * b.length > 6e6) return void 0;
1137
+ const width = b.length + 1;
1138
+ const table = new Uint32Array((a.length + 1) * width);
1139
+ for (let i = a.length - 1; i >= 0; i -= 1) for (let j = b.length - 1; j >= 0; j -= 1) table[i * width + j] = a[i] === b[j] ? table[(i + 1) * width + j + 1] + 1 : Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
1140
+ const out = [];
1141
+ let i = 0;
1142
+ let j = 0;
1143
+ while (i < a.length && j < b.length) if (a[i] === b[j]) {
1144
+ out.push({
1145
+ kind: " ",
1146
+ line: a[i]
1147
+ });
1148
+ i += 1;
1149
+ j += 1;
1150
+ } else if (table[(i + 1) * width + j] >= table[i * width + j + 1]) {
1151
+ out.push({
1152
+ kind: "-",
1153
+ line: a[i]
1154
+ });
1155
+ i += 1;
1156
+ } else {
1157
+ out.push({
1158
+ kind: "+",
1159
+ line: b[j]
1160
+ });
1161
+ j += 1;
1162
+ }
1163
+ for (; i < a.length; i += 1) out.push({
1164
+ kind: "-",
1165
+ line: a[i]
1166
+ });
1167
+ for (; j < b.length; j += 1) out.push({
1168
+ kind: "+",
1169
+ line: b[j]
1170
+ });
1171
+ return out;
1172
+ }
1173
+ const lines = (text) => text === "" ? [] : text.replace(/\r\n/gu, "\n").replace(/\n$/u, "").split("\n");
1174
+ /** How many lines a change adds and removes; undefined counts when the pair is too large to compare. */
1175
+ function diffStat(from, to) {
1176
+ const all = ops(lines(from), lines(to));
1177
+ if (all === void 0) return {
1178
+ added: lines(to).length,
1179
+ removed: lines(from).length
1180
+ };
1181
+ return {
1182
+ added: all.filter((op) => op.kind === "+").length,
1183
+ removed: all.filter((op) => op.kind === "-").length
1184
+ };
1185
+ }
1186
+ /** `--- a/<name>`, `+++ b/<name>` and the hunks; empty when the texts are equal. */
1187
+ function unifiedDiff(name, from, to, context = 3) {
1188
+ const a = lines(from);
1189
+ const b = lines(to);
1190
+ const all = ops(a, b);
1191
+ if (all === void 0) return `--- a/${name}\n+++ b/${name}\n(too large to compare line by line: ${a.length} lines became ${b.length})\n`;
1192
+ if (!all.some((op) => op.kind !== " ")) return "";
1193
+ const out = [`--- a/${name}`, `+++ b/${name}`];
1194
+ let at = 0;
1195
+ let aLine = 1;
1196
+ let bLine = 1;
1197
+ const positions = all.map((op) => {
1198
+ const here = {
1199
+ a: aLine,
1200
+ b: bLine
1201
+ };
1202
+ if (op.kind !== "+") aLine += 1;
1203
+ if (op.kind !== "-") bLine += 1;
1204
+ return here;
1205
+ });
1206
+ while (at < all.length) {
1207
+ while (at < all.length && all[at]?.kind === " ") at += 1;
1208
+ if (at >= all.length) break;
1209
+ const start = Math.max(0, at - context);
1210
+ let end = at;
1211
+ for (let k = at; k < all.length; k += 1) if (all[k]?.kind !== " ") end = k;
1212
+ else if (k - end > context * 2) break;
1213
+ const stop = Math.min(all.length, end + context + 1);
1214
+ const slice = all.slice(start, stop);
1215
+ const first = positions[start];
1216
+ out.push(`@@ -${first.a},${slice.filter((op) => op.kind !== "+").length} +${first.b},${slice.filter((op) => op.kind !== "-").length} @@`);
1217
+ for (const op of slice) out.push(`${op.kind}${op.line}`);
1218
+ at = stop;
1219
+ }
1220
+ return `${out.join("\n")}\n`;
1221
+ }
1222
+ //#endregion
1223
+ //#region src/upgrade.ts
1224
+ /** Parse `upgrade [plugin...] [--dir <app>] [--dry-run] [--force] [--diff]`. */
1225
+ function parseUpgradeOptions(argv) {
1226
+ let parsed;
1227
+ try {
1228
+ parsed = parse(argv);
1229
+ } catch (error) {
1230
+ throw new UsageError(error.message);
1231
+ }
1232
+ return {
1233
+ names: parsed.positionals,
1234
+ directory: parsed.values.dir,
1235
+ dryRun: parsed.values["dry-run"] === true,
1236
+ force: parsed.values.force === true,
1237
+ diff: parsed.values.diff === true
1238
+ };
1239
+ }
1240
+ const parse = (argv) => parseArgs({
1241
+ args: [...argv],
1242
+ allowPositionals: true,
1243
+ strict: true,
1244
+ options: {
1245
+ dir: { type: "string" },
1246
+ "dry-run": { type: "boolean" },
1247
+ force: { type: "boolean" },
1248
+ diff: { type: "boolean" }
1249
+ }
1250
+ });
1251
+ const posix$1 = (path) => path.split(sep).join("/");
1252
+ function readText(path) {
1253
+ return readFileSync(path, "utf8");
1254
+ }
1255
+ /** The library copies an app carries: the plugins listed in `plugins/library.json`, or when there is none the folders of `plugins/` that the library knows. */
1256
+ function carriedCopies(dir) {
1257
+ const listFile = join(dir, LIBRARY_LIST);
1258
+ return (existsSync(listFile) ? JSON.parse(readFileSync(listFile, "utf8")).plugins ?? [] : readdirSync(dir).filter((name) => statSync(join(dir, name)).isDirectory())).filter((name) => existsSync(join(dir, name)));
1259
+ }
1260
+ /**
1261
+ * `acrylblends upgrade`: bring the library plugins an app carries (the router, the server, the door, `extensions`, ...) up to the version of this package. Each file is compared with what
1262
+ * the app was given: an untouched file is replaced, a file the app edited stops the upgrade (it is shown as a diff and nothing changes) unless `--force`, which saves the edited version
1263
+ * under `.acrylblends/upgrade-backup/` first. Returns the exit code: 0 done or nothing to do, 1 refused or failed.
1264
+ */
1265
+ async function upgrade(options, io) {
1266
+ const appDir = resolve(io.cwd, options.directory ?? ".");
1267
+ if (!existsSync(join(appDir, "package.json")) || !existsSync(join(appDir, "src"))) {
1268
+ io.stderr.write(`error: ${appDir} is not an app (no package.json and src/); run "acrylblends init" first\n`);
1269
+ return 1;
1270
+ }
1271
+ const folder = pluginsFolder(appDir);
1272
+ if (folder.legacy) {
1273
+ io.stderr.write(`${LEGACY_WARNING}\nerror: upgrade needs the one-folder layout (plugins/); move the plugins first\n`);
1274
+ return 1;
1275
+ }
1276
+ const library = readLibrary();
1277
+ const carried = carriedCopies(folder.dir);
1278
+ const unknown = carried.filter((name) => !library.has(name));
1279
+ for (const name of options.names) if (!carried.includes(name)) {
1280
+ io.stderr.write(`error: the app carries no library plugin "${name}" (it carries: ${carried.filter((n) => library.has(n)).join(", ") || "none"})\n`);
1281
+ return 1;
1282
+ }
1283
+ const selected = (options.names.length > 0 ? [...options.names] : carried).filter((name) => library.has(name));
1284
+ if (selected.length === 0) {
1285
+ io.stdout.write("nothing to upgrade: the app carries no library plugins\n");
1286
+ return 0;
1287
+ }
1288
+ const scratch = join(appDir, ".acrylblends", `upgrade-${process.pid}-${Date.now()}`);
1289
+ const lock = readLock(folder.dir);
1290
+ const history = readHistory();
1291
+ const plans = [];
1292
+ try {
1293
+ mkdirSync(join(scratch, "plugins"), { recursive: true });
1294
+ const wanted = closure(selected, library);
1295
+ for (const plugin of wanted) copyPlugin(plugin, scratch);
1296
+ for (const plugin of wanted) {
1297
+ const here = join(folder.dir, plugin.name);
1298
+ const fresh = hashFolder(join(scratch, "plugins", plugin.name));
1299
+ const files = /* @__PURE__ */ new Map();
1300
+ const isNew = !existsSync(here);
1301
+ const mine = isNew ? {} : hashFolder(here);
1302
+ const recorded = lock.plugins[plugin.name]?.files ?? {};
1303
+ const pristine = (file, hash) => recorded[file] === hash || (history[plugin.name]?.[file]?.includes(hash) ?? false);
1304
+ for (const [file, hash] of Object.entries(fresh)) {
1305
+ const have = mine[file];
1306
+ files.set(file, have === void 0 ? "add" : have === hash ? "same" : pristine(file, have) ? "update" : "conflict");
1307
+ }
1308
+ for (const [file, hash] of Object.entries(mine)) if (!(file in fresh)) files.set(file, pristine(file, hash) ? "remove" : "extra");
1309
+ plans.push({
1310
+ plugin,
1311
+ isNew,
1312
+ files,
1313
+ fresh
1314
+ });
1315
+ }
1316
+ const count = (plan, state) => [...plan.files].filter(([, s]) => s === state).map(([file]) => file);
1317
+ io.stdout.write(`upgrade: the library plugins of ${appDir} to acrylblends ${VERSION}\n`);
1318
+ let conflicts = 0;
1319
+ let changes = 0;
1320
+ for (const plan of plans) {
1321
+ const [updated, added, removed, edited, extra] = [
1322
+ "update",
1323
+ "add",
1324
+ "remove",
1325
+ "conflict",
1326
+ "extra"
1327
+ ].map((state) => count(plan, state));
1328
+ const label = plan.plugin.name.padEnd(22);
1329
+ conflicts += edited.length;
1330
+ if (plan.isNew) {
1331
+ changes += 1;
1332
+ io.stdout.write(` ${label} new (required by a plugin of the app)\n`);
1333
+ continue;
1334
+ }
1335
+ if (updated.length + added.length + removed.length + edited.length === 0) {
1336
+ io.stdout.write(` ${label} up to date${extra.length > 0 ? ` (keeps ${extra.length} file(s) of its own)` : ""}\n`);
1337
+ continue;
1338
+ }
1339
+ changes += 1;
1340
+ let plus = 0;
1341
+ let minus = 0;
1342
+ for (const file of [...updated, ...edited]) {
1343
+ const stat = diffStat(readText(join(folder.dir, plan.plugin.name, file)), readText(join(scratch, "plugins", plan.plugin.name, file)));
1344
+ plus += stat.added;
1345
+ minus += stat.removed;
1346
+ }
1347
+ const parts = [
1348
+ updated.length > 0 ? `${updated.length} updated (+${plus} -${minus})` : "",
1349
+ added.length > 0 ? `${added.length} added` : "",
1350
+ removed.length > 0 ? `${removed.length} removed` : "",
1351
+ edited.length > 0 ? `${edited.length} EDITED HERE` : "",
1352
+ extra.length > 0 ? `${extra.length} of its own kept` : ""
1353
+ ].filter(Boolean);
1354
+ io.stdout.write(` ${label} ${parts.join(", ")}\n`);
1355
+ }
1356
+ for (const name of unknown) io.stdout.write(` ${name.padEnd(22)} not in this library: left alone\n`);
1357
+ for (const plan of plans) for (const [file, state] of plan.files) {
1358
+ if (!(state === "conflict" || options.diff && (state === "update" || state === "add" || state === "remove"))) continue;
1359
+ const name = `plugins/${plan.plugin.name}/${file}`;
1360
+ const before = state === "add" ? "" : readText(join(folder.dir, plan.plugin.name, file));
1361
+ const after = state === "remove" ? "" : readText(join(scratch, "plugins", plan.plugin.name, file));
1362
+ io.stdout.write(`\n${state === "conflict" ? "EDITED HERE, the upgrade would replace it:\n" : ""}${unifiedDiff(name, before, after)}`);
1363
+ }
1364
+ if (conflicts > 0 && !options.force) {
1365
+ io.stderr.write(`\nerror: ${conflicts} file(s) were edited in this app and the upgrade would replace them; nothing was changed. Review the diffs above, move your changes out of the plugin (a plugin of your own in plugins/<name>/), or run again with --force (the edited versions are saved under .acrylblends/upgrade-backup/)\n`);
1366
+ return 1;
1367
+ }
1368
+ if (options.dryRun) {
1369
+ io.stdout.write(`\n${changes === 0 ? "everything is up to date" : "dry run: nothing was changed"}\n`);
1370
+ return 0;
1371
+ }
1372
+ if (changes === 0) {
1373
+ io.stdout.write("\neverything is up to date\n");
1374
+ return 0;
1375
+ }
1376
+ const backup = join(appDir, ".acrylblends", "upgrade-backup", (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/gu, "-"));
1377
+ const addedDependencies = [];
1378
+ for (const plan of plans) {
1379
+ const here = join(folder.dir, plan.plugin.name);
1380
+ if (plan.isNew) {
1381
+ mkdirSync(here, { recursive: true });
1382
+ cpSync(join(scratch, "plugins", plan.plugin.name), here, { recursive: true });
1383
+ recordLibraryCopy(folder.dir, plan.plugin.name);
1384
+ } else {
1385
+ for (const [file, state] of plan.files) {
1386
+ const target = join(here, file);
1387
+ if (state === "conflict") {
1388
+ mkdirSync(dirname(join(backup, plan.plugin.name, file)), { recursive: true });
1389
+ cpSync(target, join(backup, plan.plugin.name, file));
1390
+ }
1391
+ if (state === "add" || state === "update" || state === "conflict") {
1392
+ mkdirSync(dirname(target), { recursive: true });
1393
+ cpSync(join(scratch, "plugins", plan.plugin.name, file), target);
1394
+ } else if (state === "remove") rmSync(target, { force: true });
1395
+ }
1396
+ recordLibraryCopy(folder.dir, plan.plugin.name);
1397
+ }
1398
+ recordLock(folder.dir, plan.plugin.name, plan.fresh);
1399
+ const plugin = plan.plugin;
1400
+ if (plugin.imports) mergeImports(appDir, plugin.imports, posix$1(relative(appDir, folder.dir)));
1401
+ addedDependencies.push(...mergeDependencies(appDir, plugin.dependencies));
1402
+ addedDependencies.push(...mergeTooling(appDir, plugin).devDependencies);
1403
+ appendGitignore(appDir, plugin.gitignore);
1404
+ }
1405
+ const manifestFile = join(appDir, "package.json");
1406
+ const manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
1407
+ if (manifest.dependencies?.acrylblends !== void 0 && manifest.dependencies.acrylblends !== `^${VERSION}`) {
1408
+ manifest.dependencies.acrylblends = `^${VERSION}`;
1409
+ writeFileSync(manifestFile, `${JSON.stringify(manifest, null, 2)}\n`);
1410
+ }
1411
+ io.stdout.write(`\nupgraded ${plans.filter((p) => [...p.files.values()].some((s) => s !== "same" && s !== "extra") || p.isNew).map((p) => p.plugin.name).join(", ")}\n`);
1412
+ if (conflicts > 0) io.stdout.write(`the ${conflicts} edited file(s) replaced by --force are saved in ${posix$1(relative(appDir, backup))}/\n`);
1413
+ io.stdout.write(`acrylblends is now ^${VERSION} in package.json${addedDependencies.length > 0 ? `; new dependencies: ${[...new Set(addedDependencies)].join(", ")}` : ""}: run npm install, then npm run check and npm test\n`);
1414
+ return 0;
1415
+ } finally {
1416
+ rmSync(scratch, {
1417
+ recursive: true,
1418
+ force: true
1419
+ });
1420
+ }
1421
+ }
1422
+ //#endregion
1049
1423
  //#region src/cli.ts
1050
1424
  const HELP = `acrylblends ${VERSION} - scaffold Cordis apps
1051
1425
 
@@ -1054,6 +1428,9 @@ Usage:
1054
1428
  acrylblends add <plugin...> [--dir d] [--suite] Copy library plugins (and what they require) into an app;
1055
1429
  a folder path brings an external plugin into its plugins/;
1056
1430
  --suite also copies the port's conformance suite and vitest (add kv --suite)
1431
+ acrylblends upgrade [plugin...] [--dir d] [--dry-run] [--force] [--diff]
1432
+ Bring the library plugins an app carries (the router, the server, the door, ...) up to this
1433
+ version: untouched files are replaced, an edited file stops it (diff shown) unless --force
1057
1434
  acrylblends plugins List the plugin library
1058
1435
  acrylblends build-client [root] [out] Compile the client modules once for production (src/client to dist/client)
1059
1436
  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]
@@ -1061,7 +1438,7 @@ Usage:
1061
1438
  package.json "acryl": { "opener": "src/db.ts" } (a port is imported, a provider is not)
1062
1439
 
1063
1440
  Options for init:
1064
- --template <name> blank or agent (asked when not given); --agent means --template agent,
1441
+ --template <name> one of the templates below (asked when not given); --agent means --template agent,
1065
1442
  --no-agent means --template blank
1066
1443
  --name <name> the project name (default: the directory name)
1067
1444
  --desktop also add the desktop shell: npm run desktop opens a window on the app
@@ -1070,6 +1447,9 @@ Options for init:
1070
1447
 
1071
1448
  -h, --help show this help
1072
1449
  -v, --version show the version
1450
+
1451
+ Templates (the same list as npm create acrylblends):
1452
+ ${TEMPLATES.map((name) => ` ${name.padEnd(7)} ${TEMPLATE_SUMMARY[name]}`).join("\n")}
1073
1453
  `;
1074
1454
  /**
1075
1455
  * Run the CLI and return the exit code: 0 done, 1 failed or refused, 2 a usage error. Never calls `process.exit`.
@@ -1087,6 +1467,7 @@ async function runCli(argv, io = processIo()) {
1087
1467
  }
1088
1468
  if (command === "init") return await init(parseInitOptions(args), io);
1089
1469
  if (command === "add") return await add(parseAddOptions(args), io);
1470
+ if (command === "upgrade") return await upgrade(parseUpgradeOptions(args), io);
1090
1471
  if (command === "build-client") return await buildClient(parseBuildClientOptions(args), io);
1091
1472
  if (command === "kv") return await kvImport(parseKv(args), io);
1092
1473
  if (command === "check-deps") return checkDeps(parseCheckDeps(args), io);
@@ -1112,6 +1493,6 @@ function parseKv(args) {
1112
1493
  return parseKvOptions(args);
1113
1494
  }
1114
1495
  //#endregion
1115
- 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 };
1496
+ export { HELP, TEMPLATES, TEMPLATE_ALIASES, TEMPLATE_SUMMARY, TargetNotEmptyError, UsageError, VERSION, add, assertTargetFree, closure, copyPlugin, copyTemplate, init, isOmitted, isTemplate, isValidName, libraryRoot, listPlugins, mergeDependencies, nameFrom, parseAddOptions, parseInitOptions, parseUpgradeOptions, readLibrary, readTemplatePlugins, resolveTemplate, runCli, templateDir, templatesRoot, upgrade };
1116
1497
 
1117
1498
  //# sourceMappingURL=index.js.map