Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
362 changes: 362 additions & 0 deletions apps/desktop/package-lock.json

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion apps/desktop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"type": "module",
"main": "electron.cjs",
"scripts": {
"dev": "concurrently -k -n renderer,main,electron \"vite --port 5173\" \"tsup --watch\" \"wait-on tcp:5173 && wait-on file:dist/main/main.cjs && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .\"",
"dev": "node ./scripts/ensure-electron.cjs && concurrently -k -n renderer,main,electron \"vite --port 5173\" \"tsup --watch\" \"wait-on tcp:5173 && wait-on file:dist/main/main.cjs && cross-env VITE_DEV_SERVER_URL=http://localhost:5173 electron .\"",
"build": "tsup && vite build",
"typecheck": "tsc -p tsconfig.json --noEmit",
"lint": "echo \"(todo)\"",
Expand All @@ -15,6 +15,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-dropdown-menu": "^2.1.16",
"@radix-ui/react-tabs": "^1.1.13",
"@xyflow/react": "^12.5.0",
"chokidar": "^4.0.3",
"clsx": "^2.1.1",
"lucide-react": "^0.563.0",
Expand Down
55 changes: 55 additions & 0 deletions apps/desktop/scripts/ensure-electron.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
#!/usr/bin/env node

const fs = require("node:fs");
const path = require("node:path");
const childProcess = require("node:child_process");

const projectRoot = path.resolve(__dirname, "..");
const electronDir = path.join(projectRoot, "node_modules", "electron");
const installScript = path.join(electronDir, "install.js");
const distDir = path.join(electronDir, "dist");
const pathFile = path.join(electronDir, "path.txt");
const cacheDir = path.join(projectRoot, ".cache", "electron");

function hasInstalledBinary() {
if (!fs.existsSync(distDir)) return false;
if (!fs.existsSync(pathFile)) return false;
const relative = fs.readFileSync(pathFile, "utf8").trim();
if (!relative) return false;
return fs.existsSync(path.join(distDir, relative));
}

function main() {
if (!fs.existsSync(installScript)) {
console.error("[ensure-electron] Missing electron install script at node_modules/electron.");
console.error("[ensure-electron] Run `npm install` in apps/desktop first.");
process.exit(1);
}

if (hasInstalledBinary()) {
return;
}

fs.mkdirSync(cacheDir, { recursive: true });
console.log("[ensure-electron] Electron binary missing; running install.js...");
const result = childProcess.spawnSync(process.execPath, [installScript], {
cwd: projectRoot,
stdio: "inherit",
env: {
...process.env,
electron_config_cache: process.env.electron_config_cache || cacheDir
}
});

if (result.status !== 0) {
console.error("[ensure-electron] Electron install failed.");
process.exit(result.status ?? 1);
}

if (!hasInstalledBinary()) {
console.error("[ensure-electron] Install finished but Electron binary is still missing.");
process.exit(1);
}
}

main();
44 changes: 38 additions & 6 deletions apps/desktop/src/main/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createSessionService } from "./services/sessions/sessionService";
import { createPtyService } from "./services/pty/ptyService";
import { createDiffService } from "./services/diffs/diffService";
import { createFileService } from "./services/files/fileService";
import { createConflictService } from "./services/conflicts/conflictService";
import { createProjectConfigService } from "./services/config/projectConfigService";
import { createProcessService } from "./services/processes/processService";
import { createTestService } from "./services/tests/testService";
Expand Down Expand Up @@ -125,17 +126,22 @@ app.whenReady().then(async () => {
const project = toProjectInfo(projectRoot, baseRef);
const { projectId } = upsertProjectRow({ db, repoRoot: projectRoot, displayName: project.displayName, baseRef });

const operationService = createOperationService({ db, projectId });
let jobEngine: ReturnType<typeof createJobEngine> | null = null;
const laneService = createLaneService({
db,
projectRoot,
projectId,
defaultBaseRef: baseRef,
worktreesDir: adePaths.worktreesDir
worktreesDir: adePaths.worktreesDir,
operationService,
onHeadChanged: ({ laneId, reason }) => {
jobEngine?.onHeadChanged({ laneId, reason });
}
});
await laneService.ensurePrimaryLane();
const sessionService = createSessionService({ db });
const diffService = createDiffService({ laneService });
const fileService = createFileService({ laneService });
const projectConfigService = createProjectConfigService({
projectRoot,
adeDir: adePaths.adeDir,
Expand All @@ -144,8 +150,6 @@ app.whenReady().then(async () => {
logger
});

const operationService = createOperationService({ db, projectId });

const packService = createPackService({
db,
logger,
Expand All @@ -158,9 +162,27 @@ app.whenReady().then(async () => {
operationService
});

const jobEngine = createJobEngine({
const conflictService = createConflictService({
db,
logger,
packService
projectId,
projectRoot,
laneService,
conflictPacksDir: path.join(adePaths.packsDir, "conflicts"),
onEvent: (event) => broadcast(IPC.conflictsEvent, event)
});

jobEngine = createJobEngine({
logger,
packService,
conflictService
});

const fileService = createFileService({
laneService,
onLaneWorktreeMutation: ({ laneId, reason }) => {
jobEngine.onLaneDirtyChanged({ laneId, reason });
}
});

const ptyService = createPtyService({
Expand All @@ -181,6 +203,9 @@ app.whenReady().then(async () => {
laneService,
operationService,
logger,
onWorktreeChanged: ({ laneId, reason }) => {
jobEngine.onLaneDirtyChanged({ laneId, reason });
},
onHeadChanged: ({ laneId, reason }) => {
jobEngine.onHeadChanged({ laneId, reason });
}
Expand Down Expand Up @@ -227,6 +252,8 @@ app.whenReady().then(async () => {
fileService,
operationService,
gitService,
conflictService,
jobEngine,
packService,
projectConfigService,
processService,
Expand All @@ -235,6 +262,11 @@ app.whenReady().then(async () => {
};

const closeContext = () => {
try {
ctxRef.jobEngine.dispose();
} catch {
// ignore
}
try {
ctxRef.fileService.dispose();
} catch {
Expand Down
87 changes: 87 additions & 0 deletions apps/desktop/src/main/services/config/laneOverlayMatcher.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import type { LaneOverlayOverrides, LaneOverlayPolicy, LaneSummary } from "../../../shared/types";

function escapeRegExp(value: string): string {
return value.replace(/[|\\{}()[\]^$+?.]/g, "\\$&");
}

function globToRegExp(pattern: string): RegExp {
const normalized = pattern.trim();
if (!normalized.length) return /^$/;
const parts = normalized.split("*").map((chunk) => escapeRegExp(chunk));
return new RegExp(`^${parts.join(".*")}$`, "i");
}

function normalizeSet(values: string[] | undefined): Set<string> {
return new Set((values ?? []).map((value) => value.trim().toLowerCase()).filter(Boolean));
}

function intersectOrAdopt(current: string[] | undefined, next: string[] | undefined): string[] | undefined {
if (!next || next.length === 0) return current;
if (!current || current.length === 0) return [...next];
const allowed = new Set(next);
return current.filter((entry) => allowed.has(entry));
}

function matchesPolicy(lane: LaneSummary, policy: LaneOverlayPolicy): boolean {
if (!policy.enabled) return false;
const match = policy.match ?? {};

if (match.laneIds && match.laneIds.length > 0 && !match.laneIds.includes(lane.id)) {
return false;
}
if (match.laneTypes && match.laneTypes.length > 0 && !match.laneTypes.includes(lane.laneType)) {
return false;
}
if (match.namePattern) {
const pattern = globToRegExp(match.namePattern);
if (!pattern.test(lane.name)) return false;
}
if (match.branchPattern) {
const pattern = globToRegExp(match.branchPattern);
if (!pattern.test(lane.branchRef)) return false;
}
if (match.tags && match.tags.length > 0) {
const laneTags = normalizeSet(lane.tags);
const required = normalizeSet(match.tags);
let matched = false;
for (const tag of required) {
if (laneTags.has(tag)) {
matched = true;
break;
}
}
if (!matched) return false;
}

return true;
}

export function matchLaneOverlayPolicies(lane: LaneSummary, policies: LaneOverlayPolicy[]): LaneOverlayOverrides {
const merged: LaneOverlayOverrides = {};

for (const policy of policies) {
if (!matchesPolicy(lane, policy)) continue;
const overrides = policy.overrides ?? {};
if (overrides.env) {
merged.env = {
...(merged.env ?? {}),
...overrides.env
};
}
if (typeof overrides.cwd === "string" && overrides.cwd.trim().length > 0) {
merged.cwd = overrides.cwd.trim();
}
merged.processIds = intersectOrAdopt(merged.processIds, overrides.processIds);
merged.testSuiteIds = intersectOrAdopt(merged.testSuiteIds, overrides.testSuiteIds);
}

if (merged.processIds && merged.processIds.length === 0) {
delete merged.processIds;
}
if (merged.testSuiteIds && merged.testSuiteIds.length === 0) {
delete merged.testSuiteIds;
}

return merged;
}

Loading