diff --git a/AGENTS.md b/AGENTS.md index e361eff06..93a391099 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -44,3 +44,23 @@ - Do not reframe ADE as a docs site, Mintlify project, or generic template app. - Do not store secrets in plaintext project files when an encrypted store already exists. - Do not leave policy enforcement in prompts alone when a code path can enforce it directly. + +## Cursor Cloud specific instructions + +### Environment overview + +- **Node.js 22.x** is required (`node:sqlite` is used as the primary database engine). +- Each app under `apps/` has its own independent `node_modules` and `package-lock.json` (no npm workspaces). +- Validation commands are documented in the "Validation" section above. +- The desktop test suite (265 test files) is large; CI shards it. For local iteration, run targeted tests (e.g. `npm --prefix apps/desktop run test:unit`) or a single file rather than the full suite. + +### Running the Electron desktop app on Linux + +- Set `ADE_DISABLE_HARDWARE_ACCEL=1` — the VM has no real GPU, and without this the app crashes on `WebGL1 blocklisted`. +- `node-pty` ships only macOS/Windows prebuilds. After `npm install`, run `npm --prefix apps/desktop run rebuild:native` to compile `pty.node` for Electron on Linux. Then manually compile the spawn-helper: `cd apps/desktop/node_modules/node-pty && g++ -o build/Release/spawn-helper src/unix/spawn-helper.cc`. +- The `npm run dev` script has a race condition: `predev` clears `dist/`, then tsup + Electron start in parallel, so the first Electron launch fails with "Cannot find module main.cjs" and auto-restarts. To avoid this, pre-build first (`npm run build`) then run the dev launcher directly: `node scripts/normalize-runtime-binaries.cjs && node scripts/ensure-electron.cjs && node scripts/dev.cjs`. +- Alternatively, start Vite and Electron separately for more control: `npx vite --port 5173 --strictPort --force &` then `VITE_DEV_SERVER_URL=http://localhost:5173 npx electron . --no-sandbox`. +- `cr-sqlite` extension binaries are only available for macOS. On Linux the app logs `db.crsqlite_unavailable` as a warning and continues without CRDT sync — this is non-blocking for development. +- The `ADE_PROJECT_ROOT=/workspace` env var tells the main process to auto-open a project at startup. However, there is a timing race: the renderer's initial `getProject()` call may return null before the async project switch completes, causing the welcome screen to appear even though the backend loaded the project. A workaround is to open the project manually via the "Open a project" button in the top bar. +- Computer-use features (screenshot, video capture, GUI automation) are macOS-only (`screencapture`, `osascript`). On Linux these gracefully degrade — the app returns `blocked_by_capability`. +- `electron-builder` config only defines a `mac` target. Distributable Linux builds (deb/AppImage) are not configured, but dev mode works fine. diff --git a/apps/desktop/src/main/main.ts b/apps/desktop/src/main/main.ts index 162752d69..b7a28b07f 100644 --- a/apps/desktop/src/main/main.ts +++ b/apps/desktop/src/main/main.ts @@ -650,9 +650,10 @@ app.whenReady().then(async () => { recordRecent?: boolean; userSelectedProject?: boolean; }): Promise => { - // Any pre-existing .ade directory, whether from the tracked shared scaffold or - // a prior local open, means this repo should not feel like a brand-new ADE bootstrap. - const hadAdeDir = fs.existsSync(path.join(projectRoot, ".ade")); + // The .ade directory may exist from git (shared scaffold files like ade.yaml), + // but the db is gitignored and machine-local. A missing db means this machine + // has never completed setup, so onboarding should run. + const hadAdeDir = fs.existsSync(path.join(projectRoot, ".ade", "ade.db")); const adePaths = ensureAdeDirs(projectRoot); const { initApiKeyStore } = await import("./services/ai/apiKeyStore"); initApiKeyStore(projectRoot); @@ -860,6 +861,17 @@ app.whenReady().then(async () => { projectConfigService }); + if (!hadAdeDir) { + const hasEnvCredentials = + Boolean((process.env.GITHUB_TOKEN ?? process.env.ADE_GITHUB_TOKEN ?? "").trim()) || + Boolean((process.env.ANTHROPIC_API_KEY ?? process.env.OPENAI_API_KEY ?? "").trim()) || + Boolean((process.env.LINEAR_API_KEY ?? process.env.ADE_LINEAR_TOKEN ?? "").trim()); + if (hasEnvCredentials) { + onboardingService.complete(); + logger.info("onboarding.auto_completed", { reason: "env_credentials_detected" }); + } + } + rebaseSuggestionService = createRebaseSuggestionService({ db, logger, diff --git a/apps/desktop/src/main/services/ai/apiKeyStore.ts b/apps/desktop/src/main/services/ai/apiKeyStore.ts index 246bc163e..6b257cff2 100644 --- a/apps/desktop/src/main/services/ai/apiKeyStore.ts +++ b/apps/desktop/src/main/services/ai/apiKeyStore.ts @@ -144,11 +144,30 @@ export function storeApiKey(provider: string, key: string): void { persist(); } +const ENV_KEY_PROVIDERS: Record = { + anthropic: "ANTHROPIC_API_KEY", + openai: "OPENAI_API_KEY", + google: "GOOGLE_API_KEY", + mistral: "MISTRAL_API_KEY", + deepseek: "DEEPSEEK_API_KEY", + xai: "XAI_API_KEY", + groq: "GROQ_API_KEY", + together: "TOGETHER_API_KEY", + openrouter: "OPENROUTER_API_KEY", +}; + export function getApiKey(provider: string): string | null { const normalizedProvider = provider.trim().toLowerCase(); if (!normalizedProvider.length) return null; const store = ensureStore(); - return store[normalizedProvider] ?? null; + const stored = store[normalizedProvider]; + if (stored) return stored; + const envVar = ENV_KEY_PROVIDERS[normalizedProvider]; + if (envVar) { + const envValue = (process.env[envVar] ?? "").trim(); + if (envValue.length > 0) return envValue; + } + return null; } export function deleteApiKey(provider: string): void { diff --git a/apps/desktop/src/main/services/cto/linearCredentialService.ts b/apps/desktop/src/main/services/cto/linearCredentialService.ts index c4bbd6507..ecdd669d6 100644 --- a/apps/desktop/src/main/services/cto/linearCredentialService.ts +++ b/apps/desktop/src/main/services/cto/linearCredentialService.ts @@ -246,6 +246,12 @@ export function createLinearCredentialService(args: LinearCredentialServiceArgs) if (cachedToken !== undefined) return cachedToken; importLegacyTokenIfNeeded(); cachedToken = readEncryptedToken(); + if (!cachedToken) { + const envToken = (process.env.LINEAR_API_KEY ?? process.env.ADE_LINEAR_TOKEN ?? "").trim(); + if (envToken.length > 0) { + cachedToken = { token: envToken, authMode: "manual" }; + } + } return cachedToken; }; diff --git a/apps/desktop/src/main/services/github/githubService.ts b/apps/desktop/src/main/services/github/githubService.ts index 23bd8d2ba..cad47468e 100644 --- a/apps/desktop/src/main/services/github/githubService.ts +++ b/apps/desktop/src/main/services/github/githubService.ts @@ -146,10 +146,10 @@ export function createGithubService({ const readStoredToken = (): string | null => { const token = migrateLegacyTokenIfNeeded(); - if (!token) { - tokenDecryptionFailed = false; - } - return token; + if (token) return token; + tokenDecryptionFailed = false; + const envToken = (process.env.GITHUB_TOKEN ?? process.env.ADE_GITHUB_TOKEN ?? "").trim(); + return envToken.length > 0 ? envToken : null; }; const persistToken = (token: string | null): void => { diff --git a/apps/desktop/src/main/services/state/kvDb.ts b/apps/desktop/src/main/services/state/kvDb.ts index 2fda9cca5..620f2c178 100644 --- a/apps/desktop/src/main/services/state/kvDb.ts +++ b/apps/desktop/src/main/services/state/kvDb.ts @@ -3129,17 +3129,15 @@ export async function openKvDb(dbPath: string, logger: Logger): Promise { return getRow(db, sql, params); }; - const crsqliteUnavailableError = () => new Error("cr-sqlite extension not available on this platform"); - const sync: AdeDbSyncApi = { getSiteId: () => desiredSiteId, getDbVersion: () => { - if (!hasCrsqlite) throw crsqliteUnavailableError(); + if (!hasCrsqlite) return 0; const row = get<{ db_version: number }>("select crsql_db_version() as db_version"); return Number(row?.db_version ?? 0); }, exportChangesSince: (version: number) => { - if (!hasCrsqlite) throw crsqliteUnavailableError(); + if (!hasCrsqlite) return []; const rows = allRows<{ table_name: string; pk: unknown; @@ -3180,7 +3178,7 @@ export async function openKvDb(dbPath: string, logger: Logger): Promise { })); }, applyChanges: (changes: CrsqlChangeRow[]) => { - if (!hasCrsqlite) throw crsqliteUnavailableError(); + if (!hasCrsqlite) return { appliedCount: 0, dbVersion: 0, touchedTables: [], rebuiltFts: false }; let appliedCount = 0; const touchedTables = new Set(); runStatement(db, "begin");