bambu-printer-mcp 1.0.5 → 1.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,562 @@
1
+ /**
2
+ * BambuStudio CLI profile flattener.
3
+ *
4
+ * Background: BambuStudio's bundled profile JSONs (Resources/profiles/BBL/...)
5
+ * use an `inherits` chain that the GUI resolves at runtime but the CLI does
6
+ * not. Passing a leaf profile straight to `--load-settings` / `--load-filaments`
7
+ * yields a partial config and the slicer asserts (e.g. `nozzle_volume_type
8
+ * not found` -> SIGSEGV / assertion in MutablePolygon.cpp / Geometry.hpp).
9
+ * See https://github.com/bambulab/BambuStudio/issues/9636 and #9968.
10
+ *
11
+ * This module:
12
+ * 1. Indexes every BBL profile JSON by its `name` field.
13
+ * 2. Recursively walks `inherits`, deep-merging parent into child.
14
+ * 3. Derives `nozzle_volume_type` from `default_nozzle_volume_type[0]`
15
+ * (the GUI does this implicitly; the CLI doesn't).
16
+ * 4. Merges CLI-specific machine_limits from `BBL/cli_config.json` so the
17
+ * printer doesn't run unsafe accelerations / jerks.
18
+ * 5. Writes the flattened JSON to a temp file the caller passes to
19
+ * BambuStudio CLI.
20
+ *
21
+ * BBL only. Other vendors are out of scope and rejected explicitly so we
22
+ * fail loud rather than producing dangerous gcode for hardware we don't own.
23
+ */
24
+
25
+ import fs from "node:fs/promises";
26
+ import path from "node:path";
27
+ import crypto from "node:crypto";
28
+
29
+ export type ProfileKind = "machine" | "process" | "filament";
30
+
31
+ export interface FlattenedProfiles {
32
+ machinePath: string;
33
+ processPath: string;
34
+ filamentPaths: string[];
35
+ /** Diagnostics for callers / logs. Not part of the slicer invocation. */
36
+ meta: {
37
+ profilesRoot: string;
38
+ machineLeafName: string;
39
+ processLeafName: string;
40
+ filamentLeafNames: string[];
41
+ cliOverlayApplied: boolean;
42
+ };
43
+ }
44
+
45
+ export interface FlattenOptions {
46
+ /** e.g. "Bambu Lab H2S 0.4 nozzle" */
47
+ machineLeaf: string;
48
+ /** e.g. "0.20mm Standard @BBL H2S" */
49
+ processLeaf: string;
50
+ /** e.g. ["Bambu PLA Basic @BBL H2S"] */
51
+ filamentLeaves: string[];
52
+ /** Absolute path to `.../Resources/profiles`. */
53
+ profilesRoot: string;
54
+ /** Where to write flattened temp files. */
55
+ tempDir: string;
56
+ /** Vendor subdir under profilesRoot. Currently only "BBL" supported. */
57
+ vendor?: string;
58
+ /**
59
+ * Override for installed nozzle flow type. The printer reports this on
60
+ * boot from its physical nozzle scan; both nozzles always match (you
61
+ * can't mix Standard + High Flow, just like you can't mix 0.2 + 0.4).
62
+ * If omitted we use `default_nozzle_volume_type` from the profile tree
63
+ * (= "Standard" on stock BBL machines). Pass "High Flow" when HF
64
+ * nozzles are installed.
65
+ */
66
+ nozzleVolumeType?: "Standard" | "High Flow";
67
+ /** BambuStudio display name, e.g. "Textured PEI Plate" or "Cool Plate". */
68
+ bedType?: string;
69
+ }
70
+
71
+ interface IndexedProfile {
72
+ filePath: string;
73
+ data: Record<string, unknown>;
74
+ }
75
+
76
+ type NameIndex = Map<string, IndexedProfile>;
77
+
78
+ /* -------------------------------------------------------------------------- */
79
+ /* Indexing */
80
+ /* -------------------------------------------------------------------------- */
81
+
82
+ /**
83
+ * Build a name -> {filePath, data} map for every JSON in
84
+ * <profilesRoot>/<vendor>/{machine,process,filament}/.
85
+ *
86
+ * The `name` field inside each JSON is the lookup key (this is what
87
+ * `inherits` references). We index every file, including non-instantiable
88
+ * abstract bases like `fdm_machine_common` and `fdm_bbl_3dp_001_common`,
89
+ * because those are the parents we'll walk to.
90
+ */
91
+ async function buildNameIndex(
92
+ profilesRoot: string,
93
+ vendor: string
94
+ ): Promise<NameIndex> {
95
+ const index: NameIndex = new Map();
96
+ const subdirs: ProfileKind[] = ["machine", "process", "filament"];
97
+
98
+ for (const sub of subdirs) {
99
+ const dir = path.join(profilesRoot, vendor, sub);
100
+ let entries: string[];
101
+ try {
102
+ entries = await fs.readdir(dir);
103
+ } catch {
104
+ // Vendor or subdir missing -- skip silently; flatten will fail later
105
+ // with a precise "name not found" error.
106
+ continue;
107
+ }
108
+ for (const entry of entries) {
109
+ if (!entry.endsWith(".json")) continue;
110
+ const filePath = path.join(dir, entry);
111
+ let raw: string;
112
+ try {
113
+ raw = await fs.readFile(filePath, "utf8");
114
+ } catch {
115
+ continue;
116
+ }
117
+ let data: Record<string, unknown>;
118
+ try {
119
+ data = JSON.parse(raw) as Record<string, unknown>;
120
+ } catch {
121
+ // Malformed profile -- skip, don't poison the index.
122
+ continue;
123
+ }
124
+ const name = data["name"];
125
+ if (typeof name !== "string" || name.length === 0) continue;
126
+ // First-write wins. BBL doesn't have name collisions in practice;
127
+ // log if it ever does so we notice.
128
+ if (!index.has(name)) {
129
+ index.set(name, { filePath, data });
130
+ }
131
+ }
132
+ }
133
+
134
+ return index;
135
+ }
136
+
137
+ /* -------------------------------------------------------------------------- */
138
+ /* Inheritance walk + merge */
139
+ /* -------------------------------------------------------------------------- */
140
+
141
+ /**
142
+ * Resolve the full inheritance chain for `leafName` and return a single
143
+ * deep-merged object. Child wins on key collision.
144
+ *
145
+ * Throws on:
146
+ * - Unknown name (broken `inherits` reference).
147
+ * - Cycles (A -> B -> A).
148
+ */
149
+ function flattenByName(leafName: string, index: NameIndex): Record<string, unknown> {
150
+ const chain: Record<string, unknown>[] = [];
151
+ const visited = new Set<string>();
152
+ let cursor: string | undefined = leafName;
153
+
154
+ while (cursor) {
155
+ if (visited.has(cursor)) {
156
+ throw new Error(
157
+ `Profile inheritance cycle detected at "${cursor}" (chain: ${[...visited].join(" -> ")})`
158
+ );
159
+ }
160
+ visited.add(cursor);
161
+
162
+ const entry = index.get(cursor);
163
+ if (!entry) {
164
+ throw new Error(
165
+ `Profile "${cursor}" not found in index. ` +
166
+ `Inherits chain so far: ${[...visited].join(" -> ")}. ` +
167
+ `This usually means the leaf name is misspelled or the profile tree is incomplete.`
168
+ );
169
+ }
170
+
171
+ chain.push(entry.data);
172
+ const parent = entry.data["inherits"];
173
+ cursor = typeof parent === "string" && parent.length > 0 ? parent : undefined;
174
+ }
175
+
176
+ // Merge root-most parent first, leaf last (so leaf wins).
177
+ const merged: Record<string, unknown> = {};
178
+ for (let i = chain.length - 1; i >= 0; i--) {
179
+ Object.assign(merged, chain[i]);
180
+ }
181
+ return merged;
182
+ }
183
+
184
+ /* -------------------------------------------------------------------------- */
185
+ /* CLI post-processing */
186
+ /* -------------------------------------------------------------------------- */
187
+
188
+ /**
189
+ * Bambu's CLI looks up `nozzle_volume_type`, but the profile tree only
190
+ * defines `default_nozzle_volume_type`. The GUI propagates the latter
191
+ * (or the value reported by the printer's nozzle scan on boot) into the
192
+ * former at runtime. We do the same.
193
+ *
194
+ * Shape: array of strings, one entry per physical nozzle. Hardware
195
+ * invariant from Bambu: BOTH nozzles must match (same diameter, same
196
+ * flow type) -- you cannot install 0.2 + 0.4, and you cannot install
197
+ * Standard + High Flow. So the array always contains identical entries.
198
+ *
199
+ * Verified against a GUI-sliced H2D project_settings.config:
200
+ * ["High Flow", "High Flow"] (dual-nozzle, HF installed)
201
+ * ["Standard", "Standard"] (dual-nozzle, stock)
202
+ * ["Standard"] (single-extruder default)
203
+ *
204
+ * Override priority:
205
+ * 1. Caller-supplied `nozzleVolumeType` (e.g. printer reported HF).
206
+ * 2. Existing `nozzle_volume_type` already in the flattened profile.
207
+ * 3. `default_nozzle_volume_type` from the profile tree.
208
+ * 4. ["Standard"] x extruder count.
209
+ */
210
+ function deriveNozzleVolumeType(
211
+ flat: Record<string, unknown>,
212
+ override?: "Standard" | "High Flow"
213
+ ): void {
214
+ if (override) {
215
+ const count = inferExtruderCount(flat);
216
+ flat["nozzle_volume_type"] = Array(count).fill(override);
217
+ return;
218
+ }
219
+
220
+ if (Array.isArray(flat["nozzle_volume_type"])) {
221
+ enforceMatchingNozzles(flat["nozzle_volume_type"] as unknown[]);
222
+ return;
223
+ }
224
+
225
+ const def = flat["default_nozzle_volume_type"];
226
+ if (Array.isArray(def) && def.every((v) => typeof v === "string")) {
227
+ enforceMatchingNozzles(def);
228
+ flat["nozzle_volume_type"] = [...def];
229
+ return;
230
+ }
231
+
232
+ const extruderCount = inferExtruderCount(flat);
233
+ flat["nozzle_volume_type"] = Array(extruderCount).fill("Standard");
234
+ }
235
+
236
+ /**
237
+ * Hardware invariant: all nozzles on a Bambu printer have identical flow
238
+ * type (and diameter). If a profile somehow declares mixed types it's
239
+ * either bad input or a future mistake -- fail loud so we don't ship
240
+ * gcode that could damage the printer.
241
+ */
242
+ function enforceMatchingNozzles(arr: unknown[]): void {
243
+ if (arr.length <= 1) return;
244
+ const first = arr[0];
245
+ for (let i = 1; i < arr.length; i++) {
246
+ if (arr[i] !== first) {
247
+ throw new Error(
248
+ `nozzle_volume_type entries must all match (Bambu hardware invariant). ` +
249
+ `Got: ${JSON.stringify(arr)}. ` +
250
+ `Both nozzles on H2-series printers always have identical flow type.`
251
+ );
252
+ }
253
+ }
254
+ }
255
+
256
+ /** Best-effort extruder count for fallback nozzle_volume_type sizing. */
257
+ function inferExtruderCount(flat: Record<string, unknown>): number {
258
+ for (const key of ["nozzle_diameter", "extruder_type", "extruder_variant_list"]) {
259
+ const v = flat[key];
260
+ if (Array.isArray(v) && v.length > 0) return v.length;
261
+ }
262
+ return 1;
263
+ }
264
+
265
+ /**
266
+ * Bambu ships `BBL/cli_config.json` with per-printer overlays containing
267
+ * machine_limits keys (cli_safe_acceleration_*, cli_safe_jerk_*,
268
+ * cli_safe_speed_*). Without these the slicer can emit movements faster
269
+ * than the printer's safe envelope -- dangerous on real hardware.
270
+ *
271
+ * The overlay keys are scoped under printer.<printer_name>.machine_limits.
272
+ * We look up by the leaf machine's `printer_model` or `name` and merge
273
+ * those keys into the flattened machine profile.
274
+ *
275
+ * Returns true if an overlay was found and applied, false otherwise.
276
+ */
277
+ async function applyCliOverlay(
278
+ flat: Record<string, unknown>,
279
+ profilesRoot: string,
280
+ vendor: string
281
+ ): Promise<boolean> {
282
+ const cliConfigPath = path.join(profilesRoot, vendor, "cli_config.json");
283
+ let raw: string;
284
+ try {
285
+ raw = await fs.readFile(cliConfigPath, "utf8");
286
+ } catch {
287
+ return false;
288
+ }
289
+ let cliConfig: Record<string, unknown>;
290
+ try {
291
+ cliConfig = JSON.parse(raw) as Record<string, unknown>;
292
+ } catch {
293
+ return false;
294
+ }
295
+
296
+ const printerSection = cliConfig["printer"];
297
+ if (!printerSection || typeof printerSection !== "object") return false;
298
+
299
+ // Match key: cli_config.json keys are bare printer names like
300
+ // "Bambu Lab H2D" / "Bambu Lab A1". Try, in order: explicit printer_model,
301
+ // printer_settings_id with the " 0.4 nozzle" suffix stripped, raw name
302
+ // with that suffix stripped, raw name as-is.
303
+ const candidates = [
304
+ flat["printer_model"],
305
+ typeof flat["printer_settings_id"] === "string"
306
+ ? stripNozzleSuffix(flat["printer_settings_id"])
307
+ : undefined,
308
+ typeof flat["name"] === "string" ? stripNozzleSuffix(flat["name"]) : undefined,
309
+ flat["name"],
310
+ ].filter((v): v is string => typeof v === "string" && v.length > 0);
311
+
312
+ for (const key of candidates) {
313
+ const block = (printerSection as Record<string, unknown>)[key];
314
+ if (!block || typeof block !== "object") continue;
315
+ const mlimits = (block as Record<string, unknown>)["machine_limits"];
316
+ if (!mlimits || typeof mlimits !== "object") continue;
317
+ // Merge machine_limits into the flat profile. These are CLI-only safety
318
+ // values; they should never be overridden by the leaf.
319
+ Object.assign(flat, mlimits as Record<string, unknown>);
320
+ return true;
321
+ }
322
+
323
+ return false;
324
+ }
325
+
326
+ /**
327
+ * Ensure the chosen machine is in the process/filament's
328
+ * `compatible_printers` list. This mirrors what the GUI does when you
329
+ * save a project with a printer/process combo that wasn't in the
330
+ * shipped compat list.
331
+ */
332
+ function ensureMachineInCompatList(
333
+ flat: Record<string, unknown>,
334
+ machineLeaf: string
335
+ ): void {
336
+ const list = flat["compatible_printers"];
337
+ if (!Array.isArray(list)) {
338
+ flat["compatible_printers"] = [machineLeaf];
339
+ return;
340
+ }
341
+ if (!list.includes(machineLeaf)) {
342
+ list.push(machineLeaf);
343
+ }
344
+ }
345
+
346
+ function stripNozzleSuffix(name: string): string {
347
+ return name.replace(/\s+\d+\.\d+\s+nozzle.*$/, "");
348
+ }
349
+
350
+ /**
351
+ * Adjust fields for CLI consumption.
352
+ *
353
+ * - Strip `inherits` and `instantiation` (chain marker, abstract-base flag).
354
+ * The GUI consumes them; the CLI treats anything still present as part of
355
+ * the merged config.
356
+ * - Rewrite `from`. Leaf BBL profiles ship with `"from": "system"`, which
357
+ * the CLI rejects when loaded via `--load-settings` (system presets
358
+ * aren't supposed to be loaded that way). The GUI rewrites this to
359
+ * `"from": "project"` when it embeds the merged config in a 3MF. We do
360
+ * the same so the CLI accepts our flattened temp file.
361
+ * - Strip `setting_id` / `is_custom_defined` (user-registry markers that
362
+ * make no sense on a transient flattened file).
363
+ */
364
+ function normalizeForCli(
365
+ flat: Record<string, unknown>,
366
+ kind: ProfileKind,
367
+ leafName: string
368
+ ): void {
369
+ // Set inherits to the leaf name BEFORE we delete it below, so the
370
+ // CLI's system_name resolution lands on the right value.
371
+ // (See note further down for why this matters.)
372
+ flat["inherits"] = leafName;
373
+ // Identity field. The CLI sets this internally, but we mirror it for
374
+ // consistency with what the GUI's project_settings.config emits.
375
+ switch (kind) {
376
+ case "machine":
377
+ flat["printer_settings_id"] = leafName;
378
+ break;
379
+ case "process":
380
+ flat["print_settings_id"] = leafName;
381
+ break;
382
+ case "filament":
383
+ flat["filament_settings_id"] = [leafName];
384
+ break;
385
+ }
386
+
387
+ // CRITICAL: when from=="User", the CLI computes `system_name = inherits`
388
+ // and the compatibility check compares the *other* configs'
389
+ // `compatible_printers` list against that system_name. The leaf
390
+ // profiles' compat lists contain the leaf machine name (e.g.
391
+ // "Bambu Lab H2S 0.4 nozzle"), so we must set `inherits` on the
392
+ // flattened machine to that same leaf name. For process/filament,
393
+ // `inherits` doesn't drive the compat check we hit, but we set it for
394
+ // symmetry so any future check succeeds the same way.
395
+ // Source: BambuStudio.cpp ~line 2222 (from!="system" -> system_name = inherits).
396
+ // NOTE: do NOT delete inherits -- we just set it above. The CLI uses
397
+ // its value (when from=="User") for the system_name compat check.
398
+ delete flat["instantiation"];
399
+ delete flat["setting_id"];
400
+ delete flat["is_custom_defined"];
401
+ // Always rewrite `from` to "User". "Project" is what the GUI uses when
402
+ // embedding in a 3MF, but the CLI accepts "User" for --load-settings
403
+ // paths and that's semantically what we are.
404
+ flat["from"] = "User";
405
+ // Mirror the GUI's behavior of clearing compatibility constraints in the
406
+ // merged project config. The leaf process/filament profiles ship with
407
+ // `compatible_printers: ["Bambu Lab H2S 0.4 nozzle"]` etc., and the CLI
408
+ // re-validates that list against our flattened machine config -- which
409
+ // has had `from` rewritten and inheritance collapsed, so the equality
410
+ // check fails. The GUI's project_settings.config has these set to null
411
+ // because at that point compatibility is already established by the
412
+ // user. We do the same.
413
+ // Leave compatible_printers / compatible_prints as the leaf declared
414
+ // them. The GUI nulls these in embedded project configs, but the CLI's
415
+ // --load-settings path requires a non-empty list and matches against
416
+ // the machine's `name` / `printer_settings_id`. The leaves ship with
417
+ // the correct list ([machine_leaf_name]) so just preserve.
418
+ // _condition fields can stay null (string-typed, tolerated as null).
419
+ }
420
+
421
+ function applyBedType(processFlat: Record<string, unknown>, bedType?: string): void {
422
+ if (!bedType) return;
423
+ processFlat.curr_bed_type = bedType;
424
+ }
425
+
426
+ function applyMachineModelBedMetadata(
427
+ machineFlat: Record<string, unknown>,
428
+ index: NameIndex
429
+ ): void {
430
+ const modelName = machineFlat.printer_model;
431
+ if (typeof modelName !== "string") return;
432
+
433
+ const model = index.get(modelName)?.data;
434
+ if (!model) return;
435
+
436
+ for (const key of ["default_bed_type", "image_bed_type", "not_support_bed_type"]) {
437
+ if (!(key in machineFlat) && key in model) {
438
+ machineFlat[key] = model[key];
439
+ }
440
+ }
441
+ }
442
+
443
+ /* -------------------------------------------------------------------------- */
444
+ /* Public entry point */
445
+ /* -------------------------------------------------------------------------- */
446
+
447
+ /**
448
+ * Flatten the leaf profiles, post-process for CLI, and write to temp files.
449
+ *
450
+ * Throws on unknown leaf names, missing profilesRoot, or cycles.
451
+ */
452
+ export async function flattenForCli(opts: FlattenOptions): Promise<FlattenedProfiles> {
453
+ const vendor = opts.vendor ?? "BBL";
454
+ if (vendor !== "BBL") {
455
+ throw new Error(
456
+ `profile-flatten: only BBL vendor is supported (got "${vendor}"). ` +
457
+ `Other vendors are out of scope -- contributions welcome but untested.`
458
+ );
459
+ }
460
+
461
+ // Confirm the profiles root looks plausible before doing real work.
462
+ const probe = path.join(opts.profilesRoot, vendor, "machine");
463
+ try {
464
+ await fs.access(probe);
465
+ } catch {
466
+ throw new Error(
467
+ `profile-flatten: profilesRoot "${opts.profilesRoot}" does not contain "${vendor}/machine". ` +
468
+ `Set BAMBU_PROFILES_ROOT or check your BambuStudio install.`
469
+ );
470
+ }
471
+
472
+ const index = await buildNameIndex(opts.profilesRoot, vendor);
473
+
474
+ // Flatten each leaf.
475
+ const machineFlat = flattenByName(opts.machineLeaf, index);
476
+ const processFlat = flattenByName(opts.processLeaf, index);
477
+ const filamentFlats = opts.filamentLeaves.map((n) => flattenByName(n, index));
478
+
479
+ // CLI-specific post-processing on machine profile only.
480
+ deriveNozzleVolumeType(machineFlat, opts.nozzleVolumeType);
481
+ applyMachineModelBedMetadata(machineFlat, index);
482
+ const cliOverlayApplied = await applyCliOverlay(machineFlat, opts.profilesRoot, vendor);
483
+
484
+ // Normalize each flattened profile for CLI consumption.
485
+ normalizeForCli(machineFlat, "machine", opts.machineLeaf);
486
+ normalizeForCli(processFlat, "process", opts.processLeaf);
487
+ filamentFlats.forEach((f, i) => normalizeForCli(f, "filament", opts.filamentLeaves[i]));
488
+ applyBedType(processFlat, opts.bedType);
489
+
490
+ // Mirror the GUI's auto-extend behavior: when the caller explicitly
491
+ // chose a process or filament that wasn't pre-declared compatible with
492
+ // the chosen machine (e.g. "0.20mm Standard @BBL P1P" used on a P1S),
493
+ // add the machine name to the process/filament compat list. The GUI
494
+ // does this implicitly when saving a project.
495
+ ensureMachineInCompatList(processFlat, opts.machineLeaf);
496
+ filamentFlats.forEach((f) => ensureMachineInCompatList(f, opts.machineLeaf));
497
+
498
+ // Write temp files. Hash the leaf name into the filename so concurrent
499
+ // slices for different printers don't collide.
500
+ await fs.mkdir(opts.tempDir, { recursive: true });
501
+ const machinePath = await writeTemp(opts.tempDir, "machine", opts.machineLeaf, machineFlat);
502
+ const processPath = await writeTemp(opts.tempDir, "process", opts.processLeaf, processFlat);
503
+ const filamentPaths: string[] = [];
504
+ for (let i = 0; i < filamentFlats.length; i++) {
505
+ filamentPaths.push(
506
+ await writeTemp(opts.tempDir, `filament-${i}`, opts.filamentLeaves[i], filamentFlats[i])
507
+ );
508
+ }
509
+
510
+ return {
511
+ machinePath,
512
+ processPath,
513
+ filamentPaths,
514
+ meta: {
515
+ profilesRoot: opts.profilesRoot,
516
+ machineLeafName: opts.machineLeaf,
517
+ processLeafName: opts.processLeaf,
518
+ filamentLeafNames: opts.filamentLeaves,
519
+ cliOverlayApplied,
520
+ },
521
+ };
522
+ }
523
+
524
+ async function writeTemp(
525
+ tempDir: string,
526
+ kind: string,
527
+ leafName: string,
528
+ data: Record<string, unknown>
529
+ ): Promise<string> {
530
+ const hash = crypto.createHash("sha1").update(leafName).digest("hex").slice(0, 8);
531
+ const safe = leafName.replace(/[^a-zA-Z0-9._-]+/g, "_").slice(0, 64);
532
+ const filename = `flat-${kind}-${safe}-${hash}.json`;
533
+ const out = path.join(tempDir, filename);
534
+ await fs.writeFile(out, JSON.stringify(data, null, 2), "utf8");
535
+ return out;
536
+ }
537
+
538
+ /* -------------------------------------------------------------------------- */
539
+ /* Profile root detection */
540
+ /* -------------------------------------------------------------------------- */
541
+
542
+ /**
543
+ * Given the SLICER_PATH (path to BambuStudio executable), walk up to the
544
+ * Resources/profiles directory. Falls back to common platform paths.
545
+ *
546
+ * Override via BAMBU_PROFILES_ROOT env.
547
+ */
548
+ export function detectProfilesRoot(slicerPath?: string): string {
549
+ if (process.env["BAMBU_PROFILES_ROOT"]) {
550
+ return process.env["BAMBU_PROFILES_ROOT"];
551
+ }
552
+
553
+ if (slicerPath) {
554
+ // macOS: /Applications/BambuStudio.app/Contents/MacOS/BambuStudio
555
+ // -> /Applications/BambuStudio.app/Contents/Resources/profiles
556
+ const macGuess = path.resolve(path.dirname(slicerPath), "..", "Resources", "profiles");
557
+ return macGuess;
558
+ }
559
+
560
+ // Default macOS install.
561
+ return "/Applications/BambuStudio.app/Contents/Resources/profiles";
562
+ }