Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions apps/code/src/main/db/service.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import path from "node:path";
import type { IStoragePaths } from "@posthog/platform/storage-paths";
import Database from "better-sqlite3";
import {
type BetterSQLite3Database,
drizzle,
} from "drizzle-orm/better-sqlite3";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import { app } from "electron";
import { injectable, postConstruct, preDestroy } from "inversify";
import { inject, injectable, postConstruct, preDestroy } from "inversify";
import { MAIN_TOKENS } from "../di/tokens";
import { logger } from "../utils/logger";

import * as schema from "./schema";
Expand All @@ -20,6 +21,11 @@ export class DatabaseService {
private _db: BetterSQLite3Database<typeof schema> | null = null;
private _sqlite: InstanceType<typeof Database> | null = null;

constructor(
@inject(MAIN_TOKENS.StoragePaths)
private readonly storagePaths: IStoragePaths,
) {}

get db(): BetterSQLite3Database<typeof schema> {
if (!this._db) {
throw new Error("Database not initialized — call initialize() first");
Expand All @@ -29,7 +35,7 @@ export class DatabaseService {

@postConstruct()
initialize(): void {
const dbPath = path.join(app.getPath("userData"), "posthog-code.db");
const dbPath = path.join(this.storagePaths.appDataPath, "posthog-code.db");
log.info("Opening database", {
path: dbPath,
migrationsFolder: MIGRATIONS_FOLDER,
Expand Down
12 changes: 12 additions & 0 deletions apps/code/src/main/di/container.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ import { SuspensionRepositoryImpl } from "../db/repositories/suspension-reposito
import { WorkspaceRepository } from "../db/repositories/workspace-repository";
import { WorktreeRepository } from "../db/repositories/worktree-repository";
import { DatabaseService } from "../db/service";
import { ElectronAppMeta } from "../platform-adapters/electron-app-meta";
import { ElectronClipboard } from "../platform-adapters/electron-clipboard";
import { ElectronDialog } from "../platform-adapters/electron-dialog";
import { ElectronFileIcon } from "../platform-adapters/electron-file-icon";
import { ElectronSecureStorage } from "../platform-adapters/electron-secure-storage";
import { ElectronStoragePaths } from "../platform-adapters/electron-storage-paths";
import { ElectronUrlLauncher } from "../platform-adapters/electron-url-launcher";
import { AgentAuthAdapter } from "../services/agent/auth-adapter";
import { AgentService } from "../services/agent/service";
Expand Down Expand Up @@ -54,6 +60,12 @@ export const container = new Container({
});

container.bind(MAIN_TOKENS.UrlLauncher).to(ElectronUrlLauncher);
container.bind(MAIN_TOKENS.StoragePaths).to(ElectronStoragePaths);
container.bind(MAIN_TOKENS.AppMeta).to(ElectronAppMeta);
container.bind(MAIN_TOKENS.Dialog).to(ElectronDialog);
container.bind(MAIN_TOKENS.Clipboard).to(ElectronClipboard);
container.bind(MAIN_TOKENS.FileIcon).to(ElectronFileIcon);
container.bind(MAIN_TOKENS.SecureStorage).to(ElectronSecureStorage);

container.bind(MAIN_TOKENS.DatabaseService).to(DatabaseService);
container
Expand Down
6 changes: 6 additions & 0 deletions apps/code/src/main/di/tokens.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
export const MAIN_TOKENS = Object.freeze({
// Platform ports (host-agnostic interfaces from @posthog/platform)
UrlLauncher: Symbol.for("Platform.UrlLauncher"),
StoragePaths: Symbol.for("Platform.StoragePaths"),
AppMeta: Symbol.for("Platform.AppMeta"),
Dialog: Symbol.for("Platform.Dialog"),
Clipboard: Symbol.for("Platform.Clipboard"),
FileIcon: Symbol.for("Platform.FileIcon"),
SecureStorage: Symbol.for("Platform.SecureStorage"),

// Stores
SettingsStore: Symbol.for("Main.SettingsStore"),
Expand Down
14 changes: 14 additions & 0 deletions apps/code/src/main/platform-adapters/electron-app-meta.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { IAppMeta } from "@posthog/platform/app-meta";
import { app } from "electron";
import { injectable } from "inversify";

@injectable()
export class ElectronAppMeta implements IAppMeta {
public get version(): string {
return app.getVersion();
}

public get isProduction(): boolean {
return app.isPackaged;
}
}
10 changes: 10 additions & 0 deletions apps/code/src/main/platform-adapters/electron-clipboard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { IClipboard } from "@posthog/platform/clipboard";
import { clipboard } from "electron";
import { injectable } from "inversify";

@injectable()
export class ElectronClipboard implements IClipboard {
public async writeText(text: string): Promise<void> {
clipboard.writeText(text);
}
}
61 changes: 61 additions & 0 deletions apps/code/src/main/platform-adapters/electron-dialog.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type {
ConfirmOptions,
DialogSeverity,
IDialog,
PickFileOptions,
} from "@posthog/platform/dialog";
import {
BrowserWindow,
dialog,
type MessageBoxOptions,
type OpenDialogOptions,
} from "electron";
import { injectable } from "inversify";

type OpenDialogProperty = NonNullable<OpenDialogOptions["properties"]>[number];

function severityToType(severity?: DialogSeverity): MessageBoxOptions["type"] {
return severity ?? "none";
}

function buildProperties(options: PickFileOptions): OpenDialogProperty[] {
const properties: OpenDialogProperty[] = [
options.directories ? "openDirectory" : "openFile",
"treatPackageAsDirectory",
];
if (options.multiple) properties.push("multiSelections");
if (options.createDirectories) properties.push("createDirectory");
return properties;
}

@injectable()
export class ElectronDialog implements IDialog {
public async confirm(options: ConfirmOptions): Promise<number> {
const parent = BrowserWindow.getFocusedWindow();
const electronOptions: MessageBoxOptions = {
type: severityToType(options.severity),
title: options.title,
message: options.message,
detail: options.detail,
buttons: options.options,
defaultId: options.defaultIndex,
cancelId: options.cancelIndex,
};
const result = parent
? await dialog.showMessageBox(parent, electronOptions)
: await dialog.showMessageBox(electronOptions);
return result.response;
}

public async pickFile(options: PickFileOptions): Promise<string[]> {
const parent = BrowserWindow.getFocusedWindow();
const electronOptions: OpenDialogOptions = {
title: options.title,
properties: buildProperties(options),
};
const result = parent
? await dialog.showOpenDialog(parent, electronOptions)
: await dialog.showOpenDialog(electronOptions);
return result.canceled ? [] : result.filePaths;
}
}
34 changes: 34 additions & 0 deletions apps/code/src/main/platform-adapters/electron-file-icon.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type { IFileIcon } from "@posthog/platform/file-icon";
import { app } from "electron";
import { injectable } from "inversify";

type FileIconModule = typeof import("file-icon");

@injectable()
export class ElectronFileIcon implements IFileIcon {
private fileIconModule: FileIconModule | undefined;

public async getAsDataUrl(filePath: string): Promise<string | null> {
try {
if (process.platform === "darwin") {
const mod = await this.loadFileIconModule();
const uint8Array = await mod.fileIconToBuffer(filePath, { size: 64 });
const base64 = Buffer.from(uint8Array).toString("base64");
return `data:image/png;base64,${base64}`;
}

const icon = await app.getFileIcon(filePath, { size: "normal" });
const base64 = icon.toPNG().toString("base64");
return `data:image/png;base64,${base64}`;
} catch {
return null;
}
}

private async loadFileIconModule(): Promise<FileIconModule> {
if (!this.fileIconModule) {
this.fileIconModule = await import("file-icon");
}
return this.fileIconModule;
}
}
19 changes: 19 additions & 0 deletions apps/code/src/main/platform-adapters/electron-secure-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ISecureStorage } from "@posthog/platform/secure-storage";
import { safeStorage } from "electron";
import { injectable } from "inversify";

@injectable()
export class ElectronSecureStorage implements ISecureStorage {
public isAvailable(): boolean {
return safeStorage.isEncryptionAvailable();
}

public async encryptString(text: string): Promise<Uint8Array> {
const buffer = safeStorage.encryptString(text);
return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.byteLength);
}

public async decryptString(data: Uint8Array): Promise<string> {
return safeStorage.decryptString(Buffer.from(data));
}
}
14 changes: 14 additions & 0 deletions apps/code/src/main/platform-adapters/electron-storage-paths.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import type { IStoragePaths } from "@posthog/platform/storage-paths";
import { app } from "electron";
import { injectable } from "inversify";

@injectable()
export class ElectronStoragePaths implements IStoragePaths {
public get appDataPath(): string {
return app.getPath("userData");
}

public get logsPath(): string {
return app.getPath("logs");
}
}
25 changes: 10 additions & 15 deletions apps/code/src/main/services/context-menu/service.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,8 @@
import type { IDialog } from "@posthog/platform/dialog";
import type { DetectedApplication } from "@shared/types";
import {
dialog,
Menu,
type MenuItemConstructorOptions,
nativeImage,
} from "electron";
import { Menu, type MenuItemConstructorOptions, nativeImage } from "electron";
import { inject, injectable } from "inversify";
import { MAIN_TOKENS } from "../../di/tokens";
import { getMainWindow } from "../../trpc/context";
import type { ExternalAppsService } from "../external-apps/service";
import type {
ArchivedTaskAction,
Expand Down Expand Up @@ -46,6 +41,8 @@ export class ContextMenuService {
constructor(
@inject(MAIN_TOKENS.ExternalAppsService)
private readonly externalAppsService: ExternalAppsService,
@inject(MAIN_TOKENS.Dialog)
private readonly dialog: IDialog,
) {}

private async getExternalAppsData() {
Expand Down Expand Up @@ -348,17 +345,15 @@ export class ContextMenuService {
}

private async confirm(options: ConfirmOptions): Promise<boolean> {
const win = getMainWindow();
const result = await dialog.showMessageBox({
...(win ? { parent: win } : {}),
type: "question",
const response = await this.dialog.confirm({
severity: "question",
title: options.title,
message: options.message,
detail: options.detail,
buttons: ["Cancel", options.confirmLabel],
defaultId: 1,
cancelId: 0,
options: ["Cancel", options.confirmLabel],
defaultIndex: 1,
cancelIndex: 0,
});
return result.response === 1;
return response === 1;
}
}
47 changes: 17 additions & 30 deletions apps/code/src/main/services/external-apps/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,13 @@ import { exec } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { promisify } from "node:util";
import type { IClipboard } from "@posthog/platform/clipboard";
import type { IFileIcon } from "@posthog/platform/file-icon";
import type { IStoragePaths } from "@posthog/platform/storage-paths";
import type { DetectedApplication } from "@shared/types";
import { app, clipboard } from "electron";
import Store from "electron-store";
import { injectable } from "inversify";
import { inject, injectable } from "inversify";
import { MAIN_TOKENS } from "../../di/tokens";
import type { AppDefinition, ExternalAppsSchema } from "./types";

const execAsync = promisify(exec);
Expand Down Expand Up @@ -475,46 +478,30 @@ export class ExternalAppsService {
explorer: "Explorer",
};

private fileIconModule: typeof import("file-icon") | null = null;
private cachedApps: DetectedApplication[] | null = null;
private detectionPromise: Promise<DetectedApplication[]> | null = null;
private prefsStore: Store<ExternalAppsSchema>;

constructor() {
constructor(
@inject(MAIN_TOKENS.StoragePaths)
private readonly storagePaths: IStoragePaths,
@inject(MAIN_TOKENS.Clipboard)
private readonly clipboard: IClipboard,
@inject(MAIN_TOKENS.FileIcon)
private readonly fileIcon: IFileIcon,
) {
this.prefsStore = new Store<ExternalAppsSchema>({
name: "external-apps",
cwd: app.getPath("userData"),
cwd: this.storagePaths.appDataPath,
defaults: {
externalAppsPrefs: {},
},
});
}

private async getFileIcon() {
if (!this.fileIconModule) {
this.fileIconModule = await import("file-icon");
}
return this.fileIconModule;
}

private async extractIcon(appPath: string): Promise<string | undefined> {
try {
if (process.platform === "darwin") {
const fileIconModule = await this.getFileIcon();
const uint8Array = await fileIconModule.fileIconToBuffer(appPath, {
size: 64,
});
const buffer = Buffer.from(uint8Array);
const base64 = buffer.toString("base64");
return `data:image/png;base64,${base64}`;
}

const icon = await app.getFileIcon(appPath, { size: "normal" });
const base64 = icon.toPNG().toString("base64");
return `data:image/png;base64,${base64}`;
} catch {
return undefined;
}
const dataUrl = await this.fileIcon.getAsDataUrl(appPath);
return dataUrl ?? undefined;
}

private async findWin32Executable(
Expand Down Expand Up @@ -666,7 +653,7 @@ export class ExternalAppsService {
}

async copyPath(targetPath: string): Promise<void> {
clipboard.writeText(targetPath);
await this.clipboard.writeText(targetPath);
}

getPrefsStore() {
Expand Down
Loading
Loading