mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 10:23:07 +00:00
Fix concurrency limits and remote branch fetching issues (#788)
* Changes from fix/bug-fixes * feat: Refactor worktree iteration and improve error logging across services * feat: Extract URL/port patterns to module level and fix abort condition * fix: Improve IPv6 loopback handling, select component layout, and terminal UI * feat: Add thinking level defaults and adjust list row padding * Update apps/ui/src/store/app-store.ts Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * feat: Add worktree-aware terminal creation and split options, fix npm security issues from audit * feat: Add tracked remote detection to pull dialog flow * feat: Add merge state tracking to git operations * feat: Improve merge detection and add post-merge action preferences * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * Update apps/ui/src/components/views/board-view/dialogs/git-pull-dialog.tsx Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com> * fix: Pass merge detection info to stash reapplication and handle merge state consistently * fix: Call onPulled callback in merge handlers and add validation checks --------- Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
This commit is contained in:
@@ -19,6 +19,69 @@ const logger = createLogger('DevServerService');
|
||||
// Maximum scrollback buffer size (characters) - matches TerminalService pattern
|
||||
const MAX_SCROLLBACK_SIZE = 50000; // ~50KB per dev server
|
||||
|
||||
// URL patterns for detecting full URLs from dev server output.
|
||||
// Defined once at module level to avoid reallocation on every call to detectUrlFromOutput.
|
||||
// Ordered from most specific (framework-specific) to least specific.
|
||||
const URL_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
|
||||
// Vite / Nuxt / SvelteKit / Astro / Angular CLI format: "Local: http://..."
|
||||
{
|
||||
pattern: /(?:Local|Network|External):\s+(https?:\/\/[^\s]+)/i,
|
||||
description: 'Vite/Nuxt/SvelteKit/Astro/Angular format',
|
||||
},
|
||||
// Next.js format: "ready - started server on 0.0.0.0:3000, url: http://localhost:3000"
|
||||
// Next.js 14+: "▲ Next.js 14.0.0\n- Local: http://localhost:3000"
|
||||
{
|
||||
pattern: /(?:ready|started server).*?(?:url:\s*)?(https?:\/\/[^\s,]+)/i,
|
||||
description: 'Next.js format',
|
||||
},
|
||||
// Remix format: "started at http://localhost:3000"
|
||||
// Django format: "Starting development server at http://127.0.0.1:8000/"
|
||||
// Rails / Puma: "Listening on http://127.0.0.1:3000"
|
||||
// Generic: "listening at http://...", "available at http://...", "running at http://..."
|
||||
{
|
||||
pattern:
|
||||
/(?:starting|started|listening|running|available|serving|accessible)\s+(?:at|on)\s+(https?:\/\/[^\s,)]+)/i,
|
||||
description: 'Generic "starting/started/listening at" format',
|
||||
},
|
||||
// PHP built-in server: "Development Server (http://localhost:8000) started"
|
||||
{
|
||||
pattern: /(?:server|development server)\s*\(\s*(https?:\/\/[^\s)]+)\s*\)/i,
|
||||
description: 'PHP server format',
|
||||
},
|
||||
// Webpack Dev Server: "Project is running at http://localhost:8080/"
|
||||
{
|
||||
pattern: /(?:project|app|application)\s+(?:is\s+)?running\s+(?:at|on)\s+(https?:\/\/[^\s,]+)/i,
|
||||
description: 'Webpack/generic "running at" format',
|
||||
},
|
||||
// Go / Rust / generic: "Serving on http://...", "Server on http://..."
|
||||
{
|
||||
pattern: /(?:serving|server)\s+(?:on|at)\s+(https?:\/\/[^\s,]+)/i,
|
||||
description: 'Generic "serving on" format',
|
||||
},
|
||||
// Localhost URL with port (conservative - must be localhost/127.0.0.1/[::]/0.0.0.0)
|
||||
// This catches anything that looks like a dev server URL
|
||||
{
|
||||
pattern: /(https?:\/\/(?:localhost|127\.0\.0\.1|\[::\]|0\.0\.0\.0):\d+\S*)/i,
|
||||
description: 'Generic localhost URL with port',
|
||||
},
|
||||
];
|
||||
|
||||
// Port-only patterns for detecting port numbers from dev server output
|
||||
// when a full URL is not present in the output.
|
||||
// Defined once at module level to avoid reallocation on every call to detectUrlFromOutput.
|
||||
const PORT_PATTERNS: Array<{ pattern: RegExp; description: string }> = [
|
||||
// "listening on port 3000", "server on port 3000", "started on port 3000"
|
||||
{
|
||||
pattern: /(?:listening|running|started|serving|available)\s+on\s+port\s+(\d+)/i,
|
||||
description: '"listening on port" format',
|
||||
},
|
||||
// "Port: 3000", "port 3000" (at start of line or after whitespace)
|
||||
{
|
||||
pattern: /(?:^|\s)port[:\s]+(\d{4,5})(?:\s|$|[.,;])/im,
|
||||
description: '"port:" format',
|
||||
},
|
||||
];
|
||||
|
||||
// Throttle output to prevent overwhelming WebSocket under heavy load
|
||||
const OUTPUT_THROTTLE_MS = 4; // ~250fps max update rate for responsive feedback
|
||||
const OUTPUT_BATCH_SIZE = 4096; // Smaller batches for lower latency
|
||||
@@ -105,9 +168,52 @@ class DevServerService {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Strip ANSI escape codes from a string
|
||||
* Dev server output often contains color codes that can interfere with URL detection
|
||||
*/
|
||||
private stripAnsi(str: string): string {
|
||||
// Matches ANSI escape sequences: CSI sequences, OSC sequences, and simple escapes
|
||||
// eslint-disable-next-line no-control-regex
|
||||
return str.replace(/\x1B(?:\[[0-9;]*[a-zA-Z]|\].*?(?:\x07|\x1B\\)|\[[?]?[0-9;]*[hl])/g, '');
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract port number from a URL string.
|
||||
* Returns the explicit port if present, or null if no port is specified.
|
||||
* Default protocol ports (80/443) are intentionally NOT returned to avoid
|
||||
* overwriting allocated dev server ports with protocol defaults.
|
||||
*/
|
||||
private extractPortFromUrl(url: string): number | null {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (parsed.port) {
|
||||
return parseInt(parsed.port, 10);
|
||||
}
|
||||
return null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect actual server URL from output
|
||||
* Parses stdout/stderr for common URL patterns from dev servers
|
||||
* Parses stdout/stderr for common URL patterns from dev servers.
|
||||
*
|
||||
* Supports detection of URLs from:
|
||||
* - Vite: "Local: http://localhost:5173/"
|
||||
* - Next.js: "ready - started server on 0.0.0.0:3000, url: http://localhost:3000"
|
||||
* - Nuxt: "Local: http://localhost:3000/"
|
||||
* - Remix: "started at http://localhost:3000"
|
||||
* - Astro: "Local http://localhost:4321/"
|
||||
* - SvelteKit: "Local: http://localhost:5173/"
|
||||
* - CRA/Webpack: "On Your Network: http://192.168.1.1:3000"
|
||||
* - Angular: "Local: http://localhost:4200/"
|
||||
* - Express/Fastify/Koa: "Server listening on port 3000"
|
||||
* - Django: "Starting development server at http://127.0.0.1:8000/"
|
||||
* - Rails: "Listening on http://127.0.0.1:3000"
|
||||
* - PHP: "Development Server (http://localhost:8000) started"
|
||||
* - Generic: Any localhost URL with a port
|
||||
*/
|
||||
private detectUrlFromOutput(server: DevServerInfo, content: string): void {
|
||||
// Skip if URL already detected
|
||||
@@ -115,39 +221,95 @@ class DevServerService {
|
||||
return;
|
||||
}
|
||||
|
||||
// Common URL patterns from various dev servers:
|
||||
// - Vite: "Local: http://localhost:5173/"
|
||||
// - Next.js: "ready - started server on 0.0.0.0:3000, url: http://localhost:3000"
|
||||
// - CRA/Webpack: "On Your Network: http://192.168.1.1:3000"
|
||||
// - Generic: Any http:// or https:// URL
|
||||
const urlPatterns = [
|
||||
/(?:Local|Network):\s+(https?:\/\/[^\s]+)/i, // Vite format
|
||||
/(?:ready|started server).*?(?:url:\s*)?(https?:\/\/[^\s,]+)/i, // Next.js format
|
||||
/(https?:\/\/(?:localhost|127\.0\.0\.1|\[::\]):\d+)/i, // Generic localhost URL
|
||||
/(https?:\/\/[^\s<>"{}|\\^`[\]]+)/i, // Any HTTP(S) URL
|
||||
];
|
||||
// Strip ANSI escape codes to prevent color codes from breaking regex matching
|
||||
const cleanContent = this.stripAnsi(content);
|
||||
|
||||
for (const pattern of urlPatterns) {
|
||||
const match = content.match(pattern);
|
||||
// Phase 1: Try to detect a full URL from output
|
||||
// Patterns are defined at module level (URL_PATTERNS) and reused across calls
|
||||
for (const { pattern, description } of URL_PATTERNS) {
|
||||
const match = cleanContent.match(pattern);
|
||||
if (match && match[1]) {
|
||||
const detectedUrl = match[1].trim();
|
||||
// Validate it looks like a reasonable URL
|
||||
let detectedUrl = match[1].trim();
|
||||
// Remove trailing punctuation that might have been captured
|
||||
detectedUrl = detectedUrl.replace(/[.,;:!?)\]}>]+$/, '');
|
||||
|
||||
if (detectedUrl.startsWith('http://') || detectedUrl.startsWith('https://')) {
|
||||
// Normalize 0.0.0.0 to localhost for browser accessibility
|
||||
detectedUrl = detectedUrl.replace(
|
||||
/\/\/0\.0\.0\.0(:\d+)?/,
|
||||
(_, port) => `//localhost${port || ''}`
|
||||
);
|
||||
// Normalize [::] to localhost for browser accessibility
|
||||
detectedUrl = detectedUrl.replace(
|
||||
/\/\/\[::\](:\d+)?/,
|
||||
(_, port) => `//localhost${port || ''}`
|
||||
);
|
||||
// Normalize [::1] (IPv6 loopback) to localhost for browser accessibility
|
||||
detectedUrl = detectedUrl.replace(
|
||||
/\/\/\[::1\](:\d+)?/,
|
||||
(_, port) => `//localhost${port || ''}`
|
||||
);
|
||||
|
||||
server.url = detectedUrl;
|
||||
server.urlDetected = true;
|
||||
logger.info(
|
||||
`Detected actual server URL: ${detectedUrl} (allocated port was ${server.port})`
|
||||
);
|
||||
|
||||
// Update the port to match the detected URL's actual port
|
||||
const detectedPort = this.extractPortFromUrl(detectedUrl);
|
||||
if (detectedPort && detectedPort !== server.port) {
|
||||
logger.info(
|
||||
`Port mismatch: allocated ${server.port}, detected ${detectedPort} from ${description}`
|
||||
);
|
||||
server.port = detectedPort;
|
||||
}
|
||||
|
||||
logger.info(`Detected server URL via ${description}: ${detectedUrl}`);
|
||||
|
||||
// Emit URL update event
|
||||
if (this.emitter) {
|
||||
this.emitter.emit('dev-server:url-detected', {
|
||||
worktreePath: server.worktreePath,
|
||||
url: detectedUrl,
|
||||
port: server.port,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
break;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: Try to detect just a port number from output (no full URL)
|
||||
// Some servers only print "listening on port 3000" without a full URL
|
||||
// Patterns are defined at module level (PORT_PATTERNS) and reused across calls
|
||||
for (const { pattern, description } of PORT_PATTERNS) {
|
||||
const match = cleanContent.match(pattern);
|
||||
if (match && match[1]) {
|
||||
const detectedPort = parseInt(match[1], 10);
|
||||
// Sanity check: port should be in a reasonable range
|
||||
if (detectedPort > 0 && detectedPort <= 65535) {
|
||||
const detectedUrl = `http://localhost:${detectedPort}`;
|
||||
server.url = detectedUrl;
|
||||
server.urlDetected = true;
|
||||
|
||||
if (detectedPort !== server.port) {
|
||||
logger.info(
|
||||
`Port mismatch: allocated ${server.port}, detected ${detectedPort} from ${description}`
|
||||
);
|
||||
server.port = detectedPort;
|
||||
}
|
||||
|
||||
logger.info(`Detected server port via ${description}: ${detectedPort} → ${detectedUrl}`);
|
||||
|
||||
// Emit URL update event
|
||||
if (this.emitter) {
|
||||
this.emitter.emit('dev-server:url-detected', {
|
||||
worktreePath: server.worktreePath,
|
||||
url: detectedUrl,
|
||||
port: server.port,
|
||||
timestamp: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -673,6 +835,7 @@ class DevServerService {
|
||||
worktreePath: string;
|
||||
port: number;
|
||||
url: string;
|
||||
urlDetected: boolean;
|
||||
}>;
|
||||
};
|
||||
} {
|
||||
@@ -680,6 +843,7 @@ class DevServerService {
|
||||
worktreePath: s.worktreePath,
|
||||
port: s.port,
|
||||
url: s.url,
|
||||
urlDetected: s.urlDetected,
|
||||
}));
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user