abf0e694a2
Hetzner Agent E2E / Provision + deploy + healthcheck (push) Has been cancelled
Hetzner Agent E2E / Teardown (push) Has been cancelled
Scenario PR E2E / Classify changed paths (push) Has been cancelled
Scenario PR E2E / Zero-Key unit + UI coverage (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser core (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser view lifecycle (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser all-pages (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser feature interactions (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser cloud keyless (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser ratcheted (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser auto-discovered specs (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser dashboard device matrix (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser Pixel-7 real-touch lane (push) Has been cancelled
Scenario PR E2E / Zero-Key app browser WebKit lane (push) Has been cancelled
Scenario PR E2E / Zero-Key accounts UI e2e (real API + pool + disk) (push) Has been cancelled
Scenario PR E2E / Zero-Key full-walkthrough (mock lane) (push) Has been cancelled
Scenario PR E2E / Zero-Key app diagnostics (push) Has been cancelled
Scenario PR E2E / Zero-Key scenario runner E2E (push) Has been cancelled
Scenario PR E2E / Zero-Key Deterministic E2E (push) Has been cancelled
Docker CI Smoke / Classify changed paths (push) Has been cancelled
Docker CI Smoke / Build production Docker image (+ smoke boot) (push) Has been cancelled
Build libelizainference (Android, FFI) / prepare-matrix (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Orchestrator multi-account / multi-account selection e2e (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Tests / Classify changed paths (push) Has been cancelled
UI Extended Fixture E2E / fixture-e2e (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
CUDA Coverage Continuity / Fail-closed manifest contracts (push) Has been cancelled
CUDA Coverage Continuity / Exact-head CUDA fixtures + runtime graph (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Homepage / build-and-deploy (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
Tests / Remote Capability Provider Live E2E (push) Has been cancelled
Training Stack / CPU smoke (lint + import) (push) Has been cancelled
Training Stack / GPU build (QJL nvcc + Triton JIT) (push) Has been cancelled
UI Core Fixture E2E / ui-core-fixture-e2e (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/core typecheck bun run --cwd packages/shared typecheck bun run --cwd packages/cloud/shared typecheck bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-coding-tools build node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=1 node packages/scripts/run-python.mjs --version node packages… (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Tests / Test-runner vacuous-green guard (push) Has been cancelled
Tests / Server Tests (push) Has been cancelled
Tests / Client Tests (push) Has been cancelled
Tests / Plugin Tests (1/4) (push) Has been cancelled
Tests / Plugin Tests (2/4) (push) Has been cancelled
Tests / Plugin Tests (3/4) (push) Has been cancelled
Tests / Plugin Tests (4/4) (push) Has been cancelled
Tests / Plugin Tests (push) Has been cancelled
Tests / Integration Lane (personal-assistant) (push) Has been cancelled
Tests / Electrobun Desktop Contract (push) Has been cancelled
Tests / Zero-Key unit + UI coverage (push) Has been cancelled
Tests / Zero-Key app browser (push) Has been cancelled
Tests / Zero-Key diagnostics (push) Has been cancelled
Tests / Zero-Key scenario runner (push) Has been cancelled
Tests / Zero-Key harness E2E (push) Has been cancelled
Tests / Zero-Key Deterministic E2E (push) Has been cancelled
Build libelizainference (Android, FFI) / ${{ matrix.abi }} (push) Has been cancelled
Tests / Cloud Live E2E (Eliza Cloud) (push) Has been cancelled
Tests / Remote Capability GitHub Live Artifact Validator (push) Has been cancelled
Tests / Script Tests (Linux) (push) Has been cancelled
Tests / Merge Queue Quality Gate (push) Has been cancelled
Tests / ci-ok (push) Has been cancelled
290 lines
9.0 KiB
JavaScript
Executable File
290 lines
9.0 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
/**
|
|
* probe-sd-cpp.mjs — WS3 image-gen onboarding probe.
|
|
*
|
|
* Detects whether stable-diffusion.cpp's `sd` binary is reachable on this
|
|
* host. Used by:
|
|
*
|
|
* - First-run / Settings → Image Generation onboarding to surface a
|
|
* clean "available: yes/no" badge alongside the per-platform install
|
|
* instructions.
|
|
* - `__tests__/imagegen-sd-cpp-probe.test.ts`, which forks this script
|
|
* to confirm the absent-binary path reports a structured failure
|
|
* instead of crashing.
|
|
* - CI bundle-prep step (Linux runners): the build matrix can record
|
|
* the version + supported models so the validator can cross-check
|
|
* `ELIZA_1_BUNDLE_EXTRAS.json` against what the binary actually
|
|
* accepts.
|
|
*
|
|
* Binary resolution order (matches `services/imagegen/sd-cpp.ts`):
|
|
*
|
|
* 1. `process.env.SD_CPP_BIN` (operator override).
|
|
* 2. `sd` on PATH.
|
|
*
|
|
* Output: a single JSON object on stdout, with `available` always
|
|
* present:
|
|
*
|
|
* { available: true, binary, version, supportedModels, accelerators }
|
|
* { available: false, binary, reason, hint }
|
|
*
|
|
* Exit code:
|
|
* 0 — the script ran cleanly, regardless of `available`. The caller
|
|
* inspects `available` to decide whether to enable image-gen.
|
|
* 1 — the probe itself crashed (e.g. JSON.stringify on a circular,
|
|
* which should not happen).
|
|
*
|
|
* The list of "supported models" is the same per-tier set the bundle
|
|
* extras file ships (sd-1.5 Q5_0, sdxl-turbo Q4_0, z-image-turbo Q4_K_M,
|
|
* flux-1-schnell Q4_K_M). We do not run the binary against each model —
|
|
* that's a real inference workload. The probe only confirms the binary
|
|
* exists and reports its `--version` line.
|
|
*/
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { readFile } from "node:fs/promises";
|
|
import { basename, dirname, join } from "node:path";
|
|
import process from "node:process";
|
|
|
|
const ARG_JSON = process.argv.includes("--json");
|
|
const ARG_HUMAN = process.argv.includes("--human");
|
|
const REQUIRED_ACCELERATOR = resolveRequiredAccelerator();
|
|
|
|
const SUPPORTED_MODELS = [
|
|
"imagegen-sd-1_5-q5_0",
|
|
"imagegen-sdxl-turbo-q4_0",
|
|
"imagegen-z-image-turbo-q4_k_m",
|
|
"imagegen-flux-1-schnell-q4_k_m",
|
|
];
|
|
|
|
// `auto` and `cpu` are always legal sd-cpp modes. GPU accelerators are added
|
|
// only when the binary manifest, --help, or --version proves support.
|
|
const BASE_ACCELERATORS = ["auto", "cpu"];
|
|
|
|
function resolveRequiredAccelerator() {
|
|
const raw = process.env.ELIZA_IMAGEGEN_ACCELERATOR;
|
|
if (typeof raw !== "string" || !raw.trim()) return null;
|
|
const value = raw.trim().toLowerCase();
|
|
return isAccelerator(value) || value === "auto" ? value : null;
|
|
}
|
|
|
|
function resolveBinary() {
|
|
const fromEnv = process.env.SD_CPP_BIN;
|
|
if (typeof fromEnv === "string" && fromEnv.trim()) return fromEnv.trim();
|
|
return "sd";
|
|
}
|
|
|
|
function runCommand(binary, args) {
|
|
return new Promise((resolve) => {
|
|
let stdout = "";
|
|
let stderr = "";
|
|
let proc;
|
|
try {
|
|
proc = spawn(binary, args);
|
|
} catch (err) {
|
|
resolve({ ok: false, code: null, error: err, stdout: "", stderr: "" });
|
|
return;
|
|
}
|
|
proc.stdout?.on("data", (b) => {
|
|
stdout += b.toString("utf8");
|
|
});
|
|
proc.stderr?.on("data", (b) => {
|
|
stderr += b.toString("utf8");
|
|
});
|
|
proc.on("error", (err) => {
|
|
resolve({ ok: false, code: null, error: err, stdout, stderr });
|
|
});
|
|
proc.on("exit", (code) => {
|
|
resolve({ ok: code === 0, code, stdout, stderr });
|
|
});
|
|
});
|
|
}
|
|
|
|
function runVersion(binary) {
|
|
return runCommand(binary, ["--version"]);
|
|
}
|
|
|
|
function parseVersionLine(stdout, stderr) {
|
|
const text = (stdout || stderr || "").trim();
|
|
if (!text) return null;
|
|
// stable-diffusion.cpp prints lines like `sd master-xxxxxxx` or
|
|
// `stable-diffusion.cpp v1.0.0`. We don't try to be clever — just
|
|
// return the first non-empty line so onboarding can show it verbatim.
|
|
const firstLine = text.split(/\r?\n/).find((l) => l.trim().length > 0);
|
|
return firstLine?.trim() ?? null;
|
|
}
|
|
|
|
async function probeCapabilities(binary, versionResult) {
|
|
const helpResult = await runCommand(binary, ["--help"]).catch(() => null);
|
|
const manifestAccelerators = await readCapabilityManifest(binary);
|
|
const textEvidence = [
|
|
versionResult.stdout,
|
|
versionResult.stderr,
|
|
helpResult?.stdout ?? "",
|
|
helpResult?.stderr ?? "",
|
|
].join("\n");
|
|
const accelerators = new Set(BASE_ACCELERATORS);
|
|
const evidence = [];
|
|
for (const accelerator of manifestAccelerators) {
|
|
accelerators.add(accelerator);
|
|
evidence.push("manifest");
|
|
}
|
|
if (hasPositiveCudaEvidence(textEvidence)) {
|
|
accelerators.add("cuda");
|
|
evidence.push("help_or_version");
|
|
}
|
|
if (/\b(sd_vulkan|ggml_vulkan|vulkan)\b/i.test(textEvidence)) {
|
|
accelerators.add("vulkan");
|
|
}
|
|
if (/\b(sd_metal|ggml_metal|metal)\b/i.test(textEvidence)) {
|
|
accelerators.add("metal");
|
|
}
|
|
return { accelerators: [...accelerators], evidence: [...new Set(evidence)] };
|
|
}
|
|
|
|
async function readCapabilityManifest(binary) {
|
|
const candidates = [
|
|
`${binary}.json`,
|
|
`${binary}.manifest.json`,
|
|
join(dirname(binary), `${basename(binary)}.manifest.json`),
|
|
join(dirname(binary), "sd-cpp.manifest.json"),
|
|
join(dirname(binary), "manifest.json"),
|
|
];
|
|
for (const candidate of [...new Set(candidates)]) {
|
|
try {
|
|
return extractAccelerators(JSON.parse(await readFile(candidate, "utf8")));
|
|
} catch {
|
|
// Optional sidecar; help/version may still prove capabilities.
|
|
}
|
|
}
|
|
return [];
|
|
}
|
|
|
|
function extractAccelerators(value) {
|
|
const found = new Set();
|
|
const visit = (node) => {
|
|
if (Array.isArray(node)) {
|
|
for (const item of node) visit(item);
|
|
return;
|
|
}
|
|
if (typeof node === "string") {
|
|
const normalized = node.toLowerCase();
|
|
if (isAccelerator(normalized)) found.add(normalized);
|
|
return;
|
|
}
|
|
if (!node || typeof node !== "object") return;
|
|
for (const [key, child] of Object.entries(node)) {
|
|
const normalizedKey = key.toLowerCase();
|
|
if (isAccelerator(normalizedKey) && child === true) {
|
|
found.add(normalizedKey);
|
|
}
|
|
visit(child);
|
|
}
|
|
};
|
|
visit(value);
|
|
return [...found];
|
|
}
|
|
|
|
function isAccelerator(value) {
|
|
return value === "cuda" || value === "vulkan" || value === "metal" || value === "cpu";
|
|
}
|
|
|
|
function hasPositiveCudaEvidence(text) {
|
|
const lower = text.toLowerCase();
|
|
if (
|
|
/(without|no|disabled|disable|not built with|unsupported)[^\n]{0,40}cuda/.test(
|
|
lower,
|
|
)
|
|
) {
|
|
return false;
|
|
}
|
|
return /(^|[^a-z0-9])(sd_cuda|ggml_cuda|cublas|cudart)([^a-z0-9]|$)/.test(lower);
|
|
}
|
|
|
|
async function main() {
|
|
const binary = resolveBinary();
|
|
const versionResult = await runVersion(binary);
|
|
|
|
if (!versionResult.ok) {
|
|
const reason =
|
|
versionResult.error?.code === "ENOENT" ||
|
|
versionResult.code === null
|
|
? "binary_missing"
|
|
: "binary_version_mismatch";
|
|
const hint =
|
|
reason === "binary_missing"
|
|
? `Install stable-diffusion.cpp ('git clone https://github.com/leejet/stable-diffusion.cpp && make -j') and set SD_CPP_BIN=/path/to/sd, or let the bundle installer place '${binary}' on PATH.`
|
|
: `'${binary} --version' exited with code ${versionResult.code}. The probe expected exit 0. Check the binary build flags.`;
|
|
emit({
|
|
available: false,
|
|
binary,
|
|
reason,
|
|
hint,
|
|
});
|
|
return;
|
|
}
|
|
|
|
const capabilities = await probeCapabilities(binary, versionResult);
|
|
if (
|
|
REQUIRED_ACCELERATOR &&
|
|
!capabilities.accelerators.includes(REQUIRED_ACCELERATOR)
|
|
) {
|
|
emit({
|
|
available: false,
|
|
binary,
|
|
version: parseVersionLine(versionResult.stdout, versionResult.stderr),
|
|
supportedModels: SUPPORTED_MODELS,
|
|
accelerators: capabilities.accelerators,
|
|
evidence: capabilities.evidence,
|
|
requiredAccelerator: REQUIRED_ACCELERATOR,
|
|
reason: `${REQUIRED_ACCELERATOR}_missing`,
|
|
hint: `'${binary}' is available but does not prove ${REQUIRED_ACCELERATOR} support via manifest, --help, or --version. Use an sd-cpp build for that accelerator or clear ELIZA_IMAGEGEN_ACCELERATOR.`,
|
|
});
|
|
return;
|
|
}
|
|
emit({
|
|
available: true,
|
|
binary,
|
|
version: parseVersionLine(versionResult.stdout, versionResult.stderr),
|
|
supportedModels: SUPPORTED_MODELS,
|
|
accelerators: capabilities.accelerators,
|
|
evidence: capabilities.evidence,
|
|
requiredAccelerator: REQUIRED_ACCELERATOR ?? undefined,
|
|
});
|
|
}
|
|
|
|
function emit(payload) {
|
|
if (ARG_HUMAN && !ARG_JSON) {
|
|
const lines = [];
|
|
lines.push(`available: ${payload.available ? "yes" : "no"}`);
|
|
lines.push(`binary: ${payload.binary}`);
|
|
if (payload.available) {
|
|
if (payload.version) lines.push(`version: ${payload.version}`);
|
|
lines.push(
|
|
`supported models: ${(payload.supportedModels ?? []).join(", ")}`,
|
|
);
|
|
lines.push(
|
|
`accelerators: ${(payload.accelerators ?? []).join(", ")}`,
|
|
);
|
|
if (payload.requiredAccelerator) {
|
|
lines.push(`required accelerator: ${payload.requiredAccelerator}`);
|
|
}
|
|
} else {
|
|
lines.push(`reason: ${payload.reason}`);
|
|
if (payload.requiredAccelerator) {
|
|
lines.push(`required accelerator: ${payload.requiredAccelerator}`);
|
|
}
|
|
if (payload.hint) lines.push(`hint: ${payload.hint}`);
|
|
}
|
|
process.stdout.write(`${lines.join("\n")}\n`);
|
|
return;
|
|
}
|
|
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
}
|
|
|
|
main().catch((err) => {
|
|
process.stderr.write(
|
|
`probe-sd-cpp: unexpected failure: ${err instanceof Error ? err.message : String(err)}\n`,
|
|
);
|
|
process.exit(1);
|
|
});
|