src / utils.ts
import { createReadStream } from "fs";
import { exec, ExecOptions } from "child_process";
import { execSync } from "child_process";
// ---------------------------------------------------------------------------
// CLI detection
// ---------------------------------------------------------------------------
export interface CliDetectionResult {
found: boolean;
version?: string;
error?: string;
}
export function detectCli(cliPath: string): CliDetectionResult {
try {
const out = execSync(`"${cliPath}" --version`, { timeout: 5000 })
.toString()
.trim();
return { found: true, version: out };
} catch (e: any) {
return { found: false, error: e.message };
}
}
// ---------------------------------------------------------------------------
// Shell command runner
// ---------------------------------------------------------------------------
export interface ExecResult {
stdout: string;
stderr: string;
error: Error | null;
timedOut: boolean;
}
export function runCommand(
command: string,
options: ExecOptions,
timeoutMs: number,
signal?: AbortSignal
): Promise<ExecResult> {
return new Promise((resolve) => {
let timedOut = false;
const timer = setTimeout(() => {
timedOut = true;
child.kill("SIGTERM");
resolve({ stdout: "", stderr: "Command timed out.", error: new Error("SIGTERM"), timedOut: true });
}, timeoutMs);
const child = exec(
command,
{ ...options },
(error, stdout, stderr) => {
clearTimeout(timer);
if (timedOut) return; // timeout již vyřešil Promise
resolve({
stdout: stdout.trim(),
stderr: stderr.trim(),
error,
timedOut: false,
});
}
);
// Propagate AbortSignal
signal?.addEventListener("abort", () => {
clearTimeout(timer);
child.kill("SIGTERM");
resolve({ stdout: "", stderr: "Aborted.", error: new Error("Aborted"), timedOut: false });
});
});
}
// ---------------------------------------------------------------------------
// Build draw-things-cli generate command
// ---------------------------------------------------------------------------
export interface GenerateParams {
cliPath: string;
model: string;
prompt: string;
negativePrompt?: string;
width: number;
height: number;
steps: number;
cfg: number;
seed: number;
outputPath: string;
}
export function buildGenerateCommand(p: GenerateParams): string {
const escape = (s: string) => s.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
const parts = [
`"${p.cliPath}" generate`,
`--model "${escape(p.model)}"`,
`--prompt "${escape(p.prompt)}"`,
];
if (p.negativePrompt && p.negativePrompt.trim()) {
parts.push(`--negative-prompt "${escape(p.negativePrompt)}"`);
}
parts.push(
`--width ${p.width}`,
`--height ${p.height}`,
`--steps ${p.steps}`,
`--cfg ${p.cfg}`,
);
// Current CLI refuses negative numbers as value of the --seed flag (probably parsing it as new flag)
// Seed < 0 = omit --seed completely, CLI hopefully uses a random seed in this case
if (p.seed >= 0) {
parts.push(`--seed ${p.seed}`);
}
parts.push(`--output "${escape(p.outputPath)}"`);
return parts.join(" ");
}
// ---------------------------------------------------------------------------
// PNG → JPEG conversion (pure JS, no native dependencies)
// pngjs + jpeg-js — no postinstall scripts
// ---------------------------------------------------------------------------
export async function convertPngToJpeg(
pngPath: string,
quality: number // 0–100
): Promise<Buffer> {
const { PNG } = await import("pngjs");
const jpeg = await import("jpeg-js");
const fs = await import("fs");
const pngBuffer = fs.readFileSync(pngPath);
const png = PNG.sync.read(pngBuffer);
// jpeg-js quality: 0–100
const encoded = jpeg.encode(
{ data: png.data, width: png.width, height: png.height },
quality
);
return encoded.data;
}
// ---------------------------------------------------------------------------
// Parse output path from draw-things-cli stdout
// CLI prints: "Wrote: /path/to/file.png"
// ---------------------------------------------------------------------------
export function parseOutputPath(stdout: string): string | null {
const match = stdout.match(/^Wrote:\s+(.+\.png)$/m);
return match ? match[1].trim() : null;
}