Squashed commit of the following:

commit d6c92ea0ad95c0b640ac9c7df48197412c7518e3
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date:   Thu Aug 21 23:27:44 2025 +0530

    fix: unpacking amical/smart-whisper dep

commit 87819819bb12c07b94f5b52cbb0ea42a452c16e2
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date:   Thu Aug 21 17:41:02 2025 +0530

    fix: unpacking of smart-whisper

commit 81cec166834606cbff2cdd2e750dcc1fb769d4f3
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date:   Thu Aug 21 16:08:39 2025 +0530

    chore: re-enable mac builds

commit f13069c1f350fe06c69aa8f16af41f983f34131e
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date:   Thu Aug 21 13:06:26 2025 +0530

    feat: add smart-whisper package with updated build configuration

commit a24e06856cc595f5e6c5d914090531716d208d2a
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date:   Thu Aug 21 11:37:25 2025 +0530

    chore: bump smart-whisper ver

commit 98f84b6f89c873370f1bb356f11c97dab0185ab7
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date:   Wed Aug 20 08:59:55 2025 +0530

    feat: release wf updates for win builds

commit a85825d362f2a27fdef7b49533a9139aea4785b7
Author: haritabh-z01 <haritabh.z01+github@gmail.com>
Date:   Wed Aug 20 08:36:13 2025 +0530

    feat: add windows support basics
This commit is contained in:
nchopra 2025-08-25 23:54:28 +05:30
parent 2d852a0d14
commit 17d034be80
59 changed files with 10524 additions and 3079 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 34 KiB

Before After
Before After

View file

@ -17,6 +17,8 @@ import {
mkdirSync,
cpSync,
rmSync,
lstatSync,
readlinkSync,
} from "node:fs";
import { join, normalize } from "node:path";
// Use flora-colossus for finding all dependencies of EXTERNAL_DEPENDENCIES
@ -29,7 +31,6 @@ let nativeModuleDependenciesToPackage: string[] = [];
export const EXTERNAL_DEPENDENCIES = [
"electron-squirrel-startup",
"smart-whisper",
"@libsql/client",
"@libsql/darwin-arm64",
"@libsql/darwin-x64",
@ -39,13 +40,13 @@ export const EXTERNAL_DEPENDENCIES = [
"libsql",
"onnxruntime-node",
"workerpool",
"@amical/smart-whisper",
// Add any other native modules you need here
];
const config: ForgeConfig = {
hooks: {
prePackage: async (_forgeConfig, platform, arch) => {
console.error("prePackage", { platform, arch });
const projectRoot = normalize(__dirname);
// In a monorepo, node_modules are typically at the root level
const monorepoRoot = join(projectRoot, "../../"); // Go up to monorepo root
@ -148,15 +149,56 @@ const config: ForgeConfig = {
// Copy the package
console.log(`Copying ${dep}...`);
cpSync(rootDepPath, localDepPath, { recursive: true });
cpSync(rootDepPath, localDepPath, { recursive: true, dereference: true, force: true });
console.log(`✓ Successfully copied ${dep}`);
} catch (error) {
console.error(`Failed to copy ${dep}:`, error);
}
}
// Second pass: Replace any symlinks with dereferenced copies
console.log("Checking for symlinks in copied dependencies...");
for (const dep of nativeModuleDependenciesToPackage) {
const localDepPath = join(localNodeModules, dep);
try {
if (existsSync(localDepPath)) {
const stats = lstatSync(localDepPath);
if (stats.isSymbolicLink()) {
console.log(`Found symlink for ${dep}, replacing with dereferenced copy...`);
// Read where the symlink points to
const symlinkTarget = readlinkSync(localDepPath);
const absoluteTarget = join(localDepPath, "..", symlinkTarget);
const sourcePath = normalize(absoluteTarget);
console.log(` Symlink points to: ${sourcePath}`);
// Remove the symlink
rmSync(localDepPath, { recursive: true, force: true });
// Copy with dereference to get actual content
cpSync(sourcePath, localDepPath, {
recursive: true,
force: true,
dereference: true // Follow symlinks and copy actual content
});
console.log(`✓ Successfully replaced symlink for ${dep} with actual content`);
}
}
} catch (error) {
console.error(`Failed to check/replace symlink for ${dep}:`, error);
}
}
// Prune onnxruntime-node to keep only the required binary
console.log("Pruning onnxruntime-node binaries...");
const targetPlatform = platform;
const targetArch = arch;
console.log(
`Pruning onnxruntime-node binaries for ${targetPlatform}/${targetArch}...`,
);
const onnxBinRoot = join(localNodeModules, "onnxruntime-node", "bin");
if (existsSync(onnxBinRoot)) {
const napiVersionDirs = readdirSync(onnxBinRoot);
@ -169,18 +211,18 @@ const config: ForgeConfig = {
const platformPath = join(napiVersionPath, platformDir);
if (!statSync(platformPath).isDirectory()) continue;
// Delete other platform directories
if (platformDir !== process.platform) {
// Delete unused platforms except Linux (keep for compatibility)
if (platformDir !== targetPlatform && platformDir !== "linux") {
console.log(`- Deleting unused platform: ${platformPath}`);
rmSync(platformPath, { recursive: true, force: true });
} else {
} else if (platformDir === targetPlatform) {
// Now in the correct platform dir, prune architectures
const archDirs = readdirSync(platformPath);
for (const archDir of archDirs) {
const archPath = join(platformPath, archDir);
if (!statSync(archPath).isDirectory()) continue;
if (archDir !== process.arch) {
if (archDir !== targetArch) {
console.log(`- Deleting unused arch: ${archPath}`);
rmSync(archPath, { recursive: true, force: true });
}
@ -196,6 +238,7 @@ const config: ForgeConfig = {
}
},
packageAfterPrune: async (_forgeConfig, buildPath) => {
console.error("PRE PACKAGE");
try {
function getItemsFromFolder(
path: string,
@ -264,14 +307,14 @@ const config: ForgeConfig = {
packagerConfig: {
asar: {
unpack:
"{*.node,*.dylib,*.so,*.dll,*.metal,**/whisper.cpp/**,**/.vite/build/whisper-worker-fork.js,**/node_modules/smart-whisper/**,**/node_modules/jest-worker/**}",
"{*.node,*.dylib,*.so,*.dll,*.metal,**/node_modules/@amical/smart-whisper/**,**/whisper.cpp/**,**/.vite/build/whisper-worker-fork.js,**/node_modules/jest-worker/**,**/onnxruntime-node/bin/**}",
},
name: "Amical",
executableName: "Amical",
icon: "./assets/logo", // Path to your icon file
appBundleId: "com.amical.desktop", // Proper bundle ID
extraResource: [
"../../packages/native-helpers/swift-helper/bin",
`${process.platform === "win32" ? "../../packages/native-helpers/windows-helper/bin" : "../../packages/native-helpers/swift-helper/bin"}`,
"./src/db/migrations",
// Only include the platform-specific node binary
`./node-binaries/${process.platform}-${process.arch}/node${
@ -356,8 +399,18 @@ const config: ForgeConfig = {
}
// Handle scoped packages: if dep is @scope/package, also keep @scope/ directory
// But not for our workspace packages
if (dep.includes("/") && dep.startsWith("@")) {
const scopeDir = dep.split("/")[0]; // @libsql/client -> @libsql
// for workspace packages only keep the actual package
if (scopeDir === "@amical") {
if (filePath.startsWith(`/node_modules/${dep}`) ||
filePath === `/node_modules/${scopeDir}`) {
KEEP_FILE.keep = true;
KEEP_FILE.log = true;
}
continue;
}
if (
filePath === `/node_modules/${scopeDir}/` ||
filePath === `/node_modules/${scopeDir}` ||

View file

@ -1,6 +1,6 @@
{
"name": "@amical/desktop",
"version": "0.0.9",
"version": "0.0.9-windows-ci-test",
"description": "Amical Desktop app",
"main": ".vite/build/main.js",
"productName": "Amical",
@ -8,6 +8,11 @@
"type": "git",
"url": "https://github.com/amicalhq/amical"
},
"author": {
"name": "Amical",
"email": "contact@amical.ai",
"url": "https://amical.ai"
},
"scripts": {
"start": "pnpm build:deps && electron-forge start",
"start:onboarding": "FORCE_ONBOARDING=true pnpm start",
@ -22,6 +27,8 @@
"make:zip:x64": "pnpm build:deps && electron-forge make --targets=@electron-forge/maker-zip --platform=darwin --arch=x64",
"package:arm64": "pnpm build:deps && electron-forge package --platform=darwin --arch=arm64",
"package:x64": "pnpm build:deps && electron-forge package --platform=darwin --arch=x64",
"package:windows": "pnpm build:deps && electron-forge package --platform=win32 --arch=x64",
"make:windows": "pnpm build:deps && electron-forge make --platform=win32 --arch=x64",
"publish": "electron-forge publish",
"lint": "eslint --ext .ts,.tsx .",
"format:check": "prettier --check \"**/*.{ts,tsx,md,json,mjs,mts,css,mdx}\" --cache --ignore-path=../../.prettierignore",
@ -29,9 +36,11 @@
"db:generate": "drizzle-kit generate",
"db:push": "drizzle-kit push",
"db:migrate": "drizzle-kit migrate",
"build:deps": "pnpm build:types && pnpm build:swift-helper",
"build:deps": "pnpm build:types && pnpm build:native-helper",
"build:types": "pnpm --filter @amical/types build",
"build:swift-helper": "pnpm --filter @amical/swift-helper build",
"build:windows-helper": "pnpm --filter @amical/windows-helper build",
"build:native-helper": "node -p \"process.platform === 'darwin' ? 'build:swift-helper' : process.platform === 'win32' ? 'build:windows-helper' : 'echo No native helpers'\" | xargs pnpm run",
"dev": "pnpm start",
"download-node": "tsx scripts/download-node-binaries.ts",
"download-node:all": "tsx scripts/download-node-binaries.ts --all"
@ -39,16 +48,16 @@
"keywords": [],
"license": "MIT",
"devDependencies": {
"@electron-forge/cli": "^7.8.1",
"@electron-forge/maker-deb": "^7.8.1",
"@electron-forge/maker-dmg": "^7.8.1",
"@electron-forge/maker-rpm": "^7.8.1",
"@electron-forge/maker-squirrel": "^7.8.1",
"@electron-forge/maker-zip": "^7.8.1",
"@electron-forge/plugin-auto-unpack-natives": "^7.8.1",
"@electron-forge/plugin-fuses": "^7.8.1",
"@electron-forge/plugin-vite": "^7.8.1",
"@electron-forge/publisher-github": "^7.8.1",
"@electron-forge/cli": "7.8.2",
"@electron-forge/maker-deb": "7.8.2",
"@electron-forge/maker-dmg": "7.8.2",
"@electron-forge/maker-rpm": "7.8.2",
"@electron-forge/maker-squirrel": "7.8.2",
"@electron-forge/maker-zip": "7.8.2",
"@electron-forge/plugin-auto-unpack-natives": "7.8.2",
"@electron-forge/plugin-fuses": "7.8.2",
"@electron-forge/plugin-vite": "7.8.2",
"@electron-forge/publisher-github": "7.8.2",
"@electron/fuses": "^1.8.0",
"@rollup/plugin-commonjs": "^28.0.6",
"@tailwindcss/vite": "^4.1.6",
@ -78,6 +87,7 @@
"@hookform/resolvers": "^5.0.1",
"@libsql/client": "^0.15.9",
"@libsql/darwin-x64": "0.5.13",
"@libsql/win32-x64-msvc": "^0.5.13",
"@openrouter/ai-sdk-provider": "^0.7.2",
"@radix-ui/react-accordion": "^1.2.10",
"@radix-ui/react-alert-dialog": "^1.1.13",
@ -142,7 +152,7 @@
"react-hook-form": "^7.56.3",
"react-resizable-panels": "^3.0.2",
"recharts": "^2.15.3",
"smart-whisper": "0.2.0",
"@amical/smart-whisper": "workspace:*",
"sonner": "^2.0.3",
"split2": "^4.2.0",
"superjson": "^2.2.2",

View file

@ -167,8 +167,8 @@ export class AppManager {
return this.serviceManager.getService("transcriptionService");
}
getSwiftIOBridge() {
return this.serviceManager.getService("swiftIOBridge");
getNativeBridge() {
return this.serviceManager.getService("nativeBridge");
}
getAutoUpdaterService() {

View file

@ -11,23 +11,23 @@ export class EventHandlers {
}
setupEventHandlers(): void {
this.setupSwiftBridgeEventHandlers();
this.setupNativeBridgeEventHandlers();
this.setupGeneralIPCHandlers();
this.setupOnboardingIPCHandlers();
// Note: Audio IPC handlers are now managed by RecordingService
}
private setupSwiftBridgeEventHandlers(): void {
private setupNativeBridgeEventHandlers(): void {
try {
const swiftBridge = this.appManager.getSwiftIOBridge();
if (!swiftBridge) {
logger.main.warn("Swift bridge not available for event handlers");
const nativeBridge = this.appManager.getNativeBridge();
if (!nativeBridge) {
logger.main.warn("Native bridge not available for event handlers");
return;
}
// Handle non-shortcut related events only
swiftBridge.on("helperEvent", (event: HelperEvent) => {
logger.swift.debug("Received helperEvent from SwiftIOBridge", {
nativeBridge.on("helperEvent", (event: HelperEvent) => {
logger.swift.debug("Received helperEvent from native bridge", {
event,
});
@ -35,15 +35,15 @@ export class EventHandlers {
// This handler can process other helper events if needed
});
swiftBridge.on("error", (error: Error) => {
logger.main.error("SwiftIOBridge error:", error);
nativeBridge.on("error", (error: Error) => {
logger.main.error("Native bridge error:", error);
});
swiftBridge.on("close", (code: number | null) => {
logger.swift.warn("Swift helper process closed", { code });
nativeBridge.on("close", (code: number | null) => {
logger.swift.warn("Native helper process closed", { code });
});
} catch (error) {
logger.main.warn("Swift bridge not available for event handlers");
logger.main.warn("Native bridge not available for event handlers");
}
}

View file

@ -5,13 +5,14 @@ import { app } from "electron";
import * as path from "path";
// Set GGML_METAL_PATH_RESOURCES before any other imports
// This ensures smart-whisper can find its resources when unpacked from asar
// This ensures @amical/smart-whisper can find its resources when unpacked from asar
if (app.isPackaged) {
// Point to the unpacked whisper.cpp directory
process.env.GGML_METAL_PATH_RESOURCES = path.join(
process.resourcesPath,
"app.asar.unpacked",
"node_modules",
"@amical",
"smart-whisper",
"whisper.cpp",
);

View file

@ -218,12 +218,12 @@ export class RecordingManager extends EventEmitter {
// Mute system audio
try {
const swiftBridge = this.serviceManager.getService("swiftIOBridge");
if (swiftBridge) {
await swiftBridge.call("muteSystemAudio", {});
const nativeBridge = this.serviceManager.getService("nativeBridge");
if (nativeBridge) {
await nativeBridge.call("muteSystemAudio", {});
}
} catch (error) {
logger.main.warn("Swift bridge not available for audio muting");
logger.main.warn("Native bridge not available for audio muting");
}
this.setState("recording");
@ -252,12 +252,12 @@ export class RecordingManager extends EventEmitter {
// Restore system audio
try {
const swiftBridge = this.serviceManager.getService("swiftIOBridge");
if (swiftBridge) {
await swiftBridge.call("restoreSystemAudio", {});
const nativeBridge = this.serviceManager.getService("nativeBridge");
if (nativeBridge) {
await nativeBridge.call("restoreSystemAudio", {});
}
} catch (error) {
logger.main.warn("Swift bridge not available for audio restore");
logger.main.warn("Native bridge not available for audio restore");
}
logger.audio.info("Recording stop initiated", {
@ -412,14 +412,14 @@ export class RecordingManager extends EventEmitter {
}
try {
const swiftBridge = this.serviceManager.getService("swiftIOBridge");
const nativeBridge = this.serviceManager.getService("nativeBridge");
logger.main.info("Pasting transcription to active application", {
textLength: transcription.length,
});
if (swiftBridge) {
swiftBridge.call("pasteText", {
if (nativeBridge) {
nativeBridge.call("pasteText", {
transcript: transcription,
});
}

View file

@ -2,7 +2,7 @@ import { logger } from "../logger";
import { ModelManagerService } from "../../services/model-manager";
import { TranscriptionService } from "../../services/transcription-service";
import { SettingsService } from "../../services/settings-service";
import { SwiftIOBridge } from "../../services/platform/swift-bridge-service";
import { NativeBridge } from "../../services/platform/native-bridge-service";
import { AutoUpdaterService } from "../services/auto-updater";
import { RecordingManager } from "./recording-manager";
import { VADService } from "../../services/vad-service";
@ -11,6 +11,7 @@ import { WindowManager } from "../core/window-manager";
import { createIPCHandler } from "electron-trpc-experimental/main";
import { router } from "../../trpc/router";
import { createContext } from "../../trpc/context";
import { isMacOS, isWindows } from "../../utils/platform";
/**
* Service map for type-safe service access
@ -20,7 +21,7 @@ export interface ServiceMap {
transcriptionService: TranscriptionService;
settingsService: SettingsService;
vadService: VADService;
swiftIOBridge: SwiftIOBridge;
nativeBridge: NativeBridge;
autoUpdaterService: AutoUpdaterService;
recordingManager: RecordingManager;
shortcutManager: ShortcutManager;
@ -39,7 +40,7 @@ export class ServiceManager {
private settingsService: SettingsService | null = null;
private vadService: VADService | null = null;
private swiftIOBridge: SwiftIOBridge | null = null;
private nativeBridge: NativeBridge | null = null;
private autoUpdaterService: AutoUpdaterService | null = null;
private recordingManager: RecordingManager | null = null;
private shortcutManager: ShortcutManager | null = null;
@ -145,9 +146,9 @@ export class ServiceManager {
}
private initializePlatformServices(): void {
// Initialize Swift bridge for macOS integration
if (process.platform === "darwin") {
this.swiftIOBridge = new SwiftIOBridge();
// Initialize platform-specific bridge
if (isMacOS() || isWindows()) {
this.nativeBridge = new NativeBridge();
}
}
@ -163,7 +164,7 @@ export class ServiceManager {
);
}
this.shortcutManager = new ShortcutManager(this.settingsService);
await this.shortcutManager.initialize(this.swiftIOBridge);
await this.shortcutManager.initialize(this.nativeBridge);
// Connect shortcut events to recording manager
this.recordingManager.setupShortcutListeners(this.shortcutManager);
@ -213,7 +214,7 @@ export class ServiceManager {
transcriptionService: this.transcriptionService ?? undefined,
settingsService: this.settingsService ?? undefined,
vadService: this.vadService ?? undefined,
swiftIOBridge: this.swiftIOBridge ?? undefined,
nativeBridge: this.nativeBridge ?? undefined,
autoUpdaterService: this.autoUpdaterService ?? undefined,
recordingManager: this.recordingManager ?? undefined,
shortcutManager: this.shortcutManager ?? undefined,
@ -242,9 +243,9 @@ export class ServiceManager {
await this.vadService.dispose();
}
if (this.swiftIOBridge) {
logger.main.info("Stopping Swift helper...");
this.swiftIOBridge.stopHelper();
if (this.nativeBridge) {
logger.main.info("Stopping native helper...");
this.nativeBridge.stopHelper();
}
}

View file

@ -1,8 +1,8 @@
import { EventEmitter } from "events";
import { globalShortcut } from "electron";
import { SettingsService } from "@/services/settings-service";
import { SwiftIOBridge } from "@/services/platform/swift-bridge-service";
import { matchesShortcutKey, getKeyNameFromPayload } from "@/utils/keycode-map";
import { NativeBridge } from "@/services/platform/native-bridge-service";
import { getKeyNameFromPayload } from "@/utils/keycode-map";
import { KeyEventPayload, HelperEvent } from "@amical/types";
import { logger } from "@/main/logger";
@ -25,7 +25,7 @@ export class ShortcutManager extends EventEmitter {
toggleRecording: "",
};
private settingsService: SettingsService;
private swiftIOBridge: SwiftIOBridge | null = null;
private nativeBridge: NativeBridge | null = null;
private isRecordingShortcut: boolean = false;
constructor(settingsService: SettingsService) {
@ -33,8 +33,8 @@ export class ShortcutManager extends EventEmitter {
this.settingsService = settingsService;
}
async initialize(swiftIOBridge: SwiftIOBridge | null) {
this.swiftIOBridge = swiftIOBridge;
async initialize(nativeBridge: NativeBridge | null) {
this.nativeBridge = nativeBridge;
await this.loadShortcuts();
this.setupEventListeners();
}
@ -59,12 +59,12 @@ export class ShortcutManager extends EventEmitter {
}
private setupEventListeners() {
if (!this.swiftIOBridge) {
log.warn("SwiftIOBridge not available, shortcuts will not work");
if (!this.nativeBridge) {
log.warn("Native bridge not available, shortcuts will not work");
return;
}
this.swiftIOBridge.on("helperEvent", (event: HelperEvent) => {
this.nativeBridge.on("helperEvent", (event: HelperEvent) => {
switch (event.type) {
case "flagsChanged":
this.handleFlagsChanged(event.payload);

View file

@ -1,5 +1,5 @@
// Worker process entry point for fork
import { Whisper } from "smart-whisper";
import { Whisper } from "@amical/smart-whisper";
// Simple console-based logging for worker process
const logger = {
@ -29,7 +29,7 @@ const methods = {
whisperInstance = null;
}
const { Whisper } = await import("smart-whisper");
const { Whisper } = await import("@amical/smart-whisper");
whisperInstance = new Whisper(modelPath, { gpu: true });
try {
await whisperInstance.load();

View file

@ -1,5 +1,5 @@
// This file contains just the Whisper-specific operations that need to run in a separate process
import { Whisper } from "smart-whisper";
import { Whisper } from "@amical/smart-whisper";
// Simple console-based logging for worker process
const logger = {
@ -27,7 +27,7 @@ export async function initializeModel(modelPath: string): Promise<void> {
whisperInstance = null;
}
const { Whisper } = await import("smart-whisper");
const { Whisper } = await import("@amical/smart-whisper");
whisperInstance = new Whisper(modelPath, { gpu: true });
try {
await whisperInstance.load();

View file

@ -1,10 +1,10 @@
import { spawn, ChildProcessWithoutNullStreams } from "child_process";
import path from "node:path";
import fs from "node:fs";
import process from "node:process"; // Added import for process
import { app, app as electronApp } from "electron"; // electronApp for app.getAppPath() consistency
import { app as electronApp } from "electron";
import split2 from "split2";
import { v4 as uuid } from "uuid";
import { getNativeHelperName, getNativeHelperDir } from "../../utils/platform";
import { EventEmitter } from "events";
import { createScopedLogger } from "../../main/logger";
@ -54,21 +54,21 @@ interface RPCMethods {
}
// Define event types for the client
interface SwiftIOBridgeEvents {
interface NativeBridgeEvents {
helperEvent: (event: HelperEvent) => void;
error: (error: Error) => void;
close: (code: number | null, signal: NodeJS.Signals | null) => void;
ready: () => void; // Emitted when the helper process is successfully spawned
}
export class SwiftIOBridge extends EventEmitter {
export class NativeBridge extends EventEmitter {
private proc: ChildProcessWithoutNullStreams | null = null;
private pending = new Map<
string,
{ callback: (resp: RpcResponse) => void; startTime: number }
>();
private helperPath: string;
private logger = createScopedLogger("swift-bridge");
private logger = createScopedLogger("native-bridge");
constructor() {
super();
@ -77,7 +77,9 @@ export class SwiftIOBridge extends EventEmitter {
}
private determineHelperPath(): string {
const helperName = "SwiftHelper"; // Swift native helper executable
const helperName = getNativeHelperName();
const helperDir = getNativeHelperDir();
return electronApp.isPackaged
? path.join(process.resourcesPath, "bin", helperName)
: path.join(
@ -86,7 +88,7 @@ export class SwiftIOBridge extends EventEmitter {
"..",
"packages",
"native-helpers",
"swift-helper",
helperDir,
"bin",
helperName,
);
@ -96,20 +98,33 @@ export class SwiftIOBridge extends EventEmitter {
try {
fs.accessSync(this.helperPath, fs.constants.X_OK);
} catch (err) {
this.logger.error("SwiftHelper executable not found or not executable", {
helperPath: this.helperPath,
});
this.emit(
"error",
new Error(
`Helper executable not found at ${this.helperPath}. Attempt to build it if in dev mode.`,
),
const helperName = getNativeHelperName();
this.logger.error(
`${helperName} executable not found or not executable`,
{
helperPath: this.helperPath,
},
);
// In a real app, you might try to build it here or provide more robust error handling.
// In production, provide a more user-friendly error message
const errorMessage = electronApp.isPackaged
? `${helperName} is not available. Some features may not work correctly.`
: `Helper executable not found at ${this.helperPath}. Please build it first.`;
this.emit("error", new Error(errorMessage));
// Log detailed error for debugging
this.logger.error("Helper initialization failed", {
helperPath: this.helperPath,
isPackaged: electronApp.isPackaged,
platform: process.platform,
error: err,
});
return;
}
this.logger.info("Spawning SwiftHelper", { helperPath: this.helperPath });
const helperName = getNativeHelperName();
this.logger.info(`Spawning ${helperName}`, { helperPath: this.helperPath });
this.proc = spawn(this.helperPath, [], { stdio: ["pipe", "pipe", "pipe"] });
this.proc.stdout.pipe(split2()).on("data", (line: string) => {
@ -150,19 +165,24 @@ export class SwiftIOBridge extends EventEmitter {
this.proc.stderr.on("data", (data: Buffer) => {
const errorMsg = data.toString();
this.logger.warn("SwiftHelper stderr output", { message: errorMsg });
const helperName = getNativeHelperName();
this.logger.warn(`${helperName} stderr output`, { message: errorMsg });
// Don't emit as error since stderr is often just debug info
// this.emit('error', new Error(`Helper stderr: ${errorMsg}`));
});
this.proc.on("error", (err) => {
this.logger.error("Failed to start SwiftHelper process", { error: err });
const helperName = getNativeHelperName();
this.logger.error(`Failed to start ${helperName} process`, {
error: err,
});
this.emit("error", err);
this.proc = null;
});
this.proc.on("close", (code, signal) => {
this.logger.info("SwiftHelper process exited", { code, signal });
const helperName = getNativeHelperName();
this.logger.info(`${helperName} process exited`, { code, signal });
this.emit("close", code, signal);
this.proc = null;
// Optionally, implement retry logic or notify further
@ -180,11 +200,18 @@ export class SwiftIOBridge extends EventEmitter {
timeoutMs = 5000,
): Promise<RPCMethods[M]["result"]> {
if (!this.proc || !this.proc.stdin || !this.proc.stdin.writable) {
return Promise.reject(
new Error(
"Swift helper process is not running or stdin is not writable.",
),
);
const helperName = getNativeHelperName();
const errorMessage = electronApp.isPackaged
? `${helperName} is not available for this operation.`
: "Native helper process is not running or stdin is not writable.";
this.logger.warn(`Cannot call ${method}: helper not available`, {
method,
isPackaged: electronApp.isPackaged,
platform: process.platform,
});
return Promise.reject(new Error(errorMessage));
}
const id = uuid();
@ -267,7 +294,7 @@ export class SwiftIOBridge extends EventEmitter {
const duration = timedOutAt - startTime;
reject(
new Error(
`SwiftIOBridge: RPC call "${method}" (id: ${id}) timed out after ${timeoutMs}ms (duration: ${duration}ms, started: ${new Date(startTime).toISOString()})`,
`NativeBridge: RPC call "${method}" (id: ${id}) timed out after ${timeoutMs}ms (duration: ${duration}ms, started: ${new Date(startTime).toISOString()})`,
),
);
}
@ -283,24 +310,25 @@ export class SwiftIOBridge extends EventEmitter {
public stopHelper(): void {
if (this.proc) {
this.logger.info("Stopping SwiftHelper process");
const helperName = getNativeHelperName();
this.logger.info(`Stopping ${helperName} process`);
this.proc.kill();
this.proc = null;
}
}
// Typed event emitter methods
on<E extends keyof SwiftIOBridgeEvents>(
on<E extends keyof NativeBridgeEvents>(
event: E,
listener: SwiftIOBridgeEvents[E],
listener: NativeBridgeEvents[E],
): this {
super.on(event, listener);
return this;
}
emit<E extends keyof SwiftIOBridgeEvents>(
emit<E extends keyof NativeBridgeEvents>(
event: E,
...args: Parameters<SwiftIOBridgeEvents[E]>
...args: Parameters<NativeBridgeEvents[E]>
): boolean {
return super.emit(event, ...args);
}

View file

@ -10,12 +10,12 @@ class AppContextStore {
const serviceManager = ServiceManager.getInstance();
if (!serviceManager) return; // Silent fail
const swiftBridge = serviceManager.getService("swiftIOBridge");
if (!swiftBridge) {
logger.main.warn("SwiftIOBridge not available");
const nativeBridge = serviceManager.getService("nativeBridge");
if (!nativeBridge) {
logger.main.warn("Native bridge not available");
return;
}
const context = await swiftBridge.call("getAccessibilityContext", {
const context = await nativeBridge.call("getAccessibilityContext", {
editableOnly: false,
});
this.accessibilityContext = context;

View file

@ -0,0 +1,53 @@
import process from "node:process";
/**
* Platform detection utilities
*/
export type Platform = "darwin" | "win32" | "linux";
export function getPlatform(): Platform {
return process.platform as Platform;
}
export function isWindows(): boolean {
return process.platform === "win32";
}
export function isMacOS(): boolean {
return process.platform === "darwin";
}
export function isLinux(): boolean {
return process.platform === "linux";
}
/**
* Get the native helper name for the current platform
*/
export function getNativeHelperName(): string {
return isWindows() ? "WindowsHelper.exe" : "SwiftHelper";
}
/**
* Get the native helper directory name for the current platform
*/
export function getNativeHelperDir(): string {
return isWindows() ? "windows-helper" : "swift-helper";
}
/**
* Get a platform-specific display name
*/
export function getPlatformDisplayName(): string {
switch (process.platform) {
case "darwin":
return "macOS";
case "win32":
return "Windows";
case "linux":
return "Linux";
default:
return process.platform;
}
}

View file

@ -20,7 +20,7 @@ export default defineConfig({
entryFileNames: "[name].js",
},
external: [
"smart-whisper",
"@amical/smart-whisper",
"@libsql/client",
"@libsql/darwin-arm64",
"@libsql/darwin-x64",