acrylblends 1.3.0 → 1.3.1
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.
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/docs/framework/workshop/README.md +2 -0
- package/lib/cordis/door.d.ts +10 -1
- package/lib/cordis/door.js +11 -4
- package/package.json +1 -1
- package/plugins/acryl-door/src/host/door.ts +17 -5
- package/plugins/builder/index.ts +1 -1
- package/plugins/builder/plugin.json +1 -1
- package/plugins/extensions/index.ts +57 -2
- package/plugins/extensions/routes.ts +92 -0
- package/plugins/home/index.ts +3 -1
- package/plugins/plugin-share/README.md +16 -1
- package/plugins/plugin-share/index.ts +130 -16
- package/plugins/plugin-share/plugin.json +1 -1
- package/templates/agent/src/app.ts +2 -0
- package/templates/blank/blend.yaml +3 -0
- package/templates/blank/src/app.ts +8 -0
- package/templates/blank/template.json +1 -0
- package/templates/chat/blend.yaml +3 -0
- package/templates/chat/src/app.ts +8 -0
- package/templates/chat/template.json +1 -0
- package/templates/nodejs/blend.yaml +3 -0
- package/templates/nodejs/src/app.ts +8 -0
- package/templates/nodejs/template.json +1 -0
- package/templates/notes/blend.yaml +3 -0
- package/templates/notes/src/app.ts +8 -0
- package/templates/notes/template.json +1 -0
- /package/plugins/{builder → plugin-share}/package.ts +0 -0
|
@@ -34,10 +34,10 @@ import {
|
|
|
34
34
|
sharePlugin,
|
|
35
35
|
storeRootOf,
|
|
36
36
|
} from '@webboxes/plugin-hub'
|
|
37
|
-
import {
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
415
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
|
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", "
|
|
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
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
@@ -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
|
},
|
|
File without changes
|