diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ea00ec045..a9eed0d58 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -33,6 +33,9 @@ jobs: - name: Build electron-trpc run: pnpm --filter @posthog/electron-trpc build + - name: Build platform + run: pnpm --filter @posthog/platform build + - name: Build shared run: pnpm --filter @posthog/shared build diff --git a/.github/workflows/code-release.yml b/.github/workflows/code-release.yml index 8a20432e8..e582d21c2 100644 --- a/.github/workflows/code-release.yml +++ b/.github/workflows/code-release.yml @@ -85,6 +85,9 @@ jobs: - name: Build electron-trpc package run: pnpm --filter @posthog/electron-trpc run build + - name: Build platform package + run: pnpm --filter @posthog/platform run build + - name: Build shared package run: pnpm --filter @posthog/shared run build @@ -182,6 +185,9 @@ jobs: - name: Build electron-trpc package run: pnpm --filter @posthog/electron-trpc run build + - name: Build platform package + run: pnpm --filter @posthog/platform run build + - name: Build shared package run: pnpm --filter @posthog/shared run build diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ea1cc7038..db5b51cbe 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -78,6 +78,7 @@ jobs: - name: Build packages run: | pnpm --filter @posthog/electron-trpc build & + pnpm --filter @posthog/platform build & pnpm --filter @posthog/shared build pnpm --filter @posthog/git build pnpm --filter agent build & diff --git a/apps/code/package.json b/apps/code/package.json index f765eca1d..7a608e704 100644 --- a/apps/code/package.json +++ b/apps/code/package.json @@ -134,6 +134,7 @@ "@posthog/quill-components": "link:/Users/adamleithp/Dev/posthog/packages/quill/packages/components", "@posthog/quill-primitives": "link:/Users/adamleithp/Dev/posthog/packages/quill/packages/primitives", "@posthog/quill-tokens": "link:/Users/adamleithp/Dev/posthog/packages/quill/packages/tokens", + "@posthog/platform": "workspace:*", "@posthog/shared": "workspace:*", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-icons": "^1.3.2", diff --git a/apps/code/scripts/postinstall.sh b/apps/code/scripts/postinstall.sh index 3be076571..d96063fef 100755 --- a/apps/code/scripts/postinstall.sh +++ b/apps/code/scripts/postinstall.sh @@ -8,6 +8,19 @@ set -e REPO_ROOT="$(cd ../.. && pwd)" SCRIPTS_DIR="$(cd "$(dirname "$0")" && pwd)" +# Self-heal missing Electron binary. +# pnpm skips package-level postinstall scripts when the lockfile is already +# satisfied, so if node_modules/electron/dist gets wiped (interrupted download, +# cache eviction, arch change, manual cleanup), `pnpm install` won't notice — +# and `electron-forge start` then fails with "Electron failed to install +# correctly, please delete node_modules/electron and try installing again". +# Detect the missing binary and invoke Electron's own install script to fetch it. +ELECTRON_DIST="$REPO_ROOT/node_modules/electron/dist" +if [ ! -d "$ELECTRON_DIST" ] || [ -z "$(ls -A "$ELECTRON_DIST" 2>/dev/null)" ]; then + echo "Electron binary missing at $ELECTRON_DIST — downloading..." + node "$REPO_ROOT/node_modules/electron/install.js" +fi + echo "Rebuilding native modules for Electron..." cd "$REPO_ROOT" diff --git a/apps/code/src/main/di/container.ts b/apps/code/src/main/di/container.ts index f45163c53..01d0a2313 100644 --- a/apps/code/src/main/di/container.ts +++ b/apps/code/src/main/di/container.ts @@ -9,6 +9,7 @@ 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 { ElectronUrlLauncher } from "../platform-adapters/electron-url-launcher"; import { AgentAuthAdapter } from "../services/agent/auth-adapter"; import { AgentService } from "../services/agent/service"; import { AppLifecycleService } from "../services/app-lifecycle/service"; @@ -52,6 +53,8 @@ export const container = new Container({ defaultScope: "Singleton", }); +container.bind(MAIN_TOKENS.UrlLauncher).to(ElectronUrlLauncher); + container.bind(MAIN_TOKENS.DatabaseService).to(DatabaseService); container .bind(MAIN_TOKENS.AuthPreferenceRepository) diff --git a/apps/code/src/main/di/tokens.ts b/apps/code/src/main/di/tokens.ts index 27bdbcafc..08c326b76 100644 --- a/apps/code/src/main/di/tokens.ts +++ b/apps/code/src/main/di/tokens.ts @@ -5,6 +5,9 @@ * Never import this file from renderer code. */ export const MAIN_TOKENS = Object.freeze({ + // Platform ports (host-agnostic interfaces from @posthog/platform) + UrlLauncher: Symbol.for("Platform.UrlLauncher"), + // Stores SettingsStore: Symbol.for("Main.SettingsStore"), diff --git a/apps/code/src/main/platform-adapters/electron-url-launcher.ts b/apps/code/src/main/platform-adapters/electron-url-launcher.ts new file mode 100644 index 000000000..d89ac835d --- /dev/null +++ b/apps/code/src/main/platform-adapters/electron-url-launcher.ts @@ -0,0 +1,10 @@ +import type { IUrlLauncher } from "@posthog/platform/url-launcher"; +import { shell } from "electron"; +import { injectable } from "inversify"; + +@injectable() +export class ElectronUrlLauncher implements IUrlLauncher { + public async launch(url: string): Promise { + await shell.openExternal(url); + } +} diff --git a/apps/code/src/main/services/github-integration/service.ts b/apps/code/src/main/services/github-integration/service.ts index 5a69ff13c..6d78188c8 100644 --- a/apps/code/src/main/services/github-integration/service.ts +++ b/apps/code/src/main/services/github-integration/service.ts @@ -1,6 +1,7 @@ +import type { IUrlLauncher } from "@posthog/platform/url-launcher"; import { getCloudUrlFromRegion } from "@shared/constants/oauth"; -import { shell } from "electron"; -import { injectable } from "inversify"; +import { inject, injectable } from "inversify"; +import { MAIN_TOKENS } from "../../di/tokens"; import { logger } from "../../utils/logger"; import type { CloudRegion, StartGitHubFlowOutput } from "./schemas"; @@ -8,6 +9,10 @@ const log = logger.scope("github-integration-service"); @injectable() export class GitHubIntegrationService { + constructor( + @inject(MAIN_TOKENS.UrlLauncher) private readonly urlLauncher: IUrlLauncher, + ) {} + public async startFlow( region: CloudRegion, projectId: number, @@ -18,7 +23,7 @@ export class GitHubIntegrationService { const authorizeUrl = `${cloudUrl}/api/environments/${projectId}/integrations/authorize/?kind=github&next=${encodeURIComponent(next)}`; log.info("Opening GitHub authorization URL in browser"); - await shell.openExternal(authorizeUrl); + await this.urlLauncher.launch(authorizeUrl); return { success: true }; } catch (error) { diff --git a/mprocs.yaml b/mprocs.yaml index 6c357a008..73a2de3fd 100644 --- a/mprocs.yaml +++ b/mprocs.yaml @@ -6,6 +6,7 @@ procs: depends_on: - agent - git + - platform agent: shell: 'node scripts/pnpm-run.mjs --filter agent run dev' @@ -13,6 +14,9 @@ procs: git: shell: 'node scripts/pnpm-run.mjs --filter @posthog/git run dev' + platform: + shell: 'node scripts/pnpm-run.mjs --filter @posthog/platform run dev' + enricher: shell: 'node scripts/pnpm-run.mjs --filter @posthog/enricher run dev' diff --git a/packages/platform/package.json b/packages/platform/package.json new file mode 100644 index 000000000..1d15e7f56 --- /dev/null +++ b/packages/platform/package.json @@ -0,0 +1,26 @@ +{ + "name": "@posthog/platform", + "version": "1.0.0", + "description": "Host-agnostic platform port interfaces. Zero runtime deps; implemented by per-host adapters (Electron, Node server, React Native, web).", + "type": "module", + "exports": { + "./url-launcher": { + "types": "./dist/url-launcher.d.ts", + "import": "./dist/url-launcher.js" + } + }, + "scripts": { + "build": "tsup", + "dev": "tsup --watch", + "typecheck": "tsc --noEmit", + "clean": "node ../../scripts/rimraf.mjs dist .turbo" + }, + "devDependencies": { + "tsup": "^8.5.1", + "typescript": "^5.5.0" + }, + "files": [ + "dist/**/*", + "src/**/*" + ] +} diff --git a/packages/platform/src/url-launcher.ts b/packages/platform/src/url-launcher.ts new file mode 100644 index 000000000..16edc5142 --- /dev/null +++ b/packages/platform/src/url-launcher.ts @@ -0,0 +1,3 @@ +export interface IUrlLauncher { + launch(url: string): Promise; +} diff --git a/packages/platform/tsconfig.json b/packages/platform/tsconfig.json new file mode 100644 index 000000000..c1475909f --- /dev/null +++ b/packages/platform/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "lib": ["ESNext"], + "target": "ES2022", + "module": "ESNext", + "moduleDetection": "force", + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +} diff --git a/packages/platform/tsup.config.ts b/packages/platform/tsup.config.ts new file mode 100644 index 000000000..e5fad5642 --- /dev/null +++ b/packages/platform/tsup.config.ts @@ -0,0 +1,12 @@ +import { defineConfig } from "tsup"; + +export default defineConfig({ + entry: ["src/url-launcher.ts"], + format: ["esm"], + dts: true, + sourcemap: true, + clean: true, + splitting: false, + outDir: "dist", + target: "es2022", +}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 477ba393e..92c0d1e8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -169,6 +169,9 @@ importers: '@posthog/hedgehog-mode': specifier: ^0.0.48 version: 0.0.48(prop-types@15.8.1)(react-dom@19.1.0(react@19.1.0))(react@19.1.0) + '@posthog/platform': + specifier: workspace:* + version: link:../../packages/platform '@posthog/quill-blocks': specifier: link:/Users/adamleithp/Dev/posthog/packages/quill/packages/blocks version: link:../../../../../adamleithp/Dev/posthog/packages/quill/packages/blocks @@ -790,6 +793,15 @@ importers: specifier: ^2.1.8 version: 2.1.9(@types/node@25.2.0)(jsdom@26.1.0)(lightningcss@1.32.0)(msw@2.12.8(@types/node@25.2.0)(typescript@5.9.3))(terser@5.46.0) + packages/platform: + devDependencies: + tsup: + specifier: ^8.5.1 + version: 8.5.1(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) + typescript: + specifier: ^5.5.0 + version: 5.9.3 + packages/shared: devDependencies: '@agentclientprotocol/sdk':