@elu-labs/elu 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (102) hide show
  1. package/LICENSE +21 -0
  2. package/LICENSES/Apache-2.0.txt +202 -0
  3. package/README.md +340 -2
  4. package/SECURITY.md +333 -0
  5. package/THIRD_PARTY_NOTICES.md +62 -0
  6. package/bin/elu-code.mjs +209 -0
  7. package/bin/elu.cjs +10 -0
  8. package/bin/start.cjs +6 -0
  9. package/docs/peer-network.md +257 -0
  10. package/docs/permissions.md +285 -0
  11. package/docs/vendors/antigravity.md +131 -0
  12. package/docs/vendors/claude.md +124 -0
  13. package/docs/vendors/codex.md +64 -0
  14. package/docs/vendors/grok.md +151 -0
  15. package/lib/acp-client.mjs +100 -0
  16. package/lib/adapters/agy.mjs +283 -0
  17. package/lib/adapters/claude.mjs +311 -0
  18. package/lib/adapters/codex.mjs +137 -0
  19. package/lib/adapters/grok.mjs +384 -0
  20. package/lib/adapters/index.mjs +13 -0
  21. package/lib/agents.mjs +77 -0
  22. package/lib/agy-hook.mjs +53 -0
  23. package/lib/approval-server.mjs +85 -0
  24. package/lib/approvals.mjs +128 -0
  25. package/lib/awake.mjs +45 -0
  26. package/lib/board.mjs +261 -0
  27. package/lib/bot-models.mjs +71 -0
  28. package/lib/bot-schedules.mjs +291 -0
  29. package/lib/bots.mjs +361 -0
  30. package/lib/changes.mjs +168 -0
  31. package/lib/checks.mjs +102 -0
  32. package/lib/conductor.mjs +337 -0
  33. package/lib/config.mjs +147 -0
  34. package/lib/conformance.mjs +109 -0
  35. package/lib/default-bots.mjs +150 -0
  36. package/lib/detect.mjs +108 -0
  37. package/lib/diff.mjs +217 -0
  38. package/lib/editor.mjs +165 -0
  39. package/lib/format.mjs +51 -0
  40. package/lib/git.mjs +173 -0
  41. package/lib/init.mjs +129 -0
  42. package/lib/install.mjs +130 -0
  43. package/lib/kanban.mjs +180 -0
  44. package/lib/mcp.mjs +79 -0
  45. package/lib/memory.mjs +35 -0
  46. package/lib/models.mjs +144 -0
  47. package/lib/modes.mjs +95 -0
  48. package/lib/never-list.mjs +110 -0
  49. package/lib/never-shims.mjs +311 -0
  50. package/lib/paste.mjs +56 -0
  51. package/lib/peer/a2a-http.mjs +166 -0
  52. package/lib/peer/a2a.mjs +357 -0
  53. package/lib/peer/asks.mjs +329 -0
  54. package/lib/peer/canonical.mjs +72 -0
  55. package/lib/peer/cli.mjs +154 -0
  56. package/lib/peer/daemon.mjs +525 -0
  57. package/lib/peer/identity.mjs +81 -0
  58. package/lib/peer/index.mjs +291 -0
  59. package/lib/peer/intake.mjs +207 -0
  60. package/lib/peer/invite.mjs +76 -0
  61. package/lib/peer/paths.mjs +7 -0
  62. package/lib/peer/peerdata.mjs +21 -0
  63. package/lib/peer/pictures.mjs +295 -0
  64. package/lib/peer/plans.mjs +182 -0
  65. package/lib/peer/relay.mjs +31 -0
  66. package/lib/peer/replies.mjs +128 -0
  67. package/lib/peer/roster.mjs +256 -0
  68. package/lib/peer/rpc.mjs +88 -0
  69. package/lib/peer/secrets.mjs +88 -0
  70. package/lib/peer/settings.mjs +45 -0
  71. package/lib/peer/shares.mjs +120 -0
  72. package/lib/peer/status.mjs +84 -0
  73. package/lib/peer/taskstore.mjs +108 -0
  74. package/lib/peer/transport.mjs +120 -0
  75. package/lib/pictures.mjs +104 -0
  76. package/lib/plan.mjs +238 -0
  77. package/lib/policy.mjs +98 -0
  78. package/lib/process.mjs +92 -0
  79. package/lib/project-settings.mjs +55 -0
  80. package/lib/project-trust.mjs +230 -0
  81. package/lib/projects.mjs +668 -0
  82. package/lib/publish.mjs +105 -0
  83. package/lib/runner.mjs +874 -0
  84. package/lib/sandbox.mjs +102 -0
  85. package/lib/schedules.mjs +270 -0
  86. package/lib/selection.mjs +202 -0
  87. package/lib/shell.mjs +101 -0
  88. package/lib/state.mjs +247 -0
  89. package/lib/terminal-bots.mjs +239 -0
  90. package/lib/terminal-commands.mjs +165 -0
  91. package/lib/terminal-plan.mjs +37 -0
  92. package/lib/terminal-visuals.mjs +270 -0
  93. package/lib/terminal.mjs +3181 -0
  94. package/lib/unit-file.mjs +17 -0
  95. package/lib/usage.mjs +249 -0
  96. package/lib/util.mjs +86 -0
  97. package/lib/vendor-check.mjs +96 -0
  98. package/lib/vendors.mjs +191 -0
  99. package/lib/workspace.mjs +168 -0
  100. package/lib/yaml.mjs +624 -0
  101. package/npm-shrinkwrap.json +827 -0
  102. package/package.json +70 -4
package/lib/yaml.mjs ADDED
@@ -0,0 +1,624 @@
1
+ // A YAML subset without aliases, tags, or executable extensions.
2
+ const PLAIN_STRING = /^[A-Za-z_][A-Za-z0-9_ .\/@+()-]*$/;
3
+ const RESERVED_WORDS = new Set(['null', 'true', 'false', 'yes', 'no', 'on', 'off', 'y', 'n', '~']);
4
+ const PLAIN_KEY_OUT = /^[A-Za-z0-9_][A-Za-z0-9_-]*$/;
5
+ const PLAIN_KEY_IN = /^([A-Za-z0-9_-](?:[A-Za-z0-9_.\/ -]*[A-Za-z0-9_.\/-])?)[ \t]*:(?=[ \t]|$)/;
6
+ const NUMBER = /^-?\d+(\.\d+)?([eE][-+]?\d+)?$/;
7
+ const FLOW_MAP_MAX = 72;
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Public API
11
+ // ---------------------------------------------------------------------------
12
+
13
+ /**
14
+ * Parse a YAML-subset document.
15
+ * @param {string} text
16
+ * @param {{lineOffset?: number}} [options] lineOffset shifts reported line numbers (frontmatter).
17
+ * @returns {any} the parsed value; an empty document is `{}`.
18
+ */
19
+ export function parseYaml(text, { lineOffset = 0 } = {}) {
20
+ return new BlockParser(String(text ?? '').replace(/^\uFEFF/, ''), lineOffset).document();
21
+ }
22
+
23
+ /**
24
+ * Serialize a JSON-like value to canonical YAML (insertion order, deterministic).
25
+ * `parseYaml(stringifyYaml(v))` deep-equals `v` for any JSON-like `v`.
26
+ * @param {any} value
27
+ * @returns {string} text ending in a newline
28
+ */
29
+ export function stringifyYaml(value) {
30
+ const v = normalize(value);
31
+ if (isScalar(v)) return `${formatScalar(v)}\n`;
32
+ if (Array.isArray(v)) {
33
+ if (v.length === 0 || v.every(isScalar)) return `${flowSequence(v)}\n`;
34
+ return `${emitSequence(v, 0).join('\n')}\n`;
35
+ }
36
+ if (Object.keys(v).length === 0) return '{}\n';
37
+ return `${emitMapping(v, 0).join('\n')}\n`;
38
+ }
39
+
40
+ // ---------------------------------------------------------------------------
41
+ // Serializer
42
+ // ---------------------------------------------------------------------------
43
+
44
+ function isScalar(v) {
45
+ return v === null || typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean';
46
+ }
47
+
48
+ /** JSON semantics: undefined/functions dropped from objects, null in arrays; Dates become ISO strings. */
49
+ function normalize(v) {
50
+ if (v === undefined || typeof v === 'function' || typeof v === 'symbol') return null;
51
+ if (typeof v === 'bigint') return Number(v);
52
+ if (v instanceof Date) return v.toISOString();
53
+ if (isScalar(v)) return v;
54
+ if (Array.isArray(v)) return v.map(normalize);
55
+ const out = {};
56
+ for (const [k, x] of Object.entries(v)) {
57
+ if (x === undefined || typeof x === 'function' || typeof x === 'symbol') continue;
58
+ setKey(out, k, normalize(x));
59
+ }
60
+ return out;
61
+ }
62
+
63
+ function formatScalar(v) {
64
+ if (v === null) return 'null';
65
+ if (typeof v === 'boolean') return v ? 'true' : 'false';
66
+ if (typeof v === 'number') return Number.isFinite(v) ? String(v) : 'null';
67
+ return isPlainSafe(v) ? v : JSON.stringify(v);
68
+ }
69
+
70
+ function isPlainSafe(s) {
71
+ return PLAIN_STRING.test(s) && !/\s$/.test(s) && !RESERVED_WORDS.has(s.toLowerCase());
72
+ }
73
+
74
+ function formatKey(k) {
75
+ return PLAIN_KEY_OUT.test(k) ? k : JSON.stringify(k);
76
+ }
77
+
78
+ function flowSequence(arr) {
79
+ return `[${arr.map(formatScalar).join(', ')}]`;
80
+ }
81
+
82
+ function flowMapping(obj) {
83
+ return `{${Object.entries(obj).map(([k, v]) => `${formatKey(k)}: ${formatScalar(v)}`).join(', ')}}`;
84
+ }
85
+
86
+ function emitMapping(obj, indent) {
87
+ const pad = ' '.repeat(indent);
88
+ const lines = [];
89
+ for (const [key, v] of Object.entries(obj)) {
90
+ const k = formatKey(key);
91
+ if (isScalar(v)) {
92
+ lines.push(`${pad}${k}: ${formatScalar(v)}`);
93
+ } else if (Array.isArray(v)) {
94
+ if (v.length === 0 || v.every(isScalar)) lines.push(`${pad}${k}: ${flowSequence(v)}`);
95
+ else lines.push(`${pad}${k}:`, ...emitSequence(v, indent + 2));
96
+ } else if (Object.keys(v).length === 0) {
97
+ lines.push(`${pad}${k}: {}`);
98
+ } else {
99
+ const flow = Object.values(v).every(isScalar) ? flowMapping(v) : null;
100
+ if (flow !== null && flow.length <= FLOW_MAP_MAX) lines.push(`${pad}${k}: ${flow}`);
101
+ else lines.push(`${pad}${k}:`, ...emitMapping(v, indent + 2));
102
+ }
103
+ }
104
+ return lines;
105
+ }
106
+
107
+ function emitSequence(arr, indent) {
108
+ const pad = ' '.repeat(indent);
109
+ const lines = [];
110
+ for (const item of arr) {
111
+ if (isScalar(item)) {
112
+ lines.push(`${pad}- ${formatScalar(item)}`);
113
+ } else if (Array.isArray(item)) {
114
+ if (item.length === 0 || item.every(isScalar)) lines.push(`${pad}- ${flowSequence(item)}`);
115
+ else lines.push(...dashFirst(emitSequence(item, indent + 2), pad, indent + 2));
116
+ } else if (Object.keys(item).length === 0) {
117
+ lines.push(`${pad}- {}`);
118
+ } else {
119
+ lines.push(...dashFirst(emitMapping(item, indent + 2), pad, indent + 2));
120
+ }
121
+ }
122
+ return lines;
123
+ }
124
+
125
+ /** Put the first line of a nested block on the parent's dash line. */
126
+ function dashFirst(nested, pad, nestedIndent) {
127
+ nested[0] = `${pad}- ${nested[0].slice(nestedIndent)}`;
128
+ return nested;
129
+ }
130
+
131
+ function setKey(obj, key, value) {
132
+ if (key === '__proto__') {
133
+ Object.defineProperty(obj, key, { value, enumerable: true, writable: true, configurable: true });
134
+ } else {
135
+ obj[key] = value;
136
+ }
137
+ }
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // Parser: scalars
141
+ // ---------------------------------------------------------------------------
142
+
143
+ function typePlain(s) {
144
+ if (s === '' || s === '~' || s === 'null' || s === 'Null' || s === 'NULL') return null;
145
+ if (s === 'true' || s === 'True' || s === 'TRUE') return true;
146
+ if (s === 'false' || s === 'False' || s === 'FALSE') return false;
147
+ if (NUMBER.test(s)) {
148
+ const n = Number(s);
149
+ if (Number.isFinite(n)) return n;
150
+ }
151
+ return s;
152
+ }
153
+
154
+ function clip(s, n = 40) {
155
+ return s.length > n ? `${s.slice(0, n)}…` : s;
156
+ }
157
+
158
+ function unterminated(what, incomplete) {
159
+ const e = new Error(`unterminated ${what}`);
160
+ if (incomplete) e.incomplete = true;
161
+ return e;
162
+ }
163
+
164
+ const SIMPLE_ESCAPES = {
165
+ 0: '\0', a: '\x07', b: '\b', t: '\t', '\t': '\t', n: '\n', v: '\v', f: '\f', r: '\r', e: '\x1b', ' ': ' ',
166
+ '"': '"', '/': '/', '\\': '\\', N: '\u0085', _: '\u00a0', L: '\u2028', P: '\u2029',
167
+ };
168
+ const HEX_ESCAPES = { x: 2, u: 4, U: 8 };
169
+
170
+ /** Decode the inside of a double-quoted scalar (YAML 1.2 escapes; a raw line break folds to a space). */
171
+ function decodeDoubleQuoted(inner, shown) {
172
+ let out = '';
173
+ for (let i = 0; i < inner.length; i++) {
174
+ const ch = inner[i];
175
+ if (ch === '\n') {
176
+ out += ' ';
177
+ continue;
178
+ }
179
+ if (ch !== '\\') {
180
+ out += ch;
181
+ continue;
182
+ }
183
+ const e = inner[i + 1];
184
+ if (e !== undefined && Object.prototype.hasOwnProperty.call(SIMPLE_ESCAPES, e)) {
185
+ out += SIMPLE_ESCAPES[e];
186
+ i += 1;
187
+ continue;
188
+ }
189
+ const width = HEX_ESCAPES[e];
190
+ const hex = width ? inner.slice(i + 2, i + 2 + width) : '';
191
+ if (!width || hex.length !== width || !/^[0-9A-Fa-f]+$/.test(hex) || Number.parseInt(hex, 16) > 0x10ffff) {
192
+ throw new Error(`invalid escape "\\${e ?? ''}" in double-quoted string ${clip(shown)}`);
193
+ }
194
+ const code = Number.parseInt(hex, 16);
195
+ out += width === 4 ? String.fromCharCode(code) : String.fromCodePoint(code);
196
+ i += 1 + width;
197
+ }
198
+ return out;
199
+ }
200
+
201
+ /**
202
+ * Read a quoted scalar starting at s[pos]. Double quotes take YAML escapes
203
+ * (the JSON ones plus \x \U \e \0 \a \v \N \_ \L \P and "\ ");
204
+ * single quotes escape `'` as `''`.
205
+ * @returns {{value: string, end: number}} end = index after the closing quote
206
+ */
207
+ function readQuoted(s, pos, inFlow = false) {
208
+ if (s[pos] === '"') {
209
+ let i = pos + 1;
210
+ while (i < s.length && s[i] !== '"') i += s[i] === '\\' ? 2 : 1;
211
+ if (i >= s.length) throw unterminated('double-quoted string', inFlow);
212
+ const raw = s.slice(pos, i + 1);
213
+ return { value: decodeDoubleQuoted(raw.slice(1, -1), raw), end: i + 1 };
214
+ }
215
+ let out = '';
216
+ for (let i = pos + 1; ; ) {
217
+ if (i >= s.length) throw unterminated('single-quoted string', inFlow);
218
+ if (s[i] === "'") {
219
+ if (s[i + 1] === "'") {
220
+ out += "'";
221
+ i += 2;
222
+ continue;
223
+ }
224
+ return { value: out, end: i + 1 };
225
+ }
226
+ out += s[i] === '\n' ? ' ' : s[i];
227
+ i += 1;
228
+ }
229
+ }
230
+
231
+ function stripComment(text) {
232
+ const at = text.search(/[ \t]#/);
233
+ return (at >= 0 ? text.slice(0, at) : text).trim();
234
+ }
235
+
236
+ function isSequenceItem(text) {
237
+ return text === '-' || text.startsWith('- ') || text.startsWith('-\t');
238
+ }
239
+
240
+ /** `key: rest` → {key, rest}; null when the text is not a mapping entry. */
241
+ function readKey(text) {
242
+ if (text[0] === '"' || text[0] === "'") {
243
+ let q;
244
+ try {
245
+ q = readQuoted(text, 0);
246
+ } catch {
247
+ return null;
248
+ }
249
+ const m = /^[ \t]*:(?=[ \t]|$)/.exec(text.slice(q.end));
250
+ if (!m) return null;
251
+ return { key: q.value, rest: text.slice(q.end + m[0].length).trim() };
252
+ }
253
+ const m = PLAIN_KEY_IN.exec(text);
254
+ if (!m) return null;
255
+ return { key: m[1], rest: text.slice(m[0].length).trim() };
256
+ }
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // Parser: flow collections
260
+ // ---------------------------------------------------------------------------
261
+
262
+ class FlowReader {
263
+ constructor(src) {
264
+ this.src = src;
265
+ this.pos = 0;
266
+ }
267
+
268
+ incomplete() {
269
+ return unterminated('flow collection', true);
270
+ }
271
+
272
+ skipSpace() {
273
+ const s = this.src;
274
+ while (this.pos < s.length) {
275
+ const c = s[this.pos];
276
+ if (c === ' ' || c === '\t' || c === '\n') {
277
+ this.pos += 1;
278
+ } else if (c === '#' && (this.pos === 0 || /\s/.test(s[this.pos - 1]))) {
279
+ const nl = s.indexOf('\n', this.pos);
280
+ this.pos = nl < 0 ? s.length : nl;
281
+ } else {
282
+ break;
283
+ }
284
+ }
285
+ }
286
+
287
+ value(ctx) {
288
+ this.skipSpace();
289
+ const s = this.src;
290
+ if (this.pos >= s.length) throw this.incomplete();
291
+ const c = s[this.pos];
292
+ if (c === '[') return this.sequence();
293
+ if (c === '{') return this.mapping();
294
+ if (c === '"' || c === "'") {
295
+ const q = readQuoted(s, this.pos, true);
296
+ this.pos = q.end;
297
+ return q.value;
298
+ }
299
+ if (c === ']' || c === '}' || c === ',') throw new Error(`unexpected "${c}" in a flow collection`);
300
+ return typePlain(this.plain(ctx));
301
+ }
302
+
303
+ plain(ctx) {
304
+ const s = this.src;
305
+ const start = this.pos;
306
+ while (this.pos < s.length) {
307
+ const c = s[this.pos];
308
+ if (c === ',' || c === '[' || c === ']' || c === '{' || c === '}' || c === '\n') break;
309
+ if (c === '#' && /\s/.test(s[this.pos - 1])) break;
310
+ if (ctx === 'key' && c === ':' && (this.pos + 1 >= s.length || /[\s,[\]{}]/.test(s[this.pos + 1]))) break;
311
+ this.pos += 1;
312
+ }
313
+ return s.slice(start, this.pos).trim();
314
+ }
315
+
316
+ sequence() {
317
+ const s = this.src;
318
+ this.pos += 1;
319
+ const out = [];
320
+ for (;;) {
321
+ this.skipSpace();
322
+ if (this.pos >= s.length) throw this.incomplete();
323
+ if (s[this.pos] === ']') {
324
+ this.pos += 1;
325
+ return out;
326
+ }
327
+ out.push(this.value('seq'));
328
+ this.skipSpace();
329
+ if (this.pos >= s.length) throw this.incomplete();
330
+ const c = s[this.pos];
331
+ this.pos += 1;
332
+ if (c === ']') return out;
333
+ if (c !== ',') throw new Error(`expected "," or "]" in a flow sequence, got "${c}"`);
334
+ }
335
+ }
336
+
337
+ mapping() {
338
+ const s = this.src;
339
+ this.pos += 1;
340
+ const out = {};
341
+ for (;;) {
342
+ this.skipSpace();
343
+ if (this.pos >= s.length) throw this.incomplete();
344
+ if (s[this.pos] === '}') {
345
+ this.pos += 1;
346
+ return out;
347
+ }
348
+ let key;
349
+ if (s[this.pos] === '"' || s[this.pos] === "'") {
350
+ const q = readQuoted(s, this.pos, true);
351
+ this.pos = q.end;
352
+ key = q.value;
353
+ } else {
354
+ key = this.plain('key');
355
+ if (key === '') throw new Error('empty key in a flow mapping');
356
+ }
357
+ this.skipSpace();
358
+ if (this.pos >= s.length) throw this.incomplete();
359
+ let value = null;
360
+ if (s[this.pos] === ':') {
361
+ this.pos += 1;
362
+ this.skipSpace();
363
+ if (this.pos >= s.length) throw this.incomplete();
364
+ if (s[this.pos] !== ',' && s[this.pos] !== '}') value = this.value('map');
365
+ }
366
+ if (Object.prototype.hasOwnProperty.call(out, key)) throw new Error(`duplicate key "${key}" in a flow mapping`);
367
+ setKey(out, key, value);
368
+ this.skipSpace();
369
+ if (this.pos >= s.length) throw this.incomplete();
370
+ const c = s[this.pos];
371
+ this.pos += 1;
372
+ if (c === '}') return out;
373
+ if (c !== ',') throw new Error(`expected "," or "}" in a flow mapping, got "${c}"`);
374
+ }
375
+ }
376
+ }
377
+
378
+ // ---------------------------------------------------------------------------
379
+ // Parser: block structure
380
+ // ---------------------------------------------------------------------------
381
+
382
+ class BlockParser {
383
+ constructor(text, lineOffset) {
384
+ this.lines = text.replace(/\r\n?/g, '\n').split('\n');
385
+ this.i = 0;
386
+ this.lineOffset = lineOffset;
387
+ }
388
+
389
+ error(message, line = this.i) {
390
+ return new Error(`YAML line ${line + 1 + this.lineOffset}: ${message}`);
391
+ }
392
+
393
+ /** The next significant line (blank and comment lines skipped), not consumed. */
394
+ peek() {
395
+ while (this.i < this.lines.length) {
396
+ const raw = this.lines[this.i];
397
+ const indent = raw.length - raw.replace(/^ +/, '').length;
398
+ const rest = raw.slice(indent);
399
+ const trimmed = rest.trim();
400
+ if (trimmed === '' || trimmed.startsWith('#')) {
401
+ this.i += 1;
402
+ continue;
403
+ }
404
+ if (rest[0] === '\t') throw this.error('tabs are not allowed for indentation');
405
+ return { indent, text: rest.replace(/[ \t]+$/, ''), line: this.i };
406
+ }
407
+ return null;
408
+ }
409
+
410
+ document() {
411
+ const first = this.peek();
412
+ if (!first) return {};
413
+ const value = this.node();
414
+ const extra = this.peek();
415
+ if (extra) throw this.error(`unexpected "${clip(extra.text)}"; check the indentation`, extra.line);
416
+ return value;
417
+ }
418
+
419
+ /** Parse the node that starts at the next significant line. */
420
+ node() {
421
+ const ln = this.peek();
422
+ if (isSequenceItem(ln.text)) return this.sequence(ln.indent);
423
+ if (readKey(ln.text)) return this.mapping(ln.indent);
424
+ this.i = ln.line + 1;
425
+ return this.scalar(ln.text, ln.indent - 1, ln.line);
426
+ }
427
+
428
+ mapping(indent) {
429
+ const out = {};
430
+ for (;;) {
431
+ const ln = this.peek();
432
+ if (!ln || ln.indent < indent) break;
433
+ if (ln.indent > indent) throw this.error('unexpected indentation', ln.line);
434
+ if (isSequenceItem(ln.text)) break;
435
+ const entry = readKey(ln.text);
436
+ if (!entry) throw this.error(`expected "key: value", got "${clip(ln.text)}"`, ln.line);
437
+ if (Object.prototype.hasOwnProperty.call(out, entry.key)) throw this.error(`duplicate key "${entry.key}"`, ln.line);
438
+ this.i = ln.line + 1;
439
+ setKey(out, entry.key, this.value(entry.rest, indent, ln.line));
440
+ }
441
+ return out;
442
+ }
443
+
444
+ /** The value after `key:` — inline, or a nested block on the following lines. */
445
+ value(rest, indent, line) {
446
+ if (rest === '' || rest.startsWith('#')) {
447
+ const next = this.peek();
448
+ if (next && next.indent > indent) return this.node();
449
+ if (next && next.indent === indent && isSequenceItem(next.text)) return this.sequence(indent);
450
+ return null;
451
+ }
452
+ return this.scalar(rest, indent, line);
453
+ }
454
+
455
+ sequence(indent) {
456
+ const out = [];
457
+ for (;;) {
458
+ const ln = this.peek();
459
+ if (!ln || ln.indent < indent) break;
460
+ if (ln.indent > indent) throw this.error('unexpected indentation', ln.line);
461
+ if (!isSequenceItem(ln.text)) break;
462
+ const after = ln.text.slice(1);
463
+ const content = after.replace(/^[ \t]+/, '');
464
+ const col = indent + 1 + (after.length - content.length);
465
+ if (content === '' || content.startsWith('#')) {
466
+ this.i = ln.line + 1;
467
+ const next = this.peek();
468
+ out.push(next && next.indent > indent ? this.node() : null);
469
+ } else if (isSequenceItem(content) || readKey(content)) {
470
+ // Compact nested collection ("- key: v" / "- - x"): re-read the item as a block at its column.
471
+ this.lines[ln.line] = ' '.repeat(col) + content;
472
+ out.push(this.node());
473
+ } else {
474
+ this.i = ln.line + 1;
475
+ out.push(this.scalar(content, indent, ln.line));
476
+ }
477
+ }
478
+ return out;
479
+ }
480
+
481
+ /** An inline value: block scalar header, quoted string, flow collection or plain scalar. */
482
+ scalar(text, parentIndent, line) {
483
+ const c = text[0];
484
+ if (c === '|' || c === '>') return this.blockScalar(text, parentIndent, line);
485
+ if (c === '"' || c === "'") {
486
+ let src = text;
487
+ let q;
488
+ for (;;) {
489
+ try {
490
+ q = readQuoted(src, 0);
491
+ break;
492
+ } catch (e) {
493
+ // A quoted scalar may continue on the following lines: each line
494
+ // break folds to a space, a blank line to a newline.
495
+ if (!/^unterminated/.test(e.message) || this.i >= this.lines.length) throw this.error(e.message, line);
496
+ let blanks = 0;
497
+ while (this.i < this.lines.length && this.lines[this.i].trim() === '') {
498
+ blanks += 1;
499
+ this.i += 1;
500
+ }
501
+ if (this.i >= this.lines.length) throw this.error(e.message, line);
502
+ const nl = c === '"' ? '\\n' : '\n';
503
+ src += (blanks ? nl.repeat(blanks) : ' ') + this.lines[this.i].trim();
504
+ this.i += 1;
505
+ }
506
+ }
507
+ const tail = src.slice(q.end);
508
+ if (tail.trim() !== '' && !/^[ \t]+#/.test(tail)) throw this.error(`unexpected "${clip(tail.trim())}" after a quoted string`, line);
509
+ return q.value;
510
+ }
511
+ if (c === '[' || c === '{') return this.flow(text, line);
512
+ if (c === '&' || c === '*' || c === '!') throw this.error('anchors, aliases and tags are not supported', line);
513
+ if (c === '?' && (text.length === 1 || text[1] === ' ')) throw this.error('complex keys are not supported', line);
514
+ const first = stripComment(text);
515
+ if (first !== text.trim()) return typePlain(first);
516
+ return this.plainContinuation(first, parentIndent);
517
+ }
518
+
519
+ /**
520
+ * A plain scalar continues on following lines indented deeper than its parent:
521
+ * each line break folds to a space, blank lines to newlines; a comment ends it.
522
+ */
523
+ plainContinuation(first, parentIndent) {
524
+ let out = first;
525
+ let joined = false;
526
+ let j = this.i;
527
+ let blanks = 0;
528
+ while (j < this.lines.length) {
529
+ const raw = this.lines[j];
530
+ if (raw.trim() === '') {
531
+ blanks += 1;
532
+ j += 1;
533
+ continue;
534
+ }
535
+ const ind = raw.length - raw.replace(/^ +/, '').length;
536
+ const trimmed = raw.trim();
537
+ if (ind <= parentIndent || trimmed.startsWith('#') || raw[ind] === '\t') break;
538
+ // A nested key or list item under a plain value is a mistake, not text.
539
+ if (readKey(trimmed) || isSequenceItem(trimmed)) throw this.error('unexpected indentation', j);
540
+ const piece = stripComment(trimmed);
541
+ out += (blanks ? '\n'.repeat(blanks) : ' ') + piece;
542
+ joined = true;
543
+ blanks = 0;
544
+ j += 1;
545
+ this.i = j;
546
+ if (piece !== trimmed) break;
547
+ }
548
+ return joined ? out : typePlain(first);
549
+ }
550
+
551
+ flow(text, line) {
552
+ let src = text;
553
+ for (;;) {
554
+ const reader = new FlowReader(src);
555
+ try {
556
+ const value = reader.value('top');
557
+ reader.skipSpace();
558
+ if (reader.pos < src.length) throw new Error(`unexpected "${clip(src.slice(reader.pos))}" after a flow collection`);
559
+ return value;
560
+ } catch (e) {
561
+ if (e.incomplete && this.i < this.lines.length) {
562
+ src += `\n${this.lines[this.i]}`;
563
+ this.i += 1;
564
+ continue;
565
+ }
566
+ throw this.error(e.message, line);
567
+ }
568
+ }
569
+ }
570
+
571
+ blockScalar(header, parentIndent, line) {
572
+ const m = /^([|>])([+-]?)([1-9]?)([+-]?)[ \t]*(#.*)?$/.exec(header);
573
+ if (!m || (m[2] && m[4])) throw this.error(`bad block scalar header "${clip(header)}"`, line);
574
+ const folded = m[1] === '>';
575
+ const chomp = m[2] || m[4];
576
+ let blockIndent = m[3] ? Math.max(parentIndent, 0) + Number(m[3]) : null;
577
+ const raw = [];
578
+ while (this.i < this.lines.length) {
579
+ const l = this.lines[this.i];
580
+ if (l.trim() === '') {
581
+ raw.push('');
582
+ this.i += 1;
583
+ continue;
584
+ }
585
+ const ind = l.length - l.replace(/^ +/, '').length;
586
+ if (blockIndent === null) {
587
+ if (ind <= parentIndent) break;
588
+ blockIndent = ind;
589
+ }
590
+ if (ind < blockIndent) break;
591
+ raw.push(l.slice(blockIndent));
592
+ this.i += 1;
593
+ }
594
+ let end = raw.length;
595
+ while (end > 0 && raw[end - 1] === '') end -= 1;
596
+ const content = raw.slice(0, end);
597
+ const trailing = raw.length - end;
598
+ if (content.length === 0) return chomp === '+' ? '\n'.repeat(trailing) : '';
599
+ const text = folded ? fold(content) : content.join('\n');
600
+ if (chomp === '-') return text;
601
+ if (chomp === '+') return `${text}\n${'\n'.repeat(trailing)}`;
602
+ return `${text}\n`;
603
+ }
604
+ }
605
+
606
+ /** Folding for `>` scalars: adjacent normal lines join with a space; blank lines become newlines. */
607
+ function fold(lines) {
608
+ let out = '';
609
+ let prev = null;
610
+ let blanks = 0;
611
+ for (const l of lines) {
612
+ if (l === '') {
613
+ blanks += 1;
614
+ continue;
615
+ }
616
+ const more = /^[ \t]/.test(l);
617
+ if (prev === null) out += '\n'.repeat(blanks) + l;
618
+ else if (!more && prev === 'text') out += (blanks === 0 ? ' ' : '\n'.repeat(blanks)) + l;
619
+ else out += `\n${'\n'.repeat(blanks)}${l}`;
620
+ blanks = 0;
621
+ prev = more ? 'more' : 'text';
622
+ }
623
+ return out;
624
+ }