@abraca/orchestrator 2.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.
- package/README.md +237 -0
- package/dist/abracadabra-orchestrator.cjs +2709 -0
- package/dist/abracadabra-orchestrator.cjs.map +1 -0
- package/dist/abracadabra-orchestrator.esm.js +2665 -0
- package/dist/abracadabra-orchestrator.esm.js.map +1 -0
- package/dist/index.d.ts +261 -0
- package/package.json +39 -0
- package/src/actions/awareness.ts +101 -0
- package/src/actions/chat.ts +30 -0
- package/src/actions/connect.ts +16 -0
- package/src/actions/content.ts +46 -0
- package/src/actions/cursor.ts +30 -0
- package/src/actions/document.ts +120 -0
- package/src/actions/flow.ts +46 -0
- package/src/actions/index.ts +197 -0
- package/src/actions/navigate.ts +15 -0
- package/src/actions/type.ts +113 -0
- package/src/actor-connection.ts +246 -0
- package/src/converters/markdownToYjs.ts +932 -0
- package/src/converters/types.ts +179 -0
- package/src/crypto.ts +71 -0
- package/src/define.ts +147 -0
- package/src/easing.ts +15 -0
- package/src/index.ts +105 -0
- package/src/orchestrator.ts +153 -0
- package/src/timeline-runner.ts +84 -0
- package/src/types.ts +245 -0
- package/src/utils.ts +50 -0
- package/src/yjs-utils.ts +238 -0
|
@@ -0,0 +1,2709 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
13
|
+
key = keys[i];
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
15
|
+
__defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
let node_path = require("node:path");
|
|
31
|
+
let node_url = require("node:url");
|
|
32
|
+
let yjs = require("yjs");
|
|
33
|
+
yjs = __toESM(yjs);
|
|
34
|
+
let _abraca_dabra = require("@abraca/dabra");
|
|
35
|
+
let _noble_ed25519 = require("@noble/ed25519");
|
|
36
|
+
_noble_ed25519 = __toESM(_noble_ed25519);
|
|
37
|
+
let node_fs_promises = require("node:fs/promises");
|
|
38
|
+
let node_fs = require("node:fs");
|
|
39
|
+
|
|
40
|
+
//#region node_modules/@noble/hashes/utils.js
|
|
41
|
+
/**
|
|
42
|
+
* Checks if something is Uint8Array. Be careful: nodejs Buffer will return true.
|
|
43
|
+
* @param a - value to test
|
|
44
|
+
* @returns `true` when the value is a Uint8Array-compatible view.
|
|
45
|
+
* @example
|
|
46
|
+
* Check whether a value is a Uint8Array-compatible view.
|
|
47
|
+
* ```ts
|
|
48
|
+
* isBytes(new Uint8Array([1, 2, 3]));
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
function isBytes(a) {
|
|
52
|
+
return a instanceof Uint8Array || ArrayBuffer.isView(a) && a.constructor.name === "Uint8Array" && "BYTES_PER_ELEMENT" in a && a.BYTES_PER_ELEMENT === 1;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Asserts something is Uint8Array.
|
|
56
|
+
* @param value - value to validate
|
|
57
|
+
* @param length - optional exact length constraint
|
|
58
|
+
* @param title - label included in thrown errors
|
|
59
|
+
* @returns The validated byte array.
|
|
60
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
61
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
62
|
+
* @example
|
|
63
|
+
* Validate that a value is a byte array.
|
|
64
|
+
* ```ts
|
|
65
|
+
* abytes(new Uint8Array([1, 2, 3]));
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
function abytes(value, length, title = "") {
|
|
69
|
+
const bytes = isBytes(value);
|
|
70
|
+
const len = value?.length;
|
|
71
|
+
const needsLen = length !== void 0;
|
|
72
|
+
if (!bytes || needsLen && len !== length) {
|
|
73
|
+
const prefix = title && `"${title}" `;
|
|
74
|
+
const ofLen = needsLen ? ` of length ${length}` : "";
|
|
75
|
+
const got = bytes ? `length=${len}` : `type=${typeof value}`;
|
|
76
|
+
const message = prefix + "expected Uint8Array" + ofLen + ", got " + got;
|
|
77
|
+
if (!bytes) throw new TypeError(message);
|
|
78
|
+
throw new RangeError(message);
|
|
79
|
+
}
|
|
80
|
+
return value;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Asserts a hash instance has not been destroyed or finished.
|
|
84
|
+
* @param instance - hash instance to validate
|
|
85
|
+
* @param checkFinished - whether to reject finalized instances
|
|
86
|
+
* @throws If the hash instance has already been destroyed or finalized. {@link Error}
|
|
87
|
+
* @example
|
|
88
|
+
* Validate that a hash instance is still usable.
|
|
89
|
+
* ```ts
|
|
90
|
+
* import { aexists } from '@noble/hashes/utils.js';
|
|
91
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
92
|
+
* const hash = sha256.create();
|
|
93
|
+
* aexists(hash);
|
|
94
|
+
* ```
|
|
95
|
+
*/
|
|
96
|
+
function aexists(instance, checkFinished = true) {
|
|
97
|
+
if (instance.destroyed) throw new Error("Hash instance has been destroyed");
|
|
98
|
+
if (checkFinished && instance.finished) throw new Error("Hash#digest() has already been called");
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Asserts output is a sufficiently-sized byte array.
|
|
102
|
+
* @param out - destination buffer
|
|
103
|
+
* @param instance - hash instance providing output length
|
|
104
|
+
* Oversized buffers are allowed; downstream code only promises to fill the first `outputLen` bytes.
|
|
105
|
+
* @throws On wrong argument types. {@link TypeError}
|
|
106
|
+
* @throws On wrong argument ranges or values. {@link RangeError}
|
|
107
|
+
* @example
|
|
108
|
+
* Validate a caller-provided digest buffer.
|
|
109
|
+
* ```ts
|
|
110
|
+
* import { aoutput } from '@noble/hashes/utils.js';
|
|
111
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
112
|
+
* const hash = sha256.create();
|
|
113
|
+
* aoutput(new Uint8Array(hash.outputLen), hash);
|
|
114
|
+
* ```
|
|
115
|
+
*/
|
|
116
|
+
function aoutput(out, instance) {
|
|
117
|
+
abytes(out, void 0, "digestInto() output");
|
|
118
|
+
const min = instance.outputLen;
|
|
119
|
+
if (out.length < min) throw new RangeError("\"digestInto() output\" expected to be of length >=" + min);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Zeroizes typed arrays in place. Warning: JS provides no guarantees.
|
|
123
|
+
* @param arrays - arrays to overwrite with zeros
|
|
124
|
+
* @example
|
|
125
|
+
* Zeroize sensitive buffers in place.
|
|
126
|
+
* ```ts
|
|
127
|
+
* clean(new Uint8Array([1, 2, 3]));
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
130
|
+
function clean(...arrays) {
|
|
131
|
+
for (let i = 0; i < arrays.length; i++) arrays[i].fill(0);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Creates a DataView for byte-level manipulation.
|
|
135
|
+
* @param arr - source typed array
|
|
136
|
+
* @returns DataView over the same buffer region.
|
|
137
|
+
* @example
|
|
138
|
+
* Create a DataView over an existing buffer.
|
|
139
|
+
* ```ts
|
|
140
|
+
* createView(new Uint8Array(4));
|
|
141
|
+
* ```
|
|
142
|
+
*/
|
|
143
|
+
function createView(arr) {
|
|
144
|
+
return new DataView(arr.buffer, arr.byteOffset, arr.byteLength);
|
|
145
|
+
}
|
|
146
|
+
/** Whether the current platform is little-endian. */
|
|
147
|
+
const isLE = new Uint8Array(new Uint32Array([287454020]).buffer)[0] === 68;
|
|
148
|
+
const hasHexBuiltin = typeof Uint8Array.from([]).toHex === "function" && typeof Uint8Array.fromHex === "function";
|
|
149
|
+
/**
|
|
150
|
+
* Creates a callable hash function from a stateful class constructor.
|
|
151
|
+
* @param hashCons - hash constructor or factory
|
|
152
|
+
* @param info - optional metadata such as DER OID
|
|
153
|
+
* @returns Frozen callable hash wrapper with `.create()`.
|
|
154
|
+
* Wrapper construction eagerly calls `hashCons(undefined)` once to read
|
|
155
|
+
* `outputLen` / `blockLen`, so constructor side effects happen at module
|
|
156
|
+
* init time.
|
|
157
|
+
* @example
|
|
158
|
+
* Wrap a stateful hash constructor into a callable helper.
|
|
159
|
+
* ```ts
|
|
160
|
+
* import { createHasher } from '@noble/hashes/utils.js';
|
|
161
|
+
* import { sha256 } from '@noble/hashes/sha2.js';
|
|
162
|
+
* const wrapped = createHasher(sha256.create, { oid: sha256.oid });
|
|
163
|
+
* wrapped(new Uint8Array([1]));
|
|
164
|
+
* ```
|
|
165
|
+
*/
|
|
166
|
+
function createHasher(hashCons, info = {}) {
|
|
167
|
+
const hashC = (msg, opts) => hashCons(opts).update(msg).digest();
|
|
168
|
+
const tmp = hashCons(void 0);
|
|
169
|
+
hashC.outputLen = tmp.outputLen;
|
|
170
|
+
hashC.blockLen = tmp.blockLen;
|
|
171
|
+
hashC.canXOF = tmp.canXOF;
|
|
172
|
+
hashC.create = (opts) => hashCons(opts);
|
|
173
|
+
Object.assign(hashC, info);
|
|
174
|
+
return Object.freeze(hashC);
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Creates OID metadata for NIST hashes with prefix `06 09 60 86 48 01 65 03 04 02`.
|
|
178
|
+
* @param suffix - final OID byte for the selected hash.
|
|
179
|
+
* The helper accepts any byte even though only the documented NIST hash
|
|
180
|
+
* suffixes are meaningful downstream.
|
|
181
|
+
* @returns Object containing the DER-encoded OID.
|
|
182
|
+
* @example
|
|
183
|
+
* Build OID metadata for a NIST hash.
|
|
184
|
+
* ```ts
|
|
185
|
+
* oidNist(0x01);
|
|
186
|
+
* ```
|
|
187
|
+
*/
|
|
188
|
+
const oidNist = (suffix) => ({ oid: Uint8Array.from([
|
|
189
|
+
6,
|
|
190
|
+
9,
|
|
191
|
+
96,
|
|
192
|
+
134,
|
|
193
|
+
72,
|
|
194
|
+
1,
|
|
195
|
+
101,
|
|
196
|
+
3,
|
|
197
|
+
4,
|
|
198
|
+
2,
|
|
199
|
+
suffix
|
|
200
|
+
]) });
|
|
201
|
+
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region node_modules/@noble/hashes/_md.js
|
|
204
|
+
/**
|
|
205
|
+
* Internal Merkle-Damgard hash utils.
|
|
206
|
+
* @module
|
|
207
|
+
*/
|
|
208
|
+
/**
|
|
209
|
+
* Merkle-Damgard hash construction base class.
|
|
210
|
+
* Could be used to create MD5, RIPEMD, SHA1, SHA2.
|
|
211
|
+
* Accepts only byte-aligned `Uint8Array` input, even when the underlying spec describes bit
|
|
212
|
+
* strings with partial-byte tails.
|
|
213
|
+
* @param blockLen - internal block size in bytes
|
|
214
|
+
* @param outputLen - digest size in bytes
|
|
215
|
+
* @param padOffset - trailing length field size in bytes
|
|
216
|
+
* @param isLE - whether length and state words are encoded in little-endian
|
|
217
|
+
* @example
|
|
218
|
+
* Use a concrete subclass to get the shared Merkle-Damgard update/digest flow.
|
|
219
|
+
* ```ts
|
|
220
|
+
* import { _SHA1 } from '@noble/hashes/legacy.js';
|
|
221
|
+
* const hash = new _SHA1();
|
|
222
|
+
* hash.update(new Uint8Array([97, 98, 99]));
|
|
223
|
+
* hash.digest();
|
|
224
|
+
* ```
|
|
225
|
+
*/
|
|
226
|
+
var HashMD = class {
|
|
227
|
+
blockLen;
|
|
228
|
+
outputLen;
|
|
229
|
+
canXOF = false;
|
|
230
|
+
padOffset;
|
|
231
|
+
isLE;
|
|
232
|
+
buffer;
|
|
233
|
+
view;
|
|
234
|
+
finished = false;
|
|
235
|
+
length = 0;
|
|
236
|
+
pos = 0;
|
|
237
|
+
destroyed = false;
|
|
238
|
+
constructor(blockLen, outputLen, padOffset, isLE) {
|
|
239
|
+
this.blockLen = blockLen;
|
|
240
|
+
this.outputLen = outputLen;
|
|
241
|
+
this.padOffset = padOffset;
|
|
242
|
+
this.isLE = isLE;
|
|
243
|
+
this.buffer = new Uint8Array(blockLen);
|
|
244
|
+
this.view = createView(this.buffer);
|
|
245
|
+
}
|
|
246
|
+
update(data) {
|
|
247
|
+
aexists(this);
|
|
248
|
+
abytes(data);
|
|
249
|
+
const { view, buffer, blockLen } = this;
|
|
250
|
+
const len = data.length;
|
|
251
|
+
for (let pos = 0; pos < len;) {
|
|
252
|
+
const take = Math.min(blockLen - this.pos, len - pos);
|
|
253
|
+
if (take === blockLen) {
|
|
254
|
+
const dataView = createView(data);
|
|
255
|
+
for (; blockLen <= len - pos; pos += blockLen) this.process(dataView, pos);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
buffer.set(data.subarray(pos, pos + take), this.pos);
|
|
259
|
+
this.pos += take;
|
|
260
|
+
pos += take;
|
|
261
|
+
if (this.pos === blockLen) {
|
|
262
|
+
this.process(view, 0);
|
|
263
|
+
this.pos = 0;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
this.length += data.length;
|
|
267
|
+
this.roundClean();
|
|
268
|
+
return this;
|
|
269
|
+
}
|
|
270
|
+
digestInto(out) {
|
|
271
|
+
aexists(this);
|
|
272
|
+
aoutput(out, this);
|
|
273
|
+
this.finished = true;
|
|
274
|
+
const { buffer, view, blockLen, isLE } = this;
|
|
275
|
+
let { pos } = this;
|
|
276
|
+
buffer[pos++] = 128;
|
|
277
|
+
clean(this.buffer.subarray(pos));
|
|
278
|
+
if (this.padOffset > blockLen - pos) {
|
|
279
|
+
this.process(view, 0);
|
|
280
|
+
pos = 0;
|
|
281
|
+
}
|
|
282
|
+
for (let i = pos; i < blockLen; i++) buffer[i] = 0;
|
|
283
|
+
view.setBigUint64(blockLen - 8, BigInt(this.length * 8), isLE);
|
|
284
|
+
this.process(view, 0);
|
|
285
|
+
const oview = createView(out);
|
|
286
|
+
const len = this.outputLen;
|
|
287
|
+
if (len % 4) throw new Error("_sha2: outputLen must be aligned to 32bit");
|
|
288
|
+
const outLen = len / 4;
|
|
289
|
+
const state = this.get();
|
|
290
|
+
if (outLen > state.length) throw new Error("_sha2: outputLen bigger than state");
|
|
291
|
+
for (let i = 0; i < outLen; i++) oview.setUint32(4 * i, state[i], isLE);
|
|
292
|
+
}
|
|
293
|
+
digest() {
|
|
294
|
+
const { buffer, outputLen } = this;
|
|
295
|
+
this.digestInto(buffer);
|
|
296
|
+
const res = buffer.slice(0, outputLen);
|
|
297
|
+
this.destroy();
|
|
298
|
+
return res;
|
|
299
|
+
}
|
|
300
|
+
_cloneInto(to) {
|
|
301
|
+
to ||= new this.constructor();
|
|
302
|
+
to.set(...this.get());
|
|
303
|
+
const { blockLen, buffer, length, finished, destroyed, pos } = this;
|
|
304
|
+
to.destroyed = destroyed;
|
|
305
|
+
to.finished = finished;
|
|
306
|
+
to.length = length;
|
|
307
|
+
to.pos = pos;
|
|
308
|
+
if (length % blockLen) to.buffer.set(buffer);
|
|
309
|
+
return to;
|
|
310
|
+
}
|
|
311
|
+
clone() {
|
|
312
|
+
return this._cloneInto();
|
|
313
|
+
}
|
|
314
|
+
};
|
|
315
|
+
/** Initial SHA512 state from RFC 6234 §6.3: eight RFC 64-bit `H(0)` words stored as sixteen
|
|
316
|
+
* big-endian 32-bit halves. Derived from the fractional parts of the square roots of the first
|
|
317
|
+
* eight prime numbers. Exported as a shared table; callers must treat it as read-only because
|
|
318
|
+
* constructors copy halves from it by index. */
|
|
319
|
+
const SHA512_IV = /* @__PURE__ */ Uint32Array.from([
|
|
320
|
+
1779033703,
|
|
321
|
+
4089235720,
|
|
322
|
+
3144134277,
|
|
323
|
+
2227873595,
|
|
324
|
+
1013904242,
|
|
325
|
+
4271175723,
|
|
326
|
+
2773480762,
|
|
327
|
+
1595750129,
|
|
328
|
+
1359893119,
|
|
329
|
+
2917565137,
|
|
330
|
+
2600822924,
|
|
331
|
+
725511199,
|
|
332
|
+
528734635,
|
|
333
|
+
4215389547,
|
|
334
|
+
1541459225,
|
|
335
|
+
327033209
|
|
336
|
+
]);
|
|
337
|
+
|
|
338
|
+
//#endregion
|
|
339
|
+
//#region node_modules/@noble/hashes/_u64.js
|
|
340
|
+
const U32_MASK64 = /* @__PURE__ */ BigInt(2 ** 32 - 1);
|
|
341
|
+
const _32n = /* @__PURE__ */ BigInt(32);
|
|
342
|
+
function fromBig(n, le = false) {
|
|
343
|
+
if (le) return {
|
|
344
|
+
h: Number(n & U32_MASK64),
|
|
345
|
+
l: Number(n >> _32n & U32_MASK64)
|
|
346
|
+
};
|
|
347
|
+
return {
|
|
348
|
+
h: Number(n >> _32n & U32_MASK64) | 0,
|
|
349
|
+
l: Number(n & U32_MASK64) | 0
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
function split(lst, le = false) {
|
|
353
|
+
const len = lst.length;
|
|
354
|
+
let Ah = new Uint32Array(len);
|
|
355
|
+
let Al = new Uint32Array(len);
|
|
356
|
+
for (let i = 0; i < len; i++) {
|
|
357
|
+
const { h, l } = fromBig(lst[i], le);
|
|
358
|
+
[Ah[i], Al[i]] = [h, l];
|
|
359
|
+
}
|
|
360
|
+
return [Ah, Al];
|
|
361
|
+
}
|
|
362
|
+
const shrSH = (h, _l, s) => h >>> s;
|
|
363
|
+
const shrSL = (h, l, s) => h << 32 - s | l >>> s;
|
|
364
|
+
const rotrSH = (h, l, s) => h >>> s | l << 32 - s;
|
|
365
|
+
const rotrSL = (h, l, s) => h << 32 - s | l >>> s;
|
|
366
|
+
const rotrBH = (h, l, s) => h << 64 - s | l >>> s - 32;
|
|
367
|
+
const rotrBL = (h, l, s) => h >>> s - 32 | l << 64 - s;
|
|
368
|
+
function add(Ah, Al, Bh, Bl) {
|
|
369
|
+
const l = (Al >>> 0) + (Bl >>> 0);
|
|
370
|
+
return {
|
|
371
|
+
h: Ah + Bh + (l / 2 ** 32 | 0) | 0,
|
|
372
|
+
l: l | 0
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
const add3L = (Al, Bl, Cl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0);
|
|
376
|
+
const add3H = (low, Ah, Bh, Ch) => Ah + Bh + Ch + (low / 2 ** 32 | 0) | 0;
|
|
377
|
+
const add4L = (Al, Bl, Cl, Dl) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0);
|
|
378
|
+
const add4H = (low, Ah, Bh, Ch, Dh) => Ah + Bh + Ch + Dh + (low / 2 ** 32 | 0) | 0;
|
|
379
|
+
const add5L = (Al, Bl, Cl, Dl, El) => (Al >>> 0) + (Bl >>> 0) + (Cl >>> 0) + (Dl >>> 0) + (El >>> 0);
|
|
380
|
+
const add5H = (low, Ah, Bh, Ch, Dh, Eh) => Ah + Bh + Ch + Dh + Eh + (low / 2 ** 32 | 0) | 0;
|
|
381
|
+
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region node_modules/@noble/hashes/sha2.js
|
|
384
|
+
/**
|
|
385
|
+
* SHA2 hash function. A.k.a. sha256, sha384, sha512, sha512_224, sha512_256.
|
|
386
|
+
* SHA256 is the fastest hash implementable in JS, even faster than Blake3.
|
|
387
|
+
* Check out {@link https://www.rfc-editor.org/rfc/rfc4634 | RFC 4634} and
|
|
388
|
+
* {@link https://nvlpubs.nist.gov/nistpubs/FIPS/NIST.FIPS.180-4.pdf | FIPS 180-4}.
|
|
389
|
+
* @module
|
|
390
|
+
*/
|
|
391
|
+
const K512 = split([
|
|
392
|
+
"0x428a2f98d728ae22",
|
|
393
|
+
"0x7137449123ef65cd",
|
|
394
|
+
"0xb5c0fbcfec4d3b2f",
|
|
395
|
+
"0xe9b5dba58189dbbc",
|
|
396
|
+
"0x3956c25bf348b538",
|
|
397
|
+
"0x59f111f1b605d019",
|
|
398
|
+
"0x923f82a4af194f9b",
|
|
399
|
+
"0xab1c5ed5da6d8118",
|
|
400
|
+
"0xd807aa98a3030242",
|
|
401
|
+
"0x12835b0145706fbe",
|
|
402
|
+
"0x243185be4ee4b28c",
|
|
403
|
+
"0x550c7dc3d5ffb4e2",
|
|
404
|
+
"0x72be5d74f27b896f",
|
|
405
|
+
"0x80deb1fe3b1696b1",
|
|
406
|
+
"0x9bdc06a725c71235",
|
|
407
|
+
"0xc19bf174cf692694",
|
|
408
|
+
"0xe49b69c19ef14ad2",
|
|
409
|
+
"0xefbe4786384f25e3",
|
|
410
|
+
"0x0fc19dc68b8cd5b5",
|
|
411
|
+
"0x240ca1cc77ac9c65",
|
|
412
|
+
"0x2de92c6f592b0275",
|
|
413
|
+
"0x4a7484aa6ea6e483",
|
|
414
|
+
"0x5cb0a9dcbd41fbd4",
|
|
415
|
+
"0x76f988da831153b5",
|
|
416
|
+
"0x983e5152ee66dfab",
|
|
417
|
+
"0xa831c66d2db43210",
|
|
418
|
+
"0xb00327c898fb213f",
|
|
419
|
+
"0xbf597fc7beef0ee4",
|
|
420
|
+
"0xc6e00bf33da88fc2",
|
|
421
|
+
"0xd5a79147930aa725",
|
|
422
|
+
"0x06ca6351e003826f",
|
|
423
|
+
"0x142929670a0e6e70",
|
|
424
|
+
"0x27b70a8546d22ffc",
|
|
425
|
+
"0x2e1b21385c26c926",
|
|
426
|
+
"0x4d2c6dfc5ac42aed",
|
|
427
|
+
"0x53380d139d95b3df",
|
|
428
|
+
"0x650a73548baf63de",
|
|
429
|
+
"0x766a0abb3c77b2a8",
|
|
430
|
+
"0x81c2c92e47edaee6",
|
|
431
|
+
"0x92722c851482353b",
|
|
432
|
+
"0xa2bfe8a14cf10364",
|
|
433
|
+
"0xa81a664bbc423001",
|
|
434
|
+
"0xc24b8b70d0f89791",
|
|
435
|
+
"0xc76c51a30654be30",
|
|
436
|
+
"0xd192e819d6ef5218",
|
|
437
|
+
"0xd69906245565a910",
|
|
438
|
+
"0xf40e35855771202a",
|
|
439
|
+
"0x106aa07032bbd1b8",
|
|
440
|
+
"0x19a4c116b8d2d0c8",
|
|
441
|
+
"0x1e376c085141ab53",
|
|
442
|
+
"0x2748774cdf8eeb99",
|
|
443
|
+
"0x34b0bcb5e19b48a8",
|
|
444
|
+
"0x391c0cb3c5c95a63",
|
|
445
|
+
"0x4ed8aa4ae3418acb",
|
|
446
|
+
"0x5b9cca4f7763e373",
|
|
447
|
+
"0x682e6ff3d6b2b8a3",
|
|
448
|
+
"0x748f82ee5defb2fc",
|
|
449
|
+
"0x78a5636f43172f60",
|
|
450
|
+
"0x84c87814a1f0ab72",
|
|
451
|
+
"0x8cc702081a6439ec",
|
|
452
|
+
"0x90befffa23631e28",
|
|
453
|
+
"0xa4506cebde82bde9",
|
|
454
|
+
"0xbef9a3f7b2c67915",
|
|
455
|
+
"0xc67178f2e372532b",
|
|
456
|
+
"0xca273eceea26619c",
|
|
457
|
+
"0xd186b8c721c0c207",
|
|
458
|
+
"0xeada7dd6cde0eb1e",
|
|
459
|
+
"0xf57d4f7fee6ed178",
|
|
460
|
+
"0x06f067aa72176fba",
|
|
461
|
+
"0x0a637dc5a2c898a6",
|
|
462
|
+
"0x113f9804bef90dae",
|
|
463
|
+
"0x1b710b35131c471b",
|
|
464
|
+
"0x28db77f523047d84",
|
|
465
|
+
"0x32caab7b40c72493",
|
|
466
|
+
"0x3c9ebe0a15c9bebc",
|
|
467
|
+
"0x431d67c49c100d4c",
|
|
468
|
+
"0x4cc5d4becb3e42b6",
|
|
469
|
+
"0x597f299cfc657e2a",
|
|
470
|
+
"0x5fcb6fab3ad6faec",
|
|
471
|
+
"0x6c44198c4a475817"
|
|
472
|
+
].map((n) => BigInt(n)));
|
|
473
|
+
const SHA512_Kh = K512[0];
|
|
474
|
+
const SHA512_Kl = K512[1];
|
|
475
|
+
const SHA512_W_H = /* @__PURE__ */ new Uint32Array(80);
|
|
476
|
+
const SHA512_W_L = /* @__PURE__ */ new Uint32Array(80);
|
|
477
|
+
/** Internal SHA-384 / SHA-512 compression engine from RFC 6234 §6.4. */
|
|
478
|
+
var SHA2_64B = class extends HashMD {
|
|
479
|
+
constructor(outputLen) {
|
|
480
|
+
super(128, outputLen, 16, false);
|
|
481
|
+
}
|
|
482
|
+
get() {
|
|
483
|
+
const { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
|
|
484
|
+
return [
|
|
485
|
+
Ah,
|
|
486
|
+
Al,
|
|
487
|
+
Bh,
|
|
488
|
+
Bl,
|
|
489
|
+
Ch,
|
|
490
|
+
Cl,
|
|
491
|
+
Dh,
|
|
492
|
+
Dl,
|
|
493
|
+
Eh,
|
|
494
|
+
El,
|
|
495
|
+
Fh,
|
|
496
|
+
Fl,
|
|
497
|
+
Gh,
|
|
498
|
+
Gl,
|
|
499
|
+
Hh,
|
|
500
|
+
Hl
|
|
501
|
+
];
|
|
502
|
+
}
|
|
503
|
+
set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl) {
|
|
504
|
+
this.Ah = Ah | 0;
|
|
505
|
+
this.Al = Al | 0;
|
|
506
|
+
this.Bh = Bh | 0;
|
|
507
|
+
this.Bl = Bl | 0;
|
|
508
|
+
this.Ch = Ch | 0;
|
|
509
|
+
this.Cl = Cl | 0;
|
|
510
|
+
this.Dh = Dh | 0;
|
|
511
|
+
this.Dl = Dl | 0;
|
|
512
|
+
this.Eh = Eh | 0;
|
|
513
|
+
this.El = El | 0;
|
|
514
|
+
this.Fh = Fh | 0;
|
|
515
|
+
this.Fl = Fl | 0;
|
|
516
|
+
this.Gh = Gh | 0;
|
|
517
|
+
this.Gl = Gl | 0;
|
|
518
|
+
this.Hh = Hh | 0;
|
|
519
|
+
this.Hl = Hl | 0;
|
|
520
|
+
}
|
|
521
|
+
process(view, offset) {
|
|
522
|
+
for (let i = 0; i < 16; i++, offset += 4) {
|
|
523
|
+
SHA512_W_H[i] = view.getUint32(offset);
|
|
524
|
+
SHA512_W_L[i] = view.getUint32(offset += 4);
|
|
525
|
+
}
|
|
526
|
+
for (let i = 16; i < 80; i++) {
|
|
527
|
+
const W15h = SHA512_W_H[i - 15] | 0;
|
|
528
|
+
const W15l = SHA512_W_L[i - 15] | 0;
|
|
529
|
+
const s0h = rotrSH(W15h, W15l, 1) ^ rotrSH(W15h, W15l, 8) ^ shrSH(W15h, W15l, 7);
|
|
530
|
+
const s0l = rotrSL(W15h, W15l, 1) ^ rotrSL(W15h, W15l, 8) ^ shrSL(W15h, W15l, 7);
|
|
531
|
+
const W2h = SHA512_W_H[i - 2] | 0;
|
|
532
|
+
const W2l = SHA512_W_L[i - 2] | 0;
|
|
533
|
+
const s1h = rotrSH(W2h, W2l, 19) ^ rotrBH(W2h, W2l, 61) ^ shrSH(W2h, W2l, 6);
|
|
534
|
+
const s1l = rotrSL(W2h, W2l, 19) ^ rotrBL(W2h, W2l, 61) ^ shrSL(W2h, W2l, 6);
|
|
535
|
+
const SUMl = add4L(s0l, s1l, SHA512_W_L[i - 7], SHA512_W_L[i - 16]);
|
|
536
|
+
SHA512_W_H[i] = add4H(SUMl, s0h, s1h, SHA512_W_H[i - 7], SHA512_W_H[i - 16]) | 0;
|
|
537
|
+
SHA512_W_L[i] = SUMl | 0;
|
|
538
|
+
}
|
|
539
|
+
let { Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl } = this;
|
|
540
|
+
for (let i = 0; i < 80; i++) {
|
|
541
|
+
const sigma1h = rotrSH(Eh, El, 14) ^ rotrSH(Eh, El, 18) ^ rotrBH(Eh, El, 41);
|
|
542
|
+
const sigma1l = rotrSL(Eh, El, 14) ^ rotrSL(Eh, El, 18) ^ rotrBL(Eh, El, 41);
|
|
543
|
+
const CHIh = Eh & Fh ^ ~Eh & Gh;
|
|
544
|
+
const CHIl = El & Fl ^ ~El & Gl;
|
|
545
|
+
const T1ll = add5L(Hl, sigma1l, CHIl, SHA512_Kl[i], SHA512_W_L[i]);
|
|
546
|
+
const T1h = add5H(T1ll, Hh, sigma1h, CHIh, SHA512_Kh[i], SHA512_W_H[i]);
|
|
547
|
+
const T1l = T1ll | 0;
|
|
548
|
+
const sigma0h = rotrSH(Ah, Al, 28) ^ rotrBH(Ah, Al, 34) ^ rotrBH(Ah, Al, 39);
|
|
549
|
+
const sigma0l = rotrSL(Ah, Al, 28) ^ rotrBL(Ah, Al, 34) ^ rotrBL(Ah, Al, 39);
|
|
550
|
+
const MAJh = Ah & Bh ^ Ah & Ch ^ Bh & Ch;
|
|
551
|
+
const MAJl = Al & Bl ^ Al & Cl ^ Bl & Cl;
|
|
552
|
+
Hh = Gh | 0;
|
|
553
|
+
Hl = Gl | 0;
|
|
554
|
+
Gh = Fh | 0;
|
|
555
|
+
Gl = Fl | 0;
|
|
556
|
+
Fh = Eh | 0;
|
|
557
|
+
Fl = El | 0;
|
|
558
|
+
({h: Eh, l: El} = add(Dh | 0, Dl | 0, T1h | 0, T1l | 0));
|
|
559
|
+
Dh = Ch | 0;
|
|
560
|
+
Dl = Cl | 0;
|
|
561
|
+
Ch = Bh | 0;
|
|
562
|
+
Cl = Bl | 0;
|
|
563
|
+
Bh = Ah | 0;
|
|
564
|
+
Bl = Al | 0;
|
|
565
|
+
const All = add3L(T1l, sigma0l, MAJl);
|
|
566
|
+
Ah = add3H(All, T1h, sigma0h, MAJh);
|
|
567
|
+
Al = All | 0;
|
|
568
|
+
}
|
|
569
|
+
({h: Ah, l: Al} = add(this.Ah | 0, this.Al | 0, Ah | 0, Al | 0));
|
|
570
|
+
({h: Bh, l: Bl} = add(this.Bh | 0, this.Bl | 0, Bh | 0, Bl | 0));
|
|
571
|
+
({h: Ch, l: Cl} = add(this.Ch | 0, this.Cl | 0, Ch | 0, Cl | 0));
|
|
572
|
+
({h: Dh, l: Dl} = add(this.Dh | 0, this.Dl | 0, Dh | 0, Dl | 0));
|
|
573
|
+
({h: Eh, l: El} = add(this.Eh | 0, this.El | 0, Eh | 0, El | 0));
|
|
574
|
+
({h: Fh, l: Fl} = add(this.Fh | 0, this.Fl | 0, Fh | 0, Fl | 0));
|
|
575
|
+
({h: Gh, l: Gl} = add(this.Gh | 0, this.Gl | 0, Gh | 0, Gl | 0));
|
|
576
|
+
({h: Hh, l: Hl} = add(this.Hh | 0, this.Hl | 0, Hh | 0, Hl | 0));
|
|
577
|
+
this.set(Ah, Al, Bh, Bl, Ch, Cl, Dh, Dl, Eh, El, Fh, Fl, Gh, Gl, Hh, Hl);
|
|
578
|
+
}
|
|
579
|
+
roundClean() {
|
|
580
|
+
clean(SHA512_W_H, SHA512_W_L);
|
|
581
|
+
}
|
|
582
|
+
destroy() {
|
|
583
|
+
this.destroyed = true;
|
|
584
|
+
clean(this.buffer);
|
|
585
|
+
this.set(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0);
|
|
586
|
+
}
|
|
587
|
+
};
|
|
588
|
+
/** Internal SHA-512 hash class grounded in RFC 6234 §6.3 and §6.4. */
|
|
589
|
+
var _SHA512 = class extends SHA2_64B {
|
|
590
|
+
Ah = SHA512_IV[0] | 0;
|
|
591
|
+
Al = SHA512_IV[1] | 0;
|
|
592
|
+
Bh = SHA512_IV[2] | 0;
|
|
593
|
+
Bl = SHA512_IV[3] | 0;
|
|
594
|
+
Ch = SHA512_IV[4] | 0;
|
|
595
|
+
Cl = SHA512_IV[5] | 0;
|
|
596
|
+
Dh = SHA512_IV[6] | 0;
|
|
597
|
+
Dl = SHA512_IV[7] | 0;
|
|
598
|
+
Eh = SHA512_IV[8] | 0;
|
|
599
|
+
El = SHA512_IV[9] | 0;
|
|
600
|
+
Fh = SHA512_IV[10] | 0;
|
|
601
|
+
Fl = SHA512_IV[11] | 0;
|
|
602
|
+
Gh = SHA512_IV[12] | 0;
|
|
603
|
+
Gl = SHA512_IV[13] | 0;
|
|
604
|
+
Hh = SHA512_IV[14] | 0;
|
|
605
|
+
Hl = SHA512_IV[15] | 0;
|
|
606
|
+
constructor() {
|
|
607
|
+
super(64);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
/**
|
|
611
|
+
* SHA2-512 hash function from RFC 4634.
|
|
612
|
+
* @param msg - message bytes to hash
|
|
613
|
+
* @returns Digest bytes.
|
|
614
|
+
* @example
|
|
615
|
+
* Hash a message with SHA2-512.
|
|
616
|
+
* ```ts
|
|
617
|
+
* sha512(new Uint8Array([97, 98, 99]));
|
|
618
|
+
* ```
|
|
619
|
+
*/
|
|
620
|
+
const sha512 = /* @__PURE__ */ createHasher(() => new _SHA512(), /* @__PURE__ */ oidNist(3));
|
|
621
|
+
|
|
622
|
+
//#endregion
|
|
623
|
+
//#region packages/orchestrator/src/crypto.ts
|
|
624
|
+
/**
|
|
625
|
+
* Ed25519 key generation, persistence, and challenge signing for actor auth.
|
|
626
|
+
* Copied from packages/mcp/src/crypto.ts
|
|
627
|
+
*/
|
|
628
|
+
_noble_ed25519.hashes.sha512 = sha512;
|
|
629
|
+
_noble_ed25519.hashes.sha512Async = (m) => Promise.resolve(sha512(m));
|
|
630
|
+
function toBase64url(bytes) {
|
|
631
|
+
return Buffer.from(bytes).toString("base64url");
|
|
632
|
+
}
|
|
633
|
+
function fromBase64url(b64) {
|
|
634
|
+
return new Uint8Array(Buffer.from(b64, "base64url"));
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Load an existing Ed25519 keypair from disk, or generate and persist a new one.
|
|
638
|
+
*/
|
|
639
|
+
async function loadOrCreateKeypair(keyPath) {
|
|
640
|
+
if ((0, node_fs.existsSync)(keyPath)) {
|
|
641
|
+
const seed = await (0, node_fs_promises.readFile)(keyPath);
|
|
642
|
+
if (seed.length !== 32) throw new Error(`Invalid key file at ${keyPath}: expected 32 bytes, got ${seed.length}`);
|
|
643
|
+
const privateKey = new Uint8Array(seed);
|
|
644
|
+
return {
|
|
645
|
+
privateKey,
|
|
646
|
+
publicKeyB64: toBase64url(_noble_ed25519.getPublicKey(privateKey))
|
|
647
|
+
};
|
|
648
|
+
}
|
|
649
|
+
const privateKey = _noble_ed25519.utils.randomSecretKey();
|
|
650
|
+
const publicKey = _noble_ed25519.getPublicKey(privateKey);
|
|
651
|
+
const dir = (0, node_path.dirname)(keyPath);
|
|
652
|
+
if (!(0, node_fs.existsSync)(dir)) await (0, node_fs_promises.mkdir)(dir, {
|
|
653
|
+
recursive: true,
|
|
654
|
+
mode: 448
|
|
655
|
+
});
|
|
656
|
+
await (0, node_fs_promises.writeFile)(keyPath, Buffer.from(privateKey), { mode: 384 });
|
|
657
|
+
console.error(`[orchestrator] Generated new keypair at ${keyPath}`);
|
|
658
|
+
return {
|
|
659
|
+
privateKey,
|
|
660
|
+
publicKeyB64: toBase64url(publicKey)
|
|
661
|
+
};
|
|
662
|
+
}
|
|
663
|
+
/**
|
|
664
|
+
* Generate a deterministic Ed25519 keypair from a seed string (e.g. actor name).
|
|
665
|
+
* The same seed always produces the same keypair.
|
|
666
|
+
*/
|
|
667
|
+
function deterministicKeypair(seed) {
|
|
668
|
+
const privateKey = sha512(new TextEncoder().encode(seed + ":orchestrator-salt")).slice(0, 32);
|
|
669
|
+
return {
|
|
670
|
+
privateKey,
|
|
671
|
+
publicKeyB64: toBase64url(_noble_ed25519.getPublicKey(privateKey))
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
/** Sign a base64url challenge with the agent's private key; returns base64url signature. */
|
|
675
|
+
function signChallenge(challengeB64, privateKey) {
|
|
676
|
+
const challenge = fromBase64url(challengeB64);
|
|
677
|
+
return toBase64url(_noble_ed25519.sign(challenge, privateKey));
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region packages/orchestrator/src/utils.ts
|
|
682
|
+
/**
|
|
683
|
+
* Utility functions for the orchestrator.
|
|
684
|
+
*/
|
|
685
|
+
/** Wait for a provider's `synced` event with a timeout. Resolves immediately if already synced. */
|
|
686
|
+
function waitForSync(provider, timeoutMs = 15e3) {
|
|
687
|
+
if (provider.synced) return Promise.resolve();
|
|
688
|
+
return new Promise((resolve, reject) => {
|
|
689
|
+
const timer = setTimeout(() => {
|
|
690
|
+
provider.off("synced", handler);
|
|
691
|
+
reject(/* @__PURE__ */ new Error(`Sync timed out after ${timeoutMs}ms`));
|
|
692
|
+
}, timeoutMs);
|
|
693
|
+
function handler() {
|
|
694
|
+
clearTimeout(timer);
|
|
695
|
+
resolve();
|
|
696
|
+
}
|
|
697
|
+
provider.on("synced", handler);
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
/** Sleep for a given number of milliseconds. */
|
|
701
|
+
function sleep(ms) {
|
|
702
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
703
|
+
}
|
|
704
|
+
/** Log to stderr with [orchestrator] prefix. */
|
|
705
|
+
function log(msg) {
|
|
706
|
+
console.error(`[orchestrator] ${msg}`);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region packages/orchestrator/src/yjs-utils.ts
|
|
711
|
+
/**
|
|
712
|
+
* Y.js document structure utilities for TipTap-compatible editing.
|
|
713
|
+
*
|
|
714
|
+
* TipTap documents have the structure:
|
|
715
|
+
* XmlFragment('default') → [documentHeader, documentMeta, ...body paragraphs]
|
|
716
|
+
*
|
|
717
|
+
* All text position functions skip documentHeader and documentMeta —
|
|
718
|
+
* character index 0 maps to the first character of the first body paragraph.
|
|
719
|
+
*/
|
|
720
|
+
const SCHEMA_NODES = new Set(["documentHeader", "documentMeta"]);
|
|
721
|
+
/** Check if an XmlElement is a TipTap schema node (not body content). */
|
|
722
|
+
function isSchemaNode(el) {
|
|
723
|
+
return SCHEMA_NODES.has(el.nodeName);
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Ensure the fragment has documentHeader and documentMeta nodes.
|
|
727
|
+
* Creates them if missing. Idempotent — safe to call multiple times.
|
|
728
|
+
*/
|
|
729
|
+
function ensureDocumentStructure(fragment) {
|
|
730
|
+
let hasHeader = false;
|
|
731
|
+
let hasMeta = false;
|
|
732
|
+
for (let i = 0; i < fragment.length; i++) {
|
|
733
|
+
const child = fragment.get(i);
|
|
734
|
+
if (child instanceof yjs.XmlElement) {
|
|
735
|
+
if (child.nodeName === "documentHeader") hasHeader = true;
|
|
736
|
+
if (child.nodeName === "documentMeta") hasMeta = true;
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
if (!hasMeta) {
|
|
740
|
+
const meta = new yjs.XmlElement("documentMeta");
|
|
741
|
+
fragment.insert(hasHeader ? 1 : 0, [meta]);
|
|
742
|
+
}
|
|
743
|
+
if (!hasHeader) {
|
|
744
|
+
const header = new yjs.XmlElement("documentHeader");
|
|
745
|
+
fragment.insert(0, [header]);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
/**
|
|
749
|
+
* Find the index where body content starts (after documentHeader + documentMeta).
|
|
750
|
+
*/
|
|
751
|
+
function bodyStartIndex(fragment) {
|
|
752
|
+
let idx = 0;
|
|
753
|
+
for (let i = 0; i < fragment.length; i++) {
|
|
754
|
+
const child = fragment.get(i);
|
|
755
|
+
if (child instanceof yjs.XmlElement && isSchemaNode(child)) idx = i + 1;
|
|
756
|
+
else break;
|
|
757
|
+
}
|
|
758
|
+
return idx;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Walk the XmlFragment body (skipping schema nodes) to find the XmlText node
|
|
762
|
+
* and local offset for a given global character index.
|
|
763
|
+
*/
|
|
764
|
+
function findTextPosition(fragment, globalIndex) {
|
|
765
|
+
let consumed = 0;
|
|
766
|
+
for (let i = 0; i < fragment.length; i++) {
|
|
767
|
+
const child = fragment.get(i);
|
|
768
|
+
if (child instanceof yjs.XmlElement) {
|
|
769
|
+
if (isSchemaNode(child)) continue;
|
|
770
|
+
const result = findTextInElement(child, globalIndex - consumed);
|
|
771
|
+
if (result) return result;
|
|
772
|
+
consumed += elementTextLength(child);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
return null;
|
|
776
|
+
}
|
|
777
|
+
function findTextInElement(el, remaining) {
|
|
778
|
+
for (let i = 0; i < el.length; i++) {
|
|
779
|
+
const child = el.get(i);
|
|
780
|
+
if (child instanceof yjs.XmlText) {
|
|
781
|
+
if (remaining <= child.length) return {
|
|
782
|
+
text: child,
|
|
783
|
+
offset: remaining
|
|
784
|
+
};
|
|
785
|
+
remaining -= child.length;
|
|
786
|
+
} else if (child instanceof yjs.XmlElement) {
|
|
787
|
+
const result = findTextInElement(child, remaining);
|
|
788
|
+
if (result) return result;
|
|
789
|
+
remaining -= elementTextLength(child);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
return null;
|
|
793
|
+
}
|
|
794
|
+
/** Total text length of an XmlElement (recursive). */
|
|
795
|
+
function elementTextLength(el) {
|
|
796
|
+
let len = 0;
|
|
797
|
+
for (let i = 0; i < el.length; i++) {
|
|
798
|
+
const child = el.get(i);
|
|
799
|
+
if (child instanceof yjs.XmlText) len += child.length;
|
|
800
|
+
else if (child instanceof yjs.XmlElement) len += elementTextLength(child);
|
|
801
|
+
}
|
|
802
|
+
return len;
|
|
803
|
+
}
|
|
804
|
+
/** Total text length of all body paragraphs (skips schema nodes). */
|
|
805
|
+
function fragmentTextLength(fragment) {
|
|
806
|
+
let len = 0;
|
|
807
|
+
for (let i = 0; i < fragment.length; i++) {
|
|
808
|
+
const child = fragment.get(i);
|
|
809
|
+
if (child instanceof yjs.XmlElement && !isSchemaNode(child)) len += elementTextLength(child);
|
|
810
|
+
}
|
|
811
|
+
return len;
|
|
812
|
+
}
|
|
813
|
+
/**
|
|
814
|
+
* Compute the global character index of a position within a known XmlText node.
|
|
815
|
+
* Re-derives from current document state — safe under concurrent edits.
|
|
816
|
+
* Only counts body content (skips schema nodes).
|
|
817
|
+
*/
|
|
818
|
+
function globalIndexOf(fragment, targetText, localOffset) {
|
|
819
|
+
let index = 0;
|
|
820
|
+
for (let i = 0; i < fragment.length; i++) {
|
|
821
|
+
const child = fragment.get(i);
|
|
822
|
+
if (child instanceof yjs.XmlElement) {
|
|
823
|
+
if (isSchemaNode(child)) continue;
|
|
824
|
+
const result = globalIndexInElement(child, targetText, localOffset, index);
|
|
825
|
+
if (result !== null) return result;
|
|
826
|
+
index += elementTextLength(child);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
return index + localOffset;
|
|
830
|
+
}
|
|
831
|
+
function globalIndexInElement(el, targetText, localOffset, base) {
|
|
832
|
+
for (let i = 0; i < el.length; i++) {
|
|
833
|
+
const child = el.get(i);
|
|
834
|
+
if (child instanceof yjs.XmlText) {
|
|
835
|
+
if (child === targetText) return base + localOffset;
|
|
836
|
+
base += child.length;
|
|
837
|
+
} else if (child instanceof yjs.XmlElement) {
|
|
838
|
+
const result = globalIndexInElement(child, targetText, localOffset, base);
|
|
839
|
+
if (result !== null) return result;
|
|
840
|
+
base += elementTextLength(child);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
return null;
|
|
844
|
+
}
|
|
845
|
+
/**
|
|
846
|
+
* Get or create the last body paragraph's XmlText.
|
|
847
|
+
* Skips documentHeader and documentMeta. Creates a new paragraph if none exist.
|
|
848
|
+
*/
|
|
849
|
+
function getOrCreateLastParagraph(fragment) {
|
|
850
|
+
for (let i = fragment.length - 1; i >= 0; i--) {
|
|
851
|
+
const child = fragment.get(i);
|
|
852
|
+
if (child instanceof yjs.XmlElement) {
|
|
853
|
+
if (isSchemaNode(child)) continue;
|
|
854
|
+
if (child.nodeName === "paragraph") {
|
|
855
|
+
for (let j = child.length - 1; j >= 0; j--) {
|
|
856
|
+
const grandchild = child.get(j);
|
|
857
|
+
if (grandchild instanceof yjs.XmlText) return {
|
|
858
|
+
text: grandchild,
|
|
859
|
+
globalOffset: fragmentTextLength(fragment)
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
const xt = new yjs.XmlText();
|
|
863
|
+
child.insert(0, [xt]);
|
|
864
|
+
return {
|
|
865
|
+
text: xt,
|
|
866
|
+
globalOffset: fragmentTextLength(fragment)
|
|
867
|
+
};
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
const para = new yjs.XmlElement("paragraph");
|
|
872
|
+
const xt = new yjs.XmlText();
|
|
873
|
+
para.insert(0, [xt]);
|
|
874
|
+
fragment.push([para]);
|
|
875
|
+
return {
|
|
876
|
+
text: xt,
|
|
877
|
+
globalOffset: 0
|
|
878
|
+
};
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* Insert a new paragraph after the one containing the given global character index.
|
|
882
|
+
* Skips schema nodes when computing positions.
|
|
883
|
+
*/
|
|
884
|
+
function insertParagraphAfter(fragment, afterGlobalIndex) {
|
|
885
|
+
let offset = 0;
|
|
886
|
+
let insertAt = fragment.length;
|
|
887
|
+
for (let i = 0; i < fragment.length; i++) {
|
|
888
|
+
const child = fragment.get(i);
|
|
889
|
+
if (child instanceof yjs.XmlElement) {
|
|
890
|
+
if (isSchemaNode(child)) continue;
|
|
891
|
+
const elLen = elementTextLength(child);
|
|
892
|
+
if (offset + elLen >= afterGlobalIndex) {
|
|
893
|
+
insertAt = i + 1;
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
offset += elLen;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const para = new yjs.XmlElement("paragraph");
|
|
900
|
+
const xt = new yjs.XmlText();
|
|
901
|
+
para.insert(0, [xt]);
|
|
902
|
+
fragment.insert(insertAt, [para]);
|
|
903
|
+
return xt;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
//#endregion
|
|
907
|
+
//#region packages/orchestrator/src/actor-connection.ts
|
|
908
|
+
/**
|
|
909
|
+
* ActorConnection — manages a single actor's connection to the Abracadabra server.
|
|
910
|
+
* One instance per actor. Handles auth, providers, child caching, awareness, and cursors.
|
|
911
|
+
*/
|
|
912
|
+
var ActorConnection = class {
|
|
913
|
+
constructor(actor, serverConfig) {
|
|
914
|
+
this._rootDocId = null;
|
|
915
|
+
this._rootDoc = null;
|
|
916
|
+
this._rootProvider = null;
|
|
917
|
+
this._publicKey = null;
|
|
918
|
+
this._childCache = /* @__PURE__ */ new Map();
|
|
919
|
+
this.actor = actor;
|
|
920
|
+
this.client = new _abraca_dabra.AbracadabraClient({ url: serverConfig.url.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://") });
|
|
921
|
+
}
|
|
922
|
+
get rootProvider() {
|
|
923
|
+
return this._rootProvider;
|
|
924
|
+
}
|
|
925
|
+
get rootDocId() {
|
|
926
|
+
return this._rootDocId;
|
|
927
|
+
}
|
|
928
|
+
/** Connect, authenticate, discover root doc, sync, and set awareness. */
|
|
929
|
+
async connect(serverConfig) {
|
|
930
|
+
const keypair = this.actor.keyFile ? await loadOrCreateKeypair(this.actor.keyFile) : deterministicKeypair(this.actor.name);
|
|
931
|
+
this._publicKey = keypair.publicKeyB64;
|
|
932
|
+
const signFn = (challenge) => Promise.resolve(signChallenge(challenge, keypair.privateKey));
|
|
933
|
+
try {
|
|
934
|
+
await this.client.loginWithKey(keypair.publicKeyB64, signFn);
|
|
935
|
+
} catch (err) {
|
|
936
|
+
const status = err?.status ?? err?.response?.status;
|
|
937
|
+
if (status === 404 || status === 422) {
|
|
938
|
+
log(`${this.actor.name}: registering new account...`);
|
|
939
|
+
await this.client.registerWithKey({
|
|
940
|
+
publicKey: keypair.publicKeyB64,
|
|
941
|
+
username: this.actor.name.replace(/\s+/g, "-").toLowerCase(),
|
|
942
|
+
displayName: this.actor.name,
|
|
943
|
+
deviceName: "Orchestrator Actor",
|
|
944
|
+
inviteCode: serverConfig.inviteCode
|
|
945
|
+
});
|
|
946
|
+
await this.client.loginWithKey(keypair.publicKeyB64, signFn);
|
|
947
|
+
} else throw err;
|
|
948
|
+
}
|
|
949
|
+
log(`${this.actor.name}: authenticated (${keypair.publicKeyB64.slice(0, 12)}...)`);
|
|
950
|
+
const roots = await this.client.listChildren();
|
|
951
|
+
const rootDocId = (roots.find((d) => d.kind === "space") ?? roots[0])?.id ?? null;
|
|
952
|
+
if (!rootDocId) throw new Error(`${this.actor.name}: no root document found`);
|
|
953
|
+
this._rootDocId = rootDocId;
|
|
954
|
+
const doc = new yjs.Doc({ guid: rootDocId });
|
|
955
|
+
const provider = new _abraca_dabra.AbracadabraProvider({
|
|
956
|
+
name: rootDocId,
|
|
957
|
+
document: doc,
|
|
958
|
+
client: this.client,
|
|
959
|
+
disableOfflineStore: true,
|
|
960
|
+
subdocLoading: "lazy"
|
|
961
|
+
});
|
|
962
|
+
await waitForSync(provider);
|
|
963
|
+
this._rootDoc = doc;
|
|
964
|
+
this._rootProvider = provider;
|
|
965
|
+
provider.awareness.setLocalStateField("user", {
|
|
966
|
+
name: this.actor.name,
|
|
967
|
+
color: this.actor.color,
|
|
968
|
+
publicKey: this._publicKey,
|
|
969
|
+
isAgent: false
|
|
970
|
+
});
|
|
971
|
+
provider.awareness.setLocalStateField("status", null);
|
|
972
|
+
log(`${this.actor.name}: connected to space ${rootDocId}`);
|
|
973
|
+
}
|
|
974
|
+
/** Get or create a child provider for a document. Caches and sets awareness. */
|
|
975
|
+
async getChildProvider(docId) {
|
|
976
|
+
const cached = this._childCache.get(docId);
|
|
977
|
+
if (cached) {
|
|
978
|
+
cached.lastAccessed = Date.now();
|
|
979
|
+
return cached.provider;
|
|
980
|
+
}
|
|
981
|
+
if (!this._rootProvider) throw new Error(`${this.actor.name}: not connected`);
|
|
982
|
+
const childProvider = await this._rootProvider.loadChild(docId);
|
|
983
|
+
await waitForSync(childProvider);
|
|
984
|
+
childProvider.awareness.setLocalStateField("user", {
|
|
985
|
+
name: this.actor.name,
|
|
986
|
+
color: this.actor.color,
|
|
987
|
+
publicKey: this._publicKey,
|
|
988
|
+
isAgent: false
|
|
989
|
+
});
|
|
990
|
+
this._childCache.set(docId, {
|
|
991
|
+
provider: childProvider,
|
|
992
|
+
lastAccessed: Date.now()
|
|
993
|
+
});
|
|
994
|
+
return childProvider;
|
|
995
|
+
}
|
|
996
|
+
/**
|
|
997
|
+
* Set cursor position in a document (collapsed — anchor = head).
|
|
998
|
+
*
|
|
999
|
+
* TipTap's yCursorPlugin reads awareness field 'cursor' as
|
|
1000
|
+
* { anchor: RelativePosJSON, head: RelativePosJSON }.
|
|
1001
|
+
* Positions must point into XmlText nodes (not the fragment) with assoc=-1.
|
|
1002
|
+
*/
|
|
1003
|
+
setCursor(docId, index) {
|
|
1004
|
+
const cached = this._childCache.get(docId);
|
|
1005
|
+
if (!cached) return;
|
|
1006
|
+
const fragment = cached.provider.document.getXmlFragment("default");
|
|
1007
|
+
const relPos = this._charIndexToRelativePosition(fragment, index);
|
|
1008
|
+
if (!relPos) return;
|
|
1009
|
+
const json = yjs.relativePositionToJSON(relPos);
|
|
1010
|
+
cached.provider.awareness.setLocalStateField("cursor", {
|
|
1011
|
+
anchor: json,
|
|
1012
|
+
head: json
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
/** Set text selection range in a document (anchor != head). */
|
|
1016
|
+
setSelection(docId, anchor, head) {
|
|
1017
|
+
const cached = this._childCache.get(docId);
|
|
1018
|
+
if (!cached) return;
|
|
1019
|
+
const fragment = cached.provider.document.getXmlFragment("default");
|
|
1020
|
+
const anchorPos = this._charIndexToRelativePosition(fragment, anchor);
|
|
1021
|
+
const headPos = this._charIndexToRelativePosition(fragment, head);
|
|
1022
|
+
if (!anchorPos || !headPos) return;
|
|
1023
|
+
cached.provider.awareness.setLocalStateField("cursor", {
|
|
1024
|
+
anchor: yjs.relativePositionToJSON(anchorPos),
|
|
1025
|
+
head: yjs.relativePositionToJSON(headPos)
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Convert a character index (body text only, skipping schema nodes)
|
|
1030
|
+
* to a Y.js RelativePosition pointing into the correct XmlText node.
|
|
1031
|
+
* Uses assoc=-1 (associate-left) to match TipTap's cursor behavior.
|
|
1032
|
+
*/
|
|
1033
|
+
_charIndexToRelativePosition(fragment, charIndex) {
|
|
1034
|
+
const totalLen = fragmentTextLength(fragment);
|
|
1035
|
+
const clamped = Math.max(0, Math.min(charIndex, totalLen));
|
|
1036
|
+
if (clamped === 0) return yjs.createRelativePositionFromTypeIndex(fragment, 0, -1);
|
|
1037
|
+
const pos = findTextPosition(fragment, clamped);
|
|
1038
|
+
if (pos) return yjs.createRelativePositionFromTypeIndex(pos.text, pos.offset, -1);
|
|
1039
|
+
return yjs.createRelativePositionFromTypeIndex(fragment, fragment.length, -1);
|
|
1040
|
+
}
|
|
1041
|
+
/** Set a field on root awareness. */
|
|
1042
|
+
setRootAwareness(field, value) {
|
|
1043
|
+
this._rootProvider?.awareness.setLocalStateField(field, value);
|
|
1044
|
+
}
|
|
1045
|
+
/** Set a field on a child document's awareness. */
|
|
1046
|
+
setChildAwareness(docId, field, value) {
|
|
1047
|
+
this._childCache.get(docId)?.provider.awareness.setLocalStateField(field, value);
|
|
1048
|
+
}
|
|
1049
|
+
/** Disconnect and clean up all providers. */
|
|
1050
|
+
async disconnect() {
|
|
1051
|
+
for (const [, cached] of this._childCache) cached.provider.awareness.setLocalStateField("cursor", null);
|
|
1052
|
+
if (this._rootProvider) {
|
|
1053
|
+
this._rootProvider.awareness.setLocalStateField("status", null);
|
|
1054
|
+
this._rootProvider.awareness.setLocalStateField("docId", null);
|
|
1055
|
+
}
|
|
1056
|
+
await sleep(150);
|
|
1057
|
+
for (const [, cached] of this._childCache) cached.provider.destroy();
|
|
1058
|
+
this._childCache.clear();
|
|
1059
|
+
if (this._rootProvider) {
|
|
1060
|
+
this._rootProvider.destroy();
|
|
1061
|
+
this._rootProvider = null;
|
|
1062
|
+
}
|
|
1063
|
+
this._rootDoc = null;
|
|
1064
|
+
this._rootDocId = null;
|
|
1065
|
+
log(`${this.actor.name}: disconnected`);
|
|
1066
|
+
}
|
|
1067
|
+
};
|
|
1068
|
+
|
|
1069
|
+
//#endregion
|
|
1070
|
+
//#region packages/orchestrator/src/timeline-runner.ts
|
|
1071
|
+
var TimelineRunner = class {
|
|
1072
|
+
constructor(options) {
|
|
1073
|
+
this.sceneStartTime = 0;
|
|
1074
|
+
this.errors = [];
|
|
1075
|
+
this.options = options ?? {};
|
|
1076
|
+
}
|
|
1077
|
+
/** Get elapsed ms since scene start. */
|
|
1078
|
+
get elapsed() {
|
|
1079
|
+
return Date.now() - this.sceneStartTime;
|
|
1080
|
+
}
|
|
1081
|
+
/** Execute all timeline entries in order with proper timing. */
|
|
1082
|
+
async run(timeline, execute) {
|
|
1083
|
+
const sorted = [...timeline].sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
1084
|
+
const errorStrategy = this.options.onError ?? "log";
|
|
1085
|
+
this.sceneStartTime = Date.now();
|
|
1086
|
+
const pending = [];
|
|
1087
|
+
for (let i = 0; i < sorted.length; i++) {
|
|
1088
|
+
const entry = sorted[i];
|
|
1089
|
+
const targetTime = entry.at ?? 0;
|
|
1090
|
+
const delay = targetTime - (Date.now() - this.sceneStartTime);
|
|
1091
|
+
if (delay > 0) await sleep(delay);
|
|
1092
|
+
log(`[${`${((Date.now() - this.sceneStartTime) / 1e3).toFixed(1)}s`}] ${entry.actor ?? "—"}: ${entry.action.type}`);
|
|
1093
|
+
const actionPromise = execute(entry).catch((err) => {
|
|
1094
|
+
const errorInfo = {
|
|
1095
|
+
entry,
|
|
1096
|
+
error: err,
|
|
1097
|
+
elapsed: Date.now() - this.sceneStartTime
|
|
1098
|
+
};
|
|
1099
|
+
this.errors.push(errorInfo);
|
|
1100
|
+
log(`Error in ${entry.action.type}${entry.actor ? ` for ${entry.actor}` : ""}: ${err.message}`);
|
|
1101
|
+
if (errorStrategy === "abort") throw err;
|
|
1102
|
+
});
|
|
1103
|
+
const nextEntry = sorted[i + 1];
|
|
1104
|
+
if (nextEntry && (nextEntry.at ?? 0) === targetTime) pending.push(actionPromise);
|
|
1105
|
+
else {
|
|
1106
|
+
pending.push(actionPromise);
|
|
1107
|
+
await Promise.all(pending);
|
|
1108
|
+
pending.length = 0;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
if (pending.length) await Promise.all(pending);
|
|
1112
|
+
if (this.errors.length) log(`Timeline finished with ${this.errors.length} error(s)`);
|
|
1113
|
+
}
|
|
1114
|
+
};
|
|
1115
|
+
|
|
1116
|
+
//#endregion
|
|
1117
|
+
//#region packages/orchestrator/src/actions/connect.ts
|
|
1118
|
+
async function executeConnect(actor, serverConfig) {
|
|
1119
|
+
await actor.connect(serverConfig);
|
|
1120
|
+
}
|
|
1121
|
+
async function executeDisconnect(actor) {
|
|
1122
|
+
await actor.disconnect();
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
//#endregion
|
|
1126
|
+
//#region packages/orchestrator/src/actions/navigate.ts
|
|
1127
|
+
async function executeNavigate(actor, action) {
|
|
1128
|
+
actor.setRootAwareness("docId", action.docId);
|
|
1129
|
+
await actor.getChildProvider(action.docId);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
//#endregion
|
|
1133
|
+
//#region packages/orchestrator/src/actions/type.ts
|
|
1134
|
+
/**
|
|
1135
|
+
* Typewriter action — character-by-character text insertion with live cursor.
|
|
1136
|
+
*
|
|
1137
|
+
* TipTap Y.js structure: XmlFragment('default') contains:
|
|
1138
|
+
* [documentHeader, documentMeta, ...body blocks]
|
|
1139
|
+
* Each paragraph is an XmlElement('paragraph') containing XmlText nodes.
|
|
1140
|
+
*
|
|
1141
|
+
* All character indices are relative to body content only (schema nodes skipped).
|
|
1142
|
+
*
|
|
1143
|
+
* TypeDelete action — backspace simulation, deletes characters one at a time.
|
|
1144
|
+
*/
|
|
1145
|
+
async function executeType(actor, action) {
|
|
1146
|
+
const speed = action.speed ?? 80;
|
|
1147
|
+
const variance = action.variance ?? 30;
|
|
1148
|
+
const doc = (await actor.getChildProvider(action.docId)).document;
|
|
1149
|
+
const fragment = doc.getXmlFragment("default");
|
|
1150
|
+
doc.transact(() => ensureDocumentStructure(fragment));
|
|
1151
|
+
const startIndex = action.position ?? fragmentTextLength(fragment);
|
|
1152
|
+
let currentText;
|
|
1153
|
+
let currentLocalOffset;
|
|
1154
|
+
const pos = findTextPosition(fragment, startIndex);
|
|
1155
|
+
if (pos) {
|
|
1156
|
+
currentText = pos.text;
|
|
1157
|
+
currentLocalOffset = pos.offset;
|
|
1158
|
+
} else {
|
|
1159
|
+
currentText = getOrCreateLastParagraph(fragment).text;
|
|
1160
|
+
currentLocalOffset = currentText.length;
|
|
1161
|
+
}
|
|
1162
|
+
for (const char of action.text) {
|
|
1163
|
+
await sleep(Math.max(10, speed + (Math.random() * 2 - 1) * variance));
|
|
1164
|
+
if (char === "\n") {
|
|
1165
|
+
const globalIdx = globalIndexOf(fragment, currentText, currentLocalOffset);
|
|
1166
|
+
doc.transact(() => {
|
|
1167
|
+
currentText = insertParagraphAfter(fragment, globalIdx);
|
|
1168
|
+
currentLocalOffset = 0;
|
|
1169
|
+
});
|
|
1170
|
+
} else {
|
|
1171
|
+
doc.transact(() => {
|
|
1172
|
+
currentText.insert(currentLocalOffset, char);
|
|
1173
|
+
});
|
|
1174
|
+
currentLocalOffset++;
|
|
1175
|
+
}
|
|
1176
|
+
const cursorGlobal = globalIndexOf(fragment, currentText, currentLocalOffset);
|
|
1177
|
+
actor.setCursor(action.docId, cursorGlobal);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
async function executeTypeDelete(actor, action) {
|
|
1181
|
+
const speed = action.speed ?? 60;
|
|
1182
|
+
const variance = action.variance ?? 20;
|
|
1183
|
+
const doc = (await actor.getChildProvider(action.docId)).document;
|
|
1184
|
+
const fragment = doc.getXmlFragment("default");
|
|
1185
|
+
const startIndex = action.position ?? fragmentTextLength(fragment);
|
|
1186
|
+
let remaining = action.count;
|
|
1187
|
+
while (remaining > 0) {
|
|
1188
|
+
await sleep(Math.max(10, speed + (Math.random() * 2 - 1) * variance));
|
|
1189
|
+
const deleteAt = Math.max(0, startIndex - (action.count - remaining) - 1);
|
|
1190
|
+
const pos = findTextPosition(fragment, deleteAt);
|
|
1191
|
+
if (!pos || deleteAt < 0) break;
|
|
1192
|
+
doc.transact(() => {
|
|
1193
|
+
if (pos.offset > 0) pos.text.delete(pos.offset - 1, 1);
|
|
1194
|
+
});
|
|
1195
|
+
remaining--;
|
|
1196
|
+
actor.setCursor(action.docId, Math.max(0, deleteAt));
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
//#endregion
|
|
1201
|
+
//#region packages/orchestrator/src/easing.ts
|
|
1202
|
+
const easings = {
|
|
1203
|
+
linear: (t) => t,
|
|
1204
|
+
easeIn: (t) => t * t,
|
|
1205
|
+
easeOut: (t) => t * (2 - t),
|
|
1206
|
+
easeInOut: (t) => t < .5 ? 2 * t * t : -1 + (4 - 2 * t) * t
|
|
1207
|
+
};
|
|
1208
|
+
function getEasing(name) {
|
|
1209
|
+
return easings[name ?? "easeInOut"];
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
//#endregion
|
|
1213
|
+
//#region packages/orchestrator/src/actions/cursor.ts
|
|
1214
|
+
async function executeMoveCursor(actor, action) {
|
|
1215
|
+
const easing = getEasing(action.easing);
|
|
1216
|
+
const steps = Math.max(1, Math.round(action.duration / 16));
|
|
1217
|
+
const stepDuration = action.duration / steps;
|
|
1218
|
+
for (let i = 0; i <= steps; i++) {
|
|
1219
|
+
const t = easing(i / steps);
|
|
1220
|
+
const pos = Math.round(action.from + (action.to - action.from) * t);
|
|
1221
|
+
actor.setCursor(action.docId, pos);
|
|
1222
|
+
if (i < steps) await sleep(stepDuration);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
async function executeSelect(actor, action) {
|
|
1226
|
+
actor.setSelection(action.docId, action.anchor, action.head);
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
//#endregion
|
|
1230
|
+
//#region packages/orchestrator/src/converters/markdownToYjs.ts
|
|
1231
|
+
/**
|
|
1232
|
+
* Markdown → Y.js converter.
|
|
1233
|
+
* Ported from cou-sh/app/utils/markdownToYjs.ts with Vue dependency removed.
|
|
1234
|
+
*/
|
|
1235
|
+
function parseInline(text) {
|
|
1236
|
+
const stripped = text.replace(/\{lang="[^"]*"\}/g, "").replace(/:(?!badge|icon|kbd)(\w[\w-]*)\[([^\]]*)\](\{[^}]*\})?/g, "$2").replace(/:(?!badge|icon|kbd)(\w[\w-]*)(\{[^}]*\})/g, "");
|
|
1237
|
+
const tokens = [];
|
|
1238
|
+
const re = /:badge\[([^\]]*)\](\{[^}]*\})?|:icon\{([^}]*)\}|:kbd\{([^}]*)\}|!?\[\[([^\]|]+?)(?:\|([^\]]+?))?\]\]|~~(.+?)~~|\*\*(.+?)\*\*|\*(.+?)\*|_(.+?)_|`(.+?)`|\[(.+?)\]\((.+?)\)/g;
|
|
1239
|
+
let lastIndex = 0;
|
|
1240
|
+
let match;
|
|
1241
|
+
while ((match = re.exec(stripped)) !== null) {
|
|
1242
|
+
if (match.index > lastIndex) tokens.push({ text: stripped.slice(lastIndex, match.index) });
|
|
1243
|
+
if (match[1] !== void 0) {
|
|
1244
|
+
const badgeProps = parseMdcProps(match[2]);
|
|
1245
|
+
tokens.push({
|
|
1246
|
+
text: match[1] || "Badge",
|
|
1247
|
+
attrs: { badge: {
|
|
1248
|
+
label: match[1] || "Badge",
|
|
1249
|
+
color: badgeProps["color"] || "neutral",
|
|
1250
|
+
variant: badgeProps["variant"] || "subtle"
|
|
1251
|
+
} }
|
|
1252
|
+
});
|
|
1253
|
+
} else if (match[3] !== void 0) {
|
|
1254
|
+
const iconProps = parseMdcProps(`{${match[3]}}`);
|
|
1255
|
+
tokens.push({
|
|
1256
|
+
text: "",
|
|
1257
|
+
attrs: { proseIcon: { name: iconProps["name"] || "i-lucide-star" } }
|
|
1258
|
+
});
|
|
1259
|
+
} else if (match[4] !== void 0) {
|
|
1260
|
+
const kbdProps = parseMdcProps(`{${match[4]}}`);
|
|
1261
|
+
tokens.push({
|
|
1262
|
+
text: kbdProps["value"] || "",
|
|
1263
|
+
attrs: { kbd: { value: kbdProps["value"] || "" } }
|
|
1264
|
+
});
|
|
1265
|
+
} else if (match[5] !== void 0) tokens.push({
|
|
1266
|
+
text: "",
|
|
1267
|
+
node: "docLink",
|
|
1268
|
+
nodeAttrs: { docId: match[5] }
|
|
1269
|
+
});
|
|
1270
|
+
else if (match[7] !== void 0) tokens.push({
|
|
1271
|
+
text: match[7],
|
|
1272
|
+
attrs: { strike: true }
|
|
1273
|
+
});
|
|
1274
|
+
else if (match[8] !== void 0) tokens.push({
|
|
1275
|
+
text: match[8],
|
|
1276
|
+
attrs: { bold: true }
|
|
1277
|
+
});
|
|
1278
|
+
else if (match[9] !== void 0) tokens.push({
|
|
1279
|
+
text: match[9],
|
|
1280
|
+
attrs: { italic: true }
|
|
1281
|
+
});
|
|
1282
|
+
else if (match[10] !== void 0) tokens.push({
|
|
1283
|
+
text: match[10],
|
|
1284
|
+
attrs: { italic: true }
|
|
1285
|
+
});
|
|
1286
|
+
else if (match[11] !== void 0) tokens.push({
|
|
1287
|
+
text: match[11],
|
|
1288
|
+
attrs: { code: true }
|
|
1289
|
+
});
|
|
1290
|
+
else if (match[12] !== void 0 && match[13] !== void 0) tokens.push({
|
|
1291
|
+
text: match[12],
|
|
1292
|
+
attrs: { link: { href: match[13] } }
|
|
1293
|
+
});
|
|
1294
|
+
lastIndex = match.index + match[0].length;
|
|
1295
|
+
}
|
|
1296
|
+
if (lastIndex < stripped.length) tokens.push({ text: stripped.slice(lastIndex) });
|
|
1297
|
+
return tokens.filter((t) => t.node || t.text.length > 0);
|
|
1298
|
+
}
|
|
1299
|
+
function parseTableRow(line) {
|
|
1300
|
+
const parts = line.split("|");
|
|
1301
|
+
return parts.slice(1, parts.length - 1).map((c) => c.trim());
|
|
1302
|
+
}
|
|
1303
|
+
function isTableSeparator(line) {
|
|
1304
|
+
return /^\|[\s|:-]+\|$/.test(line.trim());
|
|
1305
|
+
}
|
|
1306
|
+
function extractFencedCode(lines) {
|
|
1307
|
+
const result = [];
|
|
1308
|
+
let i = 0;
|
|
1309
|
+
while (i < lines.length) {
|
|
1310
|
+
const fenceMatch = lines[i].match(/^(`{3,})(\w*)/);
|
|
1311
|
+
if (fenceMatch) {
|
|
1312
|
+
const fence = fenceMatch[1];
|
|
1313
|
+
const lang = fenceMatch[2] ?? "";
|
|
1314
|
+
const codeLines = [];
|
|
1315
|
+
i++;
|
|
1316
|
+
while (i < lines.length && !lines[i].startsWith(fence)) {
|
|
1317
|
+
codeLines.push(lines[i]);
|
|
1318
|
+
i++;
|
|
1319
|
+
}
|
|
1320
|
+
i++;
|
|
1321
|
+
result.push({
|
|
1322
|
+
type: "codeBlock",
|
|
1323
|
+
lang,
|
|
1324
|
+
code: codeLines.join("\n")
|
|
1325
|
+
});
|
|
1326
|
+
continue;
|
|
1327
|
+
}
|
|
1328
|
+
i++;
|
|
1329
|
+
}
|
|
1330
|
+
return result;
|
|
1331
|
+
}
|
|
1332
|
+
function parseMdcProps(propsStr) {
|
|
1333
|
+
if (!propsStr) return {};
|
|
1334
|
+
const result = {};
|
|
1335
|
+
const re = /(\w[\w-]*)="([^"]*)"/g;
|
|
1336
|
+
let m;
|
|
1337
|
+
while ((m = re.exec(propsStr)) !== null) result[m[1]] = m[2];
|
|
1338
|
+
return result;
|
|
1339
|
+
}
|
|
1340
|
+
function parseMdcChildren(innerLines, slotPrefix) {
|
|
1341
|
+
const items = [];
|
|
1342
|
+
let current = null;
|
|
1343
|
+
const slotRe = new RegExp(`^#${slotPrefix}(\\{[^}]*\\})?\\s*$`);
|
|
1344
|
+
for (const line of innerLines) {
|
|
1345
|
+
const slotMatch = line.match(slotRe);
|
|
1346
|
+
if (slotMatch) {
|
|
1347
|
+
if (current) items.push(current);
|
|
1348
|
+
const props = parseMdcProps(slotMatch[1]);
|
|
1349
|
+
current = {
|
|
1350
|
+
label: props["label"] || props["title"] || `Item ${items.length + 1}`,
|
|
1351
|
+
icon: props["icon"] || "",
|
|
1352
|
+
lines: []
|
|
1353
|
+
};
|
|
1354
|
+
continue;
|
|
1355
|
+
}
|
|
1356
|
+
if (current) current.lines.push(line);
|
|
1357
|
+
else if (!items.length && !current) current = {
|
|
1358
|
+
label: `Item 1`,
|
|
1359
|
+
icon: "",
|
|
1360
|
+
lines: [line]
|
|
1361
|
+
};
|
|
1362
|
+
}
|
|
1363
|
+
if (current) items.push(current);
|
|
1364
|
+
return items.map((item) => ({
|
|
1365
|
+
label: item.label,
|
|
1366
|
+
icon: item.icon,
|
|
1367
|
+
innerBlocks: parseBlocks(item.lines.join("\n"))
|
|
1368
|
+
}));
|
|
1369
|
+
}
|
|
1370
|
+
const TASK_RE = /^[-*+]\s+\[([ xX])\]\s+(.*)/;
|
|
1371
|
+
function parseBlocks(markdown) {
|
|
1372
|
+
const rawLines = markdown.split("\n");
|
|
1373
|
+
let firstContentLine = 0;
|
|
1374
|
+
while (firstContentLine < rawLines.length) {
|
|
1375
|
+
const l = rawLines[firstContentLine];
|
|
1376
|
+
if (l.trim() === "" || /^import\s/.test(l) || /^export\s/.test(l)) firstContentLine++;
|
|
1377
|
+
else break;
|
|
1378
|
+
}
|
|
1379
|
+
const stripped = rawLines.slice(firstContentLine).join("\n");
|
|
1380
|
+
const blocks = [];
|
|
1381
|
+
const lines = stripped.split("\n");
|
|
1382
|
+
let i = 0;
|
|
1383
|
+
while (i < lines.length) {
|
|
1384
|
+
const line = lines[i];
|
|
1385
|
+
const fenceBlockMatch = line.match(/^(`{3,})(.*)$/);
|
|
1386
|
+
if (fenceBlockMatch) {
|
|
1387
|
+
const fence = fenceBlockMatch[1];
|
|
1388
|
+
const lang = fenceBlockMatch[2].trim().replace(/\{[^}]*\}$/, "").replace(/\s*\[.*\]$/, "").trim();
|
|
1389
|
+
const codeLines = [];
|
|
1390
|
+
i++;
|
|
1391
|
+
while (i < lines.length && !lines[i].startsWith(fence)) {
|
|
1392
|
+
codeLines.push(lines[i]);
|
|
1393
|
+
i++;
|
|
1394
|
+
}
|
|
1395
|
+
i++;
|
|
1396
|
+
if (lang === "svg" || lang.startsWith("svg ")) {
|
|
1397
|
+
const svgTitle = lang === "svg" ? "" : lang.slice(4).trim();
|
|
1398
|
+
blocks.push({
|
|
1399
|
+
type: "svgEmbed",
|
|
1400
|
+
svg: codeLines.join("\n"),
|
|
1401
|
+
title: svgTitle
|
|
1402
|
+
});
|
|
1403
|
+
} else blocks.push({
|
|
1404
|
+
type: "codeBlock",
|
|
1405
|
+
lang,
|
|
1406
|
+
code: codeLines.join("\n")
|
|
1407
|
+
});
|
|
1408
|
+
continue;
|
|
1409
|
+
}
|
|
1410
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.*)/);
|
|
1411
|
+
if (headingMatch) {
|
|
1412
|
+
blocks.push({
|
|
1413
|
+
type: "heading",
|
|
1414
|
+
level: headingMatch[1].length,
|
|
1415
|
+
text: headingMatch[2].trim()
|
|
1416
|
+
});
|
|
1417
|
+
i++;
|
|
1418
|
+
continue;
|
|
1419
|
+
}
|
|
1420
|
+
if (/^[-*_]{3,}\s*$/.test(line)) {
|
|
1421
|
+
blocks.push({ type: "hr" });
|
|
1422
|
+
i++;
|
|
1423
|
+
continue;
|
|
1424
|
+
}
|
|
1425
|
+
const docEmbedMatch = line.match(/^!\[\[([^\]|]+?)(?:\|[^\]]*?)?\]\](\{[^}]*\})?\s*$/);
|
|
1426
|
+
if (docEmbedMatch) {
|
|
1427
|
+
const props = parseMdcProps(docEmbedMatch[2]);
|
|
1428
|
+
const seamless = "seamless" in props || props["seamless"] === "true" || /\{[^}]*\bseamless\b[^}]*\}/.test(docEmbedMatch[2] ?? "");
|
|
1429
|
+
blocks.push({
|
|
1430
|
+
type: "docEmbed",
|
|
1431
|
+
docId: docEmbedMatch[1],
|
|
1432
|
+
seamless: seamless || void 0
|
|
1433
|
+
});
|
|
1434
|
+
i++;
|
|
1435
|
+
continue;
|
|
1436
|
+
}
|
|
1437
|
+
const imgMatch = line.match(/^!\[([^\]]*)\]\(([^)]+)\)(\{[^}]*\})?\s*$/);
|
|
1438
|
+
if (imgMatch) {
|
|
1439
|
+
const alt = imgMatch[1] ?? "";
|
|
1440
|
+
const src = imgMatch[2] ?? "";
|
|
1441
|
+
const attrs = parseMdcProps(imgMatch[3]);
|
|
1442
|
+
blocks.push({
|
|
1443
|
+
type: "image",
|
|
1444
|
+
src,
|
|
1445
|
+
alt,
|
|
1446
|
+
width: attrs["width"],
|
|
1447
|
+
height: attrs["height"]
|
|
1448
|
+
});
|
|
1449
|
+
i++;
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
if (line.startsWith("> ") || line === ">") {
|
|
1453
|
+
const bqLines = [];
|
|
1454
|
+
while (i < lines.length && (lines[i].startsWith("> ") || lines[i] === ">")) {
|
|
1455
|
+
bqLines.push(lines[i].replace(/^>\s?/, ""));
|
|
1456
|
+
i++;
|
|
1457
|
+
}
|
|
1458
|
+
blocks.push({
|
|
1459
|
+
type: "blockquote",
|
|
1460
|
+
lines: bqLines
|
|
1461
|
+
});
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
if (/^\s*\|/.test(line)) {
|
|
1465
|
+
const tableLines = [];
|
|
1466
|
+
while (i < lines.length && /^\s*\|/.test(lines[i])) {
|
|
1467
|
+
tableLines.push(lines[i]);
|
|
1468
|
+
i++;
|
|
1469
|
+
}
|
|
1470
|
+
if (tableLines.length >= 2 && isTableSeparator(tableLines[1])) {
|
|
1471
|
+
const headerRow = parseTableRow(tableLines[0]);
|
|
1472
|
+
const dataRows = tableLines.slice(2).filter((l) => !isTableSeparator(l)).map(parseTableRow);
|
|
1473
|
+
blocks.push({
|
|
1474
|
+
type: "table",
|
|
1475
|
+
headerRow,
|
|
1476
|
+
dataRows
|
|
1477
|
+
});
|
|
1478
|
+
} else for (const l of tableLines) blocks.push({
|
|
1479
|
+
type: "paragraph",
|
|
1480
|
+
text: l
|
|
1481
|
+
});
|
|
1482
|
+
continue;
|
|
1483
|
+
}
|
|
1484
|
+
const MDC_OPEN = /^\s*(:{2,})(\w[\w-]*)(\{[^}]*\})?\s*$/;
|
|
1485
|
+
if (MDC_OPEN.test(line)) {
|
|
1486
|
+
const colons = line.match(/^\s*(:+)/)?.[1]?.length ?? 2;
|
|
1487
|
+
const componentName = line.match(/^\s*:{2,}(\w[\w-]*)/)?.[1] ?? "";
|
|
1488
|
+
const innerLines = [];
|
|
1489
|
+
i++;
|
|
1490
|
+
while (i < lines.length) {
|
|
1491
|
+
const l = lines[i];
|
|
1492
|
+
if (new RegExp(`^\\s*:{${colons}}\\s*$`).test(l)) {
|
|
1493
|
+
i++;
|
|
1494
|
+
break;
|
|
1495
|
+
}
|
|
1496
|
+
const innerFence = l.match(/^(\s*`{3,})/);
|
|
1497
|
+
if (innerFence) {
|
|
1498
|
+
const fenceStr = innerFence[1].trimStart();
|
|
1499
|
+
innerLines.push(l);
|
|
1500
|
+
i++;
|
|
1501
|
+
while (i < lines.length && !lines[i].trimStart().startsWith(fenceStr)) {
|
|
1502
|
+
innerLines.push(lines[i]);
|
|
1503
|
+
i++;
|
|
1504
|
+
}
|
|
1505
|
+
if (i < lines.length) {
|
|
1506
|
+
innerLines.push(lines[i]);
|
|
1507
|
+
i++;
|
|
1508
|
+
}
|
|
1509
|
+
continue;
|
|
1510
|
+
}
|
|
1511
|
+
innerLines.push(l);
|
|
1512
|
+
i++;
|
|
1513
|
+
}
|
|
1514
|
+
const nonBlank = innerLines.filter((l) => l.trim().length > 0);
|
|
1515
|
+
if (nonBlank.length) {
|
|
1516
|
+
const minIndent = Math.min(...nonBlank.map((l) => l.match(/^(\s*)/)?.[1]?.length ?? 0));
|
|
1517
|
+
if (minIndent > 0) for (let j = 0; j < innerLines.length; j++) innerLines[j] = innerLines[j].slice(Math.min(minIndent, innerLines[j].length));
|
|
1518
|
+
}
|
|
1519
|
+
let contentStart = 0;
|
|
1520
|
+
if (innerLines[0]?.trim() === "---") {
|
|
1521
|
+
const fmEnd = innerLines.findIndex((l, idx) => idx > 0 && l.trim() === "---");
|
|
1522
|
+
if (fmEnd !== -1) contentStart = fmEnd + 1;
|
|
1523
|
+
}
|
|
1524
|
+
const contentLines = innerLines.slice(contentStart);
|
|
1525
|
+
const defaultSlotLines = [];
|
|
1526
|
+
const codeSlotLines = [];
|
|
1527
|
+
let currentSlot = "default";
|
|
1528
|
+
for (const l of contentLines) {
|
|
1529
|
+
if (/^#code\s*$/.test(l)) {
|
|
1530
|
+
currentSlot = "code";
|
|
1531
|
+
continue;
|
|
1532
|
+
}
|
|
1533
|
+
if (/^#\w+/.test(l) && !/^#{2,}\s/.test(l)) {
|
|
1534
|
+
currentSlot = "other";
|
|
1535
|
+
continue;
|
|
1536
|
+
}
|
|
1537
|
+
if (currentSlot === "default") defaultSlotLines.push(l);
|
|
1538
|
+
else if (currentSlot === "code") codeSlotLines.push(l);
|
|
1539
|
+
}
|
|
1540
|
+
const innerBlocks = parseBlocks(defaultSlotLines.join("\n"));
|
|
1541
|
+
const codeBlocks = extractFencedCode(codeSlotLines);
|
|
1542
|
+
if (new Set([
|
|
1543
|
+
"tip",
|
|
1544
|
+
"note",
|
|
1545
|
+
"info",
|
|
1546
|
+
"warning",
|
|
1547
|
+
"caution",
|
|
1548
|
+
"danger",
|
|
1549
|
+
"callout",
|
|
1550
|
+
"alert"
|
|
1551
|
+
]).has(componentName.toLowerCase())) blocks.push({
|
|
1552
|
+
type: "callout",
|
|
1553
|
+
calloutType: componentName.toLowerCase(),
|
|
1554
|
+
innerBlocks
|
|
1555
|
+
});
|
|
1556
|
+
else {
|
|
1557
|
+
const mdcProps = parseMdcProps(line.match(MDC_OPEN)?.[3]);
|
|
1558
|
+
const lc = componentName.toLowerCase();
|
|
1559
|
+
if (lc === "collapsible") blocks.push({
|
|
1560
|
+
type: "collapsible",
|
|
1561
|
+
label: mdcProps["label"] || "Details",
|
|
1562
|
+
open: mdcProps["open"] === "true",
|
|
1563
|
+
innerBlocks
|
|
1564
|
+
});
|
|
1565
|
+
else if (lc === "steps") blocks.push({
|
|
1566
|
+
type: "steps",
|
|
1567
|
+
innerBlocks
|
|
1568
|
+
});
|
|
1569
|
+
else if (lc === "card") blocks.push({
|
|
1570
|
+
type: "card",
|
|
1571
|
+
title: mdcProps["title"] || "",
|
|
1572
|
+
icon: mdcProps["icon"] || "",
|
|
1573
|
+
to: mdcProps["to"] || "",
|
|
1574
|
+
innerBlocks
|
|
1575
|
+
});
|
|
1576
|
+
else if (lc === "card-group") {
|
|
1577
|
+
const cards = innerBlocks.filter((b) => b.type === "card");
|
|
1578
|
+
if (cards.length) blocks.push({
|
|
1579
|
+
type: "cardGroup",
|
|
1580
|
+
cards
|
|
1581
|
+
});
|
|
1582
|
+
else blocks.push(...innerBlocks);
|
|
1583
|
+
} else if (lc === "code-collapse") blocks.push({
|
|
1584
|
+
type: "codeCollapse",
|
|
1585
|
+
codeBlocks: codeBlocks.length ? codeBlocks : innerBlocks.filter((b) => b.type === "codeBlock")
|
|
1586
|
+
});
|
|
1587
|
+
else if (lc === "code-group") {
|
|
1588
|
+
const allCode = [...innerBlocks.filter((b) => b.type === "codeBlock"), ...codeBlocks];
|
|
1589
|
+
blocks.push({
|
|
1590
|
+
type: "codeGroup",
|
|
1591
|
+
codeBlocks: allCode
|
|
1592
|
+
});
|
|
1593
|
+
} else if (lc === "code-preview") blocks.push({
|
|
1594
|
+
type: "codePreview",
|
|
1595
|
+
innerBlocks,
|
|
1596
|
+
codeBlocks
|
|
1597
|
+
});
|
|
1598
|
+
else if (lc === "code-tree") blocks.push({
|
|
1599
|
+
type: "codeTree",
|
|
1600
|
+
files: mdcProps["files"] || "[]"
|
|
1601
|
+
});
|
|
1602
|
+
else if (lc === "accordion") {
|
|
1603
|
+
const items = parseMdcChildren(contentLines, "item");
|
|
1604
|
+
if (items.length) blocks.push({
|
|
1605
|
+
type: "accordion",
|
|
1606
|
+
items
|
|
1607
|
+
});
|
|
1608
|
+
else blocks.push({
|
|
1609
|
+
type: "accordion",
|
|
1610
|
+
items: [{
|
|
1611
|
+
label: "Item 1",
|
|
1612
|
+
icon: "",
|
|
1613
|
+
innerBlocks
|
|
1614
|
+
}]
|
|
1615
|
+
});
|
|
1616
|
+
} else if (lc === "tabs") {
|
|
1617
|
+
const items = parseMdcChildren(contentLines, "tab");
|
|
1618
|
+
if (items.length) blocks.push({
|
|
1619
|
+
type: "tabs",
|
|
1620
|
+
items
|
|
1621
|
+
});
|
|
1622
|
+
else blocks.push({
|
|
1623
|
+
type: "tabs",
|
|
1624
|
+
items: [{
|
|
1625
|
+
label: "Tab 1",
|
|
1626
|
+
icon: "",
|
|
1627
|
+
innerBlocks
|
|
1628
|
+
}]
|
|
1629
|
+
});
|
|
1630
|
+
} else if (lc === "field") blocks.push({
|
|
1631
|
+
type: "field",
|
|
1632
|
+
name: mdcProps["name"] || "",
|
|
1633
|
+
fieldType: mdcProps["type"] || "string",
|
|
1634
|
+
required: mdcProps["required"] === "true",
|
|
1635
|
+
innerBlocks
|
|
1636
|
+
});
|
|
1637
|
+
else if (lc === "field-group") {
|
|
1638
|
+
const fields = innerBlocks.filter((b) => b.type === "field");
|
|
1639
|
+
if (fields.length) blocks.push({
|
|
1640
|
+
type: "fieldGroup",
|
|
1641
|
+
fields
|
|
1642
|
+
});
|
|
1643
|
+
else blocks.push(...innerBlocks);
|
|
1644
|
+
} else {
|
|
1645
|
+
blocks.push(...innerBlocks);
|
|
1646
|
+
blocks.push(...codeBlocks);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (TASK_RE.test(line)) {
|
|
1652
|
+
const items = [];
|
|
1653
|
+
while (i < lines.length && TASK_RE.test(lines[i])) {
|
|
1654
|
+
const m = lines[i].match(TASK_RE);
|
|
1655
|
+
items.push({
|
|
1656
|
+
checked: m[1].toLowerCase() === "x",
|
|
1657
|
+
text: m[2]
|
|
1658
|
+
});
|
|
1659
|
+
i++;
|
|
1660
|
+
}
|
|
1661
|
+
blocks.push({
|
|
1662
|
+
type: "taskList",
|
|
1663
|
+
items
|
|
1664
|
+
});
|
|
1665
|
+
continue;
|
|
1666
|
+
}
|
|
1667
|
+
if (/^[-*+]\s+/.test(line)) {
|
|
1668
|
+
const items = [];
|
|
1669
|
+
while (i < lines.length && /^[-*+]\s+/.test(lines[i]) && !TASK_RE.test(lines[i])) {
|
|
1670
|
+
items.push(lines[i].replace(/^[-*+]\s+/, ""));
|
|
1671
|
+
i++;
|
|
1672
|
+
}
|
|
1673
|
+
if (items.length) {
|
|
1674
|
+
blocks.push({
|
|
1675
|
+
type: "bulletList",
|
|
1676
|
+
items
|
|
1677
|
+
});
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
if (/^\d+\.\s+/.test(line)) {
|
|
1682
|
+
const items = [];
|
|
1683
|
+
while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
|
|
1684
|
+
items.push(lines[i].replace(/^\d+\.\s+/, ""));
|
|
1685
|
+
i++;
|
|
1686
|
+
}
|
|
1687
|
+
blocks.push({
|
|
1688
|
+
type: "orderedList",
|
|
1689
|
+
items
|
|
1690
|
+
});
|
|
1691
|
+
continue;
|
|
1692
|
+
}
|
|
1693
|
+
if (line.trim() === "") {
|
|
1694
|
+
i++;
|
|
1695
|
+
continue;
|
|
1696
|
+
}
|
|
1697
|
+
const paraLines = [];
|
|
1698
|
+
while (i < lines.length && lines[i].trim() !== "" && !/^(#{1,6}\s|[-*+]\s|\d+\.\s|>|`{3,}|\s*\||[-*_]{3,}\s*$|\s*:{2,}\w)/.test(lines[i])) {
|
|
1699
|
+
paraLines.push(lines[i]);
|
|
1700
|
+
i++;
|
|
1701
|
+
}
|
|
1702
|
+
if (paraLines.length) blocks.push({
|
|
1703
|
+
type: "paragraph",
|
|
1704
|
+
text: paraLines.join(" ")
|
|
1705
|
+
});
|
|
1706
|
+
}
|
|
1707
|
+
return blocks;
|
|
1708
|
+
}
|
|
1709
|
+
function fillTextInto(el, tokens) {
|
|
1710
|
+
const filtered = tokens.filter((t) => t.node || t.text.length > 0);
|
|
1711
|
+
if (!filtered.length) return;
|
|
1712
|
+
const children = filtered.map((tok) => {
|
|
1713
|
+
if (tok.node) {
|
|
1714
|
+
const xe = new yjs.XmlElement(tok.node);
|
|
1715
|
+
if (tok.nodeAttrs) for (const [k, v] of Object.entries(tok.nodeAttrs)) xe.setAttribute(k, v);
|
|
1716
|
+
return xe;
|
|
1717
|
+
}
|
|
1718
|
+
return new yjs.XmlText();
|
|
1719
|
+
});
|
|
1720
|
+
el.insert(0, children);
|
|
1721
|
+
filtered.forEach((tok, i) => {
|
|
1722
|
+
if (tok.node) return;
|
|
1723
|
+
const xt = children[i];
|
|
1724
|
+
if (tok.attrs) xt.insert(0, tok.text, tok.attrs);
|
|
1725
|
+
else xt.insert(0, tok.text);
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
function blockElName(b) {
|
|
1729
|
+
switch (b.type) {
|
|
1730
|
+
case "heading": return "heading";
|
|
1731
|
+
case "paragraph": return "paragraph";
|
|
1732
|
+
case "bulletList": return "bulletList";
|
|
1733
|
+
case "orderedList": return "orderedList";
|
|
1734
|
+
case "taskList": return "taskList";
|
|
1735
|
+
case "codeBlock": return "codeBlock";
|
|
1736
|
+
case "blockquote": return "blockquote";
|
|
1737
|
+
case "table": return "table";
|
|
1738
|
+
case "hr": return "horizontalRule";
|
|
1739
|
+
case "callout": return "callout";
|
|
1740
|
+
case "collapsible": return "collapsible";
|
|
1741
|
+
case "steps": return "steps";
|
|
1742
|
+
case "card": return "card";
|
|
1743
|
+
case "cardGroup": return "cardGroup";
|
|
1744
|
+
case "codeCollapse": return "codeCollapse";
|
|
1745
|
+
case "codeGroup": return "codeGroup";
|
|
1746
|
+
case "codePreview": return "codePreview";
|
|
1747
|
+
case "codeTree": return "codeTree";
|
|
1748
|
+
case "accordion": return "accordion";
|
|
1749
|
+
case "tabs": return "tabs";
|
|
1750
|
+
case "field": return "field";
|
|
1751
|
+
case "fieldGroup": return "fieldGroup";
|
|
1752
|
+
case "image": return "image";
|
|
1753
|
+
case "docEmbed": return "docEmbed";
|
|
1754
|
+
case "svgEmbed": return "svgEmbed";
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
function fillBlock(el, block) {
|
|
1758
|
+
switch (block.type) {
|
|
1759
|
+
case "heading":
|
|
1760
|
+
el.setAttribute("level", block.level);
|
|
1761
|
+
fillTextInto(el, parseInline(block.text));
|
|
1762
|
+
break;
|
|
1763
|
+
case "paragraph":
|
|
1764
|
+
fillTextInto(el, parseInline(block.text));
|
|
1765
|
+
break;
|
|
1766
|
+
case "bulletList":
|
|
1767
|
+
case "orderedList": {
|
|
1768
|
+
const listItemEls = block.items.map(() => new yjs.XmlElement("listItem"));
|
|
1769
|
+
el.insert(0, listItemEls);
|
|
1770
|
+
block.items.forEach((text, i) => {
|
|
1771
|
+
const paraEl = new yjs.XmlElement("paragraph");
|
|
1772
|
+
listItemEls[i].insert(0, [paraEl]);
|
|
1773
|
+
fillTextInto(paraEl, parseInline(text));
|
|
1774
|
+
});
|
|
1775
|
+
break;
|
|
1776
|
+
}
|
|
1777
|
+
case "taskList": {
|
|
1778
|
+
const taskItemEls = block.items.map(() => new yjs.XmlElement("taskItem"));
|
|
1779
|
+
el.insert(0, taskItemEls);
|
|
1780
|
+
block.items.forEach((item, i) => {
|
|
1781
|
+
taskItemEls[i].setAttribute("checked", item.checked);
|
|
1782
|
+
const paraEl = new yjs.XmlElement("paragraph");
|
|
1783
|
+
taskItemEls[i].insert(0, [paraEl]);
|
|
1784
|
+
fillTextInto(paraEl, parseInline(item.text));
|
|
1785
|
+
});
|
|
1786
|
+
break;
|
|
1787
|
+
}
|
|
1788
|
+
case "codeBlock": {
|
|
1789
|
+
if (block.lang) el.setAttribute("language", block.lang);
|
|
1790
|
+
const xt = new yjs.XmlText();
|
|
1791
|
+
el.insert(0, [xt]);
|
|
1792
|
+
xt.insert(0, block.code);
|
|
1793
|
+
break;
|
|
1794
|
+
}
|
|
1795
|
+
case "blockquote": {
|
|
1796
|
+
const paraEls = block.lines.map(() => new yjs.XmlElement("paragraph"));
|
|
1797
|
+
el.insert(0, paraEls);
|
|
1798
|
+
block.lines.forEach((line, i) => fillTextInto(paraEls[i], parseInline(line)));
|
|
1799
|
+
break;
|
|
1800
|
+
}
|
|
1801
|
+
case "table": {
|
|
1802
|
+
const headerRowEl = new yjs.XmlElement("tableRow");
|
|
1803
|
+
const dataRowEls = block.dataRows.map(() => new yjs.XmlElement("tableRow"));
|
|
1804
|
+
el.insert(0, [headerRowEl, ...dataRowEls]);
|
|
1805
|
+
const headerCellEls = block.headerRow.map(() => new yjs.XmlElement("tableHeader"));
|
|
1806
|
+
headerRowEl.insert(0, headerCellEls);
|
|
1807
|
+
block.headerRow.forEach((cellText, i) => {
|
|
1808
|
+
const paraEl = new yjs.XmlElement("paragraph");
|
|
1809
|
+
headerCellEls[i].insert(0, [paraEl]);
|
|
1810
|
+
fillTextInto(paraEl, parseInline(cellText));
|
|
1811
|
+
});
|
|
1812
|
+
block.dataRows.forEach((row, ri) => {
|
|
1813
|
+
const cellEls = row.map(() => new yjs.XmlElement("tableCell"));
|
|
1814
|
+
dataRowEls[ri].insert(0, cellEls);
|
|
1815
|
+
row.forEach((cellText, ci) => {
|
|
1816
|
+
const paraEl = new yjs.XmlElement("paragraph");
|
|
1817
|
+
cellEls[ci].insert(0, [paraEl]);
|
|
1818
|
+
fillTextInto(paraEl, parseInline(cellText));
|
|
1819
|
+
});
|
|
1820
|
+
});
|
|
1821
|
+
break;
|
|
1822
|
+
}
|
|
1823
|
+
case "hr": break;
|
|
1824
|
+
case "callout": {
|
|
1825
|
+
el.setAttribute("type", block.calloutType);
|
|
1826
|
+
if (!block.innerBlocks.length) {
|
|
1827
|
+
const paraEl = new yjs.XmlElement("paragraph");
|
|
1828
|
+
el.insert(0, [paraEl]);
|
|
1829
|
+
break;
|
|
1830
|
+
}
|
|
1831
|
+
const innerEls = block.innerBlocks.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1832
|
+
el.insert(0, innerEls);
|
|
1833
|
+
block.innerBlocks.forEach((b, i) => fillBlock(innerEls[i], b));
|
|
1834
|
+
break;
|
|
1835
|
+
}
|
|
1836
|
+
case "collapsible": {
|
|
1837
|
+
el.setAttribute("label", block.label);
|
|
1838
|
+
el.setAttribute("open", block.open);
|
|
1839
|
+
const inner = block.innerBlocks.length ? block.innerBlocks : [{
|
|
1840
|
+
type: "paragraph",
|
|
1841
|
+
text: ""
|
|
1842
|
+
}];
|
|
1843
|
+
const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1844
|
+
el.insert(0, innerEls);
|
|
1845
|
+
inner.forEach((b, i) => fillBlock(innerEls[i], b));
|
|
1846
|
+
break;
|
|
1847
|
+
}
|
|
1848
|
+
case "steps": {
|
|
1849
|
+
const inner = block.innerBlocks.length ? block.innerBlocks : [{
|
|
1850
|
+
type: "paragraph",
|
|
1851
|
+
text: ""
|
|
1852
|
+
}];
|
|
1853
|
+
const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1854
|
+
el.insert(0, innerEls);
|
|
1855
|
+
inner.forEach((b, i) => fillBlock(innerEls[i], b));
|
|
1856
|
+
break;
|
|
1857
|
+
}
|
|
1858
|
+
case "card": {
|
|
1859
|
+
if (block.title) el.setAttribute("title", block.title);
|
|
1860
|
+
if (block.icon) el.setAttribute("icon", block.icon);
|
|
1861
|
+
if (block.to) el.setAttribute("to", block.to);
|
|
1862
|
+
const inner = block.innerBlocks.length ? block.innerBlocks : [{
|
|
1863
|
+
type: "paragraph",
|
|
1864
|
+
text: ""
|
|
1865
|
+
}];
|
|
1866
|
+
const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1867
|
+
el.insert(0, innerEls);
|
|
1868
|
+
inner.forEach((b, i) => fillBlock(innerEls[i], b));
|
|
1869
|
+
break;
|
|
1870
|
+
}
|
|
1871
|
+
case "cardGroup": {
|
|
1872
|
+
const cardEls = block.cards.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1873
|
+
el.insert(0, cardEls);
|
|
1874
|
+
block.cards.forEach((b, i) => fillBlock(cardEls[i], b));
|
|
1875
|
+
break;
|
|
1876
|
+
}
|
|
1877
|
+
case "codeCollapse": {
|
|
1878
|
+
const codes = block.codeBlocks.length ? block.codeBlocks : [{
|
|
1879
|
+
type: "codeBlock",
|
|
1880
|
+
lang: "",
|
|
1881
|
+
code: ""
|
|
1882
|
+
}];
|
|
1883
|
+
const codeEl = new yjs.XmlElement("codeBlock");
|
|
1884
|
+
el.insert(0, [codeEl]);
|
|
1885
|
+
fillBlock(codeEl, codes[0]);
|
|
1886
|
+
break;
|
|
1887
|
+
}
|
|
1888
|
+
case "codeGroup": {
|
|
1889
|
+
const codes = block.codeBlocks.length ? block.codeBlocks : [{
|
|
1890
|
+
type: "codeBlock",
|
|
1891
|
+
lang: "",
|
|
1892
|
+
code: ""
|
|
1893
|
+
}];
|
|
1894
|
+
const codeEls = codes.map(() => new yjs.XmlElement("codeBlock"));
|
|
1895
|
+
el.insert(0, codeEls);
|
|
1896
|
+
codes.forEach((b, i) => fillBlock(codeEls[i], b));
|
|
1897
|
+
break;
|
|
1898
|
+
}
|
|
1899
|
+
case "codePreview": {
|
|
1900
|
+
const all = [...block.innerBlocks, ...block.codeBlocks];
|
|
1901
|
+
const inner = all.length ? all : [{
|
|
1902
|
+
type: "paragraph",
|
|
1903
|
+
text: ""
|
|
1904
|
+
}];
|
|
1905
|
+
const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1906
|
+
el.insert(0, innerEls);
|
|
1907
|
+
inner.forEach((b, i) => fillBlock(innerEls[i], b));
|
|
1908
|
+
break;
|
|
1909
|
+
}
|
|
1910
|
+
case "codeTree":
|
|
1911
|
+
el.setAttribute("files", block.files);
|
|
1912
|
+
break;
|
|
1913
|
+
case "accordion": {
|
|
1914
|
+
const itemEls = block.items.map(() => new yjs.XmlElement("accordionItem"));
|
|
1915
|
+
el.insert(0, itemEls);
|
|
1916
|
+
block.items.forEach((item, i) => {
|
|
1917
|
+
itemEls[i].setAttribute("label", item.label);
|
|
1918
|
+
if (item.icon) itemEls[i].setAttribute("icon", item.icon);
|
|
1919
|
+
const inner = item.innerBlocks.length ? item.innerBlocks : [{
|
|
1920
|
+
type: "paragraph",
|
|
1921
|
+
text: ""
|
|
1922
|
+
}];
|
|
1923
|
+
const childEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1924
|
+
itemEls[i].insert(0, childEls);
|
|
1925
|
+
inner.forEach((b, ci) => fillBlock(childEls[ci], b));
|
|
1926
|
+
});
|
|
1927
|
+
break;
|
|
1928
|
+
}
|
|
1929
|
+
case "tabs": {
|
|
1930
|
+
const itemEls = block.items.map(() => new yjs.XmlElement("tabsItem"));
|
|
1931
|
+
el.insert(0, itemEls);
|
|
1932
|
+
block.items.forEach((item, i) => {
|
|
1933
|
+
itemEls[i].setAttribute("label", item.label);
|
|
1934
|
+
if (item.icon) itemEls[i].setAttribute("icon", item.icon);
|
|
1935
|
+
const inner = item.innerBlocks.length ? item.innerBlocks : [{
|
|
1936
|
+
type: "paragraph",
|
|
1937
|
+
text: ""
|
|
1938
|
+
}];
|
|
1939
|
+
const childEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1940
|
+
itemEls[i].insert(0, childEls);
|
|
1941
|
+
inner.forEach((b, ci) => fillBlock(childEls[ci], b));
|
|
1942
|
+
});
|
|
1943
|
+
break;
|
|
1944
|
+
}
|
|
1945
|
+
case "field": {
|
|
1946
|
+
if (block.name) el.setAttribute("name", block.name);
|
|
1947
|
+
el.setAttribute("type", block.fieldType);
|
|
1948
|
+
el.setAttribute("required", block.required);
|
|
1949
|
+
const inner = block.innerBlocks.length ? block.innerBlocks : [{
|
|
1950
|
+
type: "paragraph",
|
|
1951
|
+
text: ""
|
|
1952
|
+
}];
|
|
1953
|
+
const innerEls = inner.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1954
|
+
el.insert(0, innerEls);
|
|
1955
|
+
inner.forEach((b, i) => fillBlock(innerEls[i], b));
|
|
1956
|
+
break;
|
|
1957
|
+
}
|
|
1958
|
+
case "fieldGroup": {
|
|
1959
|
+
const fieldEls = block.fields.map((b) => new yjs.XmlElement(blockElName(b)));
|
|
1960
|
+
el.insert(0, fieldEls);
|
|
1961
|
+
block.fields.forEach((b, i) => fillBlock(fieldEls[i], b));
|
|
1962
|
+
break;
|
|
1963
|
+
}
|
|
1964
|
+
case "image":
|
|
1965
|
+
el.setAttribute("src", block.src);
|
|
1966
|
+
if (block.alt) el.setAttribute("alt", block.alt);
|
|
1967
|
+
if (block.width) el.setAttribute("width", block.width);
|
|
1968
|
+
if (block.height) el.setAttribute("height", block.height);
|
|
1969
|
+
break;
|
|
1970
|
+
case "docEmbed":
|
|
1971
|
+
el.setAttribute("docId", block.docId);
|
|
1972
|
+
if (block.seamless) el.setAttribute("seamless", "true");
|
|
1973
|
+
break;
|
|
1974
|
+
case "svgEmbed":
|
|
1975
|
+
el.setAttribute("svg", block.svg);
|
|
1976
|
+
if (block.title) el.setAttribute("title", block.title);
|
|
1977
|
+
break;
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
function populateYDocFromMarkdown(fragment, markdown, fallbackTitle = "Untitled") {
|
|
1981
|
+
const ydoc = fragment.doc;
|
|
1982
|
+
if (!ydoc) {
|
|
1983
|
+
console.warn("[markdownToYjs] fragment has no doc — skipping population");
|
|
1984
|
+
return;
|
|
1985
|
+
}
|
|
1986
|
+
const blocks = parseBlocks(markdown);
|
|
1987
|
+
let title = fallbackTitle;
|
|
1988
|
+
let contentBlocks = blocks;
|
|
1989
|
+
const h1 = blocks.findIndex((b) => b.type === "heading" && b.level === 1);
|
|
1990
|
+
if (h1 !== -1) {
|
|
1991
|
+
title = blocks[h1].text;
|
|
1992
|
+
contentBlocks = blocks.filter((_, i) => i !== h1);
|
|
1993
|
+
}
|
|
1994
|
+
if (!contentBlocks.length) contentBlocks = [{
|
|
1995
|
+
type: "paragraph",
|
|
1996
|
+
text: ""
|
|
1997
|
+
}];
|
|
1998
|
+
ydoc.transact(() => {
|
|
1999
|
+
const headerEl = new yjs.XmlElement("documentHeader");
|
|
2000
|
+
const metaEl = new yjs.XmlElement("documentMeta");
|
|
2001
|
+
const bodyEls = contentBlocks.map((b) => {
|
|
2002
|
+
switch (b.type) {
|
|
2003
|
+
case "heading": return new yjs.XmlElement("heading");
|
|
2004
|
+
case "paragraph": return new yjs.XmlElement("paragraph");
|
|
2005
|
+
case "bulletList": return new yjs.XmlElement("bulletList");
|
|
2006
|
+
case "orderedList": return new yjs.XmlElement("orderedList");
|
|
2007
|
+
case "taskList": return new yjs.XmlElement("taskList");
|
|
2008
|
+
case "codeBlock": return new yjs.XmlElement("codeBlock");
|
|
2009
|
+
case "blockquote": return new yjs.XmlElement("blockquote");
|
|
2010
|
+
case "table": return new yjs.XmlElement("table");
|
|
2011
|
+
case "hr": return new yjs.XmlElement("horizontalRule");
|
|
2012
|
+
case "callout": return new yjs.XmlElement("callout");
|
|
2013
|
+
case "collapsible": return new yjs.XmlElement("collapsible");
|
|
2014
|
+
case "steps": return new yjs.XmlElement("steps");
|
|
2015
|
+
case "card": return new yjs.XmlElement("card");
|
|
2016
|
+
case "cardGroup": return new yjs.XmlElement("cardGroup");
|
|
2017
|
+
case "codeCollapse": return new yjs.XmlElement("codeCollapse");
|
|
2018
|
+
case "codeGroup": return new yjs.XmlElement("codeGroup");
|
|
2019
|
+
case "codePreview": return new yjs.XmlElement("codePreview");
|
|
2020
|
+
case "codeTree": return new yjs.XmlElement("codeTree");
|
|
2021
|
+
case "accordion": return new yjs.XmlElement("accordion");
|
|
2022
|
+
case "tabs": return new yjs.XmlElement("tabs");
|
|
2023
|
+
case "field": return new yjs.XmlElement("field");
|
|
2024
|
+
case "fieldGroup": return new yjs.XmlElement("fieldGroup");
|
|
2025
|
+
case "image": return new yjs.XmlElement("image");
|
|
2026
|
+
case "docEmbed": return new yjs.XmlElement("docEmbed");
|
|
2027
|
+
case "svgEmbed": return new yjs.XmlElement("svgEmbed");
|
|
2028
|
+
}
|
|
2029
|
+
});
|
|
2030
|
+
fragment.insert(0, [
|
|
2031
|
+
headerEl,
|
|
2032
|
+
metaEl,
|
|
2033
|
+
...bodyEls
|
|
2034
|
+
]);
|
|
2035
|
+
const headerXt = new yjs.XmlText();
|
|
2036
|
+
headerEl.insert(0, [headerXt]);
|
|
2037
|
+
headerXt.insert(0, title);
|
|
2038
|
+
contentBlocks.forEach((block, i) => fillBlock(bodyEls[i], block));
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
//#endregion
|
|
2043
|
+
//#region packages/orchestrator/src/actions/content.ts
|
|
2044
|
+
async function executeWriteContent(actor, action) {
|
|
2045
|
+
const provider = await actor.getChildProvider(action.docId);
|
|
2046
|
+
const fragment = provider.document.getXmlFragment("default");
|
|
2047
|
+
provider.document.transact(() => {
|
|
2048
|
+
while (fragment.length > 0) fragment.delete(0, 1);
|
|
2049
|
+
});
|
|
2050
|
+
populateYDocFromMarkdown(fragment, action.markdown);
|
|
2051
|
+
}
|
|
2052
|
+
async function executeDeleteContent(actor, action) {
|
|
2053
|
+
const provider = await actor.getChildProvider(action.docId);
|
|
2054
|
+
const fragment = provider.document.getXmlFragment("default");
|
|
2055
|
+
provider.document.transact(() => {
|
|
2056
|
+
const adjustedFrom = bodyStartIndex(fragment) + action.from;
|
|
2057
|
+
const count = Math.min(action.length, fragment.length - adjustedFrom);
|
|
2058
|
+
if (count > 0) fragment.delete(adjustedFrom, count);
|
|
2059
|
+
});
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
//#endregion
|
|
2063
|
+
//#region packages/orchestrator/src/actions/awareness.ts
|
|
2064
|
+
async function executeSetStatus(actor, action) {
|
|
2065
|
+
actor.setRootAwareness("status", action.status);
|
|
2066
|
+
}
|
|
2067
|
+
async function executeSetAwareness(actor, action) {
|
|
2068
|
+
if (action.docId) {
|
|
2069
|
+
await actor.getChildProvider(action.docId);
|
|
2070
|
+
for (const [key, value] of Object.entries(action.fields)) actor.setChildAwareness(action.docId, key, value);
|
|
2071
|
+
} else for (const [key, value] of Object.entries(action.fields)) actor.setRootAwareness(key, value);
|
|
2072
|
+
}
|
|
2073
|
+
async function executeClearAwareness(actor, action) {
|
|
2074
|
+
if (action.docId) for (const field of action.fields) actor.setChildAwareness(action.docId, field, null);
|
|
2075
|
+
else for (const field of action.fields) actor.setRootAwareness(field, null);
|
|
2076
|
+
}
|
|
2077
|
+
async function executePointerMove(actor, action) {
|
|
2078
|
+
const easing = getEasing(action.easing);
|
|
2079
|
+
const steps = Math.max(1, Math.round(action.duration / 16));
|
|
2080
|
+
const stepDuration = action.duration / steps;
|
|
2081
|
+
for (let i = 0; i <= steps; i++) {
|
|
2082
|
+
const t = easing(i / steps);
|
|
2083
|
+
const x = action.from.x + (action.to.x - action.from.x) * t;
|
|
2084
|
+
const y = action.from.y + (action.to.y - action.from.y) * t;
|
|
2085
|
+
actor.setChildAwareness(action.docId, "pos", {
|
|
2086
|
+
x,
|
|
2087
|
+
y
|
|
2088
|
+
});
|
|
2089
|
+
if (i < steps) await sleep(stepDuration);
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
async function executeScrollTo(actor, action) {
|
|
2093
|
+
actor.setChildAwareness(action.docId, "doc:scroll", action.position);
|
|
2094
|
+
}
|
|
2095
|
+
async function executeKanbanHover(actor, action) {
|
|
2096
|
+
actor.setChildAwareness(action.docId, "kanban:hovering", action.cardId);
|
|
2097
|
+
}
|
|
2098
|
+
async function executeKanbanDrag(actor, action) {
|
|
2099
|
+
actor.setChildAwareness(action.docId, "kanban:dragging", {
|
|
2100
|
+
cardId: action.cardId,
|
|
2101
|
+
toColumnId: action.toColumnId
|
|
2102
|
+
});
|
|
2103
|
+
await sleep(action.duration);
|
|
2104
|
+
actor.setChildAwareness(action.docId, "kanban:dragging", null);
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
//#endregion
|
|
2108
|
+
//#region packages/orchestrator/src/actions/document.ts
|
|
2109
|
+
async function executeCreateDocument(actor, action, vars) {
|
|
2110
|
+
const rootProvider = actor.rootProvider;
|
|
2111
|
+
if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`);
|
|
2112
|
+
const treeMap = rootProvider.document.getMap("doc-tree");
|
|
2113
|
+
const id = crypto.randomUUID();
|
|
2114
|
+
const now = Date.now();
|
|
2115
|
+
const normalizedParent = action.parentId === actor.rootDocId ? null : action.parentId;
|
|
2116
|
+
rootProvider.document.transact(() => {
|
|
2117
|
+
treeMap.set(id, {
|
|
2118
|
+
label: action.label,
|
|
2119
|
+
parentId: normalizedParent,
|
|
2120
|
+
order: now,
|
|
2121
|
+
type: action.docType,
|
|
2122
|
+
meta: action.meta,
|
|
2123
|
+
createdAt: now,
|
|
2124
|
+
updatedAt: now
|
|
2125
|
+
});
|
|
2126
|
+
});
|
|
2127
|
+
log(`${actor.actor.name}: created document "${action.label}" (${id})`);
|
|
2128
|
+
if (action.assignId) vars.set(action.assignId, id);
|
|
2129
|
+
}
|
|
2130
|
+
async function executeMoveDocument(actor, action) {
|
|
2131
|
+
const rootProvider = actor.rootProvider;
|
|
2132
|
+
if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`);
|
|
2133
|
+
const treeMap = rootProvider.document.getMap("doc-tree");
|
|
2134
|
+
const entry = treeMap.get(action.docId);
|
|
2135
|
+
if (!entry) {
|
|
2136
|
+
log(`${actor.actor.name}: document ${action.docId} not found in tree`);
|
|
2137
|
+
return;
|
|
2138
|
+
}
|
|
2139
|
+
const normalizedParent = action.newParentId === actor.rootDocId ? null : action.newParentId;
|
|
2140
|
+
rootProvider.document.transact(() => {
|
|
2141
|
+
treeMap.set(action.docId, {
|
|
2142
|
+
...entry,
|
|
2143
|
+
parentId: normalizedParent,
|
|
2144
|
+
order: action.order ?? Date.now(),
|
|
2145
|
+
updatedAt: Date.now()
|
|
2146
|
+
});
|
|
2147
|
+
});
|
|
2148
|
+
log(`${actor.actor.name}: moved document ${action.docId} to ${action.newParentId}`);
|
|
2149
|
+
}
|
|
2150
|
+
async function executeSetMeta(actor, action) {
|
|
2151
|
+
const rootProvider = actor.rootProvider;
|
|
2152
|
+
if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`);
|
|
2153
|
+
const treeMap = rootProvider.document.getMap("doc-tree");
|
|
2154
|
+
const entry = treeMap.get(action.docId);
|
|
2155
|
+
if (!entry) {
|
|
2156
|
+
log(`${actor.actor.name}: document ${action.docId} not found in tree`);
|
|
2157
|
+
return;
|
|
2158
|
+
}
|
|
2159
|
+
rootProvider.document.transact(() => {
|
|
2160
|
+
const currentMeta = entry.meta ?? {};
|
|
2161
|
+
treeMap.set(action.docId, {
|
|
2162
|
+
...entry,
|
|
2163
|
+
meta: {
|
|
2164
|
+
...currentMeta,
|
|
2165
|
+
...action.meta
|
|
2166
|
+
},
|
|
2167
|
+
updatedAt: Date.now()
|
|
2168
|
+
});
|
|
2169
|
+
});
|
|
2170
|
+
}
|
|
2171
|
+
async function executeRenameDocument(actor, action) {
|
|
2172
|
+
const rootProvider = actor.rootProvider;
|
|
2173
|
+
if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`);
|
|
2174
|
+
const treeMap = rootProvider.document.getMap("doc-tree");
|
|
2175
|
+
const entry = treeMap.get(action.docId);
|
|
2176
|
+
if (!entry) {
|
|
2177
|
+
log(`${actor.actor.name}: document ${action.docId} not found in tree`);
|
|
2178
|
+
return;
|
|
2179
|
+
}
|
|
2180
|
+
rootProvider.document.transact(() => {
|
|
2181
|
+
treeMap.set(action.docId, {
|
|
2182
|
+
...entry,
|
|
2183
|
+
label: action.label,
|
|
2184
|
+
updatedAt: Date.now()
|
|
2185
|
+
});
|
|
2186
|
+
});
|
|
2187
|
+
log(`${actor.actor.name}: renamed ${action.docId} to "${action.label}"`);
|
|
2188
|
+
}
|
|
2189
|
+
|
|
2190
|
+
//#endregion
|
|
2191
|
+
//#region packages/orchestrator/src/actions/chat.ts
|
|
2192
|
+
async function executeSendChat(actor, action) {
|
|
2193
|
+
const rootProvider = actor.rootProvider;
|
|
2194
|
+
if (!rootProvider) throw new Error(`${actor.actor.name}: not connected`);
|
|
2195
|
+
const channel_doc_id = action.channel.startsWith("group:") ? action.channel.slice(6) : action.channel;
|
|
2196
|
+
const payload = JSON.stringify({
|
|
2197
|
+
type: "messages:send",
|
|
2198
|
+
channel_doc_id,
|
|
2199
|
+
content: action.message,
|
|
2200
|
+
mentions: []
|
|
2201
|
+
});
|
|
2202
|
+
rootProvider.sendStateless(payload);
|
|
2203
|
+
}
|
|
2204
|
+
|
|
2205
|
+
//#endregion
|
|
2206
|
+
//#region packages/orchestrator/src/actions/flow.ts
|
|
2207
|
+
async function executeWait(action) {
|
|
2208
|
+
await sleep(action.duration);
|
|
2209
|
+
}
|
|
2210
|
+
async function executeParallel(action, execute) {
|
|
2211
|
+
const promises = action.actions.map(async (entry) => {
|
|
2212
|
+
const delay = entry.at ?? 0;
|
|
2213
|
+
if (delay > 0) await sleep(delay);
|
|
2214
|
+
await execute(entry);
|
|
2215
|
+
});
|
|
2216
|
+
await Promise.all(promises);
|
|
2217
|
+
}
|
|
2218
|
+
async function executeSequence(action, execute) {
|
|
2219
|
+
for (const entry of action.actions) {
|
|
2220
|
+
const delay = entry.at ?? 0;
|
|
2221
|
+
if (delay > 0) await sleep(delay);
|
|
2222
|
+
await execute(entry);
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
async function executeRepeat(action, execute) {
|
|
2226
|
+
for (let n = 0; n < action.times; n++) for (const entry of action.actions) {
|
|
2227
|
+
const delay = entry.at ?? 0;
|
|
2228
|
+
if (delay > 0) await sleep(delay);
|
|
2229
|
+
await execute(entry);
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
//#endregion
|
|
2234
|
+
//#region packages/orchestrator/src/actions/index.ts
|
|
2235
|
+
/**
|
|
2236
|
+
* Resolve variable references in string values.
|
|
2237
|
+
* Replaces ${varName} with the value from vars map.
|
|
2238
|
+
*/
|
|
2239
|
+
function resolveVars(value, vars) {
|
|
2240
|
+
return value.replace(/\$\{(\w+)\}/g, (_, key) => vars.get(key) ?? `\${${key}}`);
|
|
2241
|
+
}
|
|
2242
|
+
function resolveActionVars(action, vars) {
|
|
2243
|
+
if (vars.size === 0) return action;
|
|
2244
|
+
const a = { ...action };
|
|
2245
|
+
if (a.docId && typeof a.docId === "string") a.docId = resolveVars(a.docId, vars);
|
|
2246
|
+
if (a.parentId && typeof a.parentId === "string") a.parentId = resolveVars(a.parentId, vars);
|
|
2247
|
+
if (a.newParentId && typeof a.newParentId === "string") a.newParentId = resolveVars(a.newParentId, vars);
|
|
2248
|
+
if (a.cardId && typeof a.cardId === "string") a.cardId = resolveVars(a.cardId, vars);
|
|
2249
|
+
if (a.toColumnId && typeof a.toColumnId === "string") a.toColumnId = resolveVars(a.toColumnId, vars);
|
|
2250
|
+
if (a.text && typeof a.text === "string") a.text = resolveVars(a.text, vars);
|
|
2251
|
+
if (a.label && typeof a.label === "string") a.label = resolveVars(a.label, vars);
|
|
2252
|
+
if (a.markdown && typeof a.markdown === "string") a.markdown = resolveVars(a.markdown, vars);
|
|
2253
|
+
if (a.channel && typeof a.channel === "string") a.channel = resolveVars(a.channel, vars);
|
|
2254
|
+
if (a.message && typeof a.message === "string") a.message = resolveVars(a.message, vars);
|
|
2255
|
+
return a;
|
|
2256
|
+
}
|
|
2257
|
+
function createExecutor(ctx) {
|
|
2258
|
+
const execute = async (entry) => {
|
|
2259
|
+
const action = resolveActionVars(entry.action, ctx.vars);
|
|
2260
|
+
const actor = entry.actor ? ctx.actors.get(entry.actor) : void 0;
|
|
2261
|
+
switch (action.type) {
|
|
2262
|
+
case "connect":
|
|
2263
|
+
if (!actor) throw new Error("connect requires an actor");
|
|
2264
|
+
await executeConnect(actor, ctx.serverConfig);
|
|
2265
|
+
break;
|
|
2266
|
+
case "disconnect":
|
|
2267
|
+
if (!actor) throw new Error("disconnect requires an actor");
|
|
2268
|
+
await executeDisconnect(actor);
|
|
2269
|
+
break;
|
|
2270
|
+
case "navigate":
|
|
2271
|
+
if (!actor) throw new Error("navigate requires an actor");
|
|
2272
|
+
await executeNavigate(actor, action);
|
|
2273
|
+
break;
|
|
2274
|
+
case "type":
|
|
2275
|
+
if (!actor) throw new Error("type requires an actor");
|
|
2276
|
+
await executeType(actor, action);
|
|
2277
|
+
break;
|
|
2278
|
+
case "typeDelete":
|
|
2279
|
+
if (!actor) throw new Error("typeDelete requires an actor");
|
|
2280
|
+
await executeTypeDelete(actor, action);
|
|
2281
|
+
break;
|
|
2282
|
+
case "select":
|
|
2283
|
+
if (!actor) throw new Error("select requires an actor");
|
|
2284
|
+
await executeSelect(actor, action);
|
|
2285
|
+
break;
|
|
2286
|
+
case "moveCursor":
|
|
2287
|
+
if (!actor) throw new Error("moveCursor requires an actor");
|
|
2288
|
+
await executeMoveCursor(actor, action);
|
|
2289
|
+
break;
|
|
2290
|
+
case "setStatus":
|
|
2291
|
+
if (!actor) throw new Error("setStatus requires an actor");
|
|
2292
|
+
await executeSetStatus(actor, action);
|
|
2293
|
+
break;
|
|
2294
|
+
case "setAwareness":
|
|
2295
|
+
if (!actor) throw new Error("setAwareness requires an actor");
|
|
2296
|
+
await executeSetAwareness(actor, action);
|
|
2297
|
+
break;
|
|
2298
|
+
case "clearAwareness":
|
|
2299
|
+
if (!actor) throw new Error("clearAwareness requires an actor");
|
|
2300
|
+
await executeClearAwareness(actor, action);
|
|
2301
|
+
break;
|
|
2302
|
+
case "createDocument":
|
|
2303
|
+
if (!actor) throw new Error("createDocument requires an actor");
|
|
2304
|
+
await executeCreateDocument(actor, action, ctx.vars);
|
|
2305
|
+
break;
|
|
2306
|
+
case "moveDocument":
|
|
2307
|
+
if (!actor) throw new Error("moveDocument requires an actor");
|
|
2308
|
+
await executeMoveDocument(actor, action);
|
|
2309
|
+
break;
|
|
2310
|
+
case "renameDocument":
|
|
2311
|
+
if (!actor) throw new Error("renameDocument requires an actor");
|
|
2312
|
+
await executeRenameDocument(actor, action);
|
|
2313
|
+
break;
|
|
2314
|
+
case "writeContent":
|
|
2315
|
+
if (!actor) throw new Error("writeContent requires an actor");
|
|
2316
|
+
await executeWriteContent(actor, action);
|
|
2317
|
+
break;
|
|
2318
|
+
case "deleteContent":
|
|
2319
|
+
if (!actor) throw new Error("deleteContent requires an actor");
|
|
2320
|
+
await executeDeleteContent(actor, action);
|
|
2321
|
+
break;
|
|
2322
|
+
case "setMeta":
|
|
2323
|
+
if (!actor) throw new Error("setMeta requires an actor");
|
|
2324
|
+
await executeSetMeta(actor, action);
|
|
2325
|
+
break;
|
|
2326
|
+
case "pointerMove":
|
|
2327
|
+
if (!actor) throw new Error("pointerMove requires an actor");
|
|
2328
|
+
await executePointerMove(actor, action);
|
|
2329
|
+
break;
|
|
2330
|
+
case "scrollTo":
|
|
2331
|
+
if (!actor) throw new Error("scrollTo requires an actor");
|
|
2332
|
+
await executeScrollTo(actor, action);
|
|
2333
|
+
break;
|
|
2334
|
+
case "kanbanHover":
|
|
2335
|
+
if (!actor) throw new Error("kanbanHover requires an actor");
|
|
2336
|
+
await executeKanbanHover(actor, action);
|
|
2337
|
+
break;
|
|
2338
|
+
case "kanbanDrag":
|
|
2339
|
+
if (!actor) throw new Error("kanbanDrag requires an actor");
|
|
2340
|
+
await executeKanbanDrag(actor, action);
|
|
2341
|
+
break;
|
|
2342
|
+
case "sendChat":
|
|
2343
|
+
if (!actor) throw new Error("sendChat requires an actor");
|
|
2344
|
+
await executeSendChat(actor, action);
|
|
2345
|
+
break;
|
|
2346
|
+
case "wait":
|
|
2347
|
+
await executeWait(action);
|
|
2348
|
+
break;
|
|
2349
|
+
case "parallel":
|
|
2350
|
+
await executeParallel(action, execute);
|
|
2351
|
+
break;
|
|
2352
|
+
case "sequence":
|
|
2353
|
+
await executeSequence(action, execute);
|
|
2354
|
+
break;
|
|
2355
|
+
case "repeat":
|
|
2356
|
+
await executeRepeat(action, execute);
|
|
2357
|
+
break;
|
|
2358
|
+
default: throw new Error(`Unknown action type: ${action.type}`);
|
|
2359
|
+
}
|
|
2360
|
+
};
|
|
2361
|
+
return execute;
|
|
2362
|
+
}
|
|
2363
|
+
|
|
2364
|
+
//#endregion
|
|
2365
|
+
//#region packages/orchestrator/src/orchestrator.ts
|
|
2366
|
+
/**
|
|
2367
|
+
* Orchestrator — top-level class that loads a scene, manages actors, and runs the timeline.
|
|
2368
|
+
*/
|
|
2369
|
+
var Orchestrator = class {
|
|
2370
|
+
constructor() {
|
|
2371
|
+
this.scene = null;
|
|
2372
|
+
this.actors = /* @__PURE__ */ new Map();
|
|
2373
|
+
this.vars = /* @__PURE__ */ new Map();
|
|
2374
|
+
}
|
|
2375
|
+
/** Load a scene from a TypeScript file. */
|
|
2376
|
+
async load(scriptPath) {
|
|
2377
|
+
const mod = await import((0, node_url.pathToFileURL)((0, node_path.resolve)(scriptPath)).href);
|
|
2378
|
+
const scene = mod.default ?? mod;
|
|
2379
|
+
if (!scene.server?.url) throw new Error("Scene must have server.url");
|
|
2380
|
+
if (!scene.actors?.length) throw new Error("Scene must have at least one actor");
|
|
2381
|
+
if (!scene.timeline?.length) throw new Error("Scene must have at least one timeline entry");
|
|
2382
|
+
this.scene = scene;
|
|
2383
|
+
log(`Loaded scene: ${scene.actors.length} actors, ${scene.timeline.length} timeline entries`);
|
|
2384
|
+
}
|
|
2385
|
+
/** Prepare actor connections (does not connect yet — that's a timeline action). */
|
|
2386
|
+
prepare() {
|
|
2387
|
+
if (!this.scene) throw new Error("No scene loaded");
|
|
2388
|
+
if (this.scene.vars) for (const [k, v] of Object.entries(this.scene.vars)) this.vars.set(k, v);
|
|
2389
|
+
for (const actorDef of this.scene.actors) {
|
|
2390
|
+
if (this.actors.has(actorDef.name)) throw new Error(`Duplicate actor name: ${actorDef.name}`);
|
|
2391
|
+
const conn = new ActorConnection(actorDef, this.scene.server);
|
|
2392
|
+
this.actors.set(actorDef.name, conn);
|
|
2393
|
+
}
|
|
2394
|
+
log(`Prepared ${this.actors.size} actors: ${[...this.actors.keys()].join(", ")}`);
|
|
2395
|
+
}
|
|
2396
|
+
/** Validate the scene without connecting. Logs the timeline structure. */
|
|
2397
|
+
dryRun() {
|
|
2398
|
+
if (!this.scene) throw new Error("No scene loaded");
|
|
2399
|
+
const actorNames = new Set(this.scene.actors.map((a) => a.name));
|
|
2400
|
+
const errors = [];
|
|
2401
|
+
for (let i = 0; i < this.scene.timeline.length; i++) {
|
|
2402
|
+
const entry = this.scene.timeline[i];
|
|
2403
|
+
const prefix = `timeline[${i}]`;
|
|
2404
|
+
if (entry.actor && !actorNames.has(entry.actor)) errors.push(`${prefix}: unknown actor "${entry.actor}"`);
|
|
2405
|
+
if (![
|
|
2406
|
+
"wait",
|
|
2407
|
+
"parallel",
|
|
2408
|
+
"sequence",
|
|
2409
|
+
"repeat"
|
|
2410
|
+
].includes(entry.action.type) && !entry.actor) errors.push(`${prefix}: ${entry.action.type} requires an actor`);
|
|
2411
|
+
}
|
|
2412
|
+
if (errors.length) {
|
|
2413
|
+
log(`Dry run found ${errors.length} issue(s):`);
|
|
2414
|
+
for (const e of errors) log(` - ${e}`);
|
|
2415
|
+
} else log("Dry run: no issues found");
|
|
2416
|
+
const sorted = [...this.scene.timeline].sort((a, b) => (a.at ?? 0) - (b.at ?? 0));
|
|
2417
|
+
log("Timeline:");
|
|
2418
|
+
for (const entry of sorted) log(` [${`${((entry.at ?? 0) / 1e3).toFixed(1)}s`}] ${entry.actor ?? "—"}: ${entry.action.type}`);
|
|
2419
|
+
if (this.scene.duration) log(`Scene duration: ${(this.scene.duration / 1e3).toFixed(1)}s`);
|
|
2420
|
+
}
|
|
2421
|
+
/** Run the timeline. */
|
|
2422
|
+
async run() {
|
|
2423
|
+
if (!this.scene) throw new Error("No scene loaded");
|
|
2424
|
+
if (this.scene.onStart) {
|
|
2425
|
+
log("Running onStart hook...");
|
|
2426
|
+
await this.scene.onStart();
|
|
2427
|
+
}
|
|
2428
|
+
const executor = createExecutor({
|
|
2429
|
+
actors: this.actors,
|
|
2430
|
+
serverConfig: this.scene.server,
|
|
2431
|
+
vars: this.vars
|
|
2432
|
+
});
|
|
2433
|
+
const runner = new TimelineRunner();
|
|
2434
|
+
log("Starting timeline...");
|
|
2435
|
+
const startTime = Date.now();
|
|
2436
|
+
if (this.scene.duration) {
|
|
2437
|
+
const timelinePromise = runner.run(this.scene.timeline, executor);
|
|
2438
|
+
const timeoutPromise = new Promise((resolve) => {
|
|
2439
|
+
setTimeout(() => {
|
|
2440
|
+
log(`Scene duration reached (${this.scene.duration}ms), stopping...`);
|
|
2441
|
+
resolve();
|
|
2442
|
+
}, this.scene.duration);
|
|
2443
|
+
});
|
|
2444
|
+
await Promise.race([timelinePromise, timeoutPromise]);
|
|
2445
|
+
} else await runner.run(this.scene.timeline, executor);
|
|
2446
|
+
log(`Timeline complete in ${((Date.now() - startTime) / 1e3).toFixed(1)}s`);
|
|
2447
|
+
if (this.scene.onEnd) {
|
|
2448
|
+
log("Running onEnd hook...");
|
|
2449
|
+
await this.scene.onEnd();
|
|
2450
|
+
}
|
|
2451
|
+
}
|
|
2452
|
+
/** Disconnect all actors gracefully. */
|
|
2453
|
+
async cleanup() {
|
|
2454
|
+
const disconnects = [...this.actors.values()].map((a) => a.disconnect().catch(() => {}));
|
|
2455
|
+
await Promise.all(disconnects);
|
|
2456
|
+
this.actors.clear();
|
|
2457
|
+
this.vars.clear();
|
|
2458
|
+
log("All actors disconnected");
|
|
2459
|
+
}
|
|
2460
|
+
};
|
|
2461
|
+
|
|
2462
|
+
//#endregion
|
|
2463
|
+
//#region packages/orchestrator/src/define.ts
|
|
2464
|
+
/** Define a complete scene. */
|
|
2465
|
+
function defineScene(scene) {
|
|
2466
|
+
return scene;
|
|
2467
|
+
}
|
|
2468
|
+
/** Define an actor. */
|
|
2469
|
+
function actor(name, opts) {
|
|
2470
|
+
return {
|
|
2471
|
+
name,
|
|
2472
|
+
...opts
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
/** Factory functions for all action types. */
|
|
2476
|
+
const actions = {
|
|
2477
|
+
connect() {
|
|
2478
|
+
return { type: "connect" };
|
|
2479
|
+
},
|
|
2480
|
+
disconnect() {
|
|
2481
|
+
return { type: "disconnect" };
|
|
2482
|
+
},
|
|
2483
|
+
navigate(docId) {
|
|
2484
|
+
return {
|
|
2485
|
+
type: "navigate",
|
|
2486
|
+
docId
|
|
2487
|
+
};
|
|
2488
|
+
},
|
|
2489
|
+
type(docId, text, opts) {
|
|
2490
|
+
return {
|
|
2491
|
+
type: "type",
|
|
2492
|
+
docId,
|
|
2493
|
+
text,
|
|
2494
|
+
...opts
|
|
2495
|
+
};
|
|
2496
|
+
},
|
|
2497
|
+
typeDelete(docId, count, opts) {
|
|
2498
|
+
return {
|
|
2499
|
+
type: "typeDelete",
|
|
2500
|
+
docId,
|
|
2501
|
+
count,
|
|
2502
|
+
...opts
|
|
2503
|
+
};
|
|
2504
|
+
},
|
|
2505
|
+
select(docId, anchor, head) {
|
|
2506
|
+
return {
|
|
2507
|
+
type: "select",
|
|
2508
|
+
docId,
|
|
2509
|
+
anchor,
|
|
2510
|
+
head
|
|
2511
|
+
};
|
|
2512
|
+
},
|
|
2513
|
+
moveCursor(docId, from, to, duration, easing) {
|
|
2514
|
+
return {
|
|
2515
|
+
type: "moveCursor",
|
|
2516
|
+
docId,
|
|
2517
|
+
from,
|
|
2518
|
+
to,
|
|
2519
|
+
duration,
|
|
2520
|
+
easing
|
|
2521
|
+
};
|
|
2522
|
+
},
|
|
2523
|
+
setStatus(status) {
|
|
2524
|
+
return {
|
|
2525
|
+
type: "setStatus",
|
|
2526
|
+
status
|
|
2527
|
+
};
|
|
2528
|
+
},
|
|
2529
|
+
setAwareness(fields, docId) {
|
|
2530
|
+
return {
|
|
2531
|
+
type: "setAwareness",
|
|
2532
|
+
docId,
|
|
2533
|
+
fields
|
|
2534
|
+
};
|
|
2535
|
+
},
|
|
2536
|
+
clearAwareness(fields, docId) {
|
|
2537
|
+
return {
|
|
2538
|
+
type: "clearAwareness",
|
|
2539
|
+
docId,
|
|
2540
|
+
fields
|
|
2541
|
+
};
|
|
2542
|
+
},
|
|
2543
|
+
createDocument(parentId, label, opts) {
|
|
2544
|
+
return {
|
|
2545
|
+
type: "createDocument",
|
|
2546
|
+
parentId,
|
|
2547
|
+
label,
|
|
2548
|
+
...opts
|
|
2549
|
+
};
|
|
2550
|
+
},
|
|
2551
|
+
moveDocument(docId, newParentId, order) {
|
|
2552
|
+
return {
|
|
2553
|
+
type: "moveDocument",
|
|
2554
|
+
docId,
|
|
2555
|
+
newParentId,
|
|
2556
|
+
order
|
|
2557
|
+
};
|
|
2558
|
+
},
|
|
2559
|
+
renameDocument(docId, label) {
|
|
2560
|
+
return {
|
|
2561
|
+
type: "renameDocument",
|
|
2562
|
+
docId,
|
|
2563
|
+
label
|
|
2564
|
+
};
|
|
2565
|
+
},
|
|
2566
|
+
writeContent(docId, markdown) {
|
|
2567
|
+
return {
|
|
2568
|
+
type: "writeContent",
|
|
2569
|
+
docId,
|
|
2570
|
+
markdown
|
|
2571
|
+
};
|
|
2572
|
+
},
|
|
2573
|
+
deleteContent(docId, from, length) {
|
|
2574
|
+
return {
|
|
2575
|
+
type: "deleteContent",
|
|
2576
|
+
docId,
|
|
2577
|
+
from,
|
|
2578
|
+
length
|
|
2579
|
+
};
|
|
2580
|
+
},
|
|
2581
|
+
setMeta(docId, meta) {
|
|
2582
|
+
return {
|
|
2583
|
+
type: "setMeta",
|
|
2584
|
+
docId,
|
|
2585
|
+
meta
|
|
2586
|
+
};
|
|
2587
|
+
},
|
|
2588
|
+
pointerMove(docId, from, to, duration, easing) {
|
|
2589
|
+
return {
|
|
2590
|
+
type: "pointerMove",
|
|
2591
|
+
docId,
|
|
2592
|
+
from,
|
|
2593
|
+
to,
|
|
2594
|
+
duration,
|
|
2595
|
+
easing
|
|
2596
|
+
};
|
|
2597
|
+
},
|
|
2598
|
+
scrollTo(docId, position) {
|
|
2599
|
+
return {
|
|
2600
|
+
type: "scrollTo",
|
|
2601
|
+
docId,
|
|
2602
|
+
position
|
|
2603
|
+
};
|
|
2604
|
+
},
|
|
2605
|
+
kanbanHover(docId, cardId) {
|
|
2606
|
+
return {
|
|
2607
|
+
type: "kanbanHover",
|
|
2608
|
+
docId,
|
|
2609
|
+
cardId
|
|
2610
|
+
};
|
|
2611
|
+
},
|
|
2612
|
+
kanbanDrag(docId, cardId, toColumnId, duration) {
|
|
2613
|
+
return {
|
|
2614
|
+
type: "kanbanDrag",
|
|
2615
|
+
docId,
|
|
2616
|
+
cardId,
|
|
2617
|
+
toColumnId,
|
|
2618
|
+
duration
|
|
2619
|
+
};
|
|
2620
|
+
},
|
|
2621
|
+
sendChat(channel, message) {
|
|
2622
|
+
return {
|
|
2623
|
+
type: "sendChat",
|
|
2624
|
+
channel,
|
|
2625
|
+
message
|
|
2626
|
+
};
|
|
2627
|
+
},
|
|
2628
|
+
wait(duration) {
|
|
2629
|
+
return {
|
|
2630
|
+
type: "wait",
|
|
2631
|
+
duration
|
|
2632
|
+
};
|
|
2633
|
+
},
|
|
2634
|
+
parallel(entries) {
|
|
2635
|
+
return {
|
|
2636
|
+
type: "parallel",
|
|
2637
|
+
actions: entries
|
|
2638
|
+
};
|
|
2639
|
+
},
|
|
2640
|
+
sequence(entries) {
|
|
2641
|
+
return {
|
|
2642
|
+
type: "sequence",
|
|
2643
|
+
actions: entries
|
|
2644
|
+
};
|
|
2645
|
+
},
|
|
2646
|
+
repeat(times, entries) {
|
|
2647
|
+
return {
|
|
2648
|
+
type: "repeat",
|
|
2649
|
+
times,
|
|
2650
|
+
actions: entries
|
|
2651
|
+
};
|
|
2652
|
+
}
|
|
2653
|
+
};
|
|
2654
|
+
|
|
2655
|
+
//#endregion
|
|
2656
|
+
//#region packages/orchestrator/src/index.ts
|
|
2657
|
+
/**
|
|
2658
|
+
* Abracadabra Orchestrator — CouShell commercial director.
|
|
2659
|
+
*
|
|
2660
|
+
* Usage (after pnpm build:packages):
|
|
2661
|
+
* node --experimental-transform-types \
|
|
2662
|
+
* packages/orchestrator/dist/abracadabra-orchestrator.esm.js ./scripts/demo.ts
|
|
2663
|
+
*
|
|
2664
|
+
* Dry run:
|
|
2665
|
+
* node --experimental-transform-types \
|
|
2666
|
+
* packages/orchestrator/dist/abracadabra-orchestrator.esm.js --dry-run ./scripts/demo.ts
|
|
2667
|
+
*/
|
|
2668
|
+
async function main() {
|
|
2669
|
+
const args = process.argv.slice(2);
|
|
2670
|
+
const dryRun = args.includes("--dry-run");
|
|
2671
|
+
const scriptPath = args.find((a) => !a.startsWith("--"));
|
|
2672
|
+
if (!scriptPath) {
|
|
2673
|
+
console.error("Usage: abracadabra-orchestrator [--dry-run] <script.ts>");
|
|
2674
|
+
console.error("");
|
|
2675
|
+
console.error(" The script file should export a Scene object (use defineScene()).");
|
|
2676
|
+
console.error("");
|
|
2677
|
+
console.error("Options:");
|
|
2678
|
+
console.error(" --dry-run Validate the scene without connecting to the server");
|
|
2679
|
+
console.error("");
|
|
2680
|
+
console.error("Example:");
|
|
2681
|
+
console.error(" node --conditions=source --experimental-transform-types \\");
|
|
2682
|
+
console.error(" packages/orchestrator/src/index.ts ./scripts/demo.ts");
|
|
2683
|
+
process.exit(1);
|
|
2684
|
+
}
|
|
2685
|
+
const orchestrator = new Orchestrator();
|
|
2686
|
+
try {
|
|
2687
|
+
await orchestrator.load(scriptPath);
|
|
2688
|
+
orchestrator.prepare();
|
|
2689
|
+
if (dryRun) orchestrator.dryRun();
|
|
2690
|
+
else await orchestrator.run();
|
|
2691
|
+
} catch (err) {
|
|
2692
|
+
console.error(`[orchestrator] Fatal: ${err.message}`);
|
|
2693
|
+
process.exit(1);
|
|
2694
|
+
} finally {
|
|
2695
|
+
if (!dryRun) await orchestrator.cleanup();
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
const __dirname$1 = (0, node_path.dirname)((0, node_url.fileURLToPath)(require("url").pathToFileURL(__filename).href));
|
|
2699
|
+
if (process.argv[1]) {
|
|
2700
|
+
const resolved = (0, node_path.resolve)(process.argv[1]);
|
|
2701
|
+
if (resolved.startsWith(__dirname$1) || resolved.includes("abracadabra-orchestrator")) main();
|
|
2702
|
+
}
|
|
2703
|
+
|
|
2704
|
+
//#endregion
|
|
2705
|
+
exports.Orchestrator = Orchestrator;
|
|
2706
|
+
exports.actions = actions;
|
|
2707
|
+
exports.actor = actor;
|
|
2708
|
+
exports.defineScene = defineScene;
|
|
2709
|
+
//# sourceMappingURL=abracadabra-orchestrator.cjs.map
|