diff --git a/apps/ade-cli/pnpm-workspace.yaml b/apps/ade-cli/pnpm-workspace.yaml index 9b5c7213c..738168dd3 100644 --- a/apps/ade-cli/pnpm-workspace.yaml +++ b/apps/ade-cli/pnpm-workspace.yaml @@ -1,5 +1,5 @@ allowBuilds: - esbuild: set this to true or false - node-pty: set this to true or false - opencode-ai: set this to true or false - sqlite3: set this to true or false + esbuild: true + node-pty: true + opencode-ai: true + sqlite3: true diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts index fb29e0dba..7cd4c770c 100644 --- a/apps/desktop/src/main/main.ts +++ b/apps/desktop/src/main/main.ts @@ -1749,6 +1749,7 @@ app.whenReady().then(async () => { currentVersion: app.getVersion(), globalStatePath, updaterCacheDir: app.isPackaged ? resolveAutoUpdaterCacheDir() : undefined, + autoCheckEnabled: app.isPackaged, }); const initContextForProjectRoot = async ({ diff --git a/apps/desktop/src/main/services/appControl/appControlLaunchCommand.test.ts b/apps/desktop/src/main/services/appControl/appControlLaunchCommand.test.ts new file mode 100644 index 000000000..5f5579a38 --- /dev/null +++ b/apps/desktop/src/main/services/appControl/appControlLaunchCommand.test.ts @@ -0,0 +1,73 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { + commandForwardsAppControlDebug, + commandLooksLikeDirectElectronLaunch, + commandLooksLikePackageScriptLaunch, + insertDebugFlagsIntoDirectElectronCommand, + rewritePackageScriptElectronLaunch, +} from "./appControlLaunchCommand"; + +const DEBUG_FLAGS = ["--remote-debugging-port=9222"]; + +describe("appControlLaunchCommand", () => { + it("detects direct Electron launches and injects debug flags after electron", () => { + expect(commandLooksLikeDirectElectronLaunch("FOO=bar npx electron .")).toBe(true); + + expect(insertDebugFlagsIntoDirectElectronCommand("FOO=bar npx electron .", DEBUG_FLAGS)) + .toBe("FOO=bar npx electron --remote-debugging-port=9222 ."); + }); + + it("detects package script launches and rewrites direct Electron scripts", () => { + const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-app-control-launch-")); + try { + fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify({ + scripts: { + dev: "electron .", + }, + }), "utf8"); + + expect(commandLooksLikePackageScriptLaunch("npm run dev")).toBe(true); + expect(rewritePackageScriptElectronLaunch("npm run dev", DEBUG_FLAGS, projectRoot)) + .toBe(`PATH=${path.join(projectRoot, "node_modules", ".bin")}:$PATH electron --remote-debugging-port=9222 .`); + } finally { + fs.rmSync(projectRoot, { recursive: true, force: true }); + } + }); + + it("does not rewrite scripts that already forward App Control flags", () => { + const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-app-control-launch-")); + try { + fs.writeFileSync(path.join(projectRoot, "package.json"), JSON.stringify({ + scripts: { + dev: "electron . {ADE_APP_CONTROL_DEBUG_FLAGS}", + }, + }), "utf8"); + + expect(commandForwardsAppControlDebug("electron . {ADE_APP_CONTROL_DEBUG_FLAGS}")).toBe(true); + expect(rewritePackageScriptElectronLaunch("npm run dev", DEBUG_FLAGS, projectRoot)).toBeNull(); + } finally { + fs.rmSync(projectRoot, { recursive: true, force: true }); + } + }); + + it("resolves the last cd segment before reading package.json", () => { + const projectRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ade-app-control-launch-")); + const appDir = path.join(projectRoot, "apps", "desktop"); + try { + fs.mkdirSync(appDir, { recursive: true }); + fs.writeFileSync(path.join(appDir, "package.json"), JSON.stringify({ + scripts: { + dev: "electron .", + }, + }), "utf8"); + + expect(rewritePackageScriptElectronLaunch("cd apps/desktop && npm run dev", DEBUG_FLAGS, projectRoot)) + .toBe(`cd apps/desktop && PATH=${path.join(appDir, "node_modules", ".bin")}:$PATH electron --remote-debugging-port=9222 .`); + } finally { + fs.rmSync(projectRoot, { recursive: true, force: true }); + } + }); +}); diff --git a/apps/desktop/src/main/services/appControl/appControlLaunchCommand.ts b/apps/desktop/src/main/services/appControl/appControlLaunchCommand.ts new file mode 100644 index 000000000..efcd667ab --- /dev/null +++ b/apps/desktop/src/main/services/appControl/appControlLaunchCommand.ts @@ -0,0 +1,82 @@ +import fs from "node:fs"; +import path from "node:path"; + +export function shellQuote(value: string): string { + if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(value)) return value; + return `'${value.replace(/'/g, "'\\''")}'`; +} + +export function commandForwardsAppControlDebug(command: string): boolean { + return /\{ADE_APP_CONTROL_DEBUG_FLAGS\}|\bADE_APP_CONTROL_(?:DEBUG_FLAGS|CDP_PORT|REMOTE_DEBUGGING_PORT)\b|--remote-debugging-port\b/.test(command); +} + +export function commandLooksLikePackageScriptLaunch(command: string): boolean { + return /(?:^|[;&|]\s*)(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s;&|]+)\s+)*(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?[A-Za-z0-9:_./-]+)\s*$/.test(command.trim()); +} + +export function commandLooksLikeDirectElectronLaunch(command: string): boolean { + return /(?:^|[;&|]\s*)(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s;&|]+)\s+)*(?:npx\s+)?electron(?:\s+[^;&|]*)?\s*$/.test(command.trim()); +} + +export function unquoteShellValue(value: string): string { + const trimmed = value.trim(); + if ( + (trimmed.startsWith("'") && trimmed.endsWith("'")) + || (trimmed.startsWith("\"") && trimmed.endsWith("\"")) + ) { + return trimmed.slice(1, -1); + } + return trimmed; +} + +export function insertDebugFlagsIntoDirectElectronCommand(command: string, debugFlags: string[]): string { + const flags = debugFlags.map(shellQuote).join(" "); + return command.replace( + /((?:^|[;&|]\s*)(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s;&|]+)\s+)*(?:npx\s+)?electron)(?=\s|$)/, + `$1 ${flags}`, + ); +} + +function prependEnvToShellSegments(script: string, envPrefix: string): string { + const trimmedEnv = envPrefix.trim(); + if (!trimmedEnv) return script; + return script + .split(/(\s*&&\s*)/) + .map((segment) => { + if (/^\s*&&\s*$/.test(segment)) return segment; + const trimmed = segment.trim(); + return trimmed ? `${trimmedEnv} ${trimmed}` : segment; + }) + .join(""); +} + +export function rewritePackageScriptElectronLaunch(command: string, debugFlags: string[], fallbackCwd: string): string | null { + const match = command.trim().match(/^(?.*?)(?(?:[A-Za-z_][A-Za-z0-9_]*=(?:"[^"]*"|'[^']*'|[^\s;&|]+)\s+)*)(?npm|pnpm|yarn|bun)\s+(?:run\s+)?(?