acrylblends 1.3.0 → 1.3.2

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 (68) hide show
  1. package/dist/index.js +5 -3
  2. package/dist/index.js.map +1 -1
  3. package/docs/framework/workshop/README.md +2 -0
  4. package/legacy/README.md +4 -1
  5. package/legacy/agent/858de7a1ad0b/src/app.ts +289 -0
  6. package/legacy/agent/858de7a1ad0b/src/routes.ts +145 -0
  7. package/legacy/agent/b96ba6e3598a/src/app.ts +285 -0
  8. package/legacy/agent/b96ba6e3598a/src/routes.ts +145 -0
  9. package/legacy/agent/ecbc84f1c1e5/src/app.ts +291 -0
  10. package/legacy/agent/ecbc84f1c1e5/src/routes.ts +145 -0
  11. package/legacy/blank/2447ac6d5d24/src/app.ts +61 -0
  12. package/legacy/blank/387f7e25aaeb/src/app.ts +81 -0
  13. package/legacy/blank/5b9f10e1ba5e/src/app.ts +61 -0
  14. package/legacy/blank/5f6dd8910c50/src/app.ts +152 -0
  15. package/legacy/blank/5f6dd8910c50/src/routes.ts +75 -0
  16. package/legacy/blank/a7e8f539ffee/src/app.ts +160 -0
  17. package/legacy/blank/a7e8f539ffee/src/routes.ts +75 -0
  18. package/legacy/blank/bdb9735e013b/src/app.ts +61 -0
  19. package/legacy/blank/c3be1d735b1a/src/app.ts +61 -0
  20. package/legacy/blank/c3be1d735b1a/src/routes.ts +18 -0
  21. package/legacy/blank/e04303a7f8dd/src/app.ts +61 -0
  22. package/legacy/chat/5120c4a9fa61/src/app.ts +157 -0
  23. package/legacy/chat/5120c4a9fa61/src/routes.ts +29 -0
  24. package/legacy/chat/d4464dfb3b04/src/app.ts +165 -0
  25. package/legacy/chat/d4464dfb3b04/src/routes.ts +29 -0
  26. package/legacy/frozen.json +7 -0
  27. package/legacy/layouts.json +135 -45
  28. package/legacy/nodejs/9d59115118c7/src/app.ts +150 -0
  29. package/legacy/nodejs/9d59115118c7/src/routes.ts +59 -0
  30. package/legacy/nodejs/a6a340b4d3a6/src/app.ts +158 -0
  31. package/legacy/nodejs/a6a340b4d3a6/src/routes.ts +59 -0
  32. package/legacy/notes/fb43a1c5f3b4/src/app.ts +165 -0
  33. package/legacy/notes/fb43a1c5f3b4/src/routes.ts +21 -0
  34. package/legacy/notes/fc3fe7c749d2/src/app.ts +157 -0
  35. package/legacy/notes/fc3fe7c749d2/src/routes.ts +21 -0
  36. package/legacy/todo/164274ffcf7f/src/app.ts +145 -0
  37. package/legacy/todo/164274ffcf7f/src/todo.ts +137 -0
  38. package/lib/cordis/door.d.ts +10 -1
  39. package/lib/cordis/door.js +11 -4
  40. package/package.json +1 -1
  41. package/plugins/acryl-door/src/host/door.ts +17 -5
  42. package/plugins/builder/index.ts +1 -1
  43. package/plugins/builder/plugin.json +1 -1
  44. package/plugins/extensions/index.ts +57 -2
  45. package/plugins/extensions/routes.ts +92 -0
  46. package/plugins/home/index.ts +3 -1
  47. package/plugins/plugin-share/README.md +16 -1
  48. package/plugins/plugin-share/index.ts +130 -16
  49. package/plugins/plugin-share/plugin.json +1 -1
  50. package/templates/agent/src/app.ts +2 -0
  51. package/templates/blank/blend.yaml +3 -0
  52. package/templates/blank/src/app.ts +8 -0
  53. package/templates/blank/template.json +1 -0
  54. package/templates/chat/blend.yaml +3 -0
  55. package/templates/chat/src/app.ts +8 -0
  56. package/templates/chat/template.json +1 -0
  57. package/templates/nodejs/blend.yaml +3 -0
  58. package/templates/nodejs/src/app.ts +8 -0
  59. package/templates/nodejs/template.json +1 -0
  60. package/templates/notes/blend.yaml +3 -0
  61. package/templates/notes/src/app.ts +8 -0
  62. package/templates/notes/template.json +1 -0
  63. /package/legacy/blank/{d6d371c9a65e → 2447ac6d5d24}/src/routes.ts +0 -0
  64. /package/legacy/blank/{5a91c558af22 → 387f7e25aaeb}/src/routes.ts +0 -0
  65. /package/legacy/blank/{b2cbb759c889 → 5b9f10e1ba5e}/src/routes.ts +0 -0
  66. /package/legacy/blank/{9c650cf52bf3 → bdb9735e013b}/src/routes.ts +0 -0
  67. /package/legacy/blank/{aea589283445 → e04303a7f8dd}/src/routes.ts +0 -0
  68. /package/plugins/{builder → plugin-share}/package.ts +0 -0
@@ -50,7 +50,9 @@ export function createHome(): Home {
50
50
  `a widget id is a plugin name (lowercase letters, digits and dashes), not "${widget.id}"`,
51
51
  )
52
52
  if (registered.has(widget.id))
53
- throw new Error(`the home page already has a widget "${widget.id}"`)
53
+ throw new Error(
54
+ `the home page already has a widget "${widget.id}", the plugin ${widget.id}'s; a widget's id is the name of the plugin that puts it there`,
55
+ )
54
56
  registered.set(widget.id, widget)
55
57
  return () => {
56
58
  if (registered.get(widget.id) === widget) registered.delete(widget.id)
@@ -14,6 +14,21 @@ The app must be running (`npm run dev`), because the pull installs into it live.
14
14
 
15
15
  The app remembers where each pulled plugin came from, in the `plugin-share` row of `blend.yaml`. A copy of the app fetches the same bytes at its next start.
16
16
 
17
+ A plugin whose route or home widget another plugin already has is refused, and the message names that plugin. A different copy of a plugin with the same name is refused too, and the message says where the copy in the app came from.
18
+
19
+ ## List and remove
20
+
21
+ ```
22
+ npm run acryl -- plugin list
23
+ npm run acryl -- plugin remove <name>
24
+ ```
25
+
26
+ The list names each plugin installed while the app runs, its version and state. A pulled one also shows the source it came from and its author, the GitHub account that released it.
27
+
28
+ Remove takes the plugin out of the running app and forgets its record. Its folder goes too when it still holds the release exactly as pulled; the pull line brings it back. A folder with someone's work in it stays, and the answer says so: the app's own plugin, or a pulled one changed since. The app then starts it again at its next start unless you delete the folder.
29
+
30
+ Both work in the frame as well.
31
+
17
32
  ## Publish
18
33
 
19
34
  ```
@@ -24,7 +39,7 @@ It prints the plan: account, repository, version, visibility and license. Only a
24
39
 
25
40
  ## An app made by an older version
26
41
 
27
- Every new app has this plugin. `upgrade` does not add it to an older app, so add it and mount it by hand:
42
+ Every new app has this plugin, except one made from the todo template, which installs nothing live. `upgrade` does not add it to an older app, so add it and mount it by hand:
28
43
 
29
44
  ```
30
45
  npx acrylblends add plugin-share
@@ -34,10 +34,10 @@ import {
34
34
  sharePlugin,
35
35
  storeRootOf,
36
36
  } from '@webboxes/plugin-hub'
37
- import { packagePlugin } from '../builder/package.ts'
38
- import { NAME } from '../extensions/index.ts'
37
+ import { type ExtensionInfo, NAME } from '../extensions/index.ts'
39
38
  import type {} from '../instance/index.ts'
40
39
  import { LOCK_FILE, writeLock } from '../manifest/lock.ts'
40
+ import { packagePlugin } from './package.ts'
41
41
  import {
42
42
  type PublishOptions,
43
43
  type PublishPlan,
@@ -94,12 +94,42 @@ export type PullResult =
94
94
  }
95
95
  | PullRefused
96
96
 
97
+ /** One plugin installed while the app runs, as a person reads the list: what it is and, for one pulled, where it came from and who released it. */
98
+ /** What extensions says about an installed plugin (its state, the services a pending one waits for, ...), and where a pulled one came from. */
99
+ export interface SharedPluginInfo extends ExtensionInfo {
100
+ /** The source it was pulled from (`github:<owner>/<repo>@<version>` or an address); absent for one made in this app or pulled from a local file. */
101
+ readonly pulledFrom?: string
102
+ /** Who released it: the account of a `github:` source or of a release address on github.com. */
103
+ readonly author?: string
104
+ }
105
+
106
+ /** What removing a plugin did with its folder in `plugins/`. */
107
+ export type RemovedFolder =
108
+ /** It held the release as pulled, so nothing of anyone's is lost: the pull line brings it back. */
109
+ | 'deleted'
110
+ /** It is this app's own work, or a pulled plugin changed after the pull: it stays, and the app starts it again at its next start. */
111
+ | 'kept'
112
+ | 'none'
113
+
114
+ export type SharedRemoveResult =
115
+ | {
116
+ readonly ok: true
117
+ readonly name: string
118
+ readonly folder: RemovedFolder
119
+ readonly note?: string
120
+ }
121
+ | { readonly ok: false; readonly name: string; readonly errors: readonly string[] }
122
+
97
123
  declare module '@deepseek-ai/cordis' {
98
124
  interface Context {
99
125
  pluginShare: PluginShare
100
126
  }
101
127
  }
102
128
 
129
+ /** The account that released a source: the owner of a `github:<owner>/<repo>@<version>` source or of a release address on github.com. */
130
+ const authorOf = (source: string): string | undefined =>
131
+ (/^github:([^/]+)\//u.exec(source) ?? /^https:\/\/github\.com\/([^/]+)\//u.exec(source))?.[1]
132
+
103
133
  /** The license a new public plugin is offered under; the plan shows it before the person says yes. */
104
134
  const DEFAULT_LICENSE = 'MIT'
105
135
  const LICENSE_ID = /^[A-Za-z0-9][A-Za-z0-9.+-]*$/u
@@ -149,15 +179,26 @@ export class PluginShare extends Service {
149
179
  private readonly ports: PublishPorts
150
180
  /** One at a time: two pulls of one name must not race for its folder, nor two publishes for its package. */
151
181
  private queue: Promise<unknown> = Promise.resolve()
182
+ /** The content digest of each plugin pulled while the app runs, a local file's too, which `blend.yaml` does not remember. */
183
+ private readonly live = new Map<string, string>()
184
+
185
+ /**
186
+ * The context the service was constructed with, in the app's realm. Inside a service method `this.ctx` is Cordis's shadow context of the caller: in the
187
+ * frame that is the host's door, which does not see the app's own services (`home`), so a plugin needing them was refused. As `extensions` does.
188
+ */
189
+ private readonly own: Context
152
190
 
153
191
  constructor(ctx: Context, config: PluginShareConfig) {
154
192
  super(ctx, 'pluginShare')
193
+ this.own = ctx
155
194
  this.dir = resolve(config.dir ?? join(ctx.appInstance.root, 'plugins'))
156
195
  this.releases = config.releases ?? githubReleasesOverHttps()
157
196
  this.ports = config.publish ?? { host: githubCli(), git: gitCli() }
158
197
  // Removing a plugin removes its record. Listened to here, not asked of each caller: every way of removing it goes through extensions.
159
198
  ctx.on('extensions/changed', (name, change) => {
160
- if (change === 'removed') this.remember(name, undefined)
199
+ if (change !== 'removed') return
200
+ this.live.delete(name)
201
+ this.remember(name, undefined)
161
202
  })
162
203
  // Not awaited by the app's start: a person offline still gets their app, and the warning says what is missing.
163
204
  this.restored = this.inTurn(() => this.restore())
@@ -168,6 +209,37 @@ export class PluginShare extends Service {
168
209
  return this.inTurn(() => this.pullNow(source))
169
210
  }
170
211
 
212
+ /**
213
+ * The content digest of a pulled plugin as it was installed: set before its folder appears, so whoever sees the folder change can tell the release as
214
+ * pulled from an edit (the frame then does not load the app again for it). A pull from a local file has no record in `blend.yaml`; this knows it too.
215
+ */
216
+ pulledDigest(name: string): string | undefined {
217
+ return this.live.get(name) ?? this.recorded(name)?.digest
218
+ }
219
+
220
+ /** The plugins installed while the app runs, each pulled one with its source and author. */
221
+ list(): SharedPluginInfo[] {
222
+ const file = join(this.own.appInstance.root, 'blend.yaml')
223
+ const records = existsSync(file) ? pulledRecords(readFileSync(file, 'utf8')) : {}
224
+ return this.own.extensions.list().map((info) => {
225
+ const record = records[info.name]
226
+ const author = record === undefined ? undefined : authorOf(record.source)
227
+ return {
228
+ ...info,
229
+ ...(record === undefined ? {} : { pulledFrom: record.source }),
230
+ ...(author === undefined ? {} : { author }),
231
+ }
232
+ })
233
+ }
234
+
235
+ /**
236
+ * Take a plugin out of the running app and forget its record. A pulled plugin's folder is deleted when it still holds the release as pulled; a
237
+ * folder with someone's work in it (this app's own plugin, or a pulled one changed since) stays, and the answer says so.
238
+ */
239
+ remove(name: string): Promise<SharedRemoveResult> {
240
+ return this.inTurn(() => this.removeNow(name))
241
+ }
242
+
171
243
  private inTurn<T>(step: () => Promise<T>): Promise<T> {
172
244
  const run = this.queue.then(step)
173
245
  this.queue = run.catch(() => undefined)
@@ -199,7 +271,7 @@ export class PluginShare extends Service {
199
271
  'look at the new plan and say yes again',
200
272
  )
201
273
  }
202
- const { home } = this.ctx.appInstance
274
+ const { home } = this.own.appInstance
203
275
  try {
204
276
  const shared = await sharePlugin(
205
277
  join(this.packages, name),
@@ -233,7 +305,7 @@ export class PluginShare extends Service {
233
305
 
234
306
  /** Write one plugin's record into `blend.yaml`, and the lock when the app has one, so the two still match. @returns whether the app holds the record. */
235
307
  private remember(name: string, record: PulledRecord | undefined): boolean {
236
- const { root } = this.ctx.appInstance
308
+ const { root } = this.own.appInstance
237
309
  const file = join(root, 'blend.yaml')
238
310
  if (!existsSync(file)) return false
239
311
  const text = readFileSync(file, 'utf8')
@@ -246,9 +318,35 @@ export class PluginShare extends Service {
246
318
  return record === undefined || pulledRecords(next)[name] !== undefined
247
319
  }
248
320
 
321
+ private async removeNow(name: string): Promise<SharedRemoveResult> {
322
+ // Read before the removal, which forgets both the record and the live digest.
323
+ const pulled = this.pulledDigest(name)
324
+ const removed = await this.own.extensions.remove(name)
325
+ if (!removed.ok) return removed
326
+ const folder = join(this.dir, name)
327
+ if (!existsSync(folder)) return { ok: true, name, folder: 'none' }
328
+ if (pulled !== undefined && digestFolder(folder) === pulled) {
329
+ rmSync(folder, { recursive: true, force: true })
330
+ return { ok: true, name, folder: 'deleted' }
331
+ }
332
+ const why = pulled === undefined ? "is this app's own plugin" : 'was changed after the pull'
333
+ return {
334
+ ok: true,
335
+ name,
336
+ folder: 'kept',
337
+ note: `plugins/${name} ${why}, so it stays and the app starts it again at its next start; delete the folder to drop it for good`,
338
+ }
339
+ }
340
+
341
+ /** What `blend.yaml` remembers about one pulled plugin now. */
342
+ private recorded(name: string): PulledRecord | undefined {
343
+ const file = join(this.own.appInstance.root, 'blend.yaml')
344
+ return existsSync(file) ? pulledRecords(readFileSync(file, 'utf8'))[name] : undefined
345
+ }
346
+
249
347
  /** Fetch the remembered plugins whose folder is gone, exactly as recorded: other bytes are refused, and the record stays for the person to decide. */
250
348
  private async restore(): Promise<readonly RestoreOutcome[]> {
251
- const file = join(this.ctx.appInstance.root, 'blend.yaml')
349
+ const file = join(this.own.appInstance.root, 'blend.yaml')
252
350
  if (!existsSync(file)) return []
253
351
  const outcomes: RestoreOutcome[] = []
254
352
  for (const [name, record] of Object.entries(pulledRecords(readFileSync(file, 'utf8')))) {
@@ -271,7 +369,7 @@ export class PluginShare extends Service {
271
369
  }
272
370
 
273
371
  private get packages(): string {
274
- return join(this.ctx.appInstance.home, 'plugin-packages')
372
+ return join(this.own.appInstance.home, 'plugin-packages')
275
373
  }
276
374
 
277
375
  private async planNow(
@@ -338,7 +436,7 @@ export class PluginShare extends Service {
338
436
  }
339
437
 
340
438
  private async pullNow(input: string, expectIntegrity?: string): Promise<PullResult> {
341
- const { home, root } = this.ctx.appInstance
439
+ const { home, root } = this.own.appInstance
342
440
  let read: ReturnType<typeof readSource>
343
441
  try {
344
442
  read = readSource(input, root)
@@ -376,7 +474,7 @@ export class PluginShare extends Service {
376
474
  const refused = pullRefusal(pulled.manifest, files, {
377
475
  digest: read.digest,
378
476
  nameFits: (name) => NAME.test(name),
379
- hasService: (service) => this.ctx.get(service as never) !== undefined,
477
+ hasService: (service) => this.own.get(service as never) !== undefined,
380
478
  })
381
479
  if (refused !== undefined) return refused
382
480
  return await this.install(
@@ -411,20 +509,36 @@ export class PluginShare extends Service {
411
509
  const digest = digestFolder(incoming)
412
510
  // The folder may be there already: the same bytes are already pulled, anything else is someone's work.
413
511
  const wrote = !existsSync(target)
414
- if (wrote) renameSync(incoming, target)
415
- else if (digestFolder(target) !== digest) {
512
+ const before = this.recorded(name)
513
+ const held = wrote ? undefined : digestFolder(target)
514
+ if (held !== undefined && held !== digest) {
515
+ const holder = before === undefined ? 'made in this app' : `pulled from ${before.source}`
516
+ // Only an untouched pulled copy goes with `plugin remove`; anything else is someone's work, which the person moves themselves.
517
+ const next =
518
+ before !== undefined && held === before.digest
519
+ ? `keep it, or remove it (npm run acryl -- plugin remove ${name}) and pull again`
520
+ : `keep it, or move plugins/${name} out of plugins/ and pull again`
416
521
  return refuse(
417
522
  'folder-exists',
418
- `plugins/${name} holds a different copy of this plugin`,
419
- `keep it, or remove plugins/${name} and pull again`,
523
+ `plugins/${name} holds a different copy of this plugin, ${holder}`,
524
+ next,
420
525
  )
421
526
  }
527
+ const liveBefore = this.live.get(name)
528
+ this.live.set(name, digest)
529
+ if (wrote) renameSync(incoming, target)
530
+ // Remembered as the folder appears, before it is installed, so whoever sees the folder can tell it is these released bytes (the frame then does
531
+ // not load the app again for a plugin the app installs live); a pull that does not end running puts back what was remembered before.
532
+ const remembered = source !== undefined && this.remember(name, { source, integrity, digest })
422
533
  const undo = () => {
423
534
  if (wrote) rmSync(target, { recursive: true, force: true })
535
+ if (remembered) this.remember(name, before)
536
+ if (liveBefore === undefined) this.live.delete(name)
537
+ else this.live.set(name, liveBefore)
424
538
  }
425
539
  let result: Awaited<ReturnType<Context['extensions']['install']>>
426
540
  try {
427
- result = await this.ctx.extensions.install(target)
541
+ result = await this.own.extensions.install(target)
428
542
  } catch (cause) {
429
543
  undo()
430
544
  return refuse(
@@ -438,7 +552,7 @@ export class PluginShare extends Service {
438
552
  return refuse('install', result.errors.join('; '), result.next)
439
553
  }
440
554
  if (result.state === 'pending') {
441
- await this.ctx.extensions.remove(name)
555
+ await this.own.extensions.remove(name)
442
556
  undo()
443
557
  return missingServices(result.missing)
444
558
  }
@@ -450,7 +564,7 @@ export class PluginShare extends Service {
450
564
  digest,
451
565
  action: result.action,
452
566
  state: result.state,
453
- remembered: source !== undefined && this.remember(name, { source, integrity, digest }),
567
+ remembered,
454
568
  }
455
569
  } finally {
456
570
  rmSync(incoming, { recursive: true, force: true })
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "plugin-share",
3
3
  "description": "Share plugins between people. Pull one someone released into the running app: fetched without running anything, checked by digest and for every OS, then installed live. Publish one, only as a plan a person confirmed.",
4
- "requires": ["instance", "extensions", "builder", "manifest"],
4
+ "requires": ["instance", "extensions", "manifest"],
5
5
  "inject": ["extensions", "appInstance"],
6
6
  "provides": ["pluginShare"],
7
7
  "dependencies": {
@@ -216,6 +216,8 @@ export async function composeInto(
216
216
  ...(legacy ? [{ dir: legacyDir, trust: 'install' as const }] : []),
217
217
  ],
218
218
  rows: ownRows,
219
+ // Messages name a route as the plugin wrote it, not under the frame's canvas prefix.
220
+ ...(options.host === true ? { routePrefix: FRAME_MOUNT_PREFIX } : {}),
219
221
  },
220
222
  builder: { dir: extensionsDir },
221
223
  'plugin-share': { dir: extensionsDir },
@@ -25,6 +25,9 @@ spec:
25
25
  # a row is never started twice. Read-only when NODE_ENV=production.
26
26
  - id: extensions
27
27
  name: extensions
28
+ # Sharing: `npm run acryl -- plugin pull github:<owner>/<repo>@<version>` installs a plugin someone released (development only).
29
+ - id: plugin-share
30
+ name: plugin-share
28
31
  - id: server
29
32
  name: acryl-node-server
30
33
  config:
@@ -15,6 +15,7 @@ import {
15
15
  loadManifest,
16
16
  type ParameterValue,
17
17
  } from '../plugins/manifest/index.ts'
18
+ import { pluginShare } from '../plugins/plugin-share/index.ts'
18
19
  import { doorEnabled } from './door.ts'
19
20
  import { FRAME_MOUNT_PREFIX, routes } from './routes.ts'
20
21
 
@@ -25,6 +26,7 @@ export const registry = {
25
26
  'acryl-door': door,
26
27
  home,
27
28
  extensions,
29
+ 'plugin-share': pluginShare,
28
30
  routes,
29
31
  }
30
32
 
@@ -106,6 +108,8 @@ export async function composeInto(
106
108
  const leaveOut = new Set<string>(options.host === true ? HOST_ROWS : [])
107
109
  if (!own) leaveOut.add('acryl-node-server').add('acryl-door')
108
110
  if (!doorEnabled()) leaveOut.add('acryl-door')
111
+ // A pull runs a stranger's code and a publish leaves the machine: development only, like the door.
112
+ if (process.env.NODE_ENV === 'production') leaveOut.add('plugin-share')
109
113
  const manifest = without(read, ...leaveOut)
110
114
  // The rows that name one of the app's own plugin folders (a feature: plugins/<name>/ and its row) rather than a registry import. Each one that runs
111
115
  // is imported here; all of them, off or on, belong to their row, so the extensions row never starts them a second time.
@@ -129,7 +133,11 @@ export async function composeInto(
129
133
  ...(process.env.NODE_ENV === 'production' ? { readOnly: true } : {}),
130
134
  sources: [{ dir: PLUGINS_DIR, trust: 'install' }],
131
135
  rows: ownRows,
136
+ // Messages name a route as the plugin wrote it, not under the frame's canvas prefix.
137
+ ...(prefix === undefined ? {} : { routePrefix: prefix }),
132
138
  },
139
+ // Pulled plugins go where the extensions row installs from.
140
+ 'plugin-share': { dir: PLUGINS_DIR },
133
141
  // Only the app's own server listens; the frame host owns the server and its port.
134
142
  ...(own ? { server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 } } : {}),
135
143
  },
@@ -7,6 +7,7 @@
7
7
  "save",
8
8
  "home",
9
9
  "extensions",
10
+ "plugin-share",
10
11
  "acryl-door",
11
12
  "acryl-web-server",
12
13
  "acryl-page"
@@ -24,6 +24,9 @@ spec:
24
24
  # Read-only when NODE_ENV=production.
25
25
  - id: extensions
26
26
  name: extensions
27
+ # Sharing: `npm run acryl -- plugin pull github:<owner>/<repo>@<version>` installs a plugin someone released (development only).
28
+ - id: plugin-share
29
+ name: plugin-share
27
30
  - id: server
28
31
  name: acryl-node-server
29
32
  config:
@@ -15,6 +15,7 @@ import {
15
15
  loadManifest,
16
16
  type ParameterValue,
17
17
  } from '../plugins/manifest/index.ts'
18
+ import { pluginShare } from '../plugins/plugin-share/index.ts'
18
19
  import { doorEnabled } from './door.ts'
19
20
  import { FRAME_MOUNT_PREFIX, routes } from './routes.ts'
20
21
 
@@ -25,6 +26,7 @@ export const registry = {
25
26
  'acryl-node-server': nodeServer,
26
27
  'acryl-door': door,
27
28
  extensions,
29
+ 'plugin-share': pluginShare,
28
30
  routes,
29
31
  }
30
32
 
@@ -106,6 +108,8 @@ export async function composeInto(
106
108
  const leaveOut = new Set<string>(options.host === true ? HOST_ROWS : [])
107
109
  if (!own) leaveOut.add('acryl-node-server').add('acryl-door')
108
110
  if (!doorEnabled()) leaveOut.add('acryl-door')
111
+ // A pull runs a stranger's code and a publish leaves the machine: development only, like the door.
112
+ if (process.env.NODE_ENV === 'production') leaveOut.add('plugin-share')
109
113
  const manifest = without(read, ...leaveOut)
110
114
  // The rows that name one of the app's own plugin folders (a feature: plugins/<name>/ and its row) rather than a registry import. Each one that runs
111
115
  // is imported here; all of them, off or on, belong to their row, so the extensions row never starts them a second time.
@@ -134,7 +138,11 @@ export async function composeInto(
134
138
  ...(process.env.NODE_ENV === 'production' ? { readOnly: true } : {}),
135
139
  sources: [{ dir: PLUGINS_DIR, trust: 'install' }],
136
140
  rows: ownRows,
141
+ // Messages name a route as the plugin wrote it, not under the frame's canvas prefix.
142
+ ...(prefix === undefined ? {} : { routePrefix: prefix }),
137
143
  },
144
+ // Pulled plugins go where the extensions row installs from.
145
+ 'plugin-share': { dir: PLUGINS_DIR },
138
146
  // Only the app's own server listens; the frame host owns the server and its port.
139
147
  ...(own ? { server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 } } : {}),
140
148
  },
@@ -7,6 +7,7 @@
7
7
  "manifest",
8
8
  "save",
9
9
  "extensions",
10
+ "plugin-share",
10
11
  "acryl-door",
11
12
  "acryl-web-server",
12
13
  "acryl-page"
@@ -21,6 +21,9 @@ spec:
21
21
  # Read-only when NODE_ENV=production.
22
22
  - id: extensions
23
23
  name: extensions
24
+ # Sharing: `npm run acryl -- plugin pull github:<owner>/<repo>@<version>` installs a plugin someone released (development only).
25
+ - id: plugin-share
26
+ name: plugin-share
24
27
  - id: server
25
28
  name: acryl-node-server
26
29
  config:
@@ -14,6 +14,7 @@ import {
14
14
  loadManifest,
15
15
  type ParameterValue,
16
16
  } from '../plugins/manifest/index.ts'
17
+ import { pluginShare } from '../plugins/plugin-share/index.ts'
17
18
  import { doorEnabled } from './door.ts'
18
19
  import { FRAME_MOUNT_PREFIX, routes } from './routes.ts'
19
20
 
@@ -23,6 +24,7 @@ export const registry = {
23
24
  'acryl-node-server': nodeServer,
24
25
  'acryl-door': door,
25
26
  extensions,
27
+ 'plugin-share': pluginShare,
26
28
  routes,
27
29
  }
28
30
 
@@ -104,6 +106,8 @@ export async function composeInto(
104
106
  const leaveOut = new Set<string>(options.host === true ? HOST_ROWS : [])
105
107
  if (!own) leaveOut.add('acryl-node-server').add('acryl-door')
106
108
  if (!doorEnabled()) leaveOut.add('acryl-door')
109
+ // A pull runs a stranger's code and a publish leaves the machine: development only, like the door.
110
+ if (process.env.NODE_ENV === 'production') leaveOut.add('plugin-share')
107
111
  const manifest = without(read, ...leaveOut)
108
112
  // The rows that name one of the app's own plugin folders (a feature: plugins/<name>/ and its row) rather than a registry import. Each one that runs
109
113
  // is imported here; all of them, off or on, belong to their row, so the extensions row never starts them a second time.
@@ -127,7 +131,11 @@ export async function composeInto(
127
131
  ...(process.env.NODE_ENV === 'production' ? { readOnly: true } : {}),
128
132
  sources: [{ dir: PLUGINS_DIR, trust: 'install' }],
129
133
  rows: ownRows,
134
+ // Messages name a route as the plugin wrote it, not under the frame's canvas prefix.
135
+ ...(prefix === undefined ? {} : { routePrefix: prefix }),
130
136
  },
137
+ // Pulled plugins go where the extensions row installs from.
138
+ 'plugin-share': { dir: PLUGINS_DIR },
131
139
  // Only the app's own server listens; the frame host owns the server and its port.
132
140
  ...(own ? { server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 } } : {}),
133
141
  },
@@ -6,6 +6,7 @@
6
6
  "manifest",
7
7
  "save",
8
8
  "extensions",
9
+ "plugin-share",
9
10
  "acryl-door",
10
11
  "acryl-web-server",
11
12
  "acryl-page"
@@ -24,6 +24,9 @@ spec:
24
24
  # Read-only when NODE_ENV=production.
25
25
  - id: extensions
26
26
  name: extensions
27
+ # Sharing: `npm run acryl -- plugin pull github:<owner>/<repo>@<version>` installs a plugin someone released (development only).
28
+ - id: plugin-share
29
+ name: plugin-share
27
30
  - id: server
28
31
  name: acryl-node-server
29
32
  config:
@@ -15,6 +15,7 @@ import {
15
15
  loadManifest,
16
16
  type ParameterValue,
17
17
  } from '../plugins/manifest/index.ts'
18
+ import { pluginShare } from '../plugins/plugin-share/index.ts'
18
19
  import { doorEnabled } from './door.ts'
19
20
  import { FRAME_MOUNT_PREFIX, routes } from './routes.ts'
20
21
 
@@ -25,6 +26,7 @@ export const registry = {
25
26
  'acryl-node-server': nodeServer,
26
27
  'acryl-door': door,
27
28
  extensions,
29
+ 'plugin-share': pluginShare,
28
30
  routes,
29
31
  }
30
32
 
@@ -106,6 +108,8 @@ export async function composeInto(
106
108
  const leaveOut = new Set<string>(options.host === true ? HOST_ROWS : [])
107
109
  if (!own) leaveOut.add('acryl-node-server').add('acryl-door')
108
110
  if (!doorEnabled()) leaveOut.add('acryl-door')
111
+ // A pull runs a stranger's code and a publish leaves the machine: development only, like the door.
112
+ if (process.env.NODE_ENV === 'production') leaveOut.add('plugin-share')
109
113
  const manifest = without(read, ...leaveOut)
110
114
  // The rows that name one of the app's own plugin folders (a feature: plugins/<name>/ and its row) rather than a registry import. Each one that runs
111
115
  // is imported here; all of them, off or on, belong to their row, so the extensions row never starts them a second time.
@@ -134,7 +138,11 @@ export async function composeInto(
134
138
  ...(process.env.NODE_ENV === 'production' ? { readOnly: true } : {}),
135
139
  sources: [{ dir: PLUGINS_DIR, trust: 'install' }],
136
140
  rows: ownRows,
141
+ // Messages name a route as the plugin wrote it, not under the frame's canvas prefix.
142
+ ...(prefix === undefined ? {} : { routePrefix: prefix }),
137
143
  },
144
+ // Pulled plugins go where the extensions row installs from.
145
+ 'plugin-share': { dir: PLUGINS_DIR },
138
146
  // Only the app's own server listens; the frame host owns the server and its port.
139
147
  ...(own ? { server: { port: chosen.port.start, scan: chosen.port.scan ? 20 : 0 } } : {}),
140
148
  },
@@ -7,6 +7,7 @@
7
7
  "manifest",
8
8
  "save",
9
9
  "extensions",
10
+ "plugin-share",
10
11
  "acryl-door",
11
12
  "acryl-web-server",
12
13
  "acryl-page"
File without changes