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
@@ -0,0 +1,70 @@
1
+ /**
2
+ * What an app remembers about the plugins it pulled: their source and digests, in the `plugin-share` row's config of `blend.yaml`. A row's config is
3
+ * resolved into `blend.lock.json` too, so the lock records them with no new format, and a copy of the app can fetch exactly the same bytes. Pure text
4
+ * to text; comments and the person's formatting stay.
5
+ */
6
+ import { isMap, isSeq, parseDocument } from 'yaml'
7
+
8
+ export const ROW = 'plugin-share'
9
+
10
+ export interface PulledRecord {
11
+ /** `github:<owner>/<repo>@<version>` or an https address: something another machine can fetch. */
12
+ readonly source: string
13
+ /** The sha512 of the release file. */
14
+ readonly integrity: string
15
+ /** The sha256 of the plugin's content. */
16
+ readonly digest: string
17
+ }
18
+
19
+ const isRecord = (value: unknown): value is PulledRecord => {
20
+ const record = value as Partial<Record<keyof PulledRecord, unknown>> | null
21
+ return (
22
+ typeof record?.source === 'string' &&
23
+ typeof record.integrity === 'string' &&
24
+ typeof record.digest === 'string'
25
+ )
26
+ }
27
+
28
+ /** The plugins this definition remembers pulling. */
29
+ export function pulledRecords(text: string): Readonly<Record<string, PulledRecord>> {
30
+ const rows = (parseDocument(text).toJS() as { spec?: { rows?: unknown } } | null)?.spec?.rows
31
+ if (!Array.isArray(rows)) return {}
32
+ const pulled = (
33
+ rows.find((row: { name?: unknown }) => row?.name === ROW) as
34
+ | { config?: { pulled?: unknown } }
35
+ | undefined
36
+ )?.config?.pulled
37
+ if (typeof pulled !== 'object' || pulled === null) return {}
38
+ return Object.fromEntries(
39
+ Object.entries(pulled).filter(([, record]) => isRecord(record)),
40
+ ) as Record<string, PulledRecord>
41
+ }
42
+
43
+ /**
44
+ * The definition with one plugin's record written, or taken out when `record` is undefined. Unchanged when it has no `plugin-share` row: an app
45
+ * composed without the row has nowhere to remember.
46
+ */
47
+ export function withPulledRecord(
48
+ text: string,
49
+ name: string,
50
+ record: PulledRecord | undefined,
51
+ ): string {
52
+ const document = parseDocument(text)
53
+ const rows = document.getIn(['spec', 'rows'])
54
+ if (!isSeq(rows)) return text
55
+ const row = rows.items.find((item) => isMap(item) && item.get('name') === ROW)
56
+ if (!isMap(row)) return text
57
+ if (record === undefined) {
58
+ if (!row.hasIn(['config', 'pulled', name])) return text
59
+ row.deleteIn(['config', 'pulled', name])
60
+ const left = row.getIn(['config', 'pulled'])
61
+ if (isMap(left) && left.items.length === 0) row.deleteIn(['config', 'pulled'])
62
+ const config = row.get('config')
63
+ if (isMap(config) && config.items.length === 0) row.delete('config')
64
+ } else {
65
+ row.setIn(['config'], row.get('config') ?? document.createNode({}))
66
+ if (!row.hasIn(['config', 'pulled'])) row.setIn(['config', 'pulled'], document.createNode({}))
67
+ row.setIn(['config', 'pulled', name], document.createNode(record))
68
+ }
69
+ return String(document)
70
+ }
@@ -0,0 +1,2 @@
1
+ # Every checkout has the same line endings, on every OS. A plugin's digest counts every byte, so a CRLF checkout would make it a different plugin.
2
+ * text=auto eol=lf
@@ -49,6 +49,8 @@ spec:
49
49
  approval: "{{parameters.approval}}"
50
50
  - id: builder
51
51
  name: builder
52
+ - id: plugin-share
53
+ name: plugin-share
52
54
  - id: save
53
55
  name: save
54
56
  # The panel: a chat button on every page in development (the app's own agent, or Codex, Claude or pi started in this folder). It starts nothing in production.
@@ -20,6 +20,7 @@ import {
20
20
  type ParameterValue,
21
21
  } from '../plugins/manifest/index.ts'
22
22
  import { ASSISTANT_PROMPT, type OpenAIConfig, openai } from '../plugins/openai/index.ts'
23
+ import { pluginShare } from '../plugins/plugin-share/index.ts'
23
24
  import { sandboxPlugin } from '../plugins/sandbox/index.ts'
24
25
  import { save } from '../plugins/save/index.ts'
25
26
  import { tasks } from '../plugins/tasks/index.ts'
@@ -36,6 +37,7 @@ export const registry = {
36
37
  'acryl-state': state,
37
38
  extensions,
38
39
  builder,
40
+ 'plugin-share': pluginShare,
39
41
  save,
40
42
  sandbox: sandboxPlugin,
41
43
  routes,
@@ -77,6 +79,7 @@ export interface App {
77
79
  const DEVELOPMENT_ROWS: Record<string, string> = {
78
80
  agent: 'POST /ask runs the agent, which spends the model key and calls its tools',
79
81
  builder: 'the agent can write plugin source into plugins/',
82
+ 'plugin-share': 'a plugin pulled from a release runs in this process',
80
83
  extensions:
81
84
  'a plugin installed live runs in this process, and POST /extensions/approve decides installs',
82
85
  }
@@ -215,6 +218,7 @@ export async function composeInto(
215
218
  rows: ownRows,
216
219
  },
217
220
  builder: { dir: extensionsDir },
221
+ 'plugin-share': { dir: extensionsDir },
218
222
  ...(assistant
219
223
  ? {
220
224
  // Every answer comes right after a look, so it says what the visitor's page shows now.
@@ -12,6 +12,7 @@
12
12
  "sandbox",
13
13
  "extensions",
14
14
  "builder",
15
+ "plugin-share",
15
16
  "agent-panel",
16
17
  "manifest",
17
18
  "save",
@@ -0,0 +1,2 @@
1
+ # Every checkout has the same line endings, on every OS. A plugin's digest counts every byte, so a CRLF checkout would make it a different plugin.
2
+ * text=auto eol=lf
@@ -0,0 +1,2 @@
1
+ # Every checkout has the same line endings, on every OS. A plugin's digest counts every byte, so a CRLF checkout would make it a different plugin.
2
+ * text=auto eol=lf
@@ -0,0 +1,2 @@
1
+ # Every checkout has the same line endings, on every OS. A plugin's digest counts every byte, so a CRLF checkout would make it a different plugin.
2
+ * text=auto eol=lf
@@ -0,0 +1,2 @@
1
+ # Every checkout has the same line endings, on every OS. A plugin's digest counts every byte, so a CRLF checkout would make it a different plugin.
2
+ * text=auto eol=lf
@@ -0,0 +1,2 @@
1
+ # Every checkout has the same line endings, on every OS. A plugin's digest counts every byte, so a CRLF checkout would make it a different plugin.
2
+ * text=auto eol=lf