mcp-3d-printer-server 1.2.13 → 1.2.14
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/CHANGELOG.md +21 -0
- package/CONTRIBUTORS.md +2 -0
- package/README.md +2 -2
- package/dist/fulu-orca.d.ts +8 -0
- package/dist/fulu-orca.js +107 -10
- package/dist/index.js +3 -3
- package/dist/printers/bambu.js +11 -4
- package/dist/safety/confirmation.js +9 -2
- package/dist/safety/printer-state.d.ts +2 -0
- package/dist/safety/printer-state.js +42 -1
- package/dist/slicer/bambu-cli.d.ts +21 -1
- package/dist/slicer/bambu-cli.js +86 -11
- package/dist/slicer/run-slicer.d.ts +2 -0
- package/dist/slicer/run-slicer.js +3 -2
- package/dist/stl/stl-manipulator.d.ts +8 -0
- package/dist/stl/stl-manipulator.js +31 -2
- package/package.json +2 -2
- package/server.json +2 -2
- package/src/fulu-orca.ts +102 -10
- package/src/index.ts +3 -3
- package/src/printers/bambu.ts +11 -3
- package/src/safety/confirmation.ts +8 -2
- package/src/safety/printer-state.ts +42 -1
- package/src/slicer/bambu-cli.ts +92 -7
- package/src/slicer/run-slicer.ts +5 -2
- package/src/stl/stl-manipulator.ts +29 -1
|
@@ -46,6 +46,14 @@ export declare class STLManipulator extends EventEmitter {
|
|
|
46
46
|
private appendOrcaProfileArgs;
|
|
47
47
|
private resolveOrcaGcodeOutput;
|
|
48
48
|
private resolveSlicerTimeoutMs;
|
|
49
|
+
/**
|
|
50
|
+
* Apply slice_stl's scale and rotations to a copy of the mesh as OrcaSlicer's
|
|
51
|
+
* CLI would, about the model's centre. Orca adds each --rotate* to the
|
|
52
|
+
* object's Euler angles (ModelVolume::rotate), so the result rotates about X,
|
|
53
|
+
* then Y, then Z whatever the argument order, after scaling. The slicer then
|
|
54
|
+
* places the result on the plate.
|
|
55
|
+
*/
|
|
56
|
+
private transformMeshForCli;
|
|
49
57
|
/**
|
|
50
58
|
* Load STL file and return geometry and bounding box
|
|
51
59
|
*/
|
|
@@ -8,7 +8,7 @@ import { promisify } from 'util';
|
|
|
8
8
|
import { EventEmitter } from 'events';
|
|
9
9
|
import * as crypto from 'crypto';
|
|
10
10
|
import { detectProfilesRoot } from '../slicer/profile-flatten.js';
|
|
11
|
-
import { BambuCliProfilePreparer, assertSlicedProjectOutput, buildBambuCliArgs, } from '../slicer/bambu-cli.js';
|
|
11
|
+
import { BambuCliProfilePreparer, assertSlicedProjectOutput, bambuCliFailureReason, buildBambuCliArgs, planBambuCliInput, } from '../slicer/bambu-cli.js';
|
|
12
12
|
import { runSlicerProcess } from '../slicer/run-slicer.js';
|
|
13
13
|
import { SlicerError, asPreparationError } from '../slicer/slicer-error.js';
|
|
14
14
|
import { scanGcodeThermal } from '../safety/gcode-thermal.js';
|
|
@@ -110,6 +110,30 @@ export class STLManipulator extends EventEmitter {
|
|
|
110
110
|
}
|
|
111
111
|
return timeoutMs;
|
|
112
112
|
}
|
|
113
|
+
/**
|
|
114
|
+
* Apply slice_stl's scale and rotations to a copy of the mesh as OrcaSlicer's
|
|
115
|
+
* CLI would, about the model's centre. Orca adds each --rotate* to the
|
|
116
|
+
* object's Euler angles (ModelVolume::rotate), so the result rotates about X,
|
|
117
|
+
* then Y, then Z whatever the argument order, after scaling. The slicer then
|
|
118
|
+
* places the result on the plate.
|
|
119
|
+
*/
|
|
120
|
+
async transformMeshForCli(stlFilePath, options, outputDir) {
|
|
121
|
+
const { geometry, boundingBox } = await this.loadSTL(stlFilePath);
|
|
122
|
+
const center = new THREE.Vector3();
|
|
123
|
+
boundingBox.getCenter(center);
|
|
124
|
+
const radians = (degrees) => degrees * Math.PI / 180;
|
|
125
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(-center.x, -center.y, -center.z));
|
|
126
|
+
if (options.scale !== undefined)
|
|
127
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeScale(options.scale, options.scale, options.scale));
|
|
128
|
+
if (options.rotateX)
|
|
129
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeRotationX(radians(options.rotateX)));
|
|
130
|
+
if (options.rotateY)
|
|
131
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeRotationY(radians(options.rotateY)));
|
|
132
|
+
if (options.rotate)
|
|
133
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeRotationZ(radians(options.rotate)));
|
|
134
|
+
geometry.applyMatrix4(new THREE.Matrix4().makeTranslation(center.x, center.y, center.z));
|
|
135
|
+
return this.saveSTL(geometry, path.join(outputDir, `${path.basename(stlFilePath, path.extname(stlFilePath))}_transformed.stl`));
|
|
136
|
+
}
|
|
113
137
|
/**
|
|
114
138
|
* Load STL file and return geometry and bounding box
|
|
115
139
|
*/
|
|
@@ -806,11 +830,14 @@ export class STLManipulator extends EventEmitter {
|
|
|
806
830
|
}
|
|
807
831
|
options.loadFilaments = legacyFilaments;
|
|
808
832
|
}
|
|
833
|
+
const cliInput = planBambuCliInput(cliType, is3mf, options);
|
|
809
834
|
const profilesRoot = detectProfilesRoot(slicerPath, cliType);
|
|
810
835
|
const preparer = new BambuCliProfilePreparer(path.join(this.tempDir, 'slicer-profiles'));
|
|
811
836
|
const bundle = await preparer.prepare(stlFilePath, outputBase, cliType, profilesRoot, printerPreset, orcaProfiles.settingsProfiles[0], options);
|
|
812
837
|
bambuOutputDir = fs.mkdtempSync(path.join(this.tempDir, `${operationId}-bambu-output-`));
|
|
813
|
-
|
|
838
|
+
// The transformed copy lives in the run's output directory, which is removed afterwards.
|
|
839
|
+
const inputPath = cliInput.meshTransformed ? await this.transformMeshForCli(stlFilePath, options, bambuOutputDir) : stlFilePath;
|
|
840
|
+
args = [...buildBambuCliArgs(bundle, bambuOutputDir, exportName, options, cliInput), inputPath];
|
|
814
841
|
bambuExport = {
|
|
815
842
|
producedPath: path.join(bambuOutputDir, exportName),
|
|
816
843
|
// One folder per operation: concurrent slices of same-named inputs
|
|
@@ -871,12 +898,14 @@ export class STLManipulator extends EventEmitter {
|
|
|
871
898
|
if (progressCallback)
|
|
872
899
|
progressCallback(20, `Executing slicer: ${slicerPath} ${args.join(' ')}`);
|
|
873
900
|
console.error(`Executing: ${slicerPath} ${args.join(' ')}`);
|
|
901
|
+
const cliOutputDir = bambuOutputDir;
|
|
874
902
|
await runSlicerProcess({
|
|
875
903
|
command: slicerPath,
|
|
876
904
|
args,
|
|
877
905
|
timeoutMs: this.resolveSlicerTimeoutMs(),
|
|
878
906
|
slicerType,
|
|
879
907
|
isCancelled: () => !this.activeOperations.get(operationId),
|
|
908
|
+
failureReason: cliOutputDir ? (exitCode) => bambuCliFailureReason(exitCode, cliOutputDir) : undefined,
|
|
880
909
|
});
|
|
881
910
|
if (!this.activeOperations.get(operationId)) {
|
|
882
911
|
throw new SlicerError('execution', 'Slicing operation cancelled after process finished', { slicerType, slicerPath });
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-3d-printer-server",
|
|
3
3
|
"mcpName": "io.github.DMontgomery40/mcp-3d-printer-server",
|
|
4
|
-
"version": "1.2.
|
|
4
|
+
"version": "1.2.14",
|
|
5
5
|
"description": "MCP server for connecting Claude with 3D printer management systems",
|
|
6
6
|
"main": "dist/index.js",
|
|
7
7
|
"repository": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
@@ -88,7 +88,7 @@
|
|
|
88
88
|
"jszip": "^3.10.2",
|
|
89
89
|
"three": "^0.173.0",
|
|
90
90
|
"xml2js": "^0.6.2",
|
|
91
|
-
"zod": "^
|
|
91
|
+
"zod": "^4.6.7"
|
|
92
92
|
},
|
|
93
93
|
"devDependencies": {
|
|
94
94
|
"@types/jszip": "^3.4.0",
|
package/server.json
CHANGED
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/DMontgomery40/mcp-3D-printer-server",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.2.
|
|
10
|
+
"version": "1.2.14",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"identifier": "mcp-3d-printer-server",
|
|
15
|
-
"version": "1.2.
|
|
15
|
+
"version": "1.2.14",
|
|
16
16
|
"runtimeHint": "npx",
|
|
17
17
|
"transport": {
|
|
18
18
|
"type": "stdio"
|
package/src/fulu-orca.ts
CHANGED
|
@@ -99,6 +99,46 @@ export function isFuluPrintRpcMethod(method: string): boolean {
|
|
|
99
99
|
return FULU_PRINT_RPC_METHODS.has(method.trim());
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
const FULU_LINUX_DOCS = "https://github.com/DMontgomery40/mcp-3D-printer-server/blob/main/docs/FULU.md#linux";
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* FULU's Linux host opens its network and source libraries by bare name from
|
|
106
|
+
* its working directory instead of PJARCZAK_BAMBU_PLUGIN_DIR
|
|
107
|
+
* (FULU-Foundation/OrcaSlicer-bambulab#51), so name them in that directory
|
|
108
|
+
* unless they are already set. macOS wrappers run the host in a Linux guest
|
|
109
|
+
* that may not see host paths, so they are left alone.
|
|
110
|
+
*/
|
|
111
|
+
export function linuxPluginLibraryEnv(env: NodeJS.ProcessEnv, platform: NodeJS.Platform): Record<string, string> {
|
|
112
|
+
const pluginDir = env.PJARCZAK_BAMBU_PLUGIN_DIR?.trim();
|
|
113
|
+
if (platform !== "linux" || !pluginDir) return {};
|
|
114
|
+
const libraries: Record<string, string> = {};
|
|
115
|
+
if (!env.PJARCZAK_BAMBU_NETWORK_SO?.trim()) libraries.PJARCZAK_BAMBU_NETWORK_SO = path.resolve(pluginDir, "libbambu_networking.so");
|
|
116
|
+
if (!env.PJARCZAK_BAMBU_SOURCE_SO?.trim()) libraries.PJARCZAK_BAMBU_SOURCE_SO = path.resolve(pluginDir, "libBambuSource.so");
|
|
117
|
+
return libraries;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** FULU's Linux host exits 101 when it cannot reopen a socket stdout (FULU-Foundation/OrcaSlicer-bambulab#50). */
|
|
121
|
+
const STDOUT_SOCKET_HINT =
|
|
122
|
+
"Exit code 101 means FULU's Linux host could not reopen its stdout, which fails when stdout is a socket, as Node.js provides " +
|
|
123
|
+
"(https://github.com/FULU-Foundation/OrcaSlicer-bambulab/issues/50). Launch the host through a wrapper that pipes its output through cat: " +
|
|
124
|
+
FULU_LINUX_DOCS;
|
|
125
|
+
|
|
126
|
+
/** Actionable hints for a bridge run: the exit-101 socket bug and a host that cannot open the plug-in libraries. */
|
|
127
|
+
function bridgeHints(exitCode: number | null, responses: Record<string, unknown>): string[] {
|
|
128
|
+
const hints: string[] = [];
|
|
129
|
+
if (exitCode === 101) hints.push(STDOUT_SOCKET_HINT);
|
|
130
|
+
const fileOpenFailure = (Object.values(responses) as Array<{ network_status?: unknown; error?: unknown } | null>)
|
|
131
|
+
.find((value) => [value?.network_status, value?.error].some((text) => typeof text === "string" && /file open failed/i.test(text)));
|
|
132
|
+
if (fileOpenFailure) {
|
|
133
|
+
hints.push(
|
|
134
|
+
`FULU BambuNetwork bridge could not open its networking library (${String(fileOpenFailure.network_status ?? fileOpenFailure.error)}). ` +
|
|
135
|
+
"Set PJARCZAK_BAMBU_PLUGIN_DIR to the directory containing libbambu_networking.so and libBambuSource.so, " +
|
|
136
|
+
`or set PJARCZAK_BAMBU_NETWORK_SO and PJARCZAK_BAMBU_SOURCE_SO to their absolute paths: ${FULU_LINUX_DOCS}`
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
return hints;
|
|
140
|
+
}
|
|
141
|
+
|
|
102
142
|
function normalizePlatform(value: string | undefined): SupportedPlatform {
|
|
103
143
|
const normalized = (value || process.platform).trim().toLowerCase();
|
|
104
144
|
if (normalized === "mac" || normalized === "macos" || normalized === "darwin") {
|
|
@@ -284,7 +324,7 @@ function setupCommands(platform: SupportedPlatform, pluginDir: string, runtimeDi
|
|
|
284
324
|
const verify = path.join(pluginDir, "verify_runtime_macos.sh");
|
|
285
325
|
const bridgeHost = path.join(runtimeDir || "", "pjarczak_bambu_linux_host");
|
|
286
326
|
const wrapper = path.join(pluginDir, "pjarczak-bambu-linux-host-wrapper");
|
|
287
|
-
|
|
327
|
+
const commands = [
|
|
288
328
|
{
|
|
289
329
|
label: "install macOS Lima runtime",
|
|
290
330
|
command: `bash ${quoteShell(install)} -PackageDir ${quoteShell(pluginDir)} -PluginDir ${quoteShell(pluginDir)}`,
|
|
@@ -297,11 +337,15 @@ function setupCommands(platform: SupportedPlatform, pluginDir: string, runtimeDi
|
|
|
297
337
|
label: "verify macOS Lima runtime core before Linux plugin payload is present",
|
|
298
338
|
command: `bash ${quoteShell(verify)} -PackageDir ${quoteShell(pluginDir)} -PluginDir ${quoteShell(pluginDir)} -AllowMissingLinuxPlugin`,
|
|
299
339
|
},
|
|
300
|
-
|
|
340
|
+
];
|
|
341
|
+
// The wrapper ships in FULU's macOS bundle; suggest it only where it exists.
|
|
342
|
+
if (fs.existsSync(wrapper)) {
|
|
343
|
+
commands.push({
|
|
301
344
|
label: "bridge command for FULU BambuNetwork runtime",
|
|
302
345
|
command: `${quoteShell(wrapper)} ${quoteShell(bridgeHost)}`,
|
|
303
|
-
}
|
|
304
|
-
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return commands;
|
|
305
349
|
}
|
|
306
350
|
|
|
307
351
|
if (platform === "win32") {
|
|
@@ -336,6 +380,11 @@ function setupCommands(platform: SupportedPlatform, pluginDir: string, runtimeDi
|
|
|
336
380
|
label: "configure MCP slicer path",
|
|
337
381
|
command: "SLICER_TYPE=orcaslicer-bambulab SLICER_PATH=/path/to/orcaslicer",
|
|
338
382
|
},
|
|
383
|
+
{
|
|
384
|
+
label: "bridge command for FULU's Linux host, piped through cat (FULU #50)",
|
|
385
|
+
command: `FULU_BAMBU_BRIDGE_COMMAND="${quoteShell(path.join(pluginDir, "pjarczak_bambu_linux_host"))} | cat" ` +
|
|
386
|
+
"PJARCZAK_BAMBU_PLUGIN_DIR=\"$HOME/.config/OrcaSlicer/plugins\"",
|
|
387
|
+
},
|
|
339
388
|
];
|
|
340
389
|
}
|
|
341
390
|
|
|
@@ -350,6 +399,7 @@ function defaultBridgeCommand(platform: SupportedPlatform, pluginDir: string, ru
|
|
|
350
399
|
|
|
351
400
|
if (platform === "darwin") {
|
|
352
401
|
const wrapper = path.join(pluginDir, "pjarczak-bambu-linux-host-wrapper");
|
|
402
|
+
if (!fs.existsSync(wrapper)) return undefined;
|
|
353
403
|
const runtimeHost = path.join(
|
|
354
404
|
runtimeDir || defaultRuntimeDir(platform) || "",
|
|
355
405
|
"pjarczak_bambu_linux_host"
|
|
@@ -358,8 +408,10 @@ function defaultBridgeCommand(platform: SupportedPlatform, pluginDir: string, ru
|
|
|
358
408
|
}
|
|
359
409
|
|
|
360
410
|
if (platform === "linux") {
|
|
411
|
+
// The v1.0.0 host exits 101 on the socket stdout Node.js gives a child
|
|
412
|
+
// (FULU-Foundation/OrcaSlicer-bambulab#50); a shell pipe through cat avoids it.
|
|
361
413
|
const host = path.join(pluginDir, "pjarczak_bambu_linux_host");
|
|
362
|
-
return fs.existsSync(host) ? quoteShell(host) : undefined;
|
|
414
|
+
return fs.existsSync(host) ? `${quoteShell(host)} | cat` : undefined;
|
|
363
415
|
}
|
|
364
416
|
|
|
365
417
|
return undefined;
|
|
@@ -417,6 +469,7 @@ async function runBridgeRequests(
|
|
|
417
469
|
return await new Promise((resolve, reject) => {
|
|
418
470
|
const child = spawn(command, {
|
|
419
471
|
shell: true,
|
|
472
|
+
env: { ...process.env, ...linuxPluginLibraryEnv(process.env, process.platform) },
|
|
420
473
|
stdio: ["pipe", "pipe", "pipe"],
|
|
421
474
|
});
|
|
422
475
|
|
|
@@ -433,13 +486,35 @@ async function runBridgeRequests(
|
|
|
433
486
|
callback();
|
|
434
487
|
};
|
|
435
488
|
|
|
489
|
+
const unanswered = new Set(requests.map((request) => request.id));
|
|
436
490
|
const timer = setTimeout(() => {
|
|
491
|
+
child.stdin?.end();
|
|
437
492
|
child.kill("SIGKILL");
|
|
438
|
-
|
|
493
|
+
// Keep what the host did say: a hung host is diagnosed from its stderr and partial answers.
|
|
494
|
+
const responses = Object.fromEntries(parseBridgeFrames(Buffer.concat(stdoutChunks))
|
|
495
|
+
.filter((frame) => frame.type === FRAME_TYPE_JSON_RESPONSE)
|
|
496
|
+
.map((frame) => [String(frame.id), (() => { try { return JSON.parse(frame.payload.toString("utf8")); } catch { return null; } })()]));
|
|
497
|
+
const stderr = Buffer.concat(stderrChunks).toString("utf8").trim().slice(-1000);
|
|
498
|
+
const hints = bridgeHints(null, responses);
|
|
499
|
+
settle(() => reject(new Error(
|
|
500
|
+
`FULU bridge request timed out after ${timeoutMs}ms with ${unanswered.size} of ${requests.length} request(s) unanswered.` +
|
|
501
|
+
(stderr ? ` Host stderr: ${stderr}` : "") + (hints.length ? ` ${hints.join(" ")}` : "")
|
|
502
|
+
)));
|
|
439
503
|
}, timeoutMs);
|
|
440
504
|
|
|
441
|
-
|
|
505
|
+
// FULU's host exits on stdin EOF without answering requests still in
|
|
506
|
+
// flight, so stdin closes only once every request has a response.
|
|
507
|
+
child.stdout?.on("data", (chunk: Buffer) => {
|
|
508
|
+
stdoutChunks.push(chunk);
|
|
509
|
+
if (!unanswered.size) return;
|
|
510
|
+
for (const frame of parseBridgeFrames(Buffer.concat(stdoutChunks))) {
|
|
511
|
+
if (frame.type === FRAME_TYPE_JSON_RESPONSE) unanswered.delete(frame.id);
|
|
512
|
+
}
|
|
513
|
+
if (!unanswered.size) child.stdin?.end();
|
|
514
|
+
});
|
|
442
515
|
child.stderr?.on("data", (chunk: Buffer) => stderrChunks.push(chunk));
|
|
516
|
+
// A host that exits early (exit code 101) closes the pipe; close reports the result.
|
|
517
|
+
child.stdin?.on("error", () => {});
|
|
443
518
|
child.on("error", (error) => {
|
|
444
519
|
settle(() => reject(error));
|
|
445
520
|
});
|
|
@@ -478,7 +553,7 @@ async function runBridgeRequests(
|
|
|
478
553
|
for (const request of requests) {
|
|
479
554
|
child.stdin?.write(writeJsonRequestFrame(request.id, request.method, request.payload));
|
|
480
555
|
}
|
|
481
|
-
child.stdin?.end();
|
|
556
|
+
if (!unanswered.size) child.stdin?.end();
|
|
482
557
|
});
|
|
483
558
|
}
|
|
484
559
|
|
|
@@ -493,9 +568,13 @@ async function probeFuluBridge(command: string, timeoutMs: number): Promise<Reco
|
|
|
493
568
|
timeoutMs
|
|
494
569
|
);
|
|
495
570
|
|
|
571
|
+
const handshake = probe.responses["1"] as { ok?: unknown; network_loaded?: unknown } | undefined;
|
|
572
|
+
const hints = bridgeHints(probe.exitCode, probe.responses);
|
|
496
573
|
return {
|
|
497
574
|
exitCode: probe.exitCode,
|
|
498
|
-
|
|
575
|
+
// A handshake from a host that could not load the networking plug-in is not a ready bridge.
|
|
576
|
+
ok: probe.ok && Boolean(handshake) && handshake?.ok !== false && handshake?.network_loaded !== false,
|
|
577
|
+
...(hints.length ? { hints } : {}),
|
|
499
578
|
stderr: probe.stderr,
|
|
500
579
|
logs: probe.logs,
|
|
501
580
|
handshake: probe.responses["1"] || null,
|
|
@@ -546,6 +625,7 @@ export async function invokeFuluBridgeRpc(options: FuluBridgeRpcOptions): Promis
|
|
|
546
625
|
options.timeoutMs || 5000
|
|
547
626
|
);
|
|
548
627
|
|
|
628
|
+
const hints = bridgeHints(bridgeResult.exitCode, bridgeResult.responses);
|
|
549
629
|
return {
|
|
550
630
|
method,
|
|
551
631
|
readOnly,
|
|
@@ -554,6 +634,7 @@ export async function invokeFuluBridgeRpc(options: FuluBridgeRpcOptions): Promis
|
|
|
554
634
|
bambuModel: options.bambuModel || undefined,
|
|
555
635
|
exitCode: bridgeResult.exitCode,
|
|
556
636
|
ok: bridgeResult.ok,
|
|
637
|
+
...(hints.length ? { hints } : {}),
|
|
557
638
|
stderr: bridgeResult.stderr,
|
|
558
639
|
logs: bridgeResult.logs,
|
|
559
640
|
response: bridgeResult.responses["1"] || null,
|
|
@@ -601,6 +682,12 @@ export async function inspectFuluOrcaSetup(options: FuluOrcaSetupOptions): Promi
|
|
|
601
682
|
pluginDir: pluginDir || null,
|
|
602
683
|
runtimeDir: runtimeDir || null,
|
|
603
684
|
bridgeCommand: bridgeCommand || null,
|
|
685
|
+
// Linux only: absolute plug-in library paths passed to FULU's host (FULU #51).
|
|
686
|
+
...(platform === "linux" ? { pluginLibraries: {
|
|
687
|
+
...Object.fromEntries(["PJARCZAK_BAMBU_NETWORK_SO", "PJARCZAK_BAMBU_SOURCE_SO"]
|
|
688
|
+
.filter((name) => process.env[name]?.trim()).map((name) => [name, process.env[name]!.trim()])),
|
|
689
|
+
...linuxPluginLibraryEnv(process.env, process.platform),
|
|
690
|
+
} } : {}),
|
|
604
691
|
fileChecks,
|
|
605
692
|
missingRequiredFiles,
|
|
606
693
|
optionalMissingFiles,
|
|
@@ -626,7 +713,12 @@ export async function inspectFuluOrcaSetup(options: FuluOrcaSetupOptions): Promi
|
|
|
626
713
|
"Bambu print start still enforces BAMBU_MODEL before generating or sending project G-code.",
|
|
627
714
|
"Raw bridge print methods are disabled; print_3mf inspects the exact plate, checks the live printer and asks for human confirmation.",
|
|
628
715
|
"The FULU BambuNetwork bridge is a separate runtime from the LAN MQTT/FTPS path; use bridge probing to verify that runtime before relying on cloud behavior.",
|
|
629
|
-
|
|
716
|
+
...(platform === "darwin"
|
|
717
|
+
? ["FULU macOS release bundles may ship the bridge dylib and host shims before the Linux plugin .so payload is present; missing linux-plugin entries mean the slicer can still be tested, but the BambuNetwork bridge is not fully ready."]
|
|
718
|
+
: []),
|
|
719
|
+
...(platform === "linux"
|
|
720
|
+
? ["FULU's v1.0.0 Linux host exits 101 when its stdout is a Node.js socket (FULU #50) and opens the plug-in libraries from its working directory (FULU #51). Pipe the host through cat and set PJARCZAK_BAMBU_PLUGIN_DIR; the server then passes the absolute library paths."]
|
|
721
|
+
: []),
|
|
630
722
|
],
|
|
631
723
|
};
|
|
632
724
|
}
|
package/src/index.ts
CHANGED
|
@@ -270,11 +270,11 @@ const SLICE_TOOL_PROPERTIES = {
|
|
|
270
270
|
description: "Template directory override when resolving template_name (default: BAMBU_TEMPLATE_DIR or ~/Sync/bambu/templates).",
|
|
271
271
|
},
|
|
272
272
|
uptodate: { type: "boolean", description: "Bambu-compatible slicing: refresh 3MF preset configs to the installed slicer version (--uptodate)." },
|
|
273
|
-
repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies (--repetitions)." },
|
|
273
|
+
repetitions: { type: "number", description: "Bambu-compatible slicing: print N identical copies (--repetitions). OrcaSlicer copies an STL with --clone-objects; do not combine with clone_objects." },
|
|
274
274
|
orient: { type: "boolean", description: "Bambu-compatible slicing: auto-orient for printability (--orient)." },
|
|
275
|
-
arrange: { type: "boolean", description: "Bambu-compatible slicing: auto-arrange objects on the plate (--arrange). Set false to keep an existing layout." },
|
|
275
|
+
arrange: { type: "boolean", description: "Bambu-compatible slicing: auto-arrange objects on the plate (--arrange); STL input is arranged by default. Set false to keep an existing layout: an STL that is not already on the plate then fails to slice." },
|
|
276
276
|
ensure_on_bed: { type: "boolean", description: "Bambu-compatible slicing: lower floating models onto the bed (--ensure-on-bed)." },
|
|
277
|
-
clone_objects: { type: "string", description: "Bambu-compatible slicing:
|
|
277
|
+
clone_objects: { type: "string", description: "Bambu-compatible slicing: clone the loaded model, total copies as a single integer, e.g. '3' (--clone-objects). The Bambu-style CLIs count copies per input file and this tool loads one file, so a comma-separated list is rejected by the slicer." },
|
|
278
278
|
skip_objects: { type: "string", description: "Bambu-compatible slicing: comma-separated object indices to skip, e.g. '3,5,10'." },
|
|
279
279
|
enable_timelapse: { type: "boolean", description: "Bambu-compatible slicing: insert timelapse parking moves (--enable-timelapse)." },
|
|
280
280
|
allow_mix_temp: { type: "boolean", description: "Bambu-compatible slicing: allow filaments with different temperature requirements on one plate." },
|
package/src/printers/bambu.ts
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
manualHeatingRequirements,
|
|
13
13
|
publishBambuCommand,
|
|
14
14
|
readFreshPrinterStatus,
|
|
15
|
+
unchippedNozzles,
|
|
15
16
|
validatePrinterState,
|
|
16
17
|
type PrinterStateRequirements,
|
|
17
18
|
} from "../safety/printer-state.js";
|
|
@@ -184,11 +185,14 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
184
185
|
|
|
185
186
|
async confirmPrintPreflight(serial: string, status: any, inspection: PrintFileInspection): Promise<string | undefined> {
|
|
186
187
|
const finishedJob = this.finishedJobIdentity(status);
|
|
188
|
+
const unchipped = unchippedNozzles(status?.raw);
|
|
187
189
|
await this.confirmHardwareAction(
|
|
188
190
|
`Start a checked print on ${inspection.model.toUpperCase()} (${serial})? Nozzles: ${inspection.nozzleDiameters.join(", ")} mm. ` +
|
|
189
191
|
`Materials: ${inspection.materials.join(", ")}. Peak targets: nozzle ${inspection.maxNozzleTemperature}°C, bed ${inspection.maxBedTemperature}°C, chamber ${inspection.maxChamberTemperature}°C. ` +
|
|
190
192
|
`File SHA-256: ${inspection.sha256}. Confirm the physical spool labels and that the build plate is clear.` +
|
|
191
|
-
(finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "")
|
|
193
|
+
(finishedJob !== undefined ? " The printer reports FINISH: remove the previous part and debris before confirming." : "") +
|
|
194
|
+
(unchipped.length ? ` Printer ${unchipped.length === 1 ? `nozzle ${unchipped[0]} reports` : `nozzles ${unchipped.join(" and ")} report`} no identity chip, ` +
|
|
195
|
+
`so ${unchipped.length === 1 ? "its" : "their"} diameter and type are the printer's configured settings; confirm they match the installed ${unchipped.length === 1 ? "hotend" : "hotends"}.` : ""),
|
|
192
196
|
finishedJob !== undefined
|
|
193
197
|
);
|
|
194
198
|
return finishedJob;
|
|
@@ -261,8 +265,12 @@ export class BambuImplementation extends PrinterImplementation {
|
|
|
261
265
|
console.warn("PushAllCommand failed, continuing with cached status", error);
|
|
262
266
|
}
|
|
263
267
|
|
|
264
|
-
|
|
265
|
-
|
|
268
|
+
// The first push after connecting can be a sparse hello (model and
|
|
269
|
+
// modules); the full status follows. Returning before it merges reported
|
|
270
|
+
// status UNKNOWN, so wait for gcode_state within the same 1.5 s bound.
|
|
271
|
+
const deadline = Date.now() + 1500;
|
|
272
|
+
while (typeof printer.data?.gcode_state !== "string" && Date.now() < deadline) {
|
|
273
|
+
await sleep(50);
|
|
266
274
|
}
|
|
267
275
|
|
|
268
276
|
const data = printer.data;
|
|
@@ -50,7 +50,13 @@ export async function requireHumanConfirmation(
|
|
|
50
50
|
physicalCheck = false
|
|
51
51
|
): Promise<void> {
|
|
52
52
|
if (!physicalCheck && confirmationOptOut(scope)) return;
|
|
53
|
-
if (!confirm
|
|
54
|
-
|
|
53
|
+
if (!confirm) throw new Error(CONFIRMATION_UNAVAILABLE);
|
|
54
|
+
if (!(await confirm(message))) {
|
|
55
|
+
throw new Error("Hardware safety confirmation was declined or cancelled in the MCP client. No command was sent. " + (physicalCheck
|
|
56
|
+
? "This confirmation covers a physical check at the printer and always needs a person to accept it."
|
|
57
|
+
: "Some clients decline prompts automatically, for example in a non-interactive permission mode. A deliberately headless " +
|
|
58
|
+
`setup that approves jobs another way can set ${scope === "bambu"
|
|
59
|
+
? "PRINT_REQUIRE_CONFIRMATION=0 (or BAMBU_REQUIRE_CONFIRMATION=0)"
|
|
60
|
+
: "PRINT_REQUIRE_CONFIRMATION=0"} to skip ordinary print and heating prompts.`));
|
|
55
61
|
}
|
|
56
62
|
}
|
|
@@ -148,6 +148,43 @@ function nozzleTypeAndFlow(value: unknown): {type?: string; flow?: string} {
|
|
|
148
148
|
const type = token(value);
|
|
149
149
|
return {type: ["stainlesssteel","hardenedsteel","tungstencarbide","brass"].includes(type) ? type : undefined};
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* A nozzle without an identity chip reports serial "N/A" and keeps the
|
|
153
|
+
* unreliable bit set permanently (stock H2D HS01 nozzles), so its diameter
|
|
154
|
+
* and type are configured settings, as on single-nozzle printers.
|
|
155
|
+
*/
|
|
156
|
+
function hasNoIdentityChip(entry: any): boolean {
|
|
157
|
+
return typeof entry?.sn === "string" && entry.sn.trim().toUpperCase() === "N/A";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Bit 0 (information unreliable) is accepted only from a nozzle without an identity chip. */
|
|
161
|
+
function unreliableWithoutChip(entry: any, stat: number): boolean {
|
|
162
|
+
return (stat & 1) !== 0 && hasNoIdentityChip(entry);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Nozzles whose reported diameter and type are configured settings rather than chip readings. */
|
|
166
|
+
export function unchippedNozzles(raw: unknown): number[] {
|
|
167
|
+
const info = (raw as any)?.device?.nozzle?.info;
|
|
168
|
+
if (!Array.isArray(info)) return [];
|
|
169
|
+
return info.flatMap(entry => {
|
|
170
|
+
const index = finiteNumber(entry?.id);
|
|
171
|
+
const stat = finiteNumber(entry?.stat);
|
|
172
|
+
return index !== undefined && Number.isInteger(index) && index >= 0 && index < 16 &&
|
|
173
|
+
stat !== undefined && Number.isInteger(stat) && stat >= 0 && unreliableWithoutChip(entry, stat) ? [index] : [];
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const loggedUnchippedNozzles = new Set<string>();
|
|
178
|
+
/** Name each unchipped nozzle a check accepted once per printer on stderr, so headless runs still record it. */
|
|
179
|
+
function logUnchippedNozzles(raw: Record<string, any>, serial: string): void {
|
|
180
|
+
for (const index of unchippedNozzles(raw)) {
|
|
181
|
+
const key = `${serial}:${index}`;
|
|
182
|
+
if (loggedUnchippedNozzles.has(key)) continue;
|
|
183
|
+
loggedUnchippedNozzles.add(key);
|
|
184
|
+
console.error(`Printer ${serial || "(unknown serial)"} nozzle ${index} reports no identity chip; its diameter and type are the printer's configured settings.`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
151
188
|
function reportedNozzles(raw: Record<string,any>): ReportedNozzle[] {
|
|
152
189
|
const info = raw.device?.nozzle?.info;
|
|
153
190
|
const output: ReportedNozzle[] = [];
|
|
@@ -159,8 +196,11 @@ function reportedNozzles(raw: Record<string,any>): ReportedNozzle[] {
|
|
|
159
196
|
const diameter = finiteNumber(entry?.diameter);
|
|
160
197
|
if (index === undefined || !Number.isInteger(index) || index < 0 || diameter === undefined || diameter <= 0) throw new Error("Printer reported malformed nozzle identity or diameter.");
|
|
161
198
|
if (entry.stat !== undefined) {
|
|
199
|
+
// BambuStudio DevNozzle: bit 0 set means the information is unreliable;
|
|
200
|
+
// bits 1-2 are 0 normal, 1 abnormal, 2 unknown.
|
|
162
201
|
const stat = finiteNumber(entry.stat);
|
|
163
|
-
if (stat === undefined || !Number.isInteger(stat) || stat < 0 || (stat &
|
|
202
|
+
if (stat === undefined || !Number.isInteger(stat) || stat < 0 || (stat & 6) !== 0) throw new Error(`Printer nozzle ${index} information is unreliable, abnormal or unknown.`);
|
|
203
|
+
if ((stat & 1) !== 0 && !unreliableWithoutChip(entry, stat)) throw new Error(`Printer nozzle ${index} information is unreliable; refresh the nozzle information on the printer.`);
|
|
164
204
|
}
|
|
165
205
|
if (output.some(nozzle => nozzle.index === index)) throw new Error("Printer reported ambiguous duplicate nozzle indices.");
|
|
166
206
|
output.push({index,diameter,...nozzleTypeAndFlow(entry.type)});
|
|
@@ -316,6 +356,7 @@ export function validatePrinterState(status: any, requirements: PrinterStateRequ
|
|
|
316
356
|
filaments.push({position,material,trayIndex,nozzleMin,nozzleMax});
|
|
317
357
|
}
|
|
318
358
|
}
|
|
359
|
+
if (nozzles.length) logUnchippedNozzles(raw, identity.observedSerial ?? String(status.serial ?? ""));
|
|
319
360
|
return {model,reportedNozzles:nozzles,filaments};
|
|
320
361
|
}
|
|
321
362
|
|
package/src/slicer/bambu-cli.ts
CHANGED
|
@@ -33,7 +33,7 @@ export interface BambuSliceOptions {
|
|
|
33
33
|
orient?: boolean; // --orient 1: auto-orient for printability
|
|
34
34
|
arrange?: boolean; // --arrange 1: auto-arrange objects on plate
|
|
35
35
|
ensureOnBed?: boolean; // --ensure-on-bed: lift floating models onto bed
|
|
36
|
-
cloneObjects?: string; // --clone-objects
|
|
36
|
+
cloneObjects?: string; // --clone-objects N: total copies of the loaded model
|
|
37
37
|
skipObjects?: string; // --skip-objects "3,5,10": skip specific objects
|
|
38
38
|
loadFilaments?: string; // "f1.json;f2.json": positional filament profiles
|
|
39
39
|
loadFilamentIds?: string; // --load-filament-ids "1,2,3,1": filament-to-object mapping
|
|
@@ -567,12 +567,91 @@ const PROFILE_METADATA_KEYS = new Set([
|
|
|
567
567
|
"compatible_printers", "compatible_printers_condition", "compatible_prints", "compatible_prints_condition",
|
|
568
568
|
]);
|
|
569
569
|
|
|
570
|
+
/** How the CLI receives the model: Orca STL transforms are applied to a mesh copy first. */
|
|
571
|
+
export interface BambuCliInput {
|
|
572
|
+
slicerType: BambuCliSlicerType;
|
|
573
|
+
is3mf: boolean;
|
|
574
|
+
/** scale and rotations are already applied to the mesh passed to the CLI. */
|
|
575
|
+
meshTransformed: boolean;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
/**
|
|
579
|
+
* Decide how the CLI receives the model. OrcaSlicer's CLI (upstream and FULU)
|
|
580
|
+
* crashes with SIGSEGV in PartPlate::check_outside, which needs the GUI's
|
|
581
|
+
* Plater, when --scale or --rotate-x/-y move an object below the bed while it
|
|
582
|
+
* arranges, the default for STL input. An Orca STL's transforms are therefore
|
|
583
|
+
* applied to a mesh copy; combinations the CLI cannot handle are refused.
|
|
584
|
+
*/
|
|
585
|
+
export function planBambuCliInput(slicerType: BambuCliSlicerType, is3mf: boolean, options: BambuSliceOptions): BambuCliInput {
|
|
586
|
+
const input = { slicerType, is3mf, meshTransformed: false };
|
|
587
|
+
if (slicerType === "bambustudio") return input;
|
|
588
|
+
const tilts = !!options.rotateX || !!options.rotateY;
|
|
589
|
+
if (is3mf) {
|
|
590
|
+
if (options.arrange === true && ((options.scale ?? 1) > 1 || tilts)) {
|
|
591
|
+
throw new Error(
|
|
592
|
+
"OrcaSlicer's CLI crashes when scale above 1, rotate_x or rotate_y moves a 3MF object below the bed while arranging. " +
|
|
593
|
+
"Set arrange to false, or apply the transform in the slicer and export the project."
|
|
594
|
+
);
|
|
595
|
+
}
|
|
596
|
+
return input;
|
|
597
|
+
}
|
|
598
|
+
if (options.repetitions !== undefined && options.cloneObjects) {
|
|
599
|
+
throw new Error("Use either repetitions or clone_objects for an STL with OrcaSlicer, not both.");
|
|
600
|
+
}
|
|
601
|
+
return { ...input, meshTransformed: options.scale !== undefined || tilts };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Bambu Studio and OrcaSlicer CLI errors (libslic3r/Utils.hpp). The process
|
|
606
|
+
* exits with the low byte of the code; on Linux result.json names the error.
|
|
607
|
+
*/
|
|
608
|
+
const BAMBU_CLI_ERRORS: Record<number, string> = {
|
|
609
|
+
[-2]: "invalid parameters to the slicer",
|
|
610
|
+
[-3]: "input files not found",
|
|
611
|
+
[-5]: "preset file cannot be parsed",
|
|
612
|
+
[-6]: "model file cannot be parsed",
|
|
613
|
+
[-9]: "copying objects failed",
|
|
614
|
+
[-17]: "printer is not compatible with the process preset",
|
|
615
|
+
[-21]: "auto-arranging objects failed",
|
|
616
|
+
[-22]: "auto-orienting objects failed",
|
|
617
|
+
[-50]: "a plate is empty or has no object fully inside it; arrange the model or place it on the plate",
|
|
618
|
+
[-51]: "incorrect slicing parameters",
|
|
619
|
+
[-52]: "objects extend over the bed boundary",
|
|
620
|
+
[-60]: "no printable objects remain after skipping",
|
|
621
|
+
[-61]: "filaments are not compatible with the plate type",
|
|
622
|
+
[-62]: "the filaments' temperatures differ too much",
|
|
623
|
+
[-63]: "objects collide in print-by-object mode",
|
|
624
|
+
[-64]: "objects collide",
|
|
625
|
+
[-100]: "slicing the model failed",
|
|
626
|
+
[-101]: "G-code conflicts after slicing",
|
|
627
|
+
[-102]: "G-code in an unprintable area",
|
|
628
|
+
};
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* The CLI's own reason for a failed run: result.json in the run's fresh output
|
|
632
|
+
* directory (written on Linux for handled errors), else the exit-code table.
|
|
633
|
+
*/
|
|
634
|
+
export function bambuCliFailureReason(exitCode: number | null, outputDir: string): string | undefined {
|
|
635
|
+
try {
|
|
636
|
+
const result = JSON.parse(fs.readFileSync(path.join(outputDir, "result.json"), "utf8"));
|
|
637
|
+
if (typeof result?.error_string === "string" && result.error_string.trim()) {
|
|
638
|
+
return `${result.error_string.trim()}${result.return_code !== undefined ? ` [${result.return_code}]` : ""}`;
|
|
639
|
+
}
|
|
640
|
+
} catch {
|
|
641
|
+
// result.json is written only on Linux and only for handled errors.
|
|
642
|
+
}
|
|
643
|
+
if (exitCode === null) return undefined;
|
|
644
|
+
const code = Object.keys(BAMBU_CLI_ERRORS).map(Number).find((value) => value === exitCode || (value & 0xff) === exitCode);
|
|
645
|
+
return code === undefined ? undefined : `${BAMBU_CLI_ERRORS[code]} [${code}]`;
|
|
646
|
+
}
|
|
647
|
+
|
|
570
648
|
/** CLI flags for the prepared bundle and caller options (input path appended by the caller). */
|
|
571
649
|
export function buildBambuCliArgs(
|
|
572
650
|
bundle: BambuSettingsBundle,
|
|
573
651
|
outputDir: string,
|
|
574
652
|
exportName: string,
|
|
575
|
-
options: BambuSliceOptions
|
|
653
|
+
options: BambuSliceOptions,
|
|
654
|
+
input: BambuCliInput
|
|
576
655
|
): string[] {
|
|
577
656
|
const args = [
|
|
578
657
|
"--slice", String(options.slicePlate ?? 0),
|
|
@@ -590,11 +669,17 @@ export function buildBambuCliArgs(
|
|
|
590
669
|
if (options.skipModifiedGcodes) args.push("--skip-modified-gcodes");
|
|
591
670
|
if (options.orient !== undefined) args.push("--orient", options.orient ? "1" : "0");
|
|
592
671
|
if (options.arrange !== undefined) args.push("--arrange", options.arrange ? "1" : "0");
|
|
593
|
-
if (options.repetitions !== undefined)
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
672
|
+
if (options.repetitions !== undefined) {
|
|
673
|
+
// OrcaSlicer rejects --repetitions for one STL; it copies the loaded model with --clone-objects.
|
|
674
|
+
if (input.slicerType !== "bambustudio" && !input.is3mf) args.push("--clone-objects", String(options.repetitions));
|
|
675
|
+
else args.push("--repetitions", String(options.repetitions));
|
|
676
|
+
}
|
|
677
|
+
if (!input.meshTransformed) {
|
|
678
|
+
if (options.scale !== undefined) args.push("--scale", String(options.scale));
|
|
679
|
+
if (options.rotate !== undefined) args.push("--rotate", String(options.rotate));
|
|
680
|
+
if (options.rotateX !== undefined) args.push("--rotate-x", String(options.rotateX));
|
|
681
|
+
if (options.rotateY !== undefined) args.push("--rotate-y", String(options.rotateY));
|
|
682
|
+
}
|
|
598
683
|
if (options.cloneObjects) args.push("--clone-objects", options.cloneObjects);
|
|
599
684
|
if (options.skipObjects) args.push("--skip-objects", options.skipObjects);
|
|
600
685
|
if (bundle.filamentPaths.length > 0) {
|
package/src/slicer/run-slicer.ts
CHANGED
|
@@ -11,6 +11,8 @@ export interface SlicerRunOptions {
|
|
|
11
11
|
slicerType: string;
|
|
12
12
|
/** Polled while the slicer runs; returning true kills it. */
|
|
13
13
|
isCancelled?: () => boolean;
|
|
14
|
+
/** The slicer's own reason for a nonzero exit, read before its output directory is removed. */
|
|
15
|
+
failureReason?: (exitCode: number | null) => string | undefined;
|
|
14
16
|
}
|
|
15
17
|
|
|
16
18
|
export interface SlicerRunResult {
|
|
@@ -51,7 +53,7 @@ function describeOutput(stdoutTail: string, stderrTail: string): string {
|
|
|
51
53
|
* cancellation rejects with a SlicerError that includes the evidence.
|
|
52
54
|
*/
|
|
53
55
|
export function runSlicerProcess(options: SlicerRunOptions): Promise<SlicerRunResult> {
|
|
54
|
-
const { command, args, timeoutMs, slicerType, isCancelled } = options;
|
|
56
|
+
const { command, args, timeoutMs, slicerType, isCancelled, failureReason } = options;
|
|
55
57
|
return new Promise((resolve, reject) => {
|
|
56
58
|
const stdout = new TailBuffer();
|
|
57
59
|
const stderr = new TailBuffer();
|
|
@@ -107,9 +109,10 @@ export function runSlicerProcess(options: SlicerRunOptions): Promise<SlicerRunRe
|
|
|
107
109
|
: signal
|
|
108
110
|
? `Slicer was terminated by signal ${signal}`
|
|
109
111
|
: `Slicer exited with code ${code}`;
|
|
112
|
+
const cliReason = timedOut || cancelled ? undefined : failureReason?.(code);
|
|
110
113
|
settle(() => reject(new SlicerError(
|
|
111
114
|
"execution",
|
|
112
|
-
`${reason} (${slicerType}: ${command}).\n${describeOutput(stdoutTail, stderrTail)}`,
|
|
115
|
+
`${reason} (${slicerType}: ${command})${cliReason ? `: ${cliReason}` : ""}.\n${describeOutput(stdoutTail, stderrTail)}`,
|
|
113
116
|
{
|
|
114
117
|
slicerType,
|
|
115
118
|
slicerPath: command,
|