mirror of
https://github.com/rowboatlabs/rowboat.git
synced 2026-07-21 21:31:12 +02:00
- mic-monitor Swift helper: polls CoreAudio for mic-in-use, with owning PIDs on macOS 14.4+ (process-object API) so calls attribute to the app that actually holds the mic (Chrome vs Zoom vs Slack); no TCC permission needed. Compiled best-effort by bundle.mjs; exits when stdin closes - core meetings/detector: 5s mic debounce, PID→process attribution with running-app fallback, Granola labels (Huddle/Call/Meeting detected), calendar merge (event start −15min through end), once per mic session (30s idle reset), suppressed while Rowboat itself captures (recording or assistant voice call), gated by a meeting_detection notification category (Settings > Notifications) - popup: lean Granola-style bar (title + platform + Take notes pill, floating ×) in a transparent non-activating panel at screen-saver level with fullscreen-auxiliary behavior — floats over fullscreen meetings on the cursor's display; activation policy restored immediately so the Dock icon survives and Take Notes can foreground Rowboat - Take Notes routes into the existing take-meeting-notes flow with a synthetic or matched calendar event (source: detected); never records without a click - tray icon bumped to 18pt
142 lines
6.8 KiB
JavaScript
142 lines
6.8 KiB
JavaScript
/**
|
|
* Bundles the compiled main process into a single JavaScript file.
|
|
*
|
|
* Why we bundle:
|
|
* - pnpm uses symlinks for workspace packages (@x/core, @x/shared)
|
|
* - Electron Forge's dependency walker (flora-colossus) cannot follow these symlinks
|
|
* - Bundling inlines all dependencies into a single file, eliminating node_modules
|
|
*
|
|
* This script is called by the generateAssets hook in forge.config.js before packaging.
|
|
*/
|
|
|
|
import * as esbuild from 'esbuild';
|
|
import { readFile } from 'node:fs/promises';
|
|
import { execSync } from 'node:child_process';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
// In CommonJS, import.meta.url doesn't exist. We need to polyfill it.
|
|
// The banner defines __import_meta_url at the top of the bundle,
|
|
// and we use define to replace all import.meta.url references with it.
|
|
const cjsBanner = `var __import_meta_url = require('url').pathToFileURL(__filename).href;`;
|
|
const pkg = JSON.parse(await readFile(new URL('./package.json', import.meta.url), 'utf8'));
|
|
|
|
await esbuild.build({
|
|
entryPoints: ['./dist/main.js'],
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node20',
|
|
outfile: './.package/dist/main.cjs',
|
|
// electron is provided by the runtime. node-pty is a NATIVE module: it can't
|
|
// be inlined (its loader requires .node binaries + a spawn-helper relative to
|
|
// its own package dir), so it stays external and is copied into
|
|
// .package/node_modules below, where require() from dist/main.cjs finds it.
|
|
external: ['electron', 'node-pty'],
|
|
// Use CommonJS format - many dependencies use require() which doesn't work
|
|
// well with esbuild's ESM shim. CJS handles dynamic requires natively.
|
|
format: 'cjs',
|
|
// Inject the polyfill variable at the top
|
|
banner: { js: cjsBanner },
|
|
// Replace import.meta.url directly with our polyfill variable
|
|
define: {
|
|
'import.meta.url': '__import_meta_url',
|
|
// Inject PostHog credentials at build time. Reuse the renderer's
|
|
// VITE_PUBLIC_* envs so packaging only needs one set of values.
|
|
// Empty strings disable analytics gracefully.
|
|
'process.env.POSTHOG_KEY': JSON.stringify(process.env.VITE_PUBLIC_POSTHOG_KEY ?? ''),
|
|
'process.env.POSTHOG_HOST': JSON.stringify(process.env.VITE_PUBLIC_POSTHOG_HOST ?? 'https://us.i.posthog.com'),
|
|
'process.env.ROWBOAT_APP_VERSION': JSON.stringify(pkg.version ?? ''),
|
|
},
|
|
});
|
|
|
|
// Ship node-pty next to the bundle. Resolve through pnpm's symlink to the real
|
|
// package dir and copy only what's needed at runtime (compiled JS + prebuilt
|
|
// binaries). The macOS spawn-helper must be executable — pnpm extraction drops
|
|
// the bit, and a non-executable helper makes every PTY spawn fail.
|
|
const here = path.dirname(fileURLToPath(import.meta.url));
|
|
const ptySrc = fs.realpathSync(path.join(here, 'node_modules', 'node-pty'));
|
|
const ptyDest = path.join(here, '.package', 'node_modules', 'node-pty');
|
|
fs.rmSync(ptyDest, { recursive: true, force: true });
|
|
fs.mkdirSync(ptyDest, { recursive: true });
|
|
for (const item of ['package.json', 'lib', 'prebuilds']) {
|
|
fs.cpSync(path.join(ptySrc, item), path.join(ptyDest, item), { recursive: true, dereference: true });
|
|
}
|
|
const prebuildsDir = path.join(ptyDest, 'prebuilds');
|
|
for (const dir of fs.readdirSync(prebuildsDir)) {
|
|
const helper = path.join(prebuildsDir, dir, 'spawn-helper');
|
|
if (fs.existsSync(helper)) fs.chmodSync(helper, 0o755);
|
|
}
|
|
|
|
// Self-heal: node-pty ships prebuilt binaries only for darwin/win32, so on any
|
|
// host whose prebuild is absent (notably Linux) the staged package has no loadable
|
|
// pty.node and the app crashes on launch. Compile the native module for the host
|
|
// platform+arch if needed and stage it under prebuilds/<platform>-<arch>/, where
|
|
// node-pty's loader looks first. Keeps dev and CI working without a manual node-gyp
|
|
// step (the CI workflow's explicit build is the fast path; this is the safety net).
|
|
const hostTriple = `${process.platform}-${process.arch}`;
|
|
const stagedBinary = path.join(prebuildsDir, hostTriple, 'pty.node');
|
|
if (!fs.existsSync(stagedBinary)) {
|
|
const builtBinary = path.join(ptySrc, 'build', 'Release', 'pty.node');
|
|
if (!fs.existsSync(builtBinary)) {
|
|
console.log(`node-pty: no prebuilt binary for ${hostTriple}; compiling with node-gyp…`);
|
|
execSync('npx node-gyp rebuild', { cwd: ptySrc, stdio: 'inherit' });
|
|
}
|
|
if (!fs.existsSync(builtBinary)) {
|
|
throw new Error(`node-pty: failed to produce a native binary for ${hostTriple}`);
|
|
}
|
|
fs.mkdirSync(path.dirname(stagedBinary), { recursive: true });
|
|
fs.copyFileSync(builtBinary, stagedBinary);
|
|
console.log(`✅ node-pty: staged ${hostTriple}/pty.node`);
|
|
}
|
|
console.log('✅ node-pty staged in .package/node_modules');
|
|
|
|
// Compile the mic-monitor helper (ambient meeting detection) on macOS.
|
|
// Best-effort: without swiftc — or on other platforms — the app still works,
|
|
// ad-hoc meeting detection just stays off (main checks the binary exists).
|
|
if (process.platform === 'darwin') {
|
|
const swiftSrc = path.join(here, 'native', 'mic-monitor.swift');
|
|
const helperOut = path.join(here, '.package', 'dist', 'mic-monitor');
|
|
const upToDate = fs.existsSync(helperOut) &&
|
|
fs.statSync(helperOut).mtimeMs >= fs.statSync(swiftSrc).mtimeMs;
|
|
if (upToDate) {
|
|
console.log('✅ mic-monitor helper up to date');
|
|
} else {
|
|
try {
|
|
execSync(`swiftc -O "${swiftSrc}" -o "${helperOut}"`, { stdio: 'inherit' });
|
|
console.log('✅ mic-monitor helper compiled');
|
|
} catch {
|
|
console.warn('⚠️ mic-monitor helper not built (swiftc unavailable?) — meeting detection disabled');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bundle the vendored agent-slack CLI into a single self-contained script next
|
|
// to main.cjs. It runs as a child process (process.execPath with
|
|
// ELECTRON_RUN_AS_NODE=1), so it must exist as a real file on disk — it can't
|
|
// be inlined into main.cjs. Bundling here means the packaged app needs neither
|
|
// node_modules nor a global npm install.
|
|
const agentSlackPkg = JSON.parse(
|
|
await readFile(new URL('./node_modules/agent-slack/package.json', import.meta.url), 'utf8'),
|
|
);
|
|
await esbuild.build({
|
|
entryPoints: ['./node_modules/agent-slack/dist/index.js'],
|
|
bundle: true,
|
|
platform: 'node',
|
|
target: 'node22',
|
|
outfile: './.package/dist/agent-slack.cjs',
|
|
format: 'cjs',
|
|
banner: { js: cjsBanner },
|
|
define: {
|
|
'import.meta.url': '__import_meta_url',
|
|
// Without this constant the CLI's --version walks up the directory tree
|
|
// for a package.json and would find Rowboat's instead of agent-slack's.
|
|
'AGENT_SLACK_BUILD_VERSION': JSON.stringify(agentSlackPkg.version),
|
|
},
|
|
// The CLI probes bun:sqlite via dynamic import inside a try/catch and falls
|
|
// back to node:sqlite; keep it external so the probe fails at runtime the
|
|
// same way it does under plain node.
|
|
external: ['bun:sqlite'],
|
|
});
|
|
|
|
console.log(`✅ Main process bundled to .package/dist/main.cjs (+ agent-slack ${agentSlackPkg.version} CLI)`);
|