From 7376e82ac57d41dfcb3abbafd2c187ef36196dc4 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 16:36:51 -0500 Subject: [PATCH 01/13] Enable macOS distributable builds in CI workflows - Add macOS to verify.yml workflow matrix to test builds on both Windows and macOS - Enable macOS in publish.yml workflow matrix to publish macOS distributables - Both workflows now run on windows-latest and macos-latest This enables building and testing macOS DMG and ZIP distributables in CI. --- .github/workflows/publish.yml | 17 ++++++++--------- .github/workflows/verify.yml | 9 ++++++++- 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f817357..cfecc01 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -4,15 +4,14 @@ on: jobs: publish: - runs-on: windows-latest - # strategy: - # matrix: - # os: - # [ - # { name: 'windows', image: 'windows-latest' }, - # { name: 'macos', image: 'macos-latest' }, - # ] - # runs-on: ${{ matrix.os.image }} + strategy: + matrix: + os: + [ + { name: 'windows', image: 'windows-latest' }, + { name: 'macos', image: 'macos-latest' }, + ] + runs-on: ${{ matrix.os.image }} permissions: contents: write deployments: write diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 8140532..b31f01a 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -6,7 +6,14 @@ on: jobs: test: - runs-on: windows-latest + strategy: + matrix: + os: + [ + { name: 'windows', image: 'windows-latest' }, + { name: 'macos', image: 'macos-latest' }, + ] + runs-on: ${{ matrix.os.image }} permissions: contents: read id-token: write From 37b8f211db2dd88ed0eae86d53555326dc058edd Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 16:59:06 -0500 Subject: [PATCH 02/13] Fix macOS build: remove symlinks and add multi-architecture support - Add packageAfterPrune hook to remove problematic .bin symlinks that break ASAR packaging - Recursively remove all .bin directories and symlinks from node_modules - Add architecture matrix to workflows for building both arm64 and x64 macOS distributables - Windows builds unchanged (no architecture specified) - macOS builds now test both arm64 and x64 architectures The symlink removal is safe as .bin directories are only needed for development (npm/yarn scripts), not at runtime. --- .github/workflows/publish.yml | 19 ++++++++++++------- .github/workflows/verify.yml | 19 ++++++++++++------- forge.config.ts | 32 ++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+), 14 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index cfecc01..e6256ed 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -6,12 +6,17 @@ jobs: publish: strategy: matrix: - os: - [ - { name: 'windows', image: 'windows-latest' }, - { name: 'macos', image: 'macos-latest' }, - ] - runs-on: ${{ matrix.os.image }} + include: + - os: windows + image: windows-latest + arch: "" + - os: macos + image: macos-latest + arch: arm64 + - os: macos + image: macos-latest + arch: x64 + runs-on: ${{ matrix.image }} permissions: contents: write deployments: write @@ -23,7 +28,7 @@ jobs: - run: yarn lint - run: yarn run typecheck - run: yarn run build:docs - - run: yarn make + - run: yarn make ${{ matrix.arch != '' && '--arch=' || '' }}${{ matrix.arch }} - uses: 'paramsinghvc/gh-action-bump-version@master' env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index b31f01a..9a7f99a 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -8,12 +8,17 @@ jobs: test: strategy: matrix: - os: - [ - { name: 'windows', image: 'windows-latest' }, - { name: 'macos', image: 'macos-latest' }, - ] - runs-on: ${{ matrix.os.image }} + include: + - os: windows + image: windows-latest + arch: "" + - os: macos + image: macos-latest + arch: arm64 + - os: macos + image: macos-latest + arch: x64 + runs-on: ${{ matrix.image }} permissions: contents: read id-token: write @@ -26,4 +31,4 @@ jobs: - run: yarn run lint - run: yarn run typecheck - run: yarn run build:docs - - run: yarn run make \ No newline at end of file + - run: yarn run make ${{ matrix.arch != '' && '--arch=' || '' }}${{ matrix.arch }} \ No newline at end of file diff --git a/forge.config.ts b/forge.config.ts index 8550a7d..a159702 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -104,6 +104,38 @@ const config: ForgeConfig = { ], hooks: { + packageAfterPrune: async (config, buildPath) => { + // Remove problematic symlinks in nested node_modules/.bin directories that break ASAR + // These symlinks point outside the package and cause ASAR packaging to fail. + // .bin directories are only needed for development (npm/yarn scripts), not at runtime. + const removeBinSymlinks = async (dir: string) => { + try { + const entries = await fs.readdir(dir, { withFileTypes: true }); + for (const entry of entries) { + const fullPath = path.join(dir, entry.name); + if (entry.isDirectory()) { + // Recursively check all directories + await removeBinSymlinks(fullPath); + + // If this is a .bin directory, remove it entirely + if (entry.name === ".bin") { + await fs.remove(fullPath); + } + } else if (entry.isSymbolicLink()) { + // Remove individual symlinks + await fs.unlink(fullPath); + } + } + } catch (error) { + // Ignore errors - directory might not exist or be inaccessible + } + }; + + const nodeModulesPath = path.join(buildPath, "node_modules"); + if (await fs.pathExists(nodeModulesPath)) { + await removeBinSymlinks(nodeModulesPath); + } + }, generateAssets: async (config, platform, arch) => { const ffmpegBase = path.join( __dirname, From d2c608d9dae2797aa46fd1fcc92942bcb38f1615 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 17:11:12 -0500 Subject: [PATCH 03/13] Fix CI: use macos-15-intel for x64 builds and fix console statements - Change x64 macOS builds to use macos-15-intel runner (Intel hardware) - macos-latest runners are ARM64 and cannot natively build x64 --- .github/workflows/publish.yml | 2 +- .github/workflows/verify.yml | 2 +- .gitignore | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e6256ed..e089af5 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -14,7 +14,7 @@ jobs: image: macos-latest arch: arm64 - os: macos - image: macos-latest + image: macos-15-intel arch: x64 runs-on: ${{ matrix.image }} permissions: diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml index 9a7f99a..a8b68e1 100644 --- a/.github/workflows/verify.yml +++ b/.github/workflows/verify.yml @@ -16,7 +16,7 @@ jobs: image: macos-latest arch: arm64 - os: macos - image: macos-latest + image: macos-15-intel arch: x64 runs-on: ${{ matrix.image }} permissions: diff --git a/.gitignore b/.gitignore index 4953bc6..69db27d 100644 --- a/.gitignore +++ b/.gitignore @@ -114,4 +114,5 @@ extra dist-docs docs/index.md docs/images -.vscode \ No newline at end of file +.vscode +vector-store/vector-store.db From f1eb7cb5d30cc673843620e7b62911e1fb89a7cb Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 17:17:44 -0500 Subject: [PATCH 04/13] Make GitHub publisher repository dynamic based on fork - Update forge.config.ts to read repository owner and name from environment variables - Owner: Uses GITHUB_REPOSITORY_OWNER or parses GITHUB_REPOSITORY - Name: Parses GITHUB_REPOSITORY to extract repo name - Falls back to hardcoded 'lukasbach/pensieve' if env vars not set - Update publish.yml workflow to pass repository info as environment variables - Sets GITHUB_REPOSITORY_OWNER from github.repository_owner - Sets GITHUB_REPOSITORY from github.repository - Allows publishing releases to any fork that runs the workflow --- .github/workflows/publish.yml | 2 ++ forge.config.ts | 9 +++++++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index e089af5..407edfd 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -35,3 +35,5 @@ jobs: - run: yarn publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_REPOSITORY_OWNER: ${{ github.repository_owner }} + GITHUB_REPOSITORY: ${{ github.repository }} diff --git a/forge.config.ts b/forge.config.ts index a159702..5a97962 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -93,8 +93,13 @@ const config: ForgeConfig = { name: "@electron-forge/publisher-github", config: { repository: { - owner: "lukasbach", - name: "pensieve", + owner: + process.env.GITHUB_REPOSITORY_OWNER || + process.env.GITHUB_REPOSITORY?.split("/")[0] || + "lukasbach", + name: + process.env.GITHUB_REPOSITORY?.split("/")[1] || + "pensieve", }, prerelease: false, draft: true, From 77ddd080ae1f9d298a51ffd96f35c632ed1e9025 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 17:23:18 -0500 Subject: [PATCH 05/13] Fix linting error: format repository name on single line --- forge.config.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/forge.config.ts b/forge.config.ts index 5a97962..7502b1c 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -97,9 +97,7 @@ const config: ForgeConfig = { process.env.GITHUB_REPOSITORY_OWNER || process.env.GITHUB_REPOSITORY?.split("/")[0] || "lukasbach", - name: - process.env.GITHUB_REPOSITORY?.split("/")[1] || - "pensieve", + name: process.env.GITHUB_REPOSITORY?.split("/")[1] || "pensieve", }, prerelease: false, draft: true, From 53c61191a0a47ddedb72d61522dd2c22b7cd18c6 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 17:52:46 -0500 Subject: [PATCH 06/13] Fix version bump conflict: run in separate job before matrix builds - Move version bump to dedicated 'bump-version' job that runs first - Add 'needs: bump-version' to publish job so it waits for version bump - Prevents conflicts when multiple matrix jobs try to bump version simultaneously - Each publish job will checkout the code with the updated version already committed --- .github/workflows/publish.yml | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 407edfd..9414512 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,18 @@ on: workflow_dispatch: jobs: + bump-version: + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + - uses: 'paramsinghvc/gh-action-bump-version@master' + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + publish: + needs: bump-version strategy: matrix: include: @@ -29,9 +40,6 @@ jobs: - run: yarn run typecheck - run: yarn run build:docs - run: yarn make ${{ matrix.arch != '' && '--arch=' || '' }}${{ matrix.arch }} - - uses: 'paramsinghvc/gh-action-bump-version@master' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: yarn publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From 088fdd86a8ce4ea0c29221cb47d7dcf981ce10a4 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 17:57:20 -0500 Subject: [PATCH 07/13] Bump version only after all builds succeed - Split workflow into three jobs: build, bump-version, publish - Build job runs all platforms in parallel, uploads distributables as artifacts - Bump-version job only runs if all builds succeed (needs: build) - Publish job downloads pre-built distributables and publishes them - Version is only incremented if builds succeed, preventing version bumps on failures - Uses artifact_suffix in matrix for consistent artifact naming --- .github/workflows/publish.yml | 48 ++++++++++++++++++++++++++++++++--- 1 file changed, 44 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9414512..57687ca 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,7 +3,43 @@ on: workflow_dispatch: jobs: + build: + strategy: + matrix: + include: + - os: windows + image: windows-latest + arch: "" + artifact_suffix: "windows-default" + - os: macos + image: macos-latest + arch: arm64 + artifact_suffix: "macos-arm64" + - os: macos + image: macos-15-intel + arch: x64 + artifact_suffix: "macos-x64" + runs-on: ${{ matrix.image }} + permissions: + contents: read + steps: + - run: git config --global core.autocrlf false + - uses: actions/checkout@v4 + - uses: volta-cli/action@v4 + - run: yarn install + - run: yarn lint + - run: yarn run typecheck + - run: yarn run build:docs + - run: yarn make ${{ matrix.arch != '' && '--arch=' || '' }}${{ matrix.arch }} + - name: Upload distributables + uses: actions/upload-artifact@v4 + with: + name: distributables-${{ matrix.artifact_suffix }} + path: out/make + retention-days: 1 + bump-version: + needs: build runs-on: ubuntu-latest permissions: contents: write @@ -21,12 +57,15 @@ jobs: - os: windows image: windows-latest arch: "" + artifact_suffix: "windows-default" - os: macos image: macos-latest arch: arm64 + artifact_suffix: "macos-arm64" - os: macos image: macos-15-intel arch: x64 + artifact_suffix: "macos-x64" runs-on: ${{ matrix.image }} permissions: contents: write @@ -36,10 +75,11 @@ jobs: - uses: actions/checkout@v4 - uses: volta-cli/action@v4 - run: yarn install - - run: yarn lint - - run: yarn run typecheck - - run: yarn run build:docs - - run: yarn make ${{ matrix.arch != '' && '--arch=' || '' }}${{ matrix.arch }} + - name: Download distributables + uses: actions/download-artifact@v4 + with: + name: distributables-${{ matrix.artifact_suffix }} + path: out/make - run: yarn publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From bca725a78fb26600afee1b971b0e573128db47ad Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 18:04:06 -0500 Subject: [PATCH 08/13] Skip version bump for non-upstream repositories - Add condition to bump-version job: only runs if github.repository == 'lukasbach/pensieve' - Update publish job to depend on both build and bump-version - Publish job runs if bump-version succeeded or was skipped (for forks) - Allows forks to publish releases without version bumps - Version bumps should only happen in upstream CI --- .github/workflows/publish.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 57687ca..5542275 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -40,6 +40,7 @@ jobs: bump-version: needs: build + if: github.repository == 'lukasbach/pensieve' runs-on: ubuntu-latest permissions: contents: write @@ -50,7 +51,8 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish: - needs: bump-version + needs: [build, bump-version] + if: always() && (needs.bump-version.result == 'success' || needs.bump-version.result == 'skipped') strategy: matrix: include: From e41d30e2caaf2c4637854d603ac5799cb57dabf1 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 18:45:09 -0500 Subject: [PATCH 09/13] Revert 'Bump version only after all builds succeed' (088fdd8) - Remove build-first approach that separated build and publish jobs - Keep fork-friendly logic from bca725a (skip version bump for non-upstream) - Restore simpler workflow: bump-version -> publish (builds and publishes) - Version bump happens before builds, ensuring correct version in distributables --- .github/workflows/publish.yml | 50 ++++------------------------------- 1 file changed, 5 insertions(+), 45 deletions(-) diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 5542275..f3b62e1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -3,43 +3,7 @@ on: workflow_dispatch: jobs: - build: - strategy: - matrix: - include: - - os: windows - image: windows-latest - arch: "" - artifact_suffix: "windows-default" - - os: macos - image: macos-latest - arch: arm64 - artifact_suffix: "macos-arm64" - - os: macos - image: macos-15-intel - arch: x64 - artifact_suffix: "macos-x64" - runs-on: ${{ matrix.image }} - permissions: - contents: read - steps: - - run: git config --global core.autocrlf false - - uses: actions/checkout@v4 - - uses: volta-cli/action@v4 - - run: yarn install - - run: yarn lint - - run: yarn run typecheck - - run: yarn run build:docs - - run: yarn make ${{ matrix.arch != '' && '--arch=' || '' }}${{ matrix.arch }} - - name: Upload distributables - uses: actions/upload-artifact@v4 - with: - name: distributables-${{ matrix.artifact_suffix }} - path: out/make - retention-days: 1 - bump-version: - needs: build if: github.repository == 'lukasbach/pensieve' runs-on: ubuntu-latest permissions: @@ -51,7 +15,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} publish: - needs: [build, bump-version] + needs: bump-version if: always() && (needs.bump-version.result == 'success' || needs.bump-version.result == 'skipped') strategy: matrix: @@ -59,15 +23,12 @@ jobs: - os: windows image: windows-latest arch: "" - artifact_suffix: "windows-default" - os: macos image: macos-latest arch: arm64 - artifact_suffix: "macos-arm64" - os: macos image: macos-15-intel arch: x64 - artifact_suffix: "macos-x64" runs-on: ${{ matrix.image }} permissions: contents: write @@ -77,11 +38,10 @@ jobs: - uses: actions/checkout@v4 - uses: volta-cli/action@v4 - run: yarn install - - name: Download distributables - uses: actions/download-artifact@v4 - with: - name: distributables-${{ matrix.artifact_suffix }} - path: out/make + - run: yarn lint + - run: yarn run typecheck + - run: yarn run build:docs + - run: yarn make ${{ matrix.arch != '' && '--arch=' || '' }}${{ matrix.arch }} - run: yarn publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} From d37c4435e8637ea125cbca1acb0280c81992af86 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 19:10:44 -0500 Subject: [PATCH 10/13] Add macOS .icns icon generation support - Generate .icns file for macOS using iconutil - Create all required macOS icon sizes (16x16 through 1024x1024 with @2x variants) - Update packagerConfig to use base icon path for automatic platform detection - Update MakerDMG to use .icns format - Rename Windows ICO from icon@8x.ico to icon.ico for standard naming - Generate both .ico and .icns formats for cross-platform compatibility --- forge.config.ts | 64 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 4 deletions(-) diff --git a/forge.config.ts b/forge.config.ts index 7502b1c..a02fb81 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -12,6 +12,10 @@ import fs from "fs-extra"; import path from "path"; import { Resvg } from "@resvg/resvg-js"; import pngToIco from "png-to-ico"; +import { exec } from "child_process"; +import { promisify } from "util"; + +const execAsync = promisify(exec); const createIcon = async (factor: number, base = 32) => { const source = await fs.readFile(path.join(__dirname, "./icon.svg"), "utf-8"); @@ -30,26 +34,70 @@ const createIcon = async (factor: number, base = 32) => { ); }; +const createIcns = async () => { + const extraDir = path.join(__dirname, "extra"); + const iconsetDir = path.join(extraDir, "icon.iconset"); + await fs.ensureDir(iconsetDir); + + // macOS requires specific icon sizes in .iconset format + // Format: icon_{size}x{size}.png and icon_{size}x{size}@2x.png + const sizes = [ + { size: 16, filename: "icon_16x16.png" }, + { size: 32, filename: "icon_16x16@2x.png" }, // 32x32 for @2x + { size: 32, filename: "icon_32x32.png" }, + { size: 64, filename: "icon_32x32@2x.png" }, // 64x64 for @2x + { size: 128, filename: "icon_128x128.png" }, + { size: 256, filename: "icon_128x128@2x.png" }, // 256x256 for @2x + { size: 256, filename: "icon_256x256.png" }, + { size: 512, filename: "icon_256x256@2x.png" }, // 512x512 for @2x + { size: 512, filename: "icon_512x512.png" }, + { size: 1024, filename: "icon_512x512@2x.png" }, // 1024x1024 for @2x + ]; + + // Generate all required icon sizes + for (const { size, filename } of sizes) { + const source = await fs.readFile(path.join(__dirname, "./icon.svg"), "utf-8"); + const resvg = new Resvg(source, { + background: "transparent", + fitTo: { mode: "width", value: size }, + }); + const png = resvg.render(); + await fs.writeFile( + path.join(iconsetDir, filename), + png.asPng() as any, + ); + } + + // Convert .iconset to .icns using macOS iconutil + const icnsPath = path.join(extraDir, "icon.icns"); + await execAsync(`iconutil -c icns "${iconsetDir}" -o "${icnsPath}"`); + + // Clean up the .iconset directory + await fs.remove(iconsetDir); +}; + const config: ForgeConfig = { packagerConfig: { asar: { unpack: "*.{node,dll,exe}", }, extraResource: "./extra", - icon: "./extra/icon@8x.ico", + // Electron packager will automatically use .icns for macOS, .ico for Windows + // We generate both formats in generateAssets hook + icon: "./extra/icon", }, rebuildConfig: {}, makers: [ new MakerSquirrel({ loadingGif: path.join(__dirname, "splash.gif"), - setupIcon: "./extra/icon@8x.ico", + setupIcon: "./extra/icon.ico", }), // new MakerAppX({}), new MakerZIP({}, ["darwin"]), new MakerRpm({}), new MakerDeb({}), new MakerDMG({ - icon: "./extra/icon@8x.ico", + icon: "./extra/icon.icns", }), ], plugins: [ @@ -192,14 +240,22 @@ const config: ForgeConfig = { // Note: For macOS and Linux, FFmpeg and Whisper will use system installations + // Generate standard PNG icons (for runtime use) await createIcon(1); await createIcon(2); await createIcon(3); await createIcon(4); await createIcon(8); + + // Generate Windows ICO file (always generate for cross-platform builds) await pngToIco(path.join(__dirname, "extra/icon@8x.png")).then((buf) => - fs.writeFileSync(path.join(__dirname, "extra/icon@8x.ico"), buf as any), + fs.writeFileSync(path.join(__dirname, "extra/icon.ico"), buf as any), ); + + // Generate macOS ICNS file (only on macOS due to iconutil requirement) + if (platform === "darwin") { + await createIcns(); + } }, }, }; From b71b9d9c9667a80eea319a41f0755f530361b768 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Wed, 5 Nov 2025 19:16:10 -0500 Subject: [PATCH 11/13] Fix Prettier formatting errors in forge.config.ts --- forge.config.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/forge.config.ts b/forge.config.ts index a02fb81..3a30036 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -56,22 +56,22 @@ const createIcns = async () => { // Generate all required icon sizes for (const { size, filename } of sizes) { - const source = await fs.readFile(path.join(__dirname, "./icon.svg"), "utf-8"); + const source = await fs.readFile( + path.join(__dirname, "./icon.svg"), + "utf-8", + ); const resvg = new Resvg(source, { background: "transparent", fitTo: { mode: "width", value: size }, }); const png = resvg.render(); - await fs.writeFile( - path.join(iconsetDir, filename), - png.asPng() as any, - ); + await fs.writeFile(path.join(iconsetDir, filename), png.asPng() as any); } // Convert .iconset to .icns using macOS iconutil const icnsPath = path.join(extraDir, "icon.icns"); await execAsync(`iconutil -c icns "${iconsetDir}" -o "${icnsPath}"`); - + // Clean up the .iconset directory await fs.remove(iconsetDir); }; From 8e8b04b73a3930fc5309aee2e69a3849c479bbb6 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Thu, 6 Nov 2025 02:11:36 -0500 Subject: [PATCH 12/13] Add DMG installer helper and fix macOS icon configuration - Add postMake hook to automatically embed Pensieve Installer.app into DMG - Installer helper automatically copies app to Applications and removes quarantine - Fix icon configuration to use platform-appropriate format (.icns for macOS) - Update README with instructions for using the DMG installer helper --- README.md | 21 +++++ forge.config.ts | 156 ++++++++++++++++++++++++++++++++- scripts/dmg-helper.applescript | 105 ++++++++++++++++++++++ 3 files changed, 281 insertions(+), 1 deletion(-) create mode 100644 scripts/dmg-helper.applescript diff --git a/README.md b/README.md index ca1cb50..2d56979 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,27 @@ If you prefer not to use Homebrew: - **FFmpeg**: Download from [https://ffmpeg.org/download.html](https://ffmpeg.org/download.html) - **Whisper**: Download from [https://github.com/ggerganov/whisper.cpp](https://github.com/ggerganov/whisper.cpp) +### Opening the App After Download + +After downloading Pensieve from the releases page, you may see a message that the app is "damaged" when trying to open it. This is due to macOS Gatekeeper's quarantine feature for apps downloaded from the internet. + +**Using the DMG Installer Helper (Recommended):** + +When you open the Pensieve DMG, you'll see a "Pensieve Installer.app" helper: +1. Double-click "Pensieve Installer.app" (you may need to right-click it the first time and select "Open") +2. The helper will automatically: + - Copy Pensieve.app to your Applications folder + - Remove the quarantine attribute + - Optionally open Pensieve for you +3. You can then open Pensieve normally from Applications anytime + +**Alternative: Manual Method** + +If you prefer not to use the helper, after moving Pensieve to Applications, open Terminal and run: +```bash +xattr -d com.apple.quarantine /Applications/Pensieve.app +``` + ### Troubleshooting If Pensieve shows warning dialogs about missing dependencies: diff --git a/forge.config.ts b/forge.config.ts index 3a30036..63566b4 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -85,8 +85,25 @@ const config: ForgeConfig = { // Electron packager will automatically use .icns for macOS, .ico for Windows // We generate both formats in generateAssets hook icon: "./extra/icon", + ignore: [ + /^\/src/, + /^\/docs/, + /^\/images/, + /^\/\.github/, + /^\/\.idea/, + /^\/scripts/, + /^\/vector-store/, + /^\/\.git/, + /^\/\.gitignore/, + /^\/README\.md$/, + /^\/yarn\.lock$/, + /^\/\.yarnrc\.yml$/, + /^\/package-lock\.json$/, + ], + }, + rebuildConfig: { + onlyModules: [], }, - rebuildConfig: {}, makers: [ new MakerSquirrel({ loadingGif: path.join(__dirname, "splash.gif"), @@ -257,6 +274,143 @@ const config: ForgeConfig = { await createIcns(); } }, + postMake: async (config, makeResults) => { + // Add DMG helper to automatically remove quarantine when app is copied + if (process.platform === "darwin") { + for (const result of makeResults) { + if (result.platform === "darwin") { + // Find the DMG file - artifacts can be an array of strings or objects + const dmgPath = result.artifacts.find((artifact: any) => { + const artifactPath = typeof artifact === "string" ? artifact : artifact?.path; + return artifactPath?.endsWith(".dmg"); + }); + + const dmgPathStr = typeof dmgPath === "string" ? dmgPath : (dmgPath as any)?.path; + + if (dmgPathStr) { + try { + const helperScript = path.join( + __dirname, + "scripts/dmg-helper.applescript", + ); + const helperApp = path.join( + __dirname, + "scripts/Pensieve Installer.app", + ); + + console.log(`Looking for helper script at: ${helperScript}`); + console.log(`DMG path: ${dmgPathStr}`); + + // Compile AppleScript to .app bundle + if (await fs.pathExists(helperScript)) { + console.log("Compiling AppleScript to app bundle..."); + await execAsync( + `osacompile -o "${helperApp}" "${helperScript}"`, + ); + + // Convert DMG to read-write, mount it, add helper, then convert back + const tempDmgBase = dmgPathStr.replace(/\.dmg$/, ".rw"); + const tempDmg = `${tempDmgBase}.dmg`; + + // Remove temp file if it exists + if (await fs.pathExists(tempDmg)) { + await fs.remove(tempDmg); + } + + console.log("Converting DMG to read-write format..."); + await execAsync( + `hdiutil convert "${dmgPathStr}" -format UDRW -o "${tempDmgBase}" -quiet`, + ); + + // Mount the read-write DMG (hdiutil adds .dmg extension automatically) + console.log("Mounting DMG..."); + const mountOutput = await execAsync( + `hdiutil attach "${tempDmg}" -nobrowse -plist`, + ); + + // Parse mount point from plist output or stdout + let mountPoint: string | undefined; + if (mountOutput.stdout.includes("mount-point")) { + // Parse plist XML - find mount-point key and get the following string value + const plistLines = mountOutput.stdout.split("\n"); + for (let i = 0; i < plistLines.length; i++) { + if (plistLines[i].includes("mount-point")) { + // Next non-empty line should be the string value + for (let j = i + 1; j < plistLines.length; j++) { + const stringMatch = plistLines[j].match(/(.*?)<\/string>/); + if (stringMatch) { + mountPoint = stringMatch[1]; + break; + } + } + break; + } + } + } + + // Fallback to regex on stdout/stderr if plist parsing didn't work + if (!mountPoint) { + const match = (mountOutput.stdout + mountOutput.stderr).match(/\/Volumes\/[^\s\n<]+/); + mountPoint = match?.[0]; + } + + if (mountPoint) { + console.log(`DMG mounted at: ${mountPoint}`); + // Verify helper app exists before copying + if (!(await fs.pathExists(helperApp))) { + throw new Error(`Helper app not found at: ${helperApp}`); + } + const targetPath = path.join(mountPoint, "Pensieve Installer.app"); + console.log(`Copying ${helperApp} to ${targetPath}`); + // Copy helper app to DMG + await fs.copy(helperApp, targetPath); + // Verify copy succeeded + if (!(await fs.pathExists(targetPath))) { + throw new Error(`Failed to copy helper app to ${targetPath}`); + } + console.log("Copied installer helper to DMG"); + + // Unmount DMG + await execAsync(`hdiutil detach "${mountPoint}" -quiet`); + console.log("Unmounted DMG"); + + // Convert back to read-only and replace original + console.log("Converting DMG back to read-only..."); + const outputBase = dmgPathStr.replace(/\.dmg$/, ""); + // Remove original DMG if it exists + if (await fs.pathExists(dmgPathStr)) { + await fs.remove(dmgPathStr); + } + await execAsync( + `hdiutil convert "${tempDmg}" -format UDZO -o "${outputBase}" -quiet`, + ); + + // Clean up temp files + await fs.remove(tempDmg); + await fs.remove(helperApp); + + console.log("Successfully added installer helper to DMG"); + } else { + console.warn("Could not find mount point from hdiutil output"); + } + } else { + console.warn(`Helper script not found at: ${helperScript}`); + } + } catch (error) { + console.error("Failed to add DMG helper:", error); + // Don't fail the build if this fails + } + } else { + // Only warn if this is a darwin result (skip other platforms) + if (result.platform === "darwin") { + console.warn("DMG file not found in artifacts:", result.artifacts); + } + } + } + } + } + return makeResults; + }, }, }; diff --git a/scripts/dmg-helper.applescript b/scripts/dmg-helper.applescript new file mode 100644 index 0000000..e0cb2be --- /dev/null +++ b/scripts/dmg-helper.applescript @@ -0,0 +1,105 @@ +-- DMG Helper Script +-- This script automatically copies Pensieve.app to Applications and removes quarantine + +on run + set appName to "Pensieve.app" + set appPath to (path to applications folder as string) & appName + set dmgVolume to "" + + -- Find the DMG volume (where this script is running from) + try + tell application "Finder" + -- Get the disk where this script is located + set scriptPath to path to me as string + set dmgVolume to disk of (file scriptPath) as string + end tell + on error + -- Fallback: try to find Pensieve.app in common DMG locations + set dmgVolume to "/Volumes" + end try + + -- Find Pensieve.app in the DMG + set sourceAppPath to "" + try + tell application "Finder" + -- Look for Pensieve.app in the DMG volume + if dmgVolume is not "" then + set sourceAppPath to (dmgVolume & appName) as string + if not (file sourceAppPath exists) then + -- Try looking in /Volumes + set volumesList to list disks + repeat with vol in volumesList + try + set testPath to (vol as string) & appName + if (file testPath exists) then + set sourceAppPath to testPath + exit repeat + end if + end try + end repeat + end if + end if + end tell + on error + set sourceAppPath to "" + end try + + -- If we can't find the app, prompt user + if sourceAppPath is "" then + display dialog "Pensieve Installer" & return & return & "Could not find Pensieve.app in the DMG." & return & return & "Please ensure the DMG is open and try again." buttons {"OK"} default button "OK" with icon caution + return + end if + + -- Check if app already exists in Applications + set appExists to false + try + set appExists to file appPath exists + on error + set appExists to false + end try + + if appExists then + -- Ask if user wants to replace it + set replaceResult to display dialog "Pensieve Installer" & return & return & "Pensieve.app already exists in Applications." & return & return & "Do you want to replace it?" buttons {"Cancel", "Replace"} default button "Replace" with icon caution + if button returned of replaceResult is "Cancel" then + return + end if + end if + + -- Show progress message + display dialog "Pensieve Installer" & return & return & "Installing Pensieve to Applications folder..." buttons {} default button "OK" with icon note giving up after 1 + + -- Copy the app to Applications + try + tell application "Finder" + -- Remove existing app if it exists + if appExists then + delete file appPath + end if + -- Copy the app + duplicate file sourceAppPath to folder (path to applications folder) + end tell + + -- Wait a moment for copy to complete + delay 1 + + -- Remove quarantine attribute + try + do shell script "xattr -d com.apple.quarantine " & quoted form of POSIX path of appPath + on error + -- Try alternative method + do shell script "xattr -cr " & quoted form of POSIX path of appPath + end try + + -- Show success message + display dialog "Installation Complete!" & return & return & "Pensieve has been successfully installed to Applications." & return & return & "You can now open Pensieve normally." buttons {"Open Pensieve", "OK"} default button "Open Pensieve" with icon note + if button returned of result is "Open Pensieve" then + tell application "Finder" + open file appPath + end tell + end if + on error errorMessage + display dialog "Installation Failed" & return & return & "An error occurred while installing Pensieve:" & return & return & errorMessage buttons {"OK"} default button "OK" with icon stop + end try +end run + From cff5ba7df13581abd70ee6d3c48000254cc7c965 Mon Sep 17 00:00:00 2001 From: rogerb831 Date: Thu, 6 Nov 2025 09:58:14 -0500 Subject: [PATCH 13/13] Fix linting errors in forge.config.ts - Use array destructuring for regex match result - Convert lonely if in else block to else if - Fix Prettier formatting issues --- forge.config.ts | 49 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 17 deletions(-) diff --git a/forge.config.ts b/forge.config.ts index 63566b4..4f721d6 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -281,11 +281,13 @@ const config: ForgeConfig = { if (result.platform === "darwin") { // Find the DMG file - artifacts can be an array of strings or objects const dmgPath = result.artifacts.find((artifact: any) => { - const artifactPath = typeof artifact === "string" ? artifact : artifact?.path; + const artifactPath = + typeof artifact === "string" ? artifact : artifact?.path; return artifactPath?.endsWith(".dmg"); }); - - const dmgPathStr = typeof dmgPath === "string" ? dmgPath : (dmgPath as any)?.path; + + const dmgPathStr = + typeof dmgPath === "string" ? dmgPath : (dmgPath as any)?.path; if (dmgPathStr) { try { @@ -311,12 +313,12 @@ const config: ForgeConfig = { // Convert DMG to read-write, mount it, add helper, then convert back const tempDmgBase = dmgPathStr.replace(/\.dmg$/, ".rw"); const tempDmg = `${tempDmgBase}.dmg`; - + // Remove temp file if it exists if (await fs.pathExists(tempDmg)) { await fs.remove(tempDmg); } - + console.log("Converting DMG to read-write format..."); await execAsync( `hdiutil convert "${dmgPathStr}" -format UDRW -o "${tempDmgBase}" -quiet`, @@ -327,7 +329,7 @@ const config: ForgeConfig = { const mountOutput = await execAsync( `hdiutil attach "${tempDmg}" -nobrowse -plist`, ); - + // Parse mount point from plist output or stdout let mountPoint: string | undefined; if (mountOutput.stdout.includes("mount-point")) { @@ -337,9 +339,12 @@ const config: ForgeConfig = { if (plistLines[i].includes("mount-point")) { // Next non-empty line should be the string value for (let j = i + 1; j < plistLines.length; j++) { - const stringMatch = plistLines[j].match(/(.*?)<\/string>/); + const stringMatch = plistLines[j].match( + /(.*?)<\/string>/, + ); if (stringMatch) { - mountPoint = stringMatch[1]; + const [, mountPointValue] = stringMatch; + mountPoint = mountPointValue; break; } } @@ -347,10 +352,12 @@ const config: ForgeConfig = { } } } - + // Fallback to regex on stdout/stderr if plist parsing didn't work if (!mountPoint) { - const match = (mountOutput.stdout + mountOutput.stderr).match(/\/Volumes\/[^\s\n<]+/); + const match = ( + mountOutput.stdout + mountOutput.stderr + ).match(/\/Volumes\/[^\s\n<]+/); mountPoint = match?.[0]; } @@ -360,13 +367,18 @@ const config: ForgeConfig = { if (!(await fs.pathExists(helperApp))) { throw new Error(`Helper app not found at: ${helperApp}`); } - const targetPath = path.join(mountPoint, "Pensieve Installer.app"); + const targetPath = path.join( + mountPoint, + "Pensieve Installer.app", + ); console.log(`Copying ${helperApp} to ${targetPath}`); // Copy helper app to DMG await fs.copy(helperApp, targetPath); // Verify copy succeeded if (!(await fs.pathExists(targetPath))) { - throw new Error(`Failed to copy helper app to ${targetPath}`); + throw new Error( + `Failed to copy helper app to ${targetPath}`, + ); } console.log("Copied installer helper to DMG"); @@ -391,7 +403,9 @@ const config: ForgeConfig = { console.log("Successfully added installer helper to DMG"); } else { - console.warn("Could not find mount point from hdiutil output"); + console.warn( + "Could not find mount point from hdiutil output", + ); } } else { console.warn(`Helper script not found at: ${helperScript}`); @@ -400,11 +414,12 @@ const config: ForgeConfig = { console.error("Failed to add DMG helper:", error); // Don't fail the build if this fails } - } else { + } else if (result.platform === "darwin") { // Only warn if this is a darwin result (skip other platforms) - if (result.platform === "darwin") { - console.warn("DMG file not found in artifacts:", result.artifacts); - } + console.warn( + "DMG file not found in artifacts:", + result.artifacts, + ); } } }