@chamuka-labs/drawit-cli 0.1.0
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/index.js +2817 -0
- package/package.json +54 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2817 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
// src/cli.ts
|
|
5
|
+
var import_commander = require("commander");
|
|
6
|
+
|
|
7
|
+
// src/utils/io.ts
|
|
8
|
+
var import_node_fs = require("fs");
|
|
9
|
+
function readInput(filePath, stdin = false) {
|
|
10
|
+
if (stdin || filePath === "-") {
|
|
11
|
+
return readStdin();
|
|
12
|
+
}
|
|
13
|
+
if (!filePath) {
|
|
14
|
+
throw new Error("No input file specified. Use --stdin to read from stdin.");
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
return (0, import_node_fs.readFileSync)(filePath, "utf-8");
|
|
18
|
+
} catch (err) {
|
|
19
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
20
|
+
throw new Error(`Failed to read file "${filePath}": ${message}`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function readStdin() {
|
|
24
|
+
const chunks = [];
|
|
25
|
+
const fd = 0;
|
|
26
|
+
try {
|
|
27
|
+
const buf = Buffer.alloc(65536);
|
|
28
|
+
let bytesRead;
|
|
29
|
+
while (true) {
|
|
30
|
+
try {
|
|
31
|
+
bytesRead = (0, import_node_fs.readSync)(fd, buf, 0, buf.length, null);
|
|
32
|
+
if (bytesRead === 0) break;
|
|
33
|
+
chunks.push(Buffer.from(buf.subarray(0, bytesRead)));
|
|
34
|
+
} catch {
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch {
|
|
39
|
+
}
|
|
40
|
+
return Buffer.concat(chunks).toString("utf-8");
|
|
41
|
+
}
|
|
42
|
+
function writeOutput(content, outputPath) {
|
|
43
|
+
if (!outputPath || outputPath === "-") {
|
|
44
|
+
process.stdout.write(content);
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
try {
|
|
48
|
+
(0, import_node_fs.writeFileSync)(outputPath, content, "utf-8");
|
|
49
|
+
} catch (err) {
|
|
50
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
51
|
+
throw new Error(`Failed to write to "${outputPath}": ${message}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ../chamuka-drawit-sdk/dist/types/elements.js
|
|
56
|
+
var import_zod = require("zod");
|
|
57
|
+
var PositionSchema = import_zod.z.object({
|
|
58
|
+
x: import_zod.z.number(),
|
|
59
|
+
y: import_zod.z.number()
|
|
60
|
+
});
|
|
61
|
+
var SizeSchema = import_zod.z.object({
|
|
62
|
+
width: import_zod.z.number().positive(),
|
|
63
|
+
height: import_zod.z.number().positive()
|
|
64
|
+
});
|
|
65
|
+
var CornerRadiiSchema = import_zod.z.object({
|
|
66
|
+
topLeft: import_zod.z.number().min(0),
|
|
67
|
+
topRight: import_zod.z.number().min(0),
|
|
68
|
+
bottomRight: import_zod.z.number().min(0),
|
|
69
|
+
bottomLeft: import_zod.z.number().min(0)
|
|
70
|
+
});
|
|
71
|
+
var EdgeRoutingSchema = import_zod.z.enum(["straight", "orthogonal", "bezier"]);
|
|
72
|
+
var StyleSchema = import_zod.z.object({
|
|
73
|
+
fillStyle: import_zod.z.string().optional(),
|
|
74
|
+
strokeStyle: import_zod.z.string().optional(),
|
|
75
|
+
lineWidth: import_zod.z.number().min(0).optional(),
|
|
76
|
+
fillOpacity: import_zod.z.number().min(0).max(1).optional(),
|
|
77
|
+
strokeOpacity: import_zod.z.number().min(0).max(1).optional(),
|
|
78
|
+
cornerRadii: CornerRadiiSchema.optional(),
|
|
79
|
+
cornerRadius: import_zod.z.number().min(0).optional(),
|
|
80
|
+
lineDash: import_zod.z.array(import_zod.z.number()).optional(),
|
|
81
|
+
routing: EdgeRoutingSchema.optional(),
|
|
82
|
+
lineCap: import_zod.z.enum(["butt", "round", "square"]).optional(),
|
|
83
|
+
lineJoin: import_zod.z.enum(["miter", "round", "bevel"]).optional(),
|
|
84
|
+
shadowColor: import_zod.z.string().optional(),
|
|
85
|
+
shadowBlur: import_zod.z.number().min(0).optional(),
|
|
86
|
+
shadowOffsetX: import_zod.z.number().optional(),
|
|
87
|
+
shadowOffsetY: import_zod.z.number().optional()
|
|
88
|
+
});
|
|
89
|
+
var TextPropsSchema = import_zod.z.object({
|
|
90
|
+
content: import_zod.z.string(),
|
|
91
|
+
fontSize: import_zod.z.number().positive().optional(),
|
|
92
|
+
fontFamily: import_zod.z.string().optional(),
|
|
93
|
+
color: import_zod.z.string().optional(),
|
|
94
|
+
textAlign: import_zod.z.enum(["left", "center", "right"]).optional(),
|
|
95
|
+
verticalAlign: import_zod.z.enum(["top", "middle", "bottom"]).optional(),
|
|
96
|
+
padding: import_zod.z.number().min(0).optional(),
|
|
97
|
+
fontWeight: import_zod.z.union([import_zod.z.string(), import_zod.z.number()]).optional(),
|
|
98
|
+
fontStyle: import_zod.z.enum(["normal", "italic", "oblique"]).optional()
|
|
99
|
+
});
|
|
100
|
+
var ShapeTypeSchema = import_zod.z.enum([
|
|
101
|
+
"rectangle",
|
|
102
|
+
"ellipse",
|
|
103
|
+
"diamond",
|
|
104
|
+
"triangle",
|
|
105
|
+
"hexagon",
|
|
106
|
+
"star",
|
|
107
|
+
"polygon",
|
|
108
|
+
"icon",
|
|
109
|
+
"polyline",
|
|
110
|
+
"line"
|
|
111
|
+
]);
|
|
112
|
+
var NodeElementSchema = import_zod.z.object({
|
|
113
|
+
id: import_zod.z.string(),
|
|
114
|
+
type: import_zod.z.literal("node"),
|
|
115
|
+
position: PositionSchema,
|
|
116
|
+
size: SizeSchema.optional(),
|
|
117
|
+
shape: ShapeTypeSchema.optional(),
|
|
118
|
+
text: import_zod.z.union([import_zod.z.string(), TextPropsSchema]).optional(),
|
|
119
|
+
style: StyleSchema.optional(),
|
|
120
|
+
angle: import_zod.z.number().optional(),
|
|
121
|
+
zIndex: import_zod.z.number().optional(),
|
|
122
|
+
constrainAspectRatio: import_zod.z.boolean().optional(),
|
|
123
|
+
metadata: import_zod.z.record(import_zod.z.any()).optional()
|
|
124
|
+
});
|
|
125
|
+
var EdgeLabelSchema = import_zod.z.object({
|
|
126
|
+
text: import_zod.z.string(),
|
|
127
|
+
fontSize: import_zod.z.number().positive().optional(),
|
|
128
|
+
color: import_zod.z.string().optional(),
|
|
129
|
+
position: import_zod.z.number().min(0).max(1).optional()
|
|
130
|
+
});
|
|
131
|
+
var EdgeElementSchema = import_zod.z.object({
|
|
132
|
+
id: import_zod.z.string(),
|
|
133
|
+
type: import_zod.z.literal("edge"),
|
|
134
|
+
source: import_zod.z.string(),
|
|
135
|
+
target: import_zod.z.string(),
|
|
136
|
+
label: import_zod.z.union([import_zod.z.string(), EdgeLabelSchema]).optional(),
|
|
137
|
+
style: StyleSchema.optional(),
|
|
138
|
+
position: PositionSchema.optional(),
|
|
139
|
+
zIndex: import_zod.z.number().optional(),
|
|
140
|
+
points: import_zod.z.array(PositionSchema).optional(),
|
|
141
|
+
metadata: import_zod.z.record(import_zod.z.any()).optional()
|
|
142
|
+
});
|
|
143
|
+
var DiagramElementSchema = import_zod.z.discriminatedUnion("type", [
|
|
144
|
+
NodeElementSchema,
|
|
145
|
+
EdgeElementSchema
|
|
146
|
+
]);
|
|
147
|
+
var ElementUpdateSchema = import_zod.z.object({
|
|
148
|
+
position: PositionSchema.optional(),
|
|
149
|
+
size: SizeSchema.optional(),
|
|
150
|
+
text: import_zod.z.union([import_zod.z.string(), TextPropsSchema]).optional(),
|
|
151
|
+
style: StyleSchema.optional(),
|
|
152
|
+
angle: import_zod.z.number().optional(),
|
|
153
|
+
zIndex: import_zod.z.number().optional(),
|
|
154
|
+
metadata: import_zod.z.record(import_zod.z.any()).optional()
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
// ../chamuka-drawit-sdk/dist/tools/definitions.js
|
|
158
|
+
var import_zod2 = require("zod");
|
|
159
|
+
var AddNodeInputSchema = import_zod2.z.object({
|
|
160
|
+
id: import_zod2.z.string().optional().describe("Unique identifier for the node"),
|
|
161
|
+
position: PositionSchema.describe("Position in canvas coordinates"),
|
|
162
|
+
size: SizeSchema.optional().describe("Size of the node"),
|
|
163
|
+
shape: ShapeTypeSchema.optional().describe("Shape type"),
|
|
164
|
+
text: import_zod2.z.union([import_zod2.z.string(), TextPropsSchema]).optional().describe("Text content"),
|
|
165
|
+
style: StyleSchema.optional().describe("Visual styling options"),
|
|
166
|
+
angle: import_zod2.z.number().optional().describe("Rotation angle in degrees"),
|
|
167
|
+
zIndex: import_zod2.z.number().optional().describe("Layer order (higher = on top)"),
|
|
168
|
+
metadata: import_zod2.z.record(import_zod2.z.unknown()).optional().describe("Custom metadata")
|
|
169
|
+
});
|
|
170
|
+
var AddEdgeInputSchema = import_zod2.z.object({
|
|
171
|
+
id: import_zod2.z.string().optional().describe("Unique identifier for the edge"),
|
|
172
|
+
source: import_zod2.z.string().describe("ID of the source node"),
|
|
173
|
+
target: import_zod2.z.string().describe("ID of the target node"),
|
|
174
|
+
label: import_zod2.z.union([import_zod2.z.string(), EdgeLabelSchema]).optional().describe("Edge label"),
|
|
175
|
+
style: StyleSchema.optional().describe("Visual styling options"),
|
|
176
|
+
zIndex: import_zod2.z.number().optional().describe("Layer order"),
|
|
177
|
+
metadata: import_zod2.z.record(import_zod2.z.unknown()).optional().describe("Custom metadata")
|
|
178
|
+
});
|
|
179
|
+
var ModifyElementInputSchema = import_zod2.z.object({
|
|
180
|
+
elementId: import_zod2.z.string().describe("ID of the element to modify"),
|
|
181
|
+
position: PositionSchema.optional().describe("New position"),
|
|
182
|
+
size: SizeSchema.optional().describe("New size"),
|
|
183
|
+
text: import_zod2.z.union([import_zod2.z.string(), TextPropsSchema]).optional().describe("New text"),
|
|
184
|
+
style: StyleSchema.optional().describe("Style updates"),
|
|
185
|
+
angle: import_zod2.z.number().optional().describe("New rotation angle"),
|
|
186
|
+
zIndex: import_zod2.z.number().optional().describe("New layer order"),
|
|
187
|
+
metadata: import_zod2.z.record(import_zod2.z.unknown()).optional().describe("Metadata updates")
|
|
188
|
+
});
|
|
189
|
+
var RemoveElementInputSchema = import_zod2.z.object({
|
|
190
|
+
elementId: import_zod2.z.string().describe("ID of the element to remove")
|
|
191
|
+
});
|
|
192
|
+
var RemoveElementsInputSchema = import_zod2.z.object({
|
|
193
|
+
elementIds: import_zod2.z.array(import_zod2.z.string()).describe("IDs of elements to remove")
|
|
194
|
+
});
|
|
195
|
+
var ExportDiagramInputSchema = import_zod2.z.object({
|
|
196
|
+
format: import_zod2.z.enum(["svg", "json", "png"]).describe("Export format")
|
|
197
|
+
});
|
|
198
|
+
var GetElementInputSchema = import_zod2.z.object({
|
|
199
|
+
elementId: import_zod2.z.string().describe("ID of the element to retrieve")
|
|
200
|
+
});
|
|
201
|
+
var EmptyInputSchema = import_zod2.z.object({});
|
|
202
|
+
|
|
203
|
+
// src/core/metadata.ts
|
|
204
|
+
var import_zod3 = require("zod");
|
|
205
|
+
var MetadataSchema = import_zod3.z.object({
|
|
206
|
+
width: import_zod3.z.number().positive(),
|
|
207
|
+
height: import_zod3.z.number().positive(),
|
|
208
|
+
background: import_zod3.z.string(),
|
|
209
|
+
metadata: import_zod3.z.object({
|
|
210
|
+
name: import_zod3.z.string().optional(),
|
|
211
|
+
diagramType: import_zod3.z.string().optional(),
|
|
212
|
+
elementCount: import_zod3.z.number().optional(),
|
|
213
|
+
createdAt: import_zod3.z.number().optional(),
|
|
214
|
+
modifiedAt: import_zod3.z.number().optional()
|
|
215
|
+
}).passthrough().optional()
|
|
216
|
+
});
|
|
217
|
+
function parseMetadata(line) {
|
|
218
|
+
let obj;
|
|
219
|
+
try {
|
|
220
|
+
obj = JSON.parse(line);
|
|
221
|
+
} catch {
|
|
222
|
+
return { success: false, error: "First line is not valid JSON" };
|
|
223
|
+
}
|
|
224
|
+
const result = MetadataSchema.safeParse(obj);
|
|
225
|
+
if (!result.success) {
|
|
226
|
+
const errors = result.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
227
|
+
return { success: false, error: `Invalid metadata: ${errors}` };
|
|
228
|
+
}
|
|
229
|
+
return { success: true, data: result.data };
|
|
230
|
+
}
|
|
231
|
+
function isMetadataLine(obj) {
|
|
232
|
+
return "width" in obj && "height" in obj && !("type" in obj);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// src/core/parser.ts
|
|
236
|
+
function isElementLike(obj) {
|
|
237
|
+
if (typeof obj.id !== "string" || !obj.id) return false;
|
|
238
|
+
if (obj.type === "node") {
|
|
239
|
+
return obj.position != null && typeof obj.position === "object";
|
|
240
|
+
}
|
|
241
|
+
if (obj.type === "edge") {
|
|
242
|
+
return typeof obj.source === "string" && typeof obj.target === "string";
|
|
243
|
+
}
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
function parseDiagram(content, strict = false) {
|
|
247
|
+
const lines = content.split("\n");
|
|
248
|
+
const result = {
|
|
249
|
+
metadata: null,
|
|
250
|
+
elements: [],
|
|
251
|
+
nodes: [],
|
|
252
|
+
edges: [],
|
|
253
|
+
errors: []
|
|
254
|
+
};
|
|
255
|
+
for (let i = 0; i < lines.length; i++) {
|
|
256
|
+
const line = lines[i].trim();
|
|
257
|
+
if (!line) continue;
|
|
258
|
+
const lineNum = i + 1;
|
|
259
|
+
let obj;
|
|
260
|
+
try {
|
|
261
|
+
obj = JSON.parse(line);
|
|
262
|
+
} catch {
|
|
263
|
+
result.errors.push({ line: lineNum, message: "Invalid JSON", raw: line.slice(0, 100) });
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (typeof obj !== "object" || obj === null) {
|
|
267
|
+
result.errors.push({ line: lineNum, message: "Line is not a JSON object" });
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
const record = obj;
|
|
271
|
+
if (result.metadata === null && isMetadataLine(record)) {
|
|
272
|
+
const metaResult = parseMetadata(line);
|
|
273
|
+
if (metaResult.success) {
|
|
274
|
+
result.metadata = metaResult.data;
|
|
275
|
+
} else {
|
|
276
|
+
result.errors.push({ line: lineNum, message: metaResult.error });
|
|
277
|
+
}
|
|
278
|
+
continue;
|
|
279
|
+
}
|
|
280
|
+
if (strict) {
|
|
281
|
+
const validated = DiagramElementSchema.safeParse(obj);
|
|
282
|
+
if (!validated.success) {
|
|
283
|
+
const errorMessages = validated.error.errors.map((e) => `${e.path.join(".")}: ${e.message}`).join("; ");
|
|
284
|
+
result.errors.push({ line: lineNum, message: `Schema validation failed: ${errorMessages}` });
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const element = { ...record, ...validated.data };
|
|
288
|
+
if (record.style && typeof record.style === "object") {
|
|
289
|
+
element.style = { ...record.style };
|
|
290
|
+
}
|
|
291
|
+
if (record.metadata && typeof record.metadata === "object") {
|
|
292
|
+
element.metadata = { ...record.metadata };
|
|
293
|
+
}
|
|
294
|
+
addElement(result, element);
|
|
295
|
+
} else {
|
|
296
|
+
if (!isElementLike(record)) {
|
|
297
|
+
result.errors.push({ line: lineNum, message: "Not a recognized element (missing id, type, or required fields)" });
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
const element = record;
|
|
301
|
+
addElement(result, element);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (strict) {
|
|
305
|
+
if (!result.metadata) {
|
|
306
|
+
result.errors.push({ line: 1, message: "Missing metadata first line (required in strict mode)" });
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
const ids = /* @__PURE__ */ new Set();
|
|
310
|
+
for (const el of result.elements) {
|
|
311
|
+
if (ids.has(el.id)) {
|
|
312
|
+
result.errors.push({ line: 0, message: `Duplicate element ID: "${el.id}"` });
|
|
313
|
+
}
|
|
314
|
+
ids.add(el.id);
|
|
315
|
+
}
|
|
316
|
+
const nodeIds = new Set(result.nodes.map((n) => n.id));
|
|
317
|
+
for (const edge of result.edges) {
|
|
318
|
+
if (!nodeIds.has(edge.source)) {
|
|
319
|
+
result.errors.push({ line: 0, message: `Edge "${edge.id}" references missing source node "${edge.source}"` });
|
|
320
|
+
}
|
|
321
|
+
if (!nodeIds.has(edge.target)) {
|
|
322
|
+
result.errors.push({ line: 0, message: `Edge "${edge.id}" references missing target node "${edge.target}"` });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
function addElement(result, element) {
|
|
328
|
+
result.elements.push(element);
|
|
329
|
+
if (element.type === "node") {
|
|
330
|
+
result.nodes.push(element);
|
|
331
|
+
} else if (element.type === "edge") {
|
|
332
|
+
result.edges.push(element);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/utils/format.ts
|
|
337
|
+
function formatErrors(errors) {
|
|
338
|
+
if (errors.length === 0) return "";
|
|
339
|
+
return errors.map((e) => {
|
|
340
|
+
const lineInfo = e.line > 0 ? `Line ${e.line}: ` : "";
|
|
341
|
+
const rawInfo = e.raw ? ` (raw: "${e.raw}")` : "";
|
|
342
|
+
return ` ${lineInfo}${e.message}${rawInfo}`;
|
|
343
|
+
}).join("\n");
|
|
344
|
+
}
|
|
345
|
+
function formatKV(key, value, indent = 0) {
|
|
346
|
+
const prefix = " ".repeat(indent);
|
|
347
|
+
const displayValue = value === null || value === void 0 ? "(none)" : String(value);
|
|
348
|
+
return `${prefix}${key}: ${displayValue}`;
|
|
349
|
+
}
|
|
350
|
+
function formatHeader(title) {
|
|
351
|
+
return `
|
|
352
|
+
${title}
|
|
353
|
+
${"\u2500".repeat(title.length)}`;
|
|
354
|
+
}
|
|
355
|
+
function formatCount(label, count) {
|
|
356
|
+
return ` ${label}: ${count}`;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// src/commands/validate.ts
|
|
360
|
+
function registerValidateCommand(program2) {
|
|
361
|
+
program2.command("validate <file>").description("Validate a .drawit file against the schema").option("--format <format>", "Output format: text or json", "text").option("--strict", "Enable strict validation (duplicate IDs, edge references)", false).option("--stdin", "Read from stdin instead of file").action((file, opts) => {
|
|
362
|
+
try {
|
|
363
|
+
const content = readInput(opts.stdin ? "-" : file);
|
|
364
|
+
const result = parseDiagram(content, opts.strict);
|
|
365
|
+
if (opts.format === "json") {
|
|
366
|
+
const output = {
|
|
367
|
+
valid: result.errors.length === 0,
|
|
368
|
+
errors: result.errors,
|
|
369
|
+
stats: {
|
|
370
|
+
nodes: result.nodes.length,
|
|
371
|
+
edges: result.edges.length,
|
|
372
|
+
hasMetadata: result.metadata !== null
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
376
|
+
} else {
|
|
377
|
+
if (result.errors.length === 0) {
|
|
378
|
+
process.stdout.write(`Valid .drawit file (${result.nodes.length} nodes, ${result.edges.length} edges)
|
|
379
|
+
`);
|
|
380
|
+
} else {
|
|
381
|
+
process.stderr.write(`Found ${result.errors.length} error(s):
|
|
382
|
+
`);
|
|
383
|
+
process.stderr.write(formatErrors(result.errors) + "\n");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
process.exitCode = result.errors.length === 0 ? 0 : 1;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
389
|
+
`);
|
|
390
|
+
process.exitCode = 1;
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// src/svg/utils.ts
|
|
396
|
+
function escapeXml(text) {
|
|
397
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
398
|
+
}
|
|
399
|
+
function regularPolygonPoints(cx, cy, rx, ry, sides, startAngle = -Math.PI / 2) {
|
|
400
|
+
const points = [];
|
|
401
|
+
for (let i = 0; i < sides; i++) {
|
|
402
|
+
const angle = startAngle + 2 * Math.PI * i / sides;
|
|
403
|
+
points.push({
|
|
404
|
+
x: cx + rx * Math.cos(angle),
|
|
405
|
+
y: cy + ry * Math.sin(angle)
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return points;
|
|
409
|
+
}
|
|
410
|
+
function pointsToSvg(points) {
|
|
411
|
+
return points.map((p) => `${p.x},${p.y}`).join(" ");
|
|
412
|
+
}
|
|
413
|
+
function starPoints(cx, cy, rx, ry, numPoints, innerRadius) {
|
|
414
|
+
const points = [];
|
|
415
|
+
const irx = rx * innerRadius;
|
|
416
|
+
const iry = ry * innerRadius;
|
|
417
|
+
const startAngle = -Math.PI / 2;
|
|
418
|
+
for (let i = 0; i < numPoints * 2; i++) {
|
|
419
|
+
const angle = startAngle + Math.PI * i / numPoints;
|
|
420
|
+
const isOuter = i % 2 === 0;
|
|
421
|
+
points.push({
|
|
422
|
+
x: cx + (isOuter ? rx : irx) * Math.cos(angle),
|
|
423
|
+
y: cy + (isOuter ? ry : iry) * Math.sin(angle)
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
return points;
|
|
427
|
+
}
|
|
428
|
+
function getStyleProps(style = {}) {
|
|
429
|
+
return {
|
|
430
|
+
fill: String(style.fillStyle ?? style.fill ?? "none"),
|
|
431
|
+
stroke: String(style.strokeStyle ?? style.stroke ?? "none"),
|
|
432
|
+
strokeWidth: Number(style.lineWidth ?? style.strokeWidth ?? 2),
|
|
433
|
+
fillOpacity: Number(style.fillOpacity ?? 1),
|
|
434
|
+
strokeOpacity: Number(style.strokeOpacity ?? 1),
|
|
435
|
+
lineDash: Array.isArray(style.lineDash) ? style.lineDash : []
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/svg/shapes.ts
|
|
440
|
+
function renderNodeShape(node) {
|
|
441
|
+
const shape = node.shape ?? "rectangle";
|
|
442
|
+
const pos = node.position;
|
|
443
|
+
const size = node.size ?? { width: 100, height: 60 };
|
|
444
|
+
const style = node.style ?? {};
|
|
445
|
+
const metadata = node.metadata ?? {};
|
|
446
|
+
const s = getStyleProps(style);
|
|
447
|
+
const styleAttrs = buildStyleAttrs(s);
|
|
448
|
+
const transform = node.angle ? ` transform="rotate(${node.angle} ${pos.x + size.width / 2} ${pos.y + size.height / 2})"` : "";
|
|
449
|
+
let shapeEl = "";
|
|
450
|
+
switch (shape) {
|
|
451
|
+
case "rectangle":
|
|
452
|
+
shapeEl = renderRectangle(pos, size, style, styleAttrs);
|
|
453
|
+
break;
|
|
454
|
+
case "ellipse":
|
|
455
|
+
shapeEl = renderEllipse(pos, size, styleAttrs);
|
|
456
|
+
break;
|
|
457
|
+
case "diamond":
|
|
458
|
+
shapeEl = renderDiamond(pos, size, styleAttrs);
|
|
459
|
+
break;
|
|
460
|
+
case "triangle":
|
|
461
|
+
shapeEl = renderTriangle(pos, size, styleAttrs);
|
|
462
|
+
break;
|
|
463
|
+
case "hexagon":
|
|
464
|
+
shapeEl = renderRegularPolygon(pos, size, 6, styleAttrs);
|
|
465
|
+
break;
|
|
466
|
+
case "polygon":
|
|
467
|
+
shapeEl = renderRegularPolygon(pos, size, Number(metadata.sides ?? 6), styleAttrs);
|
|
468
|
+
break;
|
|
469
|
+
case "star":
|
|
470
|
+
shapeEl = renderStar(pos, size, metadata, styleAttrs);
|
|
471
|
+
break;
|
|
472
|
+
case "polyline":
|
|
473
|
+
shapeEl = renderPolyline(pos, metadata, styleAttrs);
|
|
474
|
+
break;
|
|
475
|
+
case "icon":
|
|
476
|
+
shapeEl = renderIcon(pos, size, style, styleAttrs);
|
|
477
|
+
break;
|
|
478
|
+
case "line":
|
|
479
|
+
shapeEl = renderLine(pos, size, styleAttrs);
|
|
480
|
+
break;
|
|
481
|
+
default:
|
|
482
|
+
shapeEl = renderRectangle(pos, size, style, styleAttrs);
|
|
483
|
+
}
|
|
484
|
+
return `<g${transform}>${shapeEl}</g>`;
|
|
485
|
+
}
|
|
486
|
+
function buildStyleAttrs(s) {
|
|
487
|
+
let attrs = ` fill="${s.fill}" stroke="${s.stroke}" stroke-width="${s.strokeWidth}"`;
|
|
488
|
+
if (s.fillOpacity < 1) attrs += ` fill-opacity="${s.fillOpacity}"`;
|
|
489
|
+
if (s.strokeOpacity < 1) attrs += ` stroke-opacity="${s.strokeOpacity}"`;
|
|
490
|
+
if (s.lineDash.length > 0) attrs += ` stroke-dasharray="${s.lineDash.join(",")}"`;
|
|
491
|
+
return attrs;
|
|
492
|
+
}
|
|
493
|
+
function renderRectangle(pos, size, style, styleAttrs) {
|
|
494
|
+
let rx = 0;
|
|
495
|
+
let ry = 0;
|
|
496
|
+
const cornerRadii = style.cornerRadii;
|
|
497
|
+
const cornerRadius = style.cornerRadius;
|
|
498
|
+
if (cornerRadii) {
|
|
499
|
+
rx = (cornerRadii.topLeft + cornerRadii.topRight + cornerRadii.bottomRight + cornerRadii.bottomLeft) / 4;
|
|
500
|
+
ry = rx;
|
|
501
|
+
} else if (cornerRadius !== void 0) {
|
|
502
|
+
rx = cornerRadius;
|
|
503
|
+
ry = cornerRadius;
|
|
504
|
+
}
|
|
505
|
+
const radiusAttrs = rx > 0 ? ` rx="${rx}" ry="${ry}"` : "";
|
|
506
|
+
return `<rect x="${pos.x}" y="${pos.y}" width="${size.width}" height="${size.height}"${radiusAttrs}${styleAttrs}/>`;
|
|
507
|
+
}
|
|
508
|
+
function renderEllipse(pos, size, styleAttrs) {
|
|
509
|
+
const cx = pos.x + size.width / 2;
|
|
510
|
+
const cy = pos.y + size.height / 2;
|
|
511
|
+
return `<ellipse cx="${cx}" cy="${cy}" rx="${size.width / 2}" ry="${size.height / 2}"${styleAttrs}/>`;
|
|
512
|
+
}
|
|
513
|
+
function renderDiamond(pos, size, styleAttrs) {
|
|
514
|
+
const cx = pos.x + size.width / 2;
|
|
515
|
+
const cy = pos.y + size.height / 2;
|
|
516
|
+
const points = [
|
|
517
|
+
{ x: cx, y: pos.y },
|
|
518
|
+
{ x: pos.x + size.width, y: cy },
|
|
519
|
+
{ x: cx, y: pos.y + size.height },
|
|
520
|
+
{ x: pos.x, y: cy }
|
|
521
|
+
];
|
|
522
|
+
return `<polygon points="${pointsToSvg(points)}"${styleAttrs}/>`;
|
|
523
|
+
}
|
|
524
|
+
function renderTriangle(pos, size, styleAttrs) {
|
|
525
|
+
const points = [
|
|
526
|
+
{ x: pos.x + size.width / 2, y: pos.y },
|
|
527
|
+
{ x: pos.x + size.width, y: pos.y + size.height },
|
|
528
|
+
{ x: pos.x, y: pos.y + size.height }
|
|
529
|
+
];
|
|
530
|
+
return `<polygon points="${pointsToSvg(points)}"${styleAttrs}/>`;
|
|
531
|
+
}
|
|
532
|
+
function renderRegularPolygon(pos, size, sides, styleAttrs) {
|
|
533
|
+
const cx = pos.x + size.width / 2;
|
|
534
|
+
const cy = pos.y + size.height / 2;
|
|
535
|
+
const points = regularPolygonPoints(cx, cy, size.width / 2, size.height / 2, sides);
|
|
536
|
+
return `<polygon points="${pointsToSvg(points)}"${styleAttrs}/>`;
|
|
537
|
+
}
|
|
538
|
+
function renderStar(pos, size, metadata, styleAttrs) {
|
|
539
|
+
const cx = pos.x + size.width / 2;
|
|
540
|
+
const cy = pos.y + size.height / 2;
|
|
541
|
+
const numPoints = Number(metadata.points ?? 5);
|
|
542
|
+
const innerRadius = Number(metadata.innerRadius ?? 0.4);
|
|
543
|
+
const points = starPoints(cx, cy, size.width / 2, size.height / 2, numPoints, innerRadius);
|
|
544
|
+
return `<polygon points="${pointsToSvg(points)}"${styleAttrs}/>`;
|
|
545
|
+
}
|
|
546
|
+
function renderPolyline(pos, metadata, styleAttrs) {
|
|
547
|
+
const polylinePoints = metadata.polylinePoints;
|
|
548
|
+
if (!polylinePoints || polylinePoints.length === 0) {
|
|
549
|
+
return "";
|
|
550
|
+
}
|
|
551
|
+
const absolutePoints = polylinePoints.map((p) => ({
|
|
552
|
+
x: pos.x + p.x,
|
|
553
|
+
y: pos.y + p.y
|
|
554
|
+
}));
|
|
555
|
+
const isClosed = metadata.isPolylineClosed === true;
|
|
556
|
+
const tag = isClosed ? "polygon" : "polyline";
|
|
557
|
+
return `<${tag} points="${pointsToSvg(absolutePoints)}"${styleAttrs}/>`;
|
|
558
|
+
}
|
|
559
|
+
function renderIcon(pos, size, style, styleAttrs) {
|
|
560
|
+
const iconName = String(style.iconName ?? "icon");
|
|
561
|
+
const iconColor = style.iconOptions?.color ?? style.strokeStyle ?? "#666666";
|
|
562
|
+
return `<rect x="${pos.x}" y="${pos.y}" width="${size.width}" height="${size.height}" fill="none" stroke="${iconColor}" stroke-width="1" rx="4"/><text x="${pos.x + size.width / 2}" y="${pos.y + size.height / 2}" fill="${iconColor}" font-size="10" text-anchor="middle" dominant-baseline="middle" font-family="sans-serif">${escapeXml(iconName)}</text>`;
|
|
563
|
+
}
|
|
564
|
+
function renderLine(pos, size, styleAttrs) {
|
|
565
|
+
return `<line x1="${pos.x}" y1="${pos.y}" x2="${pos.x + size.width}" y2="${pos.y + size.height}"${styleAttrs}/>`;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// src/svg/text.ts
|
|
569
|
+
function renderNodeText(node) {
|
|
570
|
+
if (!node.text) return "";
|
|
571
|
+
const size = node.size ?? { width: 100, height: 60 };
|
|
572
|
+
const pos = node.position;
|
|
573
|
+
let textProps;
|
|
574
|
+
if (typeof node.text === "string") {
|
|
575
|
+
textProps = { content: node.text };
|
|
576
|
+
} else {
|
|
577
|
+
textProps = node.text;
|
|
578
|
+
}
|
|
579
|
+
if (!textProps.content) return "";
|
|
580
|
+
const fontSize = textProps.fontSize ?? 14;
|
|
581
|
+
const fontFamily = mapFontFamily(textProps.fontFamily ?? "sans-serif");
|
|
582
|
+
const color = textProps.color ?? "#000000";
|
|
583
|
+
const textAlign = textProps.textAlign ?? "center";
|
|
584
|
+
const verticalAlign = textProps.verticalAlign ?? "middle";
|
|
585
|
+
const padding = textProps.padding ?? 8;
|
|
586
|
+
const fontWeight = textProps.fontWeight ?? "normal";
|
|
587
|
+
const fontStyle = textProps.fontStyle ?? "normal";
|
|
588
|
+
const lineHeight = textProps.lineHeight ?? 1.4;
|
|
589
|
+
const anchor = textAlign === "left" ? "start" : textAlign === "right" ? "end" : "middle";
|
|
590
|
+
let textX;
|
|
591
|
+
if (textAlign === "left") {
|
|
592
|
+
textX = pos.x + padding;
|
|
593
|
+
} else if (textAlign === "right") {
|
|
594
|
+
textX = pos.x + size.width - padding;
|
|
595
|
+
} else {
|
|
596
|
+
textX = pos.x + size.width / 2;
|
|
597
|
+
}
|
|
598
|
+
const lines = textProps.content.split("\n");
|
|
599
|
+
const totalLineHeight = fontSize * lineHeight;
|
|
600
|
+
const totalTextHeight = lines.length * totalLineHeight;
|
|
601
|
+
let startY;
|
|
602
|
+
if (verticalAlign === "top") {
|
|
603
|
+
startY = pos.y + padding + fontSize;
|
|
604
|
+
} else if (verticalAlign === "bottom") {
|
|
605
|
+
startY = pos.y + size.height - padding - totalTextHeight + fontSize;
|
|
606
|
+
} else {
|
|
607
|
+
startY = pos.y + (size.height - totalTextHeight) / 2 + fontSize;
|
|
608
|
+
}
|
|
609
|
+
const tspans = [];
|
|
610
|
+
for (let i = 0; i < lines.length; i++) {
|
|
611
|
+
const y = startY + i * totalLineHeight;
|
|
612
|
+
const segments = parseMarkdownLite(lines[i]);
|
|
613
|
+
const renderedSegments = segments.map((seg) => {
|
|
614
|
+
let attrs = "";
|
|
615
|
+
if (seg.bold) attrs += ' font-weight="bold"';
|
|
616
|
+
if (seg.italic) attrs += ' font-style="italic"';
|
|
617
|
+
if (seg.code) attrs += ' font-family="monospace"';
|
|
618
|
+
return `<tspan${attrs}>${escapeXml(seg.text)}</tspan>`;
|
|
619
|
+
});
|
|
620
|
+
tspans.push(`<tspan x="${textX}" y="${y}">${renderedSegments.join("")}</tspan>`);
|
|
621
|
+
}
|
|
622
|
+
return `<text fill="${color}" font-size="${fontSize}" font-family="${escapeXml(fontFamily)}" font-weight="${fontWeight}" font-style="${fontStyle}" text-anchor="${anchor}" dominant-baseline="auto">${tspans.join("")}</text>`;
|
|
623
|
+
}
|
|
624
|
+
function parseMarkdownLite(text) {
|
|
625
|
+
const segments = [];
|
|
626
|
+
let remaining = text;
|
|
627
|
+
while (remaining.length > 0) {
|
|
628
|
+
const boldMatch = remaining.match(/^\*\*(.+?)\*\*/);
|
|
629
|
+
if (boldMatch) {
|
|
630
|
+
segments.push({ text: boldMatch[1], bold: true, italic: false, code: false });
|
|
631
|
+
remaining = remaining.slice(boldMatch[0].length);
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
const italicMatch = remaining.match(/^\*(.+?)\*/);
|
|
635
|
+
if (italicMatch) {
|
|
636
|
+
segments.push({ text: italicMatch[1], bold: false, italic: true, code: false });
|
|
637
|
+
remaining = remaining.slice(italicMatch[0].length);
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
const codeMatch = remaining.match(/^`(.+?)`/);
|
|
641
|
+
if (codeMatch) {
|
|
642
|
+
segments.push({ text: codeMatch[1], bold: false, italic: false, code: true });
|
|
643
|
+
remaining = remaining.slice(codeMatch[0].length);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
const nextMarker = remaining.search(/[*`]/);
|
|
647
|
+
if (nextMarker === -1) {
|
|
648
|
+
segments.push({ text: remaining, bold: false, italic: false, code: false });
|
|
649
|
+
break;
|
|
650
|
+
} else if (nextMarker === 0) {
|
|
651
|
+
segments.push({ text: remaining[0], bold: false, italic: false, code: false });
|
|
652
|
+
remaining = remaining.slice(1);
|
|
653
|
+
} else {
|
|
654
|
+
segments.push({ text: remaining.slice(0, nextMarker), bold: false, italic: false, code: false });
|
|
655
|
+
remaining = remaining.slice(nextMarker);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return segments.length > 0 ? segments : [{ text: "", bold: false, italic: false, code: false }];
|
|
659
|
+
}
|
|
660
|
+
function mapFontFamily(family) {
|
|
661
|
+
const map = {
|
|
662
|
+
geist: "Geist, system-ui, sans-serif",
|
|
663
|
+
"geist-mono": "Geist Mono, monospace",
|
|
664
|
+
lato: "Lato, sans-serif",
|
|
665
|
+
merriweather: "Merriweather, serif",
|
|
666
|
+
montserrat: "Montserrat, sans-serif",
|
|
667
|
+
"open-sans": "Open Sans, sans-serif",
|
|
668
|
+
"playfair-display": "Playfair Display, serif",
|
|
669
|
+
roboto: "Roboto, sans-serif",
|
|
670
|
+
georgia: "Georgia, serif",
|
|
671
|
+
pacifico: "Pacifico, cursive"
|
|
672
|
+
};
|
|
673
|
+
return map[family] ?? family;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// src/svg/edges.ts
|
|
677
|
+
function generateMarkerDefs() {
|
|
678
|
+
return `<defs>
|
|
679
|
+
<marker id="arrowhead-end" markerWidth="10" markerHeight="7" refX="10" refY="3.5" orient="auto">
|
|
680
|
+
<polygon points="0 0, 10 3.5, 0 7" fill="context-stroke"/>
|
|
681
|
+
</marker>
|
|
682
|
+
<marker id="arrowhead-start" markerWidth="10" markerHeight="7" refX="0" refY="3.5" orient="auto">
|
|
683
|
+
<polygon points="10 0, 0 3.5, 10 7" fill="context-stroke"/>
|
|
684
|
+
</marker>
|
|
685
|
+
<marker id="arrowhead-diamond" markerWidth="12" markerHeight="8" refX="6" refY="4" orient="auto">
|
|
686
|
+
<polygon points="0 4, 6 0, 12 4, 6 8" fill="context-stroke"/>
|
|
687
|
+
</marker>
|
|
688
|
+
<marker id="arrowhead-circle" markerWidth="8" markerHeight="8" refX="4" refY="4" orient="auto">
|
|
689
|
+
<circle cx="4" cy="4" r="3" fill="context-stroke"/>
|
|
690
|
+
</marker>
|
|
691
|
+
</defs>`;
|
|
692
|
+
}
|
|
693
|
+
function renderEdge(edge, nodeLookup) {
|
|
694
|
+
const style = edge.style ?? {};
|
|
695
|
+
const s = getStyleProps(style);
|
|
696
|
+
const routing = String(style.routing ?? "straight");
|
|
697
|
+
const sourceNode = nodeLookup.get(edge.source);
|
|
698
|
+
const targetNode = nodeLookup.get(edge.target);
|
|
699
|
+
if (!sourceNode || !targetNode) {
|
|
700
|
+
return `<!-- Edge ${edge.id}: missing source or target -->`;
|
|
701
|
+
}
|
|
702
|
+
const sourceSize = sourceNode.size ?? { width: 100, height: 60 };
|
|
703
|
+
const targetSize = targetNode.size ?? { width: 100, height: 60 };
|
|
704
|
+
const sx = sourceNode.position.x + sourceSize.width / 2;
|
|
705
|
+
const sy = sourceNode.position.y + sourceSize.height / 2;
|
|
706
|
+
const tx = targetNode.position.x + targetSize.width / 2;
|
|
707
|
+
const ty = targetNode.position.y + targetSize.height / 2;
|
|
708
|
+
let attrs = ` stroke="${s.stroke}" stroke-width="${s.strokeWidth}" fill="none"`;
|
|
709
|
+
if (s.strokeOpacity < 1) attrs += ` stroke-opacity="${s.strokeOpacity}"`;
|
|
710
|
+
if (s.lineDash.length > 0) attrs += ` stroke-dasharray="${s.lineDash.join(",")}"`;
|
|
711
|
+
if (style.arrowheadEnd === true || style.markerEnd === "arrow") {
|
|
712
|
+
attrs += ' marker-end="url(#arrowhead-end)"';
|
|
713
|
+
}
|
|
714
|
+
if (style.arrowheadStart === true || style.markerStart === "arrow") {
|
|
715
|
+
attrs += ' marker-start="url(#arrowhead-start)"';
|
|
716
|
+
}
|
|
717
|
+
let pathEl;
|
|
718
|
+
const waypoints = edge.points;
|
|
719
|
+
if (waypoints && waypoints.length > 0) {
|
|
720
|
+
const allPoints = [{ x: sx, y: sy }, ...waypoints, { x: tx, y: ty }];
|
|
721
|
+
const d = allPoints.map((p, i) => `${i === 0 ? "M" : "L"}${p.x} ${p.y}`).join(" ");
|
|
722
|
+
pathEl = `<path d="${d}"${attrs}/>`;
|
|
723
|
+
} else if (routing === "orthogonal") {
|
|
724
|
+
pathEl = renderOrthogonalEdge(sx, sy, tx, ty, style, attrs);
|
|
725
|
+
} else if (routing === "bezier") {
|
|
726
|
+
pathEl = renderBezierEdge(sx, sy, tx, ty, attrs);
|
|
727
|
+
} else {
|
|
728
|
+
pathEl = `<line x1="${sx}" y1="${sy}" x2="${tx}" y2="${ty}"${attrs}/>`;
|
|
729
|
+
}
|
|
730
|
+
const labelEl = renderEdgeLabel(edge, sx, sy, tx, ty);
|
|
731
|
+
return pathEl + labelEl;
|
|
732
|
+
}
|
|
733
|
+
function renderOrthogonalEdge(sx, sy, tx, ty, style, attrs) {
|
|
734
|
+
const cornerRadius = Number(style.cornerRadius ?? 0);
|
|
735
|
+
const midX = tx;
|
|
736
|
+
const midY = sy;
|
|
737
|
+
if (cornerRadius > 0 && Math.abs(tx - sx) > cornerRadius * 2 && Math.abs(ty - sy) > cornerRadius * 2) {
|
|
738
|
+
const dx = tx > sx ? 1 : -1;
|
|
739
|
+
const dy = ty > sy ? 1 : -1;
|
|
740
|
+
const r = Math.min(cornerRadius, Math.abs(tx - sx) / 2, Math.abs(ty - sy) / 2);
|
|
741
|
+
const d2 = [
|
|
742
|
+
`M${sx} ${sy}`,
|
|
743
|
+
`L${midX - dx * r} ${sy}`,
|
|
744
|
+
`Q${midX} ${sy} ${midX} ${sy + dy * r}`,
|
|
745
|
+
`L${tx} ${ty}`
|
|
746
|
+
].join(" ");
|
|
747
|
+
return `<path d="${d2}"${attrs}/>`;
|
|
748
|
+
}
|
|
749
|
+
const d = `M${sx} ${sy} L${midX} ${midY} L${tx} ${ty}`;
|
|
750
|
+
return `<path d="${d}"${attrs}/>`;
|
|
751
|
+
}
|
|
752
|
+
function renderBezierEdge(sx, sy, tx, ty, attrs) {
|
|
753
|
+
const dx = Math.abs(tx - sx) * 0.5;
|
|
754
|
+
const d = `M${sx} ${sy} C${sx + dx} ${sy}, ${tx - dx} ${ty}, ${tx} ${ty}`;
|
|
755
|
+
return `<path d="${d}"${attrs}/>`;
|
|
756
|
+
}
|
|
757
|
+
function renderEdgeLabel(edge, sx, sy, tx, ty) {
|
|
758
|
+
if (!edge.label) return "";
|
|
759
|
+
let text;
|
|
760
|
+
let fontSize = 14;
|
|
761
|
+
let color = "#475569";
|
|
762
|
+
let offsetX = 0;
|
|
763
|
+
let offsetY = -10;
|
|
764
|
+
if (typeof edge.label === "string") {
|
|
765
|
+
text = edge.label;
|
|
766
|
+
} else {
|
|
767
|
+
text = edge.label.text ?? "";
|
|
768
|
+
fontSize = edge.label.fontSize ?? fontSize;
|
|
769
|
+
color = edge.label.color ?? color;
|
|
770
|
+
}
|
|
771
|
+
if (!text) return "";
|
|
772
|
+
const midX = (sx + tx) / 2 + offsetX;
|
|
773
|
+
const midY = (sy + ty) / 2 + offsetY;
|
|
774
|
+
return `<text x="${midX}" y="${midY}" fill="${color}" font-size="${fontSize}" text-anchor="middle" dominant-baseline="auto" font-family="sans-serif">${escapeXml(text)}</text>`;
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
// src/svg/renderer.ts
|
|
778
|
+
function renderToSvg(metadata, elements, options = {}) {
|
|
779
|
+
const padding = options.padding ?? 0;
|
|
780
|
+
const width = (metadata?.width ?? 800) + padding * 2;
|
|
781
|
+
const height = (metadata?.height ?? 600) + padding * 2;
|
|
782
|
+
const background = options.background ?? metadata?.background ?? "#ffffff";
|
|
783
|
+
const nodes = [];
|
|
784
|
+
const edges = [];
|
|
785
|
+
for (const el of elements) {
|
|
786
|
+
if (el.type === "node") nodes.push(el);
|
|
787
|
+
else if (el.type === "edge") edges.push(el);
|
|
788
|
+
}
|
|
789
|
+
const nodeLookup = /* @__PURE__ */ new Map();
|
|
790
|
+
for (const node of nodes) {
|
|
791
|
+
nodeLookup.set(node.id, node);
|
|
792
|
+
}
|
|
793
|
+
const sortedElements = [...elements].sort((a, b) => (a.zIndex ?? 0) - (b.zIndex ?? 0));
|
|
794
|
+
const hasArrowheads = edges.some((e) => {
|
|
795
|
+
const style = e.style ?? {};
|
|
796
|
+
return style.arrowheadEnd === true || style.arrowheadStart === true || style.markerEnd === "arrow" || style.markerStart === "arrow";
|
|
797
|
+
});
|
|
798
|
+
const parts = [];
|
|
799
|
+
parts.push(`<?xml version="1.0" encoding="UTF-8"?>`);
|
|
800
|
+
parts.push(
|
|
801
|
+
`<svg xmlns="http://www.w3.org/2000/svg" width="${width}" height="${height}" viewBox="${-padding} ${-padding} ${width} ${height}">`
|
|
802
|
+
);
|
|
803
|
+
if (hasArrowheads) {
|
|
804
|
+
parts.push(` ${generateMarkerDefs()}`);
|
|
805
|
+
}
|
|
806
|
+
parts.push(` <rect x="${-padding}" y="${-padding}" width="${width}" height="${height}" fill="${escapeXml(background)}"/>`);
|
|
807
|
+
for (const el of sortedElements) {
|
|
808
|
+
if (el.type === "node") {
|
|
809
|
+
const node = el;
|
|
810
|
+
parts.push(` ${renderNodeShape(node)}`);
|
|
811
|
+
const textSvg = renderNodeText(node);
|
|
812
|
+
if (textSvg) parts.push(` ${textSvg}`);
|
|
813
|
+
} else if (el.type === "edge") {
|
|
814
|
+
const edge = el;
|
|
815
|
+
parts.push(` ${renderEdge(edge, nodeLookup)}`);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
parts.push(`</svg>`);
|
|
819
|
+
return parts.join("\n");
|
|
820
|
+
}
|
|
821
|
+
function computeBoundingBox(elements) {
|
|
822
|
+
let minX = Infinity;
|
|
823
|
+
let minY = Infinity;
|
|
824
|
+
let maxX = -Infinity;
|
|
825
|
+
let maxY = -Infinity;
|
|
826
|
+
for (const el of elements) {
|
|
827
|
+
if (el.type === "node") {
|
|
828
|
+
const node = el;
|
|
829
|
+
const size = node.size ?? { width: 100, height: 60 };
|
|
830
|
+
minX = Math.min(minX, node.position.x);
|
|
831
|
+
minY = Math.min(minY, node.position.y);
|
|
832
|
+
maxX = Math.max(maxX, node.position.x + size.width);
|
|
833
|
+
maxY = Math.max(maxY, node.position.y + size.height);
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
if (minX === Infinity) {
|
|
837
|
+
return { minX: 0, minY: 0, maxX: 0, maxY: 0, width: 0, height: 0 };
|
|
838
|
+
}
|
|
839
|
+
return {
|
|
840
|
+
minX,
|
|
841
|
+
minY,
|
|
842
|
+
maxX,
|
|
843
|
+
maxY,
|
|
844
|
+
width: maxX - minX,
|
|
845
|
+
height: maxY - minY
|
|
846
|
+
};
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/commands/inspect.ts
|
|
850
|
+
function registerInspectCommand(program2) {
|
|
851
|
+
program2.command("inspect <file>").description("Show structured summary of a .drawit file").option("--format <format>", "Output format: text or json", "text").option("--elements", "List all element IDs").option("--stdin", "Read from stdin instead of file").action((file, opts) => {
|
|
852
|
+
try {
|
|
853
|
+
const content = readInput(opts.stdin ? "-" : file);
|
|
854
|
+
const result = parseDiagram(content, false);
|
|
855
|
+
const shapeBreakdown = {};
|
|
856
|
+
for (const node of result.nodes) {
|
|
857
|
+
const shape = node.shape ?? "rectangle";
|
|
858
|
+
shapeBreakdown[shape] = (shapeBreakdown[shape] ?? 0) + 1;
|
|
859
|
+
}
|
|
860
|
+
const zIndices = result.elements.map((e) => e.zIndex ?? 0);
|
|
861
|
+
const minZ = zIndices.length > 0 ? Math.min(...zIndices) : 0;
|
|
862
|
+
const maxZ = zIndices.length > 0 ? Math.max(...zIndices) : 0;
|
|
863
|
+
const bbox = computeBoundingBox(result.elements);
|
|
864
|
+
const connections = result.edges.map((e) => ({
|
|
865
|
+
id: e.id,
|
|
866
|
+
source: e.source,
|
|
867
|
+
target: e.target
|
|
868
|
+
}));
|
|
869
|
+
if (opts.format === "json") {
|
|
870
|
+
const output = {
|
|
871
|
+
metadata: result.metadata,
|
|
872
|
+
elementCount: result.elements.length,
|
|
873
|
+
nodeCount: result.nodes.length,
|
|
874
|
+
edgeCount: result.edges.length,
|
|
875
|
+
shapeBreakdown,
|
|
876
|
+
zIndexRange: { min: minZ, max: maxZ },
|
|
877
|
+
boundingBox: bbox,
|
|
878
|
+
connections,
|
|
879
|
+
errors: result.errors,
|
|
880
|
+
...opts.elements ? { elementIds: result.elements.map((e) => ({ id: e.id, type: e.type })) } : {}
|
|
881
|
+
};
|
|
882
|
+
process.stdout.write(JSON.stringify(output, null, 2) + "\n");
|
|
883
|
+
} else {
|
|
884
|
+
const lines = [];
|
|
885
|
+
lines.push(formatHeader("Canvas"));
|
|
886
|
+
if (result.metadata) {
|
|
887
|
+
lines.push(formatKV("Dimensions", `${result.metadata.width} x ${result.metadata.height}`, 2));
|
|
888
|
+
lines.push(formatKV("Background", result.metadata.background, 2));
|
|
889
|
+
if (result.metadata.metadata?.name) {
|
|
890
|
+
lines.push(formatKV("Name", result.metadata.metadata.name, 2));
|
|
891
|
+
}
|
|
892
|
+
if (result.metadata.metadata?.diagramType) {
|
|
893
|
+
lines.push(formatKV("Type", result.metadata.metadata.diagramType, 2));
|
|
894
|
+
}
|
|
895
|
+
} else {
|
|
896
|
+
lines.push(" (no metadata found)");
|
|
897
|
+
}
|
|
898
|
+
lines.push(formatHeader("Elements"));
|
|
899
|
+
lines.push(formatCount("Nodes", result.nodes.length));
|
|
900
|
+
lines.push(formatCount("Edges", result.edges.length));
|
|
901
|
+
lines.push(formatCount("Total", result.elements.length));
|
|
902
|
+
if (Object.keys(shapeBreakdown).length > 0) {
|
|
903
|
+
lines.push(formatHeader("Shapes"));
|
|
904
|
+
for (const [shape, count] of Object.entries(shapeBreakdown).sort((a, b) => b[1] - a[1])) {
|
|
905
|
+
lines.push(formatCount(shape, count));
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
lines.push(formatHeader("Layout"));
|
|
909
|
+
lines.push(formatKV("Z-index range", `${minZ} to ${maxZ}`, 2));
|
|
910
|
+
if (bbox.width > 0) {
|
|
911
|
+
lines.push(formatKV("Bounding box", `(${bbox.minX}, ${bbox.minY}) to (${bbox.maxX}, ${bbox.maxY})`, 2));
|
|
912
|
+
lines.push(formatKV("Content size", `${bbox.width} x ${bbox.height}`, 2));
|
|
913
|
+
}
|
|
914
|
+
if (connections.length > 0) {
|
|
915
|
+
lines.push(formatHeader("Connections"));
|
|
916
|
+
for (const conn of connections) {
|
|
917
|
+
lines.push(` ${conn.source} -> ${conn.target} [${conn.id}]`);
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
if (opts.elements) {
|
|
921
|
+
lines.push(formatHeader("Element IDs"));
|
|
922
|
+
for (const el of result.elements) {
|
|
923
|
+
const extra = el.type === "node" ? ` (${el.shape ?? "rectangle"})` : ` (${el.type})`;
|
|
924
|
+
lines.push(` ${el.id}${extra}`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
if (result.errors.length > 0) {
|
|
928
|
+
lines.push(formatHeader("Warnings"));
|
|
929
|
+
lines.push(` ${result.errors.length} parse error(s) found`);
|
|
930
|
+
}
|
|
931
|
+
process.stdout.write(lines.join("\n") + "\n");
|
|
932
|
+
}
|
|
933
|
+
} catch (err) {
|
|
934
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
935
|
+
`);
|
|
936
|
+
process.exitCode = 1;
|
|
937
|
+
}
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/commands/export.ts
|
|
942
|
+
function registerExportCommand(program2) {
|
|
943
|
+
program2.command("export <file>").description("Export a .drawit file to SVG, JSON, or NDJSON").requiredOption("--format <format>", "Export format: svg, json, or ndjson").option("--output <path>", "Output file path (default: stdout)").option("--padding <px>", "Padding around the diagram (SVG only)", "0").option("--background <color>", "Override background color (SVG only)").option("--stdin", "Read from stdin instead of file").action(
|
|
944
|
+
(file, opts) => {
|
|
945
|
+
try {
|
|
946
|
+
const content = readInput(opts.stdin ? "-" : file);
|
|
947
|
+
const result = parseDiagram(content, false);
|
|
948
|
+
if (result.errors.length > 0) {
|
|
949
|
+
process.stderr.write(
|
|
950
|
+
`Warning: ${result.errors.length} parse error(s) found. Output may be incomplete.
|
|
951
|
+
`
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
let output;
|
|
955
|
+
switch (opts.format) {
|
|
956
|
+
case "svg":
|
|
957
|
+
output = renderToSvg(result.metadata, result.elements, {
|
|
958
|
+
padding: parseInt(opts.padding, 10),
|
|
959
|
+
background: opts.background
|
|
960
|
+
});
|
|
961
|
+
break;
|
|
962
|
+
case "json": {
|
|
963
|
+
const jsonOutput = {
|
|
964
|
+
metadata: result.metadata,
|
|
965
|
+
elements: result.elements
|
|
966
|
+
};
|
|
967
|
+
output = JSON.stringify(jsonOutput, null, 2);
|
|
968
|
+
break;
|
|
969
|
+
}
|
|
970
|
+
case "ndjson": {
|
|
971
|
+
const lines = [];
|
|
972
|
+
if (result.metadata) {
|
|
973
|
+
lines.push(JSON.stringify(result.metadata));
|
|
974
|
+
}
|
|
975
|
+
for (const el of result.elements) {
|
|
976
|
+
lines.push(JSON.stringify(el));
|
|
977
|
+
}
|
|
978
|
+
output = lines.join("\n") + "\n";
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
981
|
+
default:
|
|
982
|
+
process.stderr.write(`Unsupported format: ${opts.format}. Use svg, json, or ndjson.
|
|
983
|
+
`);
|
|
984
|
+
process.exitCode = 1;
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
writeOutput(output, opts.output);
|
|
988
|
+
} catch (err) {
|
|
989
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
990
|
+
`);
|
|
991
|
+
process.exitCode = 1;
|
|
992
|
+
}
|
|
993
|
+
}
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// src/commands/create.ts
|
|
998
|
+
var TEMPLATES = {
|
|
999
|
+
blank: () => [],
|
|
1000
|
+
flowchart: () => [
|
|
1001
|
+
'{"id":"start","type":"node","position":{"x":340,"y":40},"size":{"width":120,"height":60},"shape":"rectangle","constrainAspectRatio":false,"angle":0,"zIndex":1,"style":{"fillStyle":"#10B981","strokeStyle":"#059669","lineWidth":2,"fillOpacity":1,"strokeOpacity":1,"cornerRadii":{"topLeft":8,"topRight":8,"bottomRight":8,"bottomLeft":8}},"text":{"content":"Start","fontSize":16,"fontFamily":"geist","color":"#FFFFFF","textAlign":"center","verticalAlign":"middle"},"metadata":{}}',
|
|
1002
|
+
'{"id":"process","type":"node","position":{"x":340,"y":160},"size":{"width":120,"height":60},"shape":"rectangle","constrainAspectRatio":false,"angle":0,"zIndex":1,"style":{"fillStyle":"#3B82F6","strokeStyle":"#1E40AF","lineWidth":2,"fillOpacity":1,"strokeOpacity":1,"cornerRadii":{"topLeft":8,"topRight":8,"bottomRight":8,"bottomLeft":8}},"text":{"content":"Process","fontSize":16,"fontFamily":"geist","color":"#FFFFFF","textAlign":"center","verticalAlign":"middle"},"metadata":{}}',
|
|
1003
|
+
'{"id":"end","type":"node","position":{"x":340,"y":280},"size":{"width":120,"height":60},"shape":"rectangle","constrainAspectRatio":false,"angle":0,"zIndex":1,"style":{"fillStyle":"#EF4444","strokeStyle":"#DC2626","lineWidth":2,"fillOpacity":1,"strokeOpacity":1,"cornerRadii":{"topLeft":8,"topRight":8,"bottomRight":8,"bottomLeft":8}},"text":{"content":"End","fontSize":16,"fontFamily":"geist","color":"#FFFFFF","textAlign":"center","verticalAlign":"middle"},"metadata":{}}',
|
|
1004
|
+
'{"id":"edge1","type":"edge","source":"start","target":"process","zIndex":0,"style":{"strokeStyle":"#64748B","strokeOpacity":1,"lineWidth":2,"routing":"orthogonal","cornerRadius":8,"arrowheadEnd":true},"metadata":{}}',
|
|
1005
|
+
'{"id":"edge2","type":"edge","source":"process","target":"end","zIndex":0,"style":{"strokeStyle":"#64748B","strokeOpacity":1,"lineWidth":2,"routing":"orthogonal","cornerRadius":8,"arrowheadEnd":true},"metadata":{}}'
|
|
1006
|
+
]
|
|
1007
|
+
};
|
|
1008
|
+
function registerCreateCommand(program2) {
|
|
1009
|
+
program2.command("create <file>").description("Scaffold a new .drawit file with metadata").option("--width <px>", "Canvas width", "800").option("--height <px>", "Canvas height", "600").option("--background <color>", "Background color", "#ffffff").option("--name <name>", "Diagram name").option("--type <type>", "Diagram type (e.g., flowchart, technical)").option("--template <name>", "Template: blank, flowchart", "blank").option("--stdout", "Output to stdout instead of file").action(
|
|
1010
|
+
(file, opts) => {
|
|
1011
|
+
try {
|
|
1012
|
+
const metadata = {
|
|
1013
|
+
width: parseInt(opts.width, 10),
|
|
1014
|
+
height: parseInt(opts.height, 10),
|
|
1015
|
+
background: opts.background,
|
|
1016
|
+
metadata: {
|
|
1017
|
+
...opts.name ? { name: opts.name } : {},
|
|
1018
|
+
...opts.type ? { diagramType: opts.type } : {}
|
|
1019
|
+
}
|
|
1020
|
+
};
|
|
1021
|
+
const templateFn = TEMPLATES[opts.template];
|
|
1022
|
+
if (!templateFn) {
|
|
1023
|
+
process.stderr.write(
|
|
1024
|
+
`Unknown template: ${opts.template}. Available: ${Object.keys(TEMPLATES).join(", ")}
|
|
1025
|
+
`
|
|
1026
|
+
);
|
|
1027
|
+
process.exitCode = 1;
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const lines = [JSON.stringify(metadata), ...templateFn()];
|
|
1031
|
+
const content = lines.join("\n") + "\n";
|
|
1032
|
+
writeOutput(content, opts.stdout ? void 0 : file);
|
|
1033
|
+
if (!opts.stdout) {
|
|
1034
|
+
process.stdout.write(`Created ${file}
|
|
1035
|
+
`);
|
|
1036
|
+
}
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1039
|
+
`);
|
|
1040
|
+
process.exitCode = 1;
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
);
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// src/commands/merge.ts
|
|
1047
|
+
function registerMergeCommand(program2) {
|
|
1048
|
+
program2.command("merge <files...>").description("Combine multiple .drawit files with automatic layout").option("--output <path>", "Output file path (default: stdout)", "-").option("--layout <direction>", "Layout direction: horizontal or vertical", "horizontal").option("--gap <px>", "Gap between merged diagrams", "100").action((files, opts) => {
|
|
1049
|
+
try {
|
|
1050
|
+
if (files.length < 2) {
|
|
1051
|
+
process.stderr.write("Merge requires at least two input files.\n");
|
|
1052
|
+
process.exitCode = 1;
|
|
1053
|
+
return;
|
|
1054
|
+
}
|
|
1055
|
+
const gap = parseInt(opts.gap, 10);
|
|
1056
|
+
const isHorizontal = opts.layout === "horizontal";
|
|
1057
|
+
let offsetX = 0;
|
|
1058
|
+
let offsetY = 0;
|
|
1059
|
+
let totalWidth = 0;
|
|
1060
|
+
let totalHeight = 0;
|
|
1061
|
+
const allElements = [];
|
|
1062
|
+
const usedIds = /* @__PURE__ */ new Set();
|
|
1063
|
+
let firstMetadata = null;
|
|
1064
|
+
for (const file of files) {
|
|
1065
|
+
const content = readInput(file);
|
|
1066
|
+
const parsed = parseDiagram(content, false);
|
|
1067
|
+
if (!firstMetadata && parsed.metadata) {
|
|
1068
|
+
firstMetadata = parsed.metadata;
|
|
1069
|
+
}
|
|
1070
|
+
const bbox = computeBoundingBox(parsed.elements);
|
|
1071
|
+
for (const el of parsed.elements) {
|
|
1072
|
+
let id = el.id;
|
|
1073
|
+
let suffix = 1;
|
|
1074
|
+
while (usedIds.has(id)) {
|
|
1075
|
+
id = `${el.id}_${suffix++}`;
|
|
1076
|
+
}
|
|
1077
|
+
usedIds.add(id);
|
|
1078
|
+
if (el.type === "node") {
|
|
1079
|
+
const node = el;
|
|
1080
|
+
const offsetNode = {
|
|
1081
|
+
...node,
|
|
1082
|
+
id,
|
|
1083
|
+
position: {
|
|
1084
|
+
x: node.position.x + offsetX,
|
|
1085
|
+
y: node.position.y + offsetY
|
|
1086
|
+
}
|
|
1087
|
+
};
|
|
1088
|
+
allElements.push(offsetNode);
|
|
1089
|
+
} else {
|
|
1090
|
+
allElements.push({ ...el, id });
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (isHorizontal) {
|
|
1094
|
+
offsetX += (bbox.width > 0 ? bbox.maxX - (bbox.minX < 0 ? bbox.minX : 0) : parsed.metadata?.width ?? 800) + gap;
|
|
1095
|
+
totalWidth = offsetX - gap;
|
|
1096
|
+
totalHeight = Math.max(totalHeight, bbox.height > 0 ? bbox.maxY : parsed.metadata?.height ?? 600);
|
|
1097
|
+
} else {
|
|
1098
|
+
offsetY += (bbox.height > 0 ? bbox.maxY - (bbox.minY < 0 ? bbox.minY : 0) : parsed.metadata?.height ?? 600) + gap;
|
|
1099
|
+
totalHeight = offsetY - gap;
|
|
1100
|
+
totalWidth = Math.max(totalWidth, bbox.width > 0 ? bbox.maxX : parsed.metadata?.width ?? 800);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const metadata = {
|
|
1104
|
+
width: totalWidth,
|
|
1105
|
+
height: totalHeight,
|
|
1106
|
+
background: firstMetadata?.background ?? "#ffffff",
|
|
1107
|
+
metadata: { name: "Merged Diagram", diagramType: "merged" }
|
|
1108
|
+
};
|
|
1109
|
+
const lines = [JSON.stringify(metadata)];
|
|
1110
|
+
for (const el of allElements) {
|
|
1111
|
+
lines.push(JSON.stringify(el));
|
|
1112
|
+
}
|
|
1113
|
+
writeOutput(lines.join("\n") + "\n", opts.output === "-" ? void 0 : opts.output);
|
|
1114
|
+
} catch (err) {
|
|
1115
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1116
|
+
`);
|
|
1117
|
+
process.exitCode = 1;
|
|
1118
|
+
}
|
|
1119
|
+
});
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
// src/commands/map.ts
|
|
1123
|
+
var import_node_fs3 = require("fs");
|
|
1124
|
+
var import_node_path2 = require("path");
|
|
1125
|
+
var import_node_child_process = require("child_process");
|
|
1126
|
+
|
|
1127
|
+
// src/utils/code-analyzer.ts
|
|
1128
|
+
var import_node_fs2 = require("fs");
|
|
1129
|
+
var import_node_path = require("path");
|
|
1130
|
+
var IMPORT_REGEX = /^\s*import\s+(?:(?:[\w*{][^'"]*)\s+from\s+)?['"]([^'"]+)['"]/gm;
|
|
1131
|
+
var EXPORT_FROM_REGEX = /^\s*export\s+(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?)\s+from\s+['"]([^'"]+)['"]/gm;
|
|
1132
|
+
var REQUIRE_REGEX = /(?:^|[^.])\brequire\s*\(\s*['"]([^'"]+)['"]\s*\)/gm;
|
|
1133
|
+
function sanitizeId(relativePath) {
|
|
1134
|
+
return "f_" + relativePath.replace(/[^a-zA-Z0-9]/g, "_");
|
|
1135
|
+
}
|
|
1136
|
+
function walkDir(current, root, depth, options, results) {
|
|
1137
|
+
if (depth > options.maxDepth) return;
|
|
1138
|
+
let entries;
|
|
1139
|
+
try {
|
|
1140
|
+
entries = (0, import_node_fs2.readdirSync)(current, { withFileTypes: true });
|
|
1141
|
+
} catch {
|
|
1142
|
+
return;
|
|
1143
|
+
}
|
|
1144
|
+
for (const entry of entries) {
|
|
1145
|
+
const fullPath = (0, import_node_path.join)(current, entry.name);
|
|
1146
|
+
const relFromRoot = (0, import_node_path.relative)(root, fullPath);
|
|
1147
|
+
if (entry.name.startsWith(".")) continue;
|
|
1148
|
+
if (isExcluded(entry.name, relFromRoot, options.excludePatterns)) continue;
|
|
1149
|
+
if (entry.isDirectory()) {
|
|
1150
|
+
walkDir(fullPath, root, depth + 1, options, results);
|
|
1151
|
+
} else if (entry.isFile()) {
|
|
1152
|
+
const ext = (0, import_node_path.extname)(entry.name);
|
|
1153
|
+
if (options.includeExtensions.includes(ext)) {
|
|
1154
|
+
results.push(fullPath);
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
function isExcluded(name, relPath, patterns) {
|
|
1160
|
+
for (const pattern of patterns) {
|
|
1161
|
+
if (name === pattern) return true;
|
|
1162
|
+
if (relPath === pattern || relPath.startsWith(pattern + "/")) return true;
|
|
1163
|
+
}
|
|
1164
|
+
return false;
|
|
1165
|
+
}
|
|
1166
|
+
function extractImportPaths(content) {
|
|
1167
|
+
const paths = [];
|
|
1168
|
+
let match;
|
|
1169
|
+
IMPORT_REGEX.lastIndex = 0;
|
|
1170
|
+
while ((match = IMPORT_REGEX.exec(content)) !== null) {
|
|
1171
|
+
paths.push(match[1]);
|
|
1172
|
+
}
|
|
1173
|
+
EXPORT_FROM_REGEX.lastIndex = 0;
|
|
1174
|
+
while ((match = EXPORT_FROM_REGEX.exec(content)) !== null) {
|
|
1175
|
+
paths.push(match[1]);
|
|
1176
|
+
}
|
|
1177
|
+
REQUIRE_REGEX.lastIndex = 0;
|
|
1178
|
+
while ((match = REQUIRE_REGEX.exec(content)) !== null) {
|
|
1179
|
+
paths.push(match[1]);
|
|
1180
|
+
}
|
|
1181
|
+
return [...new Set(paths)];
|
|
1182
|
+
}
|
|
1183
|
+
function resolveLocalImport(fromFile, importPath, pathToId, extensions) {
|
|
1184
|
+
const base = (0, import_node_path.resolve)((0, import_node_path.dirname)(fromFile), importPath);
|
|
1185
|
+
const candidates = [
|
|
1186
|
+
base,
|
|
1187
|
+
...extensions.map((ext) => base + ext),
|
|
1188
|
+
...extensions.map((ext) => (0, import_node_path.join)(base, "index" + ext))
|
|
1189
|
+
];
|
|
1190
|
+
for (const candidate of candidates) {
|
|
1191
|
+
const id = pathToId.get(candidate);
|
|
1192
|
+
if (id !== void 0) {
|
|
1193
|
+
return id;
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
return null;
|
|
1197
|
+
}
|
|
1198
|
+
function analyzeCodebase(rootDir, options) {
|
|
1199
|
+
const absolutePaths = [];
|
|
1200
|
+
walkDir(rootDir, rootDir, 0, options, absolutePaths);
|
|
1201
|
+
const pathToId = /* @__PURE__ */ new Map();
|
|
1202
|
+
const entries = [];
|
|
1203
|
+
for (const absPath of absolutePaths) {
|
|
1204
|
+
const rel = (0, import_node_path.relative)(rootDir, absPath);
|
|
1205
|
+
const id = sanitizeId(rel);
|
|
1206
|
+
pathToId.set(absPath, id);
|
|
1207
|
+
const parentRel = (0, import_node_path.relative)(rootDir, (0, import_node_path.dirname)(absPath));
|
|
1208
|
+
entries.push({
|
|
1209
|
+
id,
|
|
1210
|
+
absolutePath: absPath,
|
|
1211
|
+
relativePath: rel,
|
|
1212
|
+
fileName: (0, import_node_path.basename)(absPath),
|
|
1213
|
+
dirPath: parentRel || ".",
|
|
1214
|
+
imports: [],
|
|
1215
|
+
ext: (0, import_node_path.extname)(absPath),
|
|
1216
|
+
x: 0,
|
|
1217
|
+
y: 0
|
|
1218
|
+
});
|
|
1219
|
+
}
|
|
1220
|
+
for (const entry of entries) {
|
|
1221
|
+
try {
|
|
1222
|
+
const content = (0, import_node_fs2.readFileSync)(entry.absolutePath, "utf-8");
|
|
1223
|
+
const rawImports = extractImportPaths(content);
|
|
1224
|
+
for (const importPath of rawImports) {
|
|
1225
|
+
if (!importPath.startsWith("./") && !importPath.startsWith("../")) continue;
|
|
1226
|
+
const resolvedId = resolveLocalImport(
|
|
1227
|
+
entry.absolutePath,
|
|
1228
|
+
importPath,
|
|
1229
|
+
pathToId,
|
|
1230
|
+
options.includeExtensions
|
|
1231
|
+
);
|
|
1232
|
+
if (resolvedId && !entry.imports.includes(resolvedId)) {
|
|
1233
|
+
entry.imports.push(resolvedId);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
} catch {
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
return entries;
|
|
1240
|
+
}
|
|
1241
|
+
function sanitizeDirId(relativePath) {
|
|
1242
|
+
return "d_" + (relativePath === "." ? "root" : relativePath.replace(/[^a-zA-Z0-9]/g, "_"));
|
|
1243
|
+
}
|
|
1244
|
+
function dominantExtFrom(extCounts) {
|
|
1245
|
+
let best = "";
|
|
1246
|
+
let bestCount = 0;
|
|
1247
|
+
for (const [ext, count] of extCounts) {
|
|
1248
|
+
if (count > bestCount) {
|
|
1249
|
+
bestCount = count;
|
|
1250
|
+
best = ext;
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
return best || ".ts";
|
|
1254
|
+
}
|
|
1255
|
+
function buildDirNode(current, root, depth, options, parentId) {
|
|
1256
|
+
if (depth > options.maxDepth) return null;
|
|
1257
|
+
const relPath = (0, import_node_path.relative)(root, current) || ".";
|
|
1258
|
+
const name = depth === 0 ? (0, import_node_path.basename)(current) : (0, import_node_path.basename)(current);
|
|
1259
|
+
const id = sanitizeDirId(relPath);
|
|
1260
|
+
let entries;
|
|
1261
|
+
try {
|
|
1262
|
+
entries = (0, import_node_fs2.readdirSync)(current, { withFileTypes: true });
|
|
1263
|
+
} catch {
|
|
1264
|
+
return null;
|
|
1265
|
+
}
|
|
1266
|
+
let fileCount = 0;
|
|
1267
|
+
const extCounts = /* @__PURE__ */ new Map();
|
|
1268
|
+
const children = [];
|
|
1269
|
+
for (const entry of entries) {
|
|
1270
|
+
if (entry.name.startsWith(".")) continue;
|
|
1271
|
+
const fullPath = (0, import_node_path.join)(current, entry.name);
|
|
1272
|
+
const relFromRoot = (0, import_node_path.relative)(root, fullPath);
|
|
1273
|
+
if (isExcluded(entry.name, relFromRoot, options.excludePatterns)) continue;
|
|
1274
|
+
if (entry.isDirectory()) {
|
|
1275
|
+
const child = buildDirNode(fullPath, root, depth + 1, options, id);
|
|
1276
|
+
if (child && child.totalFileCount > 0) {
|
|
1277
|
+
children.push(child);
|
|
1278
|
+
for (const [ext, count] of Object.entries(
|
|
1279
|
+
Object.fromEntries(
|
|
1280
|
+
child.totalFileCount > 0 ? [[child.dominantExt, child.totalFileCount]] : []
|
|
1281
|
+
)
|
|
1282
|
+
)) {
|
|
1283
|
+
extCounts.set(ext, (extCounts.get(ext) ?? 0) + count);
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
} else if (entry.isFile()) {
|
|
1287
|
+
const ext = (0, import_node_path.extname)(entry.name);
|
|
1288
|
+
if (options.includeExtensions.includes(ext)) {
|
|
1289
|
+
fileCount++;
|
|
1290
|
+
extCounts.set(ext, (extCounts.get(ext) ?? 0) + 1);
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
const totalFileCount = fileCount + children.reduce((s, c) => s + c.totalFileCount, 0);
|
|
1295
|
+
return {
|
|
1296
|
+
id,
|
|
1297
|
+
name,
|
|
1298
|
+
relativePath: relPath,
|
|
1299
|
+
depth,
|
|
1300
|
+
fileCount,
|
|
1301
|
+
totalFileCount,
|
|
1302
|
+
children,
|
|
1303
|
+
dominantExt: dominantExtFrom(extCounts),
|
|
1304
|
+
parentId,
|
|
1305
|
+
x: 0,
|
|
1306
|
+
y: 0
|
|
1307
|
+
};
|
|
1308
|
+
}
|
|
1309
|
+
function analyzeStructure(rootDir, options) {
|
|
1310
|
+
const root = buildDirNode(rootDir, rootDir, 0, options, null);
|
|
1311
|
+
if (!root) {
|
|
1312
|
+
return {
|
|
1313
|
+
id: "d_root",
|
|
1314
|
+
name: (0, import_node_path.basename)(rootDir),
|
|
1315
|
+
relativePath: ".",
|
|
1316
|
+
depth: 0,
|
|
1317
|
+
fileCount: 0,
|
|
1318
|
+
totalFileCount: 0,
|
|
1319
|
+
children: [],
|
|
1320
|
+
dominantExt: ".ts",
|
|
1321
|
+
parentId: null,
|
|
1322
|
+
x: 0,
|
|
1323
|
+
y: 0
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
return root;
|
|
1327
|
+
}
|
|
1328
|
+
function quickCountFiles(rootDir, options, earlyExitThreshold = 1e3) {
|
|
1329
|
+
let count = 0;
|
|
1330
|
+
function walk(current, depth) {
|
|
1331
|
+
if (depth > options.maxDepth || count > earlyExitThreshold) return;
|
|
1332
|
+
let entries;
|
|
1333
|
+
try {
|
|
1334
|
+
entries = (0, import_node_fs2.readdirSync)(current, { withFileTypes: true });
|
|
1335
|
+
} catch {
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
for (const entry of entries) {
|
|
1339
|
+
if (count > earlyExitThreshold) return;
|
|
1340
|
+
if (entry.name.startsWith(".")) continue;
|
|
1341
|
+
const fullPath = (0, import_node_path.join)(current, entry.name);
|
|
1342
|
+
const relFromRoot = (0, import_node_path.relative)(rootDir, fullPath);
|
|
1343
|
+
if (isExcluded(entry.name, relFromRoot, options.excludePatterns)) continue;
|
|
1344
|
+
if (entry.isDirectory()) {
|
|
1345
|
+
walk(fullPath, depth + 1);
|
|
1346
|
+
} else if (entry.isFile()) {
|
|
1347
|
+
const ext = (0, import_node_path.extname)(entry.name);
|
|
1348
|
+
if (options.includeExtensions.includes(ext)) count++;
|
|
1349
|
+
}
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
walk(rootDir, 0);
|
|
1353
|
+
return count;
|
|
1354
|
+
}
|
|
1355
|
+
function parseIncludeExtensions(pattern) {
|
|
1356
|
+
const braceMatch = pattern.match(/\{([^}]+)\}/);
|
|
1357
|
+
if (braceMatch) {
|
|
1358
|
+
return braceMatch[1].split(",").map((e) => `.${e.trim()}`);
|
|
1359
|
+
}
|
|
1360
|
+
const extMatch = pattern.match(/\*(\.\w+)$/);
|
|
1361
|
+
if (extMatch) return [extMatch[1]];
|
|
1362
|
+
return [".ts", ".tsx", ".js", ".jsx"];
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// src/utils/map-layout.ts
|
|
1366
|
+
var NODE_W = 190;
|
|
1367
|
+
var NODE_H = 48;
|
|
1368
|
+
var NODE_GAP_X = 14;
|
|
1369
|
+
var NODE_GAP_Y = 10;
|
|
1370
|
+
var NODES_PER_ROW = 3;
|
|
1371
|
+
var GROUP_PAD = 24;
|
|
1372
|
+
var GROUP_HEADER = 30;
|
|
1373
|
+
var GROUP_GAP_X = 40;
|
|
1374
|
+
var GROUP_GAP_Y = 36;
|
|
1375
|
+
var GROUPS_PER_COL = 4;
|
|
1376
|
+
var CANVAS_PAD = 60;
|
|
1377
|
+
var MIN_CANVAS_W = 1200;
|
|
1378
|
+
var MIN_CANVAS_H = 800;
|
|
1379
|
+
function layoutGroups(files) {
|
|
1380
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
1381
|
+
for (const f of files) {
|
|
1382
|
+
const dir = f.dirPath;
|
|
1383
|
+
if (!groupMap.has(dir)) groupMap.set(dir, []);
|
|
1384
|
+
groupMap.get(dir).push(f);
|
|
1385
|
+
}
|
|
1386
|
+
const sortedDirs = [...groupMap.keys()].sort((a, b) => {
|
|
1387
|
+
if (a === ".") return -1;
|
|
1388
|
+
if (b === ".") return 1;
|
|
1389
|
+
return a.localeCompare(b);
|
|
1390
|
+
});
|
|
1391
|
+
const groups = sortedDirs.map((dir) => {
|
|
1392
|
+
const groupFiles = groupMap.get(dir);
|
|
1393
|
+
const N = groupFiles.length;
|
|
1394
|
+
const cols = Math.min(N, NODES_PER_ROW);
|
|
1395
|
+
const rows = Math.ceil(N / NODES_PER_ROW);
|
|
1396
|
+
const innerW = cols * NODE_W + (cols - 1) * NODE_GAP_X;
|
|
1397
|
+
const innerH = rows * NODE_H + (rows - 1) * NODE_GAP_Y;
|
|
1398
|
+
return {
|
|
1399
|
+
dirId: dir === "." ? "root" : dir.replace(/[^a-zA-Z0-9]/g, "_"),
|
|
1400
|
+
label: dir === "." ? "/ (root)" : dir,
|
|
1401
|
+
files: groupFiles,
|
|
1402
|
+
x: 0,
|
|
1403
|
+
y: 0,
|
|
1404
|
+
w: innerW + GROUP_PAD * 2,
|
|
1405
|
+
h: innerH + GROUP_PAD * 2 + GROUP_HEADER
|
|
1406
|
+
};
|
|
1407
|
+
});
|
|
1408
|
+
const columns = [];
|
|
1409
|
+
let currentCol = [];
|
|
1410
|
+
for (const g of groups) {
|
|
1411
|
+
if (currentCol.length >= GROUPS_PER_COL) {
|
|
1412
|
+
columns.push(currentCol);
|
|
1413
|
+
currentCol = [];
|
|
1414
|
+
}
|
|
1415
|
+
currentCol.push(g);
|
|
1416
|
+
}
|
|
1417
|
+
if (currentCol.length > 0) columns.push(currentCol);
|
|
1418
|
+
let curX = CANVAS_PAD;
|
|
1419
|
+
let maxY = CANVAS_PAD;
|
|
1420
|
+
for (const column of columns) {
|
|
1421
|
+
const colW = Math.max(...column.map((g) => g.w));
|
|
1422
|
+
let curY = CANVAS_PAD;
|
|
1423
|
+
for (const g of column) {
|
|
1424
|
+
g.x = curX;
|
|
1425
|
+
g.y = curY;
|
|
1426
|
+
curY += g.h + GROUP_GAP_Y;
|
|
1427
|
+
}
|
|
1428
|
+
maxY = Math.max(maxY, curY - GROUP_GAP_Y);
|
|
1429
|
+
curX += colW + GROUP_GAP_X;
|
|
1430
|
+
}
|
|
1431
|
+
const canvasW = Math.max(MIN_CANVAS_W, curX - GROUP_GAP_X + CANVAS_PAD);
|
|
1432
|
+
const canvasH = Math.max(MIN_CANVAS_H, maxY + CANVAS_PAD);
|
|
1433
|
+
for (const g of groups) {
|
|
1434
|
+
g.files.forEach((f, i) => {
|
|
1435
|
+
const col = i % NODES_PER_ROW;
|
|
1436
|
+
const row = Math.floor(i / NODES_PER_ROW);
|
|
1437
|
+
f.x = g.x + GROUP_PAD + col * (NODE_W + NODE_GAP_X);
|
|
1438
|
+
f.y = g.y + GROUP_HEADER + GROUP_PAD + row * (NODE_H + NODE_GAP_Y);
|
|
1439
|
+
});
|
|
1440
|
+
}
|
|
1441
|
+
return { groups, canvasW, canvasH };
|
|
1442
|
+
}
|
|
1443
|
+
var DIR_W = 260;
|
|
1444
|
+
var DIR_H = 72;
|
|
1445
|
+
var X_STEP = 340;
|
|
1446
|
+
var DIR_Y_GAP = 20;
|
|
1447
|
+
var DIR_CANVAS_PAD = 80;
|
|
1448
|
+
var DIR_MIN_W = 1400;
|
|
1449
|
+
var DIR_MIN_H = 900;
|
|
1450
|
+
function assignY(node, slotStart) {
|
|
1451
|
+
if (node.children.length === 0) {
|
|
1452
|
+
node.y = DIR_CANVAS_PAD + slotStart * (DIR_H + DIR_Y_GAP);
|
|
1453
|
+
return slotStart + 1;
|
|
1454
|
+
}
|
|
1455
|
+
let slot = slotStart;
|
|
1456
|
+
const firstChildY_ref = { y: 0 };
|
|
1457
|
+
let lastChildY = 0;
|
|
1458
|
+
let first = true;
|
|
1459
|
+
for (const child of node.children) {
|
|
1460
|
+
if (first) {
|
|
1461
|
+
}
|
|
1462
|
+
slot = assignY(child, slot);
|
|
1463
|
+
if (first) {
|
|
1464
|
+
firstChildY_ref.y = child.y;
|
|
1465
|
+
first = false;
|
|
1466
|
+
}
|
|
1467
|
+
lastChildY = child.y;
|
|
1468
|
+
}
|
|
1469
|
+
node.y = (firstChildY_ref.y + lastChildY) / 2;
|
|
1470
|
+
return slot;
|
|
1471
|
+
}
|
|
1472
|
+
function layoutTree(root) {
|
|
1473
|
+
function assignX(node) {
|
|
1474
|
+
node.x = DIR_CANVAS_PAD + node.depth * X_STEP;
|
|
1475
|
+
for (const child of node.children) assignX(child);
|
|
1476
|
+
}
|
|
1477
|
+
assignX(root);
|
|
1478
|
+
assignY(root, 0);
|
|
1479
|
+
const nodes = [];
|
|
1480
|
+
const edges = [];
|
|
1481
|
+
function collect(node) {
|
|
1482
|
+
nodes.push(node);
|
|
1483
|
+
for (const child of node.children) {
|
|
1484
|
+
edges.push({ source: node.id, target: child.id });
|
|
1485
|
+
collect(child);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
collect(root);
|
|
1489
|
+
const maxDepth = Math.max(...nodes.map((n) => n.depth));
|
|
1490
|
+
const maxY = Math.max(...nodes.map((n) => n.y));
|
|
1491
|
+
const canvasW = Math.max(DIR_MIN_W, DIR_CANVAS_PAD + (maxDepth + 1) * X_STEP + DIR_CANVAS_PAD);
|
|
1492
|
+
const canvasH = Math.max(DIR_MIN_H, maxY + DIR_H + DIR_CANVAS_PAD);
|
|
1493
|
+
return { nodes, canvasW, canvasH, edges };
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
// src/commands/map.ts
|
|
1497
|
+
var DEFAULT_EXCLUDE = "node_modules,dist,build,.next,.git,out,coverage,.turbo";
|
|
1498
|
+
var AUTO_THRESHOLD = 80;
|
|
1499
|
+
function makeAnalyzeOptions(opts) {
|
|
1500
|
+
return {
|
|
1501
|
+
maxDepth: parseInt(opts.depth, 10),
|
|
1502
|
+
excludePatterns: opts.exclude.split(",").map((s) => s.trim()).filter(Boolean),
|
|
1503
|
+
includeExtensions: parseIncludeExtensions(opts.include)
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
function tryOpenInVSCode(path) {
|
|
1507
|
+
try {
|
|
1508
|
+
(0, import_node_child_process.execSync)(`code "${path}"`, { stdio: "ignore" });
|
|
1509
|
+
} catch {
|
|
1510
|
+
process.stderr.write(
|
|
1511
|
+
`Could not open in VS Code automatically.
|
|
1512
|
+
Open manually: code "${path}"
|
|
1513
|
+
`
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
}
|
|
1517
|
+
function getFileNodeStyle(ext, fileName) {
|
|
1518
|
+
if (fileName.includes(".test.") || fileName.includes(".spec.")) {
|
|
1519
|
+
return { fillStyle: "#78350f", strokeStyle: "#d97706" };
|
|
1520
|
+
}
|
|
1521
|
+
if ([".json", ".yaml", ".yml", ".toml", ".env"].includes(ext)) {
|
|
1522
|
+
return { fillStyle: "#1e1b4b", strokeStyle: "#7c3aed" };
|
|
1523
|
+
}
|
|
1524
|
+
switch (ext) {
|
|
1525
|
+
case ".tsx":
|
|
1526
|
+
case ".jsx":
|
|
1527
|
+
return { fillStyle: "#0c4a6e", strokeStyle: "#0ea5e9" };
|
|
1528
|
+
case ".ts":
|
|
1529
|
+
return { fillStyle: "#1e3a5f", strokeStyle: "#3b82f6" };
|
|
1530
|
+
case ".js":
|
|
1531
|
+
return { fillStyle: "#422006", strokeStyle: "#f59e0b" };
|
|
1532
|
+
default:
|
|
1533
|
+
return { fillStyle: "#1e293b", strokeStyle: "#64748b" };
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
function buildFilesDiagramLines(files, groups, canvasW, canvasH, rootDirName, drawEdges) {
|
|
1537
|
+
const lines = [];
|
|
1538
|
+
lines.push(
|
|
1539
|
+
JSON.stringify({
|
|
1540
|
+
width: canvasW,
|
|
1541
|
+
height: canvasH,
|
|
1542
|
+
background: "#0f172a",
|
|
1543
|
+
metadata: {
|
|
1544
|
+
name: `${rootDirName} - Code Map`,
|
|
1545
|
+
diagramType: "code-map",
|
|
1546
|
+
mode: "files",
|
|
1547
|
+
generatedAt: Date.now(),
|
|
1548
|
+
fileCount: files.length
|
|
1549
|
+
}
|
|
1550
|
+
})
|
|
1551
|
+
);
|
|
1552
|
+
for (const group of groups) {
|
|
1553
|
+
lines.push(
|
|
1554
|
+
JSON.stringify({
|
|
1555
|
+
id: `grp_${group.dirId}`,
|
|
1556
|
+
type: "node",
|
|
1557
|
+
position: { x: group.x, y: group.y },
|
|
1558
|
+
size: { width: group.w, height: group.h },
|
|
1559
|
+
shape: "rectangle",
|
|
1560
|
+
zIndex: 1,
|
|
1561
|
+
constrainAspectRatio: false,
|
|
1562
|
+
angle: 0,
|
|
1563
|
+
style: {
|
|
1564
|
+
fillStyle: "#0f2744",
|
|
1565
|
+
strokeStyle: "#1e4080",
|
|
1566
|
+
lineWidth: 1,
|
|
1567
|
+
fillOpacity: 0.85,
|
|
1568
|
+
strokeOpacity: 1,
|
|
1569
|
+
cornerRadii: { topLeft: 8, topRight: 8, bottomRight: 8, bottomLeft: 8 }
|
|
1570
|
+
},
|
|
1571
|
+
text: {
|
|
1572
|
+
content: group.label,
|
|
1573
|
+
fontSize: 11,
|
|
1574
|
+
fontFamily: "geist",
|
|
1575
|
+
color: "#94a3b8",
|
|
1576
|
+
textAlign: "left",
|
|
1577
|
+
verticalAlign: "top"
|
|
1578
|
+
},
|
|
1579
|
+
metadata: { isGroupBox: true, dirPath: group.label }
|
|
1580
|
+
})
|
|
1581
|
+
);
|
|
1582
|
+
for (const file of group.files) {
|
|
1583
|
+
const style = getFileNodeStyle(file.ext, file.fileName);
|
|
1584
|
+
lines.push(
|
|
1585
|
+
JSON.stringify({
|
|
1586
|
+
id: file.id,
|
|
1587
|
+
type: "node",
|
|
1588
|
+
position: { x: file.x, y: file.y },
|
|
1589
|
+
size: { width: NODE_W, height: NODE_H },
|
|
1590
|
+
shape: "rectangle",
|
|
1591
|
+
zIndex: 2,
|
|
1592
|
+
constrainAspectRatio: false,
|
|
1593
|
+
angle: 0,
|
|
1594
|
+
style: {
|
|
1595
|
+
...style,
|
|
1596
|
+
lineWidth: 1,
|
|
1597
|
+
fillOpacity: 1,
|
|
1598
|
+
strokeOpacity: 1,
|
|
1599
|
+
cornerRadii: { topLeft: 4, topRight: 4, bottomRight: 4, bottomLeft: 4 }
|
|
1600
|
+
},
|
|
1601
|
+
text: {
|
|
1602
|
+
content: file.fileName,
|
|
1603
|
+
fontSize: 11,
|
|
1604
|
+
fontFamily: "geist",
|
|
1605
|
+
color: "#e2e8f0",
|
|
1606
|
+
textAlign: "center",
|
|
1607
|
+
verticalAlign: "middle"
|
|
1608
|
+
},
|
|
1609
|
+
metadata: { filePath: file.relativePath, ext: file.ext }
|
|
1610
|
+
})
|
|
1611
|
+
);
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
if (drawEdges) {
|
|
1615
|
+
const fileIdSet = new Set(files.map((f) => f.id));
|
|
1616
|
+
let edgeCounter = 0;
|
|
1617
|
+
for (const file of files) {
|
|
1618
|
+
for (const importId of file.imports) {
|
|
1619
|
+
if (fileIdSet.has(importId)) {
|
|
1620
|
+
lines.push(
|
|
1621
|
+
JSON.stringify({
|
|
1622
|
+
id: `e_${edgeCounter++}`,
|
|
1623
|
+
type: "edge",
|
|
1624
|
+
source: file.id,
|
|
1625
|
+
target: importId,
|
|
1626
|
+
zIndex: 0,
|
|
1627
|
+
style: { strokeStyle: "#334155", lineWidth: 1, arrowheadEnd: true, strokeOpacity: 0.55 },
|
|
1628
|
+
metadata: {}
|
|
1629
|
+
})
|
|
1630
|
+
);
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
}
|
|
1634
|
+
}
|
|
1635
|
+
return lines;
|
|
1636
|
+
}
|
|
1637
|
+
function getDirNodeStyle(dominantExt) {
|
|
1638
|
+
switch (dominantExt) {
|
|
1639
|
+
case ".tsx":
|
|
1640
|
+
case ".jsx":
|
|
1641
|
+
return { fillStyle: "#0c3d5e", strokeStyle: "#0ea5e9" };
|
|
1642
|
+
case ".ts":
|
|
1643
|
+
return { fillStyle: "#1a2f52", strokeStyle: "#3b82f6" };
|
|
1644
|
+
case ".js":
|
|
1645
|
+
return { fillStyle: "#3d1f00", strokeStyle: "#f59e0b" };
|
|
1646
|
+
default:
|
|
1647
|
+
return { fillStyle: "#1a2040", strokeStyle: "#6366f1" };
|
|
1648
|
+
}
|
|
1649
|
+
}
|
|
1650
|
+
function buildDirsDiagramLines(nodes, edges, canvasW, canvasH, rootDirName) {
|
|
1651
|
+
const lines = [];
|
|
1652
|
+
const totalFiles = nodes[0]?.totalFileCount ?? 0;
|
|
1653
|
+
lines.push(
|
|
1654
|
+
JSON.stringify({
|
|
1655
|
+
width: canvasW,
|
|
1656
|
+
height: canvasH,
|
|
1657
|
+
background: "#0a0f1e",
|
|
1658
|
+
metadata: {
|
|
1659
|
+
name: `${rootDirName} - Architecture Map`,
|
|
1660
|
+
diagramType: "code-map-dirs",
|
|
1661
|
+
mode: "dirs",
|
|
1662
|
+
generatedAt: Date.now(),
|
|
1663
|
+
directoryCount: nodes.length,
|
|
1664
|
+
fileCount: totalFiles
|
|
1665
|
+
}
|
|
1666
|
+
})
|
|
1667
|
+
);
|
|
1668
|
+
for (const node of nodes) {
|
|
1669
|
+
const style = getDirNodeStyle(node.dominantExt);
|
|
1670
|
+
const label = node.relativePath === "." ? rootDirName : node.name;
|
|
1671
|
+
const sublabel = node.totalFileCount === 1 ? "1 file" : `${node.totalFileCount} files`;
|
|
1672
|
+
lines.push(
|
|
1673
|
+
JSON.stringify({
|
|
1674
|
+
id: node.id,
|
|
1675
|
+
type: "node",
|
|
1676
|
+
position: { x: node.x, y: node.y },
|
|
1677
|
+
size: { width: DIR_W, height: DIR_H },
|
|
1678
|
+
shape: "rectangle",
|
|
1679
|
+
zIndex: node.depth + 1,
|
|
1680
|
+
constrainAspectRatio: false,
|
|
1681
|
+
angle: 0,
|
|
1682
|
+
style: {
|
|
1683
|
+
...style,
|
|
1684
|
+
lineWidth: 1.5,
|
|
1685
|
+
fillOpacity: 1,
|
|
1686
|
+
strokeOpacity: 1,
|
|
1687
|
+
cornerRadii: { topLeft: 12, topRight: 12, bottomRight: 12, bottomLeft: 12 }
|
|
1688
|
+
},
|
|
1689
|
+
text: {
|
|
1690
|
+
content: `${label}
|
|
1691
|
+
${sublabel}`,
|
|
1692
|
+
fontSize: 13,
|
|
1693
|
+
fontFamily: "geist",
|
|
1694
|
+
color: "#e2e8f0",
|
|
1695
|
+
textAlign: "center",
|
|
1696
|
+
verticalAlign: "middle"
|
|
1697
|
+
},
|
|
1698
|
+
metadata: {
|
|
1699
|
+
dirPath: node.relativePath,
|
|
1700
|
+
depth: node.depth,
|
|
1701
|
+
dominantExt: node.dominantExt,
|
|
1702
|
+
fileCount: node.fileCount,
|
|
1703
|
+
totalFileCount: node.totalFileCount
|
|
1704
|
+
}
|
|
1705
|
+
})
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
let edgeCounter = 0;
|
|
1709
|
+
for (const edge of edges) {
|
|
1710
|
+
lines.push(
|
|
1711
|
+
JSON.stringify({
|
|
1712
|
+
id: `te_${edgeCounter++}`,
|
|
1713
|
+
type: "edge",
|
|
1714
|
+
source: edge.source,
|
|
1715
|
+
target: edge.target,
|
|
1716
|
+
zIndex: 0,
|
|
1717
|
+
style: {
|
|
1718
|
+
strokeStyle: "#1e3a5f",
|
|
1719
|
+
lineWidth: 1.5,
|
|
1720
|
+
arrowheadEnd: true,
|
|
1721
|
+
strokeOpacity: 0.5
|
|
1722
|
+
},
|
|
1723
|
+
metadata: {}
|
|
1724
|
+
})
|
|
1725
|
+
);
|
|
1726
|
+
}
|
|
1727
|
+
return lines;
|
|
1728
|
+
}
|
|
1729
|
+
function runFilesMode(targetDir, analyzeOptions, outputPath, drawEdges) {
|
|
1730
|
+
const files = analyzeCodebase(targetDir, analyzeOptions);
|
|
1731
|
+
const dirCount = new Set(files.map((f) => f.dirPath)).size;
|
|
1732
|
+
process.stderr.write(
|
|
1733
|
+
`Found ${files.length} file${files.length === 1 ? "" : "s"} in ${dirCount} director${dirCount === 1 ? "y" : "ies"}.
|
|
1734
|
+
`
|
|
1735
|
+
);
|
|
1736
|
+
const { groups, canvasW, canvasH } = layoutGroups(files);
|
|
1737
|
+
const lines = buildFilesDiagramLines(files, groups, canvasW, canvasH, (0, import_node_path2.basename)(targetDir), drawEdges);
|
|
1738
|
+
writeOutput(lines.join("\n") + "\n", outputPath);
|
|
1739
|
+
}
|
|
1740
|
+
function runDirsMode(targetDir, analyzeOptions, outputPath, clusterDepth) {
|
|
1741
|
+
const treeOptions = { ...analyzeOptions, maxDepth: clusterDepth };
|
|
1742
|
+
const root = analyzeStructure(targetDir, treeOptions);
|
|
1743
|
+
const { nodes, canvasW, canvasH, edges } = layoutTree(root);
|
|
1744
|
+
process.stderr.write(
|
|
1745
|
+
`Found ${nodes.length} director${nodes.length === 1 ? "y" : "ies"} (${root.totalFileCount} total files).
|
|
1746
|
+
`
|
|
1747
|
+
);
|
|
1748
|
+
const lines = buildDirsDiagramLines(nodes, edges, canvasW, canvasH, (0, import_node_path2.basename)(targetDir));
|
|
1749
|
+
writeOutput(lines.join("\n") + "\n", outputPath);
|
|
1750
|
+
}
|
|
1751
|
+
function runSplitMode(targetDir, analyzeOptions, outputDir, drawEdges) {
|
|
1752
|
+
(0, import_node_fs3.mkdirSync)(outputDir, { recursive: true });
|
|
1753
|
+
const overviewPath = (0, import_node_path2.join)(outputDir, "overview.drawit");
|
|
1754
|
+
const overviewOptions = { ...analyzeOptions, maxDepth: 2 };
|
|
1755
|
+
const root = analyzeStructure(targetDir, overviewOptions);
|
|
1756
|
+
const { nodes, canvasW, canvasH, edges } = layoutTree(root);
|
|
1757
|
+
process.stderr.write(
|
|
1758
|
+
`Overview: ${nodes.length} directories, ${root.totalFileCount} total files.
|
|
1759
|
+
`
|
|
1760
|
+
);
|
|
1761
|
+
const overviewLines = buildDirsDiagramLines(nodes, edges, canvasW, canvasH, (0, import_node_path2.basename)(targetDir));
|
|
1762
|
+
writeOutput(overviewLines.join("\n") + "\n", overviewPath);
|
|
1763
|
+
process.stderr.write(` Generated: ${overviewPath}
|
|
1764
|
+
`);
|
|
1765
|
+
let subdirEntries;
|
|
1766
|
+
try {
|
|
1767
|
+
subdirEntries = (0, import_node_fs3.readdirSync)(targetDir, { withFileTypes: true });
|
|
1768
|
+
} catch {
|
|
1769
|
+
return;
|
|
1770
|
+
}
|
|
1771
|
+
for (const entry of subdirEntries) {
|
|
1772
|
+
if (!entry.isDirectory()) continue;
|
|
1773
|
+
if (entry.name.startsWith(".")) continue;
|
|
1774
|
+
if (analyzeOptions.excludePatterns.includes(entry.name)) continue;
|
|
1775
|
+
const subdirPath = (0, import_node_path2.join)(targetDir, entry.name);
|
|
1776
|
+
const subdirFiles = analyzeCodebase(subdirPath, analyzeOptions);
|
|
1777
|
+
if (subdirFiles.length === 0) continue;
|
|
1778
|
+
const { groups, canvasW: cW, canvasH: cH } = layoutGroups(subdirFiles);
|
|
1779
|
+
const lines = buildFilesDiagramLines(subdirFiles, groups, cW, cH, entry.name, drawEdges);
|
|
1780
|
+
const outPath = (0, import_node_path2.join)(outputDir, `${entry.name}.drawit`);
|
|
1781
|
+
writeOutput(lines.join("\n") + "\n", outPath);
|
|
1782
|
+
process.stderr.write(` Generated: ${outPath} (${subdirFiles.length} files)
|
|
1783
|
+
`);
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
function registerMapCommand(program2) {
|
|
1787
|
+
program2.command("map [path]").description(
|
|
1788
|
+
"Analyze a codebase and generate a .drawit map (auto-scales: dirs mode for large repos)"
|
|
1789
|
+
).option("--output <file>", "Output file path (default: <dirname>-map.drawit)").option("--depth <n>", "Max directory depth to scan", "4").option("--include <pattern>", "File extensions to include", "**/*.{ts,tsx,js,jsx}").option("--exclude <patterns>", "Comma-separated names to exclude", DEFAULT_EXCLUDE).option("--no-edges", "Skip import dependency edges (files mode only)").option("--open", "Open generated file(s) in VS Code").option(
|
|
1790
|
+
"--mode <auto|files|dirs>",
|
|
1791
|
+
"Map mode: auto detects by file count, files=import graph, dirs=directory tree",
|
|
1792
|
+
"auto"
|
|
1793
|
+
).option(
|
|
1794
|
+
"--split",
|
|
1795
|
+
"Generate one diagram per top-level subdir + an overview.drawit (outputs to <dirname>-map/ folder)"
|
|
1796
|
+
).option(
|
|
1797
|
+
"--cluster-depth <n>",
|
|
1798
|
+
"In dirs mode, max depth of directory nodes to show",
|
|
1799
|
+
"4"
|
|
1800
|
+
).action((pathArg, opts) => {
|
|
1801
|
+
try {
|
|
1802
|
+
const targetDir = (0, import_node_path2.resolve)(pathArg ?? process.cwd());
|
|
1803
|
+
if (!(0, import_node_fs3.existsSync)(targetDir) || !(0, import_node_fs3.statSync)(targetDir).isDirectory()) {
|
|
1804
|
+
process.stderr.write(`Error: "${targetDir}" is not a directory.
|
|
1805
|
+
`);
|
|
1806
|
+
process.exitCode = 1;
|
|
1807
|
+
return;
|
|
1808
|
+
}
|
|
1809
|
+
const maxDepth = parseInt(opts.depth, 10);
|
|
1810
|
+
const clusterDepth = parseInt(opts.clusterDepth, 10);
|
|
1811
|
+
if (isNaN(maxDepth) || maxDepth < 1) {
|
|
1812
|
+
process.stderr.write(`Error: --depth must be a positive integer.
|
|
1813
|
+
`);
|
|
1814
|
+
process.exitCode = 1;
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
const analyzeOptions = makeAnalyzeOptions(opts);
|
|
1818
|
+
const dirName = (0, import_node_path2.basename)(targetDir);
|
|
1819
|
+
process.stderr.write(`Scanning ${targetDir}...
|
|
1820
|
+
`);
|
|
1821
|
+
if (opts.split) {
|
|
1822
|
+
const outputDir = `${dirName}-map`;
|
|
1823
|
+
process.stderr.write(`Split mode \u2192 ${outputDir}/
|
|
1824
|
+
`);
|
|
1825
|
+
runSplitMode(targetDir, analyzeOptions, outputDir, opts.edges !== false);
|
|
1826
|
+
process.stderr.write(`Done. Open overview: code "${(0, import_node_path2.join)(outputDir, "overview.drawit")}"
|
|
1827
|
+
`);
|
|
1828
|
+
if (opts.open) tryOpenInVSCode((0, import_node_path2.join)(outputDir, "overview.drawit"));
|
|
1829
|
+
process.exitCode = 0;
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
let effectiveMode = opts.mode ?? "auto";
|
|
1833
|
+
if (effectiveMode === "auto") {
|
|
1834
|
+
const approxCount = quickCountFiles(targetDir, analyzeOptions);
|
|
1835
|
+
effectiveMode = approxCount > AUTO_THRESHOLD ? "dirs" : "files";
|
|
1836
|
+
process.stderr.write(
|
|
1837
|
+
`Auto-selected mode: ${effectiveMode} (${approxCount > 1e3 ? "1000+" : approxCount} files found)
|
|
1838
|
+
`
|
|
1839
|
+
);
|
|
1840
|
+
}
|
|
1841
|
+
const outputPath = opts.output ?? `${dirName}-map.drawit`;
|
|
1842
|
+
if (effectiveMode === "dirs") {
|
|
1843
|
+
runDirsMode(targetDir, analyzeOptions, outputPath, clusterDepth);
|
|
1844
|
+
} else {
|
|
1845
|
+
runFilesMode(targetDir, analyzeOptions, outputPath, opts.edges !== false);
|
|
1846
|
+
}
|
|
1847
|
+
process.stderr.write(`Generated: ${outputPath} [${effectiveMode} mode]
|
|
1848
|
+
`);
|
|
1849
|
+
if (opts.open) tryOpenInVSCode(outputPath);
|
|
1850
|
+
process.exitCode = 0;
|
|
1851
|
+
} catch (err) {
|
|
1852
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
1853
|
+
`);
|
|
1854
|
+
process.exitCode = 1;
|
|
1855
|
+
}
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
|
|
1859
|
+
// src/commands/schema.ts
|
|
1860
|
+
var import_node_fs4 = require("fs");
|
|
1861
|
+
var import_node_path3 = require("path");
|
|
1862
|
+
var import_node_child_process2 = require("child_process");
|
|
1863
|
+
|
|
1864
|
+
// src/utils/schema-parser.ts
|
|
1865
|
+
function parsePrismaSchema(content) {
|
|
1866
|
+
const models = [];
|
|
1867
|
+
const enums = /* @__PURE__ */ new Set();
|
|
1868
|
+
const enumRegex = /^enum\s+(\w+)\s*\{[\s\S]*?\n\}/gm;
|
|
1869
|
+
let enumMatch;
|
|
1870
|
+
while ((enumMatch = enumRegex.exec(content)) !== null) {
|
|
1871
|
+
enums.add(enumMatch[1]);
|
|
1872
|
+
}
|
|
1873
|
+
const modelRegex = /^model\s+(\w+)\s*\{([\s\S]*?)\n\}/gm;
|
|
1874
|
+
let match;
|
|
1875
|
+
while ((match = modelRegex.exec(content)) !== null) {
|
|
1876
|
+
const modelName = match[1];
|
|
1877
|
+
const block = match[2];
|
|
1878
|
+
const fields = [];
|
|
1879
|
+
const relations = [];
|
|
1880
|
+
const lines = block.split("\n");
|
|
1881
|
+
for (const line of lines) {
|
|
1882
|
+
const trimmed = line.trim();
|
|
1883
|
+
if (!trimmed || trimmed.startsWith("//") || trimmed.startsWith("@@")) {
|
|
1884
|
+
continue;
|
|
1885
|
+
}
|
|
1886
|
+
const parts = trimmed.split(/\s+/);
|
|
1887
|
+
if (parts.length < 2) continue;
|
|
1888
|
+
const fieldName = parts[0];
|
|
1889
|
+
let typeStr = parts[1];
|
|
1890
|
+
let isList = false;
|
|
1891
|
+
let isRequired = true;
|
|
1892
|
+
if (typeStr.endsWith("[]")) {
|
|
1893
|
+
isList = true;
|
|
1894
|
+
typeStr = typeStr.slice(0, -2);
|
|
1895
|
+
} else if (typeStr.endsWith("?")) {
|
|
1896
|
+
isRequired = false;
|
|
1897
|
+
typeStr = typeStr.slice(0, -1);
|
|
1898
|
+
}
|
|
1899
|
+
const isId = parts.includes("@id");
|
|
1900
|
+
const isPrimitive = /^(String|Int|Boolean|DateTime|Float|Json|Bytes|Decimal|BigInt)$/.test(typeStr);
|
|
1901
|
+
const isRelation = !isPrimitive && /^[A-Z]/.test(typeStr) && !enums.has(typeStr);
|
|
1902
|
+
fields.push({
|
|
1903
|
+
name: fieldName,
|
|
1904
|
+
type: typeStr,
|
|
1905
|
+
isRequired,
|
|
1906
|
+
isList,
|
|
1907
|
+
isId,
|
|
1908
|
+
isRelation
|
|
1909
|
+
});
|
|
1910
|
+
if (isRelation) {
|
|
1911
|
+
relations.push({
|
|
1912
|
+
fieldName,
|
|
1913
|
+
targetModel: typeStr,
|
|
1914
|
+
isList
|
|
1915
|
+
});
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
models.push({
|
|
1919
|
+
name: modelName,
|
|
1920
|
+
fields,
|
|
1921
|
+
relations
|
|
1922
|
+
});
|
|
1923
|
+
}
|
|
1924
|
+
return models;
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
// src/commands/schema.ts
|
|
1928
|
+
var MODEL_W = 260;
|
|
1929
|
+
var MODEL_H_BASE = 36;
|
|
1930
|
+
var FIELD_H = 20;
|
|
1931
|
+
var MODELS_PER_COL = 3;
|
|
1932
|
+
var NODE_GAP_X2 = 60;
|
|
1933
|
+
var NODE_GAP_Y2 = 40;
|
|
1934
|
+
var CANVAS_PAD2 = 80;
|
|
1935
|
+
var MIN_CANVAS_W2 = 1e3;
|
|
1936
|
+
var MIN_CANVAS_H2 = 800;
|
|
1937
|
+
function tryOpenInVSCode2(path) {
|
|
1938
|
+
try {
|
|
1939
|
+
(0, import_node_child_process2.execSync)(`code "${path}"`, { stdio: "ignore" });
|
|
1940
|
+
} catch {
|
|
1941
|
+
process.stderr.write(
|
|
1942
|
+
`Could not open in VS Code automatically.
|
|
1943
|
+
Open manually: code "${path}"
|
|
1944
|
+
`
|
|
1945
|
+
);
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1948
|
+
function assignLayout(models) {
|
|
1949
|
+
const layoutModels = models.map((m) => ({
|
|
1950
|
+
...m,
|
|
1951
|
+
x: 0,
|
|
1952
|
+
y: 0,
|
|
1953
|
+
w: MODEL_W,
|
|
1954
|
+
h: MODEL_H_BASE + Math.min(m.fields.length, 8) * FIELD_H,
|
|
1955
|
+
id: `m_${m.name}`
|
|
1956
|
+
}));
|
|
1957
|
+
const columns = [];
|
|
1958
|
+
let currentCol = [];
|
|
1959
|
+
for (const m of layoutModels) {
|
|
1960
|
+
if (currentCol.length >= MODELS_PER_COL) {
|
|
1961
|
+
columns.push(currentCol);
|
|
1962
|
+
currentCol = [];
|
|
1963
|
+
}
|
|
1964
|
+
currentCol.push(m);
|
|
1965
|
+
}
|
|
1966
|
+
if (currentCol.length > 0) columns.push(currentCol);
|
|
1967
|
+
let curX = CANVAS_PAD2;
|
|
1968
|
+
let maxY = CANVAS_PAD2;
|
|
1969
|
+
for (const col of columns) {
|
|
1970
|
+
let curY = CANVAS_PAD2;
|
|
1971
|
+
for (const m of col) {
|
|
1972
|
+
m.x = curX;
|
|
1973
|
+
m.y = curY;
|
|
1974
|
+
curY += m.h + NODE_GAP_Y2;
|
|
1975
|
+
}
|
|
1976
|
+
maxY = Math.max(maxY, curY - NODE_GAP_Y2);
|
|
1977
|
+
curX += MODEL_W + NODE_GAP_X2;
|
|
1978
|
+
}
|
|
1979
|
+
const canvasW = Math.max(MIN_CANVAS_W2, curX - NODE_GAP_X2 + CANVAS_PAD2);
|
|
1980
|
+
const canvasH = Math.max(MIN_CANVAS_H2, maxY + CANVAS_PAD2);
|
|
1981
|
+
return { layoutModels, canvasW, canvasH };
|
|
1982
|
+
}
|
|
1983
|
+
function buildDiagramLines(models, canvasW, canvasH, schemaName) {
|
|
1984
|
+
const lines = [];
|
|
1985
|
+
lines.push(
|
|
1986
|
+
JSON.stringify({
|
|
1987
|
+
width: canvasW,
|
|
1988
|
+
height: canvasH,
|
|
1989
|
+
background: "#0a0f1e",
|
|
1990
|
+
metadata: {
|
|
1991
|
+
name: schemaName,
|
|
1992
|
+
diagramType: "schema",
|
|
1993
|
+
generatedAt: Date.now(),
|
|
1994
|
+
modelCount: models.length
|
|
1995
|
+
}
|
|
1996
|
+
})
|
|
1997
|
+
);
|
|
1998
|
+
const edgesOutput = /* @__PURE__ */ new Set();
|
|
1999
|
+
for (const model of models) {
|
|
2000
|
+
for (const rel of model.relations) {
|
|
2001
|
+
const targetModel = models.find((m) => m.name === rel.targetModel);
|
|
2002
|
+
if (!targetModel) continue;
|
|
2003
|
+
const sourceId = model.id;
|
|
2004
|
+
const targetId = targetModel.id;
|
|
2005
|
+
const relId = `rel_${model.name}_${targetModel.name}`;
|
|
2006
|
+
const revRelId = `rel_${targetModel.name}_${model.name}`;
|
|
2007
|
+
if (edgesOutput.has(revRelId)) continue;
|
|
2008
|
+
edgesOutput.add(relId);
|
|
2009
|
+
const targetRel = targetModel.relations.find((r) => r.targetModel === model.name);
|
|
2010
|
+
const isOneToMany = rel.isList || targetRel && targetRel.isList;
|
|
2011
|
+
lines.push(
|
|
2012
|
+
JSON.stringify({
|
|
2013
|
+
id: relId,
|
|
2014
|
+
type: "edge",
|
|
2015
|
+
source: sourceId,
|
|
2016
|
+
target: targetId,
|
|
2017
|
+
zIndex: 0,
|
|
2018
|
+
label: { text: isOneToMany ? "1:N" : "1:1", fontSize: 10, color: "#94a3b8" },
|
|
2019
|
+
style: {
|
|
2020
|
+
strokeStyle: "#4c1d95",
|
|
2021
|
+
lineWidth: 1.5,
|
|
2022
|
+
arrowheadEnd: true,
|
|
2023
|
+
strokeOpacity: 0.7
|
|
2024
|
+
}
|
|
2025
|
+
})
|
|
2026
|
+
);
|
|
2027
|
+
}
|
|
2028
|
+
const maxLines = 8;
|
|
2029
|
+
const fieldsToPrint = model.fields.slice(0, maxLines);
|
|
2030
|
+
const hasMore = model.fields.length > maxLines;
|
|
2031
|
+
const printedLines = fieldsToPrint.map((f) => {
|
|
2032
|
+
let line = `${f.name}: ${f.type}${f.isList ? "[]" : ""}`;
|
|
2033
|
+
if (!f.isRequired) line += "?";
|
|
2034
|
+
if (f.isId) line += " @id";
|
|
2035
|
+
return line;
|
|
2036
|
+
});
|
|
2037
|
+
if (hasMore) {
|
|
2038
|
+
printedLines.push(`... +${model.fields.length - maxLines} more`);
|
|
2039
|
+
}
|
|
2040
|
+
const textContent = `${model.name}
|
|
2041
|
+
${"\u2500".repeat(model.name.length + 8)}
|
|
2042
|
+
${printedLines.join(
|
|
2043
|
+
"\n"
|
|
2044
|
+
)}`;
|
|
2045
|
+
lines.push(
|
|
2046
|
+
JSON.stringify({
|
|
2047
|
+
id: model.id,
|
|
2048
|
+
type: "node",
|
|
2049
|
+
position: { x: model.x, y: model.y },
|
|
2050
|
+
size: { width: model.w, height: model.h },
|
|
2051
|
+
shape: "rectangle",
|
|
2052
|
+
zIndex: 2,
|
|
2053
|
+
style: {
|
|
2054
|
+
fillStyle: "#1e1b4b",
|
|
2055
|
+
strokeStyle: "#7c3aed",
|
|
2056
|
+
lineWidth: 2,
|
|
2057
|
+
fillOpacity: 1,
|
|
2058
|
+
strokeOpacity: 1,
|
|
2059
|
+
cornerRadii: { topLeft: 6, topRight: 6, bottomRight: 6, bottomLeft: 6 }
|
|
2060
|
+
},
|
|
2061
|
+
text: {
|
|
2062
|
+
content: textContent,
|
|
2063
|
+
fontSize: 12,
|
|
2064
|
+
fontFamily: "monospace",
|
|
2065
|
+
color: "#e2e8f0",
|
|
2066
|
+
textAlign: "left",
|
|
2067
|
+
verticalAlign: "top"
|
|
2068
|
+
}
|
|
2069
|
+
})
|
|
2070
|
+
);
|
|
2071
|
+
}
|
|
2072
|
+
return lines;
|
|
2073
|
+
}
|
|
2074
|
+
function registerSchemaCommand(program2) {
|
|
2075
|
+
program2.command("schema [file]").description("Generate an ER diagram from a Prisma schema").option("--output <path>", "Output file path (default: <filename>.drawit)").option("--open", "Open generated file in VS Code").action((fileArg, opts) => {
|
|
2076
|
+
try {
|
|
2077
|
+
let targetFile = fileArg;
|
|
2078
|
+
if (!targetFile) {
|
|
2079
|
+
const autoPath = (0, import_node_path3.resolve)(process.cwd(), "prisma/schema.prisma");
|
|
2080
|
+
if ((0, import_node_fs4.existsSync)(autoPath)) {
|
|
2081
|
+
targetFile = autoPath;
|
|
2082
|
+
} else {
|
|
2083
|
+
process.stderr.write(
|
|
2084
|
+
`Error: Could not find prisma/schema.prisma and no file was provided.
|
|
2085
|
+
`
|
|
2086
|
+
);
|
|
2087
|
+
process.exitCode = 1;
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
} else {
|
|
2091
|
+
targetFile = (0, import_node_path3.resolve)(process.cwd(), targetFile);
|
|
2092
|
+
}
|
|
2093
|
+
if (!(0, import_node_fs4.existsSync)(targetFile)) {
|
|
2094
|
+
process.stderr.write(`Error: file "${targetFile}" not found.
|
|
2095
|
+
`);
|
|
2096
|
+
process.exitCode = 1;
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
const content = (0, import_node_fs4.readFileSync)(targetFile, "utf-8");
|
|
2100
|
+
const models = parsePrismaSchema(content);
|
|
2101
|
+
if (models.length === 0) {
|
|
2102
|
+
process.stderr.write(`No models found in ${targetFile}.
|
|
2103
|
+
`);
|
|
2104
|
+
process.exitCode = 0;
|
|
2105
|
+
return;
|
|
2106
|
+
}
|
|
2107
|
+
const { layoutModels, canvasW, canvasH } = assignLayout(models);
|
|
2108
|
+
const filenameWithoutExt = (0, import_node_path3.basename)(targetFile, ".prisma");
|
|
2109
|
+
const lines = buildDiagramLines(layoutModels, canvasW, canvasH, filenameWithoutExt);
|
|
2110
|
+
const outputPath = opts.output ?? `${filenameWithoutExt}.drawit`;
|
|
2111
|
+
writeOutput(lines.join("\n") + "\n", outputPath);
|
|
2112
|
+
process.stderr.write(`Generated ER diagram: ${outputPath}
|
|
2113
|
+
`);
|
|
2114
|
+
if (opts.open) tryOpenInVSCode2(outputPath);
|
|
2115
|
+
process.exitCode = 0;
|
|
2116
|
+
} catch (err) {
|
|
2117
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2118
|
+
`);
|
|
2119
|
+
process.exitCode = 1;
|
|
2120
|
+
}
|
|
2121
|
+
});
|
|
2122
|
+
}
|
|
2123
|
+
|
|
2124
|
+
// src/commands/deps.ts
|
|
2125
|
+
var import_node_fs6 = require("fs");
|
|
2126
|
+
var import_node_path5 = require("path");
|
|
2127
|
+
var import_node_child_process3 = require("child_process");
|
|
2128
|
+
|
|
2129
|
+
// src/utils/deps-analyzer.ts
|
|
2130
|
+
var import_node_fs5 = require("fs");
|
|
2131
|
+
var import_node_path4 = require("path");
|
|
2132
|
+
function sanitizePackageId(name) {
|
|
2133
|
+
return "p_" + name.replace(/[^a-zA-Z0-9]/g, "_");
|
|
2134
|
+
}
|
|
2135
|
+
function analyzeDeps(rootDir, options) {
|
|
2136
|
+
const packagePaths = [];
|
|
2137
|
+
function walkDir2(current, depth) {
|
|
2138
|
+
if (depth > options.maxDepth) return;
|
|
2139
|
+
let entries;
|
|
2140
|
+
try {
|
|
2141
|
+
entries = (0, import_node_fs5.readdirSync)(current, { withFileTypes: true });
|
|
2142
|
+
} catch {
|
|
2143
|
+
return;
|
|
2144
|
+
}
|
|
2145
|
+
for (const entry of entries) {
|
|
2146
|
+
if (entry.name.startsWith(".")) continue;
|
|
2147
|
+
const fullPath = (0, import_node_path4.join)(current, entry.name);
|
|
2148
|
+
const relFromRoot = (0, import_node_path4.relative)(rootDir, fullPath);
|
|
2149
|
+
if (isExcluded(entry.name, relFromRoot, options.excludePatterns)) continue;
|
|
2150
|
+
if (entry.isDirectory()) {
|
|
2151
|
+
walkDir2(fullPath, depth + 1);
|
|
2152
|
+
} else if (entry.isFile() && entry.name === "package.json") {
|
|
2153
|
+
packagePaths.push(fullPath);
|
|
2154
|
+
}
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
walkDir2(rootDir, 0);
|
|
2158
|
+
const rawPackages = [];
|
|
2159
|
+
const workspaceNames = /* @__PURE__ */ new Set();
|
|
2160
|
+
for (const pkgPath of packagePaths) {
|
|
2161
|
+
try {
|
|
2162
|
+
const content = (0, import_node_fs5.readFileSync)(pkgPath, "utf-8");
|
|
2163
|
+
const json = JSON.parse(content);
|
|
2164
|
+
if (json.name) {
|
|
2165
|
+
workspaceNames.add(json.name);
|
|
2166
|
+
rawPackages.push({ path: pkgPath, json });
|
|
2167
|
+
}
|
|
2168
|
+
} catch {
|
|
2169
|
+
}
|
|
2170
|
+
}
|
|
2171
|
+
const nodes = [];
|
|
2172
|
+
for (const pkg of rawPackages) {
|
|
2173
|
+
const { path, json } = pkg;
|
|
2174
|
+
const relDir = (0, import_node_path4.relative)(rootDir, (0, import_node_path4.dirname)(path));
|
|
2175
|
+
const parts = relDir.split("/");
|
|
2176
|
+
const groupDir = parts.length > 0 && parts[0] !== "" ? parts[0] : ".";
|
|
2177
|
+
const deps = /* @__PURE__ */ new Set();
|
|
2178
|
+
const allDeps = { ...json.dependencies || {} };
|
|
2179
|
+
if (options.includeDev) {
|
|
2180
|
+
Object.assign(allDeps, json.devDependencies || {});
|
|
2181
|
+
}
|
|
2182
|
+
for (const depName of Object.keys(allDeps)) {
|
|
2183
|
+
if (options.workspaceOnly) {
|
|
2184
|
+
if (workspaceNames.has(depName)) {
|
|
2185
|
+
deps.add(depName);
|
|
2186
|
+
}
|
|
2187
|
+
} else {
|
|
2188
|
+
deps.add(depName);
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
nodes.push({
|
|
2192
|
+
id: sanitizePackageId(json.name),
|
|
2193
|
+
name: json.name,
|
|
2194
|
+
version: json.version || "0.0.0",
|
|
2195
|
+
relativePath: relDir,
|
|
2196
|
+
isWorkspace: true,
|
|
2197
|
+
dependencies: Array.from(deps),
|
|
2198
|
+
fileName: "package.json",
|
|
2199
|
+
dirPath: groupDir,
|
|
2200
|
+
ext: ".json",
|
|
2201
|
+
x: 0,
|
|
2202
|
+
y: 0
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
if (!options.workspaceOnly) {
|
|
2206
|
+
const allKnownDeps = /* @__PURE__ */ new Set();
|
|
2207
|
+
for (const node of nodes) {
|
|
2208
|
+
for (const d of node.dependencies) {
|
|
2209
|
+
allKnownDeps.add(d);
|
|
2210
|
+
}
|
|
2211
|
+
}
|
|
2212
|
+
for (const d of allKnownDeps) {
|
|
2213
|
+
if (!workspaceNames.has(d)) {
|
|
2214
|
+
nodes.push({
|
|
2215
|
+
id: sanitizePackageId(d),
|
|
2216
|
+
name: d,
|
|
2217
|
+
version: "external",
|
|
2218
|
+
relativePath: "node_modules/" + d,
|
|
2219
|
+
isWorkspace: false,
|
|
2220
|
+
dependencies: [],
|
|
2221
|
+
fileName: "package.json",
|
|
2222
|
+
dirPath: "external",
|
|
2223
|
+
ext: ".json",
|
|
2224
|
+
x: 0,
|
|
2225
|
+
y: 0
|
|
2226
|
+
});
|
|
2227
|
+
}
|
|
2228
|
+
}
|
|
2229
|
+
}
|
|
2230
|
+
return nodes;
|
|
2231
|
+
}
|
|
2232
|
+
|
|
2233
|
+
// src/commands/deps.ts
|
|
2234
|
+
function tryOpenInVSCode3(path) {
|
|
2235
|
+
try {
|
|
2236
|
+
(0, import_node_child_process3.execSync)(`code "${path}"`, { stdio: "ignore" });
|
|
2237
|
+
} catch {
|
|
2238
|
+
process.stderr.write(
|
|
2239
|
+
`Could not open in VS Code automatically.
|
|
2240
|
+
Open manually: code "${path}"
|
|
2241
|
+
`
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
}
|
|
2245
|
+
function buildDiagramLines2(nodes, canvasW, canvasH, workspaceName) {
|
|
2246
|
+
const lines = [];
|
|
2247
|
+
lines.push(
|
|
2248
|
+
JSON.stringify({
|
|
2249
|
+
width: canvasW,
|
|
2250
|
+
height: canvasH,
|
|
2251
|
+
background: "#0a0f1e",
|
|
2252
|
+
metadata: {
|
|
2253
|
+
name: `${workspaceName} - Dependencies`,
|
|
2254
|
+
diagramType: "deps",
|
|
2255
|
+
generatedAt: Date.now(),
|
|
2256
|
+
packageCount: nodes.length
|
|
2257
|
+
}
|
|
2258
|
+
})
|
|
2259
|
+
);
|
|
2260
|
+
const groupMap = /* @__PURE__ */ new Map();
|
|
2261
|
+
for (const n of nodes) {
|
|
2262
|
+
if (!groupMap.has(n.dirPath)) groupMap.set(n.dirPath, []);
|
|
2263
|
+
groupMap.get(n.dirPath).push(n);
|
|
2264
|
+
}
|
|
2265
|
+
const groups = layoutGroups(nodes).groups;
|
|
2266
|
+
for (const group of groups) {
|
|
2267
|
+
lines.push(
|
|
2268
|
+
JSON.stringify({
|
|
2269
|
+
id: `grp_${group.dirId}`,
|
|
2270
|
+
type: "node",
|
|
2271
|
+
position: { x: group.x, y: group.y },
|
|
2272
|
+
size: { width: group.w, height: group.h },
|
|
2273
|
+
shape: "rectangle",
|
|
2274
|
+
zIndex: 1,
|
|
2275
|
+
style: {
|
|
2276
|
+
fillStyle: "#0f2744",
|
|
2277
|
+
strokeStyle: "#1e4080",
|
|
2278
|
+
lineWidth: 1,
|
|
2279
|
+
fillOpacity: 0.85,
|
|
2280
|
+
strokeOpacity: 1,
|
|
2281
|
+
cornerRadii: { topLeft: 8, topRight: 8, bottomRight: 8, bottomLeft: 8 }
|
|
2282
|
+
},
|
|
2283
|
+
text: {
|
|
2284
|
+
content: group.label,
|
|
2285
|
+
fontSize: 11,
|
|
2286
|
+
fontFamily: "geist",
|
|
2287
|
+
color: "#94a3b8",
|
|
2288
|
+
textAlign: "left",
|
|
2289
|
+
verticalAlign: "top"
|
|
2290
|
+
}
|
|
2291
|
+
})
|
|
2292
|
+
);
|
|
2293
|
+
}
|
|
2294
|
+
for (const node of nodes) {
|
|
2295
|
+
const textContent = `${node.name}
|
|
2296
|
+
v${node.version}`;
|
|
2297
|
+
const fillStyle = node.isWorkspace ? "#1a2f52" : "#3d1f00";
|
|
2298
|
+
const strokeStyle = node.isWorkspace ? "#3b82f6" : "#f59e0b";
|
|
2299
|
+
const w = 220;
|
|
2300
|
+
const h = 56;
|
|
2301
|
+
const cx = node.x + 190 / 2;
|
|
2302
|
+
const cy = node.y + 48 / 2;
|
|
2303
|
+
lines.push(
|
|
2304
|
+
JSON.stringify({
|
|
2305
|
+
id: node.id,
|
|
2306
|
+
type: "node",
|
|
2307
|
+
position: { x: cx - w / 2, y: cy - h / 2 },
|
|
2308
|
+
size: { width: w, height: h },
|
|
2309
|
+
shape: "rectangle",
|
|
2310
|
+
zIndex: 2,
|
|
2311
|
+
style: {
|
|
2312
|
+
fillStyle,
|
|
2313
|
+
strokeStyle,
|
|
2314
|
+
lineWidth: 1.5,
|
|
2315
|
+
fillOpacity: 1,
|
|
2316
|
+
strokeOpacity: 1,
|
|
2317
|
+
cornerRadii: { topLeft: 6, topRight: 6, bottomRight: 6, bottomLeft: 6 }
|
|
2318
|
+
},
|
|
2319
|
+
text: {
|
|
2320
|
+
content: textContent,
|
|
2321
|
+
fontSize: 12,
|
|
2322
|
+
fontFamily: "monospace",
|
|
2323
|
+
color: "#e2e8f0",
|
|
2324
|
+
textAlign: "center",
|
|
2325
|
+
verticalAlign: "middle"
|
|
2326
|
+
}
|
|
2327
|
+
})
|
|
2328
|
+
);
|
|
2329
|
+
}
|
|
2330
|
+
let edgeIndex = 0;
|
|
2331
|
+
for (const node of nodes) {
|
|
2332
|
+
for (const depName of node.dependencies) {
|
|
2333
|
+
const targetNode = nodes.find((n) => n.name === depName);
|
|
2334
|
+
if (targetNode) {
|
|
2335
|
+
lines.push(
|
|
2336
|
+
JSON.stringify({
|
|
2337
|
+
id: `edge_dep_${++edgeIndex}`,
|
|
2338
|
+
type: "edge",
|
|
2339
|
+
source: node.id,
|
|
2340
|
+
target: targetNode.id,
|
|
2341
|
+
zIndex: 0,
|
|
2342
|
+
style: {
|
|
2343
|
+
strokeStyle: "#334155",
|
|
2344
|
+
lineWidth: 1.5,
|
|
2345
|
+
arrowheadEnd: true,
|
|
2346
|
+
strokeOpacity: 0.8
|
|
2347
|
+
}
|
|
2348
|
+
})
|
|
2349
|
+
);
|
|
2350
|
+
}
|
|
2351
|
+
}
|
|
2352
|
+
}
|
|
2353
|
+
return lines;
|
|
2354
|
+
}
|
|
2355
|
+
function registerDepsCommand(program2) {
|
|
2356
|
+
program2.command("deps [path]").description("Generate a dependency graph of a workspace").option("--output <path>", "Output file path").option("--open", "Open generated file in VS Code").option("--no-workspace-only", "Include external npm packages").option("--include-dev", "Include devDependencies edges", false).option("--depth <n>", "Max dir depth to scan for package.json", "4").action((pathArg, opts) => {
|
|
2357
|
+
try {
|
|
2358
|
+
const targetDir = (0, import_node_path5.resolve)(pathArg ?? process.cwd());
|
|
2359
|
+
if (!(0, import_node_fs6.existsSync)(targetDir) || !(0, import_node_fs6.statSync)(targetDir).isDirectory()) {
|
|
2360
|
+
process.stderr.write(`Error: "${targetDir}" is not a directory.
|
|
2361
|
+
`);
|
|
2362
|
+
process.exitCode = 1;
|
|
2363
|
+
return;
|
|
2364
|
+
}
|
|
2365
|
+
const maxDepth = parseInt(opts.depth, 10);
|
|
2366
|
+
if (isNaN(maxDepth) || maxDepth < 1) {
|
|
2367
|
+
process.stderr.write(`Error: --depth must be a positive integer.
|
|
2368
|
+
`);
|
|
2369
|
+
process.exitCode = 1;
|
|
2370
|
+
return;
|
|
2371
|
+
}
|
|
2372
|
+
const options = {
|
|
2373
|
+
workspaceOnly: opts.workspaceOnly !== false,
|
|
2374
|
+
includeDev: !!opts.includeDev,
|
|
2375
|
+
maxDepth,
|
|
2376
|
+
excludePatterns: ["node_modules", "dist", "build", ".git", "out", ".turbo", "coverage"]
|
|
2377
|
+
};
|
|
2378
|
+
const nodes = analyzeDeps(targetDir, options);
|
|
2379
|
+
if (nodes.length === 0) {
|
|
2380
|
+
process.stderr.write(`No package.json files found in ${targetDir}.
|
|
2381
|
+
`);
|
|
2382
|
+
process.exitCode = 0;
|
|
2383
|
+
return;
|
|
2384
|
+
}
|
|
2385
|
+
const { canvasW, canvasH } = layoutGroups(nodes);
|
|
2386
|
+
const workspaceName = (0, import_node_path5.basename)(targetDir);
|
|
2387
|
+
const lines = buildDiagramLines2(nodes, canvasW, canvasH, workspaceName);
|
|
2388
|
+
const outputPath = opts.output ?? `${workspaceName}-deps.drawit`;
|
|
2389
|
+
writeOutput(lines.join("\n") + "\n", outputPath);
|
|
2390
|
+
process.stderr.write(`Generated dependency graph: ${outputPath}
|
|
2391
|
+
`);
|
|
2392
|
+
if (opts.open) tryOpenInVSCode3(outputPath);
|
|
2393
|
+
process.exitCode = 0;
|
|
2394
|
+
} catch (err) {
|
|
2395
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2396
|
+
`);
|
|
2397
|
+
process.exitCode = 1;
|
|
2398
|
+
}
|
|
2399
|
+
});
|
|
2400
|
+
}
|
|
2401
|
+
|
|
2402
|
+
// src/commands/flow.ts
|
|
2403
|
+
var import_node_fs7 = require("fs");
|
|
2404
|
+
var import_node_child_process4 = require("child_process");
|
|
2405
|
+
var import_node_path6 = require("path");
|
|
2406
|
+
var NODE_W2 = 200;
|
|
2407
|
+
var NODE_H2 = 60;
|
|
2408
|
+
var DIAMOND_W = 180;
|
|
2409
|
+
var DIAMOND_H = 80;
|
|
2410
|
+
var STEP_GAP = 40;
|
|
2411
|
+
var CANVAS_W = 500;
|
|
2412
|
+
var CANVAS_PAD3 = 80;
|
|
2413
|
+
function tryOpenInVSCode4(path) {
|
|
2414
|
+
try {
|
|
2415
|
+
(0, import_node_child_process4.execSync)(`code "${path}"`, { stdio: "ignore" });
|
|
2416
|
+
} catch {
|
|
2417
|
+
process.stderr.write(
|
|
2418
|
+
`Could not open in VS Code automatically.
|
|
2419
|
+
Open manually: code "${path}"
|
|
2420
|
+
`
|
|
2421
|
+
);
|
|
2422
|
+
}
|
|
2423
|
+
}
|
|
2424
|
+
function determineNodeType(text, index, total, explicitType) {
|
|
2425
|
+
if (explicitType === "start" || explicitType === "end" || explicitType === "decision" || explicitType === "process") {
|
|
2426
|
+
return explicitType;
|
|
2427
|
+
}
|
|
2428
|
+
if (index === 0) return "start";
|
|
2429
|
+
if (index === total - 1) return "end";
|
|
2430
|
+
if (text.trim().endsWith("?")) return "decision";
|
|
2431
|
+
return "process";
|
|
2432
|
+
}
|
|
2433
|
+
function getStyleForType(type) {
|
|
2434
|
+
switch (type) {
|
|
2435
|
+
case "start":
|
|
2436
|
+
return { shape: "ellipse", fillStyle: "#065f46", strokeStyle: "#34d399", w: NODE_W2, h: NODE_H2 };
|
|
2437
|
+
case "end":
|
|
2438
|
+
return { shape: "ellipse", fillStyle: "#7f1d1d", strokeStyle: "#f87171", w: NODE_W2, h: NODE_H2 };
|
|
2439
|
+
case "decision":
|
|
2440
|
+
return { shape: "diamond", fillStyle: "#78350f", strokeStyle: "#fbbf24", w: DIAMOND_W, h: DIAMOND_H };
|
|
2441
|
+
case "process":
|
|
2442
|
+
default:
|
|
2443
|
+
return { shape: "rectangle", fillStyle: "#1e3a5f", strokeStyle: "#3b82f6", w: NODE_W2, h: NODE_H2 };
|
|
2444
|
+
}
|
|
2445
|
+
}
|
|
2446
|
+
function layoutSteps(rawSteps) {
|
|
2447
|
+
const steps = [];
|
|
2448
|
+
let currentY = CANVAS_PAD3;
|
|
2449
|
+
for (let i = 0; i < rawSteps.length; i++) {
|
|
2450
|
+
const raw = rawSteps[i];
|
|
2451
|
+
const type = determineNodeType(raw.text, i, rawSteps.length, raw.type);
|
|
2452
|
+
const { shape, fillStyle, strokeStyle, w, h } = getStyleForType(type);
|
|
2453
|
+
steps.push({
|
|
2454
|
+
text: raw.text,
|
|
2455
|
+
type,
|
|
2456
|
+
shape,
|
|
2457
|
+
fillStyle,
|
|
2458
|
+
strokeStyle,
|
|
2459
|
+
w,
|
|
2460
|
+
h,
|
|
2461
|
+
x: (CANVAS_W - w) / 2,
|
|
2462
|
+
y: currentY
|
|
2463
|
+
});
|
|
2464
|
+
currentY += h + STEP_GAP;
|
|
2465
|
+
}
|
|
2466
|
+
return steps;
|
|
2467
|
+
}
|
|
2468
|
+
function buildDiagramLines3(steps) {
|
|
2469
|
+
const lines = [];
|
|
2470
|
+
const canvasH = steps.length > 0 ? steps[steps.length - 1].y + steps[steps.length - 1].h + CANVAS_PAD3 : CANVAS_PAD3 * 2;
|
|
2471
|
+
lines.push(
|
|
2472
|
+
JSON.stringify({
|
|
2473
|
+
width: CANVAS_W,
|
|
2474
|
+
height: canvasH,
|
|
2475
|
+
background: "#0a0f1e",
|
|
2476
|
+
metadata: {
|
|
2477
|
+
name: "Flowchart",
|
|
2478
|
+
diagramType: "flow",
|
|
2479
|
+
generatedAt: Date.now(),
|
|
2480
|
+
stepCount: steps.length
|
|
2481
|
+
}
|
|
2482
|
+
})
|
|
2483
|
+
);
|
|
2484
|
+
for (let i = 0; i < steps.length; i++) {
|
|
2485
|
+
const step = steps[i];
|
|
2486
|
+
const id = `step_${i}`;
|
|
2487
|
+
lines.push(
|
|
2488
|
+
JSON.stringify({
|
|
2489
|
+
id,
|
|
2490
|
+
type: "node",
|
|
2491
|
+
position: { x: step.x, y: step.y },
|
|
2492
|
+
size: { width: step.w, height: step.h },
|
|
2493
|
+
shape: step.shape,
|
|
2494
|
+
zIndex: 2,
|
|
2495
|
+
style: {
|
|
2496
|
+
fillStyle: step.fillStyle,
|
|
2497
|
+
strokeStyle: step.strokeStyle,
|
|
2498
|
+
lineWidth: 2,
|
|
2499
|
+
fillOpacity: 1,
|
|
2500
|
+
strokeOpacity: 1,
|
|
2501
|
+
cornerRadii: step.shape === "rectangle" ? { topLeft: 8, topRight: 8, bottomRight: 8, bottomLeft: 8 } : void 0
|
|
2502
|
+
},
|
|
2503
|
+
text: {
|
|
2504
|
+
content: step.text,
|
|
2505
|
+
fontSize: 14,
|
|
2506
|
+
fontFamily: "sans-serif",
|
|
2507
|
+
color: "#e2e8f0",
|
|
2508
|
+
textAlign: "center",
|
|
2509
|
+
verticalAlign: "middle"
|
|
2510
|
+
}
|
|
2511
|
+
})
|
|
2512
|
+
);
|
|
2513
|
+
if (i < steps.length - 1) {
|
|
2514
|
+
lines.push(
|
|
2515
|
+
JSON.stringify({
|
|
2516
|
+
id: `edge_${i}_to_${i + 1}`,
|
|
2517
|
+
type: "edge",
|
|
2518
|
+
source: id,
|
|
2519
|
+
target: `step_${i + 1}`,
|
|
2520
|
+
zIndex: 1,
|
|
2521
|
+
style: {
|
|
2522
|
+
strokeStyle: "#94a3b8",
|
|
2523
|
+
lineWidth: 2,
|
|
2524
|
+
arrowheadEnd: true,
|
|
2525
|
+
strokeOpacity: 0.8
|
|
2526
|
+
}
|
|
2527
|
+
})
|
|
2528
|
+
);
|
|
2529
|
+
}
|
|
2530
|
+
}
|
|
2531
|
+
return lines;
|
|
2532
|
+
}
|
|
2533
|
+
function registerFlowCommand(program2) {
|
|
2534
|
+
program2.command("flow [steps...]").description("Generate a flowchart diagram from CLI arguments or stdin").option("--output <path>", "Output file path (default: flow.drawit)").option("--open", "Open generated file in VS Code").option("--stdin", "Read steps from standard input (one per line)").action((inlineArgs, opts) => {
|
|
2535
|
+
try {
|
|
2536
|
+
let rawSteps = [];
|
|
2537
|
+
if (opts.stdin) {
|
|
2538
|
+
const content = (0, import_node_fs7.readFileSync)(0, "utf-8");
|
|
2539
|
+
const lines2 = content.split("\n").map((l) => l.trim()).filter(Boolean);
|
|
2540
|
+
rawSteps = lines2.map((text) => ({ text }));
|
|
2541
|
+
} else if (inlineArgs.length === 1 && inlineArgs[0].endsWith(".json")) {
|
|
2542
|
+
const targetFile = (0, import_node_path6.resolve)(process.cwd(), inlineArgs[0]);
|
|
2543
|
+
if (!(0, import_node_fs7.existsSync)(targetFile)) {
|
|
2544
|
+
process.stderr.write(`Error: file "${targetFile}" not found.
|
|
2545
|
+
`);
|
|
2546
|
+
process.exitCode = 1;
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
2549
|
+
const content = (0, import_node_fs7.readFileSync)(targetFile, "utf-8");
|
|
2550
|
+
const parsed = JSON.parse(content);
|
|
2551
|
+
if (Array.isArray(parsed)) {
|
|
2552
|
+
rawSteps = parsed;
|
|
2553
|
+
} else {
|
|
2554
|
+
process.stderr.write(`Error: JSON file must contain an array of steps.
|
|
2555
|
+
`);
|
|
2556
|
+
process.exitCode = 1;
|
|
2557
|
+
return;
|
|
2558
|
+
}
|
|
2559
|
+
} else {
|
|
2560
|
+
rawSteps = inlineArgs.map((text) => ({ text }));
|
|
2561
|
+
}
|
|
2562
|
+
if (rawSteps.length === 0) {
|
|
2563
|
+
process.stderr.write("Error: No steps provided. Use arguments, a JSON file, or --stdin.\n");
|
|
2564
|
+
process.exitCode = 1;
|
|
2565
|
+
return;
|
|
2566
|
+
}
|
|
2567
|
+
const steps = layoutSteps(rawSteps);
|
|
2568
|
+
const lines = buildDiagramLines3(steps);
|
|
2569
|
+
const outputPath = opts.output ?? "flow.drawit";
|
|
2570
|
+
writeOutput(lines.join("\n") + "\n", outputPath);
|
|
2571
|
+
process.stderr.write(`Generated flow diagram: ${outputPath}
|
|
2572
|
+
`);
|
|
2573
|
+
if (opts.open) tryOpenInVSCode4(outputPath);
|
|
2574
|
+
process.exitCode = 0;
|
|
2575
|
+
} catch (err) {
|
|
2576
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2577
|
+
`);
|
|
2578
|
+
process.exitCode = 1;
|
|
2579
|
+
}
|
|
2580
|
+
});
|
|
2581
|
+
}
|
|
2582
|
+
|
|
2583
|
+
// src/commands/routes.ts
|
|
2584
|
+
var import_node_fs8 = require("fs");
|
|
2585
|
+
var import_node_path7 = require("path");
|
|
2586
|
+
var import_node_child_process5 = require("child_process");
|
|
2587
|
+
function tryOpenInVSCode5(path) {
|
|
2588
|
+
try {
|
|
2589
|
+
(0, import_node_child_process5.execSync)(`code "${path}"`, { stdio: "ignore" });
|
|
2590
|
+
} catch {
|
|
2591
|
+
process.stderr.write(
|
|
2592
|
+
`Could not open in VS Code automatically.
|
|
2593
|
+
Open manually: code "${path}"
|
|
2594
|
+
`
|
|
2595
|
+
);
|
|
2596
|
+
}
|
|
2597
|
+
}
|
|
2598
|
+
function sanitizeId2(rPath) {
|
|
2599
|
+
if (rPath === ".") return "r_root";
|
|
2600
|
+
return "r_" + rPath.replace(/[^a-zA-Z0-9]/g, "_");
|
|
2601
|
+
}
|
|
2602
|
+
function determineSegmentType(segment) {
|
|
2603
|
+
if (segment.startsWith("(...)") || segment.startsWith("(..)")) return "intercept";
|
|
2604
|
+
if (segment.startsWith("[...") && segment.endsWith("]")) return "catch-all";
|
|
2605
|
+
if (segment.startsWith("[[...") && segment.endsWith("]]")) return "catch-all";
|
|
2606
|
+
if (segment.startsWith("[") && segment.endsWith("]")) return "dynamic";
|
|
2607
|
+
if (segment.startsWith("(") && segment.endsWith(")")) return "group";
|
|
2608
|
+
if (segment.startsWith("@")) return "parallel";
|
|
2609
|
+
return "static";
|
|
2610
|
+
}
|
|
2611
|
+
function buildSegmentNode(current, root, depth, parentId) {
|
|
2612
|
+
const relPath = (0, import_node_path7.relative)(root, current) || ".";
|
|
2613
|
+
const segment = depth === 0 ? (0, import_node_path7.basename)(current) : (0, import_node_path7.basename)(current);
|
|
2614
|
+
if (segment === "__tests__" || segment === "__mocks__" || segment.startsWith(".")) return null;
|
|
2615
|
+
let entries;
|
|
2616
|
+
try {
|
|
2617
|
+
entries = (0, import_node_fs8.readdirSync)(current, { withFileTypes: true });
|
|
2618
|
+
} catch {
|
|
2619
|
+
return null;
|
|
2620
|
+
}
|
|
2621
|
+
let hasPage = false;
|
|
2622
|
+
let hasRoute = false;
|
|
2623
|
+
let hasLayout = false;
|
|
2624
|
+
const childDirs = [];
|
|
2625
|
+
for (const entry of entries) {
|
|
2626
|
+
if (entry.name.startsWith(".")) continue;
|
|
2627
|
+
if (entry.isFile()) {
|
|
2628
|
+
const ext = (0, import_node_path7.extname)(entry.name);
|
|
2629
|
+
if ([".ts", ".tsx", ".js", ".jsx"].includes(ext)) {
|
|
2630
|
+
const withoutExt = entry.name.slice(0, -ext.length);
|
|
2631
|
+
if (withoutExt === "page") hasPage = true;
|
|
2632
|
+
if (withoutExt === "route") hasRoute = true;
|
|
2633
|
+
if (withoutExt === "layout") hasLayout = true;
|
|
2634
|
+
}
|
|
2635
|
+
} else if (entry.isDirectory()) {
|
|
2636
|
+
childDirs.push((0, import_node_path7.join)(current, entry.name));
|
|
2637
|
+
}
|
|
2638
|
+
}
|
|
2639
|
+
const children = [];
|
|
2640
|
+
const id = sanitizeId2(relPath);
|
|
2641
|
+
for (const childDir of childDirs) {
|
|
2642
|
+
const childNode = buildSegmentNode(childDir, root, depth + 1, id);
|
|
2643
|
+
if (childNode) {
|
|
2644
|
+
children.push(childNode);
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
if (depth > 0 && !hasPage && !hasRoute && !hasLayout && children.length === 0) {
|
|
2648
|
+
return null;
|
|
2649
|
+
}
|
|
2650
|
+
let url = "";
|
|
2651
|
+
const sType = determineSegmentType(segment);
|
|
2652
|
+
if (sType === "dynamic") url = `:${segment.slice(1, -1)}`;
|
|
2653
|
+
else if (sType === "catch-all") url = `*${segment.replace(/[[\]\.]/g, "")}`;
|
|
2654
|
+
else if (sType === "group" || sType === "parallel" || sType === "intercept") url = "";
|
|
2655
|
+
else url = segment;
|
|
2656
|
+
return {
|
|
2657
|
+
id,
|
|
2658
|
+
segment,
|
|
2659
|
+
url,
|
|
2660
|
+
segmentType: sType,
|
|
2661
|
+
hasPage,
|
|
2662
|
+
hasRoute,
|
|
2663
|
+
hasLayout,
|
|
2664
|
+
depth,
|
|
2665
|
+
children,
|
|
2666
|
+
parentId,
|
|
2667
|
+
x: 0,
|
|
2668
|
+
y: 0
|
|
2669
|
+
};
|
|
2670
|
+
}
|
|
2671
|
+
function getRouteStyle(node) {
|
|
2672
|
+
if (node.hasRoute) return { fillStyle: "#1e1b4b", strokeStyle: "#7c3aed", dashed: false };
|
|
2673
|
+
if (node.segmentType === "dynamic" || node.segmentType === "catch-all") return { fillStyle: "#3d1f00", strokeStyle: "#f59e0b", dashed: false };
|
|
2674
|
+
if (node.segmentType === "group") return { fillStyle: "#0f2744", strokeStyle: "#334155", dashed: true };
|
|
2675
|
+
if (node.hasLayout && !node.hasPage) return { fillStyle: "#14532d", strokeStyle: "#22c55e", dashed: false };
|
|
2676
|
+
return { fillStyle: "#1e3a5f", strokeStyle: "#3b82f6", dashed: false };
|
|
2677
|
+
}
|
|
2678
|
+
function buildDiagramLines4(nodes, edges, canvasW, canvasH, rootDirName) {
|
|
2679
|
+
const lines = [];
|
|
2680
|
+
lines.push(
|
|
2681
|
+
JSON.stringify({
|
|
2682
|
+
width: canvasW,
|
|
2683
|
+
height: canvasH,
|
|
2684
|
+
background: "#0a0f1e",
|
|
2685
|
+
metadata: {
|
|
2686
|
+
name: `${rootDirName} - Routes`,
|
|
2687
|
+
diagramType: "routes",
|
|
2688
|
+
generatedAt: Date.now(),
|
|
2689
|
+
routeCount: nodes.length
|
|
2690
|
+
}
|
|
2691
|
+
})
|
|
2692
|
+
);
|
|
2693
|
+
for (const node of nodes) {
|
|
2694
|
+
const style = getRouteStyle(node);
|
|
2695
|
+
const subLabels = [];
|
|
2696
|
+
if (node.hasPage) subLabels.push("page");
|
|
2697
|
+
if (node.hasLayout) subLabels.push("layout");
|
|
2698
|
+
if (node.hasRoute) subLabels.push("api");
|
|
2699
|
+
let content = node.url || node.segment;
|
|
2700
|
+
if (node.segmentType === "group" || node.segmentType === "parallel") {
|
|
2701
|
+
content = node.segment;
|
|
2702
|
+
}
|
|
2703
|
+
if (subLabels.length > 0) {
|
|
2704
|
+
content += `
|
|
2705
|
+
[${subLabels.join(", ")}]`;
|
|
2706
|
+
}
|
|
2707
|
+
lines.push(
|
|
2708
|
+
JSON.stringify({
|
|
2709
|
+
id: node.id,
|
|
2710
|
+
type: "node",
|
|
2711
|
+
position: { x: node.x, y: node.y },
|
|
2712
|
+
size: { width: DIR_W, height: DIR_H },
|
|
2713
|
+
shape: "rectangle",
|
|
2714
|
+
zIndex: node.depth + 1,
|
|
2715
|
+
style: {
|
|
2716
|
+
fillStyle: style.fillStyle,
|
|
2717
|
+
strokeStyle: style.strokeStyle,
|
|
2718
|
+
lineWidth: 1.5,
|
|
2719
|
+
fillOpacity: 1,
|
|
2720
|
+
strokeOpacity: 1,
|
|
2721
|
+
lineDash: style.dashed ? [4, 4] : void 0,
|
|
2722
|
+
cornerRadii: { topLeft: 12, topRight: 12, bottomRight: 12, bottomLeft: 12 }
|
|
2723
|
+
},
|
|
2724
|
+
text: {
|
|
2725
|
+
content,
|
|
2726
|
+
fontSize: 13,
|
|
2727
|
+
fontFamily: "monospace",
|
|
2728
|
+
color: "#e2e8f0",
|
|
2729
|
+
textAlign: "center",
|
|
2730
|
+
verticalAlign: "middle"
|
|
2731
|
+
}
|
|
2732
|
+
})
|
|
2733
|
+
);
|
|
2734
|
+
}
|
|
2735
|
+
let edgeCounter = 0;
|
|
2736
|
+
for (const edge of edges) {
|
|
2737
|
+
lines.push(
|
|
2738
|
+
JSON.stringify({
|
|
2739
|
+
id: `re_${edgeCounter++}`,
|
|
2740
|
+
type: "edge",
|
|
2741
|
+
source: edge.source,
|
|
2742
|
+
target: edge.target,
|
|
2743
|
+
zIndex: 0,
|
|
2744
|
+
style: {
|
|
2745
|
+
strokeStyle: "#334155",
|
|
2746
|
+
lineWidth: 1.5,
|
|
2747
|
+
arrowheadEnd: true,
|
|
2748
|
+
strokeOpacity: 0.8
|
|
2749
|
+
}
|
|
2750
|
+
})
|
|
2751
|
+
);
|
|
2752
|
+
}
|
|
2753
|
+
return lines;
|
|
2754
|
+
}
|
|
2755
|
+
function registerRoutesCommand(program2) {
|
|
2756
|
+
program2.command("routes [path]").description("Analyze a Next.js routes tree (app/ or pages/)").option("--output <path>", "Output file path (default: <dirname>-routes.drawit)").option("--open", "Open generated file in VS Code").action((pathArg, opts) => {
|
|
2757
|
+
try {
|
|
2758
|
+
let targetDir = (0, import_node_path7.resolve)(pathArg ?? process.cwd());
|
|
2759
|
+
if (!pathArg) {
|
|
2760
|
+
const appPath = (0, import_node_path7.join)(targetDir, "app");
|
|
2761
|
+
const pagesPath = (0, import_node_path7.join)(targetDir, "pages");
|
|
2762
|
+
if ((0, import_node_fs8.existsSync)(appPath)) {
|
|
2763
|
+
targetDir = appPath;
|
|
2764
|
+
} else if ((0, import_node_fs8.existsSync)(pagesPath)) {
|
|
2765
|
+
targetDir = pagesPath;
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
if (!(0, import_node_fs8.existsSync)(targetDir) || !(0, import_node_fs8.statSync)(targetDir).isDirectory()) {
|
|
2769
|
+
process.stderr.write(`Error: "${targetDir}" is not a directory.
|
|
2770
|
+
`);
|
|
2771
|
+
process.exitCode = 1;
|
|
2772
|
+
return;
|
|
2773
|
+
}
|
|
2774
|
+
const rootNode = buildSegmentNode(targetDir, targetDir, 0, null);
|
|
2775
|
+
if (!rootNode || rootNode.children.length === 0 && !rootNode.hasPage && !rootNode.hasRoute && !rootNode.hasLayout) {
|
|
2776
|
+
process.stderr.write(`No routes or Next.js app structure found in ${targetDir}.
|
|
2777
|
+
`);
|
|
2778
|
+
process.exitCode = 0;
|
|
2779
|
+
return;
|
|
2780
|
+
}
|
|
2781
|
+
const { nodes, canvasW, canvasH, edges } = layoutTree(rootNode);
|
|
2782
|
+
const dirname3 = (0, import_node_path7.basename)(targetDir === (0, import_node_path7.resolve)(process.cwd(), "app") ? (0, import_node_path7.resolve)(process.cwd()) : targetDir);
|
|
2783
|
+
const lines = buildDiagramLines4(nodes, edges, canvasW, canvasH, dirname3);
|
|
2784
|
+
const outputPath = opts.output ?? `${dirname3}-routes.drawit`;
|
|
2785
|
+
writeOutput(lines.join("\n") + "\n", outputPath);
|
|
2786
|
+
process.stderr.write(`Generated routes diagram: ${outputPath}
|
|
2787
|
+
`);
|
|
2788
|
+
if (opts.open) tryOpenInVSCode5(outputPath);
|
|
2789
|
+
process.exitCode = 0;
|
|
2790
|
+
} catch (err) {
|
|
2791
|
+
process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}
|
|
2792
|
+
`);
|
|
2793
|
+
process.exitCode = 1;
|
|
2794
|
+
}
|
|
2795
|
+
});
|
|
2796
|
+
}
|
|
2797
|
+
|
|
2798
|
+
// src/cli.ts
|
|
2799
|
+
function createProgram() {
|
|
2800
|
+
const program2 = new import_commander.Command();
|
|
2801
|
+
program2.name("drawit").description("CLI tool for validating, inspecting, and exporting DrawIt diagram files").version("0.1.0");
|
|
2802
|
+
registerValidateCommand(program2);
|
|
2803
|
+
registerInspectCommand(program2);
|
|
2804
|
+
registerExportCommand(program2);
|
|
2805
|
+
registerCreateCommand(program2);
|
|
2806
|
+
registerMergeCommand(program2);
|
|
2807
|
+
registerMapCommand(program2);
|
|
2808
|
+
registerSchemaCommand(program2);
|
|
2809
|
+
registerDepsCommand(program2);
|
|
2810
|
+
registerFlowCommand(program2);
|
|
2811
|
+
registerRoutesCommand(program2);
|
|
2812
|
+
return program2;
|
|
2813
|
+
}
|
|
2814
|
+
|
|
2815
|
+
// src/index.ts
|
|
2816
|
+
var program = createProgram();
|
|
2817
|
+
program.parse(process.argv);
|