buckyos 0.7.160 → 0.7.162

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/aiws_ext.mjs CHANGED
@@ -1,9 +1,11 @@
1
1
  const AIWS_COMPONENT_FILE = "component.json";
2
+ const AIWS_COMPOSITION_FILE = "composition.json";
2
3
  const AIWS_MANIFEST_FILE = "aiws-manifest.json";
3
4
  const AIWS_BUILD_STATE_FILE = ".aiws-build.json";
4
- const AIWS_MANIFEST_VERSION = 1;
5
- const AIWS_HTML_API_VERSION = 2;
5
+ const AIWS_MANIFEST_VERSION = 3;
6
+ const AIWS_HTML_API_VERSION = 3;
6
7
  const AIWS_DECLARATIVE_API_VERSION = 1;
8
+ const AIWS_TEMPLATE_FORMAT = "aiws-template/1";
7
9
  const AIWS_MAX_SOURCE_FILES = 64;
8
10
  class AiwsSourceError extends Error {
9
11
  constructor(diagnostics) {
@@ -51,30 +53,132 @@ function base64(bytes) {
51
53
  binary += String.fromCharCode(...bytes.subarray(i, i + 32768));
52
54
  return btoa(binary);
53
55
  }
54
- async function buildFiles(nfsp, files) {
56
+ async function packBuildFiles(files, upload, byRef = []) {
57
+ let total = files.reduce((n, f) => n + f.bytes.length, 0);
58
+ const uploaded = /* @__PURE__ */ new Map();
59
+ if (total > AIWS_MAX_INLINE_BYTES) {
60
+ if (!upload)
61
+ throw new AiwsSourceError([{ file: "", severity: "error", message: `a control's inline files are limited to ${AIWS_MAX_INLINE_BYTES} bytes per build` }]);
62
+ for (const f of [...files].sort((a, b) => b.bytes.length - a.bytes.length)) {
63
+ if (total <= AIWS_MAX_INLINE_BYTES)
64
+ break;
65
+ uploaded.set(f.name, await upload(f.name, f.bytes));
66
+ total -= f.bytes.length;
67
+ }
68
+ }
69
+ const out = [...byRef];
70
+ for (const f of files) {
71
+ const obj = uploaded.get(f.name);
72
+ out.push(obj ? { name: f.name, obj_id: obj } : { name: f.name, base64: base64(f.bytes) });
73
+ }
74
+ return out.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
75
+ }
76
+ async function buildFiles(nfsp, files, upload) {
55
77
  var _a;
56
- const out = [];
57
- let inline = 0;
78
+ const byRef = [];
79
+ const inline = [];
58
80
  for (const f of files) {
59
81
  if ((_a = f.obj_id) == null ? void 0 : _a.startsWith("cyfile:")) {
60
- out.push({ name: f.name, obj_id: f.obj_id });
82
+ byRef.push({ name: f.name, obj_id: f.obj_id });
61
83
  continue;
62
84
  }
63
85
  if (!f.node_id)
64
86
  throw new AiwsSourceError([{ file: f.name, severity: "error", message: "the file service did not name this file" }]);
65
- const bytes = new Uint8Array(await (await nfsp.readFile(f.node_id)).arrayBuffer());
66
- inline += bytes.length;
67
- if (inline > AIWS_MAX_INLINE_BYTES)
68
- throw new AiwsSourceError([{ file: f.name, severity: "error", message: `a control's files are limited to ${AIWS_MAX_INLINE_BYTES} bytes per build` }]);
69
- out.push({ name: f.name, base64: base64(bytes) });
87
+ inline.push({ name: f.name, bytes: new Uint8Array(await (await nfsp.readFile(f.node_id)).arrayBuffer()) });
70
88
  }
71
- return out;
89
+ return packBuildFiles(inline, upload, byRef);
72
90
  }
73
- const COMPONENT_KEYS = ["manifest_version", "ident", "kind", "title", "description", "host_api", "accepts", "allow_no_source", "default_size", "config_schema", "bindings", "entry", "assets", "catalog", "agent", "deps"];
91
+ const COMPONENT_KEYS = ["manifest_version", "ident", "kind", "title", "description", "host_api", "accepts", "allow_no_source", "default_size", "config_schema", "bindings", "entry", "assets", "catalog", "agent", "deps", "events"];
74
92
  const SCHEMA_KEYWORDS = ["type", "enum", "default", "required", "title", "description", "minimum", "maximum", "minLength", "maxLength", "properties", "items"];
75
93
  const NAME = /^[a-z0-9][a-z0-9._-]{0,63}$/;
76
94
  const DID = /^did:[a-z0-9]+:[^/#\s]+$/;
77
95
  const BINDING = /^[a-z][a-z0-9_]{0,31}$/;
96
+ const IDENT = /^did:[a-z0-9]+:[^/#\s]+#[a-z0-9][a-z0-9._-]{0,63}$/;
97
+ const V2_HINT = "needs manifest_version 2 (raise it in component.json)";
98
+ const V3_HINT = "needs manifest_version 3 (raise it in component.json)";
99
+ const EVENT_PARAM_TYPES = ["string", "number", "boolean", "object", "entity"];
100
+ const ITEM_KINDS = ["text", "value", "metric", "bar", "list", "badge"];
101
+ const ITEM_KEYS = ["kind", "text", "label", "bind", "by", "format", "limit"];
102
+ const ACTION_COMMANDS = ["open_source", "activate_editor", "open_definition"];
103
+ const INSPECTOR_KINDS = ["text", "number", "select", "boolean", "color"];
104
+ function isRecord(v) {
105
+ return !!v && typeof v === "object" && !Array.isArray(v);
106
+ }
107
+ function isSourcePath(name) {
108
+ return name.length > 0 && encoder.encode(name).length <= 255 && !name.includes("\0") && name.split("/").every((seg) => seg.length > 0 && !seg.startsWith("."));
109
+ }
110
+ function configProps(configSchema) {
111
+ const props = isRecord(configSchema) ? configSchema.properties : void 0;
112
+ return isRecord(props) ? props : {};
113
+ }
114
+ function configPlaceholders(text, at, props, error) {
115
+ for (const m of text.matchAll(/\{\{([^}]*)\}\}/g)) {
116
+ const inner = m[1].trim();
117
+ if (!inner.startsWith("config."))
118
+ error(`${at}: only {{config.<key>}} may be used here`);
119
+ else if (!(inner.slice(7) in props))
120
+ error(`${at}: {{${inner}}} names no property of config_schema`);
121
+ }
122
+ }
123
+ function eventProblems(v, out) {
124
+ if (!isRecord(v)) {
125
+ out.push("events must be an object { <name>: { title?, description?, params? } }");
126
+ return;
127
+ }
128
+ if (Object.keys(v).length > 16)
129
+ out.push("events: at most 16");
130
+ for (const [name, raw] of Object.entries(v)) {
131
+ const at = `events.${name}`;
132
+ if (!BINDING.test(name))
133
+ out.push(`${at}: an event name is [a-z][a-z0-9_]{0,31}`);
134
+ if (!isRecord(raw)) {
135
+ out.push(`${at} must be an object { title?, description?, params? }`);
136
+ continue;
137
+ }
138
+ for (const [k, x] of Object.entries(raw)) {
139
+ if (k === "title") {
140
+ if (typeof x !== "string" || x.length > 256)
141
+ out.push(`${at}.title: at most 256 characters`);
142
+ } else if (k === "description") {
143
+ if (typeof x !== "string" || x.length > 4e3)
144
+ out.push(`${at}.description: at most 4000 characters`);
145
+ } else if (k !== "params")
146
+ out.push(`${at}: unknown key ${k} (title, description, params)`);
147
+ }
148
+ if (raw.params === void 0)
149
+ continue;
150
+ if (!isRecord(raw.params)) {
151
+ out.push(`${at}.params must be an object { <param>: { type, required?, description? } }`);
152
+ continue;
153
+ }
154
+ if (Object.keys(raw.params).length > 16)
155
+ out.push(`${at}.params: at most 16`);
156
+ for (const [p, spec] of Object.entries(raw.params)) {
157
+ const pat = `${at}.params.${p}`;
158
+ if (!BINDING.test(p))
159
+ out.push(`${pat}: a param name is [a-z][a-z0-9_]{0,31}`);
160
+ if (!isRecord(spec)) {
161
+ out.push(`${pat} must be an object { type, required?, description? }`);
162
+ continue;
163
+ }
164
+ for (const [k, x] of Object.entries(spec)) {
165
+ if (k === "type") {
166
+ if (typeof x !== "string" || !EVENT_PARAM_TYPES.includes(x))
167
+ out.push(`${pat}.type: ${EVENT_PARAM_TYPES.join(", ")}`);
168
+ } else if (k === "required") {
169
+ if (typeof x !== "boolean")
170
+ out.push(`${pat}.required must be a boolean`);
171
+ } else if (k === "description") {
172
+ if (typeof x !== "string" || x.length > 1e3)
173
+ out.push(`${pat}.description: at most 1000 characters`);
174
+ } else
175
+ out.push(`${pat}: unknown key ${k} (type, required, description)`);
176
+ }
177
+ if (spec.type === void 0)
178
+ out.push(`${pat}.type required`);
179
+ }
180
+ }
181
+ }
78
182
  function schemaProblems(v, path, out) {
79
183
  if (!v || typeof v !== "object" || Array.isArray(v)) {
80
184
  out.push(`${path}: a schema is an object`);
@@ -92,7 +196,347 @@ function schemaProblems(v, path, out) {
92
196
  schemaProblems(x, `${path}/items`, out);
93
197
  }
94
198
  }
95
- function checkComponent(text, fileNames) {
199
+ function checkDeclarative(text, context = {}, file = "declarative.json") {
200
+ const out = [];
201
+ const error = (message, code) => out.push({ file, severity: "error", message, ...code ? { code } : {} });
202
+ const warn = (message) => out.push({ file, severity: "warning", message });
203
+ let o;
204
+ try {
205
+ const parsed = JSON.parse(text);
206
+ if (!isRecord(parsed)) {
207
+ error("a declarative definition is a JSON object");
208
+ return out;
209
+ }
210
+ o = parsed;
211
+ } catch (e) {
212
+ error(e instanceof Error ? e.message : String(e));
213
+ return out;
214
+ }
215
+ const bindings = isRecord(context.bindings) ? context.bindings : {};
216
+ const props = configProps(context.configSchema);
217
+ const strings = (v, at) => {
218
+ if (typeof v === "string")
219
+ configPlaceholders(v, at, props, error);
220
+ else if (Array.isArray(v))
221
+ v.forEach((x, i) => strings(x, `${at}[${i}]`));
222
+ else if (isRecord(v))
223
+ for (const [k, x] of Object.entries(v))
224
+ strings(x, `${at}.${k}`);
225
+ };
226
+ const button = (it, at) => {
227
+ if (context.manifestVersion !== void 0 && context.manifestVersion < 3)
228
+ error(`${at}: a button ${V3_HINT}`);
229
+ for (const k of Object.keys(it))
230
+ if (!["kind", "label", "emit", "params"].includes(k))
231
+ warn(`${at}: key ${k} is ignored by the interpreter`);
232
+ if (typeof it.label !== "string" || !it.label)
233
+ error(`${at}.label: the button's text (required)`);
234
+ if (typeof it.emit !== "string" || !it.emit)
235
+ error(`${at}.emit: the event the button raises (required)`);
236
+ else if (context.events && !context.events.includes(it.emit))
237
+ error(`${at}.emit ${it.emit}: not an event of component.json`, "EVENT_UNDECLARED");
238
+ if (it.params !== void 0) {
239
+ if (isRecord(it.params))
240
+ strings(it.params, `${at}.params`);
241
+ else
242
+ error(`${at}.params must be an object`);
243
+ }
244
+ };
245
+ const items = (list, at) => {
246
+ if (list === void 0)
247
+ return;
248
+ if (!Array.isArray(list)) {
249
+ error(`${at} must be an array`);
250
+ return;
251
+ }
252
+ list.forEach((it, i) => {
253
+ const iat = `${at}[${i}]`;
254
+ if (!isRecord(it)) {
255
+ error(`${iat} must be an object`);
256
+ return;
257
+ }
258
+ if (it.kind === "button") {
259
+ button(it, iat);
260
+ return;
261
+ }
262
+ if (typeof it.kind !== "string" || !ITEM_KINDS.includes(it.kind))
263
+ error(`${iat}.kind ${JSON.stringify(it.kind)}: one of ${[...ITEM_KINDS, "button"].join(", ")}`);
264
+ for (const k of Object.keys(it))
265
+ if (!ITEM_KEYS.includes(k))
266
+ warn(`${iat}: key ${k} is ignored by the interpreter`);
267
+ if (it.format !== void 0 && !["text", "number", "percent", "currency"].includes(it.format))
268
+ error(`${iat}.format ${JSON.stringify(it.format)}: text, number, percent or currency`);
269
+ if (typeof it.bind === "string" && it.bind.includes(".")) {
270
+ const prefix = it.bind.slice(0, it.bind.indexOf("."));
271
+ if (prefix !== "props" && !(prefix in bindings))
272
+ warn(`${iat}.bind ${JSON.stringify(it.bind)}: ${prefix} is not a declared binding, so it reads the field ${JSON.stringify(it.bind)} of the main source`);
273
+ }
274
+ });
275
+ };
276
+ for (const [k, v] of Object.entries(o)) {
277
+ if (k === "layout") {
278
+ if (v !== "stack" && v !== "row")
279
+ error("layout: stack or row");
280
+ } else if (k === "items")
281
+ items(v, "items");
282
+ else if (k === "view")
283
+ items(isRecord(v) ? v.items : void 0, "view.items");
284
+ else if (k === "accent") {
285
+ if (typeof v !== "string")
286
+ error("accent must be a colour string");
287
+ } else if (k === "actions") {
288
+ (Array.isArray(v) ? v : []).forEach((a, i) => {
289
+ const r = isRecord(a) ? a : {};
290
+ if (typeof r.command !== "string" || !ACTION_COMMANDS.includes(r.command))
291
+ error(`actions[${i}].command: one of ${ACTION_COMMANDS.join(", ")}`);
292
+ if (typeof r.id !== "string" || typeof r.label !== "string")
293
+ error(`actions[${i}] needs id and label`);
294
+ });
295
+ } else if (k === "inspector") {
296
+ (Array.isArray(v) ? v : []).forEach((f, i) => {
297
+ const r = isRecord(f) ? f : {};
298
+ if (typeof r.kind !== "string" || !INSPECTOR_KINDS.includes(r.kind))
299
+ error(`inspector[${i}].kind: one of ${INSPECTOR_KINDS.join(", ")}`);
300
+ if (typeof r.key !== "string" || typeof r.label !== "string")
301
+ error(`inspector[${i}] needs key and label`);
302
+ });
303
+ } else
304
+ warn(`key ${k} is not part of the declarative vocabulary`);
305
+ }
306
+ if (o.items === void 0 && o.view === void 0)
307
+ warn("no items: the control draws nothing");
308
+ return out;
309
+ }
310
+ function checkComposition(text, configSchema, file = AIWS_COMPOSITION_FILE, context = {}) {
311
+ const out = [];
312
+ const error = (message, code) => out.push({ file, severity: "error", message, ...code ? { code } : {} });
313
+ let o;
314
+ try {
315
+ const parsed = JSON.parse(text);
316
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
317
+ error("a composition is a JSON object { layout, slots, chrome?, events? }");
318
+ return out;
319
+ }
320
+ o = parsed;
321
+ } catch (e) {
322
+ error(e instanceof Error ? e.message : String(e));
323
+ return out;
324
+ }
325
+ const props = configProps(configSchema);
326
+ const placeholders = (t, at) => configPlaceholders(t, at, props, error);
327
+ const dimension = (v, at, max) => {
328
+ if (typeof v === "number" && v >= 0 && v <= max)
329
+ return;
330
+ if (typeof v === "string" && /^\s*\{\{.*\}\}\s*$/.test(v)) {
331
+ placeholders(v, at);
332
+ return;
333
+ }
334
+ error(`${at}: a number in [0, ${max}] or {{config.<key>}}`);
335
+ };
336
+ for (const k of Object.keys(o)) {
337
+ if (!["layout", "slots", "chrome", "events"].includes(k))
338
+ error(`unknown key ${k} (a composition says layout, slots, chrome and events)`);
339
+ }
340
+ const layout = o.layout;
341
+ let algorithm = "stack";
342
+ if (!layout || typeof layout !== "object")
343
+ error("layout { algorithm: stack | fixed | grid } required");
344
+ else {
345
+ if (layout.algorithm === "stack" || layout.algorithm === "fixed" || layout.algorithm === "grid")
346
+ algorithm = layout.algorithm;
347
+ else
348
+ error(`layout.algorithm ${JSON.stringify(layout.algorithm)}: stack, fixed or grid (the algorithms the host ships)`);
349
+ for (const [k, v] of Object.entries(layout)) {
350
+ if (k === "algorithm")
351
+ continue;
352
+ if ((k === "direction" || k === "spacing") && algorithm !== "stack") {
353
+ error(`layout.${k} belongs to the stack layout`);
354
+ continue;
355
+ }
356
+ if ((k === "columns" || k === "gap") && algorithm !== "grid") {
357
+ error(`layout.${k} belongs to the grid layout`);
358
+ continue;
359
+ }
360
+ if (k === "direction") {
361
+ if (v !== "column" && v !== "row")
362
+ error("layout.direction: column or row");
363
+ } else if (k === "spacing")
364
+ dimension(v, "layout.spacing", 2e3);
365
+ else if (k === "columns") {
366
+ if (!Number.isInteger(v) || v < 1 || v > 12)
367
+ error("layout.columns: an integer in [1, 12]");
368
+ } else if (k === "gap")
369
+ dimension(v, "layout.gap", 2e3);
370
+ else if (k === "padding") {
371
+ if (v && typeof v === "object" && !Array.isArray(v))
372
+ for (const [side, n] of Object.entries(v)) {
373
+ if (!["t", "r", "b", "l"].includes(side))
374
+ error(`layout.padding.${side}: t, r, b or l`);
375
+ dimension(n, `layout.padding.${side}`, 2e3);
376
+ }
377
+ else
378
+ dimension(v, "layout.padding", 2e3);
379
+ } else
380
+ error(`layout: unknown key ${k}`);
381
+ }
382
+ if (algorithm === "grid" && layout.columns === void 0)
383
+ error("layout.columns (1…12) required: the cells of a grid row");
384
+ }
385
+ const slots = o.slots && typeof o.slots === "object" && !Array.isArray(o.slots) ? o.slots : null;
386
+ if (!slots)
387
+ error("slots { <name>: { accepts?, min?, max?, placement? } } required");
388
+ else {
389
+ if (Object.keys(slots).length > 16)
390
+ error("at most 16 slots");
391
+ for (const [name, raw] of Object.entries(slots)) {
392
+ const at = `slots.${name}`;
393
+ if (!BINDING.test(name))
394
+ error(`${at}: a slot name is [a-z][a-z0-9_]{0,31}`);
395
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
396
+ error(`${at} must be an object`);
397
+ continue;
398
+ }
399
+ const slot = raw;
400
+ for (const [k, v] of Object.entries(slot)) {
401
+ if (k === "accepts") {
402
+ if (!Array.isArray(v) || v.length === 0 || v.length > 32) {
403
+ error(`${at}.accepts: 1…32 entries (cell:<ident>, a view type, container)`);
404
+ continue;
405
+ }
406
+ for (const a of v) {
407
+ if (typeof a === "string" && a.startsWith("cell:")) {
408
+ if (!IDENT.test(a.slice(5)) || encoder.encode(a.slice(5)).length > 128)
409
+ error(`${at}.accepts ${JSON.stringify(a)}: cell:<author_did>#<name>`);
410
+ } else if (a !== "container" && !(typeof a === "string" && NAME.test(a)))
411
+ error(`${at}.accepts ${JSON.stringify(a)}: cell:<ident>, a view type or container`);
412
+ }
413
+ } else if (k === "min") {
414
+ if (!Number.isInteger(v) || v < 0 || v > 64)
415
+ error(`${at}.min: an integer in [0, 64]`);
416
+ } else if (k === "max") {
417
+ if (!Number.isInteger(v) || v < 1 || v > 1e3)
418
+ error(`${at}.max: an integer in [1, 1000]`);
419
+ } else if (k === "placement") {
420
+ if (v !== "stack" && v !== "top" && v !== "bottom" && v !== "free")
421
+ error(`${at}.placement: stack, top, bottom or free`);
422
+ else if (algorithm === "fixed" && v !== "free")
423
+ error(`${at}.placement: a fixed layout keeps every child where it is (free)`);
424
+ else if (algorithm === "grid" && v !== "stack")
425
+ error(`${at}.placement: a grid fills its cells with the slots' children in slot order (stack)`);
426
+ } else if (k === "title") {
427
+ if (typeof v !== "string" || v.length > 256)
428
+ error(`${at}.title: at most 256 characters`);
429
+ } else
430
+ error(`${at}: unknown key ${k}`);
431
+ }
432
+ if (Number.isInteger(slot.min) && Number.isInteger(slot.max) && slot.min > slot.max)
433
+ error(`${at}: min exceeds max`);
434
+ }
435
+ }
436
+ const chrome = o.chrome;
437
+ if (chrome !== void 0) {
438
+ if (!chrome || typeof chrome !== "object" || Array.isArray(chrome))
439
+ error("chrome must be an object");
440
+ else
441
+ for (const [k, v] of Object.entries(chrome)) {
442
+ const at = `chrome.${k}`;
443
+ if (k === "background" || k === "border") {
444
+ if (typeof v === "string" && v.length <= 64)
445
+ placeholders(v, at);
446
+ else
447
+ error(`${at}: a colour (≤ 64 characters) or {{config.<key>}}`);
448
+ } else if (k === "radius")
449
+ dimension(v, at, 200);
450
+ else if (k === "empty") {
451
+ if (typeof v === "string" && v.length <= 1e3)
452
+ placeholders(v, at);
453
+ else
454
+ error(`${at}: a text of at most 1000 characters`);
455
+ } else if (k === "title") {
456
+ const t = v;
457
+ if (!t || typeof t !== "object" || Array.isArray(t)) {
458
+ error(`${at}: { text, slot? }`);
459
+ continue;
460
+ }
461
+ for (const [tk, tv] of Object.entries(t)) {
462
+ if (tk === "text") {
463
+ if (typeof tv === "string" && tv.length <= 256)
464
+ placeholders(tv, "chrome.title.text");
465
+ else
466
+ error("chrome.title.text: at most 256 characters");
467
+ } else if (tk === "slot") {
468
+ if (typeof tv !== "string" || !slots || !(tv in slots))
469
+ error(`chrome.title.slot ${JSON.stringify(tv)}: a slot of this composition`);
470
+ } else
471
+ error(`chrome.title: unknown key ${tk}`);
472
+ }
473
+ if (typeof t.text !== "string")
474
+ error("chrome.title.text required");
475
+ } else
476
+ error(`chrome: unknown key ${k} (background, radius, border, title, empty)`);
477
+ }
478
+ }
479
+ if (o.events !== void 0) {
480
+ if (context.manifestVersion !== void 0 && context.manifestVersion < 3)
481
+ error(`events ${V3_HINT}`);
482
+ if (!isRecord(o.events))
483
+ error("events must be an object { <name>: { from: { slot, event }, title?, description? } }");
484
+ else {
485
+ if (Object.keys(o.events).length > 16)
486
+ error("events: at most 16");
487
+ for (const [name, raw] of Object.entries(o.events)) {
488
+ const at = `events.${name}`;
489
+ if (!BINDING.test(name))
490
+ error(`${at}: an event name is [a-z][a-z0-9_]{0,31}`);
491
+ if (!isRecord(raw)) {
492
+ error(`${at} must be an object { from: { slot, event }, title?, description? }`);
493
+ continue;
494
+ }
495
+ for (const [k, v] of Object.entries(raw)) {
496
+ if (k === "title") {
497
+ if (typeof v !== "string" || v.length > 256)
498
+ error(`${at}.title: at most 256 characters`);
499
+ } else if (k === "description") {
500
+ if (typeof v !== "string" || v.length > 4e3)
501
+ error(`${at}.description: at most 4000 characters`);
502
+ } else if (k !== "from")
503
+ error(`${at}: unknown key ${k} (from, title, description)`);
504
+ }
505
+ const from = raw.from;
506
+ if (!isRecord(from)) {
507
+ error(`${at}.from { slot, event } required: the child event it re-raises`);
508
+ continue;
509
+ }
510
+ for (const k of Object.keys(from))
511
+ if (k !== "slot" && k !== "event")
512
+ error(`${at}.from: unknown key ${k} (slot, event)`);
513
+ const event = typeof from.event === "string" && BINDING.test(from.event) ? from.event : void 0;
514
+ if (!event)
515
+ error(`${at}.from.event: an event name [a-z][a-z0-9_]{0,31}`);
516
+ const slot = typeof from.slot === "string" && slots ? slots[from.slot] : void 0;
517
+ if (!isRecord(slot)) {
518
+ error(`${at}.from.slot ${JSON.stringify(from.slot)}: a slot of this composition`);
519
+ continue;
520
+ }
521
+ if (!event)
522
+ continue;
523
+ const cells = (Array.isArray(slot.accepts) ? slot.accepts : []).filter((a) => typeof a === "string" && a.startsWith("cell:")).map((a) => a.slice(5));
524
+ if (cells.length === 0) {
525
+ error(`${at}: slot ${from.slot} accepts no cell:<ident>, so no control there declares ${event}`, "EVENT_UNDECLARED");
526
+ continue;
527
+ }
528
+ const declared = cells.map((ident) => {
529
+ var _a;
530
+ return (_a = context.cellEvents) == null ? void 0 : _a.call(context, ident);
531
+ });
532
+ if (declared.every((names) => names !== void 0 && !names.includes(event)))
533
+ error(`${at}: no control slot ${from.slot} accepts (${cells.join(", ")}) declares the event ${event}`, "EVENT_UNDECLARED");
534
+ }
535
+ }
536
+ }
537
+ return out;
538
+ }
539
+ function checkComponent(text, fileNames, readText, cellEvents) {
96
540
  const out = [];
97
541
  const f = AIWS_COMPONENT_FILE;
98
542
  const error = (message) => out.push({ file: f, severity: "error", message });
@@ -112,11 +556,14 @@ function checkComponent(text, fileNames) {
112
556
  if (!COMPONENT_KEYS.includes(k))
113
557
  error(`unknown key ${k}`);
114
558
  const mv = c.manifest_version;
115
- if (typeof mv !== "number" || !Number.isInteger(mv) || mv < 1)
116
- error("manifest_version (1) required");
117
- else if (mv > AIWS_MANIFEST_VERSION)
559
+ const version = typeof mv === "number" && Number.isInteger(mv) && mv >= 1 ? mv : 0;
560
+ if (!version)
561
+ error(`manifest_version (1…${AIWS_MANIFEST_VERSION}) required`);
562
+ else if (version > AIWS_MANIFEST_VERSION)
118
563
  error(`manifest_version ${mv} is newer than this client reads (${AIWS_MANIFEST_VERSION})`);
564
+ const before3 = version > 0 && version < 3;
119
565
  const ident = c.ident;
566
+ let self = "";
120
567
  if (!ident || typeof ident !== "object")
121
568
  error("ident { author_did, name } required");
122
569
  else {
@@ -124,11 +571,22 @@ function checkComponent(text, fileNames) {
124
571
  error("ident.author_did must be a DID (did:<method>:<id>)");
125
572
  if (typeof ident.name !== "string" || !NAME.test(ident.name))
126
573
  error("ident.name must be [a-z0-9][a-z0-9._-]{0,63}");
127
- if (typeof ident.author_did === "string" && typeof ident.name === "string" && encoder.encode(`${ident.author_did}#${ident.name}`).length > 128)
128
- error("an identity is at most 128 bytes");
574
+ if (typeof ident.author_did === "string" && typeof ident.name === "string") {
575
+ self = `${ident.author_did}#${ident.name}`;
576
+ if (encoder.encode(self).length > 128)
577
+ error("an identity is at most 128 bytes");
578
+ }
579
+ }
580
+ const composite = c.kind === "composite";
581
+ if (c.kind !== "declarative" && c.kind !== "html" && !composite)
582
+ error("kind must be declarative, html or composite");
583
+ if (composite && version === 1)
584
+ error(`kind composite ${V2_HINT}`);
585
+ if (composite) {
586
+ for (const k of ["accepts", "allow_no_source"])
587
+ if (c[k] !== void 0)
588
+ error(`${k}: a composite is a container of Blocks, not a Cell showing data`);
129
589
  }
130
- if (c.kind !== "declarative" && c.kind !== "html")
131
- error("kind must be declarative or html");
132
590
  if (typeof c.title !== "string" || !c.title || c.title.length > 256)
133
591
  error("title: 1…256 characters");
134
592
  if (c.config_schema !== void 0) {
@@ -144,8 +602,38 @@ function checkComponent(text, fileNames) {
144
602
  if (!BINDING.test(n) || n === "props")
145
603
  error(`binding name ${n}: [a-z][a-z0-9_]{0,31}, not props`);
146
604
  }
147
- if (Array.isArray(c.deps) && c.deps.length > 0)
148
- error("deps must be empty: dependencies between controls come in a later version");
605
+ if (c.deps !== void 0) {
606
+ if (!Array.isArray(c.deps) || c.deps.length > 32)
607
+ error("deps: at most 32 identities <author_did>#<name>");
608
+ else if (c.deps.length > 0 && version === 1)
609
+ error(`deps ${V2_HINT}`);
610
+ else {
611
+ const seen = /* @__PURE__ */ new Set();
612
+ for (const d of c.deps) {
613
+ if (typeof d !== "string" || !IDENT.test(d))
614
+ error(`deps: ${JSON.stringify(d)} is not an identity <author_did>#<name>`);
615
+ else if (d === self)
616
+ error("deps: a control cannot depend on itself");
617
+ else if (seen.has(d))
618
+ error(`deps lists ${d} twice`);
619
+ seen.add(String(d));
620
+ }
621
+ }
622
+ }
623
+ let events = [];
624
+ if (c.events !== void 0) {
625
+ if (composite)
626
+ error("events: a composite declares its events in composition.json (events.<name>.from re-raises an event of a child)");
627
+ else if (before3)
628
+ error(`events ${V3_HINT}`);
629
+ else {
630
+ const p = [];
631
+ eventProblems(c.events, p);
632
+ p.forEach(error);
633
+ }
634
+ if (!composite && isRecord(c.events))
635
+ events = Object.keys(c.events);
636
+ }
149
637
  const has = (n) => typeof n === "string" && fileNames.includes(n);
150
638
  const named = /* @__PURE__ */ new Set([f]);
151
639
  if (c.kind === "html") {
@@ -159,13 +647,28 @@ function checkComponent(text, fileNames) {
159
647
  else
160
648
  named.add(String(n));
161
649
  }
162
- if (c.host_api !== void 0 && c.host_api !== AIWS_HTML_API_VERSION)
163
- error(`host_api: the HTML Block host speaks API ${AIWS_HTML_API_VERSION}`);
164
- } else if (c.kind === "declarative") {
650
+ if (c.host_api !== void 0 && c.host_api !== 2 && c.host_api !== 3)
651
+ error("host_api: the HTML Block host speaks API 2 and 3");
652
+ else if (c.host_api === 3 && before3)
653
+ error(`host_api 3 ${V3_HINT}`);
654
+ if (events.length > 0 && c.host_api !== 3)
655
+ out.push({ file: f, severity: "warning", message: "events: an HTML control raises them with aiws.emit, which is host API 3 (set host_api to 3)" });
656
+ } else if (c.kind === "declarative" || composite) {
165
657
  if (!has(c.entry))
166
658
  error(`entry ${JSON.stringify(c.entry)} is not in the directory`);
167
- else
659
+ else {
168
660
  named.add(String(c.entry));
661
+ const entryText = readText == null ? void 0 : readText(String(c.entry));
662
+ const manifestVersion = version || void 0;
663
+ if (entryText !== void 0 && composite)
664
+ out.push(...checkComposition(entryText, c.config_schema, String(c.entry), { manifestVersion, cellEvents }));
665
+ else if (entryText !== void 0)
666
+ out.push(...checkDeclarative(entryText, { bindings: c.bindings, events, configSchema: c.config_schema, manifestVersion }, String(c.entry)));
667
+ }
668
+ if (composite && c.host_api !== void 0)
669
+ error("host_api: a composite runs no code of its own (its layout and chrome are the host's)");
670
+ if (!composite && c.host_api !== void 0 && c.host_api !== AIWS_DECLARATIVE_API_VERSION)
671
+ error(`host_api: the declarative interpreter is version ${AIWS_DECLARATIVE_API_VERSION}`);
169
672
  }
170
673
  for (const a of Array.isArray(c.assets) ? c.assets : []) {
171
674
  if (!has(a))
@@ -179,11 +682,22 @@ function checkComponent(text, fileNames) {
179
682
  else
180
683
  named.add(String(c.agent));
181
684
  }
182
- for (const n of fileNames)
183
- if (!named.has(n))
685
+ for (const n of fileNames) {
686
+ if (!isSourcePath(n))
687
+ out.push({ file: n, severity: "error", message: "a file of a source directory: a relative path whose parts do not start with '.'" });
688
+ else if (!named.has(n))
184
689
  out.push({ file: n, severity: "warning", message: "not named by component.json: kept in the source snapshot only" });
690
+ }
185
691
  return out;
186
692
  }
693
+ function slotCells(composition) {
694
+ const out = /* @__PURE__ */ new Set();
695
+ for (const slot of Object.values(composition.slots ?? {}))
696
+ for (const a of slot.accepts ?? [])
697
+ if (a.startsWith("cell:"))
698
+ out.add(a.slice(5));
699
+ return [...out].sort();
700
+ }
187
701
  function compatManifest(value) {
188
702
  if (!value || typeof value !== "object")
189
703
  throw new AiwsSourceError([{ file: AIWS_MANIFEST_FILE, severity: "error", message: "must be a JSON object" }]);
@@ -199,6 +713,101 @@ function compatManifest(value) {
199
713
  throw new AiwsSourceError([{ file: AIWS_MANIFEST_FILE, severity: "error", message: "controls must be an array" }]);
200
714
  return { manifest_version: version, purpose: "aiws", host_api: m.host_api ?? {}, controls: m.controls, workspace_templates: Array.isArray(m.workspace_templates) ? m.workspace_templates : [] };
201
715
  }
716
+ const TEMPLATE_ID = /^[a-z0-9][a-z0-9-]{0,63}$/;
717
+ function templateDir(id) {
718
+ return `templates/${id}/`;
719
+ }
720
+ async function aiwsTemplateDigest(files, id) {
721
+ const dir = templateDir(id);
722
+ const own = [];
723
+ for (const [path, bytes] of files)
724
+ if (path.startsWith(dir))
725
+ own.push([path.slice(dir.length), bytes]);
726
+ return aiwsContentDigest(own);
727
+ }
728
+ function templateProblems(t, controls) {
729
+ if (!isRecord(t))
730
+ return ["a template is an object { id, title, snapshot, digest, requires, description?, params?, reset? }"];
731
+ const out = [];
732
+ for (const k of Object.keys(t))
733
+ if (!["id", "title", "description", "snapshot", "digest", "requires", "params", "reset"].includes(k))
734
+ out.push(`unknown key ${k}`);
735
+ if (typeof t.id !== "string" || !TEMPLATE_ID.test(t.id))
736
+ out.push("id: [a-z0-9][a-z0-9-]{0,63}");
737
+ else if (t.snapshot !== `${templateDir(t.id)}snapshot.json`)
738
+ out.push(`snapshot must be ${templateDir(t.id)}snapshot.json`);
739
+ if (typeof t.title !== "string" || !t.title || t.title.length > 256)
740
+ out.push("title: 1…256 characters");
741
+ if (t.description !== void 0 && (typeof t.description !== "string" || t.description.length > 4e3))
742
+ out.push("description: at most 4000 characters");
743
+ if (typeof t.digest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(t.digest))
744
+ out.push("digest: sha256:<64 hex digits>");
745
+ const requires = t.requires;
746
+ if (!isRecord(requires))
747
+ out.push("requires { host_api?, controls } required");
748
+ else {
749
+ for (const k of Object.keys(requires))
750
+ if (k !== "host_api" && k !== "controls")
751
+ out.push(`requires: unknown key ${k}`);
752
+ if (requires.host_api !== void 0) {
753
+ if (!isRecord(requires.host_api))
754
+ out.push("requires.host_api must be an object { html?, declarative? }");
755
+ else
756
+ for (const [k, v] of Object.entries(requires.host_api))
757
+ if (k !== "html" && k !== "declarative" || !Number.isInteger(v) || v < 1)
758
+ out.push(`requires.host_api.${k}: html or declarative, a positive integer`);
759
+ }
760
+ if (!Array.isArray(requires.controls) || requires.controls.length > 256)
761
+ out.push("requires.controls: at most 256 identities");
762
+ else {
763
+ const seen = /* @__PURE__ */ new Set();
764
+ for (const ident of requires.controls) {
765
+ if (typeof ident !== "string" || !controls.has(ident))
766
+ out.push(`requires.controls: ${JSON.stringify(ident)} is not a control of this manifest`);
767
+ else if (seen.has(ident))
768
+ out.push(`requires.controls lists ${ident} twice`);
769
+ seen.add(ident);
770
+ }
771
+ }
772
+ }
773
+ if (t.params !== void 0) {
774
+ if (!isRecord(t.params) || Object.keys(t.params).length > 32)
775
+ out.push("params: an object of at most 32 { type, title?, default? }");
776
+ else
777
+ for (const [name, p] of Object.entries(t.params)) {
778
+ const at = `params.${name}`;
779
+ if (!BINDING.test(name))
780
+ out.push(`${at}: a param name is [a-z][a-z0-9_]{0,31}`);
781
+ if (!isRecord(p)) {
782
+ out.push(`${at} must be an object { type, title?, default? }`);
783
+ continue;
784
+ }
785
+ for (const [k, v] of Object.entries(p)) {
786
+ if (k === "type") {
787
+ if (v !== "string" && v !== "number" && v !== "boolean")
788
+ out.push(`${at}.type: string, number or boolean`);
789
+ } else if (k === "title") {
790
+ if (typeof v !== "string" || v.length > 256)
791
+ out.push(`${at}.title: at most 256 characters`);
792
+ } else if (k === "default") {
793
+ if (!["string", "number", "boolean"].includes(typeof v))
794
+ out.push(`${at}.default: a string, number or boolean`);
795
+ } else
796
+ out.push(`${at}: unknown key ${k} (type, title, default)`);
797
+ }
798
+ if (p.type === void 0)
799
+ out.push(`${at}.type required`);
800
+ }
801
+ }
802
+ if (t.reset !== void 0) {
803
+ const tables = isRecord(t.reset) ? t.reset.tables : void 0;
804
+ if (!isRecord(t.reset) || Object.keys(t.reset).some((k) => k !== "tables"))
805
+ out.push("reset: { tables }");
806
+ else if (tables !== void 0 && (!Array.isArray(tables) || tables.length > 64 || new Set(tables).size !== tables.length || tables.some((x) => typeof x !== "string" || !/^[a-z0-9][a-z0-9_-]{0,63}$/.test(x))))
807
+ out.push("reset.tables: at most 64 distinct table ids");
808
+ }
809
+ return out;
810
+ }
202
811
  async function verifyBundle(files) {
203
812
  var _a, _b;
204
813
  const raw = files.get(AIWS_MANIFEST_FILE);
@@ -212,10 +821,23 @@ async function verifyBundle(files) {
212
821
  }
213
822
  const manifest = compatManifest(parsed);
214
823
  const diagnostics = [];
215
- if (manifest.workspace_templates.length > 0)
216
- diagnostics.push({ file: AIWS_MANIFEST_FILE, severity: "error", message: "Workspace templates are not supported yet" });
217
- if (manifest.controls.length === 0)
824
+ const templates = manifest.workspace_templates;
825
+ if (templates.length > 0 && manifest.manifest_version < 3)
826
+ diagnostics.push({ file: AIWS_MANIFEST_FILE, severity: "error", message: `Workspace templates need manifest_version 3 (raise it in ${AIWS_MANIFEST_FILE})` });
827
+ if (templates.length > 32)
828
+ diagnostics.push({ file: AIWS_MANIFEST_FILE, severity: "error", message: "at most 32 Workspace templates" });
829
+ if (manifest.controls.length === 0 && templates.length === 0)
218
830
  diagnostics.push({ file: AIWS_MANIFEST_FILE, severity: "error", message: "lists no control" });
831
+ const declared = /* @__PURE__ */ new Map();
832
+ for (const c of manifest.controls) {
833
+ const component = typeof c.dir === "string" ? files.get(`${c.dir}${AIWS_COMPONENT_FILE}`) : void 0;
834
+ try {
835
+ const comp = component ? JSON.parse(decoder.decode(component)) : void 0;
836
+ if (isRecord(comp))
837
+ declared.set(c.ident, isRecord(comp.events) ? Object.keys(comp.events) : []);
838
+ } catch {
839
+ }
840
+ }
219
841
  const dirs = /* @__PURE__ */ new Set();
220
842
  for (const c of manifest.controls) {
221
843
  if (typeof c.dir !== "string" || !/^controls\/[a-z0-9][a-z0-9._-]{0,70}\/$/.test(c.dir) || dirs.has(c.dir)) {
@@ -224,8 +846,6 @@ async function verifyBundle(files) {
224
846
  }
225
847
  dirs.add(c.dir);
226
848
  const own = [...files].filter(([p]) => p.startsWith(c.dir)).map(([p, b]) => [p.slice(c.dir.length), b]);
227
- if (own.some(([n]) => n.includes("/")))
228
- diagnostics.push({ file: c.dir, severity: "error", message: "a control directory is flat" });
229
849
  if (await aiwsContentDigest(own) !== c.digest)
230
850
  diagnostics.push({ file: c.dir, severity: "error", message: "the files do not match the manifest digest" });
231
851
  const component = own.find(([n]) => n === AIWS_COMPONENT_FILE);
@@ -233,18 +853,55 @@ async function verifyBundle(files) {
233
853
  diagnostics.push({ file: `${c.dir}${AIWS_COMPONENT_FILE}`, severity: "error", message: "missing" });
234
854
  continue;
235
855
  }
236
- for (const d of checkComponent(decoder.decode(component[1]), own.map(([n]) => n)))
856
+ const text = (name) => {
857
+ const hit = own.find(([n]) => n === name);
858
+ return hit ? decoder.decode(hit[1]) : void 0;
859
+ };
860
+ for (const d of checkComponent(decoder.decode(component[1]), own.map(([n]) => n), text, (ident) => declared.get(ident)))
237
861
  diagnostics.push({ ...d, file: `${c.dir}${d.file}` });
238
862
  try {
239
863
  const comp = JSON.parse(decoder.decode(component[1]));
240
864
  if (`${(_a = comp.ident) == null ? void 0 : _a.author_did}#${(_b = comp.ident) == null ? void 0 : _b.name}` !== c.ident || comp.kind !== c.kind)
241
865
  diagnostics.push({ file: c.dir, severity: "error", message: "ident and kind must match its component.json" });
866
+ if (JSON.stringify([...comp.deps ?? []].sort()) !== JSON.stringify([...c.deps ?? []].sort()))
867
+ diagnostics.push({ file: c.dir, severity: "error", message: "deps must match its component.json" });
242
868
  } catch {
243
869
  }
244
870
  }
871
+ const idents = new Set(manifest.controls.map((c) => c.ident));
872
+ for (const c of manifest.controls)
873
+ for (const d of c.deps ?? [])
874
+ if (!idents.has(d))
875
+ diagnostics.push({ file: AIWS_MANIFEST_FILE, severity: "error", message: `${c.ident} depends on ${d}, which the bundle does not carry` });
876
+ for (const [i, t] of templates.entries()) {
877
+ const label = isRecord(t) && typeof t.id === "string" ? `template ${t.id}` : `workspace_templates[${i}]`;
878
+ for (const p of templateProblems(t, idents))
879
+ diagnostics.push({ file: AIWS_MANIFEST_FILE, severity: "error", message: `${label}: ${p}` });
880
+ if (!isRecord(t) || typeof t.id !== "string" || !TEMPLATE_ID.test(t.id))
881
+ continue;
882
+ const dir = templateDir(t.id);
883
+ if (dirs.has(dir)) {
884
+ diagnostics.push({ file: AIWS_MANIFEST_FILE, severity: "error", message: `${label} is listed twice` });
885
+ continue;
886
+ }
887
+ dirs.add(dir);
888
+ const snapshot = files.get(`${dir}snapshot.json`);
889
+ let format;
890
+ try {
891
+ format = snapshot ? JSON.parse(decoder.decode(snapshot)).format : void 0;
892
+ } catch {
893
+ format = void 0;
894
+ }
895
+ if (!snapshot)
896
+ diagnostics.push({ file: `${dir}snapshot.json`, severity: "error", message: "missing" });
897
+ else if (format !== AIWS_TEMPLATE_FORMAT)
898
+ diagnostics.push({ file: `${dir}snapshot.json`, severity: "error", message: `not a template snapshot (format ${AIWS_TEMPLATE_FORMAT})` });
899
+ if (await aiwsTemplateDigest(files, t.id) !== t.digest)
900
+ diagnostics.push({ file: dir, severity: "error", message: "the files do not match the template digest" });
901
+ }
245
902
  for (const p of files.keys())
246
903
  if (p !== AIWS_MANIFEST_FILE && ![...dirs].some((d) => p.startsWith(d)))
247
- diagnostics.push({ file: p, severity: "error", message: "belongs to no control of the manifest" });
904
+ diagnostics.push({ file: p, severity: "error", message: "belongs to no control or template of the manifest" });
248
905
  return { manifest, diagnostics };
249
906
  }
250
907
  async function exportBundle(client, workspaceId, entityIds, fetchAsset) {
@@ -297,38 +954,71 @@ async function writeHomeFile(nfsp, dir, name, bytes) {
297
954
  }
298
955
  const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
299
956
  async function collectSources(nfsp, dir) {
300
- var _a, _b, _c;
301
957
  const out = [];
302
958
  const problems = [];
303
- let cursor;
304
- do {
305
- const page = await nfsp.list(dir, { limit: 500, ...cursor ? { cursor } : {} }, ["base", "ident"]);
306
- for (const e of page.entries) {
307
- if (e.name.startsWith("."))
308
- continue;
309
- if (e.target.kind !== "file") {
310
- problems.push({ file: e.name, severity: "error", message: "a control source folder is flat: move files out of sub-folders" });
311
- continue;
959
+ const walk = async (folder, prefix, depth) => {
960
+ var _a, _b, _c;
961
+ let cursor;
962
+ do {
963
+ const page = await nfsp.list(typeof folder === "string" ? folder : { ref: folder }, { limit: 500, ...cursor ? { cursor } : {} }, ["base", "ident"]);
964
+ for (const e of page.entries) {
965
+ if (e.name.startsWith("."))
966
+ continue;
967
+ const name = `${prefix}${e.name}`;
968
+ if (e.target.kind === "dir") {
969
+ if (depth >= 8)
970
+ problems.push({ file: name, severity: "error", message: "sub-folders nest at most 8 levels" });
971
+ else
972
+ await walk(e.target.ref, `${name}/`, depth + 1);
973
+ continue;
974
+ }
975
+ if (e.target.kind !== "file") {
976
+ problems.push({ file: name, severity: "error", message: "only files and folders belong to a control source" });
977
+ continue;
978
+ }
979
+ const ref = e.target.ref;
980
+ const objId = ref.type === "object" ? ref.obj_id : typeof ((_a = e.target.attrs) == null ? void 0 : _a.obj_id) === "string" ? e.target.attrs.obj_id : void 0;
981
+ out.push({
982
+ name,
983
+ ...objId ? { obj_id: objId } : {},
984
+ ...ref.type === "live" ? { node_id: ref.node_id } : {},
985
+ ...typeof ((_b = e.target.attrs) == null ? void 0 : _b.etag) === "string" ? { etag: e.target.attrs.etag } : {},
986
+ ...typeof ((_c = e.target.attrs) == null ? void 0 : _c.size) === "number" ? { size: e.target.attrs.size } : {}
987
+ });
312
988
  }
313
- const ref = e.target.ref;
314
- const objId = ref.type === "object" ? ref.obj_id : typeof ((_a = e.target.attrs) == null ? void 0 : _a.obj_id) === "string" ? e.target.attrs.obj_id : void 0;
315
- out.push({
316
- name: e.name,
317
- ...objId ? { obj_id: objId } : {},
318
- ...ref.type === "live" ? { node_id: ref.node_id } : {},
319
- ...typeof ((_b = e.target.attrs) == null ? void 0 : _b.etag) === "string" ? { etag: e.target.attrs.etag } : {},
320
- ...typeof ((_c = e.target.attrs) == null ? void 0 : _c.size) === "number" ? { size: e.target.attrs.size } : {}
321
- });
322
- }
323
- cursor = page.truncated ? page.next_cursor : void 0;
324
- } while (cursor);
989
+ cursor = page.truncated ? page.next_cursor : void 0;
990
+ } while (cursor);
991
+ };
992
+ await walk(dir, "", 0);
325
993
  if (out.length === 0)
326
994
  problems.push({ file: "", severity: "error", message: `${dir} holds no files` });
327
995
  if (out.length > AIWS_MAX_SOURCE_FILES)
328
996
  problems.push({ file: "", severity: "error", message: `a control holds at most ${AIWS_MAX_SOURCE_FILES} files` });
329
997
  if (problems.some((p) => p.severity === "error"))
330
998
  throw new AiwsSourceError(problems);
331
- return out.sort((a, b) => a.name.localeCompare(b.name));
999
+ return out.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
1000
+ }
1001
+ async function writeSourceFile(nfsp, dir, name, bytes) {
1002
+ const cut = name.lastIndexOf("/");
1003
+ await writeHomeFile(nfsp, cut < 0 ? dir : joinPath(dir, name.slice(0, cut)), cut < 0 ? name : name.slice(cut + 1), bytes);
1004
+ }
1005
+ async function sourcesDigest(nfsp, files) {
1006
+ const pairs = [];
1007
+ for (const f of files) {
1008
+ if (!f.node_id)
1009
+ throw new AiwsSourceError([{ file: f.name, severity: "error", message: "the file service did not name this file" }]);
1010
+ pairs.push([f.name, new Uint8Array(await (await nfsp.readFile(f.node_id)).arrayBuffer())]);
1011
+ }
1012
+ return aiwsContentDigest(pairs);
1013
+ }
1014
+ async function unbuiltChanges(client, nfsp, workspaceId, defEntityId, dir) {
1015
+ var _a;
1016
+ if (!await hasControlSource(nfsp, dir))
1017
+ return null;
1018
+ const home = await sourcesDigest(nfsp, await collectSources(nfsp, dir));
1019
+ const r = await client.extExport(workspaceId, [defEntityId], "pull");
1020
+ const built = String(((_a = r.controls[0]) == null ? void 0 : _a.digest) ?? "");
1021
+ return { changed: home !== built, home, built };
332
1022
  }
333
1023
  async function readBuildState(nfsp, dir) {
334
1024
  try {
@@ -351,11 +1041,12 @@ async function buildControl(client, nfsp, workspaceId, dir, opts = {}) {
351
1041
  const state = await readBuildState(nfsp, dir);
352
1042
  const base = state && state.workspace_id === workspaceId ? { def_rev: state.def_rev, dir_rev: state.dir_rev } : void 0;
353
1043
  const result = await client.extBuild(workspaceId, {
354
- files: await buildFiles(nfsp, files),
1044
+ files: await buildFiles(nfsp, files, opts.upload),
355
1045
  ...opts.folderId ? { folder_id: opts.folderId } : {},
356
1046
  ...base ? { expect: base } : {},
357
1047
  ...opts.force ? { force: true } : {},
358
- ...opts.dryRun ? { dry_run: true } : {}
1048
+ ...opts.dryRun ? { dry_run: true } : {},
1049
+ ...opts.migration ? { migration: opts.migration } : {}
359
1050
  });
360
1051
  if (!opts.dryRun && typeof result.def_entity_id === "string" && typeof result.def_rev === "number") {
361
1052
  await writeBuildState(nfsp, dir, { workspace_id: workspaceId, def_entity_id: result.def_entity_id, def_rev: result.def_rev, dir_rev: Number(result.dir_rev ?? 0), ident: String(result.ident ?? ""), at: (/* @__PURE__ */ new Date()).toISOString() });
@@ -365,9 +1056,9 @@ async function buildControl(client, nfsp, workspaceId, dir, opts = {}) {
365
1056
  async function pullControl(client, nfsp, workspaceId, defEntityId, outDir, fetchAsset) {
366
1057
  const r = await client.extExport(workspaceId, [defEntityId], "pull");
367
1058
  const control = r.controls[0];
368
- const ref = await ensureFolder(nfsp, outDir);
1059
+ await ensureFolder(nfsp, outDir);
369
1060
  for (const f of control.files)
370
- await writeHomeFile(nfsp, ref, f.name, f.text !== void 0 ? encoder.encode(f.text) : await fetchAsset(workspaceId, String(f.obj_id)));
1061
+ await writeSourceFile(nfsp, outDir, f.name, f.text !== void 0 ? encoder.encode(f.text) : await fetchAsset(workspaceId, String(f.obj_id)));
371
1062
  const state = { workspace_id: workspaceId, def_entity_id: control.entity_id, def_rev: control.def_rev, dir_rev: control.dir_rev, ident: control.ident, at: (/* @__PURE__ */ new Date()).toISOString() };
372
1063
  await writeBuildState(nfsp, outDir, state);
373
1064
  return { files: control.files.map((f) => f.name), state };
@@ -416,6 +1107,43 @@ async function watchControl(nfsp, dir, onChange, opts = {}) {
416
1107
  }
417
1108
  function controlSkeleton(opts) {
418
1109
  const title = opts.title ?? opts.name;
1110
+ if (opts.kind === "composite") {
1111
+ const fixed = opts.algorithm === "fixed";
1112
+ const slots = opts.slots ?? { items: { placement: fixed ? "free" : "stack", title: "Items" } };
1113
+ const composition = {
1114
+ layout: fixed ? { algorithm: "fixed", padding: 12 } : { algorithm: "stack", direction: "column", spacing: "{{config.gap}}", padding: 12 },
1115
+ slots,
1116
+ chrome: { background: "{{config.background}}", radius: 12, title: { text: "{{config.title}}" }, empty: "Drop Blocks here" }
1117
+ };
1118
+ const component = {
1119
+ manifest_version: AIWS_MANIFEST_VERSION,
1120
+ ident: { author_did: opts.authorDid, name: opts.name },
1121
+ kind: "composite",
1122
+ title,
1123
+ description: `${title}: a panel of Blocks`,
1124
+ default_size: { w: 320, h: 360 },
1125
+ config_schema: { type: "object", properties: {
1126
+ title: { type: "string", title: "Title", default: title },
1127
+ background: { type: "string", title: "Background", default: "#f6f7fb" },
1128
+ gap: { type: "integer", title: "Gap (px)", default: 8, minimum: 0, maximum: 64 }
1129
+ } },
1130
+ entry: AIWS_COMPOSITION_FILE,
1131
+ deps: opts.deps ?? [],
1132
+ catalog: { group: "custom", description: `${title}` },
1133
+ agent: "AGENT.md"
1134
+ };
1135
+ const names = Object.keys(slots).map((n) => `\`${n}\``).join(", ");
1136
+ return {
1137
+ [AIWS_COMPONENT_FILE]: `${JSON.stringify(component, null, 2)}
1138
+ `,
1139
+ [AIWS_COMPOSITION_FILE]: `${JSON.stringify(composition, null, 2)}
1140
+ `,
1141
+ "AGENT.md": `# ${title}
1142
+
1143
+ A composite: an instance is a \`buckyos.container { kind: "composite", def_ref }\`; put Blocks under it with \`slot\` set to one of ${names}. Config: \`title\`, \`background\`, \`gap\`.
1144
+ `
1145
+ };
1146
+ }
419
1147
  const base = {
420
1148
  manifest_version: AIWS_MANIFEST_VERSION,
421
1149
  ident: { author_did: opts.authorDid, name: opts.name },
@@ -435,13 +1163,22 @@ function controlSkeleton(opts) {
435
1163
  Bind \`data\` to a table. Config: \`label\` (text).
436
1164
  `;
437
1165
  if (opts.kind === "html") {
1166
+ const events = { clicked: { title: "Clicked", description: "Raised when someone clicks the button (in run mode)", params: { count: { type: "number", description: "Rows of the bound table" } } } };
438
1167
  return {
439
- [AIWS_COMPONENT_FILE]: `${JSON.stringify({ ...base, host_api: AIWS_HTML_API_VERSION, entry: { html: "view.html", css: "style.css", js: "behavior.js" } }, null, 2)}
1168
+ [AIWS_COMPONENT_FILE]: `${JSON.stringify({ ...base, host_api: AIWS_HTML_API_VERSION, events, entry: { html: "view.html", css: "style.css", js: "behavior.js" } }, null, 2)}
440
1169
  `,
441
- "view.html": '<div class="card"><div class="label" id="label"></div><div class="value" id="value">—</div></div>\n',
1170
+ "view.html": '<div class="card"><div class="label" id="label"></div><div class="value" id="value">—</div><button id="go" type="button">Go</button></div>\n',
442
1171
  "style.css": ".card{font:14px system-ui;padding:12px}.label{opacity:.7}.value{font-size:28px;font-weight:600}\n",
443
- "behavior.js": "document.getElementById('label').textContent = (aiws.context.config || {}).label || ''\nif (aiws.bindings.data) aiws.input('data').rows().then(function (rows) { document.getElementById('value').textContent = String(rows.length) })\n",
444
- "AGENT.md": agent
1172
+ "behavior.js": [
1173
+ "document.getElementById('label').textContent = (aiws.context.config || {}).label || ''",
1174
+ "var count = 0",
1175
+ "if (aiws.bindings.data) aiws.input('data').rows().then(function (rows) { count = rows.length; document.getElementById('value').textContent = String(count) })",
1176
+ "// the public event of component.json: an instance binds handlers to it in its `on` (host API 3)",
1177
+ "document.getElementById('go').addEventListener('click', function () { aiws.emit('clicked', { count: count }) })",
1178
+ ""
1179
+ ].join("\n"),
1180
+ "AGENT.md": `${agent}Event: \`clicked\` (\`count\`), raised by its button; bind handlers to it in the instance's \`on\`.
1181
+ `
445
1182
  };
446
1183
  }
447
1184
  return {
@@ -464,17 +1201,22 @@ async function createControlSource(nfsp, dir, files) {
464
1201
  export {
465
1202
  AIWS_BUILD_STATE_FILE,
466
1203
  AIWS_COMPONENT_FILE,
1204
+ AIWS_COMPOSITION_FILE,
467
1205
  AIWS_DECLARATIVE_API_VERSION,
468
1206
  AIWS_HTML_API_VERSION,
469
1207
  AIWS_MANIFEST_FILE,
470
1208
  AIWS_MANIFEST_VERSION,
471
1209
  AIWS_MAX_INLINE_BYTES,
472
1210
  AIWS_MAX_SOURCE_FILES,
1211
+ AIWS_TEMPLATE_FORMAT,
473
1212
  AiwsSourceError,
474
1213
  aiwsContentDigest,
1214
+ aiwsTemplateDigest,
475
1215
  buildControl,
476
1216
  buildFiles,
477
1217
  checkComponent,
1218
+ checkComposition,
1219
+ checkDeclarative,
478
1220
  collectSources,
479
1221
  compatManifest,
480
1222
  controlSkeleton,
@@ -486,14 +1228,21 @@ export {
486
1228
  hasControlSource,
487
1229
  homeControlDir,
488
1230
  homeExportDir,
1231
+ isSourcePath,
489
1232
  joinPath,
1233
+ packBuildFiles,
490
1234
  pullControl,
491
1235
  readBuildState,
1236
+ slotCells,
492
1237
  sourceStateKey,
1238
+ sourcesDigest,
493
1239
  sourcesKey,
1240
+ templateDir,
1241
+ unbuiltChanges,
494
1242
  verifyBundle,
495
1243
  watchControl,
496
1244
  writeBuildState,
497
- writeHomeFile
1245
+ writeHomeFile,
1246
+ writeSourceFile
498
1247
  };
499
1248
  //# sourceMappingURL=aiws_ext.mjs.map