Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
4cd84a4
fix: add API proxy to Vite dev server for web mode CORS
DhanushSantosh Jan 17, 2026
7eae021
chore: update package-lock.json
DhanushSantosh Jan 17, 2026
4186b80
fix: use relative URLs in web mode to leverage Vite proxy
DhanushSantosh Jan 17, 2026
b8875f7
fix: improve CORS configuration to handle localhost and private IPs
DhanushSantosh Jan 17, 2026
e10cb83
debug: add CORS logging to diagnose origin rejection
DhanushSantosh Jan 17, 2026
b0b4976
fix: add localhost to CORS_ORIGIN for web mode development
DhanushSantosh Jan 17, 2026
fdad82b
fix: enable WebSocket proxying in Vite dev server
DhanushSantosh Jan 17, 2026
a7f7898
fix: persist session token to localStorage for web mode page reload s…
DhanushSantosh Jan 17, 2026
174c02c
fix: automatically remove projects with non-existent paths
DhanushSantosh Jan 17, 2026
2a8706e
fix: add session token to image URLs for web mode authentication
DhanushSantosh Jan 17, 2026
b66efae
fix: sync projects immediately instead of debouncing
DhanushSantosh Jan 17, 2026
9137f0e
fix: keep localStorage cache in sync with server settings
DhanushSantosh Jan 17, 2026
7b7ac72
fix: use shared data directory for Electron and web modes
DhanushSantosh Jan 17, 2026
832d10e
refactor: replace Loader2 with Spinner component across the application
webdevcody Jan 17, 2026
5b1e010
refactor: standardize PR state representation across the application
Shironex Jan 17, 2026
44e665f
fix: adress pr comments
Shironex Jan 17, 2026
327aef8
Merge pull request #562 from AutoMaker-Org/feature/v0.12.0rc-17686889…
Shironex Jan 18, 2026
484d4c6
fix: use shared data directory for Electron and web modes
DhanushSantosh Jan 18, 2026
f378122
fix: resolve data directory persistence between Electron and Web modes
DhanushSantosh Jan 18, 2026
2e57553
Merge remote-tracking branch 'upstream/v0.13.0rc' into patchcraft
DhanushSantosh Jan 18, 2026
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
30 changes: 22 additions & 8 deletions apps/server/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,9 @@ const PORT = parseInt(process.env.PORT || '3008', 10);
const HOST = process.env.HOST || '0.0.0.0';
const HOSTNAME = process.env.HOSTNAME || 'localhost';
const DATA_DIR = process.env.DATA_DIR || './data';
logger.info('[SERVER_STARTUP] process.env.DATA_DIR:', process.env.DATA_DIR);
logger.info('[SERVER_STARTUP] Resolved DATA_DIR:', DATA_DIR);
logger.info('[SERVER_STARTUP] process.cwd():', process.cwd());
const ENABLE_REQUEST_LOGGING_DEFAULT = process.env.ENABLE_REQUEST_LOGGING !== 'false'; // Default to true

// Runtime-configurable request logging flag (can be changed via settings)
Expand Down Expand Up @@ -175,14 +178,25 @@ app.use(
return;
}

// For local development, allow localhost origins
if (
origin.startsWith('http://localhost:') ||
origin.startsWith('http://127.0.0.1:') ||
origin.startsWith('http://[::1]:')
) {
callback(null, origin);
return;
// For local development, allow all localhost/loopback origins (any port)
try {
const url = new URL(origin);
const hostname = url.hostname;

if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
hostname === '0.0.0.0' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')
) {
Comment on lines +186 to +194
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The check hostname.startsWith('172.') for allowing local development origins is too broad. It will match any IP starting with 172., including public IP addresses, not just the private range 172.16.0.0 - 172.31.255.255. This could be a security risk by unintentionally allowing connections from outside the local network.

Using a more specific check, like a regular expression for the 172.16.0.0/12 range, would be safer.

Suggested change
if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
hostname === '0.0.0.0' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
hostname.startsWith('172.')
) {
if (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
hostname === '0.0.0.0' ||
hostname.startsWith('192.168.') ||
hostname.startsWith('10.') ||
/^172\.(1[6-9]|2[0-9]|3[0-1])\./.test(hostname)
) {

callback(null, origin);
return;
}
} catch (err) {
// Ignore URL parsing errors
}

// Reject other origins by default for security
Expand Down
12 changes: 4 additions & 8 deletions apps/server/src/lib/worktree-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,14 @@

import * as secureFs from './secure-fs.js';
import * as path from 'path';
import type { PRState, WorktreePRInfo } from '@automaker/types';

// Re-export types for backwards compatibility
export type { PRState, WorktreePRInfo };

/** Maximum length for sanitized branch names in filesystem paths */
const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200;

export interface WorktreePRInfo {
number: number;
url: string;
title: string;
state: string;
createdAt: string;
}

export interface WorktreeMetadata {
branch: string;
createdAt: string;
Expand Down
26 changes: 16 additions & 10 deletions apps/server/src/routes/settings/routes/update-global.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,18 +45,24 @@ export function createUpdateGlobalHandler(settingsService: SettingsService) {
}

// Minimal debug logging to help diagnose accidental wipes.
if ('projects' in updates || 'theme' in updates || 'localStorageMigrated' in updates) {
const projectsLen = Array.isArray((updates as any).projects)
? (updates as any).projects.length
: undefined;
logger.info(
`Update global settings request: projects=${projectsLen ?? 'n/a'}, theme=${
(updates as any).theme ?? 'n/a'
}, localStorageMigrated=${(updates as any).localStorageMigrated ?? 'n/a'}`
);
}
const projectsLen = Array.isArray((updates as any).projects)
? (updates as any).projects.length
: undefined;
const trashedLen = Array.isArray((updates as any).trashedProjects)
? (updates as any).trashedProjects.length
: undefined;
logger.info(
`[SERVER_SETTINGS_UPDATE] Request received: projects=${projectsLen ?? 'n/a'}, trashedProjects=${trashedLen ?? 'n/a'}, theme=${
(updates as any).theme ?? 'n/a'
}, localStorageMigrated=${(updates as any).localStorageMigrated ?? 'n/a'}`
);

logger.info('[SERVER_SETTINGS_UPDATE] Calling updateGlobalSettings...');
const settings = await settingsService.updateGlobalSettings(updates);
logger.info(
'[SERVER_SETTINGS_UPDATE] Update complete, projects count:',
settings.projects?.length ?? 0
);

// Apply server log level if it was updated
if ('serverLogLevel' in updates && updates.serverLogLevel) {
Expand Down
10 changes: 7 additions & 3 deletions apps/server/src/routes/worktree/routes/create-pr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
} from '../common.js';
import { updateWorktreePRInfo } from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils';
import { validatePRState } from '@automaker/types';

const logger = createLogger('CreatePR');

Expand Down Expand Up @@ -268,11 +269,12 @@ export function createCreatePRHandler() {
prAlreadyExisted = true;

// Store the existing PR info in metadata
// GitHub CLI returns uppercase states: OPEN, MERGED, CLOSED
await updateWorktreePRInfo(effectiveProjectPath, branchName, {
number: existingPr.number,
url: existingPr.url,
title: existingPr.title || title,
state: existingPr.state || 'open',
state: validatePRState(existingPr.state),
createdAt: new Date().toISOString(),
});
logger.debug(
Expand Down Expand Up @@ -319,11 +321,12 @@ export function createCreatePRHandler() {

if (prNumber) {
try {
// Note: GitHub doesn't have a 'DRAFT' state - drafts still show as 'OPEN'
await updateWorktreePRInfo(effectiveProjectPath, branchName, {
number: prNumber,
url: prUrl,
title,
state: draft ? 'draft' : 'open',
state: 'OPEN',
createdAt: new Date().toISOString(),
});
logger.debug(`Stored PR info for branch ${branchName}: PR #${prNumber}`);
Expand Down Expand Up @@ -352,11 +355,12 @@ export function createCreatePRHandler() {
prNumber = existingPr.number;
prAlreadyExisted = true;

// GitHub CLI returns uppercase states: OPEN, MERGED, CLOSED
await updateWorktreePRInfo(effectiveProjectPath, branchName, {
number: existingPr.number,
url: existingPr.url,
title: existingPr.title || title,
state: existingPr.state || 'open',
state: validatePRState(existingPr.state),
createdAt: new Date().toISOString(),
});
logger.debug(`Fetched and stored existing PR: #${existingPr.number}`);
Expand Down
54 changes: 38 additions & 16 deletions apps/server/src/routes/worktree/routes/list.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,13 @@ import path from 'path';
import * as secureFs from '../../../lib/secure-fs.js';
import { isGitRepo } from '@automaker/git-utils';
import { getErrorMessage, logError, normalizePath, execEnv, isGhCliAvailable } from '../common.js';
import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
import {
readAllWorktreeMetadata,
updateWorktreePRInfo,
type WorktreePRInfo,
} from '../../../lib/worktree-metadata.js';
import { createLogger } from '@automaker/utils';
import { validatePRState } from '@automaker/types';
import {
checkGitHubRemote,
type GitHubRemoteStatus,
Expand Down Expand Up @@ -168,8 +173,11 @@ async function getGitHubRemoteStatus(projectPath: string): Promise<GitHubRemoteS
}

/**
* Fetch open PRs from GitHub and create a map of branch name to PR info.
* This allows detecting PRs that were created outside the app.
* Fetch all PRs from GitHub and create a map of branch name to PR info.
* Uses --state all to include merged/closed PRs, allowing detection of
* state changes (e.g., when a PR is merged on GitHub).
*
* This also allows detecting PRs that were created outside the app.
*
* Uses cached GitHub remote status to avoid repeated warnings when the
* project doesn't have a GitHub remote configured.
Expand All @@ -192,9 +200,9 @@ async function fetchGitHubPRs(projectPath: string): Promise<Map<string, Worktree
? `-R ${remoteStatus.owner}/${remoteStatus.repo}`
: '';

// Fetch open PRs from GitHub
// Fetch all PRs from GitHub (including merged/closed to detect state changes)
const { stdout } = await execAsync(
`gh pr list ${repoFlag} --state open --json number,title,url,state,headRefName,createdAt --limit 1000`,
`gh pr list ${repoFlag} --state all --json number,title,url,state,headRefName,createdAt --limit 1000`,
{ cwd: projectPath, env: execEnv, timeout: 15000 }
);

Expand All @@ -212,7 +220,8 @@ async function fetchGitHubPRs(projectPath: string): Promise<Map<string, Worktree
number: pr.number,
url: pr.url,
title: pr.title,
state: pr.state,
// GitHub CLI returns state as uppercase: OPEN, MERGED, CLOSED
state: validatePRState(pr.state),
createdAt: pr.createdAt,
});
}
Expand Down Expand Up @@ -351,23 +360,36 @@ export function createListHandler() {
}
}

// Add PR info from metadata or GitHub for each worktree
// Only fetch GitHub PRs if includeDetails is requested (performance optimization)
// Assign PR info to each worktree, preferring fresh GitHub data over cached metadata.
// Only fetch GitHub PRs if includeDetails is requested (performance optimization).
// Uses --state all to detect merged/closed PRs, limited to 1000 recent PRs.
const githubPRs = includeDetails
? await fetchGitHubPRs(projectPath)
: new Map<string, WorktreePRInfo>();

for (const worktree of worktrees) {
const metadata = allMetadata.get(worktree.branch);
if (metadata?.pr) {
// Use stored metadata (more complete info)
worktree.pr = metadata.pr;
} else if (includeDetails) {
// Fall back to GitHub PR detection only when includeDetails is requested
const githubPR = githubPRs.get(worktree.branch);
if (githubPR) {
worktree.pr = githubPR;
const githubPR = githubPRs.get(worktree.branch);

if (githubPR) {
// Prefer fresh GitHub data (it has the current state)
worktree.pr = githubPR;

// Sync metadata with GitHub state when:
// 1. No metadata exists for this PR (PR created externally)
// 2. State has changed (e.g., merged/closed on GitHub)
const needsSync = !metadata?.pr || metadata.pr.state !== githubPR.state;
if (needsSync) {
// Fire and forget - don't block the response
updateWorktreePRInfo(projectPath, worktree.branch, githubPR).catch((err) => {
logger.warn(
`Failed to update PR info for ${worktree.branch}: ${getErrorMessage(err)}`
);
});
}
} else if (metadata?.pr) {
// Fall back to stored metadata (for PRs not in recent GitHub response)
worktree.pr = metadata.pr;
}
}

Expand Down
30 changes: 28 additions & 2 deletions apps/server/src/services/settings-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,13 +273,39 @@ export class SettingsService {
};

const currentProjectsLen = Array.isArray(current.projects) ? current.projects.length : 0;
// Check if this is a legitimate project removal (moved to trash) vs accidental wipe
const newTrashedProjectsLen = Array.isArray(sanitizedUpdates.trashedProjects)
? sanitizedUpdates.trashedProjects.length
: Array.isArray(current.trashedProjects)
? current.trashedProjects.length
: 0;

if (
Array.isArray(sanitizedUpdates.projects) &&
sanitizedUpdates.projects.length === 0 &&
currentProjectsLen > 0
) {
attemptedProjectWipe = true;
delete sanitizedUpdates.projects;
// Only treat as accidental wipe if trashedProjects is also empty
// (If projects are moved to trash, they appear in trashedProjects)
if (newTrashedProjectsLen === 0) {
logger.warn(
'[WIPE_PROTECTION] Attempted to set projects to empty array with no trash! Ignoring update.',
{
currentProjectsLen,
newProjectsLen: 0,
newTrashedProjectsLen,
currentProjects: current.projects?.map((p) => p.name),
}
);
attemptedProjectWipe = true;
delete sanitizedUpdates.projects;
} else {
logger.info('[LEGITIMATE_REMOVAL] Removing all projects to trash', {
currentProjectsLen,
newProjectsLen: 0,
movedToTrash: newTrashedProjectsLen,
});
}
}

ignoreEmptyArrayOverwrite('trashedProjects');
Expand Down
14 changes: 7 additions & 7 deletions apps/server/tests/unit/lib/worktree-metadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ describe('worktree-metadata.ts', () => {
number: 123,
url: 'https://github.com/owner/repo/pull/123',
title: 'Test PR',
state: 'open',
state: 'OPEN',
createdAt: new Date().toISOString(),
},
};
Expand Down Expand Up @@ -158,7 +158,7 @@ describe('worktree-metadata.ts', () => {
number: 456,
url: 'https://github.com/owner/repo/pull/456',
title: 'Updated PR',
state: 'closed',
state: 'CLOSED',
createdAt: new Date().toISOString(),
},
};
Expand All @@ -177,7 +177,7 @@ describe('worktree-metadata.ts', () => {
number: 789,
url: 'https://github.com/owner/repo/pull/789',
title: 'New PR',
state: 'open',
state: 'OPEN',
createdAt: new Date().toISOString(),
};

Expand All @@ -201,7 +201,7 @@ describe('worktree-metadata.ts', () => {
number: 999,
url: 'https://github.com/owner/repo/pull/999',
title: 'Updated PR',
state: 'merged',
state: 'MERGED',
createdAt: new Date().toISOString(),
};

Expand All @@ -224,7 +224,7 @@ describe('worktree-metadata.ts', () => {
number: 111,
url: 'https://github.com/owner/repo/pull/111',
title: 'PR',
state: 'open',
state: 'OPEN',
createdAt: new Date().toISOString(),
};

Expand Down Expand Up @@ -259,7 +259,7 @@ describe('worktree-metadata.ts', () => {
number: 222,
url: 'https://github.com/owner/repo/pull/222',
title: 'Has PR',
state: 'open',
state: 'OPEN',
createdAt: new Date().toISOString(),
};

Expand Down Expand Up @@ -297,7 +297,7 @@ describe('worktree-metadata.ts', () => {
number: 333,
url: 'https://github.com/owner/repo/pull/333',
title: 'PR 3',
state: 'open',
state: 'OPEN',
createdAt: new Date().toISOString(),
},
};
Expand Down
3 changes: 2 additions & 1 deletion apps/ui/src/components/claude-usage-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Clock, ExternalLink } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
Expand Down Expand Up @@ -279,7 +280,7 @@ export function ClaudeUsagePopover() {
) : !claudeUsage ? (
// Loading state
<div className="flex flex-col items-center justify-center py-8 space-y-2">
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground/50" />
<Spinner size="lg" />
<p className="text-xs text-muted-foreground">Loading usage data...</p>
</div>
) : (
Expand Down
3 changes: 2 additions & 1 deletion apps/ui/src/components/codex-usage-popover.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useState, useEffect, useMemo, useCallback } from 'react';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { Button } from '@/components/ui/button';
import { RefreshCw, AlertTriangle, CheckCircle, XCircle, Clock, ExternalLink } from 'lucide-react';
import { Spinner } from '@/components/ui/spinner';
import { cn } from '@/lib/utils';
import { getElectronAPI } from '@/lib/electron';
import { useAppStore } from '@/store/app-store';
Expand Down Expand Up @@ -333,7 +334,7 @@ export function CodexUsagePopover() {
) : !codexUsage ? (
// Loading state
<div className="flex flex-col items-center justify-center py-8 space-y-2">
<RefreshCw className="w-6 h-6 animate-spin text-muted-foreground/50" />
<Spinner size="lg" />
<p className="text-xs text-muted-foreground">Loading usage data...</p>
</div>
) : codexUsage.rateLimits ? (
Expand Down
Loading
Loading