@bushscad/cli 0.0.0-stage → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,623 @@
1
+ // scripts/parallel-compilation/p7-benchmark.ts
2
+ import { readFile as readFile2 } from "node:fs/promises";
3
+ import { basename, dirname as dirname2, resolve } from "node:path";
4
+
5
+ // scripts/parallel-compilation/report-output.ts
6
+ import { mkdir, writeFile } from "node:fs/promises";
7
+ import { dirname } from "node:path";
8
+ async function writeJsonReport(outPath, report, stdoutReport = report) {
9
+ await mkdir(dirname(outPath), { recursive: true });
10
+ await writeFile(outPath, `${JSON.stringify(report, null, 2)}
11
+ `, "utf8");
12
+ process.stdout.write(`${JSON.stringify(stdoutReport, null, 2)}
13
+ `);
14
+ }
15
+
16
+ // scripts/parallel-compilation/p7-service-client.ts
17
+ import { spawn } from "node:child_process";
18
+ import { createInterface } from "node:readline";
19
+ var P7ServiceClient = class _P7ServiceClient {
20
+ constructor(child, startupWallMs, readyStatus, messages) {
21
+ this.child = child;
22
+ this.startupWallMs = startupWallMs;
23
+ this.readyStatus = readyStatus;
24
+ this.messages = messages;
25
+ createInterface({ input: child.stderr }).on(
26
+ "line",
27
+ (line) => this.stderr.push(line)
28
+ );
29
+ }
30
+ stderr = [];
31
+ nextRequestId = 1;
32
+ /** Starts the real compiled CLI and waits until every OCCT owner is ready. */
33
+ static async start(cliPath, workerCount) {
34
+ const startedAt = performance.now();
35
+ const child = spawn(process.execPath, [cliPath, "--service"], {
36
+ env: {
37
+ ...process.env,
38
+ BUSHSCAD_EXPERIMENTAL_PARALLEL: "1",
39
+ BUSHSCAD_OCCT_WORKERS: String(workerCount)
40
+ },
41
+ windowsHide: true
42
+ });
43
+ const messages = [];
44
+ createInterface({ input: child.stdout }).on("line", (line) => {
45
+ try {
46
+ messages.push(JSON.parse(line));
47
+ } catch {
48
+ messages.push({ kind: "invalid-json", line });
49
+ }
50
+ });
51
+ const status = await waitForReady(messages, child);
52
+ const client = new _P7ServiceClient(
53
+ child,
54
+ performance.now() - startedAt,
55
+ status,
56
+ messages
57
+ );
58
+ return client;
59
+ }
60
+ /** Sends one source request and captures coordinator wall time separately. */
61
+ async request(request) {
62
+ const id = this.nextRequestId++;
63
+ const startedAt = performance.now();
64
+ this.child.stdin.write(`${JSON.stringify({ id, ...request })}
65
+ `);
66
+ const response = await waitForResponse(
67
+ this.messages,
68
+ this.child,
69
+ id,
70
+ request.kind
71
+ );
72
+ return { response, wallMs: performance.now() - startedAt };
73
+ }
74
+ /** Returns the newest pool status emitted after task memory was sampled. */
75
+ latestStatus() {
76
+ const status = [...this.messages].reverse().find(isPoolStatusMessage);
77
+ if (!status) return this.readyStatus;
78
+ return status.status;
79
+ }
80
+ /** Gracefully shuts down the service and all child OCCT owners. */
81
+ async stop() {
82
+ if (this.child.exitCode !== null) return;
83
+ this.child.kill("SIGTERM");
84
+ await new Promise((resolve2) => {
85
+ this.child.once("exit", () => resolve2());
86
+ setTimeout(resolve2, 1e4);
87
+ });
88
+ }
89
+ };
90
+ async function waitForReady(messages, child) {
91
+ return waitForValue(
92
+ messages,
93
+ child,
94
+ (message) => isPoolStatusMessage(message) && message.status.state === "ready" ? message.status : void 0
95
+ );
96
+ }
97
+ async function waitForResponse(messages, child, id, kind) {
98
+ return waitForValue(
99
+ messages,
100
+ child,
101
+ (message) => responseForRequest(message, id, kind)
102
+ );
103
+ }
104
+ function responseForRequest(value, id, kind) {
105
+ if (!isServiceResult(value)) return void 0;
106
+ return matchesRequest(value, id, kind) ? value : void 0;
107
+ }
108
+ function isServiceResult(value) {
109
+ return isRecord(value) && typeof value.ok === "boolean";
110
+ }
111
+ function matchesRequest(value, id, kind) {
112
+ return value.id === id && value.kind === kind;
113
+ }
114
+ async function waitForValue(messages, child, select) {
115
+ const startedAt = Date.now();
116
+ while (Date.now() - startedAt < 24e4) {
117
+ const value = selectNewest(messages, select);
118
+ if (value !== void 0) return value;
119
+ if (child.exitCode !== null)
120
+ throw new Error(`Compiler service exited with ${child.exitCode}.`);
121
+ await new Promise((resolve2) => setTimeout(resolve2, 10));
122
+ }
123
+ throw new Error("Compiler service response timed out.");
124
+ }
125
+ function selectNewest(messages, select) {
126
+ for (let index = messages.length - 1; index >= 0; index--) {
127
+ const value = select(messages[index]);
128
+ if (value !== void 0) return value;
129
+ }
130
+ return void 0;
131
+ }
132
+ function isPoolStatusMessage(value) {
133
+ return isRecord(value) && value.kind === "pool-status" && isRecord(value.status);
134
+ }
135
+ function isRecord(value) {
136
+ return typeof value === "object" && value !== null;
137
+ }
138
+
139
+ // scripts/parallel-compilation/p7-response-contract.ts
140
+ import { createHash } from "node:crypto";
141
+ function responseContract(response) {
142
+ const value = response.ok ? successContract(response) : failureContract(response);
143
+ return {
144
+ hash: createHash("sha256").update(JSON.stringify(value)).digest("hex"),
145
+ value
146
+ };
147
+ }
148
+ function successContract(response) {
149
+ return {
150
+ ok: true,
151
+ program: response.program,
152
+ meshes: array(response.meshes).map(meshContract),
153
+ interaction: interactionContract(response.interaction),
154
+ drawings: response.drawings,
155
+ validations: response.validations,
156
+ joints: response.joints,
157
+ env: response.env,
158
+ dependents: response.dependents,
159
+ buffers: array(response.buffers).map(bufferContract),
160
+ traceErrors: array(response.occtTraceEvents).filter(isErrorTrace).map(traceContract)
161
+ };
162
+ }
163
+ function failureContract(response) {
164
+ return {
165
+ ok: false,
166
+ errors: response.errors,
167
+ env: response.env,
168
+ traceErrors: array(response.occtTraceEvents).filter(isErrorTrace).map(traceContract)
169
+ };
170
+ }
171
+ function meshContract(value) {
172
+ if (!isRecord2(value)) return value;
173
+ const vertices = numberArray(value.vertices);
174
+ const indices = numberArray(value.indices);
175
+ const geometry = meshGeometry(vertices);
176
+ return {
177
+ partName: value.partName,
178
+ bodyName: value.bodyName,
179
+ color: value.color,
180
+ material: value.material,
181
+ vertexCount: vertices.length / 3,
182
+ triangleCount: indices.length / 3,
183
+ ...geometry
184
+ };
185
+ }
186
+ function meshGeometry(vertices) {
187
+ const bounds = [
188
+ [Infinity, Infinity, Infinity],
189
+ [-Infinity, -Infinity, -Infinity]
190
+ ];
191
+ const sums = [0, 0, 0];
192
+ for (let index = 0; index < vertices.length; index += 3) {
193
+ for (let axis = 0; axis < 3; axis++) {
194
+ const coordinate = vertices[index + axis] ?? 0;
195
+ bounds[0][axis] = Math.min(bounds[0][axis], coordinate);
196
+ bounds[1][axis] = Math.max(bounds[1][axis], coordinate);
197
+ sums[axis] += coordinate;
198
+ }
199
+ }
200
+ return { bounds, sums };
201
+ }
202
+ function interactionContract(value) {
203
+ if (!isRecord2(value)) return value;
204
+ return {
205
+ ...value,
206
+ topology: array(value.topology).map((entry) => {
207
+ if (!isRecord2(entry)) return entry;
208
+ return {
209
+ meshIndex: entry.meshIndex,
210
+ partEntityId: entry.partEntityId,
211
+ objectEntityId: entry.objectEntityId,
212
+ faces: topologyContract(entry.faces, "triangleCount"),
213
+ edges: topologyContract(entry.edges, "points"),
214
+ corners: topologyContract(entry.corners)
215
+ };
216
+ })
217
+ };
218
+ }
219
+ function topologyContract(value, aggregate) {
220
+ const entries = array(value).filter(isRecord2);
221
+ const total = entries.reduce(
222
+ (sum, entry) => sum + (aggregate === "triangleCount" ? number(entry.triangleCount) : aggregate === "points" ? array(entry.points).length : 0),
223
+ 0
224
+ );
225
+ return {
226
+ count: entries.length,
227
+ ...aggregate === "triangleCount" ? { triangles: total } : {},
228
+ ...aggregate === "points" ? { points: total } : {},
229
+ owners: unique(entries.map((entry) => String(entry.ownerEntityId))),
230
+ targets: unique(
231
+ entries.flatMap((entry) => array(entry.semanticTargetIds).map(String))
232
+ )
233
+ };
234
+ }
235
+ function traceContract(value) {
236
+ if (!isRecord2(value)) return value;
237
+ const {
238
+ timestampMs: _timestamp,
239
+ elapsedMs: _elapsed,
240
+ cpuMs: _cpu,
241
+ ...stable
242
+ } = value;
243
+ return stable;
244
+ }
245
+ function isErrorTrace(value) {
246
+ return isRecord2(value) && (value.kind === "op-error" || value.kind === "build-error");
247
+ }
248
+ function bufferContract(value) {
249
+ if (!isRecord2(value)) return value;
250
+ const data = String(value.dataBase64 ?? "");
251
+ return {
252
+ name: value.name,
253
+ byteLength: Buffer.from(data, "base64").byteLength,
254
+ sha256: createHash("sha256").update(data).digest("hex")
255
+ };
256
+ }
257
+ function unique(values) {
258
+ return [...new Set(values)].sort();
259
+ }
260
+ function array(value) {
261
+ return Array.isArray(value) ? value : [];
262
+ }
263
+ function numberArray(value) {
264
+ return Array.isArray(value) ? value.filter((entry) => typeof entry === "number") : [];
265
+ }
266
+ function number(value) {
267
+ return typeof value === "number" ? value : 0;
268
+ }
269
+ function isRecord2(value) {
270
+ return typeof value === "object" && value !== null;
271
+ }
272
+
273
+ // scripts/parallel-compilation/p7-benchmark-summary.ts
274
+ function summarizeSamples(samples) {
275
+ const groups = /* @__PURE__ */ new Map();
276
+ for (const sample of samples) {
277
+ const key = [
278
+ sample.workers,
279
+ sample.workload,
280
+ sample.engine,
281
+ sample.phase
282
+ ].join("|");
283
+ groups.set(key, [...groups.get(key) ?? [], sample]);
284
+ }
285
+ return [...groups.values()].map((group) => {
286
+ const first = group[0];
287
+ return {
288
+ workers: first.workers,
289
+ workload: first.workload,
290
+ engine: first.engine,
291
+ phase: first.phase,
292
+ samples: group.length,
293
+ wallMs: statistics(group.map((sample) => sample.wallMs)),
294
+ compilerElapsedMs: statistics(
295
+ group.map((sample) => sample.compilerElapsedMs)
296
+ ),
297
+ workerCpuMs: statistics(metricValues(group, "workerCpuMs")),
298
+ workerBusyMs: statistics(metricValues(group, "workerBusyMs")),
299
+ queueWaitMs: statistics(metricValues(group, "queueWaitMs")),
300
+ transportMs: statistics(metricValues(group, "transportMs")),
301
+ workerUtilization: statistics(metricValues(group, "workerUtilization")),
302
+ peakWorkerRssBytes: Math.max(
303
+ 0,
304
+ ...metricValues(group, "peakWorkerRssBytes")
305
+ ),
306
+ schedulerTaskCount: statistics(metricValues(group, "schedulerTaskCount")),
307
+ longestWorkerTaskMs: statistics(
308
+ metricValues(group, "longestWorkerTaskMs")
309
+ ),
310
+ nativeBooleanBatchCount: statistics(
311
+ metricValues(group, "nativeBooleanBatchCount")
312
+ ),
313
+ operationTimings: operationTimings(group)
314
+ };
315
+ });
316
+ }
317
+ function statistics(values) {
318
+ if (values.length === 0) return void 0;
319
+ const sorted = [...values].sort((left, right) => left - right);
320
+ return {
321
+ median: percentile(sorted, 0.5),
322
+ p95: percentile(sorted, 0.95),
323
+ min: sorted[0],
324
+ max: sorted.at(-1)
325
+ };
326
+ }
327
+ function percentile(sorted, fraction) {
328
+ return sorted[Math.max(0, Math.ceil(sorted.length * fraction) - 1)] ?? 0;
329
+ }
330
+ function metricValues(samples, name) {
331
+ return samples.flatMap((sample) => {
332
+ const value = sample.metrics?.[name];
333
+ return typeof value === "number" ? [value] : [];
334
+ });
335
+ }
336
+ function operationTimings(samples) {
337
+ const timings = /* @__PURE__ */ new Map();
338
+ for (const event of samples.flatMap((sample) => sample.trace))
339
+ recordOperationTiming(timings, event);
340
+ return [...timings.entries()].map(([operation, values]) => ({
341
+ operation,
342
+ elapsedMs: statistics(values.elapsedMs),
343
+ cpuMs: statistics(values.cpuMs)
344
+ }));
345
+ }
346
+ function recordOperationTiming(timings, event) {
347
+ if (!isTimedOperation(event.kind)) return;
348
+ const name = operationName(event);
349
+ const timing = timings.get(name) ?? { elapsedMs: [], cpuMs: [] };
350
+ appendNumber(timing.elapsedMs, event.elapsedMs);
351
+ appendNumber(timing.cpuMs, event.cpuMs);
352
+ timings.set(name, timing);
353
+ }
354
+ function isTimedOperation(kind) {
355
+ return kind === "op-success" || kind === "native-boolean-batch";
356
+ }
357
+ function operationName(event) {
358
+ return String(event.opKind ?? event.kind);
359
+ }
360
+ function appendNumber(values, value) {
361
+ if (typeof value === "number") values.push(value);
362
+ }
363
+
364
+ // scripts/parallel-compilation/p7-performance-runner.ts
365
+ import { readFile } from "node:fs/promises";
366
+ async function runPerformance(client, rootPath, workers, workloads, runs, samples, progress2) {
367
+ for (const workload of workloads)
368
+ await runWorkload(
369
+ client,
370
+ rootPath,
371
+ workers,
372
+ workload,
373
+ runs,
374
+ samples,
375
+ progress2
376
+ );
377
+ }
378
+ async function runWorkload(client, rootPath, workers, workload, runs, samples, progress2) {
379
+ const source = await readFile(workload.filePath, "utf8");
380
+ for (let round = 0; round < runs; round++)
381
+ await runRound(
382
+ client,
383
+ rootPath,
384
+ workers,
385
+ workload,
386
+ source,
387
+ round,
388
+ samples,
389
+ progress2
390
+ );
391
+ }
392
+ async function runRound(client, rootPath, workers, workload, source, round, samples, progress2) {
393
+ for (const engine of engineOrder(round))
394
+ await sampleEngine(
395
+ client,
396
+ rootPath,
397
+ workers,
398
+ workload,
399
+ source,
400
+ round,
401
+ engine,
402
+ samples,
403
+ progress2
404
+ );
405
+ }
406
+ async function sampleEngine(client, rootPath, workers, workload, source, round, engine, samples, progress2) {
407
+ progress2(`preview w${workers} ${workload.id} ${engine} r${round + 1}`);
408
+ samples.push(await takeSample(client, rootPath, workload, engine, "preview"));
409
+ samples.push(
410
+ await takeSample(client, rootPath, workload, engine, "incremental", {
411
+ [workload.filePath]: `${source}
412
+ // P7 incremental ${round}
413
+ `
414
+ })
415
+ );
416
+ if (!workload.export) return;
417
+ progress2(`export w${workers} ${workload.id} ${engine} r${round + 1}`);
418
+ samples.push(await takeSample(client, rootPath, workload, engine, "export"));
419
+ }
420
+ async function takeSample(client, rootPath, workload, engine, phase, overlays) {
421
+ const exporting = phase === "export";
422
+ const { response, wallMs } = await client.request({
423
+ kind: exporting ? "export" : "preview",
424
+ filePath: workload.filePath,
425
+ rootPath,
426
+ engine,
427
+ occtTrace: !exporting,
428
+ ...overlays ? { overlays } : {},
429
+ ...exporting ? { format: "step" } : {}
430
+ });
431
+ requireSuccess(response, workload, engine, phase);
432
+ return {
433
+ workers: client.readyStatus.desiredWorkers,
434
+ workload: workload.id,
435
+ engine,
436
+ phase,
437
+ wallMs,
438
+ compilerElapsedMs: response.elapsedMs,
439
+ metrics: response.compilerMetrics,
440
+ trace: records(response.occtTraceEvents)
441
+ };
442
+ }
443
+ function engineOrder(round) {
444
+ return round % 2 === 0 ? ["classic", "parallel"] : ["parallel", "classic"];
445
+ }
446
+ function requireSuccess(response, workload, engine, phase) {
447
+ if (response.ok) return;
448
+ throw new Error(
449
+ `${workload.id} ${engine} ${phase} failed: ${JSON.stringify(response)}`
450
+ );
451
+ }
452
+ function records(value) {
453
+ if (!Array.isArray(value)) return [];
454
+ return value.filter(
455
+ (entry) => typeof entry === "object" && entry !== null
456
+ );
457
+ }
458
+
459
+ // scripts/parallel-compilation/p7-benchmark.ts
460
+ async function run() {
461
+ const rootPath = resolve(readArg("--root", "."));
462
+ const cliPath = resolve(readArg("--cli", "packages/cli/dist/index.js"));
463
+ const outPath = resolve(
464
+ readArg(
465
+ "--out",
466
+ ".bushscad-analysis/parallel-compilation/p7-benchmark.json"
467
+ )
468
+ );
469
+ const runs = positiveInteger(readArg("--runs", "3"));
470
+ const workers = readArg("--workers", "1,2,4").split(",").map(positiveInteger);
471
+ const scope = readArg("--scope", "full");
472
+ const correctnessCases = await loadSuiteCases(rootPath);
473
+ const performanceCases = performanceWorkloads(rootPath);
474
+ const equivalence = [];
475
+ const samples = [];
476
+ const lifecycle = [];
477
+ for (const workerCount of workers)
478
+ await runWorkerMatrix({
479
+ cliPath,
480
+ rootPath,
481
+ workerCount,
482
+ scope,
483
+ runs,
484
+ correctnessCases,
485
+ performanceCases,
486
+ equivalence,
487
+ samples,
488
+ lifecycle
489
+ });
490
+ const mismatches = equivalence.filter((result) => !result.match);
491
+ const report = {
492
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
493
+ node: process.version,
494
+ rootPath,
495
+ cliPath,
496
+ configuration: { runs, workers, scope },
497
+ lifecycle,
498
+ equivalence: {
499
+ comparisons: equivalence.length,
500
+ matches: equivalence.length - mismatches.length,
501
+ mismatches,
502
+ results: equivalence
503
+ },
504
+ summaries: summarizeSamples(samples),
505
+ samples
506
+ };
507
+ await writeJsonReport(outPath, report, {
508
+ outPath,
509
+ lifecycle,
510
+ equivalence: report.equivalence,
511
+ summaries: report.summaries
512
+ });
513
+ if (mismatches.length > 0) process.exitCode = 2;
514
+ }
515
+ async function runWorkerMatrix(input) {
516
+ progress(`starting ${input.workerCount}-worker pool`);
517
+ const client = await P7ServiceClient.start(input.cliPath, input.workerCount);
518
+ try {
519
+ input.lifecycle.push({
520
+ workers: input.workerCount,
521
+ startupWallMs: client.startupWallMs,
522
+ ready: client.readyStatus
523
+ });
524
+ if (input.scope !== "performance")
525
+ await runEquivalence(
526
+ client,
527
+ input.rootPath,
528
+ input.workerCount,
529
+ input.correctnessCases,
530
+ input.equivalence
531
+ );
532
+ if (input.scope !== "correctness")
533
+ await runPerformance(
534
+ client,
535
+ input.rootPath,
536
+ input.workerCount,
537
+ input.performanceCases,
538
+ input.runs,
539
+ input.samples,
540
+ progress
541
+ );
542
+ input.lifecycle.push({
543
+ workers: input.workerCount,
544
+ afterMatrix: client.latestStatus()
545
+ });
546
+ } finally {
547
+ await client.stop();
548
+ }
549
+ }
550
+ async function runEquivalence(client, rootPath, workers, workloads, results) {
551
+ for (const workload of workloads) {
552
+ progress(`equivalence w${workers} ${workload.id}`);
553
+ const classic = await client.request({
554
+ kind: "preview",
555
+ filePath: workload.filePath,
556
+ rootPath,
557
+ engine: "classic",
558
+ occtTrace: true
559
+ });
560
+ const parallel = await client.request({
561
+ kind: "preview",
562
+ filePath: workload.filePath,
563
+ rootPath,
564
+ engine: "parallel",
565
+ occtTrace: true
566
+ });
567
+ const classicContract = responseContract(classic.response);
568
+ const parallelContract = responseContract(parallel.response);
569
+ results.push({
570
+ workers,
571
+ workload: workload.id,
572
+ classicHash: classicContract.hash,
573
+ parallelHash: parallelContract.hash,
574
+ match: classicContract.hash === parallelContract.hash
575
+ });
576
+ }
577
+ }
578
+ async function loadSuiteCases(rootPath) {
579
+ const suitePath = resolve(rootPath, "examples/test_suite/suite.json");
580
+ const suite = JSON.parse(await readFile2(suitePath, "utf8"));
581
+ return suite.cases.map((entry) => ({
582
+ id: entry.id,
583
+ filePath: resolve(dirname2(suitePath), entry.file),
584
+ export: false
585
+ }));
586
+ }
587
+ function performanceWorkloads(rootPath) {
588
+ const paths = [
589
+ "examples/test_suite/13_module_library.bushscad",
590
+ "examples/test_suite/09_assembly_patterns.bushscad",
591
+ "examples/test_suite/04_solids_and_details.bushscad",
592
+ "examples/test_suite/12_imported_assets.bushscad",
593
+ "packages/cli/test/fixtures/parallel-components-p4-loft-benchmark.bushscad",
594
+ "packages/cli/test/fixtures/parallel-components-p5-reuse.bushscad",
595
+ "packages/cli/test/fixtures/parallel-p6-native-cut-benchmark.bushscad"
596
+ ];
597
+ return paths.map((path) => ({
598
+ id: basename(path, ".bushscad"),
599
+ filePath: resolve(rootPath, path),
600
+ export: path.includes("09_assembly") || path.includes("p6-native-cut")
601
+ }));
602
+ }
603
+ function positiveInteger(value) {
604
+ const parsed = Number(value);
605
+ if (!Number.isInteger(parsed) || parsed < 1)
606
+ throw new Error(`Expected a positive integer, received '${value}'.`);
607
+ return parsed;
608
+ }
609
+ function readArg(name, fallback) {
610
+ const index = process.argv.indexOf(name);
611
+ return index >= 0 ? process.argv[index + 1] ?? fallback : fallback;
612
+ }
613
+ function progress(message) {
614
+ process.stderr.write(`[P7] ${message}
615
+ `);
616
+ }
617
+ void run().catch((error) => {
618
+ process.stderr.write(
619
+ `${error instanceof Error ? error.stack : String(error)}
620
+ `
621
+ );
622
+ process.exitCode = 1;
623
+ });
package/package.json CHANGED
@@ -1,6 +1,50 @@
1
- {
2
- "name": "@bushscad/cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
1
+ {
2
+ "name": "@bushscad/cli",
3
+ "version": "0.3.1",
4
+ "type": "module",
5
+ "bin": {
6
+ "bushscad": "./dist/index.js"
7
+ },
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./protocol": "./src/dto/index.ts"
11
+ },
12
+ "scripts": {
13
+ "build": "node scripts/build.mjs",
14
+ "typecheck": "tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
15
+ "test": "vitest run",
16
+ "prepack": "npm run build"
17
+ },
18
+ "dependencies": {
19
+ "@bushscad/compiler": "^0.3.0",
20
+ "@bushscad/ir": "^0.3.0",
21
+ "opencascade.js": ">=2.0.0-0"
22
+ },
23
+ "devDependencies": {
24
+ "esbuild": "^0.28.0",
25
+ "typescript": "^5.4.0",
26
+ "vitest": "^4.1.5"
27
+ },
28
+ "license": "Apache-2.0",
29
+ "description": "The command-line compiler and service interface for BushSCAD. The published package includes the bundled runtime and fonts; OpenCascade is installed as a package dependency.",
30
+ "homepage": "https://bushscad.byaidan.com/",
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "git+https://github.com/AidanBoissonneault/BushSCAD-Language.git",
34
+ "directory": "packages/cli"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/AidanBoissonneault/BushSCAD-Language/issues"
38
+ },
39
+ "publishConfig": {
40
+ "access": "public",
41
+ "registry": "https://registry.npmjs.org/"
42
+ },
43
+ "files": [
44
+ "dist/**",
45
+ "src/dto/**",
46
+ "src/service/parallel/worker-protocol.ts",
47
+ "LICENSE",
48
+ "README.md"
49
+ ]
50
+ }
@@ -0,0 +1,22 @@
1
+ /**
2
+ * @author Aidan Boissonneault
3
+ * @file bufferDto.ts
4
+ * @purpose Provides buffer dto behavior for the dto in cli package.
5
+ * @last-edited October 6, 2026
6
+ */
7
+
8
+ import type { BufferDto } from './types.js';
9
+
10
+ /**
11
+ * Converts binary export buffers into base64 DTO format.
12
+ * Buffers must be serialized safely for IPC/service transport.
13
+ */
14
+ export function bufferToDto(buffer: {
15
+ name: string;
16
+ data: ArrayBuffer;
17
+ }): BufferDto {
18
+ return {
19
+ name: buffer.name,
20
+ dataBase64: Buffer.from(buffer.data).toString('base64'),
21
+ };
22
+ }