erdraft 0.0.1 → 0.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  DECISION_SECONDS,
4
+ TUNNEL_IDS,
4
5
  chooseDatabase,
5
6
  createApi,
6
7
  createServer,
@@ -16,13 +17,13 @@ import {
16
17
  storeOf,
17
18
  version_default,
18
19
  writtenIn
19
- } from "./chunk-SSLBX2OF.js";
20
+ } from "./chunk-IQ5YYSCP.js";
20
21
  import "./chunk-VSN3XJPO.js";
21
22
 
22
23
  // bin/erdraft.ts
23
24
  import { fileURLToPath } from "url";
24
- import { existsSync } from "fs";
25
- import { join as join4 } from "path";
25
+ import { existsSync as existsSync2 } from "fs";
26
+ import { join as join7 } from "path";
26
27
  import { text } from "stream/consumers";
27
28
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
28
29
 
@@ -86,10 +87,10 @@ async function serveWindow(config, folder) {
86
87
  }
87
88
 
88
89
  // src/cli/run.ts
89
- import { readFile as readFile4 } from "fs/promises";
90
- import { resolve as resolve2 } from "path";
90
+ import { readFile as readFile5 } from "fs/promises";
91
+ import { resolve as resolve4 } from "path";
91
92
  import { parseArgs } from "util";
92
- import { z as z2 } from "zod";
93
+ import { z as z4 } from "zod";
93
94
 
94
95
  // src/core/load-config.ts
95
96
  import { z } from "zod";
@@ -117,7 +118,9 @@ var configSchema = z.object({
117
118
  port: z.number().int().min(1).max(65535).default(9369),
118
119
  // The window's assistant: the Claude Code it runs headless, and its model (an alias or a model's full name)
119
120
  claude: z.string().min(1).default("claude"),
120
- model: z.string().min(1).default("sonnet")
121
+ model: z.string().min(1).default("sonnet"),
122
+ // The provider the window is shared over the internet through (src/share/tunnels.ts)
123
+ tunnel: z.enum(TUNNEL_IDS).default("ngrok")
121
124
  });
122
125
  function folderOf(name) {
123
126
  const folder = name.normalize("NFKD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLocaleLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
@@ -182,16 +185,22 @@ async function loadConfig(from) {
182
185
  }
183
186
  const parent = dirname(folder);
184
187
  if (parent === folder) {
185
- throw new Error(`No ${CONFIG_FILE} in ${resolve(from)} or above it`);
188
+ throw new Error(
189
+ `No ${CONFIG_FILE} in ${resolve(from)} or above it: erdraft init sets the project up`
190
+ );
186
191
  }
187
192
  folder = parent;
188
193
  }
189
194
  }
190
195
 
191
- // src/core/install-skill.ts
192
- import { mkdir, readFile as readFile3, writeFile } from "fs/promises";
193
- import { dirname as dirname2, join as join3 } from "path";
194
- var SKILL_PATH = join3(".claude", "skills", "erdraft", "SKILL.md");
196
+ // src/core/init-project.ts
197
+ import { z as z3 } from "zod";
198
+ import { join as join6, resolve as resolve3, relative, basename as basename2 } from "path";
199
+ import { existsSync } from "fs";
200
+ import { writeFile as writeFile3 } from "fs/promises";
201
+
202
+ // src/core/read-if-there.ts
203
+ import { readFile as readFile3 } from "fs/promises";
195
204
  async function readIfThere2(path) {
196
205
  try {
197
206
  return await readFile3(path, "utf8");
@@ -202,9 +211,122 @@ async function readIfThere2(path) {
202
211
  throw error;
203
212
  }
204
213
  }
214
+
215
+ // src/core/find-schema.ts
216
+ import { readdir, stat as stat2 } from "fs/promises";
217
+ import { join as join3, resolve as resolve2 } from "path";
218
+ var DRIZZLE_CONFIGS = [
219
+ "drizzle.config.ts",
220
+ "drizzle.config.mts",
221
+ "drizzle.config.cts",
222
+ "drizzle.config.js",
223
+ "drizzle.config.mjs",
224
+ "drizzle.config.cjs",
225
+ "drizzle.config.json"
226
+ ];
227
+ var USUAL_SCHEMAS = [
228
+ "src/db/schema.ts",
229
+ "src/db/schema/index.ts",
230
+ "src/server/db/schema.ts",
231
+ "src/lib/db/schema.ts",
232
+ "src/schema.ts",
233
+ "db/schema.ts",
234
+ "lib/db/schema.ts",
235
+ "drizzle/schema.ts"
236
+ ];
237
+ var WORKSPACE_FOLDERS = ["packages", "apps"];
238
+ var SCHEMA_KEY = /(?:^|[\s{,])["']?schema["']?\s*:\s*(["'`])([^"'`*]+)\1/;
239
+ async function moduleAt(path) {
240
+ const found = await stat2(path).catch(() => void 0);
241
+ if (found?.isFile()) {
242
+ return path;
243
+ }
244
+ if (found?.isDirectory()) {
245
+ return moduleAt(join3(path, "index.ts"));
246
+ }
247
+ return void 0;
248
+ }
249
+ async function schemaIn(folder) {
250
+ for (const name of DRIZZLE_CONFIGS) {
251
+ const text2 = await readIfThere2(join3(folder, name));
252
+ const path = text2?.match(SCHEMA_KEY)?.[2];
253
+ const found = path && await moduleAt(resolve2(folder, path));
254
+ if (found) {
255
+ return found;
256
+ }
257
+ }
258
+ for (const path of USUAL_SCHEMAS) {
259
+ const found = await moduleAt(join3(folder, path));
260
+ if (found) {
261
+ return found;
262
+ }
263
+ }
264
+ return void 0;
265
+ }
266
+ async function workspacesOf(root) {
267
+ const folders = [];
268
+ for (const parent of WORKSPACE_FOLDERS) {
269
+ const entries = await readdir(join3(root, parent), {
270
+ withFileTypes: true
271
+ }).catch(() => []);
272
+ const names = entries.filter((each) => each.isDirectory()).map((each) => each.name).sort();
273
+ for (const name of names) {
274
+ folders.push(join3(root, parent, name));
275
+ }
276
+ }
277
+ return folders;
278
+ }
279
+ async function findSchema(root) {
280
+ for (const folder of [root, ...await workspacesOf(root)]) {
281
+ const found = await schemaIn(folder);
282
+ if (found) {
283
+ return found;
284
+ }
285
+ }
286
+ return void 0;
287
+ }
288
+
289
+ // src/core/add-mcp-server.ts
290
+ import { z as z2 } from "zod";
291
+ import { join as join4 } from "path";
292
+ import { writeFile } from "fs/promises";
293
+ var MCP_FILE = ".mcp.json";
294
+ var ERDRAFT_SERVER = { command: "npx", args: ["-y", "erdraft", "mcp"] };
295
+ var mcpFile = z2.looseObject({
296
+ mcpServers: z2.record(z2.string(), z2.unknown()).optional()
297
+ });
298
+ function parsed2(text2) {
299
+ try {
300
+ return mcpFile.parse(JSON.parse(text2));
301
+ } catch (error) {
302
+ throw new Error(
303
+ `${MCP_FILE} is not one Erdraft can add to: ${error instanceof z2.ZodError ? z2.prettifyError(error) : String(error)}`
304
+ );
305
+ }
306
+ }
307
+ async function addMcpServer(root) {
308
+ const path = join4(root, MCP_FILE);
309
+ const text2 = await readIfThere2(path);
310
+ const file = text2 === void 0 ? {} : parsed2(text2);
311
+ if (file.mcpServers?.erdraft !== void 0) {
312
+ return { path, added: false };
313
+ }
314
+ const servers = { ...file.mcpServers, erdraft: ERDRAFT_SERVER };
315
+ await writeFile(
316
+ path,
317
+ `${JSON.stringify({ ...file, mcpServers: servers }, null, 2)}
318
+ `
319
+ );
320
+ return { path, added: true };
321
+ }
322
+
323
+ // src/core/install-skill.ts
324
+ import { mkdir, readFile as readFile4, writeFile as writeFile2 } from "fs/promises";
325
+ import { dirname as dirname2, join as join5 } from "path";
326
+ var SKILL_PATH = join5(".claude", "skills", "erdraft", "SKILL.md");
205
327
  async function installSkill(root, source, force = false) {
206
- const path = join3(root, SKILL_PATH);
207
- const skill = await readFile3(source, "utf8");
328
+ const path = join5(root, SKILL_PATH);
329
+ const skill = await readFile4(source, "utf8");
208
330
  const there = await readIfThere2(path);
209
331
  if (there === skill) {
210
332
  return { path, written: false };
@@ -215,16 +337,108 @@ async function installSkill(root, source, force = false) {
215
337
  );
216
338
  }
217
339
  await mkdir(dirname2(path), { recursive: true });
218
- await writeFile(path, skill);
340
+ await writeFile2(path, skill);
219
341
  return { path, written: true };
220
342
  }
221
343
 
344
+ // src/core/init-project.ts
345
+ var packageFile = z3.looseObject({
346
+ name: z3.string().min(1).optional(),
347
+ dependencies: z3.record(z3.string(), z3.string()).optional(),
348
+ devDependencies: z3.record(z3.string(), z3.string()).optional()
349
+ });
350
+ var INSTALLS = [
351
+ ["pnpm-workspace.yaml", "pnpm add -D -w erdraft"],
352
+ ["pnpm-lock.yaml", "pnpm add -D erdraft"],
353
+ ["bun.lock", "bun add -d erdraft"],
354
+ ["bun.lockb", "bun add -d erdraft"],
355
+ ["yarn.lock", "yarn add -D erdraft"]
356
+ ];
357
+ async function packageOf(root) {
358
+ const text2 = await readIfThere2(join6(root, "package.json")) ?? "{}";
359
+ try {
360
+ return packageFile.parse(JSON.parse(text2));
361
+ } catch {
362
+ return {};
363
+ }
364
+ }
365
+ async function schemaOf(root, given) {
366
+ const path = given === void 0 ? await findSchema(root) : resolve3(root, given);
367
+ if (path === void 0) {
368
+ throw new Error(
369
+ "Found no Drizzle schema here: erdraft init <the module that exports every table>"
370
+ );
371
+ }
372
+ if (!existsSync(path)) {
373
+ throw new Error(`There is no ${given}`);
374
+ }
375
+ return relative(root, path);
376
+ }
377
+ function installOf(root) {
378
+ const found = INSTALLS.find(([file]) => existsSync(join6(root, file)));
379
+ return found ? found[1] : "npm install --save-dev erdraft";
380
+ }
381
+ async function initProject(options) {
382
+ const { root, out } = options;
383
+ const project = await packageOf(root);
384
+ if (existsSync(join6(root, CONFIG_FILE)) && !options.force) {
385
+ out(`Kept ${CONFIG_FILE}, already here (--force writes it again).`);
386
+ } else {
387
+ const schema = await schemaOf(root, options.schema);
388
+ const name = options.name ?? project.name ?? basename2(root);
389
+ await writeFile3(
390
+ join6(root, CONFIG_FILE),
391
+ `${JSON.stringify({ name, schema }, null, 2)}
392
+ `
393
+ );
394
+ out(`Wrote ${CONFIG_FILE}: ${name}, its schema ${schema}.`);
395
+ }
396
+ const mcp = await addMcpServer(root);
397
+ out(
398
+ mcp.added ? `Added Erdraft's MCP to ${MCP_FILE}.` : `${MCP_FILE} has Erdraft's MCP already.`
399
+ );
400
+ const skill = await installSkill(root, options.skill, options.force);
401
+ out(
402
+ skill.written ? `Wrote Erdraft's skill to ${relative(root, skill.path)}.` : `Erdraft's skill is in ${relative(root, skill.path)} already.`
403
+ );
404
+ const config = await loadConfig(root);
405
+ const [database] = config.databases;
406
+ const base = await readSchema(config, database, storeOf(config, database));
407
+ const installed = project.dependencies?.erdraft ?? project.devDependencies?.erdraft;
408
+ out(
409
+ `Read ${base.schema.tables.length} tables and ${base.schema.enums.length} enums from ${base.source}.`
410
+ );
411
+ const studio = [
412
+ "npx erdraft studio",
413
+ `opens the window, at http://localhost:${config.port}`
414
+ ];
415
+ const steps = installed ? [studio] : [
416
+ [
417
+ installOf(root),
418
+ "keeps this version in the project, which npx erdraft then runs"
419
+ ],
420
+ studio
421
+ ];
422
+ const width = Math.max(...steps.map(([command2]) => command2.length));
423
+ out(
424
+ [
425
+ "",
426
+ "Next:",
427
+ ...steps.map(([command2, what]) => ` ${command2.padEnd(width)} ${what}`),
428
+ ' Then, in a new Claude Code session with its MCP approved, ask for a draft: "propose two ways to tag projects"'
429
+ ].join("\n")
430
+ );
431
+ }
432
+
222
433
  // src/cli/run.ts
223
434
  var HELP = `erdraft: a project's data model, drafted with an agent while a person watches
224
435
 
225
436
  Usage: erdraft <command> [options] [--database <id>]
226
437
  erdraft --version
227
438
 
439
+ init [<schema>] [--name <name>] [--force]
440
+ Set the project up: .erdraft.json (its Drizzle schema found, or the one given), the MCP
441
+ in .mcp.json, the skill for its agents
228
442
  studio Open the window (the diagram, live)
229
443
  mcp Serve the MCP on stdin and stdout, for an agent
230
444
  read Read the schema again from the code
@@ -248,11 +462,31 @@ Usage: erdraft <command> [options] [--database <id>]
248
462
 
249
463
  A command works on the database chosen last (use), or on the one --database names. The nearest .erdraft.json says
250
464
  where the databases' schemas and their drafts are.`;
465
+ var COMMANDS = /* @__PURE__ */ new Set([
466
+ "init",
467
+ "studio",
468
+ "mcp",
469
+ "read",
470
+ "tables",
471
+ "table",
472
+ "drafts",
473
+ "draft",
474
+ "propose",
475
+ "revise",
476
+ "show",
477
+ "wait",
478
+ "adopt",
479
+ "discard",
480
+ "delete",
481
+ "databases",
482
+ "install-skill",
483
+ "use"
484
+ ]);
251
485
  async function operationsFrom(path, options) {
252
486
  if (path === void 0) {
253
487
  return void 0;
254
488
  }
255
- const text2 = path === "-" ? await options.stdin() : await readFile4(resolve2(options.cwd, path), "utf8");
489
+ const text2 = path === "-" ? await options.stdin() : await readFile5(resolve4(options.cwd, path), "utf8");
256
490
  return JSON.parse(text2);
257
491
  }
258
492
  function required(value, what) {
@@ -276,6 +510,7 @@ async function command(argv, options) {
276
510
  database: { type: "string" },
277
511
  seconds: { type: "string" },
278
512
  force: { type: "boolean" },
513
+ name: { type: "string" },
279
514
  help: { type: "boolean", short: "h" },
280
515
  version: { type: "boolean", short: "v" }
281
516
  }
@@ -289,6 +524,19 @@ async function command(argv, options) {
289
524
  options.out(HELP);
290
525
  return;
291
526
  }
527
+ if (!COMMANDS.has(name)) {
528
+ throw new Error(`Unknown command "${name}". Run erdraft --help`);
529
+ }
530
+ if (name === "init") {
531
+ return initProject({
532
+ root: options.cwd,
533
+ skill: options.skill,
534
+ schema: argument,
535
+ name: values.name,
536
+ force: values.force,
537
+ out: options.out
538
+ });
539
+ }
292
540
  const config = await loadConfig(options.cwd);
293
541
  const database = values.database ? databaseOf(config, values.database) : await currentDatabase(config);
294
542
  const drafts = storeOf(config, database);
@@ -301,7 +549,7 @@ async function command(argv, options) {
301
549
  draft ? (await drafts.draft(draft)).operations : []
302
550
  );
303
551
  }
304
- async function schemaOf(draft) {
552
+ async function schemaOf2(draft) {
305
553
  return (await readOf(draft)).diff;
306
554
  }
307
555
  switch (name) {
@@ -318,11 +566,11 @@ async function command(argv, options) {
318
566
  return;
319
567
  }
320
568
  case "tables":
321
- options.out(describeTables(await schemaOf(values.draft)));
569
+ options.out(describeTables(await schemaOf2(values.draft)));
322
570
  return;
323
571
  case "table": {
324
572
  const tableName = required(argument, "the table\u2019s name");
325
- const diff = await schemaOf(values.draft);
573
+ const diff = await schemaOf2(values.draft);
326
574
  const table = diff.tables.find((each) => each.name === tableName);
327
575
  if (!table) {
328
576
  throw new Error(`There is no table "${tableName}"`);
@@ -374,7 +622,7 @@ async function command(argv, options) {
374
622
  }
375
623
  case "show": {
376
624
  const draft = values.schema ? null : values.draft;
377
- const diff = await schemaOf(draft ?? void 0);
625
+ const diff = await schemaOf2(draft ?? void 0);
378
626
  if (argument && !diff.tables.some((each) => each.name === argument)) {
379
627
  throw new Error(`There is no table "${argument}"`);
380
628
  }
@@ -447,10 +695,7 @@ async function command(argv, options) {
447
695
  required(argument, "the database\u2019s id")
448
696
  );
449
697
  options.out(`Working on ${chosen.name}.`);
450
- return;
451
698
  }
452
- default:
453
- throw new Error(`Unknown command "${name}". Run erdraft --help`);
454
699
  }
455
700
  }
456
701
  var ERROR_NAME = /^\w*Error(?: \[\w+\])?: /;
@@ -460,7 +705,7 @@ async function run(argv, options) {
460
705
  return 0;
461
706
  } catch (error) {
462
707
  options.err(
463
- error instanceof z2.ZodError ? z2.prettifyError(error) : String(error).replace(ERROR_NAME, "")
708
+ error instanceof z4.ZodError ? z4.prettifyError(error) : String(error).replace(ERROR_NAME, "")
464
709
  );
465
710
  return 1;
466
711
  }
@@ -469,17 +714,23 @@ async function run(argv, options) {
469
714
  // bin/erdraft.ts
470
715
  var WINDOW = fileURLToPath(new URL("./window", import.meta.url));
471
716
  async function listen(config) {
472
- if (existsSync(join4(WINDOW, "index.html"))) {
717
+ if (existsSync2(join7(WINDOW, "index.html"))) {
473
718
  return serveWindow(config, WINDOW);
474
719
  }
475
720
  const [{ createServer: createServer3 }, { default: studioConfig }] = await Promise.all([
476
721
  import("vite"),
477
- import("./studio-config-TIVT4MNY.js")
722
+ import("./studio-config-TCMRAVMR.js")
478
723
  ]);
479
724
  const server = await createServer3(studioConfig(config));
480
725
  await server.listen();
481
726
  return server.httpServer;
482
727
  }
728
+ process.stdout.on("error", (error) => {
729
+ if (error.code !== "EPIPE") {
730
+ throw error;
731
+ }
732
+ process.exit(0);
733
+ });
483
734
  var code = await run(process.argv.slice(2), {
484
735
  cwd: process.cwd(),
485
736
  skill: fileURLToPath(new URL("../skill/SKILL.md", import.meta.url)),
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApi
3
- } from "./chunk-SSLBX2OF.js";
3
+ } from "./chunk-IQ5YYSCP.js";
4
4
  import "./chunk-VSN3XJPO.js";
5
5
 
6
6
  // src/server/studio-config.ts