From e0a7e6d0e0cc6d636f045015a8d9adc3af6a4470 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Kuty=C5=82a?= Date: Fri, 10 Jul 2026 10:30:50 +0200 Subject: [PATCH 1/4] fix(voice): surface real whisper provisioning failure in errors On macOS/Linux the local provider claimed 'Prebuilt binary download was attempted but failed' even though no prebuilt binaries exist there and none was attempted. The actual failure (CMake missing, build error) was only console.warn'd and ensureWhisperBinary's result was discarded, so users got a misleading generic hint. - buildToolsHint(platform): platform-aware guidance, exported for tests - ensureWhisperBinary/buildFromSource return {ok, reason} with the real cause (incl. truncated compiler stderr) - transcribe() fails fast with that reason instead of letting nodejs-whisper throw its generic 'executable not found' --- .../src/providers/local-whisper.hint.test.ts | 37 ++++++ src/apps/voice/src/providers/local-whisper.ts | 125 ++++++++++++------ 2 files changed, 123 insertions(+), 39 deletions(-) create mode 100644 src/apps/voice/src/providers/local-whisper.hint.test.ts diff --git a/src/apps/voice/src/providers/local-whisper.hint.test.ts b/src/apps/voice/src/providers/local-whisper.hint.test.ts new file mode 100644 index 0000000..3d8077e --- /dev/null +++ b/src/apps/voice/src/providers/local-whisper.hint.test.ts @@ -0,0 +1,37 @@ +import { describe, expect, it } from 'vitest'; +import { buildToolsHint } from './local-whisper.js'; + +// The provisioning-failure hint must describe what actually happened on the +// current platform. Prebuilt binaries only exist for Windows — claiming a +// download was attempted on macOS/Linux sends users down the wrong path. +describe('buildToolsHint', () => { + it('darwin: explains source build failed, never claims a prebuilt download', () => { + const hint = buildToolsHint('darwin'); + expect(hint).not.toContain('Prebuilt binary download'); + expect(hint).toContain('from source'); + expect(hint).toContain('brew install cmake'); + expect(hint).not.toContain('winget'); + }); + + it('win32: mentions the prebuilt download attempt and Windows build tools', () => { + const hint = buildToolsHint('win32'); + expect(hint).toContain('Prebuilt binary download'); + expect(hint).toContain('winget install Kitware.CMake'); + expect(hint).not.toContain('brew install'); + }); + + it('linux: explains source build failed with apt instructions', () => { + const hint = buildToolsHint('linux'); + expect(hint).not.toContain('Prebuilt binary download'); + expect(hint).toContain('build-essential'); + }); + + it('always includes the manual compile steps', () => { + for (const platform of ['darwin', 'win32', 'linux'] as const) { + const hint = buildToolsHint(platform); + expect(hint).toContain('cd node_modules/nodejs-whisper/cpp/whisper.cpp'); + expect(hint).toContain('cmake -B build'); + expect(hint).toContain('cmake --build build --config Release'); + } + }); +}); diff --git a/src/apps/voice/src/providers/local-whisper.ts b/src/apps/voice/src/providers/local-whisper.ts index aaf3ffc..43f1e0b 100644 --- a/src/apps/voice/src/providers/local-whisper.ts +++ b/src/apps/voice/src/providers/local-whisper.ts @@ -66,25 +66,44 @@ const FFMPEG_INSTALL_HINT = " macOS: brew install ffmpeg\n" + " Linux: sudo apt install ffmpeg (or your distro's package manager)"; -const BUILD_TOOLS_HINT = - "The local whisper provider uses nodejs-whisper which needs whisper.cpp.\n" + - "Prebuilt binary download was attempted but failed.\n" + - "\n" + - "To compile manually:\n" + - "\n" + - "Step 1 — Install build tools (if not already installed):\n" + - " Windows: winget install Kitware.CMake\n" + - " winget install Microsoft.VisualStudio.2022.BuildTools --override \"--add Microsoft.VisualStudio.Workload.VCTools\"\n" + - " macOS: xcode-select --install\n" + - " brew install cmake (Command Line Tools do NOT include CMake)\n" + - " Linux: sudo apt install build-essential cmake\n" + - "\n" + - "Step 2 — Compile whisper.cpp (from project root):\n" + - " cd node_modules/nodejs-whisper/cpp/whisper.cpp\n" + - " cmake -B build\n" + - " cmake --build build --config Release\n" + - "\n" + - "Then restart the server."; +/** + * Platform-aware guidance for when the whisper-cli binary could not be + * provisioned. Prebuilt binaries only exist for Windows — on macOS/Linux the + * only automatic path is a CMake source build, so the hint must not claim a + * download was attempted there. + */ +export function buildToolsHint(platform: NodeJS.Platform = process.platform): string { + const attempted = + platform === "win32" + ? "Prebuilt binary download was attempted but failed." + : "Building whisper.cpp from source was attempted but failed (no prebuilt binaries are published for this platform)."; + + const installTools = + platform === "win32" + ? " Windows: winget install Kitware.CMake\n" + + " winget install Microsoft.VisualStudio.2022.BuildTools --override \"--add Microsoft.VisualStudio.Workload.VCTools\"\n" + : platform === "darwin" + ? " macOS: xcode-select --install\n" + + " brew install cmake (Command Line Tools do NOT include CMake)\n" + : " Linux: sudo apt install build-essential cmake\n"; + + return ( + "The local whisper provider uses nodejs-whisper which needs whisper.cpp.\n" + + `${attempted}\n` + + "\n" + + "To compile manually:\n" + + "\n" + + "Step 1 — Install build tools (if not already installed):\n" + + installTools + + "\n" + + "Step 2 — Compile whisper.cpp (from project root):\n" + + " cd node_modules/nodejs-whisper/cpp/whisper.cpp\n" + + " cmake -B build\n" + + " cmake --build build --config Release\n" + + "\n" + + "Then restart the server." + ); +} // --- Prebuilt whisper.cpp binary download --- @@ -224,16 +243,24 @@ function cmakeAvailable(): boolean { } } +/** Outcome of an attempt to provision the whisper-cli binary. */ +type WhisperBinaryStatus = { ok: true } | { ok: false; reason: string }; + +/** Keep only the tail of long build output — compiler errors are at the end. */ +function truncateBuildOutput(text: string, maxChars = 1500): string { + return text.length <= maxChars ? text : `…${text.slice(-maxChars)}`; +} + /** Attempt to build whisper.cpp from source using CMake. */ -function buildFromSource(whisperCppPath: string): boolean { +function buildFromSource(whisperCppPath: string): WhisperBinaryStatus { if (!cmakeAvailable()) { - console.warn( - "[devglide-voice] CMake not found on PATH — cannot build whisper.cpp.\n" + + const reason = + "CMake not found on PATH — cannot build whisper.cpp.\n" + " macOS: brew install cmake (xcode-select --install alone is not enough)\n" + " Windows: winget install Kitware.CMake\n" + - " Linux: sudo apt install cmake" - ); - return false; + " Linux: sudo apt install cmake"; + console.warn(`[devglide-voice] ${reason}`); + return { ok: false, reason }; } try { @@ -256,15 +283,18 @@ function buildFromSource(whisperCppPath: string): boolean { if (whisperCliExists(whisperCppPath)) { console.log("[devglide-voice] whisper.cpp built successfully."); - return true; + return { ok: true }; } - console.warn("[devglide-voice] Build completed but whisper-cli not found."); - return false; + const reason = "CMake build completed but whisper-cli was not found in the build output."; + console.warn(`[devglide-voice] ${reason}`); + return { ok: false, reason }; } catch (err: unknown) { + // execSync errors already carry the command's stderr in their message const msg = err instanceof Error ? err.message : String(err); - console.warn(`[devglide-voice] CMake build failed: ${msg}`); - return false; + const reason = `CMake build failed: ${truncateBuildOutput(msg)}`; + console.warn(`[devglide-voice] ${reason}`); + return { ok: false, reason }; } } @@ -275,13 +305,14 @@ function buildFromSource(whisperCppPath: string): boolean { * 3. Fall back to building from source via CMake (macOS/Linux have compilers * readily available; Windows will reach this if the prebuilt download fails). */ -async function ensureWhisperBinary(): Promise { +async function ensureWhisperBinary(): Promise { const whisperCppPath = getWhisperCppPath(); // Already available — nothing to do - if (whisperCliExists(whisperCppPath)) return true; + if (whisperCliExists(whisperCppPath)) return { ok: true }; // Step 1: Try prebuilt binary (Windows only — no official macOS/Linux binaries) + let downloadFailure: string | undefined; const asset = getPrebuiltAsset(); if (asset) { console.log(`[devglide-voice] whisper-cli not found — downloading prebuilt binary (${WHISPER_CPP_RELEASE_VERSION})…`); @@ -312,13 +343,15 @@ async function ensureWhisperBinary(): Promise { if (whisperCliExists(whisperCppPath)) { console.log("[devglide-voice] Prebuilt whisper-cli installed successfully."); - return true; + return { ok: true }; } - console.warn("[devglide-voice] Prebuilt binary extracted but not found — falling back to CMake build."); + downloadFailure = "Prebuilt binary extracted but whisper-cli was not found."; + console.warn(`[devglide-voice] ${downloadFailure} Falling back to CMake build…`); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); - console.warn(`[devglide-voice] Failed to download prebuilt binary: ${msg}`); + downloadFailure = `Failed to download prebuilt binary: ${msg}`; + console.warn(`[devglide-voice] ${downloadFailure}`); console.warn("[devglide-voice] Falling back to CMake build…"); } finally { try { unlinkSync(tmpZip); } catch { /* ignore */ } @@ -326,7 +359,12 @@ async function ensureWhisperBinary(): Promise { } // Step 2: Build from source via CMake - return buildFromSource(whisperCppPath); + const built = buildFromSource(whisperCppPath); + if (built.ok) return built; + return { + ok: false, + reason: [downloadFailure, built.reason].filter(Boolean).join("\n"), + }; } /** Check whether ffmpeg is available on PATH. */ @@ -382,8 +420,17 @@ export class LocalWhisperProvider implements TranscriptionProvider { throw new Error(ffmpeg.error!); } - // Ensure whisper-cli binary is available (download prebuilt if possible) - await ensureWhisperBinary(); + // Ensure whisper-cli binary is available (download prebuilt if possible). + // Fail fast with the real provisioning error — nodejs-whisper checks the + // same executable locations and would otherwise fail with a generic + // "whisper-cli executable not found" that hides the actual cause. + const binary = await ensureWhisperBinary(); + if (!binary.ok) { + throw new Error( + `Local whisper transcription failed — whisper-cli is not available.\n` + + `${binary.reason}\n\n${buildToolsHint()}` + ); + } // Write audio buffer to a temp file (nodejs-whisper needs a file path). // Sanitize the caller-supplied filename (strip path components, null bytes, @@ -426,7 +473,7 @@ export class LocalWhisperProvider implements TranscriptionProvider { // Detect whisper binary / build tool issues and provide actionable guidance if (/whisper.*not found|executable not found|ENOENT|cmake|build|compile/i.test(msg)) { throw new Error( - `Local whisper transcription failed: ${msg}\n\n${BUILD_TOOLS_HINT}` + `Local whisper transcription failed: ${msg}\n\n${buildToolsHint()}` ); } throw err; From 9ed498f8bf2046b63ee01457dc8055de84f7380c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Kuty=C5=82a?= Date: Fri, 10 Jul 2026 10:45:50 +0200 Subject: [PATCH 2/4] feat(voice): adopt system whisper-cli from PATH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A whisper-cli already installed on PATH (e.g. brew install whisper-cpp on macOS) is now copied into the nodejs-whisper tree before any prebuilt download or CMake build is attempted — local STT works with zero compilation. The macOS build hint now recommends this as the fastest path. --- CLAUDE.md | 12 +++-- bin/claude-md-template.js | 2 +- src/apps/voice/src/mcp.ts | 2 +- .../src/providers/local-whisper.adopt.test.ts | 52 ++++++++++++++++++ .../src/providers/local-whisper.hint.test.ts | 5 ++ src/apps/voice/src/providers/local-whisper.ts | 53 +++++++++++++++++-- 6 files changed, 118 insertions(+), 8 deletions(-) create mode 100644 src/apps/voice/src/providers/local-whisper.adopt.test.ts diff --git a/CLAUDE.md b/CLAUDE.md index 90243ae..05e245e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -154,12 +154,18 @@ broken on some Windows 11 builds (stuck in `playState 9`). ### Local Whisper (whisper.cpp) -The local STT provider uses whisper.cpp via the `nodejs-whisper` package: +The local STT provider uses whisper.cpp via the `nodejs-whisper` package. +Binary provisioning order in `ensureWhisperBinary()`: +- **All platforms:** A `whisper-cli` already on PATH (e.g. `brew install + whisper-cpp` on macOS) is adopted automatically — copied into the + nodejs-whisper tree, no compile needed. - **Windows:** Prebuilt `whisper-cli` binary is auto-downloaded from GitHub releases (v1.8.3). -- **macOS/Linux:** Built from source via CMake. On macOS, Xcode Command Line - Tools alone are not enough — CMake must be installed separately +- **macOS/Linux fallback:** Built from source via CMake. On macOS, Xcode + Command Line Tools alone are not enough — CMake must be installed separately (`brew install cmake`). - **All platforms:** Requires FFmpeg on PATH for audio conversion. +- `devglide setup` ends with a voice dependency doctor (`bin/doctor.js`) + reporting ffmpeg / whisper-cli / cmake status with install hints. - **Bundling:** `nodejs-whisper` must stay in the esbuild `external` list in `scripts/build-mcp.mjs` (and in root `package.json` dependencies so the bundle can resolve it). Inlining it breaks `__dirname` resolution and the diff --git a/bin/claude-md-template.js b/bin/claude-md-template.js index 658a831..8dc4dd2 100644 --- a/bin/claude-md-template.js +++ b/bin/claude-md-template.js @@ -81,7 +81,7 @@ Describe what to test in natural language and scenarios are generated automatica - \`voice_analytics\` — get aggregated transcription analytics - \`voice_status\` — check transcription service status and statistics - **STT providers:** openai, groq, local (whisper.cpp), whisper-cpp, faster-whisper, vllm, local-ai -- **Local whisper:** On Windows, prebuilt whisper-cli is auto-downloaded from GitHub releases. On macOS/Linux, built from source via CMake. +- **Local whisper:** A whisper-cli already on PATH (e.g. `brew install whisper-cpp`) is adopted automatically. On Windows, prebuilt whisper-cli is auto-downloaded from GitHub releases. Otherwise built from source via CMake. - **TTS engine:** msedge-tts (Microsoft Edge Read Aloud). Long text is automatically split into sentence chunks with pipelined generation + playback. - **TTS config:** \`voice\`, \`edgeRate\`, \`edgePitch\`, \`volume\`, \`chunkThreshold\`, \`fallbackRate\`, \`enabled\` — configurable via dashboard. - **TTS fallback chain:** If msedge-tts fails → PowerShell SAPI (Windows/WSL), \`say\` (macOS), espeak-ng/spd-say (Linux). diff --git a/src/apps/voice/src/mcp.ts b/src/apps/voice/src/mcp.ts index a2255ca..bd1ec5a 100644 --- a/src/apps/voice/src/mcp.ts +++ b/src/apps/voice/src/mcp.ts @@ -22,7 +22,7 @@ export function createVoiceMcpServer() { "- Supports vocabulary biasing via `prompt` parameter to improve accuracy for developer terminology.", "- Two modes: `raw` (default) returns transcription as-is, `cleanup` applies AI post-processing to remove filler words and fix grammar.", "- Providers: openai, groq, local (whisper.cpp), whisper-cpp, faster-whisper, vllm, local-ai.", - "- Local whisper: on Windows prebuilt binary is auto-downloaded; on macOS/Linux built from source via CMake.", + "- Local whisper: whisper-cli on PATH (e.g. brew install whisper-cpp) is adopted automatically; on Windows prebuilt binary is auto-downloaded; otherwise built from source via CMake.", "", "### Text-to-speech (TTS)", "- Use `voice_speak` to speak text aloud. Fire-and-forget — cancels any previous speech.", diff --git a/src/apps/voice/src/providers/local-whisper.adopt.test.ts b/src/apps/voice/src/providers/local-whisper.adopt.test.ts new file mode 100644 index 0000000..1addced --- /dev/null +++ b/src/apps/voice/src/providers/local-whisper.adopt.test.ts @@ -0,0 +1,52 @@ +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { adoptSystemWhisperCli } from './local-whisper.js'; + +// A whisper-cli installed on PATH (e.g. `brew install whisper-cpp`) can be +// adopted by copying it into the location nodejs-whisper expects, skipping +// the CMake source build entirely. +describe('adoptSystemWhisperCli', () => { + const exeName = process.platform === 'win32' ? 'whisper-cli.exe' : 'whisper-cli'; + let whisperCppDir: string; + let fixtureDir: string; + + beforeEach(() => { + whisperCppDir = mkdtempSync(join(tmpdir(), 'devglide-whisper-cpp-')); + fixtureDir = mkdtempSync(join(tmpdir(), 'devglide-system-cli-')); + }); + + afterEach(() => { + rmSync(whisperCppDir, { recursive: true, force: true }); + rmSync(fixtureDir, { recursive: true, force: true }); + }); + + it('copies a system binary into build/bin and reports success', () => { + const systemCli = join(fixtureDir, exeName); + writeFileSync(systemCli, 'fake-whisper-binary'); + + const adopted = adoptSystemWhisperCli(whisperCppDir, () => systemCli); + + expect(adopted).toBe(true); + const target = join(whisperCppDir, 'build', 'bin', exeName); + expect(existsSync(target)).toBe(true); + expect(readFileSync(target, 'utf8')).toBe('fake-whisper-binary'); + }); + + it('returns false when no system binary is found', () => { + const adopted = adoptSystemWhisperCli(whisperCppDir, () => null); + + expect(adopted).toBe(false); + expect(existsSync(join(whisperCppDir, 'build'))).toBe(false); + }); + + it('returns false when the copy fails', () => { + const adopted = adoptSystemWhisperCli( + whisperCppDir, + () => join(fixtureDir, 'does-not-exist', exeName) + ); + + expect(adopted).toBe(false); + }); +}); diff --git a/src/apps/voice/src/providers/local-whisper.hint.test.ts b/src/apps/voice/src/providers/local-whisper.hint.test.ts index 3d8077e..8e331df 100644 --- a/src/apps/voice/src/providers/local-whisper.hint.test.ts +++ b/src/apps/voice/src/providers/local-whisper.hint.test.ts @@ -13,6 +13,11 @@ describe('buildToolsHint', () => { expect(hint).not.toContain('winget'); }); + it('darwin: offers brew whisper-cpp as the no-compile path', () => { + const hint = buildToolsHint('darwin'); + expect(hint).toContain('brew install whisper-cpp'); + }); + it('win32: mentions the prebuilt download attempt and Windows build tools', () => { const hint = buildToolsHint('win32'); expect(hint).toContain('Prebuilt binary download'); diff --git a/src/apps/voice/src/providers/local-whisper.ts b/src/apps/voice/src/providers/local-whisper.ts index 43f1e0b..d5d6cb3 100644 --- a/src/apps/voice/src/providers/local-whisper.ts +++ b/src/apps/voice/src/providers/local-whisper.ts @@ -1,4 +1,4 @@ -import { writeFileSync, readFileSync, unlinkSync, mkdirSync, existsSync, createWriteStream } from "fs"; +import { writeFileSync, readFileSync, unlinkSync, mkdirSync, existsSync, createWriteStream, copyFileSync } from "fs"; import { Readable } from "stream"; import { pipeline } from "stream/promises"; import { join, resolve, dirname } from "path"; @@ -83,7 +83,9 @@ export function buildToolsHint(platform: NodeJS.Platform = process.platform): st ? " Windows: winget install Kitware.CMake\n" + " winget install Microsoft.VisualStudio.2022.BuildTools --override \"--add Microsoft.VisualStudio.Workload.VCTools\"\n" : platform === "darwin" - ? " macOS: xcode-select --install\n" + + ? " macOS (no compile needed): brew install whisper-cpp (then retry — the binary is adopted automatically)\n" + + " macOS (compile from source):\n" + + " xcode-select --install\n" + " brew install cmake (Command Line Tools do NOT include CMake)\n" : " Linux: sudo apt install build-essential cmake\n"; @@ -208,6 +210,47 @@ function whisperCliExists(whisperCppPath: string): boolean { return candidates.some((p) => existsSync(p)); } +/** Find a whisper-cli binary on PATH (e.g. installed via `brew install whisper-cpp`). */ +function findSystemWhisperCli(): string | null { + const lookup = process.platform === "win32" ? "where whisper-cli" : "command -v whisper-cli"; + try { + const first = execSync(lookup, { stdio: "pipe", timeout: 10_000 }) + .toString() + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean)[0]; + return first && existsSync(first) ? first : null; + } catch { + return null; + } +} + +/** + * Copy a system-installed whisper-cli into the location nodejs-whisper + * expects, so users who ran e.g. `brew install whisper-cpp` skip the CMake + * source build entirely. Exported for tests. + */ +export function adoptSystemWhisperCli( + whisperCppPath: string, + findBinary: () => string | null = findSystemWhisperCli +): boolean { + const source = findBinary(); + if (!source) return false; + + const exeName = process.platform === "win32" ? "whisper-cli.exe" : "whisper-cli"; + const targetDir = join(whisperCppPath, "build", "bin"); + try { + mkdirSync(targetDir, { recursive: true }); + copyFileSync(source, join(targetDir, exeName)); + console.log(`[devglide-voice] Adopted system whisper-cli from ${source}`); + return true; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + console.warn(`[devglide-voice] Found system whisper-cli at ${source} but failed to adopt it: ${msg}`); + return false; + } +} + /** Download a file from `url` to `dest`. Uses `fetch` (Node 18+). */ async function downloadFile(url: string, dest: string): Promise { const res = await fetch(url, { redirect: "follow" }); @@ -311,6 +354,11 @@ async function ensureWhisperBinary(): Promise { // Already available — nothing to do if (whisperCliExists(whisperCppPath)) return { ok: true }; + // Step 0: Adopt a system-installed whisper-cli (e.g. `brew install whisper-cpp`) + if (adoptSystemWhisperCli(whisperCppPath) && whisperCliExists(whisperCppPath)) { + return { ok: true }; + } + // Step 1: Try prebuilt binary (Windows only — no official macOS/Linux binaries) let downloadFailure: string | undefined; const asset = getPrebuiltAsset(); @@ -336,7 +384,6 @@ async function ensureWhisperBinary(): Promise { const src = join(extractDir, file); const dest = join(targetDir, file.split("/").pop()!); if (existsSync(src)) { - const { copyFileSync } = await import("fs"); copyFileSync(src, dest); } } From 93c29167bbad68eda16e18e6d6ad2627f1b582e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Kuty=C5=82a?= Date: Fri, 10 Jul 2026 10:45:51 +0200 Subject: [PATCH 3/4] feat(cli): voice dependency doctor in devglide setup devglide setup now ends with a dependency check for the voice local provider (ffmpeg, whisper-cli, cmake) with platform-specific install hints. cmake is only flagged when whisper-cli actually needs a source build. Checks are injectable and unit-tested. --- bin/devglide.js | 4 ++ bin/doctor.js | 122 +++++++++++++++++++++++++++++++++++++++++++++ bin/doctor.test.ts | 81 ++++++++++++++++++++++++++++++ 3 files changed, 207 insertions(+) create mode 100644 bin/doctor.js create mode 100644 bin/doctor.test.ts diff --git a/bin/devglide.js b/bin/devglide.js index b3ecbfd..8a33ff4 100755 --- a/bin/devglide.js +++ b/bin/devglide.js @@ -9,6 +9,7 @@ import { homedir } from "os"; import { getClaudeMdContent, injectSection, removeSection } from "./claude-md-template.js"; import { removeDevglideSectionsFromToml } from "./codex-config.js"; +import { runDoctor } from "./doctor.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); const root = resolve(__dirname, ".."); @@ -582,6 +583,9 @@ function runSetup() { } } + // Report voice/local-whisper environment dependencies (non-fatal) + runDoctor(); + console.log("\n Setup complete!\n"); } diff --git a/bin/doctor.js b/bin/doctor.js new file mode 100644 index 0000000..b8b2842 --- /dev/null +++ b/bin/doctor.js @@ -0,0 +1,122 @@ +// Dependency doctor for the voice local-whisper provider. +// Reports whether local transcription can work on this machine: ffmpeg +// (always required), whisper-cli (built in the nodejs-whisper tree, on PATH, +// or auto-provisioned), and cmake (only needed for a from-source build). + +import { execSync } from "child_process"; +import { existsSync } from "fs"; +import { join, dirname } from "path"; +import { createRequire } from "module"; + +/** Run a probe command; return its first output line, or null if it fails. */ +export function defaultProbe(cmd) { + try { + const out = execSync(cmd, { stdio: "pipe", timeout: 10_000 }).toString(); + return out.split(/\r?\n/).map((line) => line.trim()).filter(Boolean)[0] ?? "found"; + } catch { + return null; + } +} + +/** Check whether whisper-cli was already built inside the nodejs-whisper package. */ +function whisperCliBuiltInTree() { + try { + const require_ = createRequire(import.meta.url); + const whisperCppPath = join( + dirname(require_.resolve("nodejs-whisper/package.json")), + "cpp", + "whisper.cpp" + ); + const exeName = process.platform === "win32" ? "whisper-cli.exe" : "whisper-cli"; + return [ + join(whisperCppPath, "build", "bin", exeName), + join(whisperCppPath, "build", "bin", "Release", exeName), + join(whisperCppPath, "build", "bin", "Debug", exeName), + join(whisperCppPath, "build", exeName), + join(whisperCppPath, exeName), + ].some((p) => existsSync(p)); + } catch { + return false; + } +} + +/** + * Collect dependency checks as { name, ok, detail, hint } entries. + * All environment access is injectable for tests. + */ +export function collectDoctorChecks({ + platform = process.platform, + probe = defaultProbe, + builtInTree = whisperCliBuiltInTree, +} = {}) { + const checks = []; + + const ffmpeg = probe("ffmpeg -version"); + checks.push({ + name: "ffmpeg", + ok: ffmpeg !== null, + detail: ffmpeg ?? "not found on PATH", + hint: ffmpeg !== null + ? null + : platform === "darwin" + ? "brew install ffmpeg" + : platform === "win32" + ? "winget install ffmpeg" + : "sudo apt install ffmpeg", + }); + + const inTree = builtInTree(); + const onPath = probe(platform === "win32" ? "where whisper-cli" : "command -v whisper-cli"); + // Windows never needs manual provisioning: a prebuilt binary is downloaded + // on first transcription. + const whisperOk = inTree || onPath !== null || platform === "win32"; + checks.push({ + name: "whisper-cli", + ok: whisperOk, + detail: inTree + ? "already built for nodejs-whisper" + : onPath !== null + ? `found on PATH (${onPath}) — adopted automatically on first use` + : platform === "win32" + ? "not found — prebuilt binary is auto-downloaded on first transcription" + : "not found", + hint: whisperOk + ? null + : platform === "darwin" + ? "brew install whisper-cpp (no compile needed — adopted automatically)" + : "install build tools below; whisper.cpp is compiled on first transcription", + }); + + const cmakeNeeded = !inTree && onPath === null && platform !== "win32"; + const cmake = probe("cmake --version"); + const cmakeOk = cmake !== null || !cmakeNeeded; + checks.push({ + name: "cmake", + ok: cmakeOk, + detail: cmake ?? (cmakeNeeded ? "not found on PATH" : "not needed (whisper-cli already available)"), + hint: cmakeOk + ? null + : platform === "darwin" + ? "brew install cmake (xcode-select --install alone is not enough)" + : "sudo apt install build-essential cmake", + }); + + return checks; +} + +/** Render checks as an indented report block. */ +export function formatDoctorReport(checks) { + const lines = ["", " Voice dependency check:", ""]; + for (const check of checks) { + lines.push(` ${check.ok ? "✓" : "✗"} ${check.name}: ${check.detail}`); + if (check.hint) lines.push(` → ${check.hint}`); + } + return lines.join("\n"); +} + +/** Print the doctor report; returns true when every check passed. */ +export function runDoctor(options = {}) { + const checks = collectDoctorChecks(options); + console.log(formatDoctorReport(checks)); + return checks.every((check) => check.ok); +} diff --git a/bin/doctor.test.ts b/bin/doctor.test.ts new file mode 100644 index 0000000..3f9046a --- /dev/null +++ b/bin/doctor.test.ts @@ -0,0 +1,81 @@ +import { describe, expect, it } from 'vitest'; +import { collectDoctorChecks, formatDoctorReport } from './doctor.js'; + +type Check = { name: string; ok: boolean; detail: string; hint: string | null }; + +function byName(checks: Check[], name: string): Check { + const check = checks.find((c) => c.name === name); + if (!check) throw new Error(`missing check: ${name}`); + return check; +} + +// The doctor reports whether the voice local-whisper provider can work: +// ffmpeg (always required), whisper-cli (built, on PATH, or auto-provisioned), +// and cmake (only needed when whisper-cli must be compiled from source). +describe('collectDoctorChecks', () => { + it('reports all green when everything is available', () => { + const checks = collectDoctorChecks({ + platform: 'darwin', + probe: (cmd: string) => + cmd.startsWith('ffmpeg') ? 'ffmpeg version 7.1' : + cmd.startsWith('cmake') ? 'cmake version 3.30' : + '/opt/homebrew/bin/whisper-cli', + builtInTree: () => false, + }); + + expect(checks.every((c) => c.ok)).toBe(true); + expect(byName(checks, 'whisper-cli').detail).toContain('adopted automatically'); + }); + + it('darwin with nothing installed: fails with brew hints', () => { + const checks = collectDoctorChecks({ + platform: 'darwin', + probe: () => null, + builtInTree: () => false, + }); + + expect(byName(checks, 'ffmpeg').ok).toBe(false); + expect(byName(checks, 'ffmpeg').hint).toContain('brew install ffmpeg'); + expect(byName(checks, 'whisper-cli').ok).toBe(false); + expect(byName(checks, 'whisper-cli').hint).toContain('brew install whisper-cpp'); + expect(byName(checks, 'cmake').ok).toBe(false); + expect(byName(checks, 'cmake').hint).toContain('brew install cmake'); + }); + + it('win32 without whisper-cli: still ok — prebuilt is auto-downloaded', () => { + const checks = collectDoctorChecks({ + platform: 'win32', + probe: (cmd: string) => (cmd.startsWith('ffmpeg') ? 'ffmpeg version 7.1' : null), + builtInTree: () => false, + }); + + expect(byName(checks, 'whisper-cli').ok).toBe(true); + expect(byName(checks, 'whisper-cli').detail).toContain('auto-downloaded'); + expect(byName(checks, 'cmake').ok).toBe(true); + }); + + it('cmake is not required once whisper-cli is already built', () => { + const checks = collectDoctorChecks({ + platform: 'linux', + probe: (cmd: string) => (cmd.startsWith('ffmpeg') ? 'ffmpeg version 7.1' : null), + builtInTree: () => true, + }); + + expect(byName(checks, 'whisper-cli').ok).toBe(true); + expect(byName(checks, 'cmake').ok).toBe(true); + expect(byName(checks, 'cmake').detail).toContain('not needed'); + }); +}); + +describe('formatDoctorReport', () => { + it('renders pass/fail marks and hints', () => { + const report = formatDoctorReport([ + { name: 'ffmpeg', ok: true, detail: 'ffmpeg version 7.1', hint: null }, + { name: 'whisper-cli', ok: false, detail: 'not found', hint: 'brew install whisper-cpp' }, + ]); + + expect(report).toContain('✓ ffmpeg: ffmpeg version 7.1'); + expect(report).toContain('✗ whisper-cli: not found'); + expect(report).toContain('→ brew install whisper-cpp'); + }); +}); From b26bbcad6748a386c1a5f07fa82f6055e7328dd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Fri, 10 Jul 2026 09:07:49 +0000 Subject: [PATCH 4/4] release: v0.2.38 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 075f8e8..5fadc85 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "devglide", - "version": "0.2.37", + "version": "0.2.38", "description": "AI workflow toolkit with MCP servers for kanban, shell, testing, workflows, and more — built for Claude Code", "license": "MIT", "author": "Daniel Kutyla",