@trohde/excal-cli 1.0.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.
@@ -0,0 +1,1245 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli/index.ts
4
+ import { Command, CommanderError } from "commander";
5
+
6
+ // src/core/envelope.ts
7
+ function buildEnvelope(opts) {
8
+ return {
9
+ schema_version: "1.0",
10
+ request_id: opts.request_id,
11
+ ok: opts.ok,
12
+ command: opts.command,
13
+ target: opts.target ?? null,
14
+ result: opts.result,
15
+ warnings: opts.warnings ?? [],
16
+ errors: opts.errors ?? [],
17
+ metrics: { duration_ms: opts.duration_ms }
18
+ };
19
+ }
20
+
21
+ // src/core/errors.ts
22
+ var CliError = class extends Error {
23
+ constructor(structured) {
24
+ super(structured.message);
25
+ this.structured = structured;
26
+ this.name = "CliError";
27
+ }
28
+ };
29
+ function validationError(code, message, details) {
30
+ return new CliError({
31
+ code: `ERR_VALIDATION_${code}`,
32
+ message,
33
+ retryable: false,
34
+ suggested_action: "fix_input",
35
+ details
36
+ });
37
+ }
38
+ function renderError(code, message, details) {
39
+ return new CliError({
40
+ code: `ERR_RENDER_${code}`,
41
+ message,
42
+ retryable: false,
43
+ suggested_action: "escalate",
44
+ details
45
+ });
46
+ }
47
+ function ioError(code, message, details) {
48
+ return new CliError({
49
+ code: `ERR_IO_${code}`,
50
+ message,
51
+ retryable: true,
52
+ suggested_action: "retry",
53
+ details
54
+ });
55
+ }
56
+ function internalError(message, details) {
57
+ return new CliError({
58
+ code: "ERR_INTERNAL_UNEXPECTED",
59
+ message,
60
+ retryable: false,
61
+ suggested_action: "escalate",
62
+ details
63
+ });
64
+ }
65
+ function errorMessage(err) {
66
+ return err instanceof Error ? err.message : String(err);
67
+ }
68
+
69
+ // src/core/exit-codes.ts
70
+ var ExitCode = {
71
+ OK: 0,
72
+ VALIDATION: 10,
73
+ RENDER: 20,
74
+ IO: 50,
75
+ INTERNAL: 90
76
+ };
77
+ function exitCodeForError(code) {
78
+ if (code.startsWith("ERR_VALIDATION")) return ExitCode.VALIDATION;
79
+ if (code.startsWith("ERR_RENDER")) return ExitCode.RENDER;
80
+ if (code.startsWith("ERR_IO")) return ExitCode.IO;
81
+ return ExitCode.INTERNAL;
82
+ }
83
+
84
+ // src/core/request-id.ts
85
+ import { randomBytes } from "crypto";
86
+ function generateRequestId() {
87
+ const now = /* @__PURE__ */ new Date();
88
+ const pad = (n, len = 2) => String(n).padStart(len, "0");
89
+ const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`;
90
+ const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`;
91
+ const rand = randomBytes(2).toString("hex");
92
+ return `req_${date}_${time}_${rand}`;
93
+ }
94
+
95
+ // src/core/timer.ts
96
+ var Timer = class {
97
+ start;
98
+ constructor() {
99
+ this.start = process.hrtime.bigint();
100
+ }
101
+ elapsed() {
102
+ const diff = process.hrtime.bigint() - this.start;
103
+ return Number(diff / 1000000n);
104
+ }
105
+ };
106
+
107
+ // src/core/command-wrapper.ts
108
+ function wrapCommand(command, handler) {
109
+ return async (...args) => {
110
+ const timer = new Timer();
111
+ const requestId = generateRequestId();
112
+ const warnings = [];
113
+ const ctx = {
114
+ requestId,
115
+ timer,
116
+ warnings,
117
+ warn(w) {
118
+ warnings.push(w);
119
+ }
120
+ };
121
+ let envelope;
122
+ try {
123
+ const { result, target } = await handler(ctx, ...args);
124
+ envelope = buildEnvelope({
125
+ request_id: requestId,
126
+ command,
127
+ target,
128
+ result,
129
+ ok: true,
130
+ warnings,
131
+ duration_ms: timer.elapsed()
132
+ });
133
+ } catch (err) {
134
+ const structured = err instanceof CliError ? err.structured : internalError(errorMessage(err)).structured;
135
+ envelope = buildEnvelope({
136
+ request_id: requestId,
137
+ command,
138
+ result: null,
139
+ ok: false,
140
+ warnings,
141
+ errors: [structured],
142
+ duration_ms: timer.elapsed()
143
+ });
144
+ process.exitCode = exitCodeForError(structured.code);
145
+ }
146
+ const json = JSON.stringify(envelope, null, 2) + "\n";
147
+ await new Promise((resolve2) => {
148
+ process.stdout.write(json, () => resolve2());
149
+ });
150
+ };
151
+ }
152
+
153
+ // src/scene/schema.ts
154
+ import { z } from "zod";
155
+ var ExcalidrawElementSchema = z.object({
156
+ id: z.string(),
157
+ type: z.string(),
158
+ x: z.number(),
159
+ y: z.number(),
160
+ width: z.number(),
161
+ height: z.number(),
162
+ isDeleted: z.boolean().optional().default(false),
163
+ opacity: z.number().optional().default(100),
164
+ groupIds: z.array(z.string()).optional().default([]),
165
+ frameId: z.string().nullable().optional().default(null),
166
+ boundElements: z.array(
167
+ z.object({
168
+ id: z.string(),
169
+ type: z.string()
170
+ })
171
+ ).nullable().optional().default(null),
172
+ // Text-specific
173
+ text: z.string().optional(),
174
+ fontSize: z.number().optional(),
175
+ fontFamily: z.number().optional(),
176
+ containerId: z.string().nullable().optional().default(null),
177
+ // Arrow-specific
178
+ startBinding: z.object({
179
+ elementId: z.string(),
180
+ focus: z.number(),
181
+ gap: z.number()
182
+ }).nullable().optional().default(null),
183
+ endBinding: z.object({
184
+ elementId: z.string(),
185
+ focus: z.number(),
186
+ gap: z.number()
187
+ }).nullable().optional().default(null),
188
+ // Image-specific
189
+ fileId: z.string().nullable().optional().default(null),
190
+ // Frame-specific
191
+ name: z.string().nullable().optional().default(null)
192
+ }).passthrough();
193
+ var ExcalidrawSceneSchema = z.object({
194
+ type: z.string().optional().default("excalidraw"),
195
+ version: z.number().optional().default(2),
196
+ source: z.string().optional(),
197
+ elements: z.array(ExcalidrawElementSchema),
198
+ appState: z.record(z.unknown()).optional().default({}),
199
+ files: z.record(z.unknown()).optional().default({})
200
+ });
201
+
202
+ // src/core/io.ts
203
+ import { readFile, writeFile, rename, mkdir } from "fs/promises";
204
+ import { dirname, join } from "path";
205
+ import { randomBytes as randomBytes2 } from "crypto";
206
+ async function readInput(fileOrDash) {
207
+ if (fileOrDash === "-") {
208
+ return readStdin();
209
+ }
210
+ try {
211
+ const content = await readFile(fileOrDash, "utf-8");
212
+ return { content, source: fileOrDash };
213
+ } catch (err) {
214
+ const msg = errorMessage(err);
215
+ throw ioError("READ_FAILED", `Failed to read file: ${fileOrDash}: ${msg}`, {
216
+ path: fileOrDash
217
+ });
218
+ }
219
+ }
220
+ async function readStdin() {
221
+ return new Promise((resolve2, reject) => {
222
+ const chunks = [];
223
+ process.stdin.on("data", (chunk) => chunks.push(chunk));
224
+ process.stdin.on("end", () => {
225
+ resolve2({
226
+ content: Buffer.concat(chunks).toString("utf-8"),
227
+ source: "stdin"
228
+ });
229
+ });
230
+ process.stdin.on("error", (err) => {
231
+ reject(ioError("STDIN_FAILED", `Failed to read stdin: ${err.message}`));
232
+ });
233
+ });
234
+ }
235
+ async function ensureDir(dir) {
236
+ await mkdir(dir, { recursive: true });
237
+ }
238
+ async function writeOutput(path, data) {
239
+ try {
240
+ await ensureDir(dirname(path));
241
+ const tmpPath = join(dirname(path), `.tmp_${randomBytes2(4).toString("hex")}`);
242
+ await writeFile(tmpPath, data);
243
+ await rename(tmpPath, path);
244
+ } catch (err) {
245
+ const msg = errorMessage(err);
246
+ throw ioError("WRITE_FAILED", `Failed to write file: ${path}: ${msg}`, {
247
+ path
248
+ });
249
+ }
250
+ }
251
+
252
+ // src/core/fingerprint.ts
253
+ import { createHash } from "crypto";
254
+ function sha256(data) {
255
+ return createHash("sha256").update(data).digest("hex");
256
+ }
257
+
258
+ // src/scene/load-scene.ts
259
+ async function loadScene(fileOrDash) {
260
+ const { content, source } = await readInput(fileOrDash);
261
+ const fingerprint = sha256(content);
262
+ let json;
263
+ try {
264
+ json = JSON.parse(content);
265
+ } catch {
266
+ throw validationError("INVALID_JSON", "Input is not valid JSON", { source });
267
+ }
268
+ const parsed = detectAndParse(json, source);
269
+ return { parsed, source, fingerprint };
270
+ }
271
+ function detectAndParse(json, source) {
272
+ if (isObject(json) && json.type === "excalidraw/clipboard" && "elements" in json) {
273
+ const sceneData = { ...json, type: "excalidraw" };
274
+ const result = ExcalidrawSceneSchema.safeParse(sceneData);
275
+ if (result.success) return result.data;
276
+ throw validationError(
277
+ "INVALID_CLIPBOARD",
278
+ `Clipboard data validation failed: ${result.error.message}`,
279
+ { source }
280
+ );
281
+ }
282
+ if (isObject(json) && "elements" in json && Array.isArray(json.elements)) {
283
+ const result = ExcalidrawSceneSchema.safeParse(json);
284
+ if (result.success) return result.data;
285
+ throw validationError("INVALID_SCENE", `Scene validation failed: ${result.error.message}`, {
286
+ source,
287
+ issues: result.error.issues
288
+ });
289
+ }
290
+ if (Array.isArray(json)) {
291
+ const wrapped = { type: "excalidraw", version: 2, elements: json };
292
+ const result = ExcalidrawSceneSchema.safeParse(wrapped);
293
+ if (result.success) return result.data;
294
+ throw validationError(
295
+ "INVALID_ELEMENTS",
296
+ `Elements array validation failed: ${result.error.message}`,
297
+ { source, issues: result.error.issues }
298
+ );
299
+ }
300
+ throw validationError("UNKNOWN_FORMAT", "Input is not a recognized Excalidraw format", {
301
+ source
302
+ });
303
+ }
304
+ function isObject(v) {
305
+ return typeof v === "object" && v !== null && !Array.isArray(v);
306
+ }
307
+
308
+ // src/scene/inspect-scene.ts
309
+ function inspectScene(scene) {
310
+ const countsByType = {};
311
+ const deletedCountsByType = {};
312
+ const frameElements = [];
313
+ const images = [];
314
+ const frameChildCounts = /* @__PURE__ */ new Map();
315
+ let deletedCount = 0;
316
+ let textCount = 0;
317
+ let boundTextCount = 0;
318
+ let minX = Infinity;
319
+ let minY = Infinity;
320
+ let maxX = -Infinity;
321
+ let maxY = -Infinity;
322
+ let liveCount = 0;
323
+ for (const el of scene.elements) {
324
+ if (el.isDeleted) {
325
+ deletedCount++;
326
+ deletedCountsByType[el.type] = (deletedCountsByType[el.type] ?? 0) + 1;
327
+ continue;
328
+ }
329
+ liveCount++;
330
+ countsByType[el.type] = (countsByType[el.type] ?? 0) + 1;
331
+ minX = Math.min(minX, el.x);
332
+ minY = Math.min(minY, el.y);
333
+ maxX = Math.max(maxX, el.x + el.width);
334
+ maxY = Math.max(maxY, el.y + el.height);
335
+ if (el.frameId) {
336
+ frameChildCounts.set(el.frameId, (frameChildCounts.get(el.frameId) ?? 0) + 1);
337
+ }
338
+ if (el.type === "frame") {
339
+ frameElements.push(el);
340
+ }
341
+ if (el.type === "image") {
342
+ images.push({ id: el.id, fileId: el.fileId, width: el.width, height: el.height });
343
+ }
344
+ if (el.type === "text") {
345
+ textCount++;
346
+ if (el.containerId) boundTextCount++;
347
+ }
348
+ }
349
+ const frames = frameElements.map((f) => ({
350
+ id: f.id,
351
+ name: f.name,
352
+ child_count: frameChildCounts.get(f.id) ?? 0
353
+ }));
354
+ const bounding_box = liveCount === 0 ? null : { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };
355
+ const binaryFiles = [];
356
+ if (scene.files && typeof scene.files === "object") {
357
+ for (const [id, file] of Object.entries(scene.files)) {
358
+ if (isFileEntry(file)) {
359
+ const size = typeof file.dataURL === "string" ? Math.round(file.dataURL.length * 3 / 4) : 0;
360
+ binaryFiles.push({ id, mimeType: file.mimeType ?? "unknown", size });
361
+ }
362
+ }
363
+ }
364
+ return {
365
+ element_count: liveCount,
366
+ deleted_count: deletedCount,
367
+ counts_by_type: countsByType,
368
+ deleted_counts_by_type: deletedCountsByType,
369
+ frames,
370
+ images,
371
+ text_stats: {
372
+ count: textCount,
373
+ bound_count: boundTextCount,
374
+ unbound_count: textCount - boundTextCount
375
+ },
376
+ bounding_box,
377
+ binary_files: binaryFiles
378
+ };
379
+ }
380
+ function isFileEntry(v) {
381
+ return typeof v === "object" && v !== null;
382
+ }
383
+
384
+ // src/scene/filters.ts
385
+ function applyFilters(scene, opts) {
386
+ let elements = scene.elements;
387
+ const warnings = [];
388
+ if (!opts.includeDeleted) {
389
+ elements = elements.filter((e) => !e.isDeleted);
390
+ }
391
+ if (opts.frameId) {
392
+ const hasById = elements.some((e) => e.id === opts.frameId || e.frameId === opts.frameId);
393
+ if (hasById) {
394
+ elements = filterByFrame(elements, opts.frameId);
395
+ } else {
396
+ const frame = elements.find(
397
+ (e) => e.type === "frame" && e.name === opts.frameId
398
+ );
399
+ if (frame) {
400
+ elements = filterByFrame(elements, frame.id);
401
+ } else {
402
+ warnings.push({
403
+ code: "ERR_VALIDATION_FRAME_NOT_FOUND",
404
+ message: `Frame not found: "${opts.frameId}" does not match any frame ID or name`,
405
+ retryable: false,
406
+ suggested_action: "fix_input"
407
+ });
408
+ }
409
+ }
410
+ } else if (opts.frameName) {
411
+ const frame = elements.find(
412
+ (e) => e.type === "frame" && e.name === opts.frameName
413
+ );
414
+ if (frame) {
415
+ elements = filterByFrame(elements, frame.id);
416
+ } else {
417
+ warnings.push({
418
+ code: "ERR_VALIDATION_FRAME_NOT_FOUND",
419
+ message: `Frame not found: no frame with name "${opts.frameName}"`,
420
+ retryable: false,
421
+ suggested_action: "fix_input"
422
+ });
423
+ }
424
+ }
425
+ if (opts.elementIds && opts.elementIds.length > 0) {
426
+ const idSet = new Set(opts.elementIds);
427
+ const before = elements.length;
428
+ elements = elements.filter((e) => idSet.has(e.id));
429
+ const missing = [...idSet].filter((id) => !elements.some((e) => e.id === id));
430
+ if (missing.length > 0) {
431
+ warnings.push({
432
+ code: "ERR_VALIDATION_ELEMENT_NOT_FOUND",
433
+ message: `Element IDs not found: ${missing.join(", ")}`,
434
+ retryable: false,
435
+ suggested_action: "fix_input",
436
+ details: { missing_ids: missing }
437
+ });
438
+ }
439
+ }
440
+ return { scene: { ...scene, elements }, warnings };
441
+ }
442
+ function filterByFrame(elements, frameId) {
443
+ return elements.filter((e) => e.id === frameId || e.frameId === frameId);
444
+ }
445
+
446
+ // src/cli/commands/inspect.ts
447
+ function registerInspect(program2) {
448
+ program2.command("inspect <file>").description("Inspect an Excalidraw scene and return metadata").option("--include-deleted", "Include deleted elements in inspection").option("--frame <id-or-name>", "Filter to a specific frame").action(
449
+ wrapCommand("scene.inspect", async (ctx, file, opts) => {
450
+ const options = opts;
451
+ const fileStr = file;
452
+ const loaded = await loadScene(fileStr);
453
+ const { scene: filtered, warnings: filterWarnings } = applyFilters(loaded.parsed, {
454
+ includeDeleted: options.includeDeleted,
455
+ frameId: options.frame
456
+ });
457
+ for (const w of filterWarnings) ctx.warn(w);
458
+ const inspection = inspectScene(filtered);
459
+ return {
460
+ target: { file: loaded.source, fingerprint: loaded.fingerprint },
461
+ result: inspection
462
+ };
463
+ })
464
+ );
465
+ }
466
+
467
+ // src/scene/validate-scene.ts
468
+ function validateScene(scene, opts = {}) {
469
+ const checks = [];
470
+ const warnings = [];
471
+ const elementMap = new Map(scene.elements.map((e) => [e.id, e]));
472
+ const liveElements = scene.elements.filter((e) => !e.isDeleted);
473
+ {
474
+ const orphans = [];
475
+ for (const el of liveElements) {
476
+ if (el.frameId && !elementMap.has(el.frameId)) {
477
+ orphans.push(el.id);
478
+ }
479
+ }
480
+ checks.push({
481
+ name: "frame_references",
482
+ passed: orphans.length === 0,
483
+ message: orphans.length > 0 ? `${orphans.length} element(s) reference non-existent frames` : void 0
484
+ });
485
+ if (orphans.length > 0) {
486
+ warnings.push({
487
+ code: "ERR_VALIDATION_FRAME_ORPHAN",
488
+ message: `Elements reference non-existent frames: ${orphans.join(", ")}`,
489
+ retryable: false,
490
+ suggested_action: "fix_input",
491
+ details: { element_ids: orphans }
492
+ });
493
+ }
494
+ }
495
+ {
496
+ const broken = [];
497
+ for (const el of liveElements) {
498
+ if (el.type === "text" && el.containerId) {
499
+ const container = elementMap.get(el.containerId);
500
+ if (!container) {
501
+ broken.push(el.id);
502
+ } else if (container.boundElements) {
503
+ const hasRef = container.boundElements.some(
504
+ (b) => b.id === el.id && b.type === "text"
505
+ );
506
+ if (!hasRef) broken.push(el.id);
507
+ }
508
+ }
509
+ }
510
+ checks.push({
511
+ name: "bound_text",
512
+ passed: broken.length === 0,
513
+ message: broken.length > 0 ? `${broken.length} bound text element(s) have broken references` : void 0
514
+ });
515
+ if (broken.length > 0) {
516
+ warnings.push({
517
+ code: "ERR_VALIDATION_BOUND_TEXT",
518
+ message: `Bound text elements with broken references: ${broken.join(", ")}`,
519
+ retryable: false,
520
+ suggested_action: "fix_input",
521
+ details: { element_ids: broken }
522
+ });
523
+ }
524
+ }
525
+ {
526
+ const broken = [];
527
+ for (const el of liveElements) {
528
+ if (el.type === "arrow") {
529
+ if (el.startBinding && !elementMap.has(el.startBinding.elementId)) {
530
+ broken.push(`${el.id}:start`);
531
+ }
532
+ if (el.endBinding && !elementMap.has(el.endBinding.elementId)) {
533
+ broken.push(`${el.id}:end`);
534
+ }
535
+ }
536
+ }
537
+ checks.push({
538
+ name: "arrow_bindings",
539
+ passed: broken.length === 0,
540
+ message: broken.length > 0 ? `${broken.length} arrow binding(s) reference non-existent elements` : void 0
541
+ });
542
+ if (broken.length > 0) {
543
+ warnings.push({
544
+ code: "ERR_VALIDATION_ARROW_BINDING",
545
+ message: `Broken arrow bindings: ${broken.join(", ")}`,
546
+ retryable: false,
547
+ suggested_action: "fix_input",
548
+ details: { bindings: broken }
549
+ });
550
+ }
551
+ }
552
+ if (opts.checkAssets) {
553
+ const missing = [];
554
+ for (const el of liveElements) {
555
+ if (el.type === "image" && el.fileId) {
556
+ if (!scene.files || !(el.fileId in scene.files)) {
557
+ missing.push(el.id);
558
+ }
559
+ }
560
+ }
561
+ checks.push({
562
+ name: "image_assets",
563
+ passed: missing.length === 0,
564
+ message: missing.length > 0 ? `${missing.length} image(s) reference missing binary files` : void 0
565
+ });
566
+ if (missing.length > 0) {
567
+ warnings.push({
568
+ code: "ERR_VALIDATION_MISSING_ASSET",
569
+ message: `Images with missing file data: ${missing.join(", ")}`,
570
+ retryable: false,
571
+ suggested_action: "fix_input",
572
+ details: { element_ids: missing }
573
+ });
574
+ }
575
+ }
576
+ {
577
+ const knownTypes = /* @__PURE__ */ new Set([
578
+ "rectangle",
579
+ "diamond",
580
+ "ellipse",
581
+ "arrow",
582
+ "line",
583
+ "freedraw",
584
+ "text",
585
+ "image",
586
+ "frame",
587
+ "group",
588
+ "embeddable",
589
+ "iframe",
590
+ "magicframe"
591
+ ]);
592
+ const unknownTypes = /* @__PURE__ */ new Set();
593
+ for (const el of liveElements) {
594
+ if (!knownTypes.has(el.type)) unknownTypes.add(el.type);
595
+ }
596
+ if (unknownTypes.size > 0) {
597
+ warnings.push({
598
+ code: "ERR_VALIDATION_UNKNOWN_TYPE",
599
+ message: `Unknown element types: ${[...unknownTypes].join(", ")}`,
600
+ retryable: false,
601
+ suggested_action: "fix_input",
602
+ details: { types: [...unknownTypes] }
603
+ });
604
+ }
605
+ }
606
+ const valid = checks.every((c) => c.passed);
607
+ return { result: { valid, checks }, warnings };
608
+ }
609
+
610
+ // src/cli/commands/validate.ts
611
+ function registerValidate(program2) {
612
+ program2.command("validate <file>").description("Validate an Excalidraw scene for structural consistency").option("--check-assets", "Verify image file references exist").action(
613
+ wrapCommand("scene.validate", async (ctx, file, opts) => {
614
+ const options = opts;
615
+ const fileStr = file;
616
+ const loaded = await loadScene(fileStr);
617
+ const { result, warnings } = validateScene(loaded.parsed, {
618
+ checkAssets: options.checkAssets
619
+ });
620
+ for (const w of warnings) ctx.warn(w);
621
+ return {
622
+ target: { file: loaded.source, fingerprint: loaded.fingerprint },
623
+ result
624
+ };
625
+ })
626
+ );
627
+ }
628
+
629
+ // src/cli/commands/render.ts
630
+ import { basename, extname } from "path";
631
+
632
+ // src/scene/normalize-scene.ts
633
+ function deterministicSeed(id) {
634
+ let hash = 0;
635
+ for (let i = 0; i < id.length; i++) {
636
+ hash = (hash << 5) - hash + id.charCodeAt(i) | 0;
637
+ }
638
+ return Math.abs(hash);
639
+ }
640
+ function normalizeScene(scene) {
641
+ const elements = scene.elements.map(normalizeElement);
642
+ elements.sort((a, b) => {
643
+ if (a.type !== b.type) return a.type.localeCompare(b.type);
644
+ if (a.y !== b.y) return a.y - b.y;
645
+ if (a.x !== b.x) return a.x - b.x;
646
+ return a.id.localeCompare(b.id);
647
+ });
648
+ return { ...scene, elements };
649
+ }
650
+ function normalizeElement(el) {
651
+ const raw = el;
652
+ const result = {
653
+ ...el,
654
+ isDeleted: el.isDeleted ?? false,
655
+ opacity: el.opacity ?? 100,
656
+ groupIds: el.groupIds ?? [],
657
+ frameId: el.frameId ?? null,
658
+ boundElements: el.boundElements ?? null,
659
+ containerId: el.containerId ?? null,
660
+ startBinding: el.startBinding ?? null,
661
+ endBinding: el.endBinding ?? null,
662
+ fileId: el.fileId ?? null,
663
+ name: el.name ?? null,
664
+ // Properties required by @excalidraw/utils for rendering
665
+ angle: raw.angle ?? 0,
666
+ strokeColor: raw.strokeColor ?? "#1e1e1e",
667
+ backgroundColor: raw.backgroundColor ?? "transparent",
668
+ fillStyle: raw.fillStyle ?? "solid",
669
+ strokeWidth: raw.strokeWidth ?? 2,
670
+ strokeStyle: raw.strokeStyle ?? "solid",
671
+ roughness: raw.roughness ?? 1,
672
+ roundness: raw.roundness ?? null,
673
+ seed: raw.seed ?? deterministicSeed(el.id),
674
+ version: raw.version ?? 1,
675
+ versionNonce: raw.versionNonce ?? 1,
676
+ updated: raw.updated ?? Date.now(),
677
+ link: raw.link ?? null,
678
+ locked: raw.locked ?? false
679
+ };
680
+ if ((el.type === "arrow" || el.type === "line" || el.type === "freedraw") && !raw.points) {
681
+ result.points = [[0, 0], [el.width, el.height]];
682
+ }
683
+ if (el.type === "text") {
684
+ result.fontSize = raw.fontSize ?? 20;
685
+ result.fontFamily = raw.fontFamily ?? 1;
686
+ result.textAlign = raw.textAlign ?? "left";
687
+ result.verticalAlign = raw.verticalAlign ?? "top";
688
+ result.lineHeight = raw.lineHeight ?? 1.25;
689
+ result.originalText = raw.originalText ?? raw.text ?? "";
690
+ result.baseline = raw.baseline ?? 0;
691
+ }
692
+ return result;
693
+ }
694
+
695
+ // src/render/bridge-page.ts
696
+ import { readFile as readFile2 } from "fs/promises";
697
+ import { resolve, dirname as dirname2 } from "path";
698
+ import { fileURLToPath } from "url";
699
+ async function generateBridgeHtml() {
700
+ const thisDir = dirname2(fileURLToPath(import.meta.url));
701
+ const bridgePath = resolve(thisDir, "..", "render", "bridge.global.js");
702
+ let bridgeScript;
703
+ try {
704
+ bridgeScript = await readFile2(bridgePath, "utf-8");
705
+ } catch {
706
+ const fallbackPath = resolve(process.cwd(), "dist", "render", "bridge.global.js");
707
+ bridgeScript = await readFile2(fallbackPath, "utf-8");
708
+ }
709
+ return `<!DOCTYPE html>
710
+ <html>
711
+ <head><meta charset="utf-8"></head>
712
+ <body>
713
+ <script>${bridgeScript}</script>
714
+ <script>
715
+ window.__bridgeReady = typeof window.__excalidrawExport !== 'undefined' && window.__excalidrawExport.ready;
716
+ </script>
717
+ </body>
718
+ </html>`;
719
+ }
720
+
721
+ // src/render/render-bridge.ts
722
+ var RenderBridge = class {
723
+ browser = null;
724
+ page = null;
725
+ bridgeHtml = null;
726
+ async initialize() {
727
+ let pw;
728
+ try {
729
+ pw = await import("playwright");
730
+ } catch {
731
+ throw renderError(
732
+ "BROWSER_UNAVAILABLE",
733
+ "Playwright is not installed. Install it with: npm install playwright"
734
+ );
735
+ }
736
+ this.browser = await pw.chromium.launch({ headless: true });
737
+ const context = await this.browser.newContext();
738
+ this.page = await context.newPage();
739
+ try {
740
+ await import("@excalidraw/excalidraw");
741
+ const { resolve: resolve2, dirname: dirname3 } = await import("path");
742
+ const { readFile: readFile3 } = await import("fs/promises");
743
+ const { createRequire } = await import("module");
744
+ const require2 = createRequire(import.meta.url);
745
+ const excalidrawDir = dirname3(require2.resolve("@excalidraw/excalidraw/package.json"));
746
+ await this.page.route("**/*.woff2", async (route) => {
747
+ try {
748
+ const url = new URL(route.request().url());
749
+ const fontName = url.pathname.split("/").pop();
750
+ if (fontName) {
751
+ const fontPath = resolve2(excalidrawDir, "dist", "excalidraw-assets", fontName);
752
+ const body = await readFile3(fontPath);
753
+ await route.fulfill({ body, contentType: "font/woff2" });
754
+ return;
755
+ }
756
+ } catch {
757
+ }
758
+ await route.abort();
759
+ });
760
+ } catch {
761
+ }
762
+ this.bridgeHtml = await generateBridgeHtml();
763
+ await this.page.setContent(this.bridgeHtml);
764
+ await this.page.waitForFunction("window.__bridgeReady === true", {
765
+ timeout: 1e4
766
+ });
767
+ }
768
+ async exportSvg(sceneData) {
769
+ if (!this.page) throw renderError("EXPORT_FAILED", "Bridge not initialized");
770
+ try {
771
+ const svgString = await this.page.evaluate(
772
+ async (data) => {
773
+ return window.__excalidrawExport.exportToSvg(data);
774
+ },
775
+ sceneData
776
+ );
777
+ return svgString;
778
+ } catch (err) {
779
+ throw renderError("EXPORT_FAILED", `SVG export failed: ${errorMessage(err)}`);
780
+ }
781
+ }
782
+ async exportPng(sceneData, scale = 2) {
783
+ if (!this.page) throw renderError("EXPORT_FAILED", "Bridge not initialized");
784
+ try {
785
+ const base64 = await this.page.evaluate(
786
+ async (data) => {
787
+ const appState = { ...data.scene.appState, exportScale: data.scale };
788
+ return window.__excalidrawExport.exportToBlob({
789
+ ...data.scene,
790
+ appState
791
+ });
792
+ },
793
+ { scene: sceneData, scale }
794
+ );
795
+ return Buffer.from(base64, "base64");
796
+ } catch (err) {
797
+ throw renderError("EXPORT_FAILED", `PNG export failed: ${errorMessage(err)}`);
798
+ }
799
+ }
800
+ async exportPdf(sceneData) {
801
+ if (!this.page) throw renderError("EXPORT_FAILED", "Bridge not initialized");
802
+ try {
803
+ const svgString = await this.exportSvg(sceneData);
804
+ await this.page.setContent(`<!DOCTYPE html>
805
+ <html>
806
+ <head>
807
+ <meta charset="utf-8">
808
+ <style>
809
+ body { margin: 0; display: flex; justify-content: center; align-items: center; }
810
+ svg { max-width: 100%; height: auto; }
811
+ </style>
812
+ </head>
813
+ <body>${svgString}</body>
814
+ </html>`);
815
+ const pdf = await this.page.pdf({
816
+ preferCSSPageSize: true,
817
+ printBackground: true
818
+ });
819
+ if (this.bridgeHtml) {
820
+ await this.page.setContent(this.bridgeHtml);
821
+ await this.page.waitForFunction("window.__bridgeReady === true", {
822
+ timeout: 1e4
823
+ });
824
+ }
825
+ return Buffer.from(pdf);
826
+ } catch (err) {
827
+ if (err instanceof Error && err.message.includes("ERR_RENDER")) throw err;
828
+ throw renderError("EXPORT_FAILED", `PDF export failed: ${errorMessage(err)}`);
829
+ }
830
+ }
831
+ async dispose() {
832
+ if (this.browser) {
833
+ await this.browser.close();
834
+ this.browser = null;
835
+ this.page = null;
836
+ this.bridgeHtml = null;
837
+ }
838
+ }
839
+ };
840
+
841
+ // src/render/export-common.ts
842
+ import { join as join2 } from "path";
843
+ async function exportFormat(opts) {
844
+ const sceneData = {
845
+ elements: opts.scene.elements,
846
+ appState: opts.scene.appState,
847
+ files: opts.scene.files,
848
+ exportPadding: opts.exportPadding
849
+ };
850
+ const { data, bytes } = await opts.render(opts.bridge, sceneData);
851
+ const path = join2(opts.outDir, `${opts.baseName}.${opts.ext}`).replace(/\\/g, "/");
852
+ if (!opts.dryRun) {
853
+ await writeOutput(path, data);
854
+ }
855
+ return { type: opts.ext, path, bytes };
856
+ }
857
+
858
+ // src/render/export-svg.ts
859
+ async function exportSvg(opts) {
860
+ return exportFormat({
861
+ ...opts,
862
+ ext: "svg",
863
+ render: async (bridge, sceneData) => {
864
+ const svg = await bridge.exportSvg(sceneData);
865
+ return { data: svg, bytes: Buffer.byteLength(svg, "utf-8") };
866
+ }
867
+ });
868
+ }
869
+
870
+ // src/render/export-png.ts
871
+ async function exportPng(opts) {
872
+ return exportFormat({
873
+ ...opts,
874
+ ext: "png",
875
+ render: async (bridge, sceneData) => {
876
+ const buf = await bridge.exportPng(sceneData, opts.scale);
877
+ return { data: buf, bytes: buf.length };
878
+ }
879
+ });
880
+ }
881
+
882
+ // src/render/export-pdf.ts
883
+ async function exportPdf(opts) {
884
+ return exportFormat({
885
+ ...opts,
886
+ ext: "pdf",
887
+ render: async (bridge, sceneData) => {
888
+ const buf = await bridge.exportPdf(sceneData);
889
+ return { data: buf, bytes: buf.length };
890
+ }
891
+ });
892
+ }
893
+
894
+ // src/cli/commands/render.ts
895
+ function registerRender(program2) {
896
+ program2.command("render <file>").description("Render an Excalidraw scene to SVG, PNG, or PDF").option("--outDir <dir>", "Output directory", ".").option("--svg", "Export SVG").option("--png", "Export PNG (requires Playwright)").option("--pdf", "Export PDF (requires Playwright)").option("--dark-mode", "Use dark theme").option("--no-background", "Transparent background").option("--scale <n>", "Scale factor for PNG output only", "2").option("--padding <n>", "Padding in pixels", "20").option("--frame <id-or-name>", "Export specific frame only").option("--element <id>", "Export specific element only").option("--dry-run", "Run pipeline but write no files").action(
897
+ wrapCommand("scene.render", async (ctx, file, opts) => {
898
+ const options = opts;
899
+ const fileStr = file;
900
+ const loaded = await loadScene(fileStr);
901
+ const normalized = normalizeScene(loaded.parsed);
902
+ const { scene: filtered, warnings: filterWarnings } = applyFilters(normalized, {
903
+ frameId: options.frame,
904
+ frameName: options.frame,
905
+ elementIds: options.element ? [options.element] : void 0
906
+ });
907
+ for (const w of filterWarnings) ctx.warn(w);
908
+ const inspection = inspectScene(filtered);
909
+ const formats = {
910
+ svg: options.svg || !options.png && !options.pdf,
911
+ png: options.png || false,
912
+ pdf: options.pdf || false
913
+ };
914
+ const scale = parseFloat(options.scale);
915
+ if (scale !== 2 && formats.svg && !formats.png && !formats.pdf) {
916
+ ctx.warn({
917
+ code: "ERR_VALIDATION_SCALE_IGNORED",
918
+ message: "--scale has no effect on SVG output; it only applies to PNG",
919
+ retryable: false,
920
+ suggested_action: "fix_input"
921
+ });
922
+ }
923
+ const appState = { ...filtered.appState };
924
+ if (options.darkMode) appState.exportWithDarkMode = true;
925
+ if (!options.background) appState.exportBackground = false;
926
+ const sceneForExport = { ...filtered, appState };
927
+ const baseName = loaded.source === "stdin" ? "scene" : basename(loaded.source, extname(loaded.source));
928
+ const shared = {
929
+ bridge: void 0,
930
+ scene: sceneForExport,
931
+ outDir: options.outDir,
932
+ baseName,
933
+ dryRun: options.dryRun,
934
+ exportPadding: parseInt(options.padding, 10)
935
+ };
936
+ const bridge = new RenderBridge();
937
+ shared.bridge = bridge;
938
+ try {
939
+ await bridge.initialize();
940
+ const artefacts = [];
941
+ if (formats.svg) {
942
+ artefacts.push(await exportSvg(shared));
943
+ }
944
+ if (formats.png) {
945
+ artefacts.push(
946
+ await exportPng({ ...shared, scale })
947
+ );
948
+ }
949
+ if (formats.pdf) {
950
+ artefacts.push(await exportPdf(shared));
951
+ }
952
+ return {
953
+ target: { file: loaded.source, fingerprint: loaded.fingerprint },
954
+ result: {
955
+ artefacts,
956
+ scene_summary: {
957
+ element_count: inspection.element_count,
958
+ bounding_box: inspection.bounding_box,
959
+ fingerprint: loaded.fingerprint
960
+ },
961
+ dry_run: options.dryRun || false
962
+ }
963
+ };
964
+ } finally {
965
+ await bridge.dispose();
966
+ }
967
+ })
968
+ );
969
+ }
970
+
971
+ // src/guide/guide-schema.ts
972
+ function getGuideContent() {
973
+ return `# excal \u2014 CLI Guide
974
+
975
+ Agent-first CLI for Excalidraw scene inspection, validation, and rendering.
976
+
977
+ ## Commands
978
+
979
+ ### excal inspect <file|->
980
+
981
+ Inspect an Excalidraw scene and return element counts, bounds, and metadata.
982
+
983
+ | Flag | Description |
984
+ |------|-------------|
985
+ | \`--include-deleted\` | Include deleted elements |
986
+ | \`--frame <id\\|name>\` | Filter to a specific frame |
987
+
988
+ \`\`\`bash
989
+ excal inspect diagram.excalidraw
990
+ cat scene.json | excal inspect -
991
+ \`\`\`
992
+
993
+ ### excal validate <file|->
994
+
995
+ Validate scene structure: frame refs, bound text, arrow bindings, assets.
996
+
997
+ | Flag | Description |
998
+ |------|-------------|
999
+ | \`--check-assets\` | Verify image file references exist in scene |
1000
+
1001
+ \`\`\`bash
1002
+ excal validate diagram.excalidraw
1003
+ excal validate diagram.excalidraw --check-assets
1004
+ \`\`\`
1005
+
1006
+ ### excal render <file|->
1007
+
1008
+ Render scene to SVG, PNG, or PDF. PNG/PDF require Playwright.
1009
+
1010
+ | Flag | Description |
1011
+ |------|-------------|
1012
+ | \`--outDir <dir>\` | Output directory (default: .) |
1013
+ | \`--svg\` | Export SVG (default if no format specified) |
1014
+ | \`--png\` | Export PNG (requires Playwright) |
1015
+ | \`--pdf\` | Export PDF (requires Playwright) |
1016
+ | \`--dark-mode\` | Use dark theme |
1017
+ | \`--no-background\` | Transparent background |
1018
+ | \`--scale <n>\` | Scale factor for PNG output only (default: 2) |
1019
+ | \`--padding <n>\` | Padding in pixels (default: 20) |
1020
+ | \`--frame <id\\|name>\` | Export specific frame only |
1021
+ | \`--element <id>\` | Export specific element only |
1022
+ | \`--dry-run\` | Run pipeline but write no files |
1023
+
1024
+ \`\`\`bash
1025
+ excal render diagram.excalidraw --outDir ./out
1026
+ excal render diagram.excalidraw --outDir ./out --png --pdf
1027
+ excal render - --outDir ./out < scene.json
1028
+ \`\`\`
1029
+
1030
+ ### excal guide
1031
+
1032
+ Output this CLI guide as Markdown.
1033
+
1034
+ ### excal skill
1035
+
1036
+ Return Excalidraw domain knowledge for AI agents.
1037
+
1038
+ ## Error Codes
1039
+
1040
+ | Code | Exit | Description |
1041
+ |------|------|-------------|
1042
+ | \`ERR_VALIDATION_INVALID_JSON\` | 10 | Input is not valid JSON |
1043
+ | \`ERR_VALIDATION_INVALID_SCENE\` | 10 | Scene structure validation failed |
1044
+ | \`ERR_VALIDATION_UNKNOWN_FORMAT\` | 10 | Input is not a recognized format |
1045
+ | \`ERR_RENDER_BROWSER_UNAVAILABLE\` | 20 | Playwright not installed |
1046
+ | \`ERR_RENDER_EXPORT_FAILED\` | 20 | Export failed in browser bridge |
1047
+ | \`ERR_IO_READ_FAILED\` | 50 | Failed to read input file |
1048
+ | \`ERR_IO_WRITE_FAILED\` | 50 | Failed to write output file |
1049
+ | \`ERR_INTERNAL_UNEXPECTED\` | 90 | Unexpected internal error |
1050
+
1051
+ ## Response Envelope
1052
+
1053
+ Every command returns a JSON envelope on stdout:
1054
+
1055
+ \`\`\`jsonc
1056
+ {
1057
+ "schema_version": "1.0",
1058
+ "request_id": "req_20260302_143000_7f3a",
1059
+ "ok": true, // always present
1060
+ "command": "scene.inspect",
1061
+ "target": { ... }, // what was acted on (null for global commands)
1062
+ "result": { ... }, // command payload (null on failure)
1063
+ "warnings": [], // always an array
1064
+ "errors": [], // always an array
1065
+ "metrics": { "duration_ms": 42 }
1066
+ }
1067
+ \`\`\`
1068
+
1069
+ - \`errors\` and \`warnings\` are always arrays (possibly empty), never omitted.
1070
+ - \`result\` is always present; on failure it is \`null\`.
1071
+ - Each error carries \`code\`, \`message\`, \`retryable\`, and \`suggested_action\`.
1072
+ - **Note on \`validate\`**: \`ok: true\` means the command executed successfully, not that the scene is valid. Check \`result.valid\` (boolean) for scene validity, and \`result.issues\` / \`warnings\` for details.
1073
+
1074
+ ## Concurrency
1075
+
1076
+ - **Reads** (inspect, validate): safe to run concurrently.
1077
+ - **Renders**: each invocation launches its own browser; safe to parallelize.
1078
+ `;
1079
+ }
1080
+
1081
+ // src/cli/commands/guide.ts
1082
+ function registerGuide(program2) {
1083
+ program2.command("guide").description("Return CLI guide as Markdown for agent bootstrapping").action(
1084
+ wrapCommand("cli.guide", async () => {
1085
+ return { result: { content: getGuideContent(), format: "markdown" } };
1086
+ })
1087
+ );
1088
+ }
1089
+
1090
+ // src/guide/skill-content.ts
1091
+ function getSkillContent() {
1092
+ return `# Excalidraw Scene Structure \u2014 Agent Guide
1093
+
1094
+ ## File Format
1095
+
1096
+ Excalidraw scenes are JSON files (typically \`.excalidraw\`) with this structure:
1097
+
1098
+ \`\`\`json
1099
+ {
1100
+ "type": "excalidraw",
1101
+ "version": 2,
1102
+ "source": "https://excalidraw.com",
1103
+ "elements": [ ... ],
1104
+ "appState": { ... },
1105
+ "files": { ... }
1106
+ }
1107
+ \`\`\`
1108
+
1109
+ ## Element Types
1110
+
1111
+ | Type | Description |
1112
+ |------|-------------|
1113
+ | rectangle | Box shape |
1114
+ | diamond | Diamond/rhombus shape |
1115
+ | ellipse | Circle/ellipse shape |
1116
+ | arrow | Arrow connector |
1117
+ | line | Line/polyline |
1118
+ | freedraw | Freehand drawing |
1119
+ | text | Text label |
1120
+ | image | Embedded image |
1121
+ | frame | Grouping frame |
1122
+
1123
+ ## Key Element Properties
1124
+
1125
+ Every element has: \`id\`, \`type\`, \`x\`, \`y\`, \`width\`, \`height\`, \`isDeleted\`, \`opacity\`, \`groupIds\`, \`frameId\`.
1126
+
1127
+ ## Frames
1128
+
1129
+ Frames group elements visually. Elements inside a frame have \`frameId\` set to the frame's \`id\`. Frames themselves have \`type: "frame"\` and an optional \`name\`.
1130
+
1131
+ To export a single frame: use \`--frame <id|name>\`.
1132
+
1133
+ ## Bound Text
1134
+
1135
+ Text can be bound to a container (rectangle, diamond, ellipse). The text element has \`containerId\` pointing to the container, and the container has a \`boundElements\` entry with \`{ id, type: "text" }\`.
1136
+
1137
+ ## Arrows & Bindings
1138
+
1139
+ Arrows connect elements via \`startBinding\` and \`endBinding\`:
1140
+ \`\`\`json
1141
+ {
1142
+ "startBinding": { "elementId": "target-id", "focus": 0, "gap": 1 },
1143
+ "endBinding": { "elementId": "target-id", "focus": 0, "gap": 1 }
1144
+ }
1145
+ \`\`\`
1146
+
1147
+ ## Images & Binary Files
1148
+
1149
+ Image elements have \`fileId\` referencing an entry in \`files\`. The files object maps IDs to \`{ mimeType, dataURL }\` where dataURL is base64-encoded.
1150
+
1151
+ ## Export Tips
1152
+
1153
+ - SVG export inlines fonts; no external dependencies
1154
+ - PNG/PDF require Playwright for headless browser rendering
1155
+ - Use \`--scale 2\` (default) for crisp PNG exports
1156
+ - Use \`--dark-mode\` for dark theme exports
1157
+ - Use \`--no-background\` for transparent backgrounds
1158
+ - \`--dry-run\` validates the full pipeline without writing files
1159
+
1160
+ ## Common Patterns
1161
+
1162
+ 1. **Inspect before modifying**: Always run \`excal inspect\` to understand scene structure
1163
+ 2. **Validate after changes**: Run \`excal validate --check-assets\` to catch broken references
1164
+ 3. **Frame-based export**: Use frames to organize sections, export individually with \`--frame\`
1165
+ 4. **Deterministic output**: Same input + same options = same output (for CI/CD)
1166
+ `;
1167
+ }
1168
+
1169
+ // src/cli/commands/skill.ts
1170
+ function registerSkill(program2) {
1171
+ program2.command("skill").description("Return Excalidraw domain knowledge for AI agents").action(
1172
+ wrapCommand("cli.skill", async () => {
1173
+ return { result: { content: getSkillContent(), format: "markdown" } };
1174
+ })
1175
+ );
1176
+ }
1177
+
1178
+ // src/cli/index.ts
1179
+ var program = new Command();
1180
+ program.name("excal").description("Agent-first CLI for Excalidraw scene inspection, validation, and rendering").version("1.0.0");
1181
+ program.action(() => {
1182
+ const envelope = buildEnvelope({
1183
+ request_id: generateRequestId(),
1184
+ command: "",
1185
+ result: null,
1186
+ ok: false,
1187
+ errors: [
1188
+ {
1189
+ code: "ERR_VALIDATION_NO_COMMAND",
1190
+ message: "No command specified. Run `excal --help` for usage.",
1191
+ retryable: false,
1192
+ suggested_action: "fix_input"
1193
+ }
1194
+ ],
1195
+ duration_ms: 0
1196
+ });
1197
+ process.exitCode = 10;
1198
+ process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
1199
+ });
1200
+ program.exitOverride();
1201
+ program.configureOutput({
1202
+ writeOut: () => {
1203
+ },
1204
+ writeErr: () => {
1205
+ }
1206
+ });
1207
+ registerInspect(program);
1208
+ registerValidate(program);
1209
+ registerRender(program);
1210
+ registerGuide(program);
1211
+ registerSkill(program);
1212
+ try {
1213
+ program.parse();
1214
+ } catch (err) {
1215
+ if (err instanceof CommanderError) {
1216
+ if (err.code === "commander.helpDisplayed") {
1217
+ process.stdout.write(program.helpInformation());
1218
+ process.exit(0);
1219
+ }
1220
+ if (err.code === "commander.version") {
1221
+ process.stdout.write(program.version() + "\n");
1222
+ process.exit(0);
1223
+ }
1224
+ const envelope = buildEnvelope({
1225
+ request_id: generateRequestId(),
1226
+ command: "",
1227
+ result: null,
1228
+ ok: false,
1229
+ errors: [
1230
+ {
1231
+ code: "ERR_VALIDATION_INVALID_ARGS",
1232
+ message: err.message,
1233
+ retryable: false,
1234
+ suggested_action: "fix_input"
1235
+ }
1236
+ ],
1237
+ duration_ms: 0
1238
+ });
1239
+ process.exitCode = 10;
1240
+ process.stdout.write(JSON.stringify(envelope, null, 2) + "\n");
1241
+ } else {
1242
+ throw err;
1243
+ }
1244
+ }
1245
+ //# sourceMappingURL=index.js.map