acrylblends 1.2.0 → 1.3.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 (38) hide show
  1. package/dist/index.d.ts +2 -1
  2. package/dist/index.js +39 -7
  3. package/dist/index.js.map +1 -1
  4. package/docs/framework/guides/create-acrylblends.md +4 -2
  5. package/docs/framework/workshop/README.md +2 -2
  6. package/docs/framework/workshop/facilitator.md +1 -1
  7. package/lib/cordis/door.d.ts +45 -2
  8. package/lib/cordis/door.js +120 -0
  9. package/package.json +1 -1
  10. package/plugins/acryl-door/plugin.json +3 -0
  11. package/plugins/acryl-door/src/cli/acryl.ts +63 -6
  12. package/plugins/acryl-door/src/host/door.ts +75 -1
  13. package/plugins/acryl-door/src/host/operator-routes.ts +21 -1
  14. package/plugins/acryl-door/src/host/verbs.ts +11 -1
  15. package/plugins/acryl-door/src/index.ts +2 -1
  16. package/plugins/builder/index.ts +13 -35
  17. package/plugins/builder/package.ts +186 -0
  18. package/plugins/builder/plugin.json +3 -1
  19. package/plugins/extensions/index.ts +21 -17
  20. package/plugins/extensions/plugin-store.ts +27 -0
  21. package/plugins/extensions/plugin.json +1 -1
  22. package/plugins/extensions/stage.ts +15 -5
  23. package/plugins/manifest/plugin.json +1 -1
  24. package/plugins/plugin-share/README.md +51 -0
  25. package/plugins/plugin-share/index.ts +467 -0
  26. package/plugins/plugin-share/plugin.json +15 -0
  27. package/plugins/plugin-share/publish.ts +65 -0
  28. package/plugins/plugin-share/pull-check.ts +129 -0
  29. package/plugins/plugin-share/pulled-records.ts +70 -0
  30. package/templates/agent/.gitattributes +2 -0
  31. package/templates/agent/blend.yaml +2 -0
  32. package/templates/agent/src/app.ts +4 -0
  33. package/templates/agent/template.json +1 -0
  34. package/templates/blank/.gitattributes +2 -0
  35. package/templates/chat/.gitattributes +2 -0
  36. package/templates/nodejs/.gitattributes +2 -0
  37. package/templates/notes/.gitattributes +2 -0
  38. package/templates/todo/.gitattributes +2 -0
package/dist/index.d.ts CHANGED
@@ -187,7 +187,8 @@ export declare function assertTargetFree(target: string): void;
187
187
  export declare function copyTemplate(source: string, target: string, name: string): string[];
188
188
  export declare function readTemplatePlugins(templateDir: string): string[];
189
189
  /**
190
- * Add run-time dependencies to an app's package.json, keeping what is there and sorting the keys. Returns the names added. A package the app lists as a development
190
+ * Add run-time dependencies to an app's package.json and sort the keys. Returns the names to install: those added, and those moved because a plugin asks for a newer
191
+ * version than the app lists (the plugin's new code needs it). A version the app lists that is the same or newer stays. A package the app lists as a development
191
192
  * dependency too is taken out of that list: npm lets the development entry win, so `npm install --omit=dev` would leave a run-time dependency out.
192
193
  */
193
194
  export declare function mergeDependencies(appDir: string, dependencies: Readonly<Record<string, string>>): string[];
package/dist/index.js CHANGED
@@ -1002,6 +1002,18 @@ function applyIdeProfile(root, input) {
1002
1002
  //#region src/version.ts
1003
1003
  /** The version of this package. */
1004
1004
  const VERSION = createRequire(import.meta.url)("../package.json").version;
1005
+ /** Semver order for the release versions (numeric segments; a pre-release sorts before its release). */
1006
+ function compareVersions(a, b) {
1007
+ const pa = a.split(/[.-]/u).map((part) => Number.isNaN(Number(part)) ? part : Number(part));
1008
+ const pb = b.split(/[.-]/u).map((part) => Number.isNaN(Number(part)) ? part : Number(part));
1009
+ for (let i = 0; i < Math.max(pa.length, pb.length); i += 1) {
1010
+ const x = pa[i] ?? 0;
1011
+ const y = pb[i] ?? 0;
1012
+ if (x < y) return -1;
1013
+ if (x > y) return 1;
1014
+ }
1015
+ return 0;
1016
+ }
1005
1017
  /** The DSH release the frame's packages pin as peers, so an app's DSH pin is the same one. */
1006
1018
  const FRAME_DSH_VERSION = dshVersion;
1007
1019
  /** Every package the frame publishes, in the file's order. */
@@ -1151,6 +1163,11 @@ const HOST_PROVIDED_IDS = /* @__PURE__ */ new Set([
1151
1163
  "save",
1152
1164
  "acryl-door"
1153
1165
  ]);
1166
+ /**
1167
+ * Services the app's rows provide that the host's own rows call: the door and the frame reach `pluginShare`, and no host row provides that name. Left out of
1168
+ * the isolate list, so they find it; every other service stays in the app's realm.
1169
+ */
1170
+ const REACHED_FROM_HOST = /* @__PURE__ */ new Set(["pluginShare"]);
1154
1171
  /** The generated `frame/app-plugins.mjs`, by the row that mounts it. */
1155
1172
  const APP_PLUGINS_FILE = "frame/app-plugins.mjs";
1156
1173
  /**
@@ -1176,12 +1193,14 @@ function appPluginsRow(prefix) {
1176
1193
  " return path === base || path.startsWith(base + '/') ? path : base + (path.startsWith('/') ? '' : '/') + path",
1177
1194
  "}",
1178
1195
  "",
1179
- "/** The router as the app's rows see it: mounts land under the prefix, every other member is the host router's. */",
1196
+ "/** The router as the app's rows see it: mounts land under the prefix, and a plugin's own request (its install check) finds them there; every other member is the host router's. */",
1180
1197
  "function appRouter(router, prefix) {",
1181
1198
  " const route = (path, app) => router.route(prefixedAppRoute(prefix, path), app)",
1199
+ " const request = (input, ...rest) => router.request(typeof input === 'string' && input.startsWith('/') ? prefixedAppRoute(prefix, input) : input, ...rest)",
1182
1200
  " return new Proxy(router, {",
1183
1201
  " get(target, property) {",
1184
1202
  " if (property === 'route') return route",
1203
+ " if (property === 'request') return request",
1185
1204
  " const value = Reflect.get(target, property, target)",
1186
1205
  " return typeof value === 'function' ? value.bind(target) : value",
1187
1206
  " },",
@@ -1229,7 +1248,7 @@ function providedServices(appDir) {
1229
1248
  }
1230
1249
  };
1231
1250
  for (const row of ownRowIds(appDir)) walk(join(appDir, "plugins", row));
1232
- return Object.fromEntries([...provided].sort().map((name) => [name, true]));
1251
+ return Object.fromEntries([...provided].filter((name) => !REACHED_FROM_HOST.has(name)).sort().map((name) => [name, true]));
1233
1252
  }
1234
1253
  /**
1235
1254
  * The frame rows for an app: its routes row when it carries `src/frame.ts`, and the composition row when its `src/app.ts` exports `composeInto`.
@@ -1662,8 +1681,19 @@ function readTemplatePlugins(templateDir) {
1662
1681
  const { plugins } = JSON.parse(readFileSync(file, "utf8"));
1663
1682
  return plugins ?? [];
1664
1683
  }
1684
+ /** A version a plugin's pin can be compared with: an exact one or a caret or tilde range. A tag, a path or a URL is the app's own choice. */
1685
+ function comparableVersion(spec) {
1686
+ return /^[\^~]?\d+\.\d+\.\d+(?:-[\w.]+)?$/u.test(spec) ? spec.replace(/^[\^~]/u, "") : void 0;
1687
+ }
1688
+ /** Whether a plugin's pin asks for more than what the app has: the plugin's code was written against that version. */
1689
+ function asksForNewer(wanted, current) {
1690
+ const a = comparableVersion(wanted);
1691
+ const b = comparableVersion(current);
1692
+ return a !== void 0 && b !== void 0 && compareVersions(a, b) > 0;
1693
+ }
1665
1694
  /**
1666
- * Add run-time dependencies to an app's package.json, keeping what is there and sorting the keys. Returns the names added. A package the app lists as a development
1695
+ * Add run-time dependencies to an app's package.json and sort the keys. Returns the names to install: those added, and those moved because a plugin asks for a newer
1696
+ * version than the app lists (the plugin's new code needs it). A version the app lists that is the same or newer stays. A package the app lists as a development
1667
1697
  * dependency too is taken out of that list: npm lets the development entry win, so `npm install --omit=dev` would leave a run-time dependency out.
1668
1698
  */
1669
1699
  function mergeDependencies(appDir, dependencies) {
@@ -1671,14 +1701,16 @@ function mergeDependencies(appDir, dependencies) {
1671
1701
  const manifest = JSON.parse(readFileSync(file, "utf8"));
1672
1702
  const current = manifest.dependencies ?? {};
1673
1703
  const added = Object.keys(dependencies).filter((name) => !(name in current));
1704
+ const moved = Object.entries(dependencies).filter(([name, wanted]) => current[name] !== void 0 && asksForNewer(wanted, current[name]));
1674
1705
  const merged = {
1675
1706
  ...dependencies,
1676
- ...current
1707
+ ...current,
1708
+ ...Object.fromEntries(moved)
1677
1709
  };
1678
1710
  manifest.dependencies = Object.fromEntries(Object.entries(merged).sort(([a], [b]) => a.localeCompare(b)));
1679
1711
  if (manifest.devDependencies !== void 0) manifest.devDependencies = Object.fromEntries(Object.entries(manifest.devDependencies).filter(([name]) => !(name in merged)));
1680
1712
  writeFileSync(file, `${JSON.stringify(manifest, null, 2)}\n`);
1681
- return added;
1713
+ return [...added, ...moved.map(([name]) => name)];
1682
1714
  }
1683
1715
  /** 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. */
1684
1716
  function mergeImports(appDir, imports, pluginsDir) {
@@ -2142,7 +2174,7 @@ async function add(options, io) {
2142
2174
  if (refused !== 0) return refused;
2143
2175
  }
2144
2176
  io.stdout.write(`added ${toCopy.map((plugin) => plugin.name).join(", ")} to ${appDir}\n`);
2145
- if (addedDependencies.length > 0) io.stdout.write(`new dependencies: ${addedDependencies.join(", ")} (run npm install)\n`);
2177
+ if (addedDependencies.length > 0) io.stdout.write(`dependencies to install: ${addedDependencies.join(", ")} (run npm install)\n`);
2146
2178
  for (const plugin of toCopy.filter((plugin) => names.includes(plugin.name))) {
2147
2179
  const mounted = MOUNTED.find((candidate) => candidate.name === plugin.name);
2148
2180
  const result = mounted ? mount(appDir, mounted) : void 0;
@@ -3765,7 +3797,7 @@ async function upgrade(options, io) {
3765
3797
  const upgradedPlugins = plans.filter((p) => [...p.files.values()].some((s) => s !== "same" && s !== "extra") || p.isNew).map((p) => p.plugin.name);
3766
3798
  io.stdout.write(`\nupgraded ${[...upgradedPlugins, ...frameChanged ? ["the frame"] : []].join(", ")}\n`);
3767
3799
  if (conflicts + frameBackups > 0) io.stdout.write(`the ${conflicts + frameBackups} edited file(s) replaced by --force are saved in ${posix$1(relative(appDir, backup))}/\n`);
3768
- 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`);
3800
+ io.stdout.write(`acrylblends is now ^${VERSION} in package.json${addedDependencies.length > 0 ? `; dependencies to install: ${[...new Set(addedDependencies)].join(", ")}` : ""}: run npm install, then npm run check and npm test\n`);
3769
3801
  return 0;
3770
3802
  } finally {
3771
3803
  rmSync(scratch, {