erdraft 0.0.0-stage → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +204 -2
  3. package/dist/chunk-SSLBX2OF.js +2162 -0
  4. package/dist/chunk-VSN3XJPO.js +99 -0
  5. package/dist/cli.js +507 -0
  6. package/dist/inspect/index.js +373 -0
  7. package/dist/inspect/logger.js +64 -0
  8. package/dist/read-schema-child.js +238 -0
  9. package/dist/studio-config-TIVT4MNY.js +32 -0
  10. package/dist/types/core/fields.d.ts +39 -0
  11. package/dist/types/inspect/field-of.d.ts +3 -0
  12. package/dist/types/inspect/holds-value.d.ts +2 -0
  13. package/dist/types/inspect/inspect-logger.d.ts +2 -0
  14. package/dist/types/inspect/inspector.d.ts +5 -0
  15. package/dist/types/inspect/listen-on-document.d.ts +5 -0
  16. package/dist/types/inspect/read-write.d.ts +5 -0
  17. package/dist/types/inspect/use-field-changes.d.ts +6 -0
  18. package/dist/types/inspect/watch-storage.d.ts +8 -0
  19. package/dist/window/apple-touch-icon.png +0 -0
  20. package/dist/window/assets/index-B5sqm8jk.css +2 -0
  21. package/dist/window/assets/index-Dk6ZQwtE.js +132 -0
  22. package/dist/window/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  23. package/dist/window/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  24. package/dist/window/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  25. package/dist/window/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  26. package/dist/window/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  27. package/dist/window/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  28. package/dist/window/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  29. package/dist/window/favicon.ico +0 -0
  30. package/dist/window/favicon.png +0 -0
  31. package/dist/window/favicon.svg +1 -0
  32. package/dist/window/index.html +23 -0
  33. package/package.json +91 -3
  34. package/skill/SKILL.md +47 -0
@@ -0,0 +1,2162 @@
1
+ import {
2
+ checkSchema,
3
+ columnChangeSchema,
4
+ columnSchema,
5
+ enumSchema,
6
+ foreignKeyName,
7
+ foreignKeySchema,
8
+ indexName,
9
+ indexSchema,
10
+ namedTable,
11
+ parseSchema,
12
+ schemaSchema,
13
+ tableSchema
14
+ } from "./chunk-VSN3XJPO.js";
15
+
16
+ // src/mcp/create-server.ts
17
+ import { z as z5 } from "zod";
18
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
19
+
20
+ // src/core/store.ts
21
+ import { mkdir as mkdir2, readFile, rename as rename2, writeFile as writeFile2 } from "fs/promises";
22
+ import { randomUUID } from "crypto";
23
+ import { join as join2 } from "path";
24
+ import { z as z3 } from "zod";
25
+
26
+ // src/core/operations.ts
27
+ import { z } from "zod";
28
+ var operationSchema = z.discriminatedUnion("op", [
29
+ z.object({ op: z.literal("add_table"), table: tableSchema }),
30
+ z.object({ op: z.literal("drop_table"), table: z.string().min(1) }),
31
+ z.object({
32
+ op: z.literal("rename_table"),
33
+ table: z.string().min(1),
34
+ to: z.string().min(1)
35
+ }),
36
+ z.object({
37
+ op: z.literal("add_column"),
38
+ table: z.string().min(1),
39
+ column: columnSchema
40
+ }),
41
+ z.object({
42
+ op: z.literal("drop_column"),
43
+ table: z.string().min(1),
44
+ column: z.string().min(1)
45
+ }),
46
+ z.object({
47
+ op: z.literal("alter_column"),
48
+ table: z.string().min(1),
49
+ column: z.string().min(1),
50
+ set: columnChangeSchema
51
+ }),
52
+ z.object({
53
+ op: z.literal("add_foreign_key"),
54
+ table: z.string().min(1),
55
+ foreignKey: foreignKeySchema
56
+ }),
57
+ z.object({
58
+ op: z.literal("drop_foreign_key"),
59
+ table: z.string().min(1),
60
+ name: z.string().min(1)
61
+ }),
62
+ z.object({
63
+ op: z.literal("add_index"),
64
+ table: z.string().min(1),
65
+ index: indexSchema
66
+ }),
67
+ z.object({
68
+ op: z.literal("drop_index"),
69
+ table: z.string().min(1),
70
+ name: z.string().min(1)
71
+ }),
72
+ z.object({
73
+ op: z.literal("add_check"),
74
+ table: z.string().min(1),
75
+ check: checkSchema
76
+ }),
77
+ z.object({
78
+ op: z.literal("drop_check"),
79
+ table: z.string().min(1),
80
+ name: z.string().min(1)
81
+ }),
82
+ z.object({ op: z.literal("add_enum"), enum: enumSchema }),
83
+ z.object({
84
+ op: z.literal("set_enum_values"),
85
+ enum: z.string().min(1),
86
+ values: z.array(z.string().min(1)).min(1)
87
+ }),
88
+ z.object({ op: z.literal("drop_enum"), enum: z.string().min(1) })
89
+ ]);
90
+
91
+ // src/core/apply-operations.ts
92
+ var DraftError = class extends Error {
93
+ constructor(step, message) {
94
+ super(`Step ${step}: ${message}`);
95
+ this.step = step;
96
+ this.name = "DraftError";
97
+ }
98
+ step;
99
+ };
100
+ function tableIn(schema, name, step) {
101
+ const table = schema.tables.find((each) => each.name === name);
102
+ if (!table) {
103
+ throw new DraftError(step, `there is no table "${name}"`);
104
+ }
105
+ return table;
106
+ }
107
+ function columnIn(table, name, step) {
108
+ const column = table.columns.find((each) => each.name === name);
109
+ if (!column) {
110
+ throw new DraftError(
111
+ step,
112
+ `there is no column "${name}" in "${table.name}"`
113
+ );
114
+ }
115
+ return column;
116
+ }
117
+ function enumIn(schema, name, step) {
118
+ const found = schema.enums.find((each) => each.name === name);
119
+ if (!found) {
120
+ throw new DraftError(step, `there is no enum "${name}"`);
121
+ }
122
+ return found;
123
+ }
124
+ function refuseTaken(taken, what, step) {
125
+ if (taken) {
126
+ throw new DraftError(step, `${what} already exists`);
127
+ }
128
+ }
129
+ function changedColumn(column, set) {
130
+ const { default: defaultValue, note, shape, ...rest } = set;
131
+ return {
132
+ ...column,
133
+ ...rest,
134
+ default: defaultValue === void 0 ? column.default : defaultValue ?? void 0,
135
+ note: note === void 0 ? column.note : note ?? void 0,
136
+ shape: shape === void 0 ? column.shape : shape ?? void 0
137
+ };
138
+ }
139
+ function rename(map, from, to) {
140
+ const original = map[from] ?? from;
141
+ delete map[from];
142
+ map[to] = original;
143
+ }
144
+ function apply(schema, renames, operation, step) {
145
+ switch (operation.op) {
146
+ case "add_table": {
147
+ const table = namedTable(operation.table);
148
+ refuseTaken(
149
+ schema.tables.some((each) => each.name === table.name),
150
+ `The table "${table.name}"`,
151
+ step
152
+ );
153
+ for (const foreignKey of table.foreignKeys) {
154
+ checkForeignKey(schema, table, foreignKey, step);
155
+ }
156
+ for (const [at, index] of table.indexes.entries()) {
157
+ checkIndex(table, index, step, table.indexes.slice(0, at));
158
+ }
159
+ for (const [at, check] of table.checks.entries()) {
160
+ refuseTaken(
161
+ table.checks.slice(0, at).some((each) => each.name === check.name),
162
+ `The check "${check.name}"`,
163
+ step
164
+ );
165
+ }
166
+ schema.tables.push(table);
167
+ return;
168
+ }
169
+ case "drop_table": {
170
+ tableIn(schema, operation.table, step);
171
+ schema.tables = schema.tables.filter(
172
+ (each) => each.name !== operation.table
173
+ );
174
+ for (const table of schema.tables) {
175
+ table.foreignKeys = table.foreignKeys.filter(
176
+ (foreignKey) => foreignKey.table !== operation.table
177
+ );
178
+ }
179
+ delete renames.columns[operation.table];
180
+ delete renames.tables[operation.table];
181
+ return;
182
+ }
183
+ case "rename_table": {
184
+ const table = tableIn(schema, operation.table, step);
185
+ refuseTaken(
186
+ schema.tables.some((each) => each.name === operation.to),
187
+ `The table "${operation.to}"`,
188
+ step
189
+ );
190
+ table.name = operation.to;
191
+ for (const each of schema.tables) {
192
+ for (const foreignKey of each.foreignKeys) {
193
+ if (foreignKey.table === operation.table) {
194
+ foreignKey.table = operation.to;
195
+ }
196
+ }
197
+ }
198
+ rename(renames.tables, operation.table, operation.to);
199
+ const columns = renames.columns[operation.table];
200
+ if (columns) {
201
+ delete renames.columns[operation.table];
202
+ renames.columns[operation.to] = columns;
203
+ }
204
+ return;
205
+ }
206
+ case "add_column": {
207
+ const table = tableIn(schema, operation.table, step);
208
+ refuseTaken(
209
+ table.columns.some((each) => each.name === operation.column.name),
210
+ `The column "${operation.column.name}" in "${table.name}"`,
211
+ step
212
+ );
213
+ table.columns.push(operation.column);
214
+ return;
215
+ }
216
+ case "drop_column": {
217
+ const table = tableIn(schema, operation.table, step);
218
+ columnIn(table, operation.column, step);
219
+ table.columns = table.columns.filter(
220
+ (each) => each.name !== operation.column
221
+ );
222
+ table.foreignKeys = table.foreignKeys.filter(
223
+ (foreignKey) => !foreignKey.columns.includes(operation.column)
224
+ );
225
+ table.indexes = table.indexes.filter(
226
+ (index) => !index.columns.includes(operation.column)
227
+ );
228
+ for (const each of schema.tables) {
229
+ each.foreignKeys = each.foreignKeys.filter(
230
+ (foreignKey) => foreignKey.table !== table.name || !foreignKey.foreignColumns.includes(operation.column)
231
+ );
232
+ }
233
+ delete renames.columns[table.name]?.[operation.column];
234
+ return;
235
+ }
236
+ case "alter_column": {
237
+ const table = tableIn(schema, operation.table, step);
238
+ const column = columnIn(table, operation.column, step);
239
+ const to = operation.set.name;
240
+ if (to !== void 0 && to !== column.name) {
241
+ refuseTaken(
242
+ table.columns.some((each) => each.name === to),
243
+ `The column "${to}" in "${table.name}"`,
244
+ step
245
+ );
246
+ const swap = (name) => name === column.name ? to : name;
247
+ for (const foreignKey of table.foreignKeys) {
248
+ foreignKey.columns = foreignKey.columns.map(swap);
249
+ }
250
+ for (const index of table.indexes) {
251
+ index.columns = index.columns.map(swap);
252
+ }
253
+ for (const each of schema.tables) {
254
+ for (const foreignKey of each.foreignKeys) {
255
+ if (foreignKey.table === table.name) {
256
+ foreignKey.foreignColumns = foreignKey.foreignColumns.map(swap);
257
+ }
258
+ }
259
+ }
260
+ const columns = renames.columns[table.name] ?? {};
261
+ renames.columns[table.name] = columns;
262
+ rename(columns, column.name, to);
263
+ }
264
+ table.columns = table.columns.map(
265
+ (each) => each === column ? changedColumn(column, operation.set) : each
266
+ );
267
+ return;
268
+ }
269
+ case "add_foreign_key": {
270
+ const table = tableIn(schema, operation.table, step);
271
+ const foreignKey = {
272
+ ...operation.foreignKey,
273
+ name: foreignKeyName(table.name, operation.foreignKey)
274
+ };
275
+ refuseTaken(
276
+ table.foreignKeys.some((each) => each.name === foreignKey.name),
277
+ `The foreign key "${foreignKey.name}"`,
278
+ step
279
+ );
280
+ checkForeignKey(schema, table, foreignKey, step);
281
+ table.foreignKeys.push(foreignKey);
282
+ return;
283
+ }
284
+ case "drop_foreign_key": {
285
+ const table = tableIn(schema, operation.table, step);
286
+ if (!table.foreignKeys.some((each) => each.name === operation.name)) {
287
+ throw new DraftError(
288
+ step,
289
+ `there is no foreign key "${operation.name}" in "${table.name}"`
290
+ );
291
+ }
292
+ table.foreignKeys = table.foreignKeys.filter(
293
+ (each) => each.name !== operation.name
294
+ );
295
+ return;
296
+ }
297
+ case "add_index": {
298
+ const table = tableIn(schema, operation.table, step);
299
+ const index = {
300
+ ...operation.index,
301
+ name: indexName(table.name, operation.index)
302
+ };
303
+ checkIndex(table, index, step, table.indexes);
304
+ table.indexes.push(index);
305
+ return;
306
+ }
307
+ case "drop_index": {
308
+ const table = tableIn(schema, operation.table, step);
309
+ if (!table.indexes.some((each) => each.name === operation.name)) {
310
+ throw new DraftError(
311
+ step,
312
+ `there is no index "${operation.name}" in "${table.name}"`
313
+ );
314
+ }
315
+ table.indexes = table.indexes.filter(
316
+ (each) => each.name !== operation.name
317
+ );
318
+ return;
319
+ }
320
+ case "add_check": {
321
+ const table = tableIn(schema, operation.table, step);
322
+ refuseTaken(
323
+ table.checks.some((each) => each.name === operation.check.name),
324
+ `The check "${operation.check.name}"`,
325
+ step
326
+ );
327
+ table.checks.push(operation.check);
328
+ return;
329
+ }
330
+ case "drop_check": {
331
+ const table = tableIn(schema, operation.table, step);
332
+ if (!table.checks.some((each) => each.name === operation.name)) {
333
+ throw new DraftError(
334
+ step,
335
+ `there is no check "${operation.name}" in "${table.name}"`
336
+ );
337
+ }
338
+ table.checks = table.checks.filter((each) => each.name !== operation.name);
339
+ return;
340
+ }
341
+ case "add_enum": {
342
+ refuseTaken(
343
+ schema.enums.some((each) => each.name === operation.enum.name),
344
+ `The enum "${operation.enum.name}"`,
345
+ step
346
+ );
347
+ schema.enums.push(operation.enum);
348
+ return;
349
+ }
350
+ case "set_enum_values": {
351
+ enumIn(schema, operation.enum, step).values = operation.values;
352
+ return;
353
+ }
354
+ case "drop_enum": {
355
+ enumIn(schema, operation.enum, step);
356
+ schema.enums = schema.enums.filter((each) => each.name !== operation.enum);
357
+ return;
358
+ }
359
+ }
360
+ }
361
+ function checkForeignKey(schema, table, foreignKey, step) {
362
+ for (const name of foreignKey.columns) {
363
+ columnIn(table, name, step);
364
+ }
365
+ const target = foreignKey.table === table.name ? table : tableIn(schema, foreignKey.table, step);
366
+ for (const name of foreignKey.foreignColumns) {
367
+ columnIn(target, name, step);
368
+ }
369
+ if (foreignKey.columns.length !== foreignKey.foreignColumns.length) {
370
+ throw new DraftError(
371
+ step,
372
+ `the foreign key "${foreignKey.name}" has ${foreignKey.columns.length} columns and points to ${foreignKey.foreignColumns.length}`
373
+ );
374
+ }
375
+ }
376
+ function checkIndex(table, index, step, before) {
377
+ refuseTaken(
378
+ before.some((each) => each.name === index.name),
379
+ `The index "${index.name}"`,
380
+ step
381
+ );
382
+ for (const name of index.columns) {
383
+ columnIn(table, name, step);
384
+ }
385
+ }
386
+ function applyOperations(base, operations2) {
387
+ const schema = structuredClone(base);
388
+ const renames = { tables: {}, columns: {} };
389
+ for (const [index, operation] of structuredClone(operations2).entries()) {
390
+ apply(schema, renames, operation, index + 1);
391
+ }
392
+ return { schema, renames };
393
+ }
394
+
395
+ // src/core/fields.ts
396
+ import { z as z2 } from "zod";
397
+ var STORAGE_AREAS = ["local", "session"];
398
+ var destinationSchema = z2.discriminatedUnion("kind", [
399
+ z2.object({
400
+ kind: z2.literal("column"),
401
+ table: z2.string(),
402
+ column: z2.string()
403
+ }),
404
+ z2.object({
405
+ kind: z2.literal("storage"),
406
+ area: z2.enum(STORAGE_AREAS),
407
+ key: z2.string()
408
+ }),
409
+ z2.object({ kind: z2.literal("none") })
410
+ ]);
411
+ var fieldSchema = z2.object({
412
+ page: z2.string().min(1),
413
+ label: z2.string().min(1),
414
+ destination: destinationSchema,
415
+ tracedAt: z2.string()
416
+ });
417
+ function describeDestination(destination) {
418
+ if (destination.kind === "column") {
419
+ return `${destination.table}.${destination.column}`;
420
+ }
421
+ if (destination.kind === "storage") {
422
+ return `${destination.area}Storage "${destination.key}"`;
423
+ }
424
+ return "kept nowhere since it changed: not saved yet, or kept on the page only";
425
+ }
426
+ function describeFields(fields) {
427
+ if (fields.length === 0) {
428
+ return "No field traced yet: in the project's app, in development, Erdraft's inspector (Alt+Shift+D) traces a field clicked.";
429
+ }
430
+ const pages = [...new Set(fields.map((field) => field.page))];
431
+ return pages.flatMap((page) => [
432
+ page,
433
+ ...fields.filter((field) => field.page === page).map(
434
+ (field) => ` ${field.label} \u2192 ${describeDestination(field.destination)}`
435
+ )
436
+ ]).join("\n");
437
+ }
438
+
439
+ // src/core/locked.ts
440
+ import { mkdir, writeFile, stat, rm } from "fs/promises";
441
+ import { join } from "path";
442
+ var LOCK_FILE = ".lock";
443
+ var LOCK_STEP = 20;
444
+ var LOCK_STALE = 1e4;
445
+ async function ageOf(path) {
446
+ try {
447
+ return Date.now() - (await stat(path)).mtimeMs;
448
+ } catch {
449
+ return 0;
450
+ }
451
+ }
452
+ async function locked(folder, change) {
453
+ const path = join(folder, LOCK_FILE);
454
+ await mkdir(folder, { recursive: true });
455
+ for (; ; ) {
456
+ try {
457
+ await writeFile(path, String(process.pid), { flag: "wx" });
458
+ break;
459
+ } catch (error) {
460
+ const held = error instanceof Error && "code" in error && error.code === "EEXIST";
461
+ if (!held) {
462
+ throw error;
463
+ }
464
+ if (await ageOf(path) > LOCK_STALE) {
465
+ await rm(path, { force: true });
466
+ } else {
467
+ await new Promise((done) => setTimeout(done, LOCK_STEP));
468
+ }
469
+ }
470
+ }
471
+ try {
472
+ return await change();
473
+ } finally {
474
+ await rm(path, { force: true });
475
+ }
476
+ }
477
+
478
+ // src/core/store.ts
479
+ var DRAFT_STATUSES = ["proposed", "adopted", "discarded"];
480
+ var DECISION_SECONDS = 30;
481
+ var baseSchema = z3.object({
482
+ source: z3.string(),
483
+ readAt: z3.string(),
484
+ schema: schemaSchema
485
+ });
486
+ var draftSchema = z3.object({
487
+ id: z3.string(),
488
+ title: z3.string(),
489
+ summary: z3.string(),
490
+ group: z3.string().optional(),
491
+ status: z3.enum(DRAFT_STATUSES),
492
+ operations: z3.array(operationSchema),
493
+ createdAt: z3.string(),
494
+ updatedAt: z3.string()
495
+ });
496
+ var viewSchema = z3.object({
497
+ draft: z3.string().nullable().optional(),
498
+ table: z3.string().nullable().optional(),
499
+ column: z3.string().nullable().optional(),
500
+ related: z3.boolean().optional(),
501
+ at: z3.string()
502
+ });
503
+ var fieldsSchema = z3.array(fieldSchema);
504
+ var placesSchema = z3.record(
505
+ z3.string(),
506
+ z3.object({ x: z3.number(), y: z3.number() })
507
+ );
508
+ var layoutsSchema = z3.record(z3.string(), placesSchema);
509
+ var BASE_FILE = "base.json";
510
+ var DRAFTS_FILE = "drafts.json";
511
+ var VIEW_FILE = "view.json";
512
+ var LAYOUTS_FILE = "layouts.json";
513
+ var SEQUENCE_FILE = "sequence.json";
514
+ var FIELDS_FILE = "fields.json";
515
+ async function readJson(path) {
516
+ try {
517
+ return JSON.parse(await readFile(path, "utf8"));
518
+ } catch (error) {
519
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
520
+ return void 0;
521
+ }
522
+ throw error;
523
+ }
524
+ }
525
+ function pause(ms, signal) {
526
+ return new Promise((resolve) => {
527
+ const timer = setTimeout(done, ms);
528
+ function done() {
529
+ clearTimeout(timer);
530
+ signal?.removeEventListener("abort", done);
531
+ resolve();
532
+ }
533
+ signal?.addEventListener("abort", done, { once: true });
534
+ });
535
+ }
536
+ async function writeJson(path, value) {
537
+ const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
538
+ await writeFile2(temporary, `${JSON.stringify(value, null, 2)}
539
+ `);
540
+ await rename2(temporary, path);
541
+ }
542
+ function store(folder, workspace = "default") {
543
+ const own = join2(folder, "workspaces", workspace);
544
+ const basePath = join2(own, BASE_FILE);
545
+ const draftsPath = join2(folder, DRAFTS_FILE);
546
+ const viewPath = join2(own, VIEW_FILE);
547
+ const layoutsPath = join2(folder, LAYOUTS_FILE);
548
+ const sequencePath = join2(folder, SEQUENCE_FILE);
549
+ const fieldsPath = join2(folder, FIELDS_FILE);
550
+ async function readLastNumber() {
551
+ const value = await readJson(sequencePath);
552
+ return value === void 0 ? 0 : z3.object({ last: z3.number() }).parse(value).last;
553
+ }
554
+ async function readLayouts() {
555
+ const value = await readJson(layoutsPath);
556
+ return value === void 0 ? {} : layoutsSchema.parse(value);
557
+ }
558
+ async function readAllFields() {
559
+ const value = await readJson(fieldsPath);
560
+ return value === void 0 ? [] : fieldsSchema.parse(value);
561
+ }
562
+ async function readBase() {
563
+ const value = await readJson(basePath);
564
+ if (value === void 0) {
565
+ return null;
566
+ }
567
+ const base = baseSchema.parse(value);
568
+ return { ...base, schema: parseSchema(base.schema) };
569
+ }
570
+ async function readDrafts() {
571
+ const value = await readJson(draftsPath);
572
+ return value === void 0 ? [] : z3.array(draftSchema).parse(value);
573
+ }
574
+ async function writeDrafts(drafts) {
575
+ await mkdir2(folder, { recursive: true });
576
+ await writeJson(draftsPath, drafts);
577
+ }
578
+ async function requireBase() {
579
+ const base = await readBase();
580
+ if (!base) {
581
+ throw new Error("The schema has not been read yet: run `erdraft read`");
582
+ }
583
+ return base;
584
+ }
585
+ function draftIn(drafts, id) {
586
+ const draft = drafts.find((each) => each.id === id);
587
+ if (!draft) {
588
+ throw new Error(`There is no draft "${id}"`);
589
+ }
590
+ return draft;
591
+ }
592
+ return {
593
+ folder,
594
+ async read() {
595
+ const view = await readJson(viewPath);
596
+ return {
597
+ base: await readBase(),
598
+ drafts: await readDrafts(),
599
+ view: view === void 0 ? null : viewSchema.parse(view),
600
+ layouts: await readLayouts()
601
+ };
602
+ },
603
+ // The cards moved where the window showed them, or none (the layout set back to the diagram's own)
604
+ async saveLayout(key, places) {
605
+ await locked(folder, async () => {
606
+ const layouts = await readLayouts();
607
+ if (Object.keys(places).length > 0) {
608
+ layouts[key] = places;
609
+ } else {
610
+ delete layouts[key];
611
+ }
612
+ await mkdir2(folder, { recursive: true });
613
+ await writeJson(layoutsPath, layouts);
614
+ });
615
+ },
616
+ // The window asked to show something, now
617
+ async show(view) {
618
+ const asked = { ...view, at: (/* @__PURE__ */ new Date()).toISOString() };
619
+ await mkdir2(own, { recursive: true });
620
+ await writeJson(viewPath, asked);
621
+ return asked;
622
+ },
623
+ // The app's fields traced, on every page, or on one
624
+ async readFields(page) {
625
+ const fields = await readAllFields();
626
+ return page === void 0 ? fields : fields.filter((field) => field.page === page);
627
+ },
628
+ // A field traced, in the place of what was traced of it before (its page and its label the same)
629
+ async saveField(field) {
630
+ return locked(folder, async () => {
631
+ const fields = await readAllFields();
632
+ const traced = { ...field, tracedAt: (/* @__PURE__ */ new Date()).toISOString() };
633
+ const others = fields.filter(
634
+ (each) => each.page !== field.page || each.label !== field.label
635
+ );
636
+ await mkdir2(folder, { recursive: true });
637
+ await writeJson(fieldsPath, [...others, traced]);
638
+ return traced;
639
+ });
640
+ },
641
+ readBase,
642
+ readDrafts,
643
+ async writeBase(base) {
644
+ await mkdir2(own, { recursive: true });
645
+ await writeJson(basePath, base);
646
+ },
647
+ async draft(id) {
648
+ return draftIn(await readDrafts(), id);
649
+ },
650
+ // A draft once the person has adopted it or set it aside (adopting another of its group sets it aside too), or
651
+ // as it still stands when `within` milliseconds are up or the wait is called off; read again every `every`
652
+ async decision(id, within, every = 1e3, signal) {
653
+ const until = Date.now() + within;
654
+ for (; ; ) {
655
+ const draft = draftIn(await readDrafts(), id);
656
+ if (draft.status !== "proposed" || Date.now() >= until || signal?.aborted) {
657
+ return draft;
658
+ }
659
+ await pause(every, signal);
660
+ }
661
+ },
662
+ // A new draft, its operations applied to the base first, so one that cannot be is refused and never kept
663
+ async propose(input) {
664
+ return locked(folder, async () => {
665
+ const base = await requireBase();
666
+ const operations2 = z3.array(operationSchema).parse(input.operations);
667
+ applyOperations(base.schema, operations2);
668
+ const drafts = await readDrafts();
669
+ const number = Math.max(
670
+ await readLastNumber(),
671
+ ...drafts.map((each) => Number(each.id.slice(1)))
672
+ );
673
+ const now = (/* @__PURE__ */ new Date()).toISOString();
674
+ const draft = {
675
+ id: `d${number + 1}`,
676
+ title: input.title,
677
+ summary: input.summary ?? "",
678
+ group: input.group,
679
+ status: "proposed",
680
+ operations: operations2,
681
+ createdAt: now,
682
+ updatedAt: now
683
+ };
684
+ await writeDrafts([...drafts, draft]);
685
+ await writeJson(sequencePath, { last: number + 1 });
686
+ return draft;
687
+ });
688
+ },
689
+ // A draft changed: its operations followed or taken over, its title, summary or group; checked on the base, and
690
+ // proposed again
691
+ async revise(id, revision) {
692
+ return locked(folder, async () => {
693
+ const base = await requireBase();
694
+ const drafts = await readDrafts();
695
+ const draft = draftIn(drafts, id);
696
+ const given = z3.array(operationSchema).parse(revision.operations ?? []);
697
+ const operations2 = revision.mode === "replace" ? given : [...draft.operations, ...given];
698
+ applyOperations(base.schema, operations2);
699
+ const next = {
700
+ ...draft,
701
+ title: revision.title ?? draft.title,
702
+ summary: revision.summary ?? draft.summary,
703
+ group: revision.group ?? draft.group,
704
+ operations: operations2,
705
+ status: "proposed",
706
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
707
+ };
708
+ await writeDrafts(drafts.map((each) => each.id === id ? next : each));
709
+ return next;
710
+ });
711
+ },
712
+ // A draft gone for good, and where the person left the cards of what it showed: what it was is not kept
713
+ async deleteDraft(id) {
714
+ return locked(folder, async () => {
715
+ const drafts = await readDrafts();
716
+ const draft = draftIn(drafts, id);
717
+ await writeDrafts(drafts.filter((each) => each.id !== id));
718
+ const layouts = await readLayouts();
719
+ const kept = Object.fromEntries(
720
+ Object.entries(layouts).filter(
721
+ ([key]) => key !== id && !key.startsWith(`${id}|`)
722
+ )
723
+ );
724
+ if (Object.keys(kept).length !== Object.keys(layouts).length) {
725
+ await writeJson(layoutsPath, kept);
726
+ }
727
+ return draft;
728
+ });
729
+ },
730
+ // A draft adopted or discarded; adopting one sets the others of its group still proposed aside
731
+ async setStatus(id, status) {
732
+ return locked(folder, async () => {
733
+ const drafts = await readDrafts();
734
+ const draft = draftIn(drafts, id);
735
+ const updatedAt = (/* @__PURE__ */ new Date()).toISOString();
736
+ const next = { ...draft, status, updatedAt };
737
+ const sibling = (each) => status === "adopted" && draft.group !== void 0 && each.group === draft.group && each.status === "proposed";
738
+ await writeDrafts(
739
+ drafts.map((each) => {
740
+ if (each.id === id) {
741
+ return next;
742
+ }
743
+ return sibling(each) ? { ...each, status: "discarded", updatedAt } : each;
744
+ })
745
+ );
746
+ return next;
747
+ });
748
+ }
749
+ };
750
+ }
751
+
752
+ // package.json
753
+ var version = "0.0.1";
754
+
755
+ // src/core/version.ts
756
+ var VERSION = version;
757
+ var version_default = VERSION;
758
+
759
+ // src/core/databases.ts
760
+ import { join as join3 } from "path";
761
+ import { readFile as readFile2, mkdir as mkdir3, writeFile as writeFile3 } from "fs/promises";
762
+ function choicePath(config) {
763
+ return join3(config.store, "workspaces", config.workspace, "database.json");
764
+ }
765
+ function storeOf(config, database) {
766
+ return store(join3(config.store, "databases", database.id), config.workspace);
767
+ }
768
+ async function currentDatabase(config) {
769
+ const chosen = await readFile2(choicePath(config), "utf8").then(
770
+ (text5) => JSON.parse(text5).id,
771
+ () => void 0
772
+ );
773
+ return config.databases.find((database) => database.id === chosen) ?? config.databases[0];
774
+ }
775
+ function databaseOf(config, id) {
776
+ const database = config.databases.find((each) => each.id === id);
777
+ if (!database) {
778
+ throw new Error(
779
+ `There is no database "${id}": ${config.databases.map((each) => each.id).join(", ")}`
780
+ );
781
+ }
782
+ return database;
783
+ }
784
+ async function chooseDatabase(config, id) {
785
+ const database = databaseOf(config, id);
786
+ await mkdir3(join3(config.store, "workspaces", config.workspace), {
787
+ recursive: true
788
+ });
789
+ await writeFile3(choicePath(config), `${JSON.stringify({ id })}
790
+ `);
791
+ return database;
792
+ }
793
+
794
+ // src/core/read-schema.ts
795
+ import { createRequire } from "module";
796
+ import { pathToFileURL } from "url";
797
+ import { extname, dirname, join as join4, relative } from "path";
798
+ import { existsSync } from "fs";
799
+ import { z as z4 } from "zod";
800
+ import { fork } from "child_process";
801
+ var TSX = pathToFileURL(createRequire(import.meta.url).resolve("tsx")).href;
802
+ var CHILD = new URL(
803
+ `./read-schema-child${extname(new URL(import.meta.url).pathname)}`,
804
+ import.meta.url
805
+ );
806
+ function tsconfigOf(schema) {
807
+ for (let folder = dirname(schema); ; folder = dirname(folder)) {
808
+ const tsconfig = join4(folder, "tsconfig.json");
809
+ if (existsSync(tsconfig)) {
810
+ return tsconfig;
811
+ }
812
+ if (dirname(folder) === folder) {
813
+ return void 0;
814
+ }
815
+ }
816
+ }
817
+ var sentSchema = z4.union([
818
+ z4.object({ schema: z4.unknown() }),
819
+ z4.object({ error: z4.string() })
820
+ ]);
821
+ function readInChild(config, schema) {
822
+ return new Promise((done, fail) => {
823
+ const tsconfig = tsconfigOf(schema);
824
+ const child = fork(CHILD, [schema], {
825
+ cwd: config.root,
826
+ execArgv: ["--import", TSX],
827
+ env: {
828
+ ...process.env,
829
+ ...tsconfig ? { TSX_TSCONFIG_PATH: tsconfig } : {}
830
+ },
831
+ stdio: ["ignore", "ignore", "inherit", "ipc"]
832
+ });
833
+ const source = relative(config.root, schema);
834
+ let sent;
835
+ child.on("message", (message) => {
836
+ sent = sentSchema.parse(message);
837
+ });
838
+ child.on("close", (code) => {
839
+ if (sent === void 0) {
840
+ fail(
841
+ new Error(
842
+ `${source} could not be read (its process stopped with code ${code})`
843
+ )
844
+ );
845
+ } else if ("error" in sent) {
846
+ fail(new Error(`${source} could not be read: ${sent.error}`));
847
+ } else {
848
+ done(parseSchema(sent.schema));
849
+ }
850
+ });
851
+ });
852
+ }
853
+ async function readSchema(config, database, store2) {
854
+ const schema = await readInChild(config, database.schema);
855
+ if (schema.tables.length === 0 && schema.enums.length === 0) {
856
+ throw new Error(
857
+ `${relative(config.root, database.schema)} exports no Postgres table nor enum: Erdraft reads drizzle-orm/pg-core's`
858
+ );
859
+ }
860
+ const base = {
861
+ source: relative(config.root, database.schema),
862
+ readAt: (/* @__PURE__ */ new Date()).toISOString(),
863
+ schema
864
+ };
865
+ await store2.writeBase(base);
866
+ return base;
867
+ }
868
+
869
+ // src/core/diff-schemas.ts
870
+ var COLUMN_FIELDS = [
871
+ "type",
872
+ "nullable",
873
+ "primaryKey",
874
+ "unique",
875
+ "default",
876
+ "note",
877
+ "shape"
878
+ ];
879
+ function byName(items) {
880
+ return new Map(items.map((item) => [item.name, item]));
881
+ }
882
+ function originalOf(name, renames) {
883
+ const from = renames?.[name];
884
+ if (from !== void 0) {
885
+ return from;
886
+ }
887
+ const taken = new Set(Object.values(renames ?? {}));
888
+ return taken.has(name) ? void 0 : name;
889
+ }
890
+ function invert(map) {
891
+ return new Map(Object.entries(map ?? {}).map(([to, from]) => [from, to]));
892
+ }
893
+ function signature(foreignKey) {
894
+ return `${foreignKey.columns.join(",")}>${foreignKey.table}.${foreignKey.foreignColumns.join(",")}`;
895
+ }
896
+ function indexSignature(index) {
897
+ return `${index.unique ? "unique" : "index"}(${index.columns.join(",")})`;
898
+ }
899
+ function checkSignature(check) {
900
+ return `${check.name}:${check.expression}`;
901
+ }
902
+ function diffBySignature(draft, base, signature2, said2 = signature2) {
903
+ const baseItems = new Map(base.map((item) => [said2(item), item]));
904
+ const draftItems = new Set(draft.map(signature2));
905
+ return [
906
+ ...draft.map((item) => ({
907
+ item,
908
+ status: baseItems.has(signature2(item)) ? "same" : "added"
909
+ })),
910
+ ...[...baseItems].filter(([key]) => !draftItems.has(key)).map(([, item]) => ({ item, status: "removed" }))
911
+ ];
912
+ }
913
+ function allOf(table, status) {
914
+ return {
915
+ name: table.name,
916
+ status,
917
+ note: table.note,
918
+ columns: table.columns.map((column) => ({
919
+ column,
920
+ status,
921
+ changed: []
922
+ })),
923
+ foreignKeys: table.foreignKeys.map((foreignKey) => ({
924
+ foreignKey,
925
+ status
926
+ })),
927
+ indexes: table.indexes.map((index) => ({ index, status })),
928
+ checks: table.checks.map((check) => ({ check, status }))
929
+ };
930
+ }
931
+ function diffColumns(draft, base, renames) {
932
+ const baseColumns = byName(base.columns);
933
+ const matched = /* @__PURE__ */ new Set();
934
+ const columns = draft.columns.map((column) => {
935
+ const original = originalOf(column.name, renames);
936
+ const before = original === void 0 ? void 0 : baseColumns.get(original);
937
+ if (!before) {
938
+ return { column, status: "added", changed: [] };
939
+ }
940
+ matched.add(before.name);
941
+ const changed = COLUMN_FIELDS.filter(
942
+ (field) => column[field] !== before[field]
943
+ );
944
+ const was = before.name === column.name ? void 0 : before.name;
945
+ return {
946
+ column,
947
+ status: changed.length > 0 || was ? "changed" : "same",
948
+ was,
949
+ changed,
950
+ before
951
+ };
952
+ });
953
+ for (const column of base.columns) {
954
+ if (!matched.has(column.name)) {
955
+ columns.push({ column, status: "removed", changed: [] });
956
+ }
957
+ }
958
+ return columns;
959
+ }
960
+ function diffSchemas(base, { schema, renames }) {
961
+ const baseTables = byName(base.tables);
962
+ const forwardTables = invert(renames.tables);
963
+ const newName = (table) => forwardTables.get(table) ?? table;
964
+ const newColumn = (table, column) => invert(renames.columns[table]).get(column) ?? column;
965
+ const matched = /* @__PURE__ */ new Set();
966
+ const tables = schema.tables.map((table) => {
967
+ const original = originalOf(table.name, renames.tables);
968
+ const before = original === void 0 ? void 0 : baseTables.get(original);
969
+ if (!before) {
970
+ return allOf(table, "added");
971
+ }
972
+ matched.add(before.name);
973
+ const columns = diffColumns(table, before, renames.columns[table.name]);
974
+ const foreignKeys = diffBySignature(
975
+ table.foreignKeys,
976
+ before.foreignKeys,
977
+ signature,
978
+ (foreignKey) => {
979
+ const target = newName(foreignKey.table);
980
+ return signature({
981
+ ...foreignKey,
982
+ columns: foreignKey.columns.map(
983
+ (column) => newColumn(table.name, column)
984
+ ),
985
+ table: target,
986
+ foreignColumns: foreignKey.foreignColumns.map(
987
+ (column) => newColumn(target, column)
988
+ )
989
+ });
990
+ }
991
+ ).map(({ item, status }) => ({ foreignKey: item, status }));
992
+ const indexes = diffBySignature(
993
+ table.indexes,
994
+ before.indexes,
995
+ indexSignature,
996
+ (index) => indexSignature({
997
+ ...index,
998
+ columns: index.columns.map((column) => newColumn(table.name, column))
999
+ })
1000
+ ).map(({ item, status }) => ({ index: item, status }));
1001
+ const checks = diffBySignature(
1002
+ table.checks,
1003
+ before.checks,
1004
+ checkSignature
1005
+ ).map(({ item, status }) => ({ check: item, status }));
1006
+ const was = before.name === table.name ? void 0 : before.name;
1007
+ const changed = was !== void 0 || table.note !== before.note || [...columns, ...foreignKeys, ...indexes, ...checks].some(
1008
+ (each) => each.status !== "same"
1009
+ );
1010
+ return {
1011
+ name: table.name,
1012
+ was,
1013
+ status: changed ? "changed" : "same",
1014
+ note: table.note,
1015
+ columns,
1016
+ foreignKeys,
1017
+ indexes,
1018
+ checks
1019
+ };
1020
+ });
1021
+ for (const table of base.tables) {
1022
+ if (!matched.has(table.name)) {
1023
+ tables.push(allOf(table, "removed"));
1024
+ }
1025
+ }
1026
+ return { tables, enums: diffEnums(base.enums, schema.enums) };
1027
+ }
1028
+ function diffEnums(base, draft) {
1029
+ const baseEnums = byName(base);
1030
+ const draftNames = new Set(draft.map((each) => each.name));
1031
+ const enums = draft.map((each) => {
1032
+ const before = baseEnums.get(each.name);
1033
+ if (!before) {
1034
+ return {
1035
+ name: each.name,
1036
+ status: "added",
1037
+ values: each.values.map((value) => ({ value, status: "added" }))
1038
+ };
1039
+ }
1040
+ const values = [
1041
+ ...each.values.map((value) => ({
1042
+ value,
1043
+ status: before.values.includes(value) ? "same" : "added"
1044
+ })),
1045
+ ...before.values.filter((value) => !each.values.includes(value)).map((value) => ({ value, status: "removed" }))
1046
+ ];
1047
+ return {
1048
+ name: each.name,
1049
+ status: values.some((value) => value.status !== "same") ? "changed" : "same",
1050
+ values
1051
+ };
1052
+ });
1053
+ for (const each of base) {
1054
+ if (!draftNames.has(each.name)) {
1055
+ enums.push({
1056
+ name: each.name,
1057
+ status: "removed",
1058
+ values: each.values.map((value) => ({ value, status: "removed" }))
1059
+ });
1060
+ }
1061
+ }
1062
+ return enums;
1063
+ }
1064
+
1065
+ // src/core/diff-draft.ts
1066
+ function diffDraft(base, operations2 = []) {
1067
+ return diffSchemas(base, applyOperations(base, operations2));
1068
+ }
1069
+
1070
+ // src/core/written-in.ts
1071
+ function touchedBy(operations2) {
1072
+ const tables = /* @__PURE__ */ new Set();
1073
+ const enums = /* @__PURE__ */ new Set();
1074
+ for (const operation of operations2) {
1075
+ switch (operation.op) {
1076
+ case "add_table":
1077
+ tables.add(operation.table.name);
1078
+ break;
1079
+ case "rename_table":
1080
+ tables.add(operation.table).add(operation.to);
1081
+ break;
1082
+ case "add_enum":
1083
+ enums.add(operation.enum.name);
1084
+ break;
1085
+ case "set_enum_values":
1086
+ case "drop_enum":
1087
+ enums.add(operation.enum);
1088
+ break;
1089
+ default:
1090
+ tables.add(operation.table);
1091
+ }
1092
+ }
1093
+ return { tables, enums };
1094
+ }
1095
+ function alreadyHolds(schema, operation) {
1096
+ const table = schema.tables.find(
1097
+ (each) => "table" in operation && each.name === operation.table
1098
+ );
1099
+ const hasColumn = (name) => table?.columns.some((each) => each.name === name) === true;
1100
+ const named = (kind, name) => table?.[kind].some((each) => each.name === name) === true;
1101
+ switch (operation.op) {
1102
+ case "drop_table":
1103
+ return table === void 0;
1104
+ case "rename_table":
1105
+ return table === void 0 && schema.tables.some((each) => each.name === operation.to);
1106
+ case "add_column":
1107
+ return hasColumn(operation.column.name);
1108
+ case "drop_column":
1109
+ return !hasColumn(operation.column);
1110
+ case "alter_column":
1111
+ return !hasColumn(operation.column) && hasColumn(operation.set.name ?? operation.column);
1112
+ case "add_foreign_key":
1113
+ return named(
1114
+ "foreignKeys",
1115
+ foreignKeyName(operation.table, operation.foreignKey)
1116
+ );
1117
+ case "drop_foreign_key":
1118
+ return !named("foreignKeys", operation.name);
1119
+ case "add_index":
1120
+ return named("indexes", indexName(operation.table, operation.index));
1121
+ case "drop_index":
1122
+ return !named("indexes", operation.name);
1123
+ case "add_check":
1124
+ return named("checks", operation.check.name);
1125
+ case "drop_check":
1126
+ return !named("checks", operation.name);
1127
+ case "drop_enum":
1128
+ return schema.enums.every((each) => each.name !== operation.enum);
1129
+ default:
1130
+ return false;
1131
+ }
1132
+ }
1133
+ function mergeTable(table, asked) {
1134
+ const missing = (have, add) => add.filter((each) => !have.some((had) => had.name === each.name));
1135
+ table.columns.push(...missing(table.columns, asked.columns));
1136
+ table.foreignKeys.push(...missing(table.foreignKeys, asked.foreignKeys));
1137
+ table.indexes.push(...missing(table.indexes, asked.indexes));
1138
+ table.checks.push(...missing(table.checks, asked.checks));
1139
+ }
1140
+ function laidOver(base, operations2) {
1141
+ const schema = structuredClone(base);
1142
+ const renames = { tables: {}, columns: {} };
1143
+ for (const [index, operation] of structuredClone(operations2).entries()) {
1144
+ const asked = operation.op === "add_table" && namedTable(operation.table);
1145
+ const table = asked ? schema.tables.find((each) => each.name === asked.name) : void 0;
1146
+ const values = operation.op === "add_enum" ? schema.enums.find((each) => each.name === operation.enum.name) : void 0;
1147
+ if (asked && table) {
1148
+ mergeTable(table, asked);
1149
+ } else if (operation.op === "add_enum" && values) {
1150
+ values.values = operation.enum.values;
1151
+ } else if (!alreadyHolds(schema, operation)) {
1152
+ try {
1153
+ apply(schema, renames, operation, index + 1);
1154
+ } catch {
1155
+ return null;
1156
+ }
1157
+ }
1158
+ }
1159
+ return schema;
1160
+ }
1161
+ function sameSet(a, b) {
1162
+ const said2 = (list) => [...new Set(list)].sort().join();
1163
+ return said2(a) === said2(b);
1164
+ }
1165
+ function sameTable(a, b) {
1166
+ if (!a || !b) {
1167
+ return a === b;
1168
+ }
1169
+ return sameSet(
1170
+ a.columns.map((each) => each.name),
1171
+ b.columns.map((each) => each.name)
1172
+ ) && sameSet(
1173
+ a.foreignKeys.map(
1174
+ (each) => `${each.columns}>${each.table}.${each.foreignColumns.join()}`
1175
+ ),
1176
+ b.foreignKeys.map(
1177
+ (each) => `${each.columns}>${each.table}.${each.foreignColumns.join()}`
1178
+ )
1179
+ ) && sameSet(
1180
+ a.indexes.map((each) => `${each.unique}(${each.columns})`),
1181
+ b.indexes.map((each) => `${each.unique}(${each.columns})`)
1182
+ ) && sameSet(
1183
+ a.checks.map((each) => each.name),
1184
+ b.checks.map((each) => each.name)
1185
+ );
1186
+ }
1187
+ function writtenIn(base, operations2) {
1188
+ const schema = laidOver(base, operations2);
1189
+ const { tables, enums } = touchedBy(operations2);
1190
+ const tableOf = (of, name) => of.tables.find((each) => each.name === name);
1191
+ const valuesOf = (of, name) => of.enums.find((each) => each.name === name)?.values.join();
1192
+ return schema !== null && [...tables].every(
1193
+ (name) => sameTable(tableOf(schema, name), tableOf(base, name))
1194
+ ) && [...enums].every((name) => valuesOf(schema, name) === valuesOf(base, name));
1195
+ }
1196
+
1197
+ // src/core/mark-written.ts
1198
+ function markWritten(base, operations2) {
1199
+ const marks = /* @__PURE__ */ new Map();
1200
+ const markOf = (table) => {
1201
+ const mark = marks.get(table) ?? {
1202
+ status: "changed",
1203
+ columns: /* @__PURE__ */ new Map(),
1204
+ foreignKeys: [],
1205
+ indexes: [],
1206
+ checks: /* @__PURE__ */ new Set()
1207
+ };
1208
+ marks.set(table, mark);
1209
+ return mark;
1210
+ };
1211
+ for (const operation of operations2) {
1212
+ switch (operation.op) {
1213
+ case "add_table": {
1214
+ const { name, columns, foreignKeys, indexes, checks } = operation.table;
1215
+ const mark = markOf(name);
1216
+ mark.status = "added";
1217
+ for (const column of columns) {
1218
+ mark.columns.set(column.name, { status: "added" });
1219
+ }
1220
+ mark.foreignKeys.push(...foreignKeys);
1221
+ mark.indexes.push(...indexes);
1222
+ for (const check of checks) {
1223
+ mark.checks.add(check.name);
1224
+ }
1225
+ break;
1226
+ }
1227
+ case "drop_table":
1228
+ marks.delete(operation.table);
1229
+ break;
1230
+ case "rename_table": {
1231
+ const mark = markOf(operation.table);
1232
+ marks.delete(operation.table);
1233
+ marks.set(operation.to, {
1234
+ ...mark,
1235
+ was: mark.status === "added" ? void 0 : mark.was ?? operation.table
1236
+ });
1237
+ break;
1238
+ }
1239
+ case "add_column":
1240
+ markOf(operation.table).columns.set(operation.column.name, {
1241
+ status: "added"
1242
+ });
1243
+ break;
1244
+ case "drop_column":
1245
+ markOf(operation.table).columns.delete(operation.column);
1246
+ break;
1247
+ case "alter_column": {
1248
+ const { columns } = markOf(operation.table);
1249
+ const before = columns.get(operation.column);
1250
+ const name = operation.set.name ?? operation.column;
1251
+ const status = before?.status === "added" ? "added" : "changed";
1252
+ const renamed = name !== operation.column && status === "changed";
1253
+ columns.delete(operation.column);
1254
+ columns.set(name, {
1255
+ status,
1256
+ was: renamed ? before?.was ?? operation.column : before?.was
1257
+ });
1258
+ break;
1259
+ }
1260
+ case "add_foreign_key":
1261
+ markOf(operation.table).foreignKeys.push(operation.foreignKey);
1262
+ break;
1263
+ case "add_index":
1264
+ markOf(operation.table).indexes.push(operation.index);
1265
+ break;
1266
+ case "add_check":
1267
+ markOf(operation.table).checks.add(operation.check.name);
1268
+ break;
1269
+ case "drop_foreign_key":
1270
+ case "drop_index":
1271
+ case "drop_check":
1272
+ markOf(operation.table);
1273
+ break;
1274
+ case "add_enum":
1275
+ case "set_enum_values":
1276
+ case "drop_enum":
1277
+ break;
1278
+ }
1279
+ }
1280
+ const diff = diffDraft(base);
1281
+ return {
1282
+ ...diff,
1283
+ tables: diff.tables.map((table) => {
1284
+ const mark = marks.get(table.name);
1285
+ if (!mark) {
1286
+ return table;
1287
+ }
1288
+ return {
1289
+ ...table,
1290
+ status: mark.status,
1291
+ was: mark.was,
1292
+ columns: table.columns.map((each) => {
1293
+ const column = mark.columns.get(each.column.name);
1294
+ return column ? { ...each, status: column.status, was: column.was } : each;
1295
+ }),
1296
+ foreignKeys: table.foreignKeys.map((each) => ({
1297
+ ...each,
1298
+ status: mark.foreignKeys.some(
1299
+ (asked) => asked.table === each.foreignKey.table && asked.columns.join() === each.foreignKey.columns.join() && asked.foreignColumns.join() === each.foreignKey.foreignColumns.join()
1300
+ ) ? "added" : each.status
1301
+ })),
1302
+ indexes: table.indexes.map((each) => ({
1303
+ ...each,
1304
+ status: mark.indexes.some(
1305
+ (asked) => asked.unique === each.index.unique && asked.columns.join() === each.index.columns.join()
1306
+ ) ? "added" : each.status
1307
+ })),
1308
+ checks: table.checks.map((each) => ({
1309
+ ...each,
1310
+ status: mark.checks.has(each.check.name) ? "added" : each.status
1311
+ }))
1312
+ };
1313
+ })
1314
+ };
1315
+ }
1316
+
1317
+ // src/core/read-draft.ts
1318
+ function readDraft(base, operations2 = []) {
1319
+ try {
1320
+ return { diff: diffDraft(base, operations2), written: false };
1321
+ } catch (error) {
1322
+ if (writtenIn(base, operations2)) {
1323
+ return { diff: markWritten(base, operations2), written: true };
1324
+ }
1325
+ throw error;
1326
+ }
1327
+ }
1328
+
1329
+ // src/core/describe.ts
1330
+ var MARKS = {
1331
+ same: " ",
1332
+ added: "+",
1333
+ removed: "-",
1334
+ changed: "~"
1335
+ };
1336
+ function said(column, field) {
1337
+ const value = column[field];
1338
+ return value === void 0 ? "none" : String(value);
1339
+ }
1340
+ function columnLine({ column, status, was, changed, before }) {
1341
+ const words = [
1342
+ column.name,
1343
+ column.type,
1344
+ column.nullable ? "" : "not null",
1345
+ column.primaryKey ? "primary key" : "",
1346
+ column.unique ? "unique" : "",
1347
+ column.default === void 0 ? "" : `default ${column.default}`,
1348
+ column.shape === void 0 ? "" : `shape ${column.shape}`,
1349
+ was ? `(was ${was})` : "",
1350
+ before && changed.length > 0 ? `[${changed.map((field) => `${field}: ${said(before, field)} \u2192 ${said(column, field)}`).join(", ")}]` : ""
1351
+ ];
1352
+ return ` ${MARKS[status]} ${words.filter(Boolean).join(" ")}`;
1353
+ }
1354
+ function foreignKeyLine({ foreignKey, status }) {
1355
+ const onDelete = foreignKey.onDelete ? ` on delete ${foreignKey.onDelete}` : "";
1356
+ return ` ${MARKS[status]} (${foreignKey.columns.join(", ")}) \u2192 ${foreignKey.table}(${foreignKey.foreignColumns.join(", ")})${onDelete}`;
1357
+ }
1358
+ function indexLine({ index, status }) {
1359
+ return ` ${MARKS[status]} ${index.unique ? "unique" : "index"} (${index.columns.join(", ")}) ${index.name}`;
1360
+ }
1361
+ function checkLine({ check, status }) {
1362
+ return ` ${MARKS[status]} check ${check.name}: ${check.expression}`;
1363
+ }
1364
+ function describeTable(table, diff) {
1365
+ const pointing = (diff?.tables ?? []).flatMap(
1366
+ (other) => other.name === table.name ? [] : other.foreignKeys.filter(({ foreignKey }) => foreignKey.table === table.name).map(
1367
+ ({ foreignKey, status }) => ` ${MARKS[status]} \u2190 ${other.name}(${foreignKey.columns.join(", ")}) \u2192 (${foreignKey.foreignColumns.join(", ")})`
1368
+ )
1369
+ );
1370
+ return [
1371
+ `${MARKS[table.status]} ${table.name}${table.was ? ` (was ${table.was})` : ""}`,
1372
+ ...table.note ? [` ${table.note}`] : [],
1373
+ ...table.columns.map(columnLine),
1374
+ ...table.foreignKeys.map(foreignKeyLine),
1375
+ ...table.indexes.map(indexLine),
1376
+ ...table.checks.map(checkLine),
1377
+ ...pointing
1378
+ ].join("\n");
1379
+ }
1380
+ function enumLines(each) {
1381
+ return `${MARKS[each.status]} enum ${each.name}: ${each.values.map(
1382
+ ({ value, status }) => status === "same" ? value : `${MARKS[status]}${value}`
1383
+ ).join(", ")}`;
1384
+ }
1385
+ function describeTables(diff) {
1386
+ return diff.tables.map(
1387
+ (table) => `${MARKS[table.status]} ${table.name} (${table.columns.length} columns)`
1388
+ ).join("\n");
1389
+ }
1390
+ function describeDiff(diff) {
1391
+ const tables = diff.tables.filter((table) => table.status !== "same");
1392
+ const enums = diff.enums.filter((each) => each.status !== "same");
1393
+ if (tables.length === 0 && enums.length === 0) {
1394
+ return "No change to the base.";
1395
+ }
1396
+ return [
1397
+ ...tables.map((table) => describeTable(table)),
1398
+ ...enums.map(enumLines)
1399
+ ].join("\n\n");
1400
+ }
1401
+ function draftLine(draft) {
1402
+ const group = draft.group ? ` [${draft.group}]` : "";
1403
+ return `${draft.id} ${draft.status.padEnd(9)}${group} ${draft.title}`;
1404
+ }
1405
+ function describeDraft(draft, diff, written = false) {
1406
+ return [
1407
+ draftLine(draft),
1408
+ ...draft.summary ? [draft.summary] : [],
1409
+ "",
1410
+ written ? "Written in the code: the schema holds what it does." : describeDiff(diff)
1411
+ ].join("\n");
1412
+ }
1413
+ function describeDecision(draft, drafts, seconds, written = false) {
1414
+ if (draft.status === "adopted") {
1415
+ return written ? `${draft.id} adopted, and written in the code already: nothing left to do.` : `${draft.id} adopted: write it in the schema's code now, then read the schema again; the window says when the code holds it.`;
1416
+ }
1417
+ if (draft.status === "discarded") {
1418
+ const chosen = drafts.find(
1419
+ (each) => each.status === "adopted" && draft.group !== void 0 && each.group === draft.group
1420
+ );
1421
+ return chosen ? `${draft.id} set aside: ${chosen.id} of its group adopted instead (${chosen.title}).` : `${draft.id} set aside: it is no longer an option.`;
1422
+ }
1423
+ return `${draft.id} is still proposed after ${seconds} s. If the person wrote to you meanwhile, answer them first: a change they ask for is revise_draft on it, or a new draft in its group. Otherwise wait again; after about ten minutes, say it waits in the window and stop.`;
1424
+ }
1425
+
1426
+ // src/core/related-tables.ts
1427
+ function relatedTables(diff, table) {
1428
+ const joined = /* @__PURE__ */ new Set([table]);
1429
+ for (const each of diff.tables) {
1430
+ for (const { foreignKey } of each.foreignKeys) {
1431
+ if (each.name === table) {
1432
+ joined.add(foreignKey.table);
1433
+ }
1434
+ if (foreignKey.table === table) {
1435
+ joined.add(each.name);
1436
+ }
1437
+ }
1438
+ }
1439
+ return diff.tables.map((each) => each.name).filter((name) => joined.has(name));
1440
+ }
1441
+
1442
+ // src/mcp/create-server.ts
1443
+ var INSTRUCTIONS = `Erdraft shows a project's data model in a window a person watches while you work, and every proposal you make as a draft beside the schema the code has.
1444
+
1445
+ - The project may have several databases: list_databases says which the person works on; every tool works on it.
1446
+ - Read before proposing: list_tables, describe_table.
1447
+ - Propose a change as a draft (propose_draft): operations on the base, a title and what it is for. It is drawn at once in the window, its tables and columns marked added, changed or removed.
1448
+ - When asked for ideas or there is more than one good answer, propose two or three alternatives, each its own draft, sharing one group, so the person compares them side by side.
1449
+ - Build a large draft step by step with revise_draft (mode append); the window follows each step.
1450
+ - Where a field of the project's app keeps its value (a column, the browser's storage, or nowhere), as Erdraft's inspector traced it, is list_fields: a change asked for "this field" starts from its column.
1451
+ - When the person asks to see, show or open something (a table, its relationships, a draft, the whole schema), show it in the window with show_in_window rather than describing it at length.
1452
+ - A draft holds the whole design: its tables and columns, their foreign keys, and the rules Postgres holds, its indexes (unique or not) and checks, each an operation, never a note; a json or jsonb column with its shape, the person reading it in the window, and written as a zod schema the module exports as <table><Column>Shape (websites.theme: websitesThemeShape), its type inferred from it, so the window reads the shape back from the code.
1453
+ - A draft proposes; the person adopts one in the window. After proposing, wait for the person's choice with wait_for_decision, rather than asking them to say it: in short waits, one after another, answering what the person writes to you between them. Never write a draft into the code until it is adopted; then write it in the schema's code, and read_schema again.`;
1454
+ function text(value) {
1455
+ return { content: [{ type: "text", text: value }] };
1456
+ }
1457
+ async function answer(work) {
1458
+ try {
1459
+ return text(await work());
1460
+ } catch (error) {
1461
+ return { ...text(String(error)), isError: true };
1462
+ }
1463
+ }
1464
+ var draftId = z5.string().describe("A draft\u2019s id, as list_drafts gives it (d1, d2...)");
1465
+ var operations = z5.array(operationSchema).describe(
1466
+ 'Operations on the schema, applied in order: add_table (its indexes and checks with it), drop_table, rename_table, add_column, drop_column, alter_column (set: the fields that change, a new name included), add_foreign_key, drop_foreign_key, add_index (unique or not, on columns of the table), drop_index, add_check (its name and its SQL), drop_check, add_enum, set_enum_values, drop_enum. Types as Postgres names them (integer, text, varchar(120), timestamp with time zone, jsonb), an enum column\u2019s type its enum\u2019s name; a json or jsonb column\u2019s shape its TypeScript type, as the code\u2019s $type<...>() writes it ({ accent?: string; tone: "light" | "dark" }).'
1467
+ );
1468
+ function createServer({ config }) {
1469
+ const server = new McpServer(
1470
+ { name: "erdraft", version: version_default },
1471
+ { instructions: INSTRUCTIONS }
1472
+ );
1473
+ const windowUrl = `http://localhost:${config.port}`;
1474
+ async function opened() {
1475
+ const database = await currentDatabase(config);
1476
+ return { database, store: storeOf(config, database) };
1477
+ }
1478
+ async function baseSchema2({ database, store: store2 }) {
1479
+ const base = await store2.readBase();
1480
+ return base ? base.schema : (await readSchema(config, database, store2)).schema;
1481
+ }
1482
+ async function schemaOf(open, draft) {
1483
+ const schema = await baseSchema2(open);
1484
+ return readDraft(
1485
+ schema,
1486
+ draft ? (await open.store.draft(draft)).operations : []
1487
+ ).diff;
1488
+ }
1489
+ server.registerTool(
1490
+ "list_databases",
1491
+ {
1492
+ title: "List the databases",
1493
+ description: "The project\u2019s databases, * the one the person works on now: every other tool works on it. The person switches in the window."
1494
+ },
1495
+ () => answer(async () => {
1496
+ const { database } = await opened();
1497
+ return config.databases.map(
1498
+ (each) => `${each.id === database.id ? "*" : " "} ${each.id} ${each.name}`
1499
+ ).join("\n");
1500
+ })
1501
+ );
1502
+ server.registerTool(
1503
+ "read_schema",
1504
+ {
1505
+ title: "Read the schema",
1506
+ description: "Read the schema again from the code (the Drizzle module), after the code changed. Every draft is applied to it."
1507
+ },
1508
+ () => answer(async () => {
1509
+ const { database, store: store2 } = await opened();
1510
+ const base = await readSchema(config, database, store2);
1511
+ return `Read ${base.schema.tables.length} tables and ${base.schema.enums.length} enums from ${base.source}.`;
1512
+ })
1513
+ );
1514
+ server.registerTool(
1515
+ "list_tables",
1516
+ {
1517
+ title: "List the tables",
1518
+ description: "Every table with its number of columns; with a draft, the draft\u2019s, marked + added, - removed, ~ changed.",
1519
+ inputSchema: { draft: draftId.optional() }
1520
+ },
1521
+ ({ draft }) => answer(async () => describeTables(await schemaOf(await opened(), draft)))
1522
+ );
1523
+ server.registerTool(
1524
+ "describe_table",
1525
+ {
1526
+ title: "Describe a table",
1527
+ description: "A table\u2019s columns (type, not null, primary key, unique, default) and foreign keys; with a draft, as the draft has it.",
1528
+ inputSchema: { name: z5.string(), draft: draftId.optional() }
1529
+ },
1530
+ ({ name, draft }) => answer(async () => {
1531
+ const diff = await schemaOf(await opened(), draft);
1532
+ const table = diff.tables.find((each) => each.name === name);
1533
+ if (!table) {
1534
+ throw new Error(`There is no table "${name}"`);
1535
+ }
1536
+ return describeTable(table, diff);
1537
+ })
1538
+ );
1539
+ server.registerTool(
1540
+ "list_drafts",
1541
+ {
1542
+ title: "List the drafts",
1543
+ description: "Every draft: id, status (proposed, adopted, discarded), group and title. An adopted draft is the person\u2019s choice, to be written in the code."
1544
+ },
1545
+ () => answer(async () => {
1546
+ const drafts = await (await opened()).store.readDrafts();
1547
+ return drafts.length > 0 ? drafts.map(draftLine).join("\n") : "No drafts yet.";
1548
+ })
1549
+ );
1550
+ server.registerTool(
1551
+ "show_draft",
1552
+ {
1553
+ title: "Show a draft",
1554
+ description: "A draft in full: what it is for and every table and enum it changes.",
1555
+ inputSchema: { id: draftId }
1556
+ },
1557
+ ({ id }) => answer(async () => {
1558
+ const open = await opened();
1559
+ const draft = await open.store.draft(id);
1560
+ const { diff, written } = readDraft(
1561
+ await baseSchema2(open),
1562
+ draft.operations
1563
+ );
1564
+ return describeDraft(draft, diff, written);
1565
+ })
1566
+ );
1567
+ server.registerTool(
1568
+ "propose_draft",
1569
+ {
1570
+ title: "Propose a draft",
1571
+ description: "A new draft: operations on the schema, drawn at once in the window for the person to see. Alternatives to one question share a group.",
1572
+ inputSchema: {
1573
+ title: z5.string().min(1).describe("What the draft does, in a few words"),
1574
+ summary: z5.string().optional().describe("Why, and what it trades, in a sentence or two"),
1575
+ group: z5.string().optional().describe(
1576
+ "A short name shared by the alternatives to one question (project-tags)"
1577
+ ),
1578
+ operations
1579
+ }
1580
+ },
1581
+ (input) => answer(async () => {
1582
+ const open = await opened();
1583
+ const schema = await baseSchema2(open);
1584
+ const draft = await open.store.propose(input);
1585
+ return `Proposed ${draft.id}: ${draft.title}. Shown at ${windowUrl}/?draft=${draft.id}
1586
+
1587
+ ${describeDiff(diffDraft(schema, draft.operations))}`;
1588
+ })
1589
+ );
1590
+ server.registerTool(
1591
+ "revise_draft",
1592
+ {
1593
+ title: "Revise a draft",
1594
+ description: "Change a draft: operations added after its own (mode append, the default) or put in their place (mode replace), its title, summary or group. It is proposed again.",
1595
+ inputSchema: {
1596
+ id: draftId,
1597
+ operations: operations.optional(),
1598
+ mode: z5.enum(["append", "replace"]).optional(),
1599
+ title: z5.string().min(1).optional(),
1600
+ summary: z5.string().optional(),
1601
+ group: z5.string().optional()
1602
+ }
1603
+ },
1604
+ ({ id, ...revision }) => answer(async () => {
1605
+ const open = await opened();
1606
+ const schema = await baseSchema2(open);
1607
+ const draft = await open.store.revise(id, revision);
1608
+ return `Revised ${draft.id}: ${draft.title}.
1609
+
1610
+ ${describeDiff(diffDraft(schema, draft.operations))}`;
1611
+ })
1612
+ );
1613
+ server.registerTool(
1614
+ "wait_for_decision",
1615
+ {
1616
+ title: "Wait for the person\u2019s decision",
1617
+ description: "Wait until the person adopts a draft or sets it aside in the window (adopting another of its group sets it aside), or the time is up; then say what to do next. Call it after proposing, instead of asking the person to say they adopted. Keep each wait short and call it again: what the person writes to you is read only between waits.",
1618
+ inputSchema: {
1619
+ id: draftId,
1620
+ seconds: z5.number().int().min(1).max(120).optional().describe(
1621
+ `How long to wait at most, ${DECISION_SECONDS} when left out`
1622
+ )
1623
+ }
1624
+ },
1625
+ ({ id, seconds = DECISION_SECONDS }, { signal }) => answer(async () => {
1626
+ const open = await opened();
1627
+ const draft = await open.store.decision(
1628
+ id,
1629
+ seconds * 1e3,
1630
+ void 0,
1631
+ signal
1632
+ );
1633
+ return describeDecision(
1634
+ draft,
1635
+ await open.store.readDrafts(),
1636
+ seconds,
1637
+ writtenIn(await baseSchema2(open), draft.operations)
1638
+ );
1639
+ })
1640
+ );
1641
+ server.registerTool(
1642
+ "show_in_window",
1643
+ {
1644
+ title: "Show in the window",
1645
+ description: "Change what the window shows the person: a table in focus, only it and the tables it joins (related), a draft (draft: its id; null: the schema as the code has it). With no table and related false, the whole diagram.",
1646
+ inputSchema: {
1647
+ table: z5.string().optional().describe(
1648
+ "The table to focus (one a draft adds: give that draft too)"
1649
+ ),
1650
+ related: z5.boolean().optional().describe(
1651
+ "Draw only the table and the tables it joins by a foreign key, either way"
1652
+ ),
1653
+ draft: draftId.nullable().optional().describe(
1654
+ "Leave it out (most often): the window keeps the draft it shows, or the schema. A draft\u2019s id only when the person asks to open that draft, or the table is one a draft adds; null only when they ask to leave the draft for the schema as the code has it"
1655
+ )
1656
+ }
1657
+ },
1658
+ ({ table, related, draft }) => answer(async () => {
1659
+ const open = await opened();
1660
+ const diff = await schemaOf(open, draft ?? void 0);
1661
+ if (table !== void 0 && !diff.tables.some((each) => each.name === table)) {
1662
+ throw new Error(`There is no table "${table}"`);
1663
+ }
1664
+ await open.store.show({
1665
+ table: table ?? null,
1666
+ related: related === true && table !== void 0,
1667
+ draft
1668
+ });
1669
+ const shown = draft === null ? "the schema as the code has it" : draft ? `draft ${draft}` : "what the window shows, its draft or the schema kept";
1670
+ if (table === void 0) {
1671
+ return `Shown in the window: ${shown}, whole.`;
1672
+ }
1673
+ const joined = relatedTables(diff, table).length - 1;
1674
+ return related ? `Shown in the window: ${table} and the ${joined} table${joined === 1 ? "" : "s"} it joins, in ${shown}.` : `Shown in the window: ${table} in focus, in ${shown}.`;
1675
+ })
1676
+ );
1677
+ server.registerTool(
1678
+ "list_fields",
1679
+ {
1680
+ title: "List the fields traced",
1681
+ description: "The project's app's fields Erdraft's inspector traced, page by page, each with where it keeps its value: a table's column, a key of the browser's storage, or nowhere (kept on the page only, or not saved yet).",
1682
+ inputSchema: {
1683
+ page: z5.string().optional().describe("A page's path (/project): its fields alone")
1684
+ }
1685
+ },
1686
+ ({ page }) => answer(
1687
+ async () => describeFields(await (await opened()).store.readFields(page))
1688
+ )
1689
+ );
1690
+ server.registerTool(
1691
+ "discard_draft",
1692
+ {
1693
+ title: "Discard a draft",
1694
+ description: "Set a draft aside, when it is no longer an option.",
1695
+ inputSchema: { id: draftId }
1696
+ },
1697
+ ({ id }) => answer(async () => {
1698
+ const draft = await (await opened()).store.setStatus(id, "discarded");
1699
+ return `Discarded ${draft.id}: ${draft.title}.`;
1700
+ })
1701
+ );
1702
+ return server;
1703
+ }
1704
+
1705
+ // src/server/create-api.ts
1706
+ import { z as z9 } from "zod";
1707
+ import { mkdirSync, watch } from "fs";
1708
+ import { text as text4 } from "stream/consumers";
1709
+
1710
+ // src/server/create-assistant.ts
1711
+ import { z as z7 } from "zod";
1712
+ import { spawn as spawnProcess } from "child_process";
1713
+ import { text as text2 } from "stream/consumers";
1714
+ import { createInterface } from "readline";
1715
+
1716
+ // src/server/read-stream-line.ts
1717
+ import { z as z6 } from "zod";
1718
+ var TOOL_PREFIX = "mcp__erdraft__";
1719
+ var blockStart = z6.object({
1720
+ type: z6.literal("stream_event"),
1721
+ event: z6.object({
1722
+ type: z6.literal("content_block_start"),
1723
+ content_block: z6.discriminatedUnion("type", [
1724
+ z6.object({ type: z6.literal("text") }),
1725
+ z6.object({ type: z6.literal("tool_use"), name: z6.string() })
1726
+ ])
1727
+ })
1728
+ });
1729
+ var textDelta = z6.object({
1730
+ type: z6.literal("stream_event"),
1731
+ event: z6.object({
1732
+ type: z6.literal("content_block_delta"),
1733
+ delta: z6.object({ type: z6.literal("text_delta"), text: z6.string() })
1734
+ })
1735
+ });
1736
+ var result = z6.object({
1737
+ type: z6.literal("result"),
1738
+ session_id: z6.string(),
1739
+ is_error: z6.boolean()
1740
+ });
1741
+ function parsed(line) {
1742
+ try {
1743
+ return JSON.parse(line);
1744
+ } catch {
1745
+ return void 0;
1746
+ }
1747
+ }
1748
+ function readStreamLine(line) {
1749
+ const value = parsed(line);
1750
+ const start = blockStart.safeParse(value);
1751
+ if (start.success) {
1752
+ const block = start.data.event.content_block;
1753
+ return block.type === "text" ? { type: "text" } : { type: "tool", name: block.name.replace(TOOL_PREFIX, "") };
1754
+ }
1755
+ const delta = textDelta.safeParse(value);
1756
+ if (delta.success) {
1757
+ return { type: "delta", text: delta.data.event.delta.text };
1758
+ }
1759
+ const end = result.safeParse(value);
1760
+ if (end.success) {
1761
+ return {
1762
+ type: "done",
1763
+ session: end.data.session_id,
1764
+ error: end.data.is_error
1765
+ };
1766
+ }
1767
+ return null;
1768
+ }
1769
+
1770
+ // src/server/create-assistant.ts
1771
+ var askSchema = z7.object({
1772
+ message: z7.string().trim().min(1),
1773
+ session: z7.uuid().optional(),
1774
+ context: z7.object({
1775
+ draft: z7.object({ id: z7.string(), title: z7.string() }).optional(),
1776
+ table: z7.string().optional(),
1777
+ column: z7.string().optional()
1778
+ }).optional()
1779
+ });
1780
+ var SELF = {
1781
+ command: process.execPath,
1782
+ args: [...process.execArgv, ...process.argv.slice(1, 2), "mcp"]
1783
+ };
1784
+ function promptOf(config, context) {
1785
+ const seen = context ? [
1786
+ context.draft ? `The window shows draft ${context.draft.id} ("${context.draft.title}"); "this", "it" or "here" is that draft.` : "The window shows the schema as the code has it: no draft is open, whatever earlier messages said.",
1787
+ context.table ? `The person has the table ${context.table} in focus.` : "",
1788
+ context.column ? `Its column ${context.column} is lit: where a field of the app keeps its value, traced by Erdraft's inspector; "this field" or "this column" is it.` : ""
1789
+ ] : [
1790
+ "The person chose not to say what the window shows: ask which draft when it matters."
1791
+ ];
1792
+ return [
1793
+ `You are Erdraft's assistant, in the panel beside its window, on ${config.name}'s data model.`,
1794
+ INSTRUCTIONS,
1795
+ "Answer in the language the person writes in, briefly, as in a chat, in plain sentences (no Markdown headings nor tables). Read and propose with the erdraft tools alone: you cannot change the code nor the database, and say so when asked to. A draft you propose is drawn in the window at once; say what it does in a sentence, not table by table.",
1796
+ ...seen.filter(Boolean)
1797
+ ].join("\n\n");
1798
+ }
1799
+ function argumentsOf(config, ask) {
1800
+ return [
1801
+ "-p",
1802
+ "--output-format",
1803
+ "stream-json",
1804
+ "--verbose",
1805
+ "--include-partial-messages",
1806
+ "--mcp-config",
1807
+ JSON.stringify({
1808
+ mcpServers: { erdraft: SELF }
1809
+ }),
1810
+ "--strict-mcp-config",
1811
+ "--tools",
1812
+ "",
1813
+ "--allowedTools",
1814
+ "mcp__erdraft",
1815
+ "--model",
1816
+ config.model,
1817
+ "--append-system-prompt",
1818
+ promptOf(config, ask.context),
1819
+ ...ask.session ? ["--resume", ask.session] : []
1820
+ ];
1821
+ }
1822
+ function send(response, event) {
1823
+ response.write(`data: ${JSON.stringify(event)}
1824
+
1825
+ `);
1826
+ }
1827
+ function createAssistant(config, spawn = spawnProcess) {
1828
+ return async function ask(request, response) {
1829
+ const asked = askSchema.safeParse(JSON.parse(await text2(request) || "{}"));
1830
+ if (!asked.success) {
1831
+ response.writeHead(400, { "content-type": "application/json" });
1832
+ response.end(JSON.stringify({ error: z7.prettifyError(asked.error) }));
1833
+ return;
1834
+ }
1835
+ response.writeHead(200, {
1836
+ "content-type": "text/event-stream",
1837
+ "cache-control": "no-cache"
1838
+ });
1839
+ response.flushHeaders();
1840
+ const agent = spawn(config.claude, argumentsOf(config, asked.data), {
1841
+ cwd: config.root,
1842
+ stdio: ["pipe", "pipe", "pipe"]
1843
+ });
1844
+ let ended = false;
1845
+ agent.stdin.end(asked.data.message);
1846
+ response.on("close", () => agent.kill());
1847
+ agent.on("error", (error) => {
1848
+ ended = true;
1849
+ send(response, {
1850
+ type: "error",
1851
+ message: `Claude Code could not be started (${config.claude}): ${error.message}`
1852
+ });
1853
+ });
1854
+ createInterface({ input: agent.stdout }).on("line", (line) => {
1855
+ const event = readStreamLine(line);
1856
+ if (event) {
1857
+ ended ||= event.type === "done";
1858
+ send(response, event);
1859
+ }
1860
+ });
1861
+ const errors = [];
1862
+ agent.stderr.on("data", (chunk) => errors.push(chunk.toString()));
1863
+ agent.on("close", () => {
1864
+ if (!ended) {
1865
+ send(response, {
1866
+ type: "error",
1867
+ message: errors.join("").trim().split("\n").at(-1) || "The assistant stopped without an answer."
1868
+ });
1869
+ }
1870
+ response.end();
1871
+ });
1872
+ };
1873
+ }
1874
+
1875
+ // src/server/create-inspect-api.ts
1876
+ import { z as z8 } from "zod";
1877
+ import { text as text3 } from "stream/consumers";
1878
+
1879
+ // src/inspect/holds-value.ts
1880
+ var SHORTEST = 2;
1881
+ var WITHIN = 4;
1882
+ var DIGITS = 4;
1883
+ function digitsOf(value) {
1884
+ return value.replace(/\D/g, "");
1885
+ }
1886
+ function holdsValue(kept, typed) {
1887
+ const value = typed.trim().toLowerCase();
1888
+ if (value.length < SHORTEST || kept === null || kept === void 0) {
1889
+ return false;
1890
+ }
1891
+ const text5 = (typeof kept === "string" ? kept : JSON.stringify(kept)).toLowerCase();
1892
+ const digits = digitsOf(value);
1893
+ return text5.trim() === value || digits.length >= DIGITS && digits === digitsOf(text5) || value.length >= WITHIN && text5.includes(value);
1894
+ }
1895
+
1896
+ // src/server/create-inspect-api.ts
1897
+ var KEPT_WRITES = 500;
1898
+ var KEPT_FOR = 15 * 6e4;
1899
+ var writesSchema = z8.object({
1900
+ writes: z8.array(
1901
+ z8.object({ table: z8.string(), values: z8.record(z8.string(), z8.unknown()) })
1902
+ )
1903
+ });
1904
+ var tracedSchema = fieldSchema.omit({ tracedAt: true });
1905
+ function json(response, status, value, headers) {
1906
+ response.writeHead(status, { ...headers, "content-type": "application/json" });
1907
+ response.end(JSON.stringify(value));
1908
+ }
1909
+ async function body(request) {
1910
+ return JSON.parse(await text3(request) || "{}");
1911
+ }
1912
+ function createInspectApi(now = Date.now) {
1913
+ let written = [];
1914
+ return async function handle(request, response, drafts) {
1915
+ const url = new URL(String(request.url), "http://localhost");
1916
+ if (!url.pathname.startsWith("/api/inspect/")) {
1917
+ return false;
1918
+ }
1919
+ const { origin } = request.headers;
1920
+ const headers = origin ? {
1921
+ "access-control-allow-origin": origin,
1922
+ "access-control-allow-methods": "GET, POST",
1923
+ "access-control-allow-headers": "content-type",
1924
+ vary: "origin"
1925
+ } : {};
1926
+ if (request.method === "OPTIONS") {
1927
+ response.writeHead(204, headers);
1928
+ response.end();
1929
+ return true;
1930
+ }
1931
+ if (request.method === "POST" && url.pathname === "/api/inspect/writes") {
1932
+ const sent = writesSchema.safeParse(await body(request));
1933
+ if (!sent.success) {
1934
+ json(response, 400, { error: z8.prettifyError(sent.error) }, headers);
1935
+ return true;
1936
+ }
1937
+ const at = now();
1938
+ written = [
1939
+ ...written.filter((write) => at - write.at < KEPT_FOR),
1940
+ ...sent.data.writes.map((write) => ({ ...write, at }))
1941
+ ].slice(-KEPT_WRITES);
1942
+ json(response, 200, { ok: true }, headers);
1943
+ return true;
1944
+ }
1945
+ if (request.method === "GET" && url.pathname === "/api/inspect/find") {
1946
+ const value = url.searchParams.get("value") ?? "";
1947
+ const since = Number(url.searchParams.get("since") ?? 0);
1948
+ const found = /* @__PURE__ */ new Map();
1949
+ for (const write of [...written].reverse()) {
1950
+ if (write.at < since) {
1951
+ continue;
1952
+ }
1953
+ for (const [column, kept] of Object.entries(write.values)) {
1954
+ const key = `${write.table}.${column}`;
1955
+ if (!found.has(key) && holdsValue(kept, value)) {
1956
+ found.set(key, { table: write.table, column, at: write.at });
1957
+ }
1958
+ }
1959
+ }
1960
+ json(response, 200, { columns: [...found.values()] }, headers);
1961
+ return true;
1962
+ }
1963
+ if (request.method === "GET" && url.pathname === "/api/inspect/fields") {
1964
+ const page = url.searchParams.get("page") ?? void 0;
1965
+ json(response, 200, { fields: await drafts.readFields(page) }, headers);
1966
+ return true;
1967
+ }
1968
+ if (request.method === "POST" && url.pathname === "/api/inspect/fields") {
1969
+ const field = tracedSchema.safeParse(await body(request));
1970
+ if (!field.success) {
1971
+ json(response, 400, { error: z8.prettifyError(field.error) }, headers);
1972
+ return true;
1973
+ }
1974
+ const kept = await drafts.saveField(field.data);
1975
+ const { destination } = field.data;
1976
+ if (destination.kind === "column") {
1977
+ await drafts.show({
1978
+ table: destination.table,
1979
+ column: destination.column
1980
+ });
1981
+ }
1982
+ json(response, 200, { field: kept }, headers);
1983
+ return true;
1984
+ }
1985
+ return false;
1986
+ };
1987
+ }
1988
+
1989
+ // src/server/refusal-of.ts
1990
+ var LARGEST_BODY = 1e6;
1991
+ function loopback(url) {
1992
+ if (!URL.canParse(url)) {
1993
+ return false;
1994
+ }
1995
+ const { hostname } = new URL(url);
1996
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
1997
+ }
1998
+ function refusalOf(request, asker) {
1999
+ const host = `http://${request.headers.host ?? ""}`;
2000
+ const { origin } = request.headers;
2001
+ if (!loopback(host)) {
2002
+ return { status: 403, error: "Asked of a host that is not this machine" };
2003
+ }
2004
+ if (origin !== void 0 && (asker === "window" ? origin !== new URL(host).origin : !loopback(origin))) {
2005
+ return { status: 403, error: "Asked from a page of another site" };
2006
+ }
2007
+ if (Number(request.headers["content-length"] ?? 0) > LARGEST_BODY) {
2008
+ return { status: 413, error: "The body is too large" };
2009
+ }
2010
+ return void 0;
2011
+ }
2012
+
2013
+ // src/server/create-api.ts
2014
+ var SETTLE = 40;
2015
+ var DRAFT_ACTION = /^\/api\/drafts\/([^/]+)\/(adopt|discard)$/;
2016
+ var DRAFT_DELETE = /^\/api\/drafts\/([^/]+)$/;
2017
+ var layoutSchema = z9.object({ key: z9.string().min(1), places: placesSchema });
2018
+ function json2(response, status, value) {
2019
+ response.writeHead(status, { "content-type": "application/json" });
2020
+ response.end(JSON.stringify(value));
2021
+ }
2022
+ function createApi(config, { spawn } = {}) {
2023
+ const ask = createAssistant(config, spawn);
2024
+ const inspect = createInspectApi();
2025
+ const clients = /* @__PURE__ */ new Set();
2026
+ let timer;
2027
+ mkdirSync(config.store, { recursive: true });
2028
+ const watcher = watch(config.store, { recursive: true }, () => {
2029
+ clearTimeout(timer);
2030
+ timer = setTimeout(() => {
2031
+ for (const client of clients) {
2032
+ client.write("event: change\ndata: {}\n\n");
2033
+ }
2034
+ }, SETTLE);
2035
+ });
2036
+ async function handle(request, response) {
2037
+ const { pathname } = new URL(String(request.url), "http://localhost");
2038
+ if (!pathname.startsWith("/api/")) {
2039
+ return false;
2040
+ }
2041
+ const refused = refusalOf(
2042
+ request,
2043
+ pathname.startsWith("/api/inspect/") ? "app" : "window"
2044
+ );
2045
+ if (refused) {
2046
+ json2(response, refused.status, { error: refused.error });
2047
+ return true;
2048
+ }
2049
+ const database = await currentDatabase(config);
2050
+ const drafts = storeOf(config, database);
2051
+ if (await inspect(request, response, drafts)) {
2052
+ return true;
2053
+ }
2054
+ if (request.method === "GET" && pathname === "/api/state") {
2055
+ json2(response, 200, {
2056
+ name: config.name,
2057
+ port: config.port,
2058
+ databases: config.databases.map(({ id, name }) => ({ id, name })),
2059
+ database: { id: database.id, name: database.name },
2060
+ ...await drafts.read()
2061
+ });
2062
+ return true;
2063
+ }
2064
+ if (request.method === "GET" && pathname === "/api/events") {
2065
+ response.writeHead(200, {
2066
+ "content-type": "text/event-stream",
2067
+ "cache-control": "no-cache",
2068
+ connection: "keep-alive"
2069
+ });
2070
+ response.write(": connected\n\n");
2071
+ clients.add(response);
2072
+ request.on("close", () => clients.delete(response));
2073
+ return true;
2074
+ }
2075
+ if (request.method === "POST" && pathname === "/api/assistant") {
2076
+ await ask(request, response);
2077
+ return true;
2078
+ }
2079
+ if (request.method === "POST" && pathname === "/api/database") {
2080
+ const chosen = z9.object({ id: z9.string() }).safeParse(JSON.parse(await text4(request) || "{}"));
2081
+ try {
2082
+ const id = chosen.success ? chosen.data.id : "";
2083
+ json2(response, 200, await chooseDatabase(config, id));
2084
+ } catch (error) {
2085
+ json2(response, 404, { error: String(error) });
2086
+ }
2087
+ return true;
2088
+ }
2089
+ if (request.method === "PUT" && pathname === "/api/layout") {
2090
+ const layout = layoutSchema.safeParse(
2091
+ JSON.parse(await text4(request) || "{}")
2092
+ );
2093
+ if (!layout.success) {
2094
+ json2(response, 400, { error: z9.prettifyError(layout.error) });
2095
+ return true;
2096
+ }
2097
+ await drafts.saveLayout(layout.data.key, layout.data.places);
2098
+ json2(response, 200, { ok: true });
2099
+ return true;
2100
+ }
2101
+ const deleted = DRAFT_DELETE.exec(pathname);
2102
+ if (request.method === "DELETE" && deleted) {
2103
+ try {
2104
+ json2(response, 200, await drafts.deleteDraft(String(deleted[1])));
2105
+ } catch (error) {
2106
+ json2(response, 404, { error: String(error) });
2107
+ }
2108
+ return true;
2109
+ }
2110
+ const action = DRAFT_ACTION.exec(pathname);
2111
+ if (request.method === "POST" && action) {
2112
+ try {
2113
+ json2(
2114
+ response,
2115
+ 200,
2116
+ await drafts.setStatus(
2117
+ String(action[1]),
2118
+ action[2] === "adopt" ? "adopted" : "discarded"
2119
+ )
2120
+ );
2121
+ } catch (error) {
2122
+ json2(response, 404, { error: String(error) });
2123
+ }
2124
+ return true;
2125
+ }
2126
+ return false;
2127
+ }
2128
+ return {
2129
+ middleware(request, response, next) {
2130
+ handle(request, response).then(
2131
+ (handled) => handled ? void 0 : next(),
2132
+ next
2133
+ );
2134
+ },
2135
+ close() {
2136
+ clearTimeout(timer);
2137
+ watcher.close();
2138
+ for (const client of clients) {
2139
+ client.end();
2140
+ }
2141
+ }
2142
+ };
2143
+ }
2144
+
2145
+ export {
2146
+ DECISION_SECONDS,
2147
+ storeOf,
2148
+ currentDatabase,
2149
+ databaseOf,
2150
+ chooseDatabase,
2151
+ version_default,
2152
+ readSchema,
2153
+ writtenIn,
2154
+ readDraft,
2155
+ describeTable,
2156
+ describeTables,
2157
+ draftLine,
2158
+ describeDraft,
2159
+ describeDecision,
2160
+ createServer,
2161
+ createApi
2162
+ };