claude-do-it 0.1.1 → 0.1.3

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/bin/daemon.js CHANGED
@@ -1,2212 +1,15 @@
1
1
  // @bun
2
- // packages/server/src/main.ts
3
- import { BunRuntime } from "@effect/platform-bun";
4
- import { Cause as Cause3, Effect as Effect12, Exit as Exit2 } from "effect";
5
-
6
- // packages/doc/src/constants.ts
7
- var MAX_BYTES = 2 * 1024 * 1024;
8
- var KEPT = 512;
9
- var COMPACT_KEEP = 128;
10
- var MAX_DIFF = 64 * 1024;
11
- var MAX_ITEM = 4096;
12
- // packages/doc/src/errors.ts
13
- import * as Data from "effect/Data";
14
-
15
- class DocError extends Data.TaggedError("DocError") {
16
- }
17
- // packages/doc/src/version.ts
18
- function checkVersion(version) {
19
- if (!Number.isSafeInteger(version) || version < 1) {
20
- throw new DocError({ code: "invalid", message: "A version must be a positive safe integer" });
21
- }
22
- }
23
-
24
- class History {
25
- entries = new Array(KEPT);
26
- start = 0;
27
- count = 0;
28
- get size() {
29
- return this.count;
30
- }
31
- record(version, frontiers, origin) {
32
- const entry = { version, frontiers, origin: origin === null ? null : { ...origin } };
33
- const last = (this.start + this.count - 1) % KEPT;
34
- if (this.count && this.entries[last]?.version === version) {
35
- this.entries[last] = entry;
36
- return;
37
- }
38
- if (this.count === KEPT) {
39
- this.entries[this.start] = entry;
40
- this.start = (this.start + 1) % KEPT;
41
- } else {
42
- this.entries[(this.start + this.count) % KEPT] = entry;
43
- this.count++;
44
- }
45
- }
46
- find(version) {
47
- for (let i = this.count - 1;i >= 0; i--) {
48
- const entry = this.entries[(this.start + i) % KEPT];
49
- if (entry.version === version)
50
- return entry;
51
- }
52
- return null;
53
- }
54
- list() {
55
- return Array.from({ length: this.count }, (_, i) => this.entries[(this.start + i) % KEPT]);
56
- }
57
- originsSince(version) {
58
- if (!this.find(version))
59
- return null;
60
- const seen = new Set;
61
- const origins = [];
62
- for (let i = 0;i < this.count; i++) {
63
- const entry = this.entries[(this.start + i) % KEPT];
64
- if (entry.version <= version || !entry.origin)
65
- continue;
66
- const key = originKey(entry.origin);
67
- if (!seen.has(key)) {
68
- seen.add(key);
69
- origins.push({ ...entry.origin });
70
- }
71
- }
72
- return origins;
73
- }
74
- }
75
- function originKey(origin) {
76
- return origin.kind === "agent" ? `agent:${origin.name}` : origin.kind;
77
- }
78
- // packages/doc/src/edit.ts
79
- var encoder = new TextEncoder;
80
- function utf8Length(text) {
81
- return encoder.encode(text).length;
82
- }
83
- function checkSize(text) {
84
- const bytes = utf8Length(text);
85
- if (bytes > MAX_BYTES) {
86
- throw new DocError({ code: "too-large", message: `The document would be ${bytes} bytes, over the limit of ${MAX_BYTES}` });
87
- }
88
- }
89
- function checkText(text) {
90
- checkSize(text);
91
- if (!text.isWellFormed()) {
92
- throw new DocError({ code: "invalid", message: "Text contains an unpaired surrogate" });
93
- }
94
- }
95
- function boundary(text, at) {
96
- const left = text.charCodeAt(at - 1);
97
- const right = text.charCodeAt(at);
98
- return !(left >= 55296 && left <= 56319 && right >= 56320 && right <= 57343);
99
- }
100
- function checkedEdits(text, edits) {
101
- const sorted = edits.map((edit) => ({ ...edit })).sort((a, b) => a.from - b.from || a.to - b.to);
102
- let end = 0;
103
- for (const edit of sorted) {
104
- if (!Number.isSafeInteger(edit.from) || !Number.isSafeInteger(edit.to) || edit.from < end || edit.from > edit.to || edit.to > text.length || !boundary(text, edit.from) || !boundary(text, edit.to)) {
105
- throw new DocError({ code: "invalid", message: `The edit ${edit.from}..${edit.to} overlaps another edit, is outside the text, or splits a character` });
106
- }
107
- checkText(edit.insert);
108
- end = edit.to;
109
- }
110
- return sorted.filter((edit) => text.slice(edit.from, edit.to) !== edit.insert);
111
- }
112
- function applyEdits(text, edits) {
113
- const sorted = checkedEdits(text, edits);
114
- const parts = [];
115
- let at = 0;
116
- for (const edit of sorted) {
117
- parts.push(text.slice(at, edit.from), edit.insert);
118
- at = edit.to;
119
- }
120
- parts.push(text.slice(at));
121
- const result = parts.join("");
122
- checkText(result);
123
- return result;
124
- }
125
- // packages/doc/src/diff.ts
126
- import { createTwoFilesPatch, diffArrays, diffChars } from "diff";
127
- var MAX_REFINED = 16 * 1024;
128
- var DEADLINE_MS = 250;
129
- function replacements(parts, offset = 0) {
130
- const result = [];
131
- let at = offset;
132
- let from = at;
133
- let inserted = [];
134
- function flush() {
135
- if (from !== at || inserted.length)
136
- result.push({ from, to: at, insert: inserted.join("") });
137
- inserted = [];
138
- }
139
- for (const part of parts) {
140
- if (part.added)
141
- inserted.push(part.value);
142
- else if (part.removed)
143
- at += part.value.length;
144
- else {
145
- flush();
146
- at += part.value.length;
147
- from = at;
148
- }
149
- }
150
- flush();
151
- return result;
152
- }
153
- function edits(old, next) {
154
- if (old === next)
155
- return [];
156
- const deadline = performance.now() + DEADLINE_MS;
157
- const oldLines = old.match(/[^\n]*\n|[^\n]+$/g) ?? [];
158
- const newLines = next.match(/[^\n]*\n|[^\n]+$/g) ?? [];
159
- const remaining = deadline - performance.now();
160
- const parts = remaining > 0 ? diffArrays(oldLines, newLines, { timeout: remaining }) : undefined;
161
- if (!parts)
162
- return [{ from: 0, to: old.length, insert: next }];
163
- const hunks = replacements(parts.map((part) => ({ ...part, value: part.value.join("") })));
164
- const result = [];
165
- for (const hunk of hunks) {
166
- const before = old.slice(hunk.from, hunk.to);
167
- const remaining = deadline - performance.now();
168
- if (!before || !hunk.insert || remaining <= 0 || utf8Length(before) > MAX_REFINED || utf8Length(hunk.insert) > MAX_REFINED) {
169
- result.push(hunk);
170
- continue;
171
- }
172
- const refined = diffChars(before, hunk.insert, { timeout: Math.min(DEADLINE_MS / 4, remaining) });
173
- if (refined)
174
- result.push(...replacements(refined, hunk.from));
175
- else
176
- result.push(hunk);
177
- }
178
- return result;
179
- }
180
- function unified(old, next, from, to) {
181
- if (old === next)
182
- return "";
183
- const diff = createTwoFilesPatch(String(from), String(to), old, next, undefined, undefined, {
184
- context: 2,
185
- headerOptions: { includeIndex: false, includeUnderline: false, includeFileHeaders: true }
186
- });
187
- return utf8Length(diff) > MAX_DIFF ? null : diff;
188
- }
189
- // packages/doc/src/markdown.ts
190
- function lines(text) {
191
- const out = [];
192
- let fence = null;
193
- let from = 0;
194
- while (true) {
195
- const newline = text.indexOf(`
196
- `, from);
197
- const end = newline === -1 ? text.length : newline;
198
- const to = end > from && text[end - 1] === "\r" ? end - 1 : end;
199
- const raw = text.slice(from, to);
200
- const lead = /^[ \t]*/.exec(raw)[0];
201
- const rest = raw.slice(lead.length);
202
- const at = from + lead.length;
203
- const line = {
204
- from,
205
- to,
206
- indent: lead.length,
207
- depth: [...lead].reduce((n, ch) => n + (ch === "\t" ? 4 : 1), 0),
208
- content: at,
209
- kind: "paragraph"
210
- };
211
- if (fence) {
212
- let run = 0;
213
- while (rest[run] === fence.marker)
214
- run++;
215
- if (run >= fence.count && !rest.slice(run).trim()) {
216
- line.kind = "fence";
217
- fence = null;
218
- } else
219
- line.kind = "code";
220
- } else {
221
- const opening = /^(`{3,}|~{3,})(.*)$/.exec(rest);
222
- const heading = /^(#{1,6})(?: |$)/.exec(rest);
223
- const list = /^(?:[-*+]|[0-9]{1,9}[.)])(?:[ \t]|$)/.exec(rest);
224
- if (opening && (opening[1][0] === "~" || !opening[2].includes("`"))) {
225
- fence = { marker: opening[1][0], count: opening[1].length };
226
- line.kind = "fence";
227
- } else if (!rest)
228
- line.kind = "blank";
229
- else if (heading) {
230
- line.kind = "heading";
231
- line.level = heading[1].length;
232
- line.content = at + heading[0].length;
233
- } else if (/^(?:-(?: *-){2,}|\*(?: *\*){2,}|_(?: *_){2,})\s*$/.test(rest)) {
234
- line.kind = "rule";
235
- } else if (rest.startsWith(">"))
236
- line.kind = "quote";
237
- else if (list) {
238
- line.kind = "item";
239
- line.content = at + list[0].length;
240
- const task = /^\[([ xX-])\](?:[ \t]|$)/.exec(rest.slice(list[0].length));
241
- if (task) {
242
- line.checkbox = line.content;
243
- line.content += task[0].length;
244
- line.state = task[1] === " " ? "open" : task[1] === "-" ? "cancelled" : "done";
245
- }
246
- }
247
- }
248
- out.push(line);
249
- if (newline === -1)
250
- return out;
251
- from = end + 1;
252
- }
253
- }
254
- function blockEnd(all, index) {
255
- const depth = all[index].depth;
256
- let end = index + 1;
257
- for (let probe = end;probe < all.length; probe++) {
258
- const line = all[probe];
259
- if (line.kind === "blank")
260
- continue;
261
- if (line.depth <= depth || line.kind === "heading")
262
- break;
263
- end = probe + 1;
264
- }
265
- return end;
266
- }
267
-
268
- // packages/doc/src/section.ts
269
- function slug(title) {
270
- return title.toLowerCase().replace(/[^\p{L}\p{N} _-]/gu, "").replace(/ /g, "-");
271
- }
272
- function sections(text) {
273
- const result = [];
274
- const counts = new Map;
275
- for (const [index, line] of lines(text).entries()) {
276
- if (line.kind !== "heading" || line.level > 3)
277
- continue;
278
- const title = text.slice(line.content, line.to).trim().replace(/#+$/, "").trim();
279
- const base = slug(title);
280
- const count = counts.get(base) ?? 0;
281
- counts.set(base, count + 1);
282
- const previous = result.at(-1);
283
- if (previous)
284
- previous.to = line.from;
285
- result.push({
286
- id: count ? `${base}-${count}` : base,
287
- title,
288
- level: line.level,
289
- line: index,
290
- from: line.from,
291
- to: text.length
292
- });
293
- }
294
- return result;
295
- }
296
- function findSection(text, id) {
297
- const query = id.trim().replace(/^#+/, "");
298
- const section = sections(text).find((section) => section.id === query);
299
- if (!section)
300
- throw new DocError({ code: "not-found", message: `No section has the ID ${JSON.stringify(query)}` });
301
- return section;
302
- }
303
- function sectionText(text, id) {
304
- const query = id.trim().replace(/^#+/, "");
305
- const section = sections(text).find((section) => section.id === query);
306
- return section ? text.slice(section.from, section.to) : null;
307
- }
308
- // packages/doc/src/stamp.ts
309
- var stampNames = "started|done|cancelled";
310
- function stampPattern(tag) {
311
- const name = tag ?? stampNames;
312
- return new RegExp(`(?:^|[ \\t]+)@(?:${name})(?![A-Za-z0-9_])(?:[ \\t]*\\([^()\\r\\n]*\\))?`, "g");
313
- }
314
- function removeStamp(label, tag) {
315
- return label.replace(stampPattern(tag), "");
316
- }
317
- function hasStamp(label, tag) {
318
- return stampPattern(tag).test(label);
319
- }
320
- function taskLabel(text, todo) {
321
- const line = lines(text)[todo.line];
322
- if (!line || line.state === undefined)
323
- return null;
324
- return { from: line.content, to: line.to, body: text.slice(line.content, line.to) };
325
- }
326
- function stampText(label, stamp) {
327
- const trailing = /[ \t]*$/.exec(label);
328
- const core = label.slice(0, trailing.index);
329
- const project = /[ \t]+@project(?:[ \t]*\([^()\r\n]*\))?$/.exec(core);
330
- const beforeProject = project ? core.slice(0, project.index) : core;
331
- const prefix = beforeProject.trimEnd();
332
- const withStamp = prefix ? `${prefix} ${stamp}` : stamp;
333
- return `${withStamp}${project?.[0] ?? ""}${trailing[0]}`;
334
- }
335
- function labelEdit(text, todo, make) {
336
- const target = taskLabel(text, todo);
337
- if (!target)
338
- return [];
339
- const insert = make(target.body);
340
- return insert === target.body ? [] : [{ from: target.from, to: target.to, insert }];
341
- }
342
- function stampTextFor(tag, date) {
343
- const formatted = formatStamp(date);
344
- return tag === "started" ? `@${tag}(${formatted})` : `@${tag} (${formatted})`;
345
- }
346
- function checkboxEdit(text, todo, state) {
347
- const line = lines(text)[todo.line];
348
- if (!line || line.checkbox === undefined || line.state === state)
349
- return [];
350
- const insert = state === "done" ? "[x]" : state === "cancelled" ? "[-]" : "[ ]";
351
- return [{ from: line.checkbox, to: line.checkbox + 3, insert }];
352
- }
353
- function formatStamp(date) {
354
- const two = (value) => String(value).padStart(2, "0");
355
- return `${two(date.getFullYear() % 100)}-${two(date.getMonth() + 1)}-${two(date.getDate())} ${two(date.getHours())}:${two(date.getMinutes())}`;
356
- }
357
- function stampTodo(text, todo, tag, date) {
358
- const stamp = stampTextFor(tag, date);
359
- return labelEdit(text, todo, (label) => stampText(removeStamp(label, tag), stamp));
360
- }
361
- function setTodoStateStamped(text, todo, state, date) {
362
- const currentState = lines(text)[todo.line]?.state;
363
- const edits = checkboxEdit(text, todo, state);
364
- const tag = state === "done" ? "done" : state === "cancelled" ? "cancelled" : null;
365
- const labelEdits = labelEdit(text, todo, (label) => {
366
- let next = label;
367
- if (state === "done")
368
- next = removeStamp(next, "cancelled");
369
- else if (state === "cancelled")
370
- next = removeStamp(next, "done");
371
- else
372
- next = removeStamp(removeStamp(next, "done"), "cancelled");
373
- if (tag && (currentState !== state || !hasStamp(next, tag))) {
374
- next = stampText(removeStamp(next, tag), stampTextFor(tag, date));
375
- }
376
- return next;
377
- });
378
- return [...edits, ...labelEdits].sort((a, b) => a.from - b.from || a.to - b.to);
379
- }
380
- function withoutStamps(label) {
381
- return label.replace(stampPattern(), "").trim();
382
- }
383
-
384
- // packages/doc/src/todo.ts
385
- function todos(text) {
386
- const result = [];
387
- let section = null;
388
- const sectionByLine = new Map(sections(text).map((value) => [value.line, value.id]));
389
- for (const [index, line] of lines(text).entries()) {
390
- const id = sectionByLine.get(index);
391
- if (id !== undefined)
392
- section = id;
393
- if (line.state === undefined)
394
- continue;
395
- result.push({
396
- number: result.length + 1,
397
- line: index,
398
- state: line.state,
399
- text: text.slice(line.content, line.to).trim(),
400
- indent: line.depth,
401
- section
402
- });
403
- }
404
- return result;
405
- }
406
- function findTodo(text, query, section) {
407
- const scope = section === undefined ? null : findSection(text, section);
408
- const allLines = scope ? lines(text) : null;
409
- const candidates = todos(text).filter((todo) => !scope || allLines[todo.line].from >= scope.from && allLines[todo.line].from < scope.to);
410
- const needle = withoutStamps(String(query).trim()).toLowerCase();
411
- let matches;
412
- if (typeof query === "number" || /^\+?\d+$/.test(needle)) {
413
- matches = candidates.filter((todo) => todo.number === Number(needle));
414
- } else {
415
- matches = candidates.filter((todo) => withoutStamps(todo.text).toLowerCase() === needle);
416
- if (!matches.length && needle)
417
- matches = candidates.filter((todo) => withoutStamps(todo.text).toLowerCase().includes(needle));
418
- }
419
- if (!matches.length)
420
- throw new DocError({ code: "not-found", message: `No todo matches ${JSON.stringify(query)}` });
421
- if (matches.length > 1)
422
- throw new DocError({ code: "ambiguous", message: `${matches.length} todos match ${JSON.stringify(query)}, name one by its number` });
423
- return matches[0];
424
- }
425
- function checked(text, edit) {
426
- applyEdits(text, [edit]);
427
- return [edit];
428
- }
429
- function addTodo(text, item, section) {
430
- item = item.trim();
431
- if ([...item].length > MAX_ITEM) {
432
- throw new DocError({ code: "too-large", message: `A todo may have at most ${MAX_ITEM} characters` });
433
- }
434
- if (!item || /[\r\n]/.test(item)) {
435
- throw new DocError({ code: "invalid", message: "A todo must be one nonempty line of text" });
436
- }
437
- const scope = section === undefined ? null : findSection(text, section);
438
- const all = lines(text);
439
- let last = -1;
440
- for (const [index, line] of all.entries()) {
441
- if (line.state !== undefined && (!scope || line.from >= scope.from && line.from < scope.to))
442
- last = index;
443
- }
444
- const task = `- [ ] ${item}`;
445
- if (last === -1)
446
- return appendBlock(text, task, section);
447
- const line = all[last];
448
- const end = all[blockEnd(all, last) - 1].to;
449
- const indent = text.slice(line.from, line.from + line.indent);
450
- return checked(text, { from: end, to: end, insert: `
451
- ${indent}${task}` });
452
- }
453
- function append(text, block) {
454
- const trimmed = text.replace(/[\r\n]+$/, "");
455
- const last = lines(trimmed).at(-1);
456
- const separator = !trimmed ? "" : last?.kind === "item" && block.startsWith("- ") ? `
457
- ` : `
458
-
459
- `;
460
- return { from: trimmed.length, to: text.length, insert: `${separator}${block}
461
- ` };
462
- }
463
- function appendBlock(text, block, section) {
464
- block = block.replace(/[\r\n]+$/, "");
465
- if (!block.trim())
466
- throw new DocError({ code: "invalid", message: "There is no text to add" });
467
- if (section === undefined)
468
- return checked(text, append(text, block));
469
- const scope = findSection(text, section);
470
- const body = text.slice(scope.from, scope.to);
471
- const content = body.replace(/[\r\n]+$/, "");
472
- const edit = append(content, block);
473
- return checked(text, {
474
- from: scope.from + edit.from,
475
- to: scope.from + edit.to,
476
- insert: content.length === body.length ? edit.insert : edit.insert.replace(/\n+$/, "")
477
- });
478
- }
479
- function replaceUnique(text, old, next, section) {
480
- if (!old)
481
- throw new DocError({ code: "invalid", message: "Text to replace must not be empty" });
482
- const scope = section === undefined ? { from: 0, to: text.length } : findSection(text, section);
483
- const body = text.slice(scope.from, scope.to);
484
- const at = body.indexOf(old);
485
- if (at === -1)
486
- throw new DocError({ code: "not-found", message: "The text to replace is not in the document" });
487
- if (body.indexOf(old, at + old.length) !== -1) {
488
- throw new DocError({ code: "ambiguous", message: "Several places match the text to replace, include more of it so one does" });
489
- }
490
- return checked(text, { from: scope.from + at, to: scope.from + at + old.length, insert: next });
491
- }
492
- // packages/doc/src/todo-changes.ts
493
- import { diffArrays as diffArrays2, diffChars as diffChars2 } from "diff";
494
- function change(kind, now, before = now) {
495
- return { kind, number: now.number, text: withoutStamps(now.text), line: now.line, oldLine: before.line };
496
- }
497
- function stateChange(before, now) {
498
- if (before === now)
499
- return null;
500
- if (now === "done")
501
- return "checked";
502
- if (now === "cancelled")
503
- return "cancelled";
504
- return before === "done" ? "unchecked" : "uncancelled";
505
- }
506
- function similarity(a, b) {
507
- const length = [...a].length + [...b].length;
508
- if (!length)
509
- return 1;
510
- const equal = diffChars2(a, b).reduce((n, part) => n + (part.added || part.removed ? 0 : part.count), 0);
511
- return 2 * equal / length;
512
- }
513
- function todoChanges(old, next) {
514
- const before = todos(old);
515
- const after = todos(next);
516
- const parts = diffArrays2(before.map((todo) => withoutStamps(todo.text)), after.map((todo) => withoutStamps(todo.text)));
517
- const result = [];
518
- let oldAt = 0;
519
- let newAt = 0;
520
- for (let i = 0;i < parts.length; i++) {
521
- const part = parts[i];
522
- if (!part.added && !part.removed) {
523
- for (let k = 0;k < part.count; k++) {
524
- const was = before[oldAt++];
525
- const now = after[newAt++];
526
- const kind = stateChange(was.state, now.state);
527
- if (kind)
528
- result.push(change(kind, now, was));
529
- }
530
- continue;
531
- }
532
- const olds = [];
533
- const news = [];
534
- while (i < parts.length && (parts[i].added || parts[i].removed)) {
535
- const run = parts[i];
536
- if (run.removed) {
537
- olds.push(...before.slice(oldAt, oldAt + run.count));
538
- oldAt += run.count;
539
- } else {
540
- news.push(...after.slice(newAt, newAt + run.count));
541
- newAt += run.count;
542
- }
543
- i++;
544
- }
545
- i--;
546
- const taken = new Set;
547
- for (const now of news) {
548
- let best = -1;
549
- let score = 0.5;
550
- for (const [k, was] of olds.entries()) {
551
- if (taken.has(k))
552
- continue;
553
- const candidate = similarity(was.text, now.text);
554
- if (candidate >= score) {
555
- best = k;
556
- score = candidate;
557
- }
558
- }
559
- if (best === -1)
560
- result.push(change("added", now));
561
- else {
562
- taken.add(best);
563
- result.push(change("edited", now, olds[best]));
564
- }
565
- }
566
- for (const [k, was] of olds.entries()) {
567
- if (!taken.has(k))
568
- result.push(change("removed", was));
569
- }
570
- }
571
- return result;
572
- }
573
- // packages/doc/src/document.ts
574
- import { LoroDoc } from "loro-crdt";
575
- var TEXT = "markdown";
576
- var MAX_WRITING = 8;
577
- var ticketBrand = Symbol("WriteTicket");
578
- function sameFrontiers(a, b) {
579
- return a.length === b.length && a.every((id) => b.some((other) => other.peer === id.peer && other.counter === id.counter));
580
- }
581
- function splice(body, changes) {
582
- for (let i = changes.length - 1;i >= 0; i--) {
583
- const edit = changes[i];
584
- if (edit.to !== edit.from)
585
- body.delete(edit.from, edit.to - edit.from);
586
- if (edit.insert)
587
- body.insert(edit.from, edit.insert);
588
- }
589
- }
590
-
591
- class Document {
592
- doc;
593
- body;
594
- current;
595
- currentVersion;
596
- history = new History;
597
- disk;
598
- agents = new Map;
599
- writing = [];
600
- tickets = new WeakMap;
601
- generation = 0;
602
- constructor(text, version) {
603
- this.doc = new LoroDoc;
604
- this.body = this.doc.getText(TEXT);
605
- this.body.insert(0, text);
606
- this.doc.commit();
607
- this.current = text;
608
- this.currentVersion = version;
609
- this.disk = this.snapshot();
610
- this.history.record(version, this.doc.frontiers(), null);
611
- }
612
- static open(text, version = 1) {
613
- checkText(text);
614
- checkVersion(version);
615
- return new Document(text, version);
616
- }
617
- get text() {
618
- return this.current;
619
- }
620
- get version() {
621
- return this.currentVersion;
622
- }
623
- get synced() {
624
- return { text: this.disk.text, version: this.disk.version };
625
- }
626
- get dirty() {
627
- return this.current !== this.disk.text;
628
- }
629
- agentEdit(origin, scope, make) {
630
- if (scope.section !== undefined)
631
- findSection(this.current, scope.section);
632
- let others = null;
633
- if (scope.version !== this.version) {
634
- const old = this.textAt(scope.version);
635
- const diff = old === null ? null : unified(old, this.current, scope.version, this.version);
636
- const then = old === null || scope.section === undefined ? null : sectionText(old, scope.section);
637
- if (then === null || then !== sectionText(this.current, scope.section)) {
638
- throw new DocError({
639
- code: "conflict",
640
- message: `The document is at version ${this.version}, not ${scope.version}`,
641
- conflict: { expected: scope.version, current: this.version, diff, forgotten: old === null }
642
- });
643
- }
644
- others = { since: scope.version, diff };
645
- }
646
- const edits = checkedEdits(this.current, make(this.current));
647
- const next = applyEdits(this.current, edits);
648
- if (next === this.current)
649
- return this.applied(false, others);
650
- this.checkNextVersion();
651
- const fork = this.doc.fork();
652
- const body = fork.getText(TEXT);
653
- const from = this.doc.oplogVersion();
654
- try {
655
- const key = originKey(origin);
656
- const peer = this.agents.get(key);
657
- if (peer !== undefined)
658
- fork.setPeerId(peer);
659
- splice(body, edits);
660
- fork.commit();
661
- this.doc.import(fork.export({ mode: "update", from }));
662
- this.agents.set(key, fork.peerIdStr);
663
- return this.applied(this.refresh(origin), others);
664
- } finally {
665
- from.free();
666
- body.free();
667
- fork.free();
668
- }
669
- }
670
- userEdit(base, text) {
671
- checkText(text);
672
- const entry = this.history.find(base);
673
- if (!entry)
674
- throw new DocError({ code: "forgotten", message: `Version ${base} is no longer kept` });
675
- if (text === this.current)
676
- return this.applied(false);
677
- if (base === this.version) {
678
- this.checkNextVersion();
679
- splice(this.body, edits(this.current, text));
680
- this.doc.commit();
681
- return this.applied(this.refresh({ kind: "user" }));
682
- }
683
- const fork = this.doc.forkAt(entry.frontiers);
684
- const body = fork.getText(TEXT);
685
- try {
686
- const baseText = body.toString();
687
- if (baseText === text)
688
- return this.applied(false);
689
- splice(body, edits(baseText, text));
690
- this.importFork(fork);
691
- return this.applied(this.refresh({ kind: "user" }));
692
- } finally {
693
- body.free();
694
- fork.free();
695
- }
696
- }
697
- mergeFile(diskText) {
698
- checkText(diskText);
699
- if (diskText === this.disk.text)
700
- return false;
701
- const echo = this.writing.find((ticket) => ticket.text === diskText);
702
- if (echo) {
703
- this.wrote(echo);
704
- return false;
705
- }
706
- if (diskText === this.current) {
707
- this.disk = this.snapshot();
708
- return false;
709
- }
710
- if (sameFrontiers(this.disk.frontiers, this.doc.frontiers())) {
711
- this.checkNextVersion();
712
- splice(this.body, edits(this.disk.text, diskText));
713
- this.doc.commit();
714
- this.disk = { text: diskText, version: this.disk.version, frontiers: this.doc.frontiers() };
715
- return this.refresh({ kind: "file" });
716
- }
717
- const fork = this.doc.forkAt(this.disk.frontiers);
718
- const body = fork.getText(TEXT);
719
- try {
720
- splice(body, edits(this.disk.text, diskText));
721
- fork.commit();
722
- const frontiers = fork.frontiers();
723
- this.importFork(fork);
724
- this.disk = { text: diskText, version: this.disk.version, frontiers };
725
- return this.refresh({ kind: "file" });
726
- } finally {
727
- body.free();
728
- fork.free();
729
- }
730
- }
731
- importFork(fork) {
732
- fork.commit();
733
- const from = this.doc.oplogVersion();
734
- const forkVersion = fork.oplogVersion();
735
- const body = fork.getText(TEXT);
736
- try {
737
- const updates = fork.export({ mode: "update", from });
738
- fork.import(this.doc.export({ mode: "update", from: forkVersion }));
739
- const merged = body.toString();
740
- checkText(merged);
741
- if (merged !== this.current)
742
- this.checkNextVersion();
743
- this.doc.import(updates);
744
- } finally {
745
- body.free();
746
- forkVersion.free();
747
- from.free();
748
- }
749
- }
750
- toWrite() {
751
- const snapshot = this.snapshot();
752
- const ticket = Object.freeze({ text: snapshot.text, version: snapshot.version, [ticketBrand]: true });
753
- this.tickets.set(ticket, { snapshot, generation: this.generation });
754
- if (this.writing.length === MAX_WRITING)
755
- this.writing.shift();
756
- this.writing.push(ticket);
757
- return ticket;
758
- }
759
- wrote(ticket) {
760
- const snapshot = this.ticketSnapshot(ticket);
761
- const at = this.writing.indexOf(ticket);
762
- if (at !== -1)
763
- this.writing.splice(0, at + 1);
764
- this.disk = snapshot;
765
- }
766
- notWritten(ticket) {
767
- this.ticketSnapshot(ticket);
768
- const at = this.writing.indexOf(ticket);
769
- if (at !== -1)
770
- this.writing.splice(at, 1);
771
- }
772
- ticketSnapshot(ticket) {
773
- const data = this.tickets.get(ticket);
774
- if (!data || data.generation !== this.generation) {
775
- throw new DocError({ code: "invalid", message: "The write ticket does not belong to this document's current history" });
776
- }
777
- return data.snapshot;
778
- }
779
- textAt(version) {
780
- if (version === this.version)
781
- return this.current;
782
- const entry = this.history.find(version);
783
- if (!entry)
784
- return null;
785
- const fork = this.doc.forkAt(entry.frontiers);
786
- const body = fork.getText(TEXT);
787
- try {
788
- return body.toString();
789
- } finally {
790
- body.free();
791
- fork.free();
792
- }
793
- }
794
- changes(since, section) {
795
- const by = this.history.originsSince(since);
796
- if (by === null)
797
- return null;
798
- const old = this.textAt(since);
799
- const next = this.current;
800
- const oldPart = section === undefined ? old : sectionText(old, section) ?? "";
801
- const newPart = section === undefined ? next : sectionText(next, section) ?? "";
802
- let changes = todoChanges(old, next);
803
- if (section !== undefined) {
804
- const id = section.trim().replace(/^#+/, "");
805
- const before = sections(old).find((found) => found.id === id);
806
- const after = sections(next).find((found) => found.id === id);
807
- const oldLines = lines(old);
808
- const newLines = lines(next);
809
- changes = changes.filter((change) => {
810
- const removed = change.kind === "removed";
811
- const scope = removed ? before : after;
812
- const line = removed ? oldLines[change.oldLine] : newLines[change.line];
813
- return scope !== undefined && line !== undefined && line.from >= scope.from && line.from < scope.to;
814
- });
815
- }
816
- return { from: since, to: this.version, by, diff: unified(oldPart, newPart, since, this.version), todos: changes };
817
- }
818
- origin(version) {
819
- const origin = this.history.find(version)?.origin;
820
- return origin ? { ...origin } : null;
821
- }
822
- compact(maxOps = Math.max(50000, 4 * this.current.length), maxPeers = 256, keep = COMPACT_KEEP) {
823
- if (this.dirty || this.writing.length)
824
- return null;
825
- let ops = 0;
826
- let peers = 0;
827
- const vv = this.doc.oplogVersion();
828
- try {
829
- ops = this.doc.opCount();
830
- peers = vv.length();
831
- } finally {
832
- vv.free();
833
- }
834
- if (ops <= maxOps && peers <= maxPeers)
835
- return null;
836
- const reason = ops > maxOps ? "ops" : "peers";
837
- const snapshots = this.history.list().slice(-Math.max(1, keep)).map((entry) => ({
838
- ...entry,
839
- text: entry.version === this.version ? this.current : this.textAt(entry.version)
840
- }));
841
- if (snapshots.length === 0 || snapshots.at(-1).version !== this.version)
842
- throw new Error("Compaction history does not include the current document");
843
- const doc = new LoroDoc;
844
- const body = doc.getText(TEXT);
845
- const history = new History;
846
- try {
847
- const first = snapshots[0];
848
- body.insert(0, first.text);
849
- doc.commit();
850
- history.record(first.version, doc.frontiers(), first.origin);
851
- let previous = first.text;
852
- for (const entry of snapshots.slice(1)) {
853
- splice(body, edits(previous, entry.text));
854
- doc.commit();
855
- history.record(entry.version, doc.frontiers(), entry.origin);
856
- previous = entry.text;
857
- }
858
- if (previous !== this.current)
859
- throw new Error("Compaction did not preserve the current document");
860
- } catch (error) {
861
- body.free();
862
- doc.free();
863
- throw error;
864
- }
865
- this.body.free();
866
- this.doc.free();
867
- this.doc = doc;
868
- this.body = body;
869
- this.disk = { ...this.disk, frontiers: doc.frontiers() };
870
- this.history = history;
871
- this.generation++;
872
- return { reason, ops, peers, kept: snapshots.length, oldest: snapshots[0].version };
873
- }
874
- snapshot() {
875
- return { text: this.current, version: this.version, frontiers: this.doc.frontiers() };
876
- }
877
- checkNextVersion() {
878
- checkVersion(this.version + 1);
879
- }
880
- refresh(origin) {
881
- const text = this.body.toString();
882
- const changed = text !== this.current;
883
- if (changed) {
884
- this.current = text;
885
- this.currentVersion++;
886
- }
887
- this.history.record(this.version, this.doc.frontiers(), changed ? origin : this.origin(this.version));
888
- return changed;
889
- }
890
- applied(changed, others = null) {
891
- return { version: this.version, changed, others };
892
- }
893
- free() {
894
- this.body.free();
895
- this.doc.free();
896
- }
897
- }
898
- // packages/doc/src/disk.ts
899
- import { Effect, FileSystem, Option, Path, PlatformError } from "effect";
900
- import { SecureRandom } from "effect/crypto";
901
- function of(info) {
902
- return {
903
- mtimeMs: Option.match(info.mtime, { onNone: () => 0, onSome: (date) => date.getTime() }),
904
- size: Number(info.size),
905
- ino: Number(Option.getOrElse(info.ino, () => 0n))
906
- };
907
- }
908
- function isNotFound(error) {
909
- return PlatformError.isPlatformError(error) && error.reason._tag === "NotFound";
910
- }
911
- function ioError(path, error) {
912
- if (error instanceof DocError)
913
- return error;
914
- return new DocError({
915
- code: isNotFound(error) ? "not-found" : "invalid",
916
- message: `Could not access ${path}: ${error instanceof Error ? error.message : String(error)}`
917
- });
918
- }
919
- function checkBytes(size) {
920
- return size > MAX_BYTES ? Effect.fail(new DocError({ code: "too-large", message: `The file is ${size} bytes, over the limit of ${MAX_BYTES}` })) : Effect.void;
921
- }
922
- var notRegular = (path) => new DocError({ code: "invalid", message: `${path} is not a regular file` });
923
- var stamp = Effect.fn("stamp")(function* (path) {
924
- const fs = yield* FileSystem.FileSystem;
925
- return yield* fs.stat(path).pipe(Effect.map((info) => of(info)), Effect.catchIf(isNotFound, () => Effect.succeed(null)), Effect.mapError((error) => ioError(path, error)));
926
- });
927
- var readFile = Effect.fn("readFile")(function* (path) {
928
- const fs = yield* FileSystem.FileSystem;
929
- if ((yield* fs.stat(path)).type !== "File")
930
- return yield* Effect.fail(notRegular(path));
931
- return yield* Effect.scoped(Effect.gen(function* () {
932
- const file = yield* fs.open(path, { flag: "r" });
933
- const info = yield* file.stat;
934
- if (info.type !== "File")
935
- return yield* Effect.fail(notRegular(path));
936
- yield* checkBytes(Number(info.size));
937
- const bytes = new Uint8Array(MAX_BYTES + 1);
938
- let size = 0;
939
- while (size < bytes.length) {
940
- const read = yield* file.read(bytes.subarray(size));
941
- if (!read)
942
- break;
943
- size += read;
944
- }
945
- yield* checkBytes(size);
946
- let raw;
947
- try {
948
- raw = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes.subarray(0, size));
949
- } catch {
950
- return yield* Effect.fail(new DocError({ code: "invalid", message: `${path} is not valid UTF-8 text` }));
951
- }
952
- const bom = raw.startsWith("\uFEFF");
953
- if (bom)
954
- raw = raw.slice(1);
955
- const firstBreak = raw.indexOf(`
956
- `);
957
- const crlf = firstBreak > 0 && raw[firstBreak - 1] === "\r";
958
- return { text: raw.replace(/\r\n/g, `
959
- `), stamp: of(info), format: { bom, crlf } };
960
- }));
961
- }, (effect, path) => Effect.mapError(effect, (error) => ioError(path, error)));
962
- var writeAtomic = Effect.fn("writeAtomic")(function* (path, text, format) {
963
- const fs = yield* FileSystem.FileSystem;
964
- const paths = yield* Path.Path;
965
- yield* Effect.try({ try: () => checkText(text), catch: (error) => error });
966
- const encoded = new TextEncoder().encode((format.bom ? "\uFEFF" : "") + (format.crlf ? text.replace(/\n/g, `\r
967
- `) : text));
968
- yield* checkBytes(encoded.length);
969
- const temporary = paths.join(paths.dirname(path), `.${paths.basename(path)}.do-it-${SecureRandom.uuidV4Unsafe()}.tmp`);
970
- const mode = yield* fs.stat(path).pipe(Effect.map((info) => info.mode & 4095), Effect.catchIf(isNotFound, () => Effect.succeed(undefined)));
971
- let created = false;
972
- const write = Effect.scoped(Effect.gen(function* () {
973
- const file = yield* fs.open(temporary, { flag: "wx", mode: mode ?? 438 });
974
- created = true;
975
- yield* file.writeAll(encoded);
976
- if (mode !== undefined)
977
- yield* fs.chmod(temporary, mode);
978
- yield* file.sync;
979
- return of(yield* file.stat);
980
- }));
981
- return yield* write.pipe(Effect.tap(() => fs.rename(temporary, path)), Effect.onError(() => created ? Effect.ignore(fs.remove(temporary)) : Effect.void));
982
- }, (effect, path) => Effect.mapError(effect, (error) => ioError(path, error)));
983
- // packages/protocol/src/schema.ts
984
- import { Schema } from "effect";
985
- var MAX_TEXT_LENGTH = 2 * 1024 * 1024;
986
- var MAX_TASK_TEXT_LENGTH = 4096;
987
- var Text = Schema.String.pipe(Schema.check(Schema.isMaxLength(MAX_TEXT_LENGTH)));
988
- var TaskText = Schema.String.pipe(Schema.check(Schema.isMaxLength(MAX_TASK_TEXT_LENGTH)));
989
- var Path2 = Schema.String.pipe(Schema.check(Schema.makeFilter((value) => value.length > 0 && value.startsWith("/") ? undefined : "an absolute, non-empty path is required"), Schema.isMaxLength(MAX_TEXT_LENGTH)));
990
- var Version = Schema.Int.pipe(Schema.check(Schema.isGreaterThan(0)));
991
- var User = Schema.TaggedStruct("User", {});
992
- var Agent = Schema.TaggedStruct("Agent", {
993
- name: Schema.String.pipe(Schema.check(Schema.isBetweenLength(1, 64), Schema.isPattern(/^[A-Za-z0-9._-]+$/)))
994
- });
995
- var File = Schema.TaggedStruct("File", {});
996
- var Author = Schema.Union([User, Agent, File]);
997
- var TaskState = Schema.Literals(["open", "done", "cancelled"]);
998
- var Todo = Schema.Struct({
999
- number: Schema.Number,
1000
- line: Schema.Number,
1001
- state: TaskState,
1002
- text: TaskText,
1003
- indent: Schema.Number,
1004
- section: Schema.NullOr(Text)
1005
- });
1006
- var TodoChange = Schema.Struct({
1007
- kind: Schema.Literals([
1008
- "added",
1009
- "removed",
1010
- "checked",
1011
- "unchecked",
1012
- "cancelled",
1013
- "uncancelled",
1014
- "edited"
1015
- ]),
1016
- number: Schema.Number,
1017
- text: TaskText,
1018
- line: Schema.Number,
1019
- oldLine: Schema.Number
1020
- });
1021
- var Section = Schema.Struct({
1022
- id: Text,
1023
- title: Text,
1024
- level: Schema.Literals([1, 2, 3]),
1025
- line: Schema.Number
1026
- });
1027
- var Others = Schema.Struct({
1028
- since: Version,
1029
- diff: Schema.NullOr(Text)
1030
- });
1031
- var Replace = Schema.TaggedStruct("Replace", {
1032
- old: Text,
1033
- new: Text
1034
- });
1035
- var Append = Schema.TaggedStruct("Append", {
1036
- text: Text
1037
- });
1038
- var TodoAdd = Schema.TaggedStruct("TodoAdd", {
1039
- text: TaskText
1040
- });
1041
- var TodoSet = Schema.TaggedStruct("TodoSet", {
1042
- target: Schema.Union([Schema.Number, Text]),
1043
- state: TaskState
1044
- });
1045
- var TodoStart = Schema.TaggedStruct("TodoStart", {
1046
- target: Schema.Union([Schema.Number, Text])
1047
- });
1048
- var EditOp = Schema.Union([Replace, Append, TodoAdd, TodoSet, TodoStart]);
1049
- var SectionId = Schema.optional(Text);
1050
- var StatusPayload = Schema.Struct({});
1051
- var StatusDocument = Schema.Struct({
1052
- path: Path2,
1053
- version: Version,
1054
- watchers: Schema.Number
1055
- });
1056
- var StatusSuccess = Schema.Struct({
1057
- pid: Schema.Number,
1058
- appVersion: Text,
1059
- port: Schema.Number,
1060
- startedAt: Schema.Number,
1061
- documents: Schema.Array(StatusDocument)
1062
- });
1063
- var StopPayload = Schema.Struct({});
1064
- var StopSuccess = Schema.Struct({});
1065
- var OpenPayload = Schema.Struct({
1066
- path: Path2,
1067
- create: Schema.Boolean
1068
- });
1069
- var OpenSuccess = Schema.Struct({
1070
- path: Path2,
1071
- version: Version,
1072
- url: Text,
1073
- created: Schema.Boolean
1074
- });
1075
- var ReadPayload = Schema.Struct({
1076
- path: Path2,
1077
- section: SectionId
1078
- });
1079
- var ReadSuccess = Schema.Struct({
1080
- version: Version,
1081
- text: Text
1082
- });
1083
- var SectionsPayload = Schema.Struct({ path: Path2 });
1084
- var SectionsSuccess = Schema.Struct({
1085
- version: Version,
1086
- sections: Schema.Array(Section)
1087
- });
1088
- var TodosPayload = Schema.Struct({
1089
- path: Path2,
1090
- section: SectionId
1091
- });
1092
- var TodosSuccess = Schema.Struct({
1093
- version: Version,
1094
- todos: Schema.Array(Todo)
1095
- });
1096
- var EditPayload = Schema.Struct({
1097
- path: Path2,
1098
- author: Author,
1099
- version: Version,
1100
- section: SectionId,
1101
- op: EditOp
1102
- });
1103
- var EditSuccess = Schema.Struct({
1104
- version: Version,
1105
- changed: Schema.Boolean,
1106
- section: Schema.NullOr(Text),
1107
- others: Schema.NullOr(Others)
1108
- });
1109
- var SyncPayload = Schema.Struct({
1110
- path: Path2,
1111
- base: Version,
1112
- text: Text
1113
- });
1114
- var SyncSuccess = Schema.Struct({
1115
- version: Version,
1116
- text: Text
1117
- });
1118
- var DiffPayload = Schema.Struct({
1119
- path: Path2,
1120
- since: Version,
1121
- wait: Schema.Number.pipe(Schema.check(Schema.isBetween({ minimum: 0, maximum: 600 })))
1122
- });
1123
- var DiffSuccess = Schema.Struct({
1124
- from: Version,
1125
- to: Version,
1126
- by: Schema.Array(Author),
1127
- diff: Schema.NullOr(Text),
1128
- todos: Schema.Array(TodoChange)
1129
- });
1130
- var WatchPayload = Schema.Struct({
1131
- path: Path2,
1132
- since: Schema.optional(Version),
1133
- section: SectionId,
1134
- text: Schema.Boolean
1135
- });
1136
- var Watching = Schema.TaggedStruct("Watching", {
1137
- version: Version,
1138
- text: Schema.optionalKey(Text)
1139
- });
1140
- var Change = Schema.TaggedStruct("Change", {
1141
- from: Version,
1142
- to: Version,
1143
- by: Schema.Array(Author),
1144
- diff: Schema.NullOr(Text),
1145
- todos: Schema.Array(TodoChange),
1146
- text: Schema.optionalKey(Text)
1147
- });
1148
- var Resync = Schema.TaggedStruct("Resync", {
1149
- version: Version,
1150
- text: Schema.optionalKey(Text)
1151
- });
1152
- var Deleted = Schema.TaggedStruct("Deleted", {});
1153
- var WatchEvent = Schema.Union([Watching, Change, Resync, Deleted]);
1154
- // packages/protocol/src/errors.ts
1155
- import { Schema as Schema2 } from "effect";
1156
- class VersionMismatch extends Schema2.TaggedError()("VersionMismatch", {
1157
- latest: Version,
1158
- since: Version,
1159
- diff: Schema2.NullOr(Text),
1160
- forgotten: Schema2.Boolean
1161
- }) {
1162
- }
1163
- var RefusedCode = Schema2.Literals(["not-found", "ambiguous", "too-large", "invalid"]);
1164
-
1165
- class Refused extends Schema2.TaggedError()("Refused", {
1166
- code: RefusedCode,
1167
- message: Text
1168
- }) {
1169
- }
1170
-
1171
- class Forgotten extends Schema2.TaggedError()("Forgotten", {
1172
- latest: Version
1173
- }) {
1174
- }
1175
-
1176
- class NothingYet extends Schema2.TaggedError()("NothingYet", {}) {
1177
- }
1178
-
1179
- class DocumentGone extends Schema2.TaggedError()("DocumentGone", {
1180
- path: Path2
1181
- }) {
1182
- }
1183
-
1184
- class Unauthorized extends Schema2.TaggedError()("Unauthorized", {}) {
1185
- }
1186
- var RpcFailure = Schema2.Union([
1187
- VersionMismatch,
1188
- Refused,
1189
- Forgotten,
1190
- NothingYet,
1191
- DocumentGone,
1192
- Unauthorized
1193
- ]);
1194
- // packages/protocol/src/rpcs.ts
1195
- import { Rpc, RpcGroup } from "effect/rpc";
1196
- var statusRpc = Rpc.make("status", {
1197
- payload: StatusPayload,
1198
- success: StatusSuccess,
1199
- error: RpcFailure
1200
- });
1201
- var stopRpc = Rpc.make("stop", {
1202
- payload: StopPayload,
1203
- success: StopSuccess,
1204
- error: RpcFailure
1205
- });
1206
- var openRpc = Rpc.make("open", {
1207
- payload: OpenPayload,
1208
- success: OpenSuccess,
1209
- error: RpcFailure
1210
- });
1211
- var readRpc = Rpc.make("read", {
1212
- payload: ReadPayload,
1213
- success: ReadSuccess,
1214
- error: RpcFailure
1215
- });
1216
- var sectionsRpc = Rpc.make("sections", {
1217
- payload: SectionsPayload,
1218
- success: SectionsSuccess,
1219
- error: RpcFailure
1220
- });
1221
- var todosRpc = Rpc.make("todos", {
1222
- payload: TodosPayload,
1223
- success: TodosSuccess,
1224
- error: RpcFailure
1225
- });
1226
- var editRpc = Rpc.make("edit", {
1227
- payload: EditPayload,
1228
- success: EditSuccess,
1229
- error: RpcFailure
1230
- });
1231
- var syncRpc = Rpc.make("sync", {
1232
- payload: SyncPayload,
1233
- success: SyncSuccess,
1234
- error: RpcFailure
1235
- });
1236
- var diffRpc = Rpc.make("diff", {
1237
- payload: DiffPayload,
1238
- success: DiffSuccess,
1239
- error: RpcFailure
1240
- });
1241
- var watchRpc = Rpc.make("watch", {
1242
- payload: WatchPayload,
1243
- success: WatchEvent,
1244
- error: RpcFailure,
1245
- stream: true
1246
- });
1247
- var DoItRpcs = RpcGroup.make(statusRpc, stopRpc, openRpc, readRpc, sectionsRpc, todosRpc, editRpc, syncRpc, diffRpc, watchRpc);
1248
- // packages/protocol/src/index.ts
1249
- var APP_VERSION = "0.1.1";
1250
-
1251
- // packages/server/src/http.ts
1252
- import { BunHttpServer, BunHttpServerRequest } from "@effect/platform-bun";
1253
- import { Deferred as Deferred4, Effect as Effect11, FileSystem as FileSystem9, Layer as Layer2, Path as Path10, Schedule } from "effect";
1254
- import { HttpEffect, HttpRouter, HttpServerRequest as Request, HttpServerResponse as Response2 } from "effect/http";
1255
- import { RpcSerialization, RpcServer } from "effect/rpc";
1256
-
1257
- // packages/server/src/auth.ts
1258
- import { Data as Data2, Effect as Effect2, FileSystem as FileSystem2, Path as Path3 } from "effect";
1259
- import { Mac, SecureRandom as SecureRandom2 } from "effect/crypto";
1260
- import { Hex } from "effect/encoding";
1261
- var encoder2 = new TextEncoder;
1262
- var CODE_LIFETIME = 5 * 60 * 1000;
1263
-
1264
- class InvalidToken extends Data2.TaggedError("InvalidToken") {
1265
- }
1266
- function equal(a, b) {
1267
- const left = encoder2.encode(a);
1268
- const right = encoder2.encode(b);
1269
- return left.length === right.length && Mac.timingSafeEqual(left, right);
1270
- }
1271
- function alreadyExists(error) {
1272
- return error.reason._tag === "AlreadyExists";
1273
- }
1274
-
1275
- class Auth {
1276
- token;
1277
- cookie;
1278
- random;
1279
- codes = new Map;
1280
- constructor(token, cookie, random) {
1281
- this.token = token;
1282
- this.cookie = cookie;
1283
- this.random = random;
1284
- }
1285
- static make = Effect2.fn("Auth.make")(function* (home) {
1286
- const fs = yield* FileSystem2.FileSystem;
1287
- const path = yield* Path3.Path;
1288
- const random = yield* SecureRandom2.SecureRandom;
1289
- const mac = yield* Mac.Mac;
1290
- const file = path.join(home, "token");
1291
- const fresh = Hex.encode(yield* random.randomBytes(32));
1292
- yield* fs.writeFileString(file, fresh, { flag: "wx", mode: 384 }).pipe(Effect2.catchIf(alreadyExists, () => Effect2.void));
1293
- const invalid = new InvalidToken({ message: "The daemon token file is invalid" });
1294
- if (Number((yield* fs.stat(file)).size) > 128)
1295
- return yield* Effect2.fail(invalid);
1296
- const token = (yield* fs.readFileString(file)).trim();
1297
- if (!/^[a-f0-9]{64}$/.test(token))
1298
- return yield* Effect2.fail(invalid);
1299
- yield* fs.chmod(file, 384);
1300
- const key = yield* Mac.key(Mac.hmacSha256, encoder2.encode(token));
1301
- const cookie = Hex.encode(yield* mac.sign(key, encoder2.encode("web cookie")));
1302
- return new Auth(token, cookie, random);
1303
- });
1304
- authorized(authorization, cookie) {
1305
- return authorization !== undefined && equal(authorization, `Bearer ${this.token}`) || cookie !== undefined && equal(cookie, this.cookie);
1306
- }
1307
- mint(path, now = Date.now()) {
1308
- return Effect2.gen({ self: this }, function* () {
1309
- for (const [code, entry] of this.codes)
1310
- if (entry.until <= now)
1311
- this.codes.delete(code);
1312
- if (this.codes.size >= 64) {
1313
- return yield* Effect2.fail(new Refused({ code: "too-large", message: "Too many unused browser sign-in codes, use an existing link or wait five minutes" }));
1314
- }
1315
- const code = Hex.encode(yield* this.random.randomBytes(32));
1316
- this.codes.set(code, { path, until: now + CODE_LIFETIME });
1317
- return code;
1318
- });
1319
- }
1320
- consume(code, path, now = Date.now()) {
1321
- const entry = this.codes.get(code);
1322
- if (!entry)
1323
- return false;
1324
- if (entry.until <= now) {
1325
- this.codes.delete(code);
1326
- return false;
1327
- }
1328
- if (entry.path !== path)
1329
- return false;
1330
- this.codes.delete(code);
1331
- return true;
1332
- }
1333
- }
1334
-
1335
- // packages/server/src/config.ts
1336
- import { Config, Data as Data3, Effect as Effect3, Option as Option2, Path as Path4 } from "effect";
1337
- var MAX_BODY = 4 * 1024 * 1024;
1338
- var TEMPLATE = `# TODO
1339
-
1340
- Add one task per line. Agents pick up new lines, plan them, and write their status underneath.
1341
-
1342
- ## Tasks
1343
-
1344
- - [ ]
1345
- `;
1346
-
1347
- class InvalidConfig extends Data3.TaggedError("InvalidConfig") {
1348
- }
1349
- var webRoot = new URL("../web", import.meta.url);
1350
- var optional = (name) => Config.option(Config.String(name)).pipe(Effect3.map(Option2.getOrUndefined));
1351
- var config = Effect3.gen(function* () {
1352
- const path = yield* Path4.Path;
1353
- const port = Number((yield* optional("DO_IT_PORT")) ?? 47100);
1354
- const idleMs = Number((yield* optional("DO_IT_IDLE_MS")) ?? 30 * 60 * 1000);
1355
- if (!Number.isInteger(port) || port < 0 || port > 65535)
1356
- return yield* Effect3.fail(new InvalidConfig({ message: "DO_IT_PORT must be between 0 and 65535" }));
1357
- if (!Number.isFinite(idleMs) || idleMs <= 0)
1358
- return yield* Effect3.fail(new InvalidConfig({ message: "DO_IT_IDLE_MS must be positive" }));
1359
- const home = (yield* optional("DO_IT_HOME")) ?? path.join(yield* Config.String("HOME"), ".do-it");
1360
- const webDist = (yield* optional("DO_IT_WEB_DIST")) ?? (yield* path.fromFileUrl(webRoot));
1361
- return { home: path.resolve(home), port, idleMs, webDist: path.resolve(webDist) };
1362
- });
1363
-
1364
- // packages/server/src/documents.ts
1365
- import { Cause, Context, Deferred as Deferred2, Effect as Effect5, Exit, Fiber as Fiber2, FileSystem as FileSystem4, Layer, Path as Path6, Scope, Semaphore } from "effect";
1366
-
1367
- // packages/server/src/watcher.ts
1368
- import { Deferred, Effect as Effect4, Fiber, FileSystem as FileSystem3, Path as Path5, Stream } from "effect";
1369
- var watchFile = Effect4.fn("watchFile")(function* (path, changed) {
1370
- const fs = yield* FileSystem3.FileSystem;
1371
- const paths = yield* Path5.Path;
1372
- const name = paths.basename(path);
1373
- const ready = yield* Deferred.make();
1374
- const events = yield* fs.watch(paths.dirname(path), { onReady: () => Deferred.doneUnsafe(ready, Effect4.void) }).pipe(Stream.filter((event) => paths.basename(event.path) === name), Stream.debounce("50 millis"), Stream.runForEach(() => changed), Effect4.ignore, Effect4.forkScoped);
1375
- yield* changed.pipe(Effect4.delay("2 seconds"), Effect4.forever, Effect4.forkScoped);
1376
- yield* Effect4.race(Deferred.await(ready), Fiber.await(events));
1377
- });
1378
-
1379
- // packages/server/src/documents.ts
1380
- var MAX_OPEN = 64;
1381
- var WRITE_DEBOUNCE = "25 millis";
1382
- var MISSING_GRACE = "500 millis";
1383
- function rpcError(error, latest = 1) {
1384
- if (error instanceof DocError) {
1385
- if (error.code === "conflict" && error.conflict) {
1386
- const c = error.conflict;
1387
- return new VersionMismatch({ latest: c.current, since: c.expected, diff: c.diff, forgotten: c.forgotten });
1388
- }
1389
- if (error.code === "forgotten")
1390
- return new Forgotten({ latest });
1391
- return new Refused({ code: error.code === "conflict" ? "invalid" : error.code, message: error.message });
1392
- }
1393
- if (error instanceof Refused || error instanceof DocumentGone || error instanceof Forgotten || error instanceof VersionMismatch)
1394
- return error;
1395
- return new Refused({ code: "invalid", message: error instanceof Error ? error.message : String(error) });
1396
- }
1397
- function rpcTry(f, latest) {
1398
- return Effect5.try({ try: f, catch: (error) => rpcError(error, latest?.()) });
1399
- }
1400
- function same(a, b) {
1401
- return a.mtimeMs === b.mtimeMs && a.ino === b.ino && a.size === b.size;
1402
- }
1403
-
1404
- class DocumentRegistry {
1405
- versions;
1406
- log;
1407
- context;
1408
- scope;
1409
- entries = new Map;
1410
- opening = Semaphore.makeUnsafe(1);
1411
- totalWatchers = 0;
1412
- closing = false;
1413
- constructor(versions, log, context, scope) {
1414
- this.versions = versions;
1415
- this.log = log;
1416
- this.context = context;
1417
- this.scope = scope;
1418
- }
1419
- static make = Effect5.fn("DocumentRegistry.make")(function* (versions, log) {
1420
- return new DocumentRegistry(versions, log, yield* Effect5.context(), yield* Effect5.scope);
1421
- });
1422
- disk(effect) {
1423
- return Effect5.provideContext(effect, this.context);
1424
- }
1425
- logged(message, effect) {
1426
- return effect.pipe(Effect5.catchCause((cause) => Cause.hasInterruptsOnly(cause) ? Effect5.failCause(cause) : this.log.write(`${message}: ${String(Cause.squash(cause))}`)));
1427
- }
1428
- validate(input) {
1429
- return this.disk(Effect5.gen({ self: this }, function* () {
1430
- const paths = yield* Path6.Path;
1431
- const fs = yield* FileSystem4.FileSystem;
1432
- if (!paths.isAbsolute(input) || ![".md", ".markdown"].includes(paths.extname(input).toLowerCase())) {
1433
- return yield* Effect5.fail(new Refused({ code: "invalid", message: "The path must be absolute and end in .md or .markdown" }));
1434
- }
1435
- const path = paths.resolve(input);
1436
- const directory = yield* fs.stat(paths.dirname(path)).pipe(Effect5.map((info) => info.type === "Directory"), Effect5.catch(() => Effect5.succeed(false)));
1437
- if (!directory) {
1438
- if (this.entries.has(path) || this.versions.has(path))
1439
- return yield* Effect5.fail(new DocumentGone({ path }));
1440
- return yield* Effect5.fail(new Refused({ code: "invalid", message: "The document's parent directory must exist" }));
1441
- }
1442
- return path;
1443
- }));
1444
- }
1445
- open(input, create = false) {
1446
- return this.acquire(input, create, false);
1447
- }
1448
- get(path) {
1449
- return Effect5.map(this.open(path), ({ entry }) => entry);
1450
- }
1451
- withEntry(path, use, create = false) {
1452
- return this.withOpened(path, ({ entry }) => use(entry), create);
1453
- }
1454
- withOpened(path, use, create = false) {
1455
- return Effect5.acquireUseRelease(this.acquire(path, create, true), use, ({ entry }) => Effect5.sync(() => {
1456
- entry.waiters--;
1457
- entry.lastUsed = Date.now();
1458
- }));
1459
- }
1460
- acquire(input, create, pin) {
1461
- return Effect5.gen({ self: this }, function* () {
1462
- const path = yield* this.validate(input);
1463
- while (true) {
1464
- if (this.closing)
1465
- return yield* Effect5.fail(new Refused({ code: "invalid", message: "The daemon is shutting down" }));
1466
- const existing = this.entries.get(path);
1467
- const opened = existing ? yield* this.reuse(existing) : yield* this.opening.withPermit(Effect5.suspend(() => {
1468
- const raced = this.entries.get(path);
1469
- return raced ? this.reuse(raced) : this.create(path, create);
1470
- }));
1471
- if (opened.entry.closed)
1472
- continue;
1473
- if (pin)
1474
- opened.entry.waiters++;
1475
- return opened;
1476
- }
1477
- }).pipe(Effect5.mapError((error) => rpcError(error)));
1478
- }
1479
- status() {
1480
- return [...this.entries.values()].map((entry) => ({ path: entry.path, version: entry.doc.version, watchers: entry.watchers }));
1481
- }
1482
- reuse(entry) {
1483
- entry.lastUsed = Date.now();
1484
- return this.disk(stamp(entry.path)).pipe(Effect5.flatMap((current) => entry.deleted || current === null ? Effect5.fail(new DocumentGone({ path: entry.path })) : Effect5.succeed({ entry, created: false })));
1485
- }
1486
- create(path, create) {
1487
- const prepare = Effect5.gen({ self: this }, function* () {
1488
- const missing = (yield* this.disk(stamp(path))) === null;
1489
- if (missing && !create) {
1490
- if (this.versions.has(path))
1491
- return yield* Effect5.fail(new DocumentGone({ path }));
1492
- return yield* Effect5.fail(new Refused({ code: "invalid", message: "The document does not exist, open it with create enabled" }));
1493
- }
1494
- yield* this.makeRoom();
1495
- if (missing)
1496
- yield* this.disk(writeAtomic(path, TEMPLATE, { bom: false, crlf: false }));
1497
- return { missing, file: yield* this.disk(readFile(path)) };
1498
- });
1499
- return Effect5.uninterruptibleMask((restore) => Effect5.gen({ self: this }, function* () {
1500
- const { missing, file } = yield* restore(prepare);
1501
- const doc = yield* this.versions.open(path, file.text);
1502
- const entry = {
1503
- path,
1504
- doc,
1505
- signal: Deferred2.makeUnsafe(),
1506
- format: file.format,
1507
- stamp: file.stamp,
1508
- watchers: 0,
1509
- waiters: 0,
1510
- lastUsed: Date.now(),
1511
- deleted: false,
1512
- closed: false,
1513
- lock: Semaphore.makeUnsafe(1),
1514
- scope: yield* Scope.fork(this.scope),
1515
- writing: undefined,
1516
- missing: undefined
1517
- };
1518
- this.entries.set(path, entry);
1519
- yield* this.disk(watchFile(path, this.poll(entry))).pipe(Scope.provide(entry.scope));
1520
- return { entry, created: missing };
1521
- }));
1522
- }
1523
- makeRoom() {
1524
- return Effect5.gen({ self: this }, function* () {
1525
- if (this.entries.size < MAX_OPEN)
1526
- return;
1527
- const idle = [...this.entries.values()].filter((entry) => entry.watchers === 0 && entry.waiters === 0).sort((a, b) => a.lastUsed - b.lastUsed);
1528
- for (const entry of idle) {
1529
- yield* this.flush(entry);
1530
- if (yield* this.evict(entry))
1531
- return;
1532
- }
1533
- return yield* Effect5.fail(new Refused({ code: "too-large", message: "At most 64 documents may be open, all available documents are busy" }));
1534
- });
1535
- }
1536
- notify(entry) {
1537
- const signal = entry.signal;
1538
- entry.signal = Deferred2.makeUnsafe();
1539
- Deferred2.doneUnsafe(signal, Effect5.void);
1540
- }
1541
- changed(entry, previous) {
1542
- if (entry.doc.version === previous)
1543
- return Effect5.void;
1544
- entry.lastUsed = Date.now();
1545
- this.notify(entry);
1546
- return Effect5.andThen(this.versions.record(entry.path, entry.doc.version, entry.doc.text), this.scheduleWrite(entry));
1547
- }
1548
- subscribe(entry) {
1549
- if (entry.deleted)
1550
- throw new DocumentGone({ path: entry.path });
1551
- if (entry.watchers >= 32 || this.totalWatchers >= 256) {
1552
- throw new Refused({ code: "too-large", message: "The document or daemon watch stream limit has been reached" });
1553
- }
1554
- entry.watchers++;
1555
- this.totalWatchers++;
1556
- return () => {
1557
- entry.watchers--;
1558
- this.totalWatchers--;
1559
- entry.lastUsed = Date.now();
1560
- };
1561
- }
1562
- scheduleWrite(entry) {
1563
- return Effect5.suspend(() => {
1564
- if (entry.writing || this.closing || entry.deleted || entry.closed || !entry.doc.dirty)
1565
- return Effect5.void;
1566
- const write = Effect5.suspend(() => {
1567
- entry.writing = undefined;
1568
- return this.flushNow(entry);
1569
- });
1570
- return Effect5.forkIn(Effect5.delay(write, WRITE_DEBOUNCE), this.scope).pipe(Effect5.map((fiber) => {
1571
- entry.writing = fiber;
1572
- }));
1573
- });
1574
- }
1575
- poll(entry) {
1576
- return entry.lock.withPermit(this.pollLocked(entry));
1577
- }
1578
- pollLocked(entry) {
1579
- return this.logged(`Could not merge ${entry.path}`, Effect5.gen({ self: this }, function* () {
1580
- if (entry.deleted || entry.closed || this.closing)
1581
- return;
1582
- const current = yield* this.disk(stamp(entry.path));
1583
- if (!current) {
1584
- if (!entry.missing)
1585
- entry.missing = yield* Effect5.forkIn(this.recheck(entry), this.scope);
1586
- return;
1587
- }
1588
- if (entry.missing) {
1589
- const missing = entry.missing;
1590
- entry.missing = undefined;
1591
- yield* Fiber2.interrupt(missing);
1592
- }
1593
- if (!same(entry.stamp, current)) {
1594
- const file = yield* this.disk(readFile(entry.path));
1595
- const previous = entry.doc.version;
1596
- entry.doc.mergeFile(file.text);
1597
- entry.format = file.format;
1598
- entry.stamp = file.stamp;
1599
- yield* this.changed(entry, previous);
1600
- yield* this.versions.record(entry.path, entry.doc.version, entry.doc.text);
1601
- yield* this.compact(entry);
1602
- }
1603
- yield* this.scheduleWrite(entry);
1604
- }).pipe(Effect5.uninterruptible));
1605
- }
1606
- recheck(entry) {
1607
- const check = Effect5.suspend(() => {
1608
- entry.missing = undefined;
1609
- return entry.lock.withPermit(Effect5.gen({ self: this }, function* () {
1610
- if (entry.deleted || entry.closed || this.closing)
1611
- return false;
1612
- if ((yield* this.disk(stamp(entry.path))) === null)
1613
- return true;
1614
- yield* this.pollLocked(entry);
1615
- return false;
1616
- })).pipe(Effect5.flatMap((gone) => gone ? this.deleted(entry) : Effect5.void));
1617
- });
1618
- return this.logged(`Could not check ${entry.path}`, Effect5.delay(check, MISSING_GRACE));
1619
- }
1620
- flush(entry) {
1621
- return Effect5.suspend(() => {
1622
- const timer = entry.writing;
1623
- entry.writing = undefined;
1624
- return Effect5.andThen(timer ? Fiber2.interrupt(timer) : Effect5.void, this.flushNow(entry));
1625
- });
1626
- }
1627
- flushNow(entry) {
1628
- return entry.lock.withPermit(this.flushLocked(entry));
1629
- }
1630
- flushLocked(entry) {
1631
- let ticket;
1632
- const write = Effect5.gen({ self: this }, function* () {
1633
- if (entry.deleted || entry.closed)
1634
- return;
1635
- const current = yield* this.disk(stamp(entry.path));
1636
- if (!current) {
1637
- if (!this.closing)
1638
- yield* this.pollLocked(entry);
1639
- return;
1640
- }
1641
- if (!same(entry.stamp, current)) {
1642
- const file = yield* this.disk(readFile(entry.path));
1643
- const previous = entry.doc.version;
1644
- entry.doc.mergeFile(file.text);
1645
- entry.format = file.format;
1646
- entry.stamp = file.stamp;
1647
- if (entry.doc.version !== previous)
1648
- this.notify(entry);
1649
- }
1650
- if (entry.doc.dirty) {
1651
- ticket = entry.doc.toWrite();
1652
- entry.stamp = yield* this.disk(writeAtomic(entry.path, ticket.text, entry.format));
1653
- entry.doc.wrote(ticket);
1654
- ticket = undefined;
1655
- }
1656
- yield* this.versions.record(entry.path, entry.doc.version, entry.doc.text);
1657
- yield* this.compact(entry);
1658
- }).pipe(Effect5.onError(() => Effect5.sync(() => {
1659
- if (ticket)
1660
- entry.doc.notWritten(ticket);
1661
- })));
1662
- return this.logged(`Could not write ${entry.path}`, Effect5.uninterruptible(write));
1663
- }
1664
- compact(entry) {
1665
- const compacted = entry.doc.compact();
1666
- return compacted ? this.log.write(`Compacted ${entry.path} at version ${entry.doc.version} (${compacted.reason}: ${compacted.ops} ops, ${compacted.peers} peers), kept versions ${compacted.oldest}-${entry.doc.version}`) : Effect5.void;
1667
- }
1668
- deleted(entry) {
1669
- return Effect5.suspend(() => {
1670
- entry.deleted = true;
1671
- this.notify(entry);
1672
- return this.closeEntry(entry);
1673
- });
1674
- }
1675
- evict(entry) {
1676
- return entry.lock.withPermit(Effect5.suspend(() => {
1677
- if (entry.closed)
1678
- return Effect5.succeed(true);
1679
- if (entry.watchers > 0 || entry.waiters > 0 || !entry.deleted && entry.doc.dirty)
1680
- return Effect5.succeed(false);
1681
- return Effect5.as(this.closeLocked(entry), true);
1682
- }));
1683
- }
1684
- closeEntry(entry) {
1685
- return entry.lock.withPermit(this.closeLocked(entry));
1686
- }
1687
- closeLocked(entry) {
1688
- return Effect5.suspend(() => {
1689
- if (entry.closed)
1690
- return Effect5.void;
1691
- entry.closed = true;
1692
- const timers = [entry.writing, entry.missing].filter((fiber) => fiber !== undefined);
1693
- entry.writing = undefined;
1694
- entry.missing = undefined;
1695
- if (this.entries.get(entry.path) === entry) {
1696
- this.entries.delete(entry.path);
1697
- entry.doc.free();
1698
- }
1699
- return Effect5.andThen(Fiber2.interruptAll(timers), Scope.close(entry.scope, Exit.void));
1700
- });
1701
- }
1702
- close() {
1703
- return Effect5.gen({ self: this }, function* () {
1704
- if (this.closing)
1705
- return;
1706
- this.closing = true;
1707
- for (const entry of [...this.entries.values()]) {
1708
- yield* this.flush(entry);
1709
- entry.deleted = true;
1710
- this.notify(entry);
1711
- yield* this.closeEntry(entry);
1712
- }
1713
- yield* this.versions.flush();
1714
- });
1715
- }
1716
- }
1717
-
1718
- class Documents extends Context.Service()("@do-it/server/Documents") {
1719
- }
1720
- function DocumentsLive(versions, log) {
1721
- return Layer.effect(Documents, Effect5.acquireRelease(DocumentRegistry.make(versions, log), (registry) => registry.close()));
1722
- }
1723
-
1724
- // packages/server/src/handlers.ts
1725
- import { Cause as Cause2, Deferred as Deferred3, Effect as Effect6, Stream as Stream2 } from "effect";
1726
- function author(origin) {
1727
- return origin.kind === "agent" ? { _tag: "Agent", name: origin.name } : origin.kind === "user" ? { _tag: "User" } : { _tag: "File" };
1728
- }
1729
- function origin(by) {
1730
- return by._tag === "Agent" ? { kind: "agent", name: by.name } : { kind: by._tag === "User" ? "user" : "file" };
1731
- }
1732
- function changes(entry, since, section) {
1733
- const result = entry.doc.changes(since, section);
1734
- return result ? { ...result, by: result.by.map(author) } : null;
1735
- }
1736
- function watchDocument(registry, request) {
1737
- return Stream2.fromPull(Effect6.gen(function* () {
1738
- const { entry, release } = yield* registry.withEntry(request.path, (entry) => Effect6.gen(function* () {
1739
- if (request.section !== undefined)
1740
- yield* rpcTry(() => findSection(entry.doc.text, request.section));
1741
- return { entry, release: yield* rpcTry(() => registry.subscribe(entry)) };
1742
- }));
1743
- yield* Effect6.addFinalizer(() => Effect6.sync(release));
1744
- let first = true;
1745
- let ended = false;
1746
- let since = request.since ?? entry.doc.version;
1747
- const pull = Effect6.gen(function* () {
1748
- if (ended)
1749
- return yield* Cause2.done();
1750
- while (true) {
1751
- const signal = entry.signal;
1752
- if (first) {
1753
- first = false;
1754
- const resync = request.since !== undefined && (since > entry.doc.version || since < entry.doc.version && entry.doc.textAt(since) === null);
1755
- if (request.since === undefined || resync)
1756
- since = entry.doc.version;
1757
- return [{
1758
- _tag: resync ? "Resync" : "Watching",
1759
- version: entry.doc.version,
1760
- ...request.text ? { text: entry.doc.text } : {}
1761
- }];
1762
- }
1763
- if (entry.deleted) {
1764
- ended = true;
1765
- return [{ _tag: "Deleted" }];
1766
- }
1767
- if (entry.doc.version > since) {
1768
- const result = changes(entry, since, request.section);
1769
- since = entry.doc.version;
1770
- const event = result ? { _tag: "Change", ...result, ...request.text ? { text: entry.doc.text } : {} } : { _tag: "Resync", version: since, ...request.text ? { text: entry.doc.text } : {} };
1771
- return [event];
1772
- }
1773
- yield* Deferred3.await(signal);
1774
- }
1775
- });
1776
- return pull;
1777
- }));
1778
- }
1779
- function HandlersLive(auth, runtime) {
1780
- return DoItRpcs.toLayer(Effect6.gen(function* () {
1781
- const registry = yield* Documents;
1782
- return {
1783
- status: Effect6.fn("rpc.status")(function* () {
1784
- return { pid: process.pid, appVersion: APP_VERSION, port: runtime.port, startedAt: runtime.startedAt, documents: registry.status() };
1785
- }),
1786
- stop: Effect6.fn("rpc.stop")(function* () {
1787
- yield* runtime.stop;
1788
- return {};
1789
- }),
1790
- open: (request) => registry.withOpened(request.path, ({ entry, created }) => Effect6.gen(function* () {
1791
- const version = entry.doc.version;
1792
- const code = yield* auth.mint(entry.path);
1793
- return {
1794
- path: entry.path,
1795
- version,
1796
- created,
1797
- url: `http://127.0.0.1:${runtime.port}/auth?code=${code}&path=${encodeURIComponent(entry.path)}`
1798
- };
1799
- }), request.create).pipe(Effect6.withSpan("rpc.open")),
1800
- read: (request) => registry.withEntry(request.path, (entry) => rpcTry(() => {
1801
- const scope = request.section === undefined ? null : findSection(entry.doc.text, request.section);
1802
- return { version: entry.doc.version, text: scope ? entry.doc.text.slice(scope.from, scope.to) : entry.doc.text };
1803
- })).pipe(Effect6.withSpan("rpc.read")),
1804
- sections: (request) => registry.withEntry(request.path, (entry) => Effect6.sync(() => ({
1805
- version: entry.doc.version,
1806
- sections: sections(entry.doc.text).map(({ id, title, level, line }) => ({ id, title, level, line }))
1807
- }))).pipe(Effect6.withSpan("rpc.sections")),
1808
- todos: (request) => registry.withEntry(request.path, (entry) => rpcTry(() => {
1809
- const all = todos(entry.doc.text);
1810
- if (request.section === undefined)
1811
- return { version: entry.doc.version, todos: all };
1812
- const scope = findSection(entry.doc.text, request.section);
1813
- const following = sections(entry.doc.text).find((section) => section.from === scope.to)?.line ?? Infinity;
1814
- return { version: entry.doc.version, todos: all.filter((todo) => todo.line >= scope.line && todo.line < following) };
1815
- })).pipe(Effect6.withSpan("rpc.todos")),
1816
- edit: (request) => registry.withEntry(request.path, (entry) => Effect6.gen(function* () {
1817
- const previous = entry.doc.version;
1818
- const result = yield* rpcTry(() => entry.doc.agentEdit(origin(request.author), { version: request.version, section: request.section }, (text) => {
1819
- const op = request.op;
1820
- switch (op._tag) {
1821
- case "Replace":
1822
- return replaceUnique(text, op.old, op.new, request.section);
1823
- case "Append":
1824
- return appendBlock(text, op.text, request.section);
1825
- case "TodoAdd":
1826
- return addTodo(text, op.text, request.section);
1827
- case "TodoSet":
1828
- return setTodoStateStamped(text, findTodo(text, op.target, request.section), op.state, new Date);
1829
- case "TodoStart":
1830
- return stampTodo(text, findTodo(text, op.target, request.section), "started", new Date);
1831
- }
1832
- }), () => entry.doc.version);
1833
- yield* registry.changed(entry, previous);
1834
- return { ...result, section: request.section ?? null };
1835
- })).pipe(Effect6.withSpan("rpc.edit")),
1836
- sync: (request) => registry.withEntry(request.path, (entry) => Effect6.gen(function* () {
1837
- const previous = entry.doc.version;
1838
- yield* rpcTry(() => entry.doc.userEdit(request.base, request.text), () => entry.doc.version);
1839
- const reply = { version: entry.doc.version, text: entry.doc.text };
1840
- yield* registry.changed(entry, previous);
1841
- return reply;
1842
- })).pipe(Effect6.withSpan("rpc.sync")),
1843
- diff: (request) => registry.withEntry(request.path, (entry) => Effect6.gen(function* () {
1844
- const deadline = Date.now() + request.wait * 1000;
1845
- while (true) {
1846
- const signal = entry.signal;
1847
- if (entry.deleted)
1848
- return yield* Effect6.fail(new DocumentGone({ path: entry.path }));
1849
- const result = changes(entry, request.since);
1850
- if (!result)
1851
- return yield* Effect6.fail(new Forgotten({ latest: entry.doc.version }));
1852
- if (entry.doc.version !== request.since)
1853
- return result;
1854
- const remaining = deadline - Date.now();
1855
- if (remaining <= 0)
1856
- return yield* Effect6.fail(new NothingYet({}));
1857
- yield* Deferred3.await(signal).pipe(Effect6.timeoutOption(remaining));
1858
- }
1859
- })).pipe(Effect6.withSpan("rpc.diff")),
1860
- watch: (request) => watchDocument(registry, request)
1861
- };
1862
- }));
1863
- }
1864
-
1865
- // packages/server/src/files.ts
1866
- import { Data as Data4, Effect as Effect7, FileSystem as FileSystem5 } from "effect";
1867
- import { SecureRandom as SecureRandom3 } from "effect/crypto";
1868
-
1869
- class StateFileError extends Data4.TaggedError("StateFileError") {
1870
- }
1871
- var readJson = Effect7.fn("readJson")(function* (path, maxBytes = 2 * 1024 * 1024) {
1872
- const fs = yield* FileSystem5.FileSystem;
1873
- const bytes = yield* Effect7.scoped(Effect7.gen(function* () {
1874
- const file = yield* fs.open(path, { flag: "r" });
1875
- const data = new Uint8Array(maxBytes + 1);
1876
- let size = 0;
1877
- while (size <= maxBytes) {
1878
- const count = yield* file.read(data.subarray(size));
1879
- if (count === 0)
1880
- return data.subarray(0, size);
1881
- size += count;
1882
- }
1883
- return yield* Effect7.fail(new StateFileError({ message: `State file exceeds ${maxBytes} bytes` }));
1884
- }));
1885
- return yield* Effect7.try({
1886
- try: () => JSON.parse(new TextDecoder().decode(bytes)),
1887
- catch: (error) => new StateFileError({ message: `State file is not JSON: ${String(error)}` })
1888
- });
1889
- });
1890
- var writeJson = Effect7.fn("writeJson")(function* (path, data) {
1891
- const fs = yield* FileSystem5.FileSystem;
1892
- const temporary = `${path}.${SecureRandom3.uuidV4Unsafe()}.tmp`;
1893
- let created = false;
1894
- yield* Effect7.scoped(Effect7.gen(function* () {
1895
- const file = yield* fs.open(temporary, { flag: "wx", mode: 384 });
1896
- created = true;
1897
- yield* file.writeAll(new TextEncoder().encode(JSON.stringify(data) + `
1898
- `));
1899
- yield* file.sync;
1900
- })).pipe(Effect7.andThen(fs.rename(temporary, path)), Effect7.onError(() => created ? Effect7.ignore(fs.remove(temporary)) : Effect7.void));
1901
- });
1902
-
1903
- // packages/server/src/log.ts
1904
- import { Effect as Effect8, FileSystem as FileSystem6, Option as Option3, Path as Path7, Semaphore as Semaphore2 } from "effect";
1905
- var MAX_LOG = 1024 * 1024;
1906
- var MAX_LINE = 16 * 1024;
1907
-
1908
- class Log {
1909
- fs;
1910
- path;
1911
- lock = Semaphore2.makeUnsafe(1);
1912
- constructor(fs, path) {
1913
- this.fs = fs;
1914
- this.path = path;
1915
- }
1916
- static make = Effect8.fn("Log.make")(function* (home) {
1917
- const fs = yield* FileSystem6.FileSystem;
1918
- const path = yield* Path7.Path;
1919
- return new Log(fs, path.join(home, "daemon.log"));
1920
- });
1921
- rotate() {
1922
- return this.lock.withPermit(this.rotateUnlocked());
1923
- }
1924
- write(message) {
1925
- const { fs, path } = this;
1926
- return this.lock.withPermit(Effect8.gen({ self: this }, function* () {
1927
- yield* this.rotateUnlocked();
1928
- const line = new TextEncoder().encode(`${new Date().toISOString()} ${message}
1929
- `);
1930
- if (line.length <= MAX_LINE)
1931
- yield* fs.writeFile(path, line, { flag: "a", mode: 384 });
1932
- })).pipe(Effect8.ignore);
1933
- }
1934
- rotateUnlocked() {
1935
- const { fs, path } = this;
1936
- return Effect8.gen(function* () {
1937
- const info = yield* fs.stat(path).pipe(Effect8.option);
1938
- if (Option3.isNone(info) || Number(info.value.size) < MAX_LOG)
1939
- return;
1940
- yield* fs.copyFile(path, `${path}.old`);
1941
- yield* fs.truncate(path, 0);
1942
- });
1943
- }
1944
- }
1945
-
1946
- // packages/server/src/process.ts
1947
- function alive(pid) {
1948
- try {
1949
- process.kill(pid, 0);
1950
- return true;
1951
- } catch (error) {
1952
- return error.code === "EPERM";
1953
- }
1954
- }
1955
-
1956
- // packages/server/src/static.ts
1957
- import { Effect as Effect9, FileSystem as FileSystem7, Option as Option4, Path as Path8 } from "effect";
1958
- import { HttpServerResponse as Response, Mime } from "effect/http";
1959
- var staticResponse = Effect9.fn("staticResponse")(function* (root, pathname) {
1960
- const fs = yield* FileSystem7.FileSystem;
1961
- const paths = yield* Path8.Path;
1962
- if (!(yield* fs.exists(paths.resolve(root, "index.html"))))
1963
- return Response.html("<!doctype html><p>Run <code>bun run build:web</code> to build the web app.</p>");
1964
- let decoded;
1965
- try {
1966
- decoded = decodeURIComponent(pathname);
1967
- } catch {
1968
- return Response.text("Invalid path", { status: 400 });
1969
- }
1970
- if (decoded.includes("\x00") || decoded.includes("\\"))
1971
- return Response.text("Invalid path", { status: 400 });
1972
- const canonical = yield* fs.realPath(root);
1973
- let path = paths.resolve(canonical, `.${decoded}`);
1974
- if (path !== canonical && !path.startsWith(canonical + paths.sep))
1975
- return Response.text("Forbidden", { status: 403 });
1976
- const file = yield* fs.stat(path).pipe(Effect9.map((info) => info.type === "File"), Effect9.catch(() => Effect9.succeed(false)));
1977
- if (!file)
1978
- path = paths.resolve(canonical, "index.html");
1979
- path = yield* fs.realPath(path);
1980
- if (!path.startsWith(canonical + paths.sep))
1981
- return Response.text("Forbidden", { status: 403 });
1982
- return yield* Response.file(path, { contentType: Option4.getOrElse(Mime.getType(path), () => "application/octet-stream") });
1983
- });
1984
-
1985
- // packages/server/src/versions.ts
1986
- import { Effect as Effect10, Fiber as Fiber3, Path as Path9 } from "effect";
1987
- import { Digest } from "effect/crypto";
1988
- var MAX_ENTRIES = 256;
1989
- var encoder3 = new TextEncoder;
1990
-
1991
- class Versions {
1992
- path;
1993
- log;
1994
- digest;
1995
- context;
1996
- scope;
1997
- entries = new Map;
1998
- timer;
1999
- constructor(path, log, digest, context, scope) {
2000
- this.path = path;
2001
- this.log = log;
2002
- this.digest = digest;
2003
- this.context = context;
2004
- this.scope = scope;
2005
- }
2006
- static make = Effect10.fn("Versions.make")(function* (home, log) {
2007
- const path = yield* Path9.Path;
2008
- const versions = new Versions(path.join(home, "versions.json"), log, yield* Digest.Digest, yield* Effect10.context(), yield* Effect10.scope);
2009
- const value = yield* readJson(versions.path).pipe(Effect10.catch((error) => isNotFound(error) ? Effect10.succeed(undefined) : Effect10.as(log.write("Could not read versions.json, starting with fresh version records"), undefined)));
2010
- if (value && typeof value === "object" && !Array.isArray(value)) {
2011
- for (const [path2, entry] of Object.entries(value)) {
2012
- const item = entry;
2013
- if (item && Number.isSafeInteger(item.version) && item.version > 0 && typeof item.sha256 === "string" && /^[a-f0-9]{64}$/.test(item.sha256)) {
2014
- versions.entries.set(path2, item);
2015
- versions.trim();
2016
- }
2017
- }
2018
- }
2019
- return versions;
2020
- });
2021
- sha256(text) {
2022
- return this.digest.digestHex(Digest.sha256, encoder3.encode(text));
2023
- }
2024
- has(path) {
2025
- return this.entries.has(path);
2026
- }
2027
- open(path, text) {
2028
- return Effect10.gen({ self: this }, function* () {
2029
- const old = this.entries.get(path);
2030
- let doc;
2031
- if (old && old.sha256 !== (yield* this.sha256(text))) {
2032
- doc = Document.open(text === "" ? `
2033
- ` : "", old.version);
2034
- doc.mergeFile(text);
2035
- doc.compact(0, 0, 1);
2036
- } else
2037
- doc = Document.open(text, old?.version);
2038
- yield* this.record(path, doc.version, text);
2039
- return doc;
2040
- });
2041
- }
2042
- record(path, version, text) {
2043
- return Effect10.gen({ self: this }, function* () {
2044
- const sha256 = yield* this.sha256(text);
2045
- this.entries.delete(path);
2046
- this.entries.set(path, { version, sha256 });
2047
- this.trim();
2048
- if (!this.timer) {
2049
- const save = Effect10.suspend(() => {
2050
- this.timer = undefined;
2051
- return this.save();
2052
- });
2053
- this.timer = yield* Effect10.forkIn(Effect10.delay(save, "1 second"), this.scope);
2054
- }
2055
- });
2056
- }
2057
- flush() {
2058
- return Effect10.suspend(() => {
2059
- const timer = this.timer;
2060
- this.timer = undefined;
2061
- return Effect10.andThen(timer ? Fiber3.interrupt(timer) : Effect10.void, this.save());
2062
- });
2063
- }
2064
- trim() {
2065
- while (this.entries.size > MAX_ENTRIES)
2066
- this.entries.delete(this.entries.keys().next().value);
2067
- }
2068
- save() {
2069
- return writeJson(this.path, Object.fromEntries(this.entries)).pipe(Effect10.provideContext(this.context), Effect10.catch((error) => this.log.write(`Could not save versions: ${String(error)}`)));
2070
- }
2071
- }
2072
-
2073
- // packages/server/src/http.ts
2074
- var encoder4 = new TextEncoder;
2075
- var makeServer = Effect11.fn("makeServer")(function* (options) {
2076
- const settings = options ?? (yield* config);
2077
- const fs = yield* FileSystem9.FileSystem;
2078
- const path = yield* Path10.Path;
2079
- yield* fs.makeDirectory(settings.home, { recursive: true, mode: 448 });
2080
- yield* fs.chmod(settings.home, 448);
2081
- const log = yield* Log.make(settings.home);
2082
- const auth = yield* Auth.make(settings.home);
2083
- const versions = yield* Versions.make(settings.home, log);
2084
- const stop = yield* Deferred4.make();
2085
- const shutdown = new AbortController;
2086
- const scope = yield* Effect11.scope;
2087
- let stopping = false;
2088
- const runtime = { port: settings.port, startedAt: Date.now(), stop: Effect11.suspend(() => {
2089
- if (stopping)
2090
- return Effect11.void;
2091
- stopping = true;
2092
- return Effect11.asVoid(Effect11.forkIn(Effect11.delay(Deferred4.succeed(stop, undefined), "50 millis"), scope));
2093
- }) };
2094
- const serverOptions = {
2095
- hostname: "127.0.0.1",
2096
- port: settings.port,
2097
- maxRequestBodySize: MAX_BODY,
2098
- disablePreemptiveShutdown: true
2099
- };
2100
- const server = yield* BunHttpServer.make(serverOptions).pipe(Effect11.catchCause((cause) => settings.port === 0 ? Effect11.failCause(cause) : BunHttpServer.make({ ...serverOptions, port: 0 })));
2101
- if (!("port" in server.address))
2102
- return yield* Effect11.die(new Error("Expected an IP listening address"));
2103
- runtime.port = server.address.port;
2104
- const documentsLayer = DocumentsLive(versions, log);
2105
- const context = yield* Layer2.build(documentsLayer);
2106
- const registry = yield* Documents.pipe(Effect11.provideContext(context));
2107
- const rpcLayer = RpcServer.layerHttp({ group: DoItRpcs, path: "/rpc", protocol: "http", streamBufferSize: 1, concurrency: 512 }).pipe(Layer2.provide(HandlersLive(auth, runtime)), Layer2.provide(Layer2.succeed(Documents, registry)), Layer2.provide(RpcSerialization.layerNdjson));
2108
- const rpc = yield* HttpRouter.toHttpEffect(rpcLayer);
2109
- let active = 0;
2110
- let lastRequest = Date.now();
2111
- const http = Effect11.gen(function* () {
2112
- const request = yield* Request.HttpServerRequest;
2113
- const host = request.headers.host;
2114
- const origin = request.headers.origin;
2115
- const allowed = [`127.0.0.1:${runtime.port}`, `localhost:${runtime.port}`];
2116
- if (!host || !allowed.includes(host) || origin !== undefined && !allowed.map((host) => `http://${host}`).includes(origin)) {
2117
- return Response2.text("Forbidden", { status: 403 });
2118
- }
2119
- lastRequest = Date.now();
2120
- if (active >= 512)
2121
- return Response2.text("Too many requests", { status: 503 });
2122
- active++;
2123
- yield* Effect11.addFinalizer(() => Effect11.sync(() => {
2124
- active--;
2125
- lastRequest = Date.now();
2126
- }));
2127
- const url = new URL(request.url, `http://127.0.0.1:${runtime.port}`);
2128
- if (request.method === "GET" && url.pathname === "/health") {
2129
- return Response2.jsonUnsafe({ ok: true, pid: process.pid, appVersion: APP_VERSION });
2130
- }
2131
- if (request.method === "GET" && url.pathname === "/auth") {
2132
- const path = url.searchParams.get("path") ?? "";
2133
- if (!auth.consume(url.searchParams.get("code") ?? "", path)) {
2134
- return Response2.text("<!doctype html><p>This link is invalid or expired. Run <code>do-it open</code> again.</p>", { status: 401, contentType: "text/html" });
2135
- }
2136
- return Response2.redirect(`/?path=${encodeURIComponent(path)}`, {
2137
- status: 303,
2138
- headers: { "set-cookie": `do_it=${auth.cookie}; HttpOnly; SameSite=Strict; Path=/`, "cache-control": "no-store", "referrer-policy": "no-referrer" }
2139
- });
2140
- }
2141
- if (url.pathname === "/rpc" || url.pathname === "/rpc/") {
2142
- if (!auth.authorized(request.headers.authorization, request.cookies.do_it))
2143
- return Response2.text("Unauthorized", { status: 401 });
2144
- if (request.method !== "POST")
2145
- return Response2.text("Use POST", { status: 405 });
2146
- if (Number(request.headers["content-length"] ?? 0) > MAX_BODY)
2147
- return Response2.text("Request body is too large", { status: 413 });
2148
- const body = yield* request.text.pipe(Effect11.result);
2149
- if (body._tag === "Failure")
2150
- return Response2.text("Could not read request body", { status: 413 });
2151
- if (encoder4.encode(body.success).length > MAX_BODY)
2152
- return Response2.text("Request body is too large", { status: 413 });
2153
- let frames = 0;
2154
- for (let i = 0;i < body.success.length; i++) {
2155
- if (body.success.charCodeAt(i) === 10 && ++frames > 64)
2156
- return Response2.text("Too many RPC messages", { status: 413 });
2157
- }
2158
- return yield* rpc;
2159
- }
2160
- if (request.method !== "GET" && request.method !== "HEAD")
2161
- return Response2.text("Not found", { status: 404 });
2162
- return yield* staticResponse(settings.webDist, url.pathname);
2163
- });
2164
- yield* server.serve(http.pipe(Effect11.flatMap((response) => Effect11.gen(function* () {
2165
- if (response.body._tag !== "Stream")
2166
- return response;
2167
- const request = yield* Request.HttpServerRequest;
2168
- if (Response2.omitsBody(response, request.method === "HEAD"))
2169
- return response;
2170
- const context = yield* Effect11.context();
2171
- return Response2.raw(Response2.toWeb(HttpEffect.scopeTransferToStream(response), {
2172
- context,
2173
- signal: AbortSignal.any([BunHttpServerRequest.toBunServerRequest(request).signal, shutdown.signal])
2174
- }));
2175
- }))));
2176
- const record = { pid: process.pid, port: runtime.port, appVersion: APP_VERSION, startedAt: runtime.startedAt };
2177
- const recordPath = path.join(settings.home, "daemon.json");
2178
- const prior = yield* readJson(recordPath, 4096).pipe(Effect11.map((value) => value), Effect11.catch(() => Effect11.succeed(undefined)));
2179
- if (prior?.pid && prior.pid !== process.pid && alive(prior.pid)) {
2180
- return yield* Effect11.fail(new Error("A daemon is already running for this home"));
2181
- }
2182
- yield* fs.remove(recordPath).pipe(Effect11.catchIf(isNotFound, () => Effect11.void));
2183
- yield* fs.writeFileString(recordPath, JSON.stringify(record), { mode: 384, flag: "wx" });
2184
- const tick = Effect11.gen(function* () {
2185
- yield* Effect11.ignore(log.rotate());
2186
- if (active === 0 && registry.totalWatchers === 0 && Date.now() - lastRequest >= settings.idleMs)
2187
- yield* runtime.stop;
2188
- });
2189
- yield* Effect11.forkScoped(Effect11.repeat(tick, Schedule.spaced(Math.max(10, Math.min(1000, settings.idleMs / 4)))));
2190
- yield* Effect11.addFinalizer(() => Effect11.gen(function* () {
2191
- shutdown.abort();
2192
- yield* registry.close();
2193
- yield* readJson(recordPath, 4096).pipe(Effect11.flatMap((value) => {
2194
- const current = value;
2195
- return current.pid === record.pid && current.startedAt === record.startedAt && current.port === record.port ? fs.remove(recordPath) : Effect11.void;
2196
- }), Effect11.ignore);
2197
- yield* log.write("Daemon stopped");
2198
- }));
2199
- yield* log.write(`Daemon listening on 127.0.0.1:${runtime.port}`);
2200
- return { ...record, home: settings.home, auth, documents: registry, stopped: Deferred4.await(stop), stop: runtime.stop };
2201
- });
2202
-
2203
- // packages/server/src/services.ts
2204
- import { BunCrypto, BunHttpPlatform, BunServices } from "@effect/platform-bun";
2205
- import { Layer as Layer3 } from "effect";
2206
- var ServerServices = Layer3.mergeAll(BunServices.layer, BunCrypto.layerDigest, BunCrypto.layerMac, BunCrypto.layerSecureRandom, BunHttpPlatform.layer);
2207
-
2208
- // packages/server/src/main.ts
2209
- BunRuntime.runMain(Effect12.scoped(Effect12.gen(function* () {
2210
- const server = yield* makeServer();
2211
- yield* server.stopped;
2212
- })).pipe(Effect12.provide(ServerServices)), { teardown: (exit, done) => done(Exit2.isFailure(exit) && !Cause3.hasInterruptsOnly(exit.cause) ? 1 : 0) });
2
+ import{BunRuntime as Ir}from"@effect/platform-bun";import{Cause as Lr,Effect as St,Exit as Mr}from"effect";var j=2097152,L=512,Ke=128,Xe=65536,Ee=4096;import*as wt from"effect/Data";class y extends wt.TaggedError("DocError"){}function Ge(e){if(!Number.isSafeInteger(e)||e<1)throw new y({code:"invalid",message:"A version must be a positive safe integer"})}class we{entries=Array(L);start=0;count=0;get size(){return this.count}record(e,t,o){let n={version:e,frontiers:t,origin:o===null?null:{...o}},r=(this.start+this.count-1)%L;if(this.count&&this.entries[r]?.version===e){this.entries[r]=n;return}if(this.count===L)this.entries[this.start]=n,this.start=(this.start+1)%L;else this.entries[(this.start+this.count)%L]=n,this.count++}find(e){for(let t=this.count-1;t>=0;t--){let o=this.entries[(this.start+t)%L];if(o.version===e)return o}return null}list(){return Array.from({length:this.count},(e,t)=>this.entries[(this.start+t)%L])}originsSince(e){if(!this.find(e))return null;let t=new Set,o=[];for(let n=0;n<this.count;n++){let r=this.entries[(this.start+n)%L];if(r.version<=e||!r.origin)continue;let i=Je(r.origin);if(!t.has(i))t.add(i),o.push({...r.origin})}return o}}function Je(e){return e.kind==="agent"?`agent:${e.name}`:e.kind}var Ho=new TextEncoder;function le(e){return Ho.encode(e).length}function zo(e){let t=le(e);if(t>j)throw new y({code:"too-large",message:`The document would be ${t} bytes, over the limit of ${j}`})}function U(e){if(zo(e),!e.isWellFormed())throw new y({code:"invalid",message:"Text contains an unpaired surrogate"})}function Tt(e,t){let o=e.charCodeAt(t-1),n=e.charCodeAt(t);return!(o>=55296&&o<=56319&&n>=56320&&n<=57343)}function Ye(e,t){let o=t.map((r)=>({...r})).sort((r,i)=>r.from-i.from||r.to-i.to),n=0;for(let r of o){if(!Number.isSafeInteger(r.from)||!Number.isSafeInteger(r.to)||r.from<n||r.from>r.to||r.to>e.length||!Tt(e,r.from)||!Tt(e,r.to))throw new y({code:"invalid",message:`The edit ${r.from}..${r.to} overlaps another edit, is outside the text, or splits a character`});U(r.insert),n=r.to}return o.filter((r)=>e.slice(r.from,r.to)!==r.insert)}function pe(e,t){let o=Ye(e,t),n=[],r=0;for(let s of o)n.push(e.slice(r,s.from),s.insert),r=s.to;n.push(e.slice(r));let i=n.join("");return U(i),i}import{createTwoFilesPatch as jo,diffArrays as Ko,diffChars as Xo}from"diff";var bt=16384,kt=250;function Dt(e,t=0){let o=[],n=t,r=n,i=[];function s(){if(r!==n||i.length)o.push({from:r,to:n,insert:i.join("")});i=[]}for(let a of e)if(a.added)i.push(a.value);else if(a.removed)n+=a.value.length;else s(),n+=a.value.length,r=n;return s(),o}function Z(e,t){if(e===t)return[];let o=performance.now()+kt,n=e.match(/[^\n]*\n|[^\n]+$/g)??[],r=t.match(/[^\n]*\n|[^\n]+$/g)??[],i=o-performance.now(),s=i>0?Ko(n,r,{timeout:i}):void 0;if(!s)return[{from:0,to:e.length,insert:t}];let a=Dt(s.map((l)=>({...l,value:l.value.join("")}))),d=[];for(let l of a){let u=e.slice(l.from,l.to),f=o-performance.now();if(!u||!l.insert||f<=0||le(u)>bt||le(l.insert)>bt){d.push(l);continue}let h=Xo(u,l.insert,{timeout:Math.min(kt/4,f)});if(h)d.push(...Dt(h,l.from));else d.push(l)}return d}function Te(e,t,o,n){if(e===t)return"";let r=jo(String(o),String(n),e,t,void 0,void 0,{context:2,headerOptions:{includeIndex:!1,includeUnderline:!1,includeFileHeaders:!0}});return le(r)>Xe?null:r}function A(e){let t=[],o=null,n=0;while(!0){let r=e.indexOf(`
3
+ `,n),i=r===-1?e.length:r,s=i>n&&e[i-1]==="\r"?i-1:i,a=e.slice(n,s),d=/^[ \t]*/.exec(a)[0],l=a.slice(d.length),u=n+d.length,f={from:n,to:s,indent:d.length,depth:Array.from(d).reduce((h,g)=>h+(g==="\t"?4:1),0),content:u,kind:"paragraph"};if(o){let h=0;while(l[h]===o.marker)h++;if(h>=o.count&&!l.slice(h).trim())f.kind="fence",o=null;else f.kind="code"}else{let h=/^(`{3,}|~{3,})(.*)$/.exec(l),g=/^(#{1,6})(?: |$)/.exec(l),w=/^(?:[-*+]|[0-9]{1,9}[.)])(?:[ \t]|$)/.exec(l);if(h&&(h[1][0]==="~"||!h[2].includes("`")))o={marker:h[1][0],count:h[1].length},f.kind="fence";else if(!l)f.kind="blank";else if(g)f.kind="heading",f.level=g[1].length,f.content=u+g[0].length;else if(/^(?:-(?: *-){2,}|\*(?: *\*){2,}|_(?: *_){2,})\s*$/.test(l))f.kind="rule";else if(l.startsWith(">"))f.kind="quote";else if(w){f.kind="item",f.content=u+w[0].length;let b=/^\[([ xX-])\](?:[ \t]|$)/.exec(l.slice(w[0].length));if(b)f.checkbox=f.content,f.content+=b[0].length,f.state=b[1]===" "?"open":b[1]==="-"?"cancelled":"done"}}if(t.push(f),r===-1)return t;n=i+1}}function At(e,t){let o=e[t].depth,n=t+1;for(let r=n;r<e.length;r++){let i=e[r];if(i.kind==="blank"||i.kind==="code"&&i.from===i.to)continue;if(i.depth<=o||i.kind==="heading")break;n=r+1}return n}function Rt(e){return e.toLowerCase().replace(/[^\p{L}\p{N} _-]/gu,"").replace(/ /g,"-")}function _(e){let t=[],o=new Map;for(let[n,r]of A(e).entries()){if(r.kind!=="heading"||r.level>3)continue;let i=e.slice(r.content,r.to).trim().replace(/#+$/,"").trim(),s=Rt(i),a=o.get(s)??0;o.set(s,a+1);let d=t.at(-1);if(d)d.to=r.from;t.push({id:a?`${s}-${a}`:s,title:i,level:r.level,line:n,from:r.from,to:e.length})}return t}function F(e,t){let o=t.trim().replace(/^#+/,""),n=_(e).find((r)=>r.id===o);if(!n)throw new y({code:"not-found",message:`No section has the ID ${JSON.stringify(o)}`});return n}function ie(e,t){let o=t.trim().replace(/^#+/,""),n=_(e).find((r)=>r.id===o);return n?e.slice(n.from,n.to):null}var Go="started|done|cancelled|lasted";function Ze(e){return new RegExp(`(?:^|[ \\t]+)@(?:${e??Go})(?![A-Za-z0-9_])[ \\t]*\\([^()\\r\\n]*\\)`,"g")}function B(e,t){return e.replace(Ze(t),"")}function Jo(e,t){return Ze(t).test(e)}function Yo(e,t){let o=A(e)[t.line];if(!o||o.state===void 0)return null;return{from:o.content,to:o.to,body:e.slice(o.content,o.to)}}function Ft(e,t){let o=/[ \t]*$/.exec(e),n=e.slice(0,o.index),r=/[ \t]+@project(?:[ \t]*\([^()\r\n]*\))?$/.exec(n),s=(r?n.slice(0,r.index):n).trimEnd();return`${s?`${s} ${t}`:t}${r?.[0]??""}${o[0]}`}function Pt(e,t,o){let n=Yo(e,t);if(!n)return[];let r=o(n.body);return r===n.body?[]:[{from:n.from,to:n.to,insert:r}]}function Ct(e,t){let o=Nt(t);return e==="started"?`@${e}(${o})`:`@${e} (${o})`}function $t(e){let t=/(?:^|[ \t])@started[ \t]*\((\d{2})-(\d{2})-(\d{2}) (\d{2}):(\d{2})\)/.exec(e);if(!t)return null;let[,o,n,r,i,s]=t.map(Number);return new Date(2000+o,n-1,r,i,s)}var Ot=(e)=>Date.UTC(e.getFullYear(),e.getMonth(),e.getDate(),e.getHours(),e.getMinutes());function _t(e){let t=Math.max(0,Math.floor(e/60000)),o=Math.floor(t/1440),n=Math.floor(t%1440/60),r=t%60,i=`${n}:${String(r).padStart(2,"0")}`;if(!o)return i;let s=`${o} day${o===1?"":"s"}`;return n||r?`${s}, ${i}`:s}function Zo(e,t,o){let n=A(e)[t.line];if(!n||n.checkbox===void 0||n.state===o)return[];let r=o==="done"?"[x]":o==="cancelled"?"[-]":"[ ]";return[{from:n.checkbox,to:n.checkbox+3,insert:r}]}function Nt(e){let t=(o)=>String(o).padStart(2,"0");return`${t(e.getFullYear()%100)}-${t(e.getMonth()+1)}-${t(e.getDate())} ${t(e.getHours())}:${t(e.getMinutes())}`}function Qe(e,t,o,n){let r=Ct(o,n);return Pt(e,t,(i)=>Ft(B(i,o),r))}function qe(e,t,o,n){let r=A(e)[t.line]?.state,i=Zo(e,t,o),s=o==="done"?"done":o==="cancelled"?"cancelled":null,a=Pt(e,t,(d)=>{let l=d;if(o==="done")l=B(l,"cancelled");else if(o==="cancelled")l=B(B(l,"done"),"lasted");else l=B(B(B(l,"done"),"cancelled"),"lasted");if(s&&(r!==o||!Jo(l,s))){let u=Ct(s,n),f=s==="done"?$t(l):null;if(f)u+=` @lasted(${_t(Ot(n)-Ot(f))})`;l=Ft(B(B(l,s),"lasted"),u)}return l});return[...i,...a].sort((d,l)=>d.from-l.from||d.to-l.to)}function H(e){return e.replace(Ze(),"").trim()}function Q(e){let t=[],o=null,n=new Map(_(e).map((r)=>[r.line,r.id]));for(let[r,i]of A(e).entries()){let s=n.get(r);if(s!==void 0)o=s;if(i.state===void 0)continue;t.push({number:t.length+1,line:r,state:i.state,text:e.slice(i.content,i.to).trim(),indent:i.depth,section:o})}return t}function ke(e,t,o){let n=o===void 0?null:F(e,o),r=n?A(e):null,i=Q(e).filter((d)=>!n||r[d.line].from>=n.from&&r[d.line].from<n.to),s=H(String(t).trim()).toLowerCase(),a;if(typeof t==="number"||/^\+?\d+$/.test(s))a=i.filter((d)=>d.number===Number(s));else if(a=i.filter((d)=>H(d.text).toLowerCase()===s),!a.length&&s)a=i.filter((d)=>H(d.text).toLowerCase().includes(s));if(!a.length)throw new y({code:"not-found",message:`No todo matches ${JSON.stringify(t)}`});if(a.length>1)throw new y({code:"ambiguous",message:`${a.length} todos match ${JSON.stringify(t)}, name one by its number`});return a[0]}function be(e,t){return pe(e,[t]),[t]}function et(e,t,o){if(t=t.trim(),Array.from(t).length>Ee)throw new y({code:"too-large",message:`A todo may have at most ${Ee} characters`});if(!t||/[\r\n]/.test(t))throw new y({code:"invalid",message:"A todo must be one nonempty line of text"});let n=o===void 0?null:F(e,o),r=A(e),i=-1;for(let[u,f]of r.entries())if(f.state!==void 0&&(!n||f.from>=n.from&&f.from<n.to))i=u;let s=`- [ ] ${t}`;if(i===-1)return De(e,s,o);let a=r[i],d=r[At(r,i)-1].to,l=e.slice(a.from,a.from+a.indent);return be(e,{from:d,to:d,insert:`
4
+ ${l}${s}`})}function It(e,t){let o=e.replace(/[\r\n]+$/,""),n=A(o).at(-1),r=!o?"":n?.kind==="item"&&t.startsWith("- ")?`
5
+ `:`
6
+
7
+ `;return{from:o.length,to:e.length,insert:`${r}${t}
8
+ `}}function De(e,t,o){if(t=t.replace(/[\r\n]+$/,""),!t.trim())throw new y({code:"invalid",message:"There is no text to add"});if(o===void 0)return be(e,It(e,t));let n=F(e,o),r=e.slice(n.from,n.to),i=r.replace(/[\r\n]+$/,""),s=It(i,t);return be(e,{from:n.from+s.from,to:n.from+s.to,insert:i.length===r.length?s.insert:s.insert.replace(/\n+$/,"")})}function tt(e,t,o,n){if(!t)throw new y({code:"invalid",message:"Text to replace must not be empty"});let r=n===void 0?{from:0,to:e.length}:F(e,n),i=e.slice(r.from,r.to),s=i.indexOf(t);if(s===-1)throw new y({code:"not-found",message:"The text to replace is not in the document"});if(i.indexOf(t,s+t.length)!==-1)throw new y({code:"ambiguous",message:"Several places match the text to replace, include more of it so one does"});return be(e,{from:r.from+s,to:r.from+s+t.length,insert:o})}import{diffArrays as Qo,diffChars as qo}from"diff";function Ae(e,t,o=t){return{kind:e,number:t.number,text:H(t.text),line:t.line,oldLine:o.line}}function en(e,t){if(e===t)return null;if(t==="done")return"checked";if(t==="cancelled")return"cancelled";return e==="done"?"unchecked":"uncancelled"}function tn(e,t){let o=Array.from(e).length+Array.from(t).length;if(!o)return 1;return 2*qo(e,t).reduce((r,i)=>r+(i.added||i.removed?0:i.count),0)/o}function ot(e,t){let o=Q(e),n=Q(t),r=Qo(o.map((d)=>H(d.text)),n.map((d)=>H(d.text))),i=[],s=0,a=0;for(let d=0;d<r.length;d++){let l=r[d];if(!l.added&&!l.removed){for(let g=0;g<l.count;g++){let w=o[s++],b=n[a++],O=en(w.state,b.state);if(O)i.push(Ae(O,b,w))}continue}let u=[],f=[];while(d<r.length&&(r[d].added||r[d].removed)){let g=r[d];if(g.removed)u.push(...o.slice(s,s+g.count)),s+=g.count;else f.push(...n.slice(a,a+g.count)),a+=g.count;d++}d--;let h=new Set;for(let g of f){let w=-1,b=0.5;for(let[O,Ue]of u.entries()){if(h.has(O))continue;let xe=tn(Ue.text,g.text);if(xe>=b)w=O,b=xe}if(w===-1)i.push(Ae("added",g));else h.add(w),i.push(Ae("edited",g,u[w]))}for(let[g,w]of u.entries())if(!h.has(g))i.push(Ae("removed",w))}return i}import{LoroDoc as Lt}from"loro-crdt";var q="markdown",on=8,nn=Symbol("WriteTicket");function rn(e,t){return e.length===t.length&&e.every((o)=>t.some((n)=>n.peer===o.peer&&n.counter===o.counter))}function se(e,t){for(let o=t.length-1;o>=0;o--){let n=t[o];if(n.to!==n.from)e.delete(n.from,n.to-n.from);if(n.insert)e.insert(n.from,n.insert)}}class ce{doc;body;current;currentVersion;history=new we;disk;agents=new Map;writing=[];tickets=new WeakMap;generation=0;constructor(e,t){this.doc=new Lt,this.body=this.doc.getText(q),this.body.insert(0,e),this.doc.commit(),this.current=e,this.currentVersion=t,this.disk=this.snapshot(),this.history.record(t,this.doc.frontiers(),null)}static open(e,t=1){return U(e),Ge(t),new ce(e,t)}get text(){return this.current}get version(){return this.currentVersion}get synced(){return{text:this.disk.text,version:this.disk.version}}get dirty(){return this.current!==this.disk.text}agentEdit(e,t,o){if(t.section!==void 0)F(this.current,t.section);let n=null;if(t.version!==this.version){let l=this.textAt(t.version),u=l===null?null:Te(l,this.current,t.version,this.version),f=l===null||t.section===void 0?null:ie(l,t.section);if(f===null||f!==ie(this.current,t.section))throw new y({code:"conflict",message:`The document is at version ${this.version}, not ${t.version}`,conflict:{expected:t.version,current:this.version,diff:u,forgotten:l===null}});n={since:t.version,diff:u}}let r=Ye(this.current,o(this.current));if(pe(this.current,r)===this.current)return this.applied(!1,n);this.checkNextVersion();let s=this.doc.fork(),a=s.getText(q),d=this.doc.oplogVersion();try{let l=Je(e),u=this.agents.get(l);if(u!==void 0)s.setPeerId(u);return se(a,r),s.commit(),this.doc.import(s.export({mode:"update",from:d})),this.agents.set(l,s.peerIdStr),this.applied(this.refresh(e),n)}finally{d.free(),a.free(),s.free()}}userEdit(e,t){U(t);let o=this.history.find(e);if(!o)throw new y({code:"forgotten",message:`Version ${e} is no longer kept`});if(t===this.current)return this.applied(!1);if(e===this.version)return this.checkNextVersion(),se(this.body,Z(this.current,t)),this.doc.commit(),this.applied(this.refresh({kind:"user"}));let n=this.doc.forkAt(o.frontiers),r=n.getText(q);try{let i=r.toString();if(i===t)return this.applied(!1);return se(r,Z(i,t)),this.importFork(n),this.applied(this.refresh({kind:"user"}))}finally{r.free(),n.free()}}mergeFile(e){if(U(e),e===this.disk.text)return!1;let t=this.writing.find((r)=>r.text===e);if(t)return this.wrote(t),!1;if(e===this.current)return this.disk=this.snapshot(),!1;if(rn(this.disk.frontiers,this.doc.frontiers()))return this.checkNextVersion(),se(this.body,Z(this.disk.text,e)),this.doc.commit(),this.disk={text:e,version:this.disk.version,frontiers:this.doc.frontiers()},this.refresh({kind:"file"});let o=this.doc.forkAt(this.disk.frontiers),n=o.getText(q);try{se(n,Z(this.disk.text,e)),o.commit();let r=o.frontiers();return this.importFork(o),this.disk={text:e,version:this.disk.version,frontiers:r},this.refresh({kind:"file"})}finally{n.free(),o.free()}}importFork(e){e.commit();let t=this.doc.oplogVersion(),o=e.oplogVersion(),n=e.getText(q);try{let r=e.export({mode:"update",from:t});e.import(this.doc.export({mode:"update",from:o}));let i=n.toString();if(U(i),i!==this.current)this.checkNextVersion();this.doc.import(r)}finally{n.free(),o.free(),t.free()}}toWrite(){let e=this.snapshot(),t=Object.freeze({text:e.text,version:e.version,[nn]:!0});if(this.tickets.set(t,{snapshot:e,generation:this.generation}),this.writing.length===on)this.writing.shift();return this.writing.push(t),t}wrote(e){let t=this.ticketSnapshot(e),o=this.writing.indexOf(e);if(o!==-1)this.writing.splice(0,o+1);this.disk=t}notWritten(e){this.ticketSnapshot(e);let t=this.writing.indexOf(e);if(t!==-1)this.writing.splice(t,1)}ticketSnapshot(e){let t=this.tickets.get(e);if(!t||t.generation!==this.generation)throw new y({code:"invalid",message:"The write ticket does not belong to this document's current history"});return t.snapshot}textAt(e){if(e===this.version)return this.current;let t=this.history.find(e);if(!t)return null;let o=this.doc.forkAt(t.frontiers),n=o.getText(q);try{return n.toString()}finally{n.free(),o.free()}}changes(e,t){let o=this.history.originsSince(e);if(o===null)return null;let n=this.textAt(e),r=this.current,i=t===void 0?n:ie(n,t)??"",s=t===void 0?r:ie(r,t)??"",a=ot(n,r);if(t!==void 0){let d=t.trim().replace(/^#+/,""),l=_(n).find((g)=>g.id===d),u=_(r).find((g)=>g.id===d),f=A(n),h=A(r);a=a.filter((g)=>{let w=g.kind==="removed",b=w?l:u,O=w?f[g.oldLine]:h[g.line];return b!==void 0&&O!==void 0&&O.from>=b.from&&O.from<b.to})}return{from:e,to:this.version,by:o,diff:Te(i,s,e,this.version),todos:a}}origin(e){let t=this.history.find(e)?.origin;return t?{...t}:null}compact(e=Math.max(50000,4*this.current.length),t=256,o=Ke){if(this.dirty||this.writing.length)return null;let n=0,r=0,i=this.doc.oplogVersion();try{n=this.doc.opCount(),r=i.length()}finally{i.free()}if(n<=e&&r<=t)return null;let s=n>e?"ops":"peers",a=this.history.list().slice(-Math.max(1,o)).map((f)=>({...f,text:f.version===this.version?this.current:this.textAt(f.version)}));if(a.length===0||a.at(-1).version!==this.version)throw Error("Compaction history does not include the current document");let d=new Lt,l=d.getText(q),u=new we;try{let f=a[0];l.insert(0,f.text),d.commit(),u.record(f.version,d.frontiers(),f.origin);let h=f.text;for(let g of a.slice(1))se(l,Z(h,g.text)),d.commit(),u.record(g.version,d.frontiers(),g.origin),h=g.text;if(h!==this.current)throw Error("Compaction did not preserve the current document")}catch(f){throw l.free(),d.free(),f}return this.body.free(),this.doc.free(),this.doc=d,this.body=l,this.disk={...this.disk,frontiers:d.frontiers()},this.history=u,this.generation++,{reason:s,ops:n,peers:r,kept:a.length,oldest:a[0].version}}snapshot(){return{text:this.current,version:this.version,frontiers:this.doc.frontiers()}}checkNextVersion(){Ge(this.version+1)}refresh(e){let t=this.body.toString(),o=t!==this.current;if(o)this.current=t,this.currentVersion++;return this.history.record(this.version,this.doc.frontiers(),o?e:this.origin(this.version)),o}applied(e,t=null){return{version:this.version,changed:e,others:t}}free(){this.body.free(),this.doc.free()}}import{Effect as S,FileSystem as rt,Option as Mt,Path as sn,PlatformError as cn}from"effect";import{SecureRandom as an}from"effect/crypto";function it(e){return{mtimeMs:Mt.match(e.mtime,{onNone:()=>0,onSome:(t)=>t.getTime()}),size:Number(e.size),ino:Number(Mt.getOrElse(e.ino,()=>0n))}}function K(e){return cn.isPlatformError(e)&&e.reason._tag==="NotFound"}function Re(e,t){if(t instanceof y)return t;return new y({code:K(t)?"not-found":"invalid",message:`Could not access ${e}: ${t instanceof Error?t.message:String(t)}`})}function nt(e){return e>j?S.fail(new y({code:"too-large",message:`The file is ${e} bytes, over the limit of ${j}`})):S.void}var Vt=(e)=>new y({code:"invalid",message:`${e} is not a regular file`}),ee=S.fn("stamp")(function*(e){return yield*(yield*rt.FileSystem).stat(e).pipe(S.map((o)=>it(o)),S.catchIf(K,()=>S.succeed(null)),S.mapError((o)=>Re(e,o)))}),fe=S.fn("readFile")(function*(e){let t=yield*rt.FileSystem;if((yield*t.stat(e)).type!=="File")return yield*S.fail(Vt(e));return yield*S.scoped(S.gen(function*(){let o=yield*t.open(e,{flag:"r"}),n=yield*o.stat;if(n.type!=="File")return yield*S.fail(Vt(e));yield*nt(Number(n.size));let r=new Uint8Array(j+1),i=0;while(i<r.length){let u=yield*o.read(r.subarray(i));if(!u)break;i+=u}yield*nt(i);let s;try{s=new TextDecoder("utf-8",{fatal:!0,ignoreBOM:!0}).decode(r.subarray(0,i))}catch{return yield*S.fail(new y({code:"invalid",message:`${e} is not valid UTF-8 text`}))}let a=s.startsWith("\uFEFF");if(a)s=s.slice(1);let d=s.indexOf(`
9
+ `),l=d>0&&s[d-1]==="\r";return{text:s.replace(/\r\n/g,`
10
+ `),stamp:it(n),format:{bom:a,crlf:l}}}))},(e,t)=>S.mapError(e,(o)=>Re(t,o))),Oe=S.fn("writeAtomic")(function*(e,t,o){let n=yield*rt.FileSystem,r=yield*sn.Path;yield*S.try({try:()=>U(t),catch:(u)=>Re(e,u)});let i=new TextEncoder().encode((o.bom?"\uFEFF":"")+(o.crlf?t.replace(/\n/g,`\r
11
+ `):t));yield*nt(i.length);let s=r.join(r.dirname(e),`.${r.basename(e)}.do-it-${an.uuidV4Unsafe()}.tmp`),a=yield*n.stat(e).pipe(S.map((u)=>u.mode&4095),S.catchIf(K,()=>S.succeed(void 0))),d=!1;return yield*S.scoped(S.gen(function*(){let u=yield*n.open(s,{flag:"wx",mode:a??438});if(d=!0,yield*u.writeAll(i),a!==void 0)yield*n.chmod(s,a);return yield*u.sync,it(yield*u.stat)})).pipe(S.tap(()=>n.rename(s,e)),S.onError(()=>d?S.ignore(n.remove(s)):S.void))},(e,t)=>S.mapError(e,(o)=>Re(t,o)));import{Schema as c}from"effect";var Wt=2097152,dn=4096,E=c.String.pipe(c.check(c.isMaxLength(Wt))),st=c.String.pipe(c.check(c.isMaxLength(dn))),P=c.String.pipe(c.check(c.makeFilter((e)=>e.length>0&&e.startsWith("/")?void 0:"an absolute, non-empty path is required"),c.isMaxLength(Wt))),T=c.Int.pipe(c.check(c.isGreaterThan(0))),te=c.String.pipe(c.check(c.isPattern(/^[a-f0-9]{64}$/))),ln=c.TaggedStruct("User",{}),pn=c.TaggedStruct("Agent",{name:c.String.pipe(c.check(c.isBetweenLength(1,64),c.isPattern(/^[A-Za-z0-9._-]+$/)))}),fn=c.TaggedStruct("File",{}),ct=c.Union([ln,pn,fn]),Ut=c.Literals(["open","done","cancelled"]),un=c.Struct({number:c.Number,line:c.Number,state:Ut,text:st,indent:c.Number,section:c.NullOr(E)}),Bt=c.Struct({kind:c.Literals(["added","removed","checked","unchecked","cancelled","uncancelled","edited"]),number:c.Number,text:st,line:c.Number,oldLine:c.Number}),hn=c.Struct({id:E,title:E,level:c.Literals([1,2,3]),line:c.Number}),mn=c.Struct({since:T,diff:c.NullOr(E)}),gn=c.TaggedStruct("Replace",{old:E,new:E}),yn=c.TaggedStruct("Append",{text:E}),xn=c.TaggedStruct("TodoAdd",{text:st}),vn=c.TaggedStruct("TodoSet",{target:c.Union([c.Number,E]),state:Ut}),Sn=c.TaggedStruct("TodoStart",{target:c.Union([c.Number,E])}),En=c.Union([gn,yn,xn,vn,Sn]),Fe=c.optional(E),Ht=c.Struct({}),wn=c.Struct({path:P,version:T,watchers:c.Number}),zt=c.Struct({pid:c.Number,appVersion:E,port:c.Number,startedAt:c.Number,build:c.optionalKey(E),documents:c.Array(wn)}),jt=c.Struct({}),Kt=c.Struct({}),Xt=c.Struct({path:P,create:c.Boolean}),Gt=c.Struct({path:P,version:T,url:E,created:c.Boolean}),Jt=c.Struct({path:P,section:Fe}),Yt=c.Struct({version:T,text:E,hash:c.optionalKey(te)}),Zt=c.Struct({path:P}),Qt=c.Struct({version:T,sections:c.Array(hn)}),qt=c.Struct({path:P,section:Fe}),eo=c.Struct({version:T,todos:c.Array(un)}),to=c.Struct({path:P,author:ct,version:T,section:Fe,op:En}),oo=c.Struct({version:T,changed:c.Boolean,section:c.NullOr(E),others:c.NullOr(mn)}),no=c.Struct({path:P,base:T,text:E,baseHash:c.optionalKey(te)}),ro=c.Struct({version:T,text:E,hash:c.optionalKey(te)}),io=c.Struct({path:P,since:T,wait:c.Number.pipe(c.check(c.isBetween({minimum:0,maximum:600})))}),so=c.Struct({from:T,to:T,by:c.Array(ct),diff:c.NullOr(E),todos:c.Array(Bt)}),co=c.Struct({path:P,since:c.optional(T),sinceHash:c.optionalKey(te),section:Fe,text:c.Boolean}),ao=c.TaggedStruct("Watching",{version:T,hash:c.optionalKey(te),text:c.optionalKey(E)}),lo=c.TaggedStruct("Change",{from:T,to:T,by:c.Array(ct),diff:c.NullOr(E),todos:c.Array(Bt),hash:c.optionalKey(te),text:c.optionalKey(E)}),po=c.TaggedStruct("Resync",{version:T,hash:c.optionalKey(te),text:c.optionalKey(E)}),fo=c.TaggedStruct("Deleted",{}),uo=c.Union([ao,lo,po,fo]);import{Schema as M}from"effect";class ue extends M.TaggedError()("VersionMismatch",{latest:T,since:T,diff:M.NullOr(E),forgotten:M.Boolean}){}var Tn=M.Literals(["not-found","ambiguous","too-large","invalid"]);class R extends M.TaggedError()("Refused",{code:Tn,message:E}){}class X extends M.TaggedError()("Forgotten",{latest:T}){}class Pe extends M.TaggedError()("NothingYet",{}){}class N extends M.TaggedError()("DocumentGone",{path:P}){}class ho extends M.TaggedError()("Unauthorized",{}){}var I=M.Union([ue,R,X,Pe,N,ho]);import{Rpc as V,RpcGroup as bn}from"effect/rpc";var kn=V.make("status",{payload:Ht,success:zt,error:I}),Dn=V.make("stop",{payload:jt,success:Kt,error:I}),An=V.make("open",{payload:Xt,success:Gt,error:I}),Rn=V.make("read",{payload:Jt,success:Yt,error:I}),On=V.make("sections",{payload:Zt,success:Qt,error:I}),Fn=V.make("todos",{payload:qt,success:eo,error:I}),Pn=V.make("edit",{payload:to,success:oo,error:I}),Cn=V.make("sync",{payload:no,success:ro,error:I}),$n=V.make("diff",{payload:io,success:so,error:I}),_n=V.make("watch",{payload:co,success:uo,error:I,stream:!0}),Ce=bn.make(kn,Dn,An,Rn,On,Fn,Pn,Cn,$n,_n);var ae="0.1.3";import{BunHttpServer as _o,BunHttpServerRequest as br}from"@effect/platform-bun";import{Deferred as xt,Effect as m,FileSystem as kr,Layer as ye,Path as Dr,Schedule as Ar}from"effect";import{HttpEffect as Rr,HttpRouter as Or,HttpServerRequest as No,HttpServerResponse as D}from"effect/http";import{RpcSerialization as Fr,RpcServer as Pr}from"effect/rpc";import{Data as Nn,Effect as oe,FileSystem as In,Path as Ln}from"effect";import{Mac as $e,SecureRandom as Mn}from"effect/crypto";import{Hex as at}from"effect/encoding";var _e=new TextEncoder,Vn=300000;class go extends Nn.TaggedError("InvalidToken"){}function mo(e,t){let o=_e.encode(e),n=_e.encode(t);return o.length===n.length&&$e.timingSafeEqual(o,n)}function Wn(e){return e.reason._tag==="AlreadyExists"}class Ne{token;cookie;random;codes=new Map;constructor(e,t,o){this.token=e;this.cookie=t;this.random=o}static make=oe.fn("Auth.make")(function*(e){let t=yield*In.FileSystem,o=yield*Ln.Path,n=yield*Mn.SecureRandom,r=yield*$e.Mac,i=o.join(e,"token"),s=at.encode(yield*n.randomBytes(32));yield*t.writeFileString(i,s,{flag:"wx",mode:384}).pipe(oe.catchIf(Wn,()=>oe.void));let a=new go({message:"The daemon token file is invalid"});if(Number((yield*t.stat(i)).size)>128)return yield*oe.fail(a);let d=(yield*t.readFileString(i)).trim();if(!/^[a-f0-9]{64}$/.test(d))return yield*oe.fail(a);yield*t.chmod(i,384);let l=yield*$e.key($e.hmacSha256,_e.encode(d)),u=at.encode(yield*r.sign(l,_e.encode("web cookie")));return new Ne(d,u,n)});authorized(e,t){return e!==void 0&&mo(e,`Bearer ${this.token}`)||t!==void 0&&mo(t,this.cookie)}mint(e,t=Date.now()){return oe.gen({self:this},function*(){for(let[n,r]of this.codes)if(r.until<=t)this.codes.delete(n);if(this.codes.size>=64)return yield*oe.fail(new R({code:"too-large",message:"Too many unused browser sign-in codes, use an existing link or wait five minutes"}));let o=at.encode(yield*this.random.randomBytes(32));return this.codes.set(o,{path:e,until:t+Vn}),o})}consume(e,t,o=Date.now()){let n=this.codes.get(e);if(!n)return!1;if(n.until<=o)return this.codes.delete(e),!1;if(n.path!==t)return!1;return this.codes.delete(e),!0}}import{Config as dt,Data as Un,Effect as he,Option as Bn,Path as Hn}from"effect";var Le=4194304,yo=["# TODO","","Write one task per line under Tasks. Agents pick up each new line, plan it, and write their status under it. Keys say Cmd and Alt: on Windows and Linux use Ctrl for Cmd, and Alt is Option on a Mac. Delete the Example section once you know your way around.","","## Example","","- [ ] Fix the login redirect @high @today"," - **t0** M, building with Opus","- [ ] Pick a chart library for the stats page @due(friday)"," - **t0** waiting for you"," - **t0 question:** Recharts or Visx? You answer by typing under a question like this","- [x] Cmd-Enter starts a task, and Cmd-D or a click on its box marks it done"," Shift-Enter adds a note like this one, which moves and archives with its task","- [-] Cmd-Alt-Enter or Alt-click cancels a task","- [x] Cmd-Shift-A moves done and cancelled tasks to Archive","- [x] Type t, h, or d then Tab for the today, high, or due tag; s then Tab twice stamps a start time","- [x] Cmd-/ lists every key","","## Tasks","","- [ ] ",""].join(`
12
+ `);class lt extends Un.TaggedError("InvalidConfig"){}var zn=new URL("../web",import.meta.url),Ie=(e)=>dt.option(dt.String(e)).pipe(he.map(Bn.getOrUndefined)),xo=he.gen(function*(){let e=yield*Hn.Path,t=Number((yield*Ie("DO_IT_PORT"))??47100),o=Number((yield*Ie("DO_IT_IDLE_MS"))??1800000);if(!Number.isInteger(t)||t<0||t>65535)return yield*he.fail(new lt({message:"DO_IT_PORT must be between 0 and 65535"}));if(!Number.isFinite(o)||o<=0)return yield*he.fail(new lt({message:"DO_IT_IDLE_MS must be positive"}));let n=(yield*Ie("DO_IT_HOME"))??e.join(yield*dt.String("HOME"),".do-it"),r=(yield*Ie("DO_IT_WEB_DIST"))??(yield*e.fromFileUrl(zn));return{home:e.resolve(n),port:t,idleMs:o,webDist:e.resolve(r)}});var ys=new URL("../../../",import.meta.url),vo=he.succeed("36d2353.mw8d2srx");import{Cause as Eo,Context as Gn,Deferred as ut,Effect as p,Exit as Jn,Fiber as ht,FileSystem as Yn,Layer as Zn,Path as Qn,Scope as mt,Semaphore as wo}from"effect";import{Deferred as pt,Effect as G,Fiber as jn,FileSystem as Kn,Path as Xn,Stream as ft}from"effect";var So=G.fn("watchFile")(function*(e,t){let o=yield*Kn.FileSystem,n=yield*Xn.Path,r=n.basename(e),i=yield*pt.make(),s=yield*o.watch(n.dirname(e),{onReady:()=>pt.doneUnsafe(i,G.void)}).pipe(ft.filter((a)=>n.basename(a.path)===r),ft.debounce("50 millis"),ft.runForEach(()=>t),G.ignore,G.forkScoped);yield*t.pipe(G.delay("2 seconds"),G.forever,G.forkScoped),yield*G.race(pt.await(i),jn.await(s))});var qn=64,er="25 millis",tr="500 millis";function bo(e,t=1){if(e instanceof y){if(e.code==="conflict"&&e.conflict){let o=e.conflict;return new ue({latest:o.current,since:o.expected,diff:o.diff,forgotten:o.forgotten})}if(e.code==="forgotten")return new X({latest:t});return new R({code:e.code==="conflict"?"invalid":e.code,message:e.message})}if(e instanceof R||e instanceof N||e instanceof X||e instanceof ue)return e;return new R({code:"invalid",message:e instanceof Error?e.message:String(e)})}function ne(e,t){return p.try({try:e,catch:(o)=>bo(o,t?.())})}function To(e,t){return e.mtimeMs===t.mtimeMs&&e.ino===t.ino&&e.size===t.size}class gt{versions;log;context;scope;entries=new Map;opening=wo.makeUnsafe(1);totalWatchers=0;closing=!1;constructor(e,t,o,n){this.versions=e;this.log=t;this.context=o;this.scope=n}static make=p.fn("DocumentRegistry.make")(function*(e,t){return new gt(e,t,yield*p.context(),yield*p.scope)});disk(e){return p.provideContext(e,this.context)}logged(e,t){return t.pipe(p.catchCause((o)=>Eo.hasInterruptsOnly(o)?p.failCause(o):this.log.write(`${e}: ${String(Eo.squash(o))}`)))}validate(e){return this.disk(p.gen({self:this},function*(){let t=yield*Qn.Path,o=yield*Yn.FileSystem;if(!t.isAbsolute(e)||![".md",".markdown"].includes(t.extname(e).toLowerCase()))return yield*p.fail(new R({code:"invalid",message:"The path must be absolute and end in .md or .markdown"}));let n=t.resolve(e);if(!(yield*o.stat(t.dirname(n)).pipe(p.map((i)=>i.type==="Directory"),p.catch(()=>p.succeed(!1))))){if(this.entries.has(n)||this.versions.has(n))return yield*p.fail(new N({path:n}));return yield*p.fail(new R({code:"invalid",message:"The document's parent directory must exist"}))}return n}))}open(e,t=!1){return this.acquire(e,t,!1)}get(e){return p.map(this.open(e),({entry:t})=>t)}withEntry(e,t,o=!1){return this.withOpened(e,({entry:n})=>t(n),o)}withOpened(e,t,o=!1){return p.acquireUseRelease(this.acquire(e,o,!0),t,({entry:n})=>p.sync(()=>{n.waiters--,n.lastUsed=Date.now()}))}acquire(e,t,o){return p.gen({self:this},function*(){let n=yield*this.validate(e);while(!0){if(this.closing)return yield*p.fail(new R({code:"invalid",message:"The daemon is shutting down"}));let r=this.entries.get(n),i=r?yield*this.reuse(r):yield*this.opening.withPermit(p.suspend(()=>{let s=this.entries.get(n);return s?this.reuse(s):this.create(n,t)}));if(i.entry.closed)continue;if(o)i.entry.waiters++;return i}}).pipe(p.mapError((n)=>bo(n)))}status(){return[...this.entries.values()].map((e)=>({path:e.path,version:e.doc.version,watchers:e.watchers}))}reuse(e){return e.lastUsed=Date.now(),this.disk(ee(e.path)).pipe(p.flatMap((t)=>e.deleted||t===null?p.fail(new N({path:e.path})):p.succeed({entry:e,created:!1})))}create(e,t){let o=p.gen({self:this},function*(){let n=(yield*this.disk(ee(e)))===null;if(n&&!t){if(this.versions.has(e))return yield*p.fail(new N({path:e}));return yield*p.fail(new R({code:"invalid",message:"The document does not exist, open it with create enabled"}))}if(yield*this.makeRoom(),n)yield*this.disk(Oe(e,yo,{bom:!1,crlf:!1}));return{missing:n,file:yield*this.disk(fe(e))}});return p.uninterruptibleMask((n)=>p.gen({self:this},function*(){let{missing:r,file:i}=yield*n(o),s=yield*this.versions.open(e,i.text),a={path:e,doc:s,signal:ut.makeUnsafe(),format:i.format,stamp:i.stamp,watchers:0,waiters:0,lastUsed:Date.now(),deleted:!1,closed:!1,lock:wo.makeUnsafe(1),scope:yield*mt.fork(this.scope),writing:void 0,missing:void 0,hash:void 0};return this.entries.set(e,a),yield*this.disk(So(e,this.poll(a))).pipe(mt.provide(a.scope)),{entry:a,created:r}}))}makeRoom(){return p.gen({self:this},function*(){if(this.entries.size<qn)return;let e=[...this.entries.values()].filter((t)=>t.watchers===0&&t.waiters===0).sort((t,o)=>t.lastUsed-o.lastUsed);for(let t of e)if(yield*this.flush(t),yield*this.evict(t))return;return yield*p.fail(new R({code:"too-large",message:"At most 64 documents may be open, all available documents are busy"}))})}notify(e){let t=e.signal;e.signal=ut.makeUnsafe(),ut.doneUnsafe(t,p.void)}changed(e,t){if(e.doc.version===t)return p.void;return e.lastUsed=Date.now(),this.notify(e),p.andThen(this.versions.record(e.path,e.doc.version,e.doc.text),this.scheduleWrite(e))}hashOf(e,t,o){return p.suspend(()=>{if(e.hash?.version===t)return p.succeed(e.hash.value);return p.tap(this.versions.sha256(o),(n)=>p.sync(()=>{if(t>(e.hash?.version??0))e.hash={version:t,value:n}}))})}hashAt(e,t){return p.suspend(()=>{if(e.hash?.version===t)return p.succeed(e.hash.value);let o=e.doc.textAt(t);return o===null?p.succeed(null):this.hashOf(e,t,o)})}subscribe(e){if(e.deleted)throw new N({path:e.path});if(e.watchers>=32||this.totalWatchers>=256)throw new R({code:"too-large",message:"The document or daemon watch stream limit has been reached"});return e.watchers++,this.totalWatchers++,()=>{e.watchers--,this.totalWatchers--,e.lastUsed=Date.now()}}scheduleWrite(e){return p.suspend(()=>{if(e.writing||this.closing||e.deleted||e.closed||!e.doc.dirty)return p.void;let t=p.suspend(()=>(e.writing=void 0,this.flushNow(e)));return p.forkIn(p.delay(t,er),this.scope).pipe(p.map((o)=>{e.writing=o}))})}poll(e){return e.lock.withPermit(this.pollLocked(e))}pollLocked(e){return this.logged(`Could not merge ${e.path}`,p.gen({self:this},function*(){if(e.deleted||e.closed||this.closing)return;let t=yield*this.disk(ee(e.path));if(!t){if(!e.missing)e.missing=yield*p.forkIn(this.recheck(e),this.scope);return}if(e.missing){let o=e.missing;e.missing=void 0,yield*ht.interrupt(o)}if(!To(e.stamp,t)){let o=yield*this.disk(fe(e.path)),n=e.doc.version;e.doc.mergeFile(o.text),e.format=o.format,e.stamp=o.stamp,yield*this.changed(e,n),yield*this.versions.record(e.path,e.doc.version,e.doc.text),yield*this.compact(e)}yield*this.scheduleWrite(e)}).pipe(p.uninterruptible))}recheck(e){let t=p.suspend(()=>(e.missing=void 0,e.lock.withPermit(p.gen({self:this},function*(){if(e.deleted||e.closed||this.closing)return!1;if((yield*this.disk(ee(e.path)))===null)return!0;return yield*this.pollLocked(e),!1})).pipe(p.flatMap((o)=>o?this.deleted(e):p.void))));return this.logged(`Could not check ${e.path}`,p.delay(t,tr))}flush(e){return p.suspend(()=>{let t=e.writing;return e.writing=void 0,p.andThen(t?ht.interrupt(t):p.void,this.flushNow(e))})}flushNow(e){return e.lock.withPermit(this.flushLocked(e))}flushLocked(e){let t,o=p.gen({self:this},function*(){if(e.deleted||e.closed)return;let n=yield*this.disk(ee(e.path));if(!n){if(!this.closing)yield*this.pollLocked(e);return}if(!To(e.stamp,n)){let r=yield*this.disk(fe(e.path)),i=e.doc.version;if(e.doc.mergeFile(r.text),e.format=r.format,e.stamp=r.stamp,e.doc.version!==i)this.notify(e)}if(e.doc.dirty)t=e.doc.toWrite(),e.stamp=yield*this.disk(Oe(e.path,t.text,e.format)),e.doc.wrote(t),t=void 0;yield*this.versions.record(e.path,e.doc.version,e.doc.text),yield*this.compact(e)}).pipe(p.onError(()=>p.sync(()=>{if(t)e.doc.notWritten(t)})));return this.logged(`Could not write ${e.path}`,p.uninterruptible(o))}compact(e){let t=e.doc.compact();return t?this.log.write(`Compacted ${e.path} at version ${e.doc.version} (${t.reason}: ${t.ops} ops, ${t.peers} peers), kept versions ${t.oldest}-${e.doc.version}`):p.void}deleted(e){return p.suspend(()=>(e.deleted=!0,this.notify(e),this.closeEntry(e)))}evict(e){return e.lock.withPermit(p.suspend(()=>{if(e.closed)return p.succeed(!0);if(e.watchers>0||e.waiters>0||!e.deleted&&e.doc.dirty)return p.succeed(!1);return p.as(this.closeLocked(e),!0)}))}closeEntry(e){return e.lock.withPermit(this.closeLocked(e))}closeLocked(e){return p.suspend(()=>{if(e.closed)return p.void;e.closed=!0;let t=[e.writing,e.missing].filter((o)=>o!==void 0);if(e.writing=void 0,e.missing=void 0,this.entries.get(e.path)===e)this.entries.delete(e.path),e.doc.free();return p.andThen(ht.interruptAll(t),mt.close(e.scope,Jn.void))})}close(){return p.gen({self:this},function*(){if(this.closing)return;this.closing=!0;for(let e of[...this.entries.values()])yield*this.flush(e),e.deleted=!0,this.notify(e),yield*this.closeEntry(e);yield*this.versions.flush()})}}class re extends Gn.Service()("@do-it/server/Documents"){}function ko(e,t){return Zn.effect(re,p.acquireRelease(gt.make(e,t),(o)=>o.close()))}import{Cause as or,Deferred as Do,Effect as x,Stream as nr}from"effect";function rr(e){return e.kind==="agent"?{_tag:"Agent",name:e.name}:e.kind==="user"?{_tag:"User"}:{_tag:"File"}}function ir(e){return e._tag==="Agent"?{kind:"agent",name:e.name}:{kind:e._tag==="User"?"user":"file"}}function Ao(e,t,o){let n=e.doc.changes(t,o);return n?{...n,by:n.by.map(rr)}:null}function sr(e,t){return nr.fromPull(x.gen(function*(){let{entry:o,release:n}=yield*e.withEntry(t.path,(d)=>x.gen(function*(){if(t.section!==void 0)yield*ne(()=>F(d.doc.text,t.section));return{entry:d,release:yield*ne(()=>e.subscribe(d))}}));yield*x.addFinalizer(()=>x.sync(n));let r=!0,i=!1,s=t.since??o.doc.version;return x.gen(function*(){if(i)return yield*or.done();while(!0){let d=o.signal;if(r){r=!1;let l=t.since!==void 0&&(s>o.doc.version||s<o.doc.version&&o.doc.textAt(s)===null);if(!l&&t.since!==void 0&&t.sinceHash!==void 0)l=(yield*e.hashAt(o,s))!==t.sinceHash;if(t.since===void 0||l)s=o.doc.version;let{version:u,text:f}=o.doc;return[{_tag:l?"Resync":"Watching",version:u,hash:yield*e.hashOf(o,u,f),...t.text?{text:f}:{}}]}if(o.deleted)return i=!0,[{_tag:"Deleted"}];if(o.doc.version>s){let l=Ao(o,s,t.section);s=o.doc.version;let u=o.doc.text,f=yield*e.hashOf(o,s,u);return[l?{_tag:"Change",...l,hash:f,...t.text?{text:u}:{}}:{_tag:"Resync",version:s,hash:f,...t.text?{text:u}:{}}]}yield*Do.await(d)}})}))}function cr(e,t){return e.withEntry(t.path,(o)=>x.gen(function*(){if(t.baseHash!==void 0&&(yield*e.hashAt(o,t.base))!==t.baseHash)return yield*x.fail(new X({latest:o.doc.version}));if(o.deleted||o.closed)return yield*x.fail(new N({path:o.path}));let n=o.doc.version;yield*ne(()=>o.doc.userEdit(t.base,t.text),()=>o.doc.version);let{version:r,text:i}=o.doc;return yield*e.changed(o,n),{version:r,text:i,hash:yield*e.hashOf(o,r,i)}}))}function Ro(e,t){return Ce.toLayer(x.gen(function*(){let o=yield*re;return{status:()=>x.sync(()=>({pid:process.pid,appVersion:ae,port:t.port,startedAt:t.startedAt,build:t.build,documents:o.status()})).pipe(x.withSpan("rpc.status")),stop:x.fn("rpc.stop")(function*(){return yield*t.stop,{}}),open:(n)=>o.withOpened(n.path,({entry:r,created:i})=>x.gen(function*(){let s=r.doc.version,a=yield*e.mint(r.path);return{path:r.path,version:s,created:i,url:`http://127.0.0.1:${t.port}/auth?code=${a}&path=${encodeURIComponent(r.path)}`}}),n.create).pipe(x.withSpan("rpc.open")),read:(n)=>o.withEntry(n.path,(r)=>x.gen(function*(){let{version:i,text:s}=r.doc,a=n.section===void 0?null:yield*ne(()=>F(s,n.section));return{version:i,text:a?s.slice(a.from,a.to):s,hash:yield*o.hashOf(r,i,s)}})).pipe(x.withSpan("rpc.read")),sections:(n)=>o.withEntry(n.path,(r)=>x.sync(()=>({version:r.doc.version,sections:_(r.doc.text).map(({id:i,title:s,level:a,line:d})=>({id:i,title:s,level:a,line:d}))}))).pipe(x.withSpan("rpc.sections")),todos:(n)=>o.withEntry(n.path,(r)=>ne(()=>{let i=Q(r.doc.text);if(n.section===void 0)return{version:r.doc.version,todos:i};let s=F(r.doc.text,n.section),a=_(r.doc.text).find((d)=>d.from===s.to)?.line??1/0;return{version:r.doc.version,todos:i.filter((d)=>d.line>=s.line&&d.line<a)}})).pipe(x.withSpan("rpc.todos")),edit:(n)=>o.withEntry(n.path,(r)=>x.gen(function*(){let i=r.doc.version,s=yield*ne(()=>r.doc.agentEdit(ir(n.author),{version:n.version,section:n.section},(a)=>{let d=n.op;switch(d._tag){case"Replace":return tt(a,d.old,d.new,n.section);case"Append":return De(a,d.text,n.section);case"TodoAdd":return et(a,d.text,n.section);case"TodoSet":return qe(a,ke(a,d.target,n.section),d.state,new Date);case"TodoStart":return Qe(a,ke(a,d.target,n.section),"started",new Date)}}),()=>r.doc.version);return yield*o.changed(r,i),{...s,section:n.section??null}})).pipe(x.withSpan("rpc.edit")),sync:(n)=>cr(o,n).pipe(x.withSpan("rpc.sync")),diff:(n)=>o.withEntry(n.path,(r)=>x.gen(function*(){let i=Date.now()+n.wait*1000;while(!0){let s=r.signal;if(r.deleted)return yield*x.fail(new N({path:r.path}));let a=Ao(r,n.since);if(!a)return yield*x.fail(new X({latest:r.doc.version}));if(r.doc.version!==n.since)return a;let d=i-Date.now();if(d<=0)return yield*x.fail(new Pe({}));yield*Do.await(s).pipe(x.timeoutOption(d))}})).pipe(x.withSpan("rpc.diff")),watch:(n)=>sr(o,n)}}))}import{Data as ar,Effect as C,FileSystem as Oo}from"effect";import{SecureRandom as dr}from"effect/crypto";class yt extends ar.TaggedError("StateFileError"){}var me=C.fn("readJson")(function*(e,t=2097152){let o=yield*Oo.FileSystem,n=yield*C.scoped(C.gen(function*(){let r=yield*o.open(e,{flag:"r"}),i=new Uint8Array(t+1),s=0;while(s<=t){let a=yield*r.read(i.subarray(s));if(a===0)return i.subarray(0,s);s+=a}return yield*C.fail(new yt({message:`State file exceeds ${t} bytes`}))}));return yield*C.try({try:()=>JSON.parse(new TextDecoder().decode(n)),catch:(r)=>new yt({message:`State file is not JSON: ${String(r)}`})})}),Fo=C.fn("writeJson")(function*(e,t){let o=yield*Oo.FileSystem,n=`${e}.${dr.uuidV4Unsafe()}.tmp`,r=!1;yield*C.scoped(C.gen(function*(){let i=yield*o.open(n,{flag:"wx",mode:384});r=!0,yield*i.writeAll(new TextEncoder().encode(JSON.stringify(t)+`
13
+ `)),yield*i.sync})).pipe(C.andThen(o.rename(n,e)),C.onError(()=>r?C.ignore(o.remove(n)):C.void))});import{Effect as ge,FileSystem as lr,Option as pr,Path as fr,Semaphore as ur}from"effect";var hr=1048576,mr=16384;class Me{fs;path;lock=ur.makeUnsafe(1);constructor(e,t){this.fs=e;this.path=t}static make=ge.fn("Log.make")(function*(e){let t=yield*lr.FileSystem,o=yield*fr.Path;return new Me(t,o.join(e,"daemon.log"))});rotate(){return this.lock.withPermit(this.rotateUnlocked())}write(e){let{fs:t,path:o}=this;return this.lock.withPermit(ge.gen({self:this},function*(){yield*this.rotateUnlocked();let n=new TextEncoder().encode(`${new Date().toISOString()} ${e}
14
+ `);if(n.length<=mr)yield*t.writeFile(o,n,{flag:"a",mode:384})})).pipe(ge.ignore)}rotateUnlocked(){let{fs:e,path:t}=this;return ge.gen(function*(){let o=yield*e.stat(t).pipe(ge.option);if(pr.isNone(o)||Number(o.value.size)<hr)return;yield*e.copyFile(t,`${t}.old`),yield*e.truncate(t,0)})}}function Po(e){try{return process.kill(e,0),!0}catch(t){return t.code==="EPERM"}}import{Effect as Ve,FileSystem as gr,Option as yr,Path as xr}from"effect";import{HttpServerResponse as de,Mime as vr}from"effect/http";var Co=Ve.fn("staticResponse")(function*(e,t){let o=yield*gr.FileSystem,n=yield*xr.Path;if(!(yield*o.exists(n.resolve(e,"index.html"))))return de.html("<!doctype html><p>Run <code>bun run build:web</code> to build the web app.</p>");let r;try{r=decodeURIComponent(t)}catch{return de.text("Invalid path",{status:400})}if(r.includes("\x00")||r.includes("\\"))return de.text("Invalid path",{status:400});let i=yield*o.realPath(e),s=n.resolve(i,`.${r}`);if(s!==i&&!s.startsWith(i+n.sep))return de.text("Forbidden",{status:403});if(!(yield*o.stat(s).pipe(Ve.map((d)=>d.type==="File"),Ve.catch(()=>Ve.succeed(!1)))))s=n.resolve(i,"index.html");if(s=yield*o.realPath(s),!s.startsWith(i+n.sep))return de.text("Forbidden",{status:403});return yield*de.file(s,{contentType:yr.getOrElse(vr.getType(s),()=>"application/octet-stream")})});import{Effect as k,Fiber as Sr,Path as Er}from"effect";import{Digest as $o}from"effect/crypto";var wr=256,Tr=new TextEncoder;class We{path;log;digest;context;scope;entries=new Map;timer;constructor(e,t,o,n,r){this.path=e;this.log=t;this.digest=o;this.context=n;this.scope=r}static make=k.fn("Versions.make")(function*(e,t){let o=yield*Er.Path,n=new We(o.join(e,"versions.json"),t,yield*$o.Digest,yield*k.context(),yield*k.scope),r=yield*me(n.path).pipe(k.catch((i)=>K(i)?k.succeed(void 0):k.as(t.write("Could not read versions.json, starting with fresh version records"),void 0)));if(r&&typeof r==="object"&&!Array.isArray(r))for(let[i,s]of Object.entries(r)){let a=s;if(a&&Number.isSafeInteger(a.version)&&a.version>0&&typeof a.sha256==="string"&&/^[a-f0-9]{64}$/.test(a.sha256))n.entries.set(i,a),n.trim()}return n});sha256(e){return this.digest.digestHex($o.sha256,Tr.encode(e))}has(e){return this.entries.has(e)}open(e,t){return k.gen({self:this},function*(){let o=this.entries.get(e),n;if(o&&o.sha256!==(yield*this.sha256(t)))n=ce.open(t===""?`
15
+ `:"",o.version),n.mergeFile(t),n.compact(0,0,1);else n=ce.open(t,o?.version);return yield*this.record(e,n.version,t),n})}record(e,t,o){return k.gen({self:this},function*(){let n=yield*this.sha256(o);if(this.entries.delete(e),this.entries.set(e,{version:t,sha256:n}),this.trim(),!this.timer){let r=k.suspend(()=>(this.timer=void 0,this.save()));this.timer=yield*k.forkIn(k.delay(r,"1 second"),this.scope)}})}flush(){return k.suspend(()=>{let e=this.timer;return this.timer=void 0,k.andThen(e?Sr.interrupt(e):k.void,this.save())})}trim(){while(this.entries.size>wr)this.entries.delete(this.entries.keys().next().value)}save(){return Fo(this.path,Object.fromEntries(this.entries)).pipe(k.provideContext(this.context),k.catch((e)=>this.log.write(`Could not save versions: ${String(e)}`)))}}var Cr=new TextEncoder,Io=m.fn("makeServer")(function*(e){let t=e??(yield*xo),o=yield*kr.FileSystem,n=yield*Dr.Path;yield*o.makeDirectory(t.home,{recursive:!0,mode:448}),yield*o.chmod(t.home,448);let r=yield*Me.make(t.home),i=yield*Ne.make(t.home),s=yield*We.make(t.home,r),a=yield*xt.make(),d=new AbortController,l=yield*m.scope,u=!1,f=yield*vo,h={port:t.port,startedAt:Date.now(),build:f,stop:m.suspend(()=>{if(u)return m.void;return u=!0,m.asVoid(m.forkIn(m.delay(xt.succeed(a,void 0),"50 millis"),l))})},g={hostname:"127.0.0.1",port:t.port,maxRequestBodySize:Le,disablePreemptiveShutdown:!0},w=n.join(t.home,"daemon.json"),b;yield*m.addFinalizer(()=>m.suspend(()=>{let v=b;if(v===void 0)return m.void;return m.gen(function*(){yield*me(w,4096).pipe(m.flatMap((J)=>{let z=J;return z.pid===v.pid&&z.startedAt===v.startedAt&&z.port===v.port?o.remove(w):m.void}),m.ignore),yield*r.write("Daemon stopped")})}));let O=yield*_o.make(g).pipe(m.catchCause((v)=>t.port===0?m.failCause(v):_o.make({...g,port:0})));if(!("port"in O.address))return yield*m.die(Error("Expected an IP listening address"));h.port=O.address.port;let Ue=ko(s,r),xe=yield*ye.build(Ue),ve=yield*re.pipe(m.provideContext(xe)),Mo=Pr.layerHttp({group:Ce,path:"/rpc",protocol:"http",streamBufferSize:1,concurrency:512}).pipe(ye.provide(Ro(i,h)),ye.provide(ye.succeed(re,ve)),ye.provide(Fr.layerNdjson)),Vo=yield*Or.toHttpEffect(Mo),Se=0,Be=Date.now(),Wo=m.gen(function*(){let v=yield*No.HttpServerRequest,J=v.headers.host,z=v.headers.origin,Et=[`127.0.0.1:${h.port}`,`localhost:${h.port}`];if(!J||!Et.includes(J)||z!==void 0&&!Et.map((W)=>`http://${W}`).includes(z))return D.text("Forbidden",{status:403});if(Be=Date.now(),Se>=512)return D.text("Too many requests",{status:503});Se++,yield*m.addFinalizer(()=>m.sync(()=>{Se--,Be=Date.now()}));let Y=new URL(v.url,`http://127.0.0.1:${h.port}`);if(v.method==="GET"&&Y.pathname==="/health")return D.jsonUnsafe({ok:!0,pid:process.pid,appVersion:ae,build:f});if(v.method==="GET"&&Y.pathname==="/auth"){let W=Y.searchParams.get("path")??"";if(!i.consume(Y.searchParams.get("code")??"",W))return D.text("<!doctype html><p>This link is invalid or expired. Run <code>do-it open</code> again.</p>",{status:401,contentType:"text/html"});return D.redirect(`/?path=${encodeURIComponent(W)}`,{status:303,headers:{"set-cookie":`do_it=${i.cookie}; HttpOnly; SameSite=Strict; Path=/`,"cache-control":"no-store","referrer-policy":"no-referrer"}})}if(Y.pathname==="/rpc"||Y.pathname==="/rpc/"){if(!i.authorized(v.headers.authorization,v.cookies.do_it))return D.text("Unauthorized",{status:401});if(v.method!=="POST")return D.text("Use POST",{status:405});if(Number(v.headers["content-length"]??0)>Le)return D.text("Request body is too large",{status:413});let W=yield*v.text.pipe(m.result);if(W._tag==="Failure")return D.text("Could not read request body",{status:413});if(Cr.encode(W.success).length>Le)return D.text("Request body is too large",{status:413});let Bo=0;for(let je=0;je<W.success.length;je++)if(W.success.charCodeAt(je)===10&&++Bo>64)return D.text("Too many RPC messages",{status:413});return yield*Vo}if(v.method!=="GET"&&v.method!=="HEAD")return D.text("Not found",{status:404});return yield*Co(t.webDist,Y.pathname)});yield*O.serve(Wo.pipe(m.flatMap((v)=>m.gen(function*(){if(v.body._tag!=="Stream")return v;let J=yield*No.HttpServerRequest;if(D.omitsBody(v,J.method==="HEAD"))return v;let z=yield*m.context();return D.raw(D.toWeb(Rr.scopeTransferToStream(v),{context:z,signal:AbortSignal.any([br.toBunServerRequest(J).signal,d.signal])}))}))));let He={pid:process.pid,port:h.port,appVersion:ae,build:f,startedAt:h.startedAt},ze=yield*me(w,4096).pipe(m.map((v)=>v),m.catch(()=>m.succeed(void 0)));if(ze?.pid&&ze.pid!==process.pid&&Po(ze.pid))return yield*m.fail(Error("A daemon is already running for this home"));yield*o.remove(w).pipe(m.catchIf(K,()=>m.void)),yield*o.writeFileString(w,JSON.stringify(He),{mode:384,flag:"wx"}),b=He;let Uo=m.gen(function*(){if(yield*m.ignore(r.rotate()),Se===0&&ve.totalWatchers===0&&Date.now()-Be>=t.idleMs)yield*h.stop});return yield*m.forkScoped(m.repeat(Uo,Ar.spaced(Math.max(10,Math.min(1000,t.idleMs/4))))),yield*m.addFinalizer(()=>m.gen(function*(){d.abort(),yield*ve.close()})),yield*r.write(`Daemon listening on 127.0.0.1:${h.port}`),{...He,home:t.home,auth:i,documents:ve,stopped:xt.await(a),stop:h.stop}});import{BunCrypto as vt,BunHttpPlatform as $r,BunServices as _r}from"@effect/platform-bun";import{Layer as Nr}from"effect";var Lo=Nr.mergeAll(_r.layer,vt.layerDigest,vt.layerMac,vt.layerSecureRandom,$r.layer);Ir.runMain(St.scoped(St.gen(function*(){yield*(yield*Io()).stopped})).pipe(St.provide(Lo)),{teardown:(e,t)=>t(Mr.isFailure(e)&&!Lr.hasInterruptsOnly(e.cause)?1:0)});