blob: 8007411afc561ec7fa20d88a15dd62d3668b6d8f [file]
import { spawnSync } from 'node:child_process';
import { createHash } from 'node:crypto';
import {
closeSync,
existsSync,
mkdirSync,
openSync,
readFileSync,
renameSync,
rmSync,
unlinkSync,
writeFileSync,
} from 'node:fs';
import { dirname, join, resolve } from 'node:path';
import { homedir } from 'node:os';
import { fileURLToPath } from 'node:url';
import { createPackage } from '@electron/asar';
const DESKTOP_DIR = resolve(fileURLToPath(new URL('..', import.meta.url)));
const REPO_ROOT = resolve(DESKTOP_DIR, '..', '..');
const DEV_RUNTIME_DIR = join(DESKTOP_DIR, '.maka-dev');
const DEV_SESSION_DIR = join(DESKTOP_DIR, '.maka-dev-session');
const DEV_APP = join(DEV_RUNTIME_DIR, 'Maka Dev.app');
const DEV_EXECUTABLE = join(DEV_APP, 'Contents', 'MacOS', 'Electron');
const WORKTREE_ID = createHash('sha256').update(REPO_ROOT).digest('hex').slice(0, 12);
const DEV_USER_DATA_DIR = join(
homedir(),
'Library',
'Application Support',
`Maka Dev-${WORKTREE_ID}`,
);
const MARKER = join(DEV_RUNTIME_DIR, 'runtime.json');
const SESSION_FILE = join(DEV_SESSION_DIR, 'session.json');
const APP_PID_FILE = join(DEV_SESSION_DIR, 'app.pid');
const LAUNCH_STATUS_FILE = join(DEV_SESSION_DIR, 'launch-status.json');
const RUNTIME_LOCK = join(DEV_SESSION_DIR, 'runtime.lock');
const ELECTRON_PACKAGE = join(REPO_ROOT, 'node_modules', 'electron', 'package.json');
const SOURCE_APP = join(REPO_ROOT, 'node_modules', 'electron', 'dist', 'Electron.app');
const DEV_BUNDLE_ID = 'com.maka.dev';
const RUNTIME_SCHEMA_VERSION = 4;
const SESSION_SCHEMA_VERSION = 1;
export async function resolveMacosDevelopmentLaunch() {
if (!shouldUseMacosDevelopmentApp(process.platform)) return null;
const appPath = await prepareDevelopmentApp();
return createMacosDevelopmentLaunch(appPath);
}
export function shouldUseMacosDevelopmentApp(platform) {
return platform === 'darwin';
}
export function createMacosDevelopmentLaunch(appPath) {
return {
command: 'open',
// LaunchServices must own process launch so macOS TCC attributes the
// running executable to Maka Dev rather than to its parent terminal.
args: [
'-n',
'-a',
appPath,
],
};
}
export async function quitMacosDevelopmentApp(options = {}) {
const platform = options.platform ?? process.platform;
const appPidFile = options.appPidFile ?? APP_PID_FILE;
const supervisorPid = options.supervisorPid ?? process.pid;
const processAlive = options.processAlive ?? isProcessAlive;
const kill = options.kill ?? process.kill.bind(process);
const graceMs = options.graceMs ?? 3_000;
const delay = options.delay ?? ((ms) => new Promise((resolve_) => setTimeout(resolve_, ms)));
if (platform !== 'darwin' || !existsSync(appPidFile)) return false;
let appProcess;
try {
appProcess = JSON.parse(readFileSync(appPidFile, 'utf8'));
} catch {
return false;
}
if (appProcess.supervisorPid !== supervisorPid) return false;
const pid = appProcess.pid;
if (!Number.isSafeInteger(pid) || pid <= 0 || !processAlive(pid)) return false;
try {
kill(pid, 'SIGTERM');
} catch {
return false;
}
await delay(graceMs);
if (processAlive(pid)) {
try {
kill(pid, 'SIGKILL');
} catch {
// Exited between the liveness check and fallback signal.
}
}
return true;
}
export async function prepareDevelopmentApp() {
if (process.platform !== 'darwin') {
throw new Error('Maka Dev.app is only available on macOS');
}
if (!existsSync(SOURCE_APP)) {
throw new Error(`Electron.app is missing at ${SOURCE_APP}; run npm install first`);
}
const electronVersion = JSON.parse(readFileSync(ELECTRON_PACKAGE, 'utf8')).version;
if (isCurrentRuntime(electronVersion)) return DEV_APP;
assertNoActiveDevelopmentSession();
const releaseLock = acquirePidLock(RUNTIME_LOCK);
try {
// Another process may have completed preparation before this lock landed.
if (isCurrentRuntime(electronVersion)) return DEV_APP;
await rebuildDevelopmentRuntime({
reset: () => {
rmSync(DEV_RUNTIME_DIR, { recursive: true, force: true });
run('mkdir', ['-p', DEV_RUNTIME_DIR]);
},
build: async () => {
run('ditto', [SOURCE_APP, DEV_APP]);
const plist = join(DEV_APP, 'Contents', 'Info.plist');
replacePlistValue(plist, 'CFBundleIdentifier', DEV_BUNDLE_ID);
replacePlistValue(plist, 'CFBundleName', 'Maka Dev');
replacePlistValue(plist, 'CFBundleDisplayName', 'Maka Dev');
await installRelaunchBootstrap();
// `ditto` does not copy quarantine metadata by default. Clear any root-level
// attribute without relying on the newer recursive `xattr -r` flag, which
// is unavailable on older supported macOS releases.
run('xattr', ['-c', DEV_APP]);
run('codesign', [
'--force',
'--deep',
'--sign',
'-',
'--identifier',
DEV_BUNDLE_ID,
DEV_APP,
]);
run('codesign', ['--verify', '--deep', '--strict', DEV_APP]);
},
writeMarker: () => {
writeFileSync(
MARKER,
`${JSON.stringify({ schemaVersion: RUNTIME_SCHEMA_VERSION, electronVersion, bundleId: DEV_BUNDLE_ID, desktopDir: DESKTOP_DIR }, null, 2)}\n`,
);
},
});
} finally {
releaseLock();
}
return DEV_APP;
}
async function installRelaunchBootstrap() {
const bootstrapDir = join(DEV_RUNTIME_DIR, 'relaunch-bootstrap');
mkdirSync(bootstrapDir, { recursive: true });
writeFileSync(
join(bootstrapDir, 'package.json'),
`${JSON.stringify({ name: 'maka-dev-relaunch', main: 'main.cjs', private: true })}\n`,
);
writeFileSync(
join(bootstrapDir, 'main.cjs'),
createRelaunchBootstrapSource(
DESKTOP_DIR,
DEV_USER_DATA_DIR,
SESSION_FILE,
APP_PID_FILE,
LAUNCH_STATUS_FILE,
),
);
await createPackage(
bootstrapDir,
join(DEV_APP, 'Contents', 'Resources', 'default_app.asar'),
);
rmSync(bootstrapDir, { recursive: true, force: true });
}
export function createRelaunchBootstrapSource(
desktopDir,
defaultUserDataDir,
sessionFile = SESSION_FILE,
appPidFile = APP_PID_FILE,
launchStatusFile = LAUNCH_STATUS_FILE,
) {
return [
"const { app } = require('electron');",
"const { join } = require('node:path');",
"const { pathToFileURL } = require('node:url');",
`const desktopDir = ${JSON.stringify(desktopDir)};`,
`const defaultUserDataDir = ${JSON.stringify(defaultUserDataDir)};`,
`const sessionFile = ${JSON.stringify(sessionFile)};`,
`const appPidFile = ${JSON.stringify(appPidFile)};`,
`const launchStatusFile = ${JSON.stringify(launchStatusFile)};`,
"let session = null;",
"try {",
" const candidate = JSON.parse(require('node:fs').readFileSync(sessionFile, 'utf8'));",
" process.kill(candidate.supervisorPid, 0);",
` if (candidate.schemaVersion === ${SESSION_SCHEMA_VERSION}) session = candidate;`,
"} catch {}",
"if (session?.env) Object.assign(process.env, session.env);",
"for (const argument of session?.electronArgs || []) {",
" const match = /^--([^=]+)(?:=(.*))?$/.exec(argument);",
" if (match) app.commandLine.appendSwitch(match[1], match[2]);",
"}",
"const userDataDir = session?.userDataDir || defaultUserDataDir;",
'app.setAppPath(desktopDir);',
"app.setPath('userData', userDataDir);",
"if (session) {",
" process.env.MAKA_DEV_APP_PID_FILE = appPidFile;",
" process.env.MAKA_DEV_LAUNCH_STATUS_FILE = launchStatusFile;",
" process.env.MAKA_DEV_SUPERVISOR_PID = String(session.supervisorPid);",
"}",
'process.chdir(desktopDir);',
"import(pathToFileURL(join(desktopDir, 'dist/main/main.js')).href).catch((error) => {",
" console.error('[maka-dev] relaunch bootstrap failed', error);",
' app.exit(1);',
'});',
'',
].join('\n');
}
export function createDevelopmentSession(input) {
return {
schemaVersion: SESSION_SCHEMA_VERSION,
supervisorPid: input.supervisorPid,
userDataDir: input.userDataDir ?? DEV_USER_DATA_DIR,
env: selectDevelopmentEnvironment(input.env, input.viteUrl),
electronArgs: input.electronArgs ?? [],
};
}
export function selectDevelopmentEnvironment(env, viteUrl) {
const selected = {};
for (const [key, value] of Object.entries(env)) {
if (typeof value !== 'string') continue;
if (isAllowedDevelopmentEnvironmentKey(key)) selected[key] = value;
}
if (viteUrl) selected.VITE_DEV_SERVER_URL = viteUrl;
return selected;
}
export function writeDevelopmentSession(session, options = {}) {
const sessionFile = options.sessionFile ?? SESSION_FILE;
const appPidFile = options.appPidFile ?? APP_PID_FILE;
const launchStatusFile = options.launchStatusFile ?? LAUNCH_STATUS_FILE;
const processAlive = options.processAlive ?? isProcessAlive;
mkdirSync(dirname(sessionFile), { recursive: true });
if (existsSync(sessionFile)) {
try {
const current = JSON.parse(readFileSync(sessionFile, 'utf8'));
if (current.supervisorPid !== session.supervisorPid && processAlive(current.supervisorPid)) {
throw new Error(`Maka Dev session is already supervised by PID ${current.supervisorPid}`);
}
} catch (error) {
if (error instanceof Error && error.message.includes('already supervised')) throw error;
// Corrupt or stale sessions are replaced atomically below.
}
}
const temporary = `${sessionFile}.tmp-${process.pid}`;
rmSync(appPidFile, { force: true });
rmSync(launchStatusFile, { force: true });
writeFileSync(temporary, `${JSON.stringify(session, null, 2)}\n`, { mode: 0o600 });
renameSync(temporary, sessionFile);
}
export async function recoverStaleDevelopmentSession(options = {}) {
const sessionFile = options.sessionFile ?? SESSION_FILE;
const appPidFile = options.appPidFile ?? APP_PID_FILE;
const launchStatusFile = options.launchStatusFile ?? LAUNCH_STATUS_FILE;
const processAlive = options.processAlive ?? isProcessAlive;
let staleSupervisorPid;
try {
const current = JSON.parse(readFileSync(sessionFile, 'utf8'));
staleSupervisorPid = current.supervisorPid;
} catch {
try {
staleSupervisorPid = JSON.parse(readFileSync(appPidFile, 'utf8')).supervisorPid;
} catch {
return false;
}
}
if (!Number.isSafeInteger(staleSupervisorPid) || processAlive(staleSupervisorPid)) return false;
await quitMacosDevelopmentApp({
...options,
appPidFile,
supervisorPid: staleSupervisorPid,
processAlive,
});
clearDevelopmentSession(staleSupervisorPid, { sessionFile, appPidFile, launchStatusFile });
return true;
}
export function assertNoActiveDevelopmentSession(options = {}) {
const sessionFile = options.sessionFile ?? SESSION_FILE;
const processAlive = options.processAlive ?? isProcessAlive;
if (!existsSync(sessionFile)) return;
try {
const current = JSON.parse(readFileSync(sessionFile, 'utf8'));
if (Number.isSafeInteger(current.supervisorPid) && processAlive(current.supervisorPid)) {
throw new Error(
`Maka Dev session is already supervised by PID ${current.supervisorPid}; stop it before rebuilding the development app`,
);
}
} catch (error) {
if (error instanceof Error && error.message.includes('already supervised')) throw error;
// Corrupt or stale ownership state cannot protect a live session.
}
}
export function clearDevelopmentSession(supervisorPid = process.pid, options = {}) {
const sessionFile = options.sessionFile ?? SESSION_FILE;
const appPidFile = options.appPidFile ?? APP_PID_FILE;
const launchStatusFile = options.launchStatusFile ?? LAUNCH_STATUS_FILE;
try {
const current = JSON.parse(readFileSync(sessionFile, 'utf8'));
if (current.supervisorPid === supervisorPid) {
unlinkSync(sessionFile);
rmSync(appPidFile, { force: true });
rmSync(launchStatusFile, { force: true });
}
} catch {}
}
function isAllowedDevelopmentEnvironmentKey(key) {
return (
key.startsWith('MAKA_') ||
key.startsWith('CUA_') ||
[
'ANTHROPIC_API_KEY',
'OPENAI_API_KEY',
'DEEPSEEK_API_KEY',
'TAVILY_API_KEY',
'COPILOT_GITHUB_TOKEN',
'GH_TOKEN',
'GITHUB_TOKEN',
'RIVE_BIN',
'HTTP_PROXY',
'HTTPS_PROXY',
'ALL_PROXY',
'NO_PROXY',
'PATH',
'PYTHONPATH',
'SHELL',
'NODE_ENV',
'NO_COLOR',
'COLORTERM',
'TERM',
].includes(key)
);
}
function isCurrentRuntime(electronVersion) {
if (!existsSync(DEV_EXECUTABLE) || !existsSync(MARKER)) return false;
try {
const marker = JSON.parse(readFileSync(MARKER, 'utf8'));
return isDevelopmentRuntimeCurrent({
marker,
schemaVersion: RUNTIME_SCHEMA_VERSION,
electronVersion,
bundleId: DEV_BUNDLE_ID,
desktopDir: DESKTOP_DIR,
signatureValid:
spawnSync('codesign', ['--verify', '--deep', '--strict', DEV_APP]).status === 0,
});
} catch {
return false;
}
}
export function isDevelopmentRuntimeCurrent(input) {
const marker = input.marker;
return (
marker !== null &&
typeof marker === 'object' &&
marker.schemaVersion === input.schemaVersion &&
marker.electronVersion === input.electronVersion &&
marker.bundleId === input.bundleId &&
marker.desktopDir === input.desktopDir &&
input.signatureValid
);
}
export async function rebuildDevelopmentRuntime(deps) {
await deps.reset();
await deps.build();
// The marker is the cache commit point. Never write it until copying,
// bootstrap generation, signing, and strict signature verification succeed.
await deps.writeMarker();
}
export function acquirePidLock(path, options = {}) {
const processAlive = options.processAlive ?? isProcessAlive;
mkdirSync(dirname(path), { recursive: true });
try {
const fd = openSync(path, 'wx', 0o600);
writeFileSync(fd, `${process.pid}\n`);
closeSync(fd);
} catch (error) {
if (error?.code !== 'EEXIST') throw error;
let owner = 0;
try {
owner = Number(readFileSync(path, 'utf8').trim());
} catch {}
if (Number.isSafeInteger(owner) && owner > 0 && processAlive(owner)) {
throw new Error(`Maka Dev runtime preparation is already running in PID ${owner}`);
}
rmSync(path, { force: true });
return acquirePidLock(path, options);
}
return () => rmSync(path, { force: true });
}
function isProcessAlive(pid) {
try {
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
function replacePlistValue(plist, key, value) {
const result = spawnSync('plutil', ['-replace', key, '-string', value, plist], {
encoding: 'utf8',
});
if (result.status === 0) return;
run('plutil', ['-insert', key, '-string', value, plist]);
}
function run(command, args) {
const result = spawnSync(command, args, { encoding: 'utf8', stdio: 'pipe' });
if (result.status === 0) return;
const detail = result.stderr?.trim() || result.stdout?.trim() || `exit ${result.status}`;
throw new Error(`${command} failed: ${detail}`);
}
export const developmentAppPath = DEV_APP;
export async function waitForMacosDevelopmentApp(options = {}) {
const supervisorPid = options.supervisorPid ?? process.pid;
const timeoutMs = options.timeoutMs ?? 30_000;
const pollMs = options.pollMs ?? 100;
const statusFile = options.statusFile ?? LAUNCH_STATUS_FILE;
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
try {
const status = JSON.parse(readFileSync(statusFile, 'utf8'));
if (status.supervisorPid !== supervisorPid) throw new Error('stale launch status');
if (status.status === 'ready' && isProcessAlive(status.pid)) return status;
if (status.status === 'failed') {
throw new Error(status.message || 'Maka Dev failed during startup');
}
} catch (error) {
if (
error?.code !== 'ENOENT' &&
!(error instanceof Error && error.message === 'stale launch status')
) {
throw error;
}
}
await new Promise((resolve_) => setTimeout(resolve_, pollMs));
}
throw new Error(
`Maka Dev did not finish starting within ${timeoutMs}ms; another worktree instance or an app boot failure may be blocking it`,
);
}