bambu-printer-mcp 1.1.26 → 1.1.27
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/README.md +18 -0
- package/dist/index.js +53 -0
- package/dist/safety/print-file.js +7 -3
- package/dist/slicer/profile-files.d.ts +2 -0
- package/dist/slicer/profile-files.js +24 -0
- package/dist/slicer/profile-flatten.js +2 -13
- package/dist/stl/stl-manipulator.js +5 -8
- package/package.json +1 -1
- package/src/index.ts +45 -0
- package/src/safety/print-file.ts +6 -3
- package/src/slicer/profile-files.ts +21 -0
- package/src/slicer/profile-flatten.ts +2 -11
- package/src/stl/stl-manipulator.ts +5 -10
package/README.md
CHANGED
|
@@ -878,6 +878,24 @@ To stop drying:
|
|
|
878
878
|
}
|
|
879
879
|
```
|
|
880
880
|
|
|
881
|
+
#### inspect_print_file
|
|
882
|
+
|
|
883
|
+
Inspect a prepared sliced 3MF or textual G-code file offline before deciding what to export or print. This tool copies the file into a private read-only snapshot and reuses the same archive, selected-plate, model/nozzle/material and temperature checks used by print preflight. It never contacts a printer or auto-slices an unsliced project.
|
|
884
|
+
|
|
885
|
+
```json
|
|
886
|
+
{
|
|
887
|
+
"file_path": "/path/to/bracket.gcode.3mf",
|
|
888
|
+
"bambu_model": "p1s",
|
|
889
|
+
"plate_index": 0,
|
|
890
|
+
"nozzle_diameters": [0.4],
|
|
891
|
+
"bed_type": "textured_plate"
|
|
892
|
+
}
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
`file_path` and explicit `bambu_model` are required; the model is never inferred from configuration. `plate_index` is zero-based and defaults to 0. Nozzle/bed checks are optional comparisons against file declarations, not observations of installed hardware. Only these five argument names are accepted; credentials, host and executable/bridge overrides are not part of this tool.
|
|
896
|
+
|
|
897
|
+
The generic result has `receipt_version: "1"`, `evidence: "file-only"`, `dispatch_authorized: false`, `artifact` (`sha256`, `byte_length`, `file_format`), the requested `selection`, and the original `inspection` including declared model, nozzles, materials/used filament positions, internal plate path and checked temperature peaks. Hash and byte length identify the exact inspected snapshot; a consumer must compare them with its own artifact and selected plate. The temporary snapshot is removed, and its path is not a dispatch capability. The receipt does not prove printer readiness, physical material/nozzle contents, fit, full motion simulation or permission to print. Actual print routes still independently inspect the dispatched snapshot and require fresh hardware state and human preflight.
|
|
898
|
+
|
|
881
899
|
#### print_3mf
|
|
882
900
|
|
|
883
901
|
The primary tool for starting a Bambu print. **Recommended input: a pre-sliced `.gcode.3mf` exported from FULU OrcaSlicer-bambulab, OrcaSlicer, or Bambu Studio** — see [slicing guide](https://github.com/DMontgomery40/bambu-printer-mcp/blob/main/docs/SLICING.md). This tool handles the complete workflow:
|
package/dist/index.js
CHANGED
|
@@ -2377,6 +2377,23 @@ class BambuPrinterMCPServer {
|
|
|
2377
2377
|
required: ["action", "ams_id"]
|
|
2378
2378
|
}
|
|
2379
2379
|
},
|
|
2380
|
+
{
|
|
2381
|
+
name: "inspect_print_file",
|
|
2382
|
+
description: "Inspect an exact private snapshot of a prepared sliced 3MF or textual G-code file and selected plate without contacting a printer. Returns versioned file-only evidence; no printer readiness or permission to dispatch is established. Does not auto-slice.",
|
|
2383
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2384
|
+
inputSchema: {
|
|
2385
|
+
type: "object",
|
|
2386
|
+
additionalProperties: false,
|
|
2387
|
+
properties: {
|
|
2388
|
+
file_path: { type: "string", minLength: 1, description: "Local prepared .3mf or textual .gcode artifact to inspect; checksum-only and unsliced projects reject." },
|
|
2389
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Explicit file-inspection target model; never inferred from environment or live state." },
|
|
2390
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based selected plate; defaults to 0. Raw G-code only supports 0." },
|
|
2391
|
+
nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Optional requested nozzle diameters to compare with file declarations; this does not observe installed hardware." },
|
|
2392
|
+
bed_type: { type: "string", enum: [...VALID_BED_TYPES], description: "Optional declared bed type to compare with file metadata." },
|
|
2393
|
+
},
|
|
2394
|
+
required: ["file_path", "bambu_model"],
|
|
2395
|
+
},
|
|
2396
|
+
},
|
|
2380
2397
|
{
|
|
2381
2398
|
name: "print_3mf",
|
|
2382
2399
|
description: "Print a 3MF file on a Bambu Lab printer. Auto-slices if the 3MF has no gcode. IMPORTANT: bambu_model must be specified to ensure safe printer operation.",
|
|
@@ -2613,6 +2630,42 @@ class BambuPrinterMCPServer {
|
|
|
2613
2630
|
try {
|
|
2614
2631
|
let result;
|
|
2615
2632
|
switch (name) {
|
|
2633
|
+
case "inspect_print_file": {
|
|
2634
|
+
const allowed = new Set(["file_path", "bambu_model", "plate_index", "nozzle_diameters", "bed_type"]);
|
|
2635
|
+
if (!args || Object.keys(args).some(key => !allowed.has(key)))
|
|
2636
|
+
throw new Error("inspect_print_file accepts only file/model/plate/nozzle/bed arguments.");
|
|
2637
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim() || args.file_path.includes("\0"))
|
|
2638
|
+
throw new Error("file_path must name a local prepared artifact.");
|
|
2639
|
+
if (typeof args.bambu_model !== "string" || !args.bambu_model.trim())
|
|
2640
|
+
throw new Error("An explicit bambu_model is required for file inspection.");
|
|
2641
|
+
const model = validateBambuModel(args.bambu_model);
|
|
2642
|
+
const plateIndex = args.plate_index === undefined ? 0 : args.plate_index;
|
|
2643
|
+
if (typeof plateIndex !== "number" || !Number.isSafeInteger(plateIndex) || plateIndex < 0)
|
|
2644
|
+
throw new Error("plate_index must be a nonnegative safe integer.");
|
|
2645
|
+
if (args.nozzle_diameters !== undefined && (!Array.isArray(args.nozzle_diameters) || args.nozzle_diameters.some(value => typeof value !== "number")))
|
|
2646
|
+
throw new Error("nozzle_diameters must be numeric sizes.");
|
|
2647
|
+
const nozzles = requestedPrintNozzles(args);
|
|
2648
|
+
if (args.bed_type !== undefined && (typeof args.bed_type !== "string" || !args.bed_type.trim()))
|
|
2649
|
+
throw new Error("bed_type must be a supported plate type.");
|
|
2650
|
+
const bedType = args.bed_type === undefined ? undefined : resolveBedType(args.bed_type);
|
|
2651
|
+
if (extra.signal.aborted)
|
|
2652
|
+
throw new Error("File inspection cancelled.");
|
|
2653
|
+
result = await withPrintSnapshot(args.file_path, async (snapshot) => {
|
|
2654
|
+
const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: nozzles, plateIndex, bedType });
|
|
2655
|
+
if (extra.signal.aborted)
|
|
2656
|
+
throw new Error("File inspection cancelled.");
|
|
2657
|
+
const byteLength = (await fs.promises.stat(snapshot)).size;
|
|
2658
|
+
return {
|
|
2659
|
+
receipt_version: "1",
|
|
2660
|
+
evidence: "file-only",
|
|
2661
|
+
dispatch_authorized: false,
|
|
2662
|
+
artifact: { sha256: inspection.sha256, byte_length: byteLength, file_format: path.extname(snapshot).toLowerCase() === ".3mf" ? "3mf" : "gcode" },
|
|
2663
|
+
selection: { bambu_model: model, plate_index: plateIndex, ...(nozzles === undefined ? {} : { nozzle_diameters: nozzles }), ...(bedType === undefined ? {} : { bed_type: bedType }) },
|
|
2664
|
+
inspection,
|
|
2665
|
+
};
|
|
2666
|
+
});
|
|
2667
|
+
break;
|
|
2668
|
+
}
|
|
2616
2669
|
case "get_printer_status":
|
|
2617
2670
|
result = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
2618
2671
|
break;
|
|
@@ -372,10 +372,14 @@ export async function inspectPrintFile(filePath, options) {
|
|
|
372
372
|
if (/[{}\[\]#]/.test(line))
|
|
373
373
|
fail('unresolved dynamic command syntax');
|
|
374
374
|
if (/^SYNC(?=$|\s)/i.test(line)) {
|
|
375
|
-
//
|
|
376
|
-
//
|
|
377
|
-
// vendor template establishes no independent maximum duration.
|
|
375
|
+
// Vendor SYNC T is a duration, not a heater target. X2D and P2S
|
|
376
|
+
// startup templates additionally emit R0; no other R form is supported.
|
|
378
377
|
const args = parameters(line.slice(4));
|
|
378
|
+
if (args.has('R')) {
|
|
379
|
+
if (!['x2d', 'p2s'].includes(model) || args.get('R') !== 0)
|
|
380
|
+
fail('unsupported SYNC reset parameters');
|
|
381
|
+
args.delete('R');
|
|
382
|
+
}
|
|
379
383
|
if (args.size !== 1 || !args.has('T') || !Number.isFinite(args.get('T')) || args.get('T') < 0)
|
|
380
384
|
fail('unsupported SYNC duration parameters');
|
|
381
385
|
commandCount++;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/** Discover vendor subfolders without following links into another profile tree. */
|
|
4
|
+
export function profileJsonFiles(directory) {
|
|
5
|
+
let entries;
|
|
6
|
+
try {
|
|
7
|
+
if (lstatSync(directory).isSymbolicLink())
|
|
8
|
+
return [];
|
|
9
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
10
|
+
}
|
|
11
|
+
catch (error) {
|
|
12
|
+
if (error.code === 'ENOENT')
|
|
13
|
+
return [];
|
|
14
|
+
throw error;
|
|
15
|
+
}
|
|
16
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
17
|
+
const files = entries.filter(entry => entry.isFile() && entry.name.endsWith('.json'))
|
|
18
|
+
.map(entry => path.join(directory, entry.name));
|
|
19
|
+
for (const entry of entries) {
|
|
20
|
+
if (entry.isDirectory())
|
|
21
|
+
files.push(...profileJsonFiles(path.join(directory, entry.name)));
|
|
22
|
+
}
|
|
23
|
+
return files;
|
|
24
|
+
}
|
|
@@ -27,6 +27,7 @@ import fs from "node:fs/promises";
|
|
|
27
27
|
import { existsSync, realpathSync } from "node:fs";
|
|
28
28
|
import path from "node:path";
|
|
29
29
|
import crypto from "node:crypto";
|
|
30
|
+
import { profileJsonFiles } from './profile-files.js';
|
|
30
31
|
/* -------------------------------------------------------------------------- */
|
|
31
32
|
/* Indexing */
|
|
32
33
|
/* -------------------------------------------------------------------------- */
|
|
@@ -48,19 +49,7 @@ async function buildNameIndex(profilesRoot, vendor, userProfileRoots = []) {
|
|
|
48
49
|
...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
|
|
49
50
|
];
|
|
50
51
|
for (const dir of directories) {
|
|
51
|
-
|
|
52
|
-
try {
|
|
53
|
-
entries = await fs.readdir(dir);
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
// Vendor or subdir missing -- skip silently; flatten will fail later
|
|
57
|
-
// with a precise "name not found" error.
|
|
58
|
-
continue;
|
|
59
|
-
}
|
|
60
|
-
for (const entry of entries) {
|
|
61
|
-
if (!entry.endsWith(".json"))
|
|
62
|
-
continue;
|
|
63
|
-
const filePath = path.join(dir, entry);
|
|
52
|
+
for (const filePath of profileJsonFiles(dir)) {
|
|
64
53
|
let raw;
|
|
65
54
|
try {
|
|
66
55
|
raw = await fs.readFile(filePath, "utf8");
|
|
@@ -10,6 +10,7 @@ import { EventEmitter } from 'events';
|
|
|
10
10
|
import * as crypto from 'crypto';
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
12
|
import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
|
|
13
|
+
import { profileJsonFiles } from '../slicer/profile-files.js';
|
|
13
14
|
const readFileAsync = promisify(fs.readFile);
|
|
14
15
|
const writeFileAsync = promisify(fs.writeFile);
|
|
15
16
|
const BAMBU_PROFILE_ROOTS = [
|
|
@@ -134,10 +135,10 @@ export class STLManipulator extends EventEmitter {
|
|
|
134
135
|
return undefined;
|
|
135
136
|
}
|
|
136
137
|
for (const root of roots) {
|
|
137
|
-
const candidate = path.join(root, category
|
|
138
|
-
|
|
138
|
+
const candidate = profileJsonFiles(path.join(root, category))
|
|
139
|
+
.find(file => path.basename(file) === `${profileName}.json`);
|
|
140
|
+
if (candidate)
|
|
139
141
|
return candidate;
|
|
140
|
-
}
|
|
141
142
|
}
|
|
142
143
|
return undefined;
|
|
143
144
|
}
|
|
@@ -148,11 +149,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
148
149
|
if (!fs.existsSync(filamentDir)) {
|
|
149
150
|
continue;
|
|
150
151
|
}
|
|
151
|
-
for (const
|
|
152
|
-
if (!entry.endsWith('.json')) {
|
|
153
|
-
continue;
|
|
154
|
-
}
|
|
155
|
-
const filePath = path.join(filamentDir, entry);
|
|
152
|
+
for (const filePath of profileJsonFiles(filamentDir)) {
|
|
156
153
|
try {
|
|
157
154
|
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
158
155
|
const filamentId = typeof parsed?.filament_id === 'string' ? parsed.filament_id.trim() : '';
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -2900,6 +2900,23 @@ class BambuPrinterMCPServer {
|
|
|
2900
2900
|
required: ["action", "ams_id"]
|
|
2901
2901
|
}
|
|
2902
2902
|
},
|
|
2903
|
+
{
|
|
2904
|
+
name: "inspect_print_file",
|
|
2905
|
+
description: "Inspect an exact private snapshot of a prepared sliced 3MF or textual G-code file and selected plate without contacting a printer. Returns versioned file-only evidence; no printer readiness or permission to dispatch is established. Does not auto-slice.",
|
|
2906
|
+
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
|
|
2907
|
+
inputSchema: {
|
|
2908
|
+
type: "object",
|
|
2909
|
+
additionalProperties: false,
|
|
2910
|
+
properties: {
|
|
2911
|
+
file_path: { type: "string", minLength: 1, description: "Local prepared .3mf or textual .gcode artifact to inspect; checksum-only and unsliced projects reject." },
|
|
2912
|
+
bambu_model: { type: "string", enum: [...VALID_BAMBU_MODELS], description: "Explicit file-inspection target model; never inferred from environment or live state." },
|
|
2913
|
+
plate_index: { type: "integer", minimum: 0, description: "Zero-based selected plate; defaults to 0. Raw G-code only supports 0." },
|
|
2914
|
+
nozzle_diameters: { type: "array", minItems: 1, maxItems: 2, items: { type: "number", enum: [0.2, 0.4, 0.6, 0.8] }, description: "Optional requested nozzle diameters to compare with file declarations; this does not observe installed hardware." },
|
|
2915
|
+
bed_type: { type: "string", enum: [...VALID_BED_TYPES], description: "Optional declared bed type to compare with file metadata." },
|
|
2916
|
+
},
|
|
2917
|
+
required: ["file_path", "bambu_model"],
|
|
2918
|
+
},
|
|
2919
|
+
},
|
|
2903
2920
|
{
|
|
2904
2921
|
name: "print_3mf",
|
|
2905
2922
|
description: "Print a 3MF file on a Bambu Lab printer. Auto-slices if the 3MF has no gcode. IMPORTANT: bambu_model must be specified to ensure safe printer operation.",
|
|
@@ -3147,6 +3164,34 @@ class BambuPrinterMCPServer {
|
|
|
3147
3164
|
let result;
|
|
3148
3165
|
|
|
3149
3166
|
switch (name) {
|
|
3167
|
+
case "inspect_print_file": {
|
|
3168
|
+
const allowed = new Set(["file_path", "bambu_model", "plate_index", "nozzle_diameters", "bed_type"]);
|
|
3169
|
+
if (!args || Object.keys(args).some(key => !allowed.has(key))) throw new Error("inspect_print_file accepts only file/model/plate/nozzle/bed arguments.");
|
|
3170
|
+
if (typeof args.file_path !== "string" || !args.file_path.trim() || args.file_path.includes("\0")) throw new Error("file_path must name a local prepared artifact.");
|
|
3171
|
+
if (typeof args.bambu_model !== "string" || !args.bambu_model.trim()) throw new Error("An explicit bambu_model is required for file inspection.");
|
|
3172
|
+
const model = validateBambuModel(args.bambu_model);
|
|
3173
|
+
const plateIndex = args.plate_index === undefined ? 0 : args.plate_index;
|
|
3174
|
+
if (typeof plateIndex !== "number" || !Number.isSafeInteger(plateIndex) || plateIndex < 0) throw new Error("plate_index must be a nonnegative safe integer.");
|
|
3175
|
+
if (args.nozzle_diameters !== undefined && (!Array.isArray(args.nozzle_diameters) || args.nozzle_diameters.some(value => typeof value !== "number"))) throw new Error("nozzle_diameters must be numeric sizes.");
|
|
3176
|
+
const nozzles = requestedPrintNozzles(args);
|
|
3177
|
+
if (args.bed_type !== undefined && (typeof args.bed_type !== "string" || !args.bed_type.trim())) throw new Error("bed_type must be a supported plate type.");
|
|
3178
|
+
const bedType = args.bed_type === undefined ? undefined : resolveBedType(args.bed_type);
|
|
3179
|
+
if (extra.signal.aborted) throw new Error("File inspection cancelled.");
|
|
3180
|
+
result = await withPrintSnapshot(args.file_path, async snapshot => {
|
|
3181
|
+
const inspection = await inspectPrintFile(snapshot, { model, nozzleDiameters: nozzles, plateIndex, bedType });
|
|
3182
|
+
if (extra.signal.aborted) throw new Error("File inspection cancelled.");
|
|
3183
|
+
const byteLength = (await fs.promises.stat(snapshot)).size;
|
|
3184
|
+
return {
|
|
3185
|
+
receipt_version: "1",
|
|
3186
|
+
evidence: "file-only",
|
|
3187
|
+
dispatch_authorized: false,
|
|
3188
|
+
artifact: { sha256: inspection.sha256, byte_length: byteLength, file_format: path.extname(snapshot).toLowerCase() === ".3mf" ? "3mf" : "gcode" },
|
|
3189
|
+
selection: { bambu_model: model, plate_index: plateIndex, ...(nozzles === undefined ? {} : { nozzle_diameters: nozzles }), ...(bedType === undefined ? {} : { bed_type: bedType }) },
|
|
3190
|
+
inspection,
|
|
3191
|
+
};
|
|
3192
|
+
});
|
|
3193
|
+
break;
|
|
3194
|
+
}
|
|
3150
3195
|
case "get_printer_status":
|
|
3151
3196
|
result = await this.bambu.getStatus(host, bambuSerial, bambuToken);
|
|
3152
3197
|
break;
|
package/src/safety/print-file.ts
CHANGED
|
@@ -278,10 +278,13 @@ export async function inspectPrintFile(filePath: string, options: {model:string;
|
|
|
278
278
|
line=line.replace(/^N\d+\s*/i,'').replace(/\*\d+\s*$/,'');
|
|
279
279
|
if(/[{}\[\]#]/.test(line)) fail('unresolved dynamic command syntax');
|
|
280
280
|
if(/^SYNC(?=$|\s)/i.test(line)) {
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
// vendor template establishes no independent maximum duration.
|
|
281
|
+
// Vendor SYNC T is a duration, not a heater target. X2D and P2S
|
|
282
|
+
// startup templates additionally emit R0; no other R form is supported.
|
|
284
283
|
const args=parameters(line.slice(4));
|
|
284
|
+
if(args.has('R')) {
|
|
285
|
+
if(!['x2d','p2s'].includes(model) || args.get('R')!==0) fail('unsupported SYNC reset parameters');
|
|
286
|
+
args.delete('R');
|
|
287
|
+
}
|
|
285
288
|
if(args.size!==1 || !args.has('T') || !Number.isFinite(args.get('T')) || args.get('T')!<0) fail('unsupported SYNC duration parameters');
|
|
286
289
|
commandCount++;continue;
|
|
287
290
|
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { lstatSync, readdirSync } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
/** Discover vendor subfolders without following links into another profile tree. */
|
|
5
|
+
export function profileJsonFiles(directory: string): string[] {
|
|
6
|
+
let entries;
|
|
7
|
+
try {
|
|
8
|
+
if (lstatSync(directory).isSymbolicLink()) return [];
|
|
9
|
+
entries = readdirSync(directory, { withFileTypes: true });
|
|
10
|
+
} catch (error) {
|
|
11
|
+
if ((error as NodeJS.ErrnoException).code === 'ENOENT') return [];
|
|
12
|
+
throw error;
|
|
13
|
+
}
|
|
14
|
+
entries.sort((a, b) => a.name.localeCompare(b.name));
|
|
15
|
+
const files = entries.filter(entry => entry.isFile() && entry.name.endsWith('.json'))
|
|
16
|
+
.map(entry => path.join(directory, entry.name));
|
|
17
|
+
for (const entry of entries) {
|
|
18
|
+
if (entry.isDirectory()) files.push(...profileJsonFiles(path.join(directory, entry.name)));
|
|
19
|
+
}
|
|
20
|
+
return files;
|
|
21
|
+
}
|
|
@@ -28,6 +28,7 @@ import fs from "node:fs/promises";
|
|
|
28
28
|
import { existsSync, realpathSync } from "node:fs";
|
|
29
29
|
import path from "node:path";
|
|
30
30
|
import crypto from "node:crypto";
|
|
31
|
+
import { profileJsonFiles } from './profile-files.js';
|
|
31
32
|
|
|
32
33
|
export type ProfileKind = "machine" | "process" | "filament";
|
|
33
34
|
|
|
@@ -125,17 +126,7 @@ async function buildNameIndex(
|
|
|
125
126
|
...userProfileRoots.flatMap(root => ['process', 'filament'].map(sub => path.join(root, sub))),
|
|
126
127
|
];
|
|
127
128
|
for (const dir of directories) {
|
|
128
|
-
|
|
129
|
-
try {
|
|
130
|
-
entries = await fs.readdir(dir);
|
|
131
|
-
} catch {
|
|
132
|
-
// Vendor or subdir missing -- skip silently; flatten will fail later
|
|
133
|
-
// with a precise "name not found" error.
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
for (const entry of entries) {
|
|
137
|
-
if (!entry.endsWith(".json")) continue;
|
|
138
|
-
const filePath = path.join(dir, entry);
|
|
129
|
+
for (const filePath of profileJsonFiles(dir)) {
|
|
139
130
|
let raw: string;
|
|
140
131
|
try {
|
|
141
132
|
raw = await fs.readFile(filePath, "utf8");
|
|
@@ -10,6 +10,7 @@ import { EventEmitter } from 'events';
|
|
|
10
10
|
import * as crypto from 'crypto';
|
|
11
11
|
import { execFile } from 'child_process';
|
|
12
12
|
import { flattenForCli, detectProfilesRoot, resolveBblMachineProfile } from '../slicer/profile-flatten.js';
|
|
13
|
+
import { profileJsonFiles } from '../slicer/profile-files.js';
|
|
13
14
|
|
|
14
15
|
const readFileAsync = promisify(fs.readFile);
|
|
15
16
|
const writeFileAsync = promisify(fs.writeFile);
|
|
@@ -232,10 +233,9 @@ export class STLManipulator extends EventEmitter {
|
|
|
232
233
|
}
|
|
233
234
|
|
|
234
235
|
for (const root of roots) {
|
|
235
|
-
const candidate = path.join(root, category
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
}
|
|
236
|
+
const candidate = profileJsonFiles(path.join(root, category))
|
|
237
|
+
.find(file => path.basename(file) === `${profileName}.json`);
|
|
238
|
+
if (candidate) return candidate;
|
|
239
239
|
}
|
|
240
240
|
|
|
241
241
|
return undefined;
|
|
@@ -250,12 +250,7 @@ export class STLManipulator extends EventEmitter {
|
|
|
250
250
|
continue;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
for (const
|
|
254
|
-
if (!entry.endsWith('.json')) {
|
|
255
|
-
continue;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
const filePath = path.join(filamentDir, entry);
|
|
253
|
+
for (const filePath of profileJsonFiles(filamentDir)) {
|
|
259
254
|
try {
|
|
260
255
|
const parsed = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
261
256
|
const filamentId = typeof parsed?.filament_id === 'string' ? parsed.filament_id.trim() : '';
|