erdraft 0.0.0-stage → 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/LICENSE +21 -0
- package/README.md +221 -2
- package/dist/chunk-IQ5YYSCP.js +2464 -0
- package/dist/chunk-VSN3XJPO.js +99 -0
- package/dist/cli.js +758 -0
- package/dist/inspect/index.js +373 -0
- package/dist/inspect/logger.js +64 -0
- package/dist/read-schema-child.js +238 -0
- package/dist/studio-config-TCMRAVMR.js +32 -0
- package/dist/types/core/fields.d.ts +39 -0
- package/dist/types/inspect/field-of.d.ts +3 -0
- package/dist/types/inspect/holds-value.d.ts +2 -0
- package/dist/types/inspect/inspect-logger.d.ts +2 -0
- package/dist/types/inspect/inspector.d.ts +5 -0
- package/dist/types/inspect/listen-on-document.d.ts +5 -0
- package/dist/types/inspect/read-write.d.ts +5 -0
- package/dist/types/inspect/use-field-changes.d.ts +6 -0
- package/dist/types/inspect/watch-storage.d.ts +8 -0
- package/dist/window/apple-touch-icon.png +0 -0
- package/dist/window/assets/index-BCt7ERad.css +2 -0
- package/dist/window/assets/index-v2x3QhC-.js +132 -0
- package/dist/window/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/dist/window/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/dist/window/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/dist/window/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/dist/window/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/dist/window/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/dist/window/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/dist/window/favicon.ico +0 -0
- package/dist/window/favicon.png +0 -0
- package/dist/window/favicon.svg +1 -0
- package/dist/window/index.html +23 -0
- package/package.json +97 -3
- package/skill/SKILL.md +47 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,758 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
DECISION_SECONDS,
|
|
4
|
+
TUNNEL_IDS,
|
|
5
|
+
chooseDatabase,
|
|
6
|
+
createApi,
|
|
7
|
+
createServer,
|
|
8
|
+
currentDatabase,
|
|
9
|
+
databaseOf,
|
|
10
|
+
describeDecision,
|
|
11
|
+
describeDraft,
|
|
12
|
+
describeTable,
|
|
13
|
+
describeTables,
|
|
14
|
+
draftLine,
|
|
15
|
+
readDraft,
|
|
16
|
+
readSchema,
|
|
17
|
+
storeOf,
|
|
18
|
+
version_default,
|
|
19
|
+
writtenIn
|
|
20
|
+
} from "./chunk-IQ5YYSCP.js";
|
|
21
|
+
import "./chunk-VSN3XJPO.js";
|
|
22
|
+
|
|
23
|
+
// bin/erdraft.ts
|
|
24
|
+
import { fileURLToPath } from "url";
|
|
25
|
+
import { existsSync as existsSync2 } from "fs";
|
|
26
|
+
import { join as join7 } from "path";
|
|
27
|
+
import { text } from "stream/consumers";
|
|
28
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
29
|
+
|
|
30
|
+
// src/server/serve-window.ts
|
|
31
|
+
import { createServer as createServer2 } from "http";
|
|
32
|
+
import { readFile, stat } from "fs/promises";
|
|
33
|
+
import { extname, join } from "path";
|
|
34
|
+
var TYPES = {
|
|
35
|
+
".html": "text/html; charset=utf-8",
|
|
36
|
+
".js": "text/javascript; charset=utf-8",
|
|
37
|
+
".css": "text/css; charset=utf-8",
|
|
38
|
+
".svg": "image/svg+xml",
|
|
39
|
+
".png": "image/png",
|
|
40
|
+
".ico": "image/x-icon",
|
|
41
|
+
".woff2": "font/woff2"
|
|
42
|
+
};
|
|
43
|
+
async function fileOf(folder, url) {
|
|
44
|
+
const { pathname } = new URL(String(url), "http://localhost");
|
|
45
|
+
const path = join(folder, pathname);
|
|
46
|
+
const found = await stat(path).catch(() => void 0);
|
|
47
|
+
return found?.isFile() ? path : join(folder, "index.html");
|
|
48
|
+
}
|
|
49
|
+
function refuse(response, status, message) {
|
|
50
|
+
response.writeHead(status, { "content-type": "text/plain; charset=utf-8" });
|
|
51
|
+
response.end(message);
|
|
52
|
+
}
|
|
53
|
+
async function serveWindow(config, folder) {
|
|
54
|
+
const api = createApi(config);
|
|
55
|
+
const server = createServer2((request, response) => {
|
|
56
|
+
api.middleware(request, response, (error) => {
|
|
57
|
+
if (error) {
|
|
58
|
+
refuse(response, 500, String(error));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (request.method !== "GET" && request.method !== "HEAD") {
|
|
62
|
+
refuse(response, 404, "Not found");
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
fileOf(folder, request.url).then(async (path) => {
|
|
66
|
+
const body = await readFile(path);
|
|
67
|
+
response.writeHead(200, {
|
|
68
|
+
"content-type": TYPES[extname(path)] ?? "application/octet-stream"
|
|
69
|
+
});
|
|
70
|
+
response.end(body);
|
|
71
|
+
}).catch((failure) => refuse(response, 500, String(failure)));
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
server.on("close", api.close);
|
|
75
|
+
await new Promise((done, fail) => {
|
|
76
|
+
server.once("error", (error) => {
|
|
77
|
+
api.close();
|
|
78
|
+
fail(
|
|
79
|
+
error.code === "EADDRINUSE" ? new Error(
|
|
80
|
+
`Port ${config.port} is taken: stop what holds it, or give the window another "port" in .erdraft.json`
|
|
81
|
+
) : error
|
|
82
|
+
);
|
|
83
|
+
});
|
|
84
|
+
server.listen(config.port, "127.0.0.1", done);
|
|
85
|
+
});
|
|
86
|
+
return server;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// src/cli/run.ts
|
|
90
|
+
import { readFile as readFile5 } from "fs/promises";
|
|
91
|
+
import { resolve as resolve4 } from "path";
|
|
92
|
+
import { parseArgs } from "util";
|
|
93
|
+
import { z as z4 } from "zod";
|
|
94
|
+
|
|
95
|
+
// src/core/load-config.ts
|
|
96
|
+
import { z } from "zod";
|
|
97
|
+
import { join as join2, resolve, dirname, basename } from "path";
|
|
98
|
+
import { homedir } from "os";
|
|
99
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
100
|
+
import { createHash } from "crypto";
|
|
101
|
+
var CONFIG_FILE = ".erdraft.json";
|
|
102
|
+
var databaseSchema = z.object({
|
|
103
|
+
// The name the window's menu shows it by
|
|
104
|
+
name: z.string().min(1),
|
|
105
|
+
// The module of Drizzle that exports every table and enum (drizzle-kit's `schema`)
|
|
106
|
+
schema: z.string().min(1)
|
|
107
|
+
});
|
|
108
|
+
var configSchema = z.object({
|
|
109
|
+
// The project's name; none given, its folder's
|
|
110
|
+
name: z.string().min(1).optional(),
|
|
111
|
+
// The project's one database, its module of Drizzle; or its databases, each named, the window switching between
|
|
112
|
+
schema: z.string().min(1).optional(),
|
|
113
|
+
databases: z.array(databaseSchema).min(1).optional(),
|
|
114
|
+
// The folder Erdraft keeps the drafts in, shared by the CLI, the MCP and the window, and by every workspace of
|
|
115
|
+
// the project on the machine: `~` is the home folder; none given, ~/.erdraft/<the project's name>
|
|
116
|
+
store: z.string().min(1).optional(),
|
|
117
|
+
// The window's port
|
|
118
|
+
port: z.number().int().min(1).max(65535).default(9369),
|
|
119
|
+
// The window's assistant: the Claude Code it runs headless, and its model (an alias or a model's full name)
|
|
120
|
+
claude: z.string().min(1).default("claude"),
|
|
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")
|
|
124
|
+
});
|
|
125
|
+
function folderOf(name) {
|
|
126
|
+
const folder = name.normalize("NFKD").replace(new RegExp("\\p{Diacritic}", "gu"), "").toLocaleLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
127
|
+
return folder || createHash("sha256").update(name).digest("hex").slice(0, 12);
|
|
128
|
+
}
|
|
129
|
+
function storeOf2(folder, name, store) {
|
|
130
|
+
if (store === void 0) {
|
|
131
|
+
return join2(homedir(), ".erdraft", folderOf(name));
|
|
132
|
+
}
|
|
133
|
+
if (store === "~" || store.startsWith("~/")) {
|
|
134
|
+
return join2(homedir(), store.slice(1));
|
|
135
|
+
}
|
|
136
|
+
return resolve(folder, store);
|
|
137
|
+
}
|
|
138
|
+
function parsed(path, text2) {
|
|
139
|
+
try {
|
|
140
|
+
return configSchema.parse(JSON.parse(text2));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
throw new Error(
|
|
143
|
+
`${path} is not a config Erdraft reads: ${error instanceof z.ZodError ? z.prettifyError(error) : String(error)}`
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
async function readIfThere(path) {
|
|
148
|
+
try {
|
|
149
|
+
return await readFile2(path, "utf8");
|
|
150
|
+
} catch {
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
async function loadConfig(from) {
|
|
155
|
+
let folder = resolve(from);
|
|
156
|
+
for (; ; ) {
|
|
157
|
+
const text2 = await readIfThere(join2(folder, CONFIG_FILE));
|
|
158
|
+
if (text2 !== void 0) {
|
|
159
|
+
const {
|
|
160
|
+
schema,
|
|
161
|
+
databases,
|
|
162
|
+
name = basename(folder),
|
|
163
|
+
...config
|
|
164
|
+
} = parsed(join2(folder, CONFIG_FILE), text2);
|
|
165
|
+
const [first, ...rest] = (databases ?? (schema ? [{ name, schema }] : [])).map((database) => ({
|
|
166
|
+
id: folderOf(database.name),
|
|
167
|
+
name: database.name,
|
|
168
|
+
schema: resolve(folder, database.schema)
|
|
169
|
+
}));
|
|
170
|
+
if (!first) {
|
|
171
|
+
throw new Error(`${CONFIG_FILE} names no schema nor databases`);
|
|
172
|
+
}
|
|
173
|
+
const ids = new Set([first, ...rest].map((database) => database.id));
|
|
174
|
+
if (ids.size !== rest.length + 1) {
|
|
175
|
+
throw new Error(`${CONFIG_FILE} names two databases alike`);
|
|
176
|
+
}
|
|
177
|
+
return {
|
|
178
|
+
...config,
|
|
179
|
+
name,
|
|
180
|
+
databases: [first, ...rest],
|
|
181
|
+
store: storeOf2(folder, name, config.store),
|
|
182
|
+
root: folder,
|
|
183
|
+
workspace: createHash("sha256").update(folder).digest("hex").slice(0, 12)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
const parent = dirname(folder);
|
|
187
|
+
if (parent === folder) {
|
|
188
|
+
throw new Error(
|
|
189
|
+
`No ${CONFIG_FILE} in ${resolve(from)} or above it: erdraft init sets the project up`
|
|
190
|
+
);
|
|
191
|
+
}
|
|
192
|
+
folder = parent;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
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";
|
|
204
|
+
async function readIfThere2(path) {
|
|
205
|
+
try {
|
|
206
|
+
return await readFile3(path, "utf8");
|
|
207
|
+
} catch (error) {
|
|
208
|
+
if (error instanceof Error && "code" in error && error.code === "ENOENT") {
|
|
209
|
+
return void 0;
|
|
210
|
+
}
|
|
211
|
+
throw error;
|
|
212
|
+
}
|
|
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");
|
|
327
|
+
async function installSkill(root, source, force = false) {
|
|
328
|
+
const path = join5(root, SKILL_PATH);
|
|
329
|
+
const skill = await readFile4(source, "utf8");
|
|
330
|
+
const there = await readIfThere2(path);
|
|
331
|
+
if (there === skill) {
|
|
332
|
+
return { path, written: false };
|
|
333
|
+
}
|
|
334
|
+
if (there !== void 0 && !force) {
|
|
335
|
+
throw new Error(
|
|
336
|
+
`${SKILL_PATH} differs from this Erdraft's skill (changed, or of another version): --force writes it over`
|
|
337
|
+
);
|
|
338
|
+
}
|
|
339
|
+
await mkdir(dirname2(path), { recursive: true });
|
|
340
|
+
await writeFile2(path, skill);
|
|
341
|
+
return { path, written: true };
|
|
342
|
+
}
|
|
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
|
+
|
|
433
|
+
// src/cli/run.ts
|
|
434
|
+
var HELP = `erdraft: a project's data model, drafted with an agent while a person watches
|
|
435
|
+
|
|
436
|
+
Usage: erdraft <command> [options] [--database <id>]
|
|
437
|
+
erdraft --version
|
|
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
|
|
442
|
+
studio Open the window (the diagram, live)
|
|
443
|
+
mcp Serve the MCP on stdin and stdout, for an agent
|
|
444
|
+
read Read the schema again from the code
|
|
445
|
+
tables [--draft <id>] List the tables
|
|
446
|
+
table <name> [--draft <id>] Describe a table
|
|
447
|
+
drafts List the drafts
|
|
448
|
+
draft <id> Show a draft and what it changes
|
|
449
|
+
propose <file|-> --title <title> [--summary <text>] [--group <name>]
|
|
450
|
+
Propose a draft from a JSON array of operations
|
|
451
|
+
revise <id> [<file|->] [--replace] [--title <title>] [--summary <text>] [--group <name>]
|
|
452
|
+
Add operations to a draft (or put them in its place)
|
|
453
|
+
show [<table>] [--related] [--draft <id> | --schema]
|
|
454
|
+
Show it in the window: a table in focus, only it and the tables it joins, a draft
|
|
455
|
+
wait <id> [--seconds <n>] Wait until the person adopts a draft or sets it aside (30 s at most when not said)
|
|
456
|
+
adopt <id> Adopt a draft (the others of its group are set aside)
|
|
457
|
+
discard <id> Set a draft aside
|
|
458
|
+
delete <id> Delete a draft for good
|
|
459
|
+
databases List the project's databases, * the one worked on
|
|
460
|
+
install-skill [--force] Write Erdraft's skill into the project (.claude/skills/erdraft), for its agents
|
|
461
|
+
use <id> Work on another database (the window switches too)
|
|
462
|
+
|
|
463
|
+
A command works on the database chosen last (use), or on the one --database names. The nearest .erdraft.json says
|
|
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
|
+
]);
|
|
485
|
+
async function operationsFrom(path, options) {
|
|
486
|
+
if (path === void 0) {
|
|
487
|
+
return void 0;
|
|
488
|
+
}
|
|
489
|
+
const text2 = path === "-" ? await options.stdin() : await readFile5(resolve4(options.cwd, path), "utf8");
|
|
490
|
+
return JSON.parse(text2);
|
|
491
|
+
}
|
|
492
|
+
function required(value, what) {
|
|
493
|
+
if (!value) {
|
|
494
|
+
throw new Error(`Missing ${what}. Run erdraft --help`);
|
|
495
|
+
}
|
|
496
|
+
return value;
|
|
497
|
+
}
|
|
498
|
+
async function command(argv, options) {
|
|
499
|
+
const { values, positionals } = parseArgs({
|
|
500
|
+
args: argv,
|
|
501
|
+
allowPositionals: true,
|
|
502
|
+
options: {
|
|
503
|
+
draft: { type: "string" },
|
|
504
|
+
title: { type: "string" },
|
|
505
|
+
summary: { type: "string" },
|
|
506
|
+
group: { type: "string" },
|
|
507
|
+
replace: { type: "boolean" },
|
|
508
|
+
related: { type: "boolean" },
|
|
509
|
+
schema: { type: "boolean" },
|
|
510
|
+
database: { type: "string" },
|
|
511
|
+
seconds: { type: "string" },
|
|
512
|
+
force: { type: "boolean" },
|
|
513
|
+
name: { type: "string" },
|
|
514
|
+
help: { type: "boolean", short: "h" },
|
|
515
|
+
version: { type: "boolean", short: "v" }
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
const [name, argument] = positionals;
|
|
519
|
+
if (values.version) {
|
|
520
|
+
options.out(version_default);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
523
|
+
if (!name || values.help) {
|
|
524
|
+
options.out(HELP);
|
|
525
|
+
return;
|
|
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
|
+
}
|
|
540
|
+
const config = await loadConfig(options.cwd);
|
|
541
|
+
const database = values.database ? databaseOf(config, values.database) : await currentDatabase(config);
|
|
542
|
+
const drafts = storeOf(config, database);
|
|
543
|
+
async function base() {
|
|
544
|
+
return await drafts.readBase() ?? readSchema(config, database, drafts);
|
|
545
|
+
}
|
|
546
|
+
async function readOf(draft) {
|
|
547
|
+
return readDraft(
|
|
548
|
+
(await base()).schema,
|
|
549
|
+
draft ? (await drafts.draft(draft)).operations : []
|
|
550
|
+
);
|
|
551
|
+
}
|
|
552
|
+
async function schemaOf2(draft) {
|
|
553
|
+
return (await readOf(draft)).diff;
|
|
554
|
+
}
|
|
555
|
+
switch (name) {
|
|
556
|
+
case "studio":
|
|
557
|
+
await readSchema(config, database, drafts);
|
|
558
|
+
return options.startStudio(config);
|
|
559
|
+
case "mcp":
|
|
560
|
+
return options.startMcp(config);
|
|
561
|
+
case "read": {
|
|
562
|
+
const base2 = await readSchema(config, database, drafts);
|
|
563
|
+
options.out(
|
|
564
|
+
`Read ${base2.schema.tables.length} tables and ${base2.schema.enums.length} enums from ${base2.source}.`
|
|
565
|
+
);
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
case "tables":
|
|
569
|
+
options.out(describeTables(await schemaOf2(values.draft)));
|
|
570
|
+
return;
|
|
571
|
+
case "table": {
|
|
572
|
+
const tableName = required(argument, "the table\u2019s name");
|
|
573
|
+
const diff = await schemaOf2(values.draft);
|
|
574
|
+
const table = diff.tables.find((each) => each.name === tableName);
|
|
575
|
+
if (!table) {
|
|
576
|
+
throw new Error(`There is no table "${tableName}"`);
|
|
577
|
+
}
|
|
578
|
+
options.out(describeTable(table, diff));
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
case "drafts": {
|
|
582
|
+
const all = await drafts.readDrafts();
|
|
583
|
+
options.out(
|
|
584
|
+
all.length > 0 ? all.map(draftLine).join("\n") : "No drafts yet."
|
|
585
|
+
);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
case "draft": {
|
|
589
|
+
const draft = await drafts.draft(required(argument, "the draft\u2019s id"));
|
|
590
|
+
const { diff, written } = await readOf(draft.id);
|
|
591
|
+
options.out(describeDraft(draft, diff, written));
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
case "propose": {
|
|
595
|
+
await base();
|
|
596
|
+
const operations = await operationsFrom(
|
|
597
|
+
required(argument, "the operations\u2019 file"),
|
|
598
|
+
options
|
|
599
|
+
);
|
|
600
|
+
const draft = await drafts.propose({
|
|
601
|
+
title: required(values.title, "--title"),
|
|
602
|
+
summary: values.summary,
|
|
603
|
+
group: values.group,
|
|
604
|
+
operations
|
|
605
|
+
});
|
|
606
|
+
options.out(`Proposed ${draft.id}: ${draft.title}.`);
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
609
|
+
case "revise": {
|
|
610
|
+
await base();
|
|
611
|
+
const id = required(argument, "the draft\u2019s id");
|
|
612
|
+
const operations = await operationsFrom(positionals[2], options);
|
|
613
|
+
const draft = await drafts.revise(id, {
|
|
614
|
+
title: values.title,
|
|
615
|
+
summary: values.summary,
|
|
616
|
+
group: values.group,
|
|
617
|
+
mode: values.replace ? "replace" : "append",
|
|
618
|
+
operations
|
|
619
|
+
});
|
|
620
|
+
options.out(`Revised ${draft.id}: ${draft.title}.`);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
case "show": {
|
|
624
|
+
const draft = values.schema ? null : values.draft;
|
|
625
|
+
const diff = await schemaOf2(draft ?? void 0);
|
|
626
|
+
if (argument && !diff.tables.some((each) => each.name === argument)) {
|
|
627
|
+
throw new Error(`There is no table "${argument}"`);
|
|
628
|
+
}
|
|
629
|
+
await drafts.show({
|
|
630
|
+
table: argument ?? null,
|
|
631
|
+
related: values.related === true && argument !== void 0,
|
|
632
|
+
draft
|
|
633
|
+
});
|
|
634
|
+
options.out(`Shown in the window.`);
|
|
635
|
+
return;
|
|
636
|
+
}
|
|
637
|
+
case "wait": {
|
|
638
|
+
const seconds = Number(values.seconds ?? DECISION_SECONDS);
|
|
639
|
+
if (!Number.isInteger(seconds) || seconds < 1) {
|
|
640
|
+
throw new Error("--seconds is a whole number of seconds, 1 or more");
|
|
641
|
+
}
|
|
642
|
+
const draft = await drafts.decision(
|
|
643
|
+
required(argument, "the draft\u2019s id"),
|
|
644
|
+
seconds * 1e3
|
|
645
|
+
);
|
|
646
|
+
options.out(
|
|
647
|
+
describeDecision(
|
|
648
|
+
draft,
|
|
649
|
+
await drafts.readDrafts(),
|
|
650
|
+
seconds,
|
|
651
|
+
writtenIn((await base()).schema, draft.operations)
|
|
652
|
+
)
|
|
653
|
+
);
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
case "adopt":
|
|
657
|
+
case "discard": {
|
|
658
|
+
const draft = await drafts.setStatus(
|
|
659
|
+
required(argument, "the draft\u2019s id"),
|
|
660
|
+
name === "adopt" ? "adopted" : "discarded"
|
|
661
|
+
);
|
|
662
|
+
options.out(
|
|
663
|
+
`${name === "adopt" ? "Adopted" : "Discarded"} ${draft.id}: ${draft.title}.`
|
|
664
|
+
);
|
|
665
|
+
return;
|
|
666
|
+
}
|
|
667
|
+
case "delete": {
|
|
668
|
+
const draft = await drafts.deleteDraft(
|
|
669
|
+
required(argument, "the draft\u2019s id")
|
|
670
|
+
);
|
|
671
|
+
options.out(`Deleted ${draft.id}: ${draft.title}.`);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
case "databases":
|
|
675
|
+
options.out(
|
|
676
|
+
config.databases.map(
|
|
677
|
+
(each) => `${each.id === database.id ? "*" : " "} ${each.id} ${each.name}`
|
|
678
|
+
).join("\n")
|
|
679
|
+
);
|
|
680
|
+
return;
|
|
681
|
+
case "install-skill": {
|
|
682
|
+
const { path, written } = await installSkill(
|
|
683
|
+
config.root,
|
|
684
|
+
options.skill,
|
|
685
|
+
values.force
|
|
686
|
+
);
|
|
687
|
+
options.out(
|
|
688
|
+
written ? `Wrote Erdraft's skill to ${path}.` : `Erdraft's skill is already in ${path}, the same.`
|
|
689
|
+
);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
case "use": {
|
|
693
|
+
const chosen = await chooseDatabase(
|
|
694
|
+
config,
|
|
695
|
+
required(argument, "the database\u2019s id")
|
|
696
|
+
);
|
|
697
|
+
options.out(`Working on ${chosen.name}.`);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
var ERROR_NAME = /^\w*Error(?: \[\w+\])?: /;
|
|
702
|
+
async function run(argv, options) {
|
|
703
|
+
try {
|
|
704
|
+
await command(argv, options);
|
|
705
|
+
return 0;
|
|
706
|
+
} catch (error) {
|
|
707
|
+
options.err(
|
|
708
|
+
error instanceof z4.ZodError ? z4.prettifyError(error) : String(error).replace(ERROR_NAME, "")
|
|
709
|
+
);
|
|
710
|
+
return 1;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// bin/erdraft.ts
|
|
715
|
+
var WINDOW = fileURLToPath(new URL("./window", import.meta.url));
|
|
716
|
+
async function listen(config) {
|
|
717
|
+
if (existsSync2(join7(WINDOW, "index.html"))) {
|
|
718
|
+
return serveWindow(config, WINDOW);
|
|
719
|
+
}
|
|
720
|
+
const [{ createServer: createServer3 }, { default: studioConfig }] = await Promise.all([
|
|
721
|
+
import("vite"),
|
|
722
|
+
import("./studio-config-TCMRAVMR.js")
|
|
723
|
+
]);
|
|
724
|
+
const server = await createServer3(studioConfig(config));
|
|
725
|
+
await server.listen();
|
|
726
|
+
return server.httpServer;
|
|
727
|
+
}
|
|
728
|
+
process.stdout.on("error", (error) => {
|
|
729
|
+
if (error.code !== "EPIPE") {
|
|
730
|
+
throw error;
|
|
731
|
+
}
|
|
732
|
+
process.exit(0);
|
|
733
|
+
});
|
|
734
|
+
var code = await run(process.argv.slice(2), {
|
|
735
|
+
cwd: process.cwd(),
|
|
736
|
+
skill: fileURLToPath(new URL("../skill/SKILL.md", import.meta.url)),
|
|
737
|
+
out: (value) => process.stdout.write(`${value}
|
|
738
|
+
`),
|
|
739
|
+
err: (value) => process.stderr.write(`${value}
|
|
740
|
+
`),
|
|
741
|
+
stdin: () => text(process.stdin),
|
|
742
|
+
startMcp: async (config) => {
|
|
743
|
+
await createServer({ config }).connect(new StdioServerTransport());
|
|
744
|
+
await new Promise((done) => process.stdin.on("end", done));
|
|
745
|
+
},
|
|
746
|
+
startStudio: async (config) => {
|
|
747
|
+
const server = await listen(config);
|
|
748
|
+
process.stdout.write(
|
|
749
|
+
`Erdraft: ${config.name}'s data model at http://localhost:${config.port}
|
|
750
|
+
`
|
|
751
|
+
);
|
|
752
|
+
await new Promise((done) => server?.on("close", done));
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
process.stderr.write(
|
|
756
|
+
"",
|
|
757
|
+
() => process.stdout.write("", () => process.exit(code))
|
|
758
|
+
);
|