mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 08:13:37 +00:00
style: fix formatting with Prettier
🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,7 @@
|
||||
* Supports API key authentication via header or environment variable.
|
||||
*/
|
||||
|
||||
import type { Request, Response, NextFunction } from "express";
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
// API key from environment (optional - if not set, auth is disabled)
|
||||
const API_KEY = process.env.AUTOMAKER_API_KEY;
|
||||
@@ -23,12 +23,12 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
|
||||
}
|
||||
|
||||
// Check for API key in header
|
||||
const providedKey = req.headers["x-api-key"] as string | undefined;
|
||||
const providedKey = req.headers['x-api-key'] as string | undefined;
|
||||
|
||||
if (!providedKey) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: "Authentication required. Provide X-API-Key header.",
|
||||
error: 'Authentication required. Provide X-API-Key header.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export function authMiddleware(req: Request, res: Response, next: NextFunction):
|
||||
if (providedKey !== API_KEY) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: "Invalid API key.",
|
||||
error: 'Invalid API key.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -57,6 +57,6 @@ export function isAuthEnabled(): boolean {
|
||||
export function getAuthStatus(): { enabled: boolean; method: string } {
|
||||
return {
|
||||
enabled: !!API_KEY,
|
||||
method: API_KEY ? "api_key" : "none",
|
||||
method: API_KEY ? 'api_key' : 'none',
|
||||
};
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
InstallationStatus,
|
||||
ValidationResult,
|
||||
ModelDefinition,
|
||||
} from "./types.js";
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* Base provider class that all provider implementations must extend
|
||||
@@ -33,9 +33,7 @@ export abstract class BaseProvider {
|
||||
* @param options Execution options
|
||||
* @returns AsyncGenerator yielding provider messages
|
||||
*/
|
||||
abstract executeQuery(
|
||||
options: ExecuteOptions
|
||||
): AsyncGenerator<ProviderMessage>;
|
||||
abstract executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage>;
|
||||
|
||||
/**
|
||||
* Detect if the provider is installed and configured
|
||||
@@ -59,7 +57,7 @@ export abstract class BaseProvider {
|
||||
|
||||
// Base validation (can be overridden)
|
||||
if (!this.config) {
|
||||
errors.push("Provider config is missing");
|
||||
errors.push('Provider config is missing');
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -76,7 +74,7 @@ export abstract class BaseProvider {
|
||||
*/
|
||||
supportsFeature(feature: string): boolean {
|
||||
// Default implementation - override in subclasses
|
||||
const commonFeatures = ["tools", "text"];
|
||||
const commonFeatures = ['tools', 'text'];
|
||||
return commonFeatures.includes(feature);
|
||||
}
|
||||
|
||||
|
||||
@@ -5,26 +5,24 @@
|
||||
* with the provider architecture.
|
||||
*/
|
||||
|
||||
import { query, type Options } from "@anthropic-ai/claude-agent-sdk";
|
||||
import { BaseProvider } from "./base-provider.js";
|
||||
import { query, type Options } from '@anthropic-ai/claude-agent-sdk';
|
||||
import { BaseProvider } from './base-provider.js';
|
||||
import type {
|
||||
ExecuteOptions,
|
||||
ProviderMessage,
|
||||
InstallationStatus,
|
||||
ModelDefinition,
|
||||
} from "./types.js";
|
||||
} from './types.js';
|
||||
|
||||
export class ClaudeProvider extends BaseProvider {
|
||||
getName(): string {
|
||||
return "claude";
|
||||
return 'claude';
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a query using Claude Agent SDK
|
||||
*/
|
||||
async *executeQuery(
|
||||
options: ExecuteOptions
|
||||
): AsyncGenerator<ProviderMessage> {
|
||||
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
|
||||
const {
|
||||
prompt,
|
||||
model,
|
||||
@@ -38,16 +36,7 @@ export class ClaudeProvider extends BaseProvider {
|
||||
} = options;
|
||||
|
||||
// Build Claude SDK options
|
||||
const defaultTools = [
|
||||
"Read",
|
||||
"Write",
|
||||
"Edit",
|
||||
"Glob",
|
||||
"Grep",
|
||||
"Bash",
|
||||
"WebSearch",
|
||||
"WebFetch",
|
||||
];
|
||||
const defaultTools = ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'];
|
||||
const toolsToUse = allowedTools || defaultTools;
|
||||
|
||||
const sdkOptions: Options = {
|
||||
@@ -56,7 +45,7 @@ export class ClaudeProvider extends BaseProvider {
|
||||
maxTurns,
|
||||
cwd,
|
||||
allowedTools: toolsToUse,
|
||||
permissionMode: "acceptEdits",
|
||||
permissionMode: 'acceptEdits',
|
||||
sandbox: {
|
||||
enabled: true,
|
||||
autoAllowBashIfSandboxed: true,
|
||||
@@ -75,10 +64,10 @@ export class ClaudeProvider extends BaseProvider {
|
||||
// Multi-part prompt (with images)
|
||||
promptPayload = (async function* () {
|
||||
const multiPartPrompt = {
|
||||
type: "user" as const,
|
||||
session_id: "",
|
||||
type: 'user' as const,
|
||||
session_id: '',
|
||||
message: {
|
||||
role: "user" as const,
|
||||
role: 'user' as const,
|
||||
content: prompt,
|
||||
},
|
||||
parent_tool_use_id: null,
|
||||
@@ -99,10 +88,7 @@ export class ClaudeProvider extends BaseProvider {
|
||||
yield msg as ProviderMessage;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(
|
||||
"[ClaudeProvider] executeQuery() error during execution:",
|
||||
error
|
||||
);
|
||||
console.error('[ClaudeProvider] executeQuery() error during execution:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
@@ -116,7 +102,7 @@ export class ClaudeProvider extends BaseProvider {
|
||||
|
||||
const status: InstallationStatus = {
|
||||
installed: true,
|
||||
method: "sdk",
|
||||
method: 'sdk',
|
||||
hasApiKey,
|
||||
authenticated: hasApiKey,
|
||||
};
|
||||
@@ -130,53 +116,53 @@ export class ClaudeProvider extends BaseProvider {
|
||||
getAvailableModels(): ModelDefinition[] {
|
||||
const models = [
|
||||
{
|
||||
id: "claude-opus-4-5-20251101",
|
||||
name: "Claude Opus 4.5",
|
||||
modelString: "claude-opus-4-5-20251101",
|
||||
provider: "anthropic",
|
||||
description: "Most capable Claude model",
|
||||
id: 'claude-opus-4-5-20251101',
|
||||
name: 'Claude Opus 4.5',
|
||||
modelString: 'claude-opus-4-5-20251101',
|
||||
provider: 'anthropic',
|
||||
description: 'Most capable Claude model',
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 16000,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
tier: "premium" as const,
|
||||
tier: 'premium' as const,
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-20250514",
|
||||
name: "Claude Sonnet 4",
|
||||
modelString: "claude-sonnet-4-20250514",
|
||||
provider: "anthropic",
|
||||
description: "Balanced performance and cost",
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
name: 'Claude Sonnet 4',
|
||||
modelString: 'claude-sonnet-4-20250514',
|
||||
provider: 'anthropic',
|
||||
description: 'Balanced performance and cost',
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 16000,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
tier: "standard" as const,
|
||||
tier: 'standard' as const,
|
||||
},
|
||||
{
|
||||
id: "claude-3-5-sonnet-20241022",
|
||||
name: "Claude 3.5 Sonnet",
|
||||
modelString: "claude-3-5-sonnet-20241022",
|
||||
provider: "anthropic",
|
||||
description: "Fast and capable",
|
||||
id: 'claude-3-5-sonnet-20241022',
|
||||
name: 'Claude 3.5 Sonnet',
|
||||
modelString: 'claude-3-5-sonnet-20241022',
|
||||
provider: 'anthropic',
|
||||
description: 'Fast and capable',
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 8000,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
tier: "standard" as const,
|
||||
tier: 'standard' as const,
|
||||
},
|
||||
{
|
||||
id: "claude-3-5-haiku-20241022",
|
||||
name: "Claude 3.5 Haiku",
|
||||
modelString: "claude-3-5-haiku-20241022",
|
||||
provider: "anthropic",
|
||||
description: "Fastest Claude model",
|
||||
id: 'claude-3-5-haiku-20241022',
|
||||
name: 'Claude 3.5 Haiku',
|
||||
modelString: 'claude-3-5-haiku-20241022',
|
||||
provider: 'anthropic',
|
||||
description: 'Fastest Claude model',
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 8000,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
tier: "basic" as const,
|
||||
tier: 'basic' as const,
|
||||
},
|
||||
] satisfies ModelDefinition[];
|
||||
return models;
|
||||
@@ -186,7 +172,7 @@ export class ClaudeProvider extends BaseProvider {
|
||||
* Check if the provider supports a specific feature
|
||||
*/
|
||||
supportsFeature(feature: string): boolean {
|
||||
const supportedFeatures = ["tools", "text", "vision", "thinking"];
|
||||
const supportedFeatures = ['tools', 'text', 'vision', 'thinking'];
|
||||
return supportedFeatures.includes(feature);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
* new providers (Cursor, OpenCode, etc.) trivial - just add one line.
|
||||
*/
|
||||
|
||||
import { BaseProvider } from "./base-provider.js";
|
||||
import { ClaudeProvider } from "./claude-provider.js";
|
||||
import type { InstallationStatus } from "./types.js";
|
||||
import { BaseProvider } from './base-provider.js';
|
||||
import { ClaudeProvider } from './claude-provider.js';
|
||||
import type { InstallationStatus } from './types.js';
|
||||
|
||||
export class ProviderFactory {
|
||||
/**
|
||||
@@ -21,10 +21,7 @@ export class ProviderFactory {
|
||||
const lowerModel = modelId.toLowerCase();
|
||||
|
||||
// Claude models (claude-*, opus, sonnet, haiku)
|
||||
if (
|
||||
lowerModel.startsWith("claude-") ||
|
||||
["haiku", "sonnet", "opus"].includes(lowerModel)
|
||||
) {
|
||||
if (lowerModel.startsWith('claude-') || ['haiku', 'sonnet', 'opus'].includes(lowerModel)) {
|
||||
return new ClaudeProvider();
|
||||
}
|
||||
|
||||
@@ -37,9 +34,7 @@ export class ProviderFactory {
|
||||
// }
|
||||
|
||||
// Default to Claude for unknown models
|
||||
console.warn(
|
||||
`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`
|
||||
);
|
||||
console.warn(`[ProviderFactory] Unknown model prefix for "${modelId}", defaulting to Claude`);
|
||||
return new ClaudeProvider();
|
||||
}
|
||||
|
||||
@@ -58,9 +53,7 @@ export class ProviderFactory {
|
||||
*
|
||||
* @returns Map of provider name to installation status
|
||||
*/
|
||||
static async checkAllProviders(): Promise<
|
||||
Record<string, InstallationStatus>
|
||||
> {
|
||||
static async checkAllProviders(): Promise<Record<string, InstallationStatus>> {
|
||||
const providers = this.getAllProviders();
|
||||
const statuses: Record<string, InstallationStatus> = {};
|
||||
|
||||
@@ -83,8 +76,8 @@ export class ProviderFactory {
|
||||
const lowerName = name.toLowerCase();
|
||||
|
||||
switch (lowerName) {
|
||||
case "claude":
|
||||
case "anthropic":
|
||||
case 'claude':
|
||||
case 'anthropic':
|
||||
return new ClaudeProvider();
|
||||
|
||||
// Future providers:
|
||||
|
||||
@@ -15,7 +15,7 @@ export interface ProviderConfig {
|
||||
* Message in conversation history
|
||||
*/
|
||||
export interface ConversationMessage {
|
||||
role: "user" | "assistant";
|
||||
role: 'user' | 'assistant';
|
||||
content: string | Array<{ type: string; text?: string; source?: object }>;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ export interface ExecuteOptions {
|
||||
* Content block in a provider message (matches Claude SDK format)
|
||||
*/
|
||||
export interface ContentBlock {
|
||||
type: "text" | "tool_use" | "thinking" | "tool_result";
|
||||
type: 'text' | 'tool_use' | 'thinking' | 'tool_result';
|
||||
text?: string;
|
||||
thinking?: string;
|
||||
name?: string;
|
||||
@@ -52,11 +52,11 @@ export interface ContentBlock {
|
||||
* Message returned by a provider (matches Claude SDK streaming format)
|
||||
*/
|
||||
export interface ProviderMessage {
|
||||
type: "assistant" | "user" | "error" | "result";
|
||||
subtype?: "success" | "error";
|
||||
type: 'assistant' | 'user' | 'error' | 'result';
|
||||
subtype?: 'success' | 'error';
|
||||
session_id?: string;
|
||||
message?: {
|
||||
role: "user" | "assistant";
|
||||
role: 'user' | 'assistant';
|
||||
content: ContentBlock[];
|
||||
};
|
||||
result?: string;
|
||||
@@ -71,7 +71,7 @@ export interface InstallationStatus {
|
||||
installed: boolean;
|
||||
path?: string;
|
||||
version?: string;
|
||||
method?: "cli" | "npm" | "brew" | "sdk";
|
||||
method?: 'cli' | 'npm' | 'brew' | 'sdk';
|
||||
hasApiKey?: boolean;
|
||||
authenticated?: boolean;
|
||||
error?: string;
|
||||
@@ -99,6 +99,6 @@ export interface ModelDefinition {
|
||||
maxOutputTokens?: number;
|
||||
supportsVision?: boolean;
|
||||
supportsTools?: boolean;
|
||||
tier?: "basic" | "standard" | "premium";
|
||||
tier?: 'basic' | 'standard' | 'premium';
|
||||
default?: boolean;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /clear endpoint - Clear conversation
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createClearHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -12,16 +12,14 @@ export function createClearHandler(agentService: AgentService) {
|
||||
const { sessionId } = req.body as { sessionId: string };
|
||||
|
||||
if (!sessionId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "sessionId is required" });
|
||||
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await agentService.clearSession(sessionId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError(error, "Clear session failed");
|
||||
logError(error, 'Clear session failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /history endpoint - Get conversation history
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createHistoryHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -12,16 +12,14 @@ export function createHistoryHandler(agentService: AgentService) {
|
||||
const { sessionId } = req.body as { sessionId: string };
|
||||
|
||||
if (!sessionId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "sessionId is required" });
|
||||
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = agentService.getHistory(sessionId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError(error, "Get history failed");
|
||||
logError(error, 'Get history failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /model endpoint - Set session model
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createModelHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,16 +15,14 @@ export function createModelHandler(agentService: AgentService) {
|
||||
};
|
||||
|
||||
if (!sessionId || !model) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "sessionId and model are required" });
|
||||
res.status(400).json({ success: false, error: 'sessionId and model are required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await agentService.setSessionModel(sessionId, model);
|
||||
res.json({ success: result });
|
||||
} catch (error) {
|
||||
logError(error, "Set session model failed");
|
||||
logError(error, 'Set session model failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /stop endpoint - Stop execution
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createStopHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -12,16 +12,14 @@ export function createStopHandler(agentService: AgentService) {
|
||||
const { sessionId } = req.body as { sessionId: string };
|
||||
|
||||
if (!sessionId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "sessionId is required" });
|
||||
res.status(400).json({ success: false, error: 'sessionId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const result = await agentService.stopExecution(sessionId);
|
||||
res.json(result);
|
||||
} catch (error) {
|
||||
logError(error, "Stop execution failed");
|
||||
logError(error, 'Stop execution failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,25 +2,22 @@
|
||||
* Spec Regeneration routes - HTTP API for AI-powered spec generation
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import type { EventEmitter } from "../../lib/events.js";
|
||||
import { createCreateHandler } from "./routes/create.js";
|
||||
import { createGenerateHandler } from "./routes/generate.js";
|
||||
import { createGenerateFeaturesHandler } from "./routes/generate-features.js";
|
||||
import { createStopHandler } from "./routes/stop.js";
|
||||
import { createStatusHandler } from "./routes/status.js";
|
||||
import { Router } from 'express';
|
||||
import type { EventEmitter } from '../../lib/events.js';
|
||||
import { createCreateHandler } from './routes/create.js';
|
||||
import { createGenerateHandler } from './routes/generate.js';
|
||||
import { createGenerateFeaturesHandler } from './routes/generate-features.js';
|
||||
import { createStopHandler } from './routes/stop.js';
|
||||
import { createStatusHandler } from './routes/status.js';
|
||||
|
||||
export function createSpecRegenerationRoutes(events: EventEmitter): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post("/create", createCreateHandler(events));
|
||||
router.post("/generate", createGenerateHandler(events));
|
||||
router.post("/generate-features", createGenerateFeaturesHandler(events));
|
||||
router.post("/stop", createStopHandler());
|
||||
router.get("/status", createStatusHandler());
|
||||
router.post('/create', createCreateHandler(events));
|
||||
router.post('/generate', createGenerateHandler(events));
|
||||
router.post('/generate-features', createGenerateFeaturesHandler(events));
|
||||
router.post('/stop', createStopHandler());
|
||||
router.get('/status', createStatusHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* GET /status endpoint - Get generation status
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getSpecRegenerationStatus, getErrorMessage } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getSpecRegenerationStatus, getErrorMessage } from '../common.js';
|
||||
|
||||
export function createStatusHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
|
||||
@@ -2,12 +2,8 @@
|
||||
* POST /stop endpoint - Stop generation
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import {
|
||||
getSpecRegenerationStatus,
|
||||
setRunningState,
|
||||
getErrorMessage,
|
||||
} from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getSpecRegenerationStatus, setRunningState, getErrorMessage } from '../common.js';
|
||||
|
||||
export function createStopHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /commit-feature endpoint - Commit feature changes
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createCommitFeatureHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -16,23 +16,17 @@ export function createCommitFeatureHandler(autoModeService: AutoModeService) {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath and featureId are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const commitHash = await autoModeService.commitFeature(
|
||||
projectPath,
|
||||
featureId,
|
||||
worktreePath
|
||||
);
|
||||
const commitHash = await autoModeService.commitFeature(projectPath, featureId, worktreePath);
|
||||
res.json({ success: true, commitHash });
|
||||
} catch (error) {
|
||||
logError(error, "Commit feature failed");
|
||||
logError(error, 'Commit feature failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /context-exists endpoint - Check if context exists for a feature
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createContextExistsHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,22 +15,17 @@ export function createContextExistsHandler(autoModeService: AutoModeService) {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath and featureId are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const exists = await autoModeService.contextExists(
|
||||
projectPath,
|
||||
featureId
|
||||
);
|
||||
const exists = await autoModeService.contextExists(projectPath, featureId);
|
||||
res.json({ success: true, exists });
|
||||
} catch (error) {
|
||||
logError(error, "Check context exists failed");
|
||||
logError(error, 'Check context exists failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /status endpoint - Get auto mode status
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createStatusHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,7 +15,7 @@ export function createStatusHandler(autoModeService: AutoModeService) {
|
||||
...status,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get status failed");
|
||||
logError(error, 'Get status failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /stop-feature endpoint - Stop a specific feature
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createStopFeatureHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -12,16 +12,14 @@ export function createStopFeatureHandler(autoModeService: AutoModeService) {
|
||||
const { featureId } = req.body as { featureId: string };
|
||||
|
||||
if (!featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "featureId is required" });
|
||||
res.status(400).json({ success: false, error: 'featureId is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const stopped = await autoModeService.stopFeature(featureId);
|
||||
res.json({ success: true, stopped });
|
||||
} catch (error) {
|
||||
logError(error, "Stop feature failed");
|
||||
logError(error, 'Stop feature failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /verify-feature endpoint - Verify a feature
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,22 +15,17 @@ export function createVerifyFeatureHandler(autoModeService: AutoModeService) {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath and featureId are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const passes = await autoModeService.verifyFeature(
|
||||
projectPath,
|
||||
featureId
|
||||
);
|
||||
const passes = await autoModeService.verifyFeature(projectPath, featureId);
|
||||
res.json({ success: true, passes });
|
||||
} catch (error) {
|
||||
logError(error, "Verify feature failed");
|
||||
logError(error, 'Verify feature failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* with different enhancement modes (improve, expand, simplify, etc.)
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { createEnhanceHandler } from "./routes/enhance.js";
|
||||
import { Router } from 'express';
|
||||
import { createEnhanceHandler } from './routes/enhance.js';
|
||||
|
||||
/**
|
||||
* Create the enhance-prompt router
|
||||
@@ -16,7 +16,7 @@ import { createEnhanceHandler } from "./routes/enhance.js";
|
||||
export function createEnhancePromptRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post("/", createEnhanceHandler());
|
||||
router.post('/', createEnhanceHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /agent-output endpoint - Get agent output for a feature
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { FeatureLoader } from "../../../services/feature-loader.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createAgentOutputHandler(featureLoader: FeatureLoader) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,22 +15,17 @@ export function createAgentOutputHandler(featureLoader: FeatureLoader) {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath and featureId are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const content = await featureLoader.getAgentOutput(
|
||||
projectPath,
|
||||
featureId
|
||||
);
|
||||
const content = await featureLoader.getAgentOutput(projectPath, featureId);
|
||||
res.json({ success: true, content });
|
||||
} catch (error) {
|
||||
logError(error, "Get agent output failed");
|
||||
logError(error, 'Get agent output failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /delete endpoint - Delete a feature
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { FeatureLoader } from "../../../services/feature-loader.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createDeleteHandler(featureLoader: FeatureLoader) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,19 +15,17 @@ export function createDeleteHandler(featureLoader: FeatureLoader) {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath and featureId are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const success = await featureLoader.delete(projectPath, featureId);
|
||||
res.json({ success });
|
||||
} catch (error) {
|
||||
logError(error, "Delete feature failed");
|
||||
logError(error, 'Delete feature failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /get endpoint - Get a single feature
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { FeatureLoader } from "../../../services/feature-loader.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { FeatureLoader } from '../../../services/feature-loader.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createGetHandler(featureLoader: FeatureLoader) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,24 +15,22 @@ export function createGetHandler(featureLoader: FeatureLoader) {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId are required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath and featureId are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const feature = await featureLoader.get(projectPath, featureId);
|
||||
if (!feature) {
|
||||
res.status(404).json({ success: false, error: "Feature not found" });
|
||||
res.status(404).json({ success: false, error: 'Feature not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, feature });
|
||||
} catch (error) {
|
||||
logError(error, "Get feature failed");
|
||||
logError(error, 'Get feature failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,40 +3,40 @@
|
||||
* Provides REST API equivalents for Electron IPC file operations
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import type { EventEmitter } from "../../lib/events.js";
|
||||
import { createReadHandler } from "./routes/read.js";
|
||||
import { createWriteHandler } from "./routes/write.js";
|
||||
import { createMkdirHandler } from "./routes/mkdir.js";
|
||||
import { createReaddirHandler } from "./routes/readdir.js";
|
||||
import { createExistsHandler } from "./routes/exists.js";
|
||||
import { createStatHandler } from "./routes/stat.js";
|
||||
import { createDeleteHandler } from "./routes/delete.js";
|
||||
import { createValidatePathHandler } from "./routes/validate-path.js";
|
||||
import { createResolveDirectoryHandler } from "./routes/resolve-directory.js";
|
||||
import { createSaveImageHandler } from "./routes/save-image.js";
|
||||
import { createBrowseHandler } from "./routes/browse.js";
|
||||
import { createImageHandler } from "./routes/image.js";
|
||||
import { createSaveBoardBackgroundHandler } from "./routes/save-board-background.js";
|
||||
import { createDeleteBoardBackgroundHandler } from "./routes/delete-board-background.js";
|
||||
import { Router } from 'express';
|
||||
import type { EventEmitter } from '../../lib/events.js';
|
||||
import { createReadHandler } from './routes/read.js';
|
||||
import { createWriteHandler } from './routes/write.js';
|
||||
import { createMkdirHandler } from './routes/mkdir.js';
|
||||
import { createReaddirHandler } from './routes/readdir.js';
|
||||
import { createExistsHandler } from './routes/exists.js';
|
||||
import { createStatHandler } from './routes/stat.js';
|
||||
import { createDeleteHandler } from './routes/delete.js';
|
||||
import { createValidatePathHandler } from './routes/validate-path.js';
|
||||
import { createResolveDirectoryHandler } from './routes/resolve-directory.js';
|
||||
import { createSaveImageHandler } from './routes/save-image.js';
|
||||
import { createBrowseHandler } from './routes/browse.js';
|
||||
import { createImageHandler } from './routes/image.js';
|
||||
import { createSaveBoardBackgroundHandler } from './routes/save-board-background.js';
|
||||
import { createDeleteBoardBackgroundHandler } from './routes/delete-board-background.js';
|
||||
|
||||
export function createFsRoutes(_events: EventEmitter): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post("/read", createReadHandler());
|
||||
router.post("/write", createWriteHandler());
|
||||
router.post("/mkdir", createMkdirHandler());
|
||||
router.post("/readdir", createReaddirHandler());
|
||||
router.post("/exists", createExistsHandler());
|
||||
router.post("/stat", createStatHandler());
|
||||
router.post("/delete", createDeleteHandler());
|
||||
router.post("/validate-path", createValidatePathHandler());
|
||||
router.post("/resolve-directory", createResolveDirectoryHandler());
|
||||
router.post("/save-image", createSaveImageHandler());
|
||||
router.post("/browse", createBrowseHandler());
|
||||
router.get("/image", createImageHandler());
|
||||
router.post("/save-board-background", createSaveBoardBackgroundHandler());
|
||||
router.post("/delete-board-background", createDeleteBoardBackgroundHandler());
|
||||
router.post('/read', createReadHandler());
|
||||
router.post('/write', createWriteHandler());
|
||||
router.post('/mkdir', createMkdirHandler());
|
||||
router.post('/readdir', createReaddirHandler());
|
||||
router.post('/exists', createExistsHandler());
|
||||
router.post('/stat', createStatHandler());
|
||||
router.post('/delete', createDeleteHandler());
|
||||
router.post('/validate-path', createValidatePathHandler());
|
||||
router.post('/resolve-directory', createResolveDirectoryHandler());
|
||||
router.post('/save-image', createSaveImageHandler());
|
||||
router.post('/browse', createBrowseHandler());
|
||||
router.get('/image', createImageHandler());
|
||||
router.post('/save-board-background', createSaveBoardBackgroundHandler());
|
||||
router.post('/delete-board-background', createDeleteBoardBackgroundHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /diffs endpoint - Get diffs for the main project
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { getGitRepositoryDiffs } from "../../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { getGitRepositoryDiffs } from '../../common.js';
|
||||
|
||||
export function createDiffsHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -12,7 +12,7 @@ export function createDiffsHandler() {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath required" });
|
||||
res.status(400).json({ success: false, error: 'projectPath required' });
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -25,11 +25,11 @@ export function createDiffsHandler() {
|
||||
hasChanges: result.hasChanges,
|
||||
});
|
||||
} catch (innerError) {
|
||||
logError(innerError, "Git diff failed");
|
||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
||||
logError(innerError, 'Git diff failed');
|
||||
res.json({ success: true, diff: '', files: [], hasChanges: false });
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Get diffs failed");
|
||||
logError(error, 'Get diffs failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* POST /file-diff endpoint - Get diff for a specific file
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import { generateSyntheticDiffForNewFile } from "../../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
import { generateSyntheticDiffForNewFile } from '../../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -19,20 +19,17 @@ export function createFileDiffHandler() {
|
||||
};
|
||||
|
||||
if (!projectPath || !filePath) {
|
||||
res
|
||||
.status(400)
|
||||
.json({ success: false, error: "projectPath and filePath required" });
|
||||
res.status(400).json({ success: false, error: 'projectPath and filePath required' });
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// First check if the file is untracked
|
||||
const { stdout: status } = await execAsync(
|
||||
`git status --porcelain -- "${filePath}"`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
const { stdout: status } = await execAsync(`git status --porcelain -- "${filePath}"`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
const isUntracked = status.trim().startsWith("??");
|
||||
const isUntracked = status.trim().startsWith('??');
|
||||
|
||||
let diff: string;
|
||||
if (isUntracked) {
|
||||
@@ -40,23 +37,20 @@ export function createFileDiffHandler() {
|
||||
diff = await generateSyntheticDiffForNewFile(projectPath, filePath);
|
||||
} else {
|
||||
// Use regular git diff for tracked files
|
||||
const result = await execAsync(
|
||||
`git diff HEAD -- "${filePath}"`,
|
||||
{
|
||||
cwd: projectPath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
}
|
||||
);
|
||||
const result = await execAsync(`git diff HEAD -- "${filePath}"`, {
|
||||
cwd: projectPath,
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
});
|
||||
diff = result.stdout;
|
||||
}
|
||||
|
||||
res.json({ success: true, diff, filePath });
|
||||
} catch (innerError) {
|
||||
logError(innerError, "Git file diff failed");
|
||||
res.json({ success: true, diff: "", filePath });
|
||||
logError(innerError, 'Git file diff failed');
|
||||
res.json({ success: true, diff: '', filePath });
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Get file diff failed");
|
||||
logError(error, 'Get file diff failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
* Health check routes
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { createIndexHandler } from "./routes/index.js";
|
||||
import { createDetailedHandler } from "./routes/detailed.js";
|
||||
import { Router } from 'express';
|
||||
import { createIndexHandler } from './routes/index.js';
|
||||
import { createDetailedHandler } from './routes/detailed.js';
|
||||
|
||||
export function createHealthRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/", createIndexHandler());
|
||||
router.get("/detailed", createDetailedHandler());
|
||||
router.get('/', createIndexHandler());
|
||||
router.get('/detailed', createDetailedHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
* GET /detailed endpoint - Detailed health check
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getAuthStatus } from "../../../lib/auth.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getAuthStatus } from '../../../lib/auth.js';
|
||||
|
||||
export function createDetailedHandler() {
|
||||
return (_req: Request, res: Response): void => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || "0.1.0",
|
||||
version: process.env.npm_package_version || '0.1.0',
|
||||
uptime: process.uptime(),
|
||||
memory: process.memoryUsage(),
|
||||
dataDir: process.env.DATA_DIR || "./data",
|
||||
dataDir: process.env.DATA_DIR || './data',
|
||||
auth: getAuthStatus(),
|
||||
env: {
|
||||
nodeVersion: process.version,
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* GET / endpoint - Basic health check
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { Request, Response } from 'express';
|
||||
|
||||
export function createIndexHandler() {
|
||||
return (_req: Request, res: Response): void => {
|
||||
res.json({
|
||||
status: "ok",
|
||||
status: 'ok',
|
||||
timestamp: new Date().toISOString(),
|
||||
version: process.env.npm_package_version || "0.1.0",
|
||||
version: process.env.npm_package_version || '0.1.0',
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2,15 +2,15 @@
|
||||
* Models routes - HTTP API for model providers and availability
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { createAvailableHandler } from "./routes/available.js";
|
||||
import { createProvidersHandler } from "./routes/providers.js";
|
||||
import { Router } from 'express';
|
||||
import { createAvailableHandler } from './routes/available.js';
|
||||
import { createProvidersHandler } from './routes/providers.js';
|
||||
|
||||
export function createModelsRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/available", createAvailableHandler());
|
||||
router.get("/providers", createProvidersHandler());
|
||||
router.get('/available', createAvailableHandler());
|
||||
router.get('/providers', createProvidersHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* GET /available endpoint - Get available models
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
interface ModelDefinition {
|
||||
id: string;
|
||||
@@ -20,36 +20,36 @@ export function createAvailableHandler() {
|
||||
try {
|
||||
const models: ModelDefinition[] = [
|
||||
{
|
||||
id: "claude-opus-4-5-20251101",
|
||||
name: "Claude Opus 4.5",
|
||||
provider: "anthropic",
|
||||
id: 'claude-opus-4-5-20251101',
|
||||
name: 'Claude Opus 4.5',
|
||||
provider: 'anthropic',
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "claude-sonnet-4-20250514",
|
||||
name: "Claude Sonnet 4",
|
||||
provider: "anthropic",
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
name: 'Claude Sonnet 4',
|
||||
provider: 'anthropic',
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "claude-3-5-sonnet-20241022",
|
||||
name: "Claude 3.5 Sonnet",
|
||||
provider: "anthropic",
|
||||
id: 'claude-3-5-sonnet-20241022',
|
||||
name: 'Claude 3.5 Sonnet',
|
||||
provider: 'anthropic',
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 8192,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "claude-3-5-haiku-20241022",
|
||||
name: "Claude 3.5 Haiku",
|
||||
provider: "anthropic",
|
||||
id: 'claude-3-5-haiku-20241022',
|
||||
name: 'Claude 3.5 Haiku',
|
||||
provider: 'anthropic',
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 8192,
|
||||
supportsVision: true,
|
||||
@@ -59,7 +59,7 @@ export function createAvailableHandler() {
|
||||
|
||||
res.json({ success: true, models });
|
||||
} catch (error) {
|
||||
logError(error, "Get available models failed");
|
||||
logError(error, 'Get available models failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,16 +2,14 @@
|
||||
* Running Agents routes - HTTP API for tracking active agent executions
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import type { AutoModeService } from "../../services/auto-mode-service.js";
|
||||
import { createIndexHandler } from "./routes/index.js";
|
||||
import { Router } from 'express';
|
||||
import type { AutoModeService } from '../../services/auto-mode-service.js';
|
||||
import { createIndexHandler } from './routes/index.js';
|
||||
|
||||
export function createRunningAgentsRoutes(
|
||||
autoModeService: AutoModeService
|
||||
): Router {
|
||||
export function createRunningAgentsRoutes(autoModeService: AutoModeService): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/", createIndexHandler(autoModeService));
|
||||
router.get('/', createIndexHandler(autoModeService));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* GET / endpoint - Get all running agents
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { AutoModeService } from "../../../services/auto-mode-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import type { AutoModeService } from '../../../services/auto-mode-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createIndexHandler(autoModeService: AutoModeService) {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
@@ -18,7 +18,7 @@ export function createIndexHandler(autoModeService: AutoModeService) {
|
||||
totalCount: runningAgents.length,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get running agents failed");
|
||||
logError(error, 'Get running agents failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,24 +2,24 @@
|
||||
* Sessions routes - HTTP API for session management
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { AgentService } from "../../services/agent-service.js";
|
||||
import { createIndexHandler } from "./routes/index.js";
|
||||
import { createCreateHandler } from "./routes/create.js";
|
||||
import { createUpdateHandler } from "./routes/update.js";
|
||||
import { createArchiveHandler } from "./routes/archive.js";
|
||||
import { createUnarchiveHandler } from "./routes/unarchive.js";
|
||||
import { createDeleteHandler } from "./routes/delete.js";
|
||||
import { Router } from 'express';
|
||||
import { AgentService } from '../../services/agent-service.js';
|
||||
import { createIndexHandler } from './routes/index.js';
|
||||
import { createCreateHandler } from './routes/create.js';
|
||||
import { createUpdateHandler } from './routes/update.js';
|
||||
import { createArchiveHandler } from './routes/archive.js';
|
||||
import { createUnarchiveHandler } from './routes/unarchive.js';
|
||||
import { createDeleteHandler } from './routes/delete.js';
|
||||
|
||||
export function createSessionsRoutes(agentService: AgentService): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/", createIndexHandler(agentService));
|
||||
router.post("/", createCreateHandler(agentService));
|
||||
router.put("/:sessionId", createUpdateHandler(agentService));
|
||||
router.post("/:sessionId/archive", createArchiveHandler(agentService));
|
||||
router.post("/:sessionId/unarchive", createUnarchiveHandler(agentService));
|
||||
router.delete("/:sessionId", createDeleteHandler(agentService));
|
||||
router.get('/', createIndexHandler(agentService));
|
||||
router.post('/', createCreateHandler(agentService));
|
||||
router.put('/:sessionId', createUpdateHandler(agentService));
|
||||
router.post('/:sessionId/archive', createArchiveHandler(agentService));
|
||||
router.post('/:sessionId/unarchive', createUnarchiveHandler(agentService));
|
||||
router.delete('/:sessionId', createDeleteHandler(agentService));
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /:sessionId/archive endpoint - Archive a session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createArchiveHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -13,13 +13,13 @@ export function createArchiveHandler(agentService: AgentService) {
|
||||
const success = await agentService.archiveSession(sessionId);
|
||||
|
||||
if (!success) {
|
||||
res.status(404).json({ success: false, error: "Session not found" });
|
||||
res.status(404).json({ success: false, error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Archive session failed");
|
||||
logError(error, 'Archive session failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST / endpoint - Create a new session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createCreateHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -17,19 +17,14 @@ export function createCreateHandler(agentService: AgentService) {
|
||||
};
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({ success: false, error: "name is required" });
|
||||
res.status(400).json({ success: false, error: 'name is required' });
|
||||
return;
|
||||
}
|
||||
|
||||
const session = await agentService.createSession(
|
||||
name,
|
||||
projectPath,
|
||||
workingDirectory,
|
||||
model
|
||||
);
|
||||
const session = await agentService.createSession(name, projectPath, workingDirectory, model);
|
||||
res.json({ success: true, session });
|
||||
} catch (error) {
|
||||
logError(error, "Create session failed");
|
||||
logError(error, 'Create session failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* DELETE /:sessionId endpoint - Delete a session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createDeleteHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -13,13 +13,13 @@ export function createDeleteHandler(agentService: AgentService) {
|
||||
const success = await agentService.deleteSession(sessionId);
|
||||
|
||||
if (!success) {
|
||||
res.status(404).json({ success: false, error: "Session not found" });
|
||||
res.status(404).json({ success: false, error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Delete session failed");
|
||||
logError(error, 'Delete session failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,14 +2,14 @@
|
||||
* GET / endpoint - List all sessions
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createIndexHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const includeArchived = req.query.includeArchived === "true";
|
||||
const includeArchived = req.query.includeArchived === 'true';
|
||||
const sessionsRaw = await agentService.listSessions(includeArchived);
|
||||
|
||||
// Transform to match frontend SessionListItem interface
|
||||
@@ -17,7 +17,7 @@ export function createIndexHandler(agentService: AgentService) {
|
||||
sessionsRaw.map(async (s) => {
|
||||
const messages = await agentService.loadSession(s.id);
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
const preview = lastMessage?.content?.slice(0, 100) || "";
|
||||
const preview = lastMessage?.content?.slice(0, 100) || '';
|
||||
|
||||
return {
|
||||
id: s.id,
|
||||
@@ -36,7 +36,7 @@ export function createIndexHandler(agentService: AgentService) {
|
||||
|
||||
res.json({ success: true, sessions });
|
||||
} catch (error) {
|
||||
logError(error, "List sessions failed");
|
||||
logError(error, 'List sessions failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* POST /:sessionId/unarchive endpoint - Unarchive a session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createUnarchiveHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -13,13 +13,13 @@ export function createUnarchiveHandler(agentService: AgentService) {
|
||||
const success = await agentService.unarchiveSession(sessionId);
|
||||
|
||||
if (!success) {
|
||||
res.status(404).json({ success: false, error: "Session not found" });
|
||||
res.status(404).json({ success: false, error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Unarchive session failed");
|
||||
logError(error, 'Unarchive session failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* PUT /:sessionId endpoint - Update a session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { AgentService } from "../../../services/agent-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { AgentService } from '../../../services/agent-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createUpdateHandler(agentService: AgentService) {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -22,13 +22,13 @@ export function createUpdateHandler(agentService: AgentService) {
|
||||
model,
|
||||
});
|
||||
if (!session) {
|
||||
res.status(404).json({ success: false, error: "Session not found" });
|
||||
res.status(404).json({ success: false, error: 'Session not found' });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, session });
|
||||
} catch (error) {
|
||||
logError(error, "Update session failed");
|
||||
logError(error, 'Update session failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,36 +2,36 @@
|
||||
* Business logic for getting Claude CLI status
|
||||
*/
|
||||
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { getApiKey } from "./common.js";
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { getApiKey } from './common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
export async function getClaudeStatus() {
|
||||
let installed = false;
|
||||
let version = "";
|
||||
let cliPath = "";
|
||||
let method = "none";
|
||||
let version = '';
|
||||
let cliPath = '';
|
||||
let method = 'none';
|
||||
|
||||
const isWindows = process.platform === "win32";
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Try to find Claude CLI using platform-specific command
|
||||
try {
|
||||
// Use 'where' on Windows, 'which' on Unix-like systems
|
||||
const findCommand = isWindows ? "where claude" : "which claude";
|
||||
const findCommand = isWindows ? 'where claude' : 'which claude';
|
||||
const { stdout } = await execAsync(findCommand);
|
||||
// 'where' on Windows can return multiple paths - take the first one
|
||||
cliPath = stdout.trim().split(/\r?\n/)[0];
|
||||
installed = true;
|
||||
method = "path";
|
||||
method = 'path';
|
||||
|
||||
// Get version
|
||||
try {
|
||||
const { stdout: versionOut } = await execAsync("claude --version");
|
||||
const { stdout: versionOut } = await execAsync('claude --version');
|
||||
version = versionOut.trim();
|
||||
} catch {
|
||||
// Version command might not be available
|
||||
@@ -40,22 +40,22 @@ export async function getClaudeStatus() {
|
||||
// Not in PATH, try common locations based on platform
|
||||
const commonPaths = isWindows
|
||||
? (() => {
|
||||
const appData = process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming");
|
||||
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
||||
return [
|
||||
// Windows-specific paths
|
||||
path.join(os.homedir(), ".local", "bin", "claude.exe"),
|
||||
path.join(appData, "npm", "claude.cmd"),
|
||||
path.join(appData, "npm", "claude"),
|
||||
path.join(appData, ".npm-global", "bin", "claude.cmd"),
|
||||
path.join(appData, ".npm-global", "bin", "claude"),
|
||||
path.join(os.homedir(), '.local', 'bin', 'claude.exe'),
|
||||
path.join(appData, 'npm', 'claude.cmd'),
|
||||
path.join(appData, 'npm', 'claude'),
|
||||
path.join(appData, '.npm-global', 'bin', 'claude.cmd'),
|
||||
path.join(appData, '.npm-global', 'bin', 'claude'),
|
||||
];
|
||||
})()
|
||||
: [
|
||||
// Unix (Linux/macOS) paths
|
||||
path.join(os.homedir(), ".local", "bin", "claude"),
|
||||
path.join(os.homedir(), ".claude", "local", "claude"),
|
||||
"/usr/local/bin/claude",
|
||||
path.join(os.homedir(), ".npm-global", "bin", "claude"),
|
||||
path.join(os.homedir(), '.local', 'bin', 'claude'),
|
||||
path.join(os.homedir(), '.claude', 'local', 'claude'),
|
||||
'/usr/local/bin/claude',
|
||||
path.join(os.homedir(), '.npm-global', 'bin', 'claude'),
|
||||
];
|
||||
|
||||
for (const p of commonPaths) {
|
||||
@@ -63,7 +63,7 @@ export async function getClaudeStatus() {
|
||||
await fs.access(p);
|
||||
cliPath = p;
|
||||
installed = true;
|
||||
method = "local";
|
||||
method = 'local';
|
||||
|
||||
// Get version from this path
|
||||
try {
|
||||
@@ -84,11 +84,11 @@ export async function getClaudeStatus() {
|
||||
// apiKeys.anthropic stores direct API keys for pay-per-use
|
||||
let auth = {
|
||||
authenticated: false,
|
||||
method: "none" as string,
|
||||
method: 'none' as string,
|
||||
hasCredentialsFile: false,
|
||||
hasToken: false,
|
||||
hasStoredOAuthToken: !!getApiKey("anthropic_oauth_token"),
|
||||
hasStoredApiKey: !!getApiKey("anthropic"),
|
||||
hasStoredOAuthToken: !!getApiKey('anthropic_oauth_token'),
|
||||
hasStoredApiKey: !!getApiKey('anthropic'),
|
||||
hasEnvApiKey: !!process.env.ANTHROPIC_API_KEY,
|
||||
// Additional fields for detailed status
|
||||
oauthTokenValid: false,
|
||||
@@ -97,13 +97,13 @@ export async function getClaudeStatus() {
|
||||
hasRecentActivity: false,
|
||||
};
|
||||
|
||||
const claudeDir = path.join(os.homedir(), ".claude");
|
||||
const claudeDir = path.join(os.homedir(), '.claude');
|
||||
|
||||
// Check for recent Claude CLI activity - indicates working authentication
|
||||
// The stats-cache.json file is only populated when the CLI is working properly
|
||||
const statsCachePath = path.join(claudeDir, "stats-cache.json");
|
||||
const statsCachePath = path.join(claudeDir, 'stats-cache.json');
|
||||
try {
|
||||
const statsContent = await fs.readFile(statsCachePath, "utf-8");
|
||||
const statsContent = await fs.readFile(statsCachePath, 'utf-8');
|
||||
const stats = JSON.parse(statsContent);
|
||||
|
||||
// Check if there's any activity (which means the CLI is authenticated and working)
|
||||
@@ -111,26 +111,26 @@ export async function getClaudeStatus() {
|
||||
auth.hasRecentActivity = true;
|
||||
auth.hasCliAuth = true;
|
||||
auth.authenticated = true;
|
||||
auth.method = "cli_authenticated";
|
||||
auth.method = 'cli_authenticated';
|
||||
}
|
||||
} catch {
|
||||
// Stats file doesn't exist or is invalid
|
||||
}
|
||||
|
||||
// Check for settings.json - indicates CLI has been set up
|
||||
const settingsPath = path.join(claudeDir, "settings.json");
|
||||
const settingsPath = path.join(claudeDir, 'settings.json');
|
||||
try {
|
||||
await fs.access(settingsPath);
|
||||
// If settings exist but no activity, CLI might be set up but not authenticated
|
||||
if (!auth.hasCliAuth) {
|
||||
// Try to check for other indicators of auth
|
||||
const sessionsDir = path.join(claudeDir, "projects");
|
||||
const sessionsDir = path.join(claudeDir, 'projects');
|
||||
try {
|
||||
const sessions = await fs.readdir(sessionsDir);
|
||||
if (sessions.length > 0) {
|
||||
auth.hasCliAuth = true;
|
||||
auth.authenticated = true;
|
||||
auth.method = "cli_authenticated";
|
||||
auth.method = 'cli_authenticated';
|
||||
}
|
||||
} catch {
|
||||
// Sessions directory doesn't exist
|
||||
@@ -143,13 +143,13 @@ export async function getClaudeStatus() {
|
||||
// Check for credentials file (OAuth tokens from claude login)
|
||||
// Note: Claude CLI may use ".credentials.json" (hidden) or "credentials.json" depending on version/platform
|
||||
const credentialsPaths = [
|
||||
path.join(claudeDir, ".credentials.json"),
|
||||
path.join(claudeDir, "credentials.json"),
|
||||
path.join(claudeDir, '.credentials.json'),
|
||||
path.join(claudeDir, 'credentials.json'),
|
||||
];
|
||||
|
||||
for (const credentialsPath of credentialsPaths) {
|
||||
try {
|
||||
const credentialsContent = await fs.readFile(credentialsPath, "utf-8");
|
||||
const credentialsContent = await fs.readFile(credentialsPath, 'utf-8');
|
||||
const credentials = JSON.parse(credentialsContent);
|
||||
auth.hasCredentialsFile = true;
|
||||
|
||||
@@ -158,11 +158,11 @@ export async function getClaudeStatus() {
|
||||
auth.hasStoredOAuthToken = true;
|
||||
auth.oauthTokenValid = true;
|
||||
auth.authenticated = true;
|
||||
auth.method = "oauth_token"; // Stored OAuth token from credentials file
|
||||
auth.method = 'oauth_token'; // Stored OAuth token from credentials file
|
||||
} else if (credentials.api_key) {
|
||||
auth.apiKeyValid = true;
|
||||
auth.authenticated = true;
|
||||
auth.method = "api_key"; // Stored API key in credentials file
|
||||
auth.method = 'api_key'; // Stored API key in credentials file
|
||||
}
|
||||
break; // Found and processed credentials file
|
||||
} catch {
|
||||
@@ -174,25 +174,25 @@ export async function getClaudeStatus() {
|
||||
if (auth.hasEnvApiKey) {
|
||||
auth.authenticated = true;
|
||||
auth.apiKeyValid = true;
|
||||
auth.method = "api_key_env"; // API key from ANTHROPIC_API_KEY env var
|
||||
auth.method = 'api_key_env'; // API key from ANTHROPIC_API_KEY env var
|
||||
}
|
||||
|
||||
// In-memory stored OAuth token (from setup wizard - subscription auth)
|
||||
if (!auth.authenticated && getApiKey("anthropic_oauth_token")) {
|
||||
if (!auth.authenticated && getApiKey('anthropic_oauth_token')) {
|
||||
auth.authenticated = true;
|
||||
auth.oauthTokenValid = true;
|
||||
auth.method = "oauth_token"; // Stored OAuth token from setup wizard
|
||||
auth.method = 'oauth_token'; // Stored OAuth token from setup wizard
|
||||
}
|
||||
|
||||
// In-memory stored API key (from settings UI - pay-per-use)
|
||||
if (!auth.authenticated && getApiKey("anthropic")) {
|
||||
if (!auth.authenticated && getApiKey('anthropic')) {
|
||||
auth.authenticated = true;
|
||||
auth.apiKeyValid = true;
|
||||
auth.method = "api_key"; // Manually stored API key
|
||||
auth.method = 'api_key'; // Manually stored API key
|
||||
}
|
||||
|
||||
return {
|
||||
status: installed ? "installed" : "not_installed",
|
||||
status: installed ? 'installed' : 'not_installed',
|
||||
installed,
|
||||
method,
|
||||
version,
|
||||
|
||||
@@ -2,29 +2,29 @@
|
||||
* Setup routes - HTTP API for CLI detection, API keys, and platform info
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { createClaudeStatusHandler } from "./routes/claude-status.js";
|
||||
import { createInstallClaudeHandler } from "./routes/install-claude.js";
|
||||
import { createAuthClaudeHandler } from "./routes/auth-claude.js";
|
||||
import { createStoreApiKeyHandler } from "./routes/store-api-key.js";
|
||||
import { createDeleteApiKeyHandler } from "./routes/delete-api-key.js";
|
||||
import { createApiKeysHandler } from "./routes/api-keys.js";
|
||||
import { createPlatformHandler } from "./routes/platform.js";
|
||||
import { createVerifyClaudeAuthHandler } from "./routes/verify-claude-auth.js";
|
||||
import { createGhStatusHandler } from "./routes/gh-status.js";
|
||||
import { Router } from 'express';
|
||||
import { createClaudeStatusHandler } from './routes/claude-status.js';
|
||||
import { createInstallClaudeHandler } from './routes/install-claude.js';
|
||||
import { createAuthClaudeHandler } from './routes/auth-claude.js';
|
||||
import { createStoreApiKeyHandler } from './routes/store-api-key.js';
|
||||
import { createDeleteApiKeyHandler } from './routes/delete-api-key.js';
|
||||
import { createApiKeysHandler } from './routes/api-keys.js';
|
||||
import { createPlatformHandler } from './routes/platform.js';
|
||||
import { createVerifyClaudeAuthHandler } from './routes/verify-claude-auth.js';
|
||||
import { createGhStatusHandler } from './routes/gh-status.js';
|
||||
|
||||
export function createSetupRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/claude-status", createClaudeStatusHandler());
|
||||
router.post("/install-claude", createInstallClaudeHandler());
|
||||
router.post("/auth-claude", createAuthClaudeHandler());
|
||||
router.post("/store-api-key", createStoreApiKeyHandler());
|
||||
router.post("/delete-api-key", createDeleteApiKeyHandler());
|
||||
router.get("/api-keys", createApiKeysHandler());
|
||||
router.get("/platform", createPlatformHandler());
|
||||
router.post("/verify-claude-auth", createVerifyClaudeAuthHandler());
|
||||
router.get("/gh-status", createGhStatusHandler());
|
||||
router.get('/claude-status', createClaudeStatusHandler());
|
||||
router.post('/install-claude', createInstallClaudeHandler());
|
||||
router.post('/auth-claude', createAuthClaudeHandler());
|
||||
router.post('/store-api-key', createStoreApiKeyHandler());
|
||||
router.post('/delete-api-key', createDeleteApiKeyHandler());
|
||||
router.get('/api-keys', createApiKeysHandler());
|
||||
router.get('/platform', createPlatformHandler());
|
||||
router.post('/verify-claude-auth', createVerifyClaudeAuthHandler());
|
||||
router.get('/gh-status', createGhStatusHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* POST /auth-claude endpoint - Auth Claude
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createAuthClaudeHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
@@ -11,11 +11,11 @@ export function createAuthClaudeHandler() {
|
||||
res.json({
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
command: "claude login",
|
||||
command: 'claude login',
|
||||
message: "Please run 'claude login' in your terminal to authenticate",
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Auth Claude failed");
|
||||
logError(error, 'Auth Claude failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* GET /claude-status endpoint - Get Claude CLI status
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getClaudeStatus } from "../get-claude-status.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getClaudeStatus } from '../get-claude-status.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createClaudeStatusHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,7 +15,7 @@ export function createClaudeStatusHandler() {
|
||||
...status,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get Claude status failed");
|
||||
logError(error, 'Get Claude status failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,24 +2,26 @@
|
||||
* GET /gh-status endpoint - Get GitHub CLI status
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import os from "os";
|
||||
import path from "path";
|
||||
import fs from "fs/promises";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import os from 'os';
|
||||
import path from 'path';
|
||||
import fs from 'fs/promises';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
// Extended PATH to include common tool installation locations
|
||||
const extendedPath = [
|
||||
process.env.PATH,
|
||||
"/opt/homebrew/bin",
|
||||
"/usr/local/bin",
|
||||
"/home/linuxbrew/.linuxbrew/bin",
|
||||
'/opt/homebrew/bin',
|
||||
'/usr/local/bin',
|
||||
'/home/linuxbrew/.linuxbrew/bin',
|
||||
`${process.env.HOME}/.local/bin`,
|
||||
].filter(Boolean).join(":");
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(':');
|
||||
|
||||
const execEnv = {
|
||||
...process.env,
|
||||
@@ -44,11 +46,11 @@ async function getGhStatus(): Promise<GhStatus> {
|
||||
user: null,
|
||||
};
|
||||
|
||||
const isWindows = process.platform === "win32";
|
||||
const isWindows = process.platform === 'win32';
|
||||
|
||||
// Check if gh CLI is installed
|
||||
try {
|
||||
const findCommand = isWindows ? "where gh" : "command -v gh";
|
||||
const findCommand = isWindows ? 'where gh' : 'command -v gh';
|
||||
const { stdout } = await execAsync(findCommand, { env: execEnv });
|
||||
status.path = stdout.trim().split(/\r?\n/)[0];
|
||||
status.installed = true;
|
||||
@@ -56,14 +58,14 @@ async function getGhStatus(): Promise<GhStatus> {
|
||||
// gh not in PATH, try common locations
|
||||
const commonPaths = isWindows
|
||||
? [
|
||||
path.join(process.env.LOCALAPPDATA || "", "Programs", "gh", "bin", "gh.exe"),
|
||||
path.join(process.env.ProgramFiles || "", "GitHub CLI", "gh.exe"),
|
||||
path.join(process.env.LOCALAPPDATA || '', 'Programs', 'gh', 'bin', 'gh.exe'),
|
||||
path.join(process.env.ProgramFiles || '', 'GitHub CLI', 'gh.exe'),
|
||||
]
|
||||
: [
|
||||
"/opt/homebrew/bin/gh",
|
||||
"/usr/local/bin/gh",
|
||||
path.join(os.homedir(), ".local", "bin", "gh"),
|
||||
"/home/linuxbrew/.linuxbrew/bin/gh",
|
||||
'/opt/homebrew/bin/gh',
|
||||
'/usr/local/bin/gh',
|
||||
path.join(os.homedir(), '.local', 'bin', 'gh'),
|
||||
'/home/linuxbrew/.linuxbrew/bin/gh',
|
||||
];
|
||||
|
||||
for (const p of commonPaths) {
|
||||
@@ -84,30 +86,31 @@ async function getGhStatus(): Promise<GhStatus> {
|
||||
|
||||
// Get version
|
||||
try {
|
||||
const { stdout } = await execAsync("gh --version", { env: execEnv });
|
||||
const { stdout } = await execAsync('gh --version', { env: execEnv });
|
||||
// Extract version from output like "gh version 2.40.1 (2024-01-09)"
|
||||
const versionMatch = stdout.match(/gh version ([\d.]+)/);
|
||||
status.version = versionMatch ? versionMatch[1] : stdout.trim().split("\n")[0];
|
||||
status.version = versionMatch ? versionMatch[1] : stdout.trim().split('\n')[0];
|
||||
} catch {
|
||||
// Version command failed
|
||||
}
|
||||
|
||||
// Check authentication status
|
||||
try {
|
||||
const { stdout } = await execAsync("gh auth status", { env: execEnv });
|
||||
const { stdout } = await execAsync('gh auth status', { env: execEnv });
|
||||
// If this succeeds without error, we're authenticated
|
||||
status.authenticated = true;
|
||||
|
||||
// Try to extract username from output
|
||||
const userMatch = stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) ||
|
||||
stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i);
|
||||
const userMatch =
|
||||
stdout.match(/Logged in to [^\s]+ account ([^\s]+)/i) ||
|
||||
stdout.match(/Logged in to [^\s]+ as ([^\s]+)/i);
|
||||
if (userMatch) {
|
||||
status.user = userMatch[1];
|
||||
}
|
||||
} catch (error: unknown) {
|
||||
// Auth status returns non-zero if not authenticated
|
||||
const err = error as { stderr?: string };
|
||||
if (err.stderr?.includes("not logged in")) {
|
||||
if (err.stderr?.includes('not logged in')) {
|
||||
status.authenticated = false;
|
||||
}
|
||||
}
|
||||
@@ -124,7 +127,7 @@ export function createGhStatusHandler() {
|
||||
...status,
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get GitHub CLI status failed");
|
||||
logError(error, 'Get GitHub CLI status failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* POST /install-claude endpoint - Install Claude CLI
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createInstallClaudeHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
@@ -13,10 +13,10 @@ export function createInstallClaudeHandler() {
|
||||
res.json({
|
||||
success: false,
|
||||
error:
|
||||
"CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code",
|
||||
'CLI installation requires terminal access. Please install manually using: npm install -g @anthropic-ai/claude-code',
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Install Claude CLI failed");
|
||||
logError(error, 'Install Claude CLI failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,9 +2,9 @@
|
||||
* GET /platform endpoint - Get platform info
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import os from "os";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import os from 'os';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createPlatformHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
@@ -15,12 +15,12 @@ export function createPlatformHandler() {
|
||||
platform,
|
||||
arch: os.arch(),
|
||||
homeDir: os.homedir(),
|
||||
isWindows: platform === "win32",
|
||||
isMac: platform === "darwin",
|
||||
isLinux: platform === "linux",
|
||||
isWindows: platform === 'win32',
|
||||
isMac: platform === 'darwin',
|
||||
isLinux: platform === 'linux',
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get platform info failed");
|
||||
logError(error, 'Get platform info failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* GET /status endpoint - Get status
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getSuggestionsStatus, getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getSuggestionsStatus, getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createStatusHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
@@ -11,7 +11,7 @@ export function createStatusHandler() {
|
||||
const { isRunning } = getSuggestionsStatus();
|
||||
res.json({ success: true, isRunning });
|
||||
} catch (error) {
|
||||
logError(error, "Get status failed");
|
||||
logError(error, 'Get status failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,13 +2,8 @@
|
||||
* POST /stop endpoint - Stop suggestions generation
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import {
|
||||
getSuggestionsStatus,
|
||||
setRunningState,
|
||||
getErrorMessage,
|
||||
logError,
|
||||
} from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getSuggestionsStatus, setRunningState, getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createStopHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
@@ -20,7 +15,7 @@ export function createStopHandler() {
|
||||
setRunningState(false, null);
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
logError(error, "Stop suggestions failed");
|
||||
logError(error, 'Stop suggestions failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,13 +3,13 @@
|
||||
* Provides API for cloning GitHub starter templates
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { createCloneHandler } from "./routes/clone.js";
|
||||
import { Router } from 'express';
|
||||
import { createCloneHandler } from './routes/clone.js';
|
||||
|
||||
export function createTemplatesRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.post("/clone", createCloneHandler());
|
||||
router.post('/clone', createCloneHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -5,26 +5,20 @@
|
||||
* WebSocket connections for real-time I/O are handled separately in index.ts.
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { Router } from 'express';
|
||||
import {
|
||||
terminalAuthMiddleware,
|
||||
validateTerminalToken,
|
||||
isTerminalEnabled,
|
||||
isTerminalPasswordRequired,
|
||||
} from "./common.js";
|
||||
import { createStatusHandler } from "./routes/status.js";
|
||||
import { createAuthHandler } from "./routes/auth.js";
|
||||
import { createLogoutHandler } from "./routes/logout.js";
|
||||
import {
|
||||
createSessionsListHandler,
|
||||
createSessionsCreateHandler,
|
||||
} from "./routes/sessions.js";
|
||||
import { createSessionDeleteHandler } from "./routes/session-delete.js";
|
||||
import { createSessionResizeHandler } from "./routes/session-resize.js";
|
||||
import {
|
||||
createSettingsGetHandler,
|
||||
createSettingsUpdateHandler,
|
||||
} from "./routes/settings.js";
|
||||
} from './common.js';
|
||||
import { createStatusHandler } from './routes/status.js';
|
||||
import { createAuthHandler } from './routes/auth.js';
|
||||
import { createLogoutHandler } from './routes/logout.js';
|
||||
import { createSessionsListHandler, createSessionsCreateHandler } from './routes/sessions.js';
|
||||
import { createSessionDeleteHandler } from './routes/session-delete.js';
|
||||
import { createSessionResizeHandler } from './routes/session-resize.js';
|
||||
import { createSettingsGetHandler, createSettingsUpdateHandler } from './routes/settings.js';
|
||||
|
||||
// Re-export for use in main index.ts
|
||||
export { validateTerminalToken, isTerminalEnabled, isTerminalPasswordRequired };
|
||||
@@ -32,19 +26,19 @@ export { validateTerminalToken, isTerminalEnabled, isTerminalPasswordRequired };
|
||||
export function createTerminalRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/status", createStatusHandler());
|
||||
router.post("/auth", createAuthHandler());
|
||||
router.post("/logout", createLogoutHandler());
|
||||
router.get('/status', createStatusHandler());
|
||||
router.post('/auth', createAuthHandler());
|
||||
router.post('/logout', createLogoutHandler());
|
||||
|
||||
// Apply terminal auth middleware to all routes below
|
||||
router.use(terminalAuthMiddleware);
|
||||
|
||||
router.get("/sessions", createSessionsListHandler());
|
||||
router.post("/sessions", createSessionsCreateHandler());
|
||||
router.delete("/sessions/:id", createSessionDeleteHandler());
|
||||
router.post("/sessions/:id/resize", createSessionResizeHandler());
|
||||
router.get("/settings", createSettingsGetHandler());
|
||||
router.put("/settings", createSettingsUpdateHandler());
|
||||
router.get('/sessions', createSessionsListHandler());
|
||||
router.post('/sessions', createSessionsCreateHandler());
|
||||
router.delete('/sessions/:id', createSessionDeleteHandler());
|
||||
router.post('/sessions/:id/resize', createSessionResizeHandler());
|
||||
router.get('/settings', createSettingsGetHandler());
|
||||
router.put('/settings', createSettingsUpdateHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* POST /auth endpoint - Authenticate with password to get a session token
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { Request, Response } from 'express';
|
||||
import {
|
||||
getTerminalEnabledConfigValue,
|
||||
getTerminalPasswordConfig,
|
||||
@@ -10,14 +10,14 @@ import {
|
||||
addToken,
|
||||
getTokenExpiryMs,
|
||||
getErrorMessage,
|
||||
} from "../common.js";
|
||||
} from '../common.js';
|
||||
|
||||
export function createAuthHandler() {
|
||||
return (req: Request, res: Response): void => {
|
||||
if (!getTerminalEnabledConfigValue()) {
|
||||
res.status(403).json({
|
||||
success: false,
|
||||
error: "Terminal access is disabled",
|
||||
error: 'Terminal access is disabled',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -41,7 +41,7 @@ export function createAuthHandler() {
|
||||
if (!password || password !== terminalPassword) {
|
||||
res.status(401).json({
|
||||
success: false,
|
||||
error: "Invalid password",
|
||||
error: 'Invalid password',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,12 +2,12 @@
|
||||
* POST /logout endpoint - Invalidate a session token
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { deleteToken } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { deleteToken } from '../common.js';
|
||||
|
||||
export function createLogoutHandler() {
|
||||
return (req: Request, res: Response): void => {
|
||||
const token = (req.headers["x-terminal-token"] as string) || req.body.token;
|
||||
const token = (req.headers['x-terminal-token'] as string) || req.body.token;
|
||||
|
||||
if (token) {
|
||||
deleteToken(token);
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* DELETE /sessions/:id endpoint - Kill a terminal session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getTerminalService } from "../../../services/terminal-service.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getTerminalService } from '../../../services/terminal-service.js';
|
||||
|
||||
export function createSessionDeleteHandler() {
|
||||
return (req: Request, res: Response): void => {
|
||||
@@ -14,7 +14,7 @@ export function createSessionDeleteHandler() {
|
||||
if (!killed) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: "Session not found",
|
||||
error: 'Session not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
* POST /sessions/:id/resize endpoint - Resize a terminal session
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getTerminalService } from "../../../services/terminal-service.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getTerminalService } from '../../../services/terminal-service.js';
|
||||
|
||||
export function createSessionResizeHandler() {
|
||||
return (req: Request, res: Response): void => {
|
||||
@@ -14,7 +14,7 @@ export function createSessionResizeHandler() {
|
||||
if (!cols || !rows) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "cols and rows are required",
|
||||
error: 'cols and rows are required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -24,7 +24,7 @@ export function createSessionResizeHandler() {
|
||||
if (!resized) {
|
||||
res.status(404).json({
|
||||
success: false,
|
||||
error: "Session not found",
|
||||
error: 'Session not found',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,9 +2,13 @@
|
||||
* GET/PUT /settings endpoint - Get/Update terminal settings
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getTerminalService, MIN_MAX_SESSIONS, MAX_MAX_SESSIONS } from "../../../services/terminal-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import {
|
||||
getTerminalService,
|
||||
MIN_MAX_SESSIONS,
|
||||
MAX_MAX_SESSIONS,
|
||||
} from '../../../services/terminal-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createSettingsGetHandler() {
|
||||
return (_req: Request, res: Response): void => {
|
||||
@@ -18,10 +22,10 @@ export function createSettingsGetHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get terminal settings failed");
|
||||
logError(error, 'Get terminal settings failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to get terminal settings",
|
||||
error: 'Failed to get terminal settings',
|
||||
details: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
@@ -36,17 +40,17 @@ export function createSettingsUpdateHandler() {
|
||||
|
||||
// Validate maxSessions if provided
|
||||
if (maxSessions !== undefined) {
|
||||
if (typeof maxSessions !== "number") {
|
||||
if (typeof maxSessions !== 'number') {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "maxSessions must be a number",
|
||||
error: 'maxSessions must be a number',
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (!Number.isInteger(maxSessions)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "maxSessions must be an integer",
|
||||
error: 'maxSessions must be an integer',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -68,10 +72,10 @@ export function createSettingsUpdateHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Update terminal settings failed");
|
||||
logError(error, 'Update terminal settings failed');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: "Failed to update terminal settings",
|
||||
error: 'Failed to update terminal settings',
|
||||
details: getErrorMessage(error),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,12 +2,9 @@
|
||||
* GET /status endpoint - Get terminal status
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getTerminalService } from "../../../services/terminal-service.js";
|
||||
import {
|
||||
getTerminalEnabledConfigValue,
|
||||
isTerminalPasswordRequired,
|
||||
} from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getTerminalService } from '../../../services/terminal-service.js';
|
||||
import { getTerminalEnabledConfigValue, isTerminalPasswordRequired } from '../common.js';
|
||||
|
||||
export function createStatusHandler() {
|
||||
return (_req: Request, res: Response): void => {
|
||||
|
||||
@@ -3,15 +3,15 @@
|
||||
* Provides API endpoints for workspace directory management
|
||||
*/
|
||||
|
||||
import { Router } from "express";
|
||||
import { createConfigHandler } from "./routes/config.js";
|
||||
import { createDirectoriesHandler } from "./routes/directories.js";
|
||||
import { Router } from 'express';
|
||||
import { createConfigHandler } from './routes/config.js';
|
||||
import { createDirectoriesHandler } from './routes/directories.js';
|
||||
|
||||
export function createWorkspaceRoutes(): Router {
|
||||
const router = Router();
|
||||
|
||||
router.get("/config", createConfigHandler());
|
||||
router.get("/directories", createDirectoriesHandler());
|
||||
router.get('/config', createConfigHandler());
|
||||
router.get('/directories', createDirectoriesHandler());
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* POST /checkout-branch endpoint - Create and checkout a new branch
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -20,7 +20,7 @@ export function createCheckoutBranchHandler() {
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath required",
|
||||
error: 'worktreePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -28,7 +28,7 @@ export function createCheckoutBranchHandler() {
|
||||
if (!branchName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "branchName required",
|
||||
error: 'branchName required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -38,16 +38,15 @@ export function createCheckoutBranchHandler() {
|
||||
if (invalidChars.test(branchName)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "Branch name contains invalid characters",
|
||||
error: 'Branch name contains invalid characters',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current branch for reference
|
||||
const { stdout: currentBranchOutput } = await execAsync(
|
||||
"git rev-parse --abbrev-ref HEAD",
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const currentBranch = currentBranchOutput.trim();
|
||||
|
||||
// Check if branch already exists
|
||||
@@ -79,7 +78,7 @@ export function createCheckoutBranchHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Checkout branch failed");
|
||||
logError(error, 'Checkout branch failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* POST /commit endpoint - Commit changes in a worktree
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -20,13 +20,13 @@ export function createCommitHandler() {
|
||||
if (!worktreePath || !message) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath and message required",
|
||||
error: 'worktreePath and message required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for uncommitted changes
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
const { stdout: status } = await execAsync('git status --porcelain', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
|
||||
@@ -35,14 +35,14 @@ export function createCommitHandler() {
|
||||
success: true,
|
||||
result: {
|
||||
committed: false,
|
||||
message: "No changes to commit",
|
||||
message: 'No changes to commit',
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Stage all changes
|
||||
await execAsync("git add -A", { cwd: worktreePath });
|
||||
await execAsync('git add -A', { cwd: worktreePath });
|
||||
|
||||
// Create commit
|
||||
await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
|
||||
@@ -50,16 +50,15 @@ export function createCommitHandler() {
|
||||
});
|
||||
|
||||
// Get commit hash
|
||||
const { stdout: hashOutput } = await execAsync("git rev-parse HEAD", {
|
||||
const { stdout: hashOutput } = await execAsync('git rev-parse HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const commitHash = hashOutput.trim().substring(0, 8);
|
||||
|
||||
// Get branch name
|
||||
const { stdout: branchOutput } = await execAsync(
|
||||
"git rev-parse --abbrev-ref HEAD",
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const branchName = branchOutput.trim();
|
||||
|
||||
res.json({
|
||||
@@ -72,7 +71,7 @@ export function createCommitHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Commit worktree failed");
|
||||
logError(error, 'Commit worktree failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* POST /create-pr endpoint - Commit changes and create a pull request from a worktree
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { Request, Response } from 'express';
|
||||
import {
|
||||
getErrorMessage,
|
||||
logError,
|
||||
@@ -10,26 +10,27 @@ import {
|
||||
execEnv,
|
||||
isValidBranchName,
|
||||
isGhCliAvailable,
|
||||
} from "../common.js";
|
||||
import { updateWorktreePRInfo } from "../../../lib/worktree-metadata.js";
|
||||
} from '../common.js';
|
||||
import { updateWorktreePRInfo } from '../../../lib/worktree-metadata.js';
|
||||
|
||||
export function createCreatePRHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
try {
|
||||
const { worktreePath, projectPath, commitMessage, prTitle, prBody, baseBranch, draft } = req.body as {
|
||||
worktreePath: string;
|
||||
projectPath?: string;
|
||||
commitMessage?: string;
|
||||
prTitle?: string;
|
||||
prBody?: string;
|
||||
baseBranch?: string;
|
||||
draft?: boolean;
|
||||
};
|
||||
const { worktreePath, projectPath, commitMessage, prTitle, prBody, baseBranch, draft } =
|
||||
req.body as {
|
||||
worktreePath: string;
|
||||
projectPath?: string;
|
||||
commitMessage?: string;
|
||||
prTitle?: string;
|
||||
prBody?: string;
|
||||
baseBranch?: string;
|
||||
draft?: boolean;
|
||||
};
|
||||
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath required",
|
||||
error: 'worktreePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -39,23 +40,23 @@ export function createCreatePRHandler() {
|
||||
const effectiveProjectPath = projectPath || worktreePath;
|
||||
|
||||
// Get current branch name
|
||||
const { stdout: branchOutput } = await execAsync(
|
||||
"git rev-parse --abbrev-ref HEAD",
|
||||
{ cwd: worktreePath, env: execEnv }
|
||||
);
|
||||
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
const branchName = branchOutput.trim();
|
||||
|
||||
// Validate branch name for security
|
||||
if (!isValidBranchName(branchName)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "Invalid branch name contains unsafe characters",
|
||||
error: 'Invalid branch name contains unsafe characters',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for uncommitted changes
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
const { stdout: status } = await execAsync('git status --porcelain', {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
@@ -67,7 +68,7 @@ export function createCreatePRHandler() {
|
||||
const message = commitMessage || `Changes from ${branchName}`;
|
||||
|
||||
// Stage all changes
|
||||
await execAsync("git add -A", { cwd: worktreePath, env: execEnv });
|
||||
await execAsync('git add -A', { cwd: worktreePath, env: execEnv });
|
||||
|
||||
// Create commit
|
||||
await execAsync(`git commit -m "${message.replace(/"/g, '\\"')}"`, {
|
||||
@@ -76,7 +77,7 @@ export function createCreatePRHandler() {
|
||||
});
|
||||
|
||||
// Get commit hash
|
||||
const { stdout: hashOutput } = await execAsync("git rev-parse HEAD", {
|
||||
const { stdout: hashOutput } = await execAsync('git rev-parse HEAD', {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
@@ -100,8 +101,8 @@ export function createCreatePRHandler() {
|
||||
} catch (error2: unknown) {
|
||||
// Capture push error for reporting
|
||||
const err = error2 as { stderr?: string; message?: string };
|
||||
pushError = err.stderr || err.message || "Push failed";
|
||||
console.error("[CreatePR] Push failed:", pushError);
|
||||
pushError = err.stderr || err.message || 'Push failed';
|
||||
console.error('[CreatePR] Push failed:', pushError);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -115,10 +116,10 @@ export function createCreatePRHandler() {
|
||||
}
|
||||
|
||||
// Create PR using gh CLI or provide browser fallback
|
||||
const base = baseBranch || "main";
|
||||
const base = baseBranch || 'main';
|
||||
const title = prTitle || branchName;
|
||||
const body = prBody || `Changes from branch ${branchName}`;
|
||||
const draftFlag = draft ? "--draft" : "";
|
||||
const draftFlag = draft ? '--draft' : '';
|
||||
|
||||
let prUrl: string | null = null;
|
||||
let prError: string | null = null;
|
||||
@@ -131,7 +132,7 @@ export function createCreatePRHandler() {
|
||||
let upstreamRepo: string | null = null;
|
||||
let originOwner: string | null = null;
|
||||
try {
|
||||
const { stdout: remotes } = await execAsync("git remote -v", {
|
||||
const { stdout: remotes } = await execAsync('git remote -v', {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
@@ -150,15 +151,17 @@ export function createCreatePRHandler() {
|
||||
}
|
||||
if (!match) {
|
||||
// Try HTTPS format: https://github.com/owner/repo.git
|
||||
match = line.match(/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/);
|
||||
match = line.match(
|
||||
/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/
|
||||
);
|
||||
}
|
||||
|
||||
if (match) {
|
||||
const [, remoteName, owner, repo] = match;
|
||||
if (remoteName === "upstream") {
|
||||
if (remoteName === 'upstream') {
|
||||
upstreamRepo = `${owner}/${repo}`;
|
||||
repoUrl = `https://github.com/${owner}/${repo}`;
|
||||
} else if (remoteName === "origin") {
|
||||
} else if (remoteName === 'origin') {
|
||||
originOwner = owner;
|
||||
if (!repoUrl) {
|
||||
repoUrl = `https://github.com/${owner}/${repo}`;
|
||||
@@ -173,7 +176,7 @@ export function createCreatePRHandler() {
|
||||
// Fallback: Try to get repo URL from git config if remote parsing failed
|
||||
if (!repoUrl) {
|
||||
try {
|
||||
const { stdout: originUrl } = await execAsync("git config --get remote.origin.url", {
|
||||
const { stdout: originUrl } = await execAsync('git config --get remote.origin.url', {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
@@ -217,9 +220,11 @@ export function createCreatePRHandler() {
|
||||
// This is more reliable than gh pr view as it explicitly searches by branch name
|
||||
// For forks, we need to use owner:branch format for the head parameter
|
||||
const headRef = upstreamRepo && originOwner ? `${originOwner}:${branchName}` : branchName;
|
||||
const repoArg = upstreamRepo ? ` --repo "${upstreamRepo}"` : "";
|
||||
const repoArg = upstreamRepo ? ` --repo "${upstreamRepo}"` : '';
|
||||
|
||||
console.log(`[CreatePR] Checking for existing PR for branch: ${branchName} (headRef: ${headRef})`);
|
||||
console.log(
|
||||
`[CreatePR] Checking for existing PR for branch: ${branchName} (headRef: ${headRef})`
|
||||
);
|
||||
try {
|
||||
const listCmd = `gh pr list${repoArg} --head "${headRef}" --json number,title,url,state --limit 1`;
|
||||
console.log(`[CreatePR] Running: ${listCmd}`);
|
||||
@@ -234,7 +239,9 @@ export function createCreatePRHandler() {
|
||||
if (Array.isArray(existingPrs) && existingPrs.length > 0) {
|
||||
const existingPr = existingPrs[0];
|
||||
// PR already exists - use it and store metadata
|
||||
console.log(`[CreatePR] PR already exists for branch ${branchName}: PR #${existingPr.number}`);
|
||||
console.log(
|
||||
`[CreatePR] PR already exists for branch ${branchName}: PR #${existingPr.number}`
|
||||
);
|
||||
prUrl = existingPr.url;
|
||||
prNumber = existingPr.number;
|
||||
prAlreadyExisted = true;
|
||||
@@ -244,10 +251,12 @@ export function createCreatePRHandler() {
|
||||
number: existingPr.number,
|
||||
url: existingPr.url,
|
||||
title: existingPr.title || title,
|
||||
state: existingPr.state || "open",
|
||||
state: existingPr.state || 'open',
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
console.log(`[CreatePR] Stored existing PR info for branch ${branchName}: PR #${existingPr.number}`);
|
||||
console.log(
|
||||
`[CreatePR] Stored existing PR info for branch ${branchName}: PR #${existingPr.number}`
|
||||
);
|
||||
} else {
|
||||
console.log(`[CreatePR] No existing PR found for branch ${branchName}`);
|
||||
}
|
||||
@@ -293,23 +302,25 @@ export function createCreatePRHandler() {
|
||||
number: prNumber,
|
||||
url: prUrl,
|
||||
title,
|
||||
state: draft ? "draft" : "open",
|
||||
state: draft ? 'draft' : 'open',
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
console.log(`[CreatePR] Stored PR info for branch ${branchName}: PR #${prNumber}`);
|
||||
console.log(
|
||||
`[CreatePR] Stored PR info for branch ${branchName}: PR #${prNumber}`
|
||||
);
|
||||
} catch (metadataError) {
|
||||
console.error("[CreatePR] Failed to store PR metadata:", metadataError);
|
||||
console.error('[CreatePR] Failed to store PR metadata:', metadataError);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ghError: unknown) {
|
||||
// gh CLI failed - check if it's "already exists" error and try to fetch the PR
|
||||
const err = ghError as { stderr?: string; message?: string };
|
||||
const errorMessage = err.stderr || err.message || "PR creation failed";
|
||||
const errorMessage = err.stderr || err.message || 'PR creation failed';
|
||||
console.log(`[CreatePR] gh pr create failed: ${errorMessage}`);
|
||||
|
||||
// If error indicates PR already exists, try to fetch it
|
||||
if (errorMessage.toLowerCase().includes("already exists")) {
|
||||
if (errorMessage.toLowerCase().includes('already exists')) {
|
||||
console.log(`[CreatePR] PR already exists error - trying to fetch existing PR`);
|
||||
try {
|
||||
const { stdout: viewOutput } = await execAsync(
|
||||
@@ -326,13 +337,13 @@ export function createCreatePRHandler() {
|
||||
number: existingPr.number,
|
||||
url: existingPr.url,
|
||||
title: existingPr.title || title,
|
||||
state: existingPr.state || "open",
|
||||
state: existingPr.state || 'open',
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
console.log(`[CreatePR] Fetched and stored existing PR: #${existingPr.number}`);
|
||||
}
|
||||
} catch (viewError) {
|
||||
console.error("[CreatePR] Failed to fetch existing PR:", viewError);
|
||||
console.error('[CreatePR] Failed to fetch existing PR:', viewError);
|
||||
prError = errorMessage;
|
||||
}
|
||||
} else {
|
||||
@@ -341,7 +352,7 @@ export function createCreatePRHandler() {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
prError = "gh_cli_not_available";
|
||||
prError = 'gh_cli_not_available';
|
||||
}
|
||||
|
||||
// Return result with browser fallback URL
|
||||
@@ -362,7 +373,7 @@ export function createCreatePRHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Create PR failed");
|
||||
logError(error, 'Create PR failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* POST /list-branches endpoint - List all local branches
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logWorktreeError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getErrorMessage, logWorktreeError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -25,33 +25,31 @@ export function createListBranchesHandler() {
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath required",
|
||||
error: 'worktreePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
const { stdout: currentBranchOutput } = await execAsync(
|
||||
"git rev-parse --abbrev-ref HEAD",
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const currentBranch = currentBranchOutput.trim();
|
||||
|
||||
// List all local branches
|
||||
// Use double quotes around the format string for cross-platform compatibility
|
||||
// Single quotes are preserved literally on Windows; double quotes work on both
|
||||
const { stdout: branchesOutput } = await execAsync(
|
||||
'git branch --format="%(refname:short)"',
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
const { stdout: branchesOutput } = await execAsync('git branch --format="%(refname:short)"', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
|
||||
const branches: BranchInfo[] = branchesOutput
|
||||
.trim()
|
||||
.split("\n")
|
||||
.split('\n')
|
||||
.filter((b) => b.trim())
|
||||
.map((name) => {
|
||||
// Remove any surrounding quotes (Windows git may preserve them)
|
||||
const cleanName = name.trim().replace(/^['"]|['"]$/g, "");
|
||||
const cleanName = name.trim().replace(/^['"]|['"]$/g, '');
|
||||
return {
|
||||
name: cleanName,
|
||||
isCurrent: cleanName === currentBranch,
|
||||
@@ -93,7 +91,7 @@ export function createListBranchesHandler() {
|
||||
});
|
||||
} catch (error) {
|
||||
const worktreePath = req.body?.worktreePath;
|
||||
logWorktreeError(error, "List branches failed", worktreePath);
|
||||
logWorktreeError(error, 'List branches failed', worktreePath);
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* including their ports and URLs.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getDevServerService } from "../../../services/dev-server-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getDevServerService } from '../../../services/dev-server-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createListDevServersHandler() {
|
||||
return async (_req: Request, res: Response): Promise<void> => {
|
||||
@@ -22,7 +22,7 @@ export function createListDevServersHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "List dev servers failed");
|
||||
logError(error, 'List dev servers failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,11 +2,11 @@
|
||||
* POST /merge endpoint - Merge feature (merge worktree branch into main)
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import path from "path";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import path from 'path';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -20,42 +20,34 @@ export function createMergeHandler() {
|
||||
};
|
||||
|
||||
if (!projectPath || !featureId) {
|
||||
res
|
||||
.status(400)
|
||||
.json({
|
||||
success: false,
|
||||
error: "projectPath and featureId required",
|
||||
});
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: 'projectPath and featureId required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const branchName = `feature/${featureId}`;
|
||||
// Git worktrees are stored in project directory
|
||||
const worktreePath = path.join(projectPath, ".worktrees", featureId);
|
||||
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||
|
||||
// Get current branch
|
||||
const { stdout: currentBranch } = await execAsync(
|
||||
"git rev-parse --abbrev-ref HEAD",
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
const { stdout: currentBranch } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: projectPath,
|
||||
});
|
||||
|
||||
// Merge the feature branch
|
||||
const mergeCmd = options?.squash
|
||||
? `git merge --squash ${branchName}`
|
||||
: `git merge ${branchName} -m "${
|
||||
options?.message || `Merge ${branchName}`
|
||||
}"`;
|
||||
: `git merge ${branchName} -m "${options?.message || `Merge ${branchName}`}"`;
|
||||
|
||||
await execAsync(mergeCmd, { cwd: projectPath });
|
||||
|
||||
// If squash merge, need to commit
|
||||
if (options?.squash) {
|
||||
await execAsync(
|
||||
`git commit -m "${
|
||||
options?.message || `Merge ${branchName} (squash)`
|
||||
}"`,
|
||||
{ cwd: projectPath }
|
||||
);
|
||||
await execAsync(`git commit -m "${options?.message || `Merge ${branchName} (squash)`}"`, {
|
||||
cwd: projectPath,
|
||||
});
|
||||
}
|
||||
|
||||
// Clean up worktree and branch
|
||||
@@ -70,7 +62,7 @@ export function createMergeHandler() {
|
||||
|
||||
res.json({ success: true, mergedBranch: branchName });
|
||||
} catch (error) {
|
||||
logError(error, "Merge worktree failed");
|
||||
logError(error, 'Merge worktree failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -3,10 +3,10 @@
|
||||
* GET /default-editor endpoint - Get the name of the default code editor
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -29,8 +29,8 @@ async function detectDefaultEditor(): Promise<EditorInfo> {
|
||||
|
||||
// Try Cursor first (if user has Cursor, they probably prefer it)
|
||||
try {
|
||||
await execAsync("which cursor || where cursor");
|
||||
cachedEditor = { name: "Cursor", command: "cursor" };
|
||||
await execAsync('which cursor || where cursor');
|
||||
cachedEditor = { name: 'Cursor', command: 'cursor' };
|
||||
return cachedEditor;
|
||||
} catch {
|
||||
// Cursor not found
|
||||
@@ -38,8 +38,8 @@ async function detectDefaultEditor(): Promise<EditorInfo> {
|
||||
|
||||
// Try VS Code
|
||||
try {
|
||||
await execAsync("which code || where code");
|
||||
cachedEditor = { name: "VS Code", command: "code" };
|
||||
await execAsync('which code || where code');
|
||||
cachedEditor = { name: 'VS Code', command: 'code' };
|
||||
return cachedEditor;
|
||||
} catch {
|
||||
// VS Code not found
|
||||
@@ -47,8 +47,8 @@ async function detectDefaultEditor(): Promise<EditorInfo> {
|
||||
|
||||
// Try Zed
|
||||
try {
|
||||
await execAsync("which zed || where zed");
|
||||
cachedEditor = { name: "Zed", command: "zed" };
|
||||
await execAsync('which zed || where zed');
|
||||
cachedEditor = { name: 'Zed', command: 'zed' };
|
||||
return cachedEditor;
|
||||
} catch {
|
||||
// Zed not found
|
||||
@@ -56,8 +56,8 @@ async function detectDefaultEditor(): Promise<EditorInfo> {
|
||||
|
||||
// Try Sublime Text
|
||||
try {
|
||||
await execAsync("which subl || where subl");
|
||||
cachedEditor = { name: "Sublime Text", command: "subl" };
|
||||
await execAsync('which subl || where subl');
|
||||
cachedEditor = { name: 'Sublime Text', command: 'subl' };
|
||||
return cachedEditor;
|
||||
} catch {
|
||||
// Sublime not found
|
||||
@@ -65,12 +65,12 @@ async function detectDefaultEditor(): Promise<EditorInfo> {
|
||||
|
||||
// Fallback to file manager
|
||||
const platform = process.platform;
|
||||
if (platform === "darwin") {
|
||||
cachedEditor = { name: "Finder", command: "open" };
|
||||
} else if (platform === "win32") {
|
||||
cachedEditor = { name: "Explorer", command: "explorer" };
|
||||
if (platform === 'darwin') {
|
||||
cachedEditor = { name: 'Finder', command: 'open' };
|
||||
} else if (platform === 'win32') {
|
||||
cachedEditor = { name: 'Explorer', command: 'explorer' };
|
||||
} else {
|
||||
cachedEditor = { name: "File Manager", command: "xdg-open" };
|
||||
cachedEditor = { name: 'File Manager', command: 'xdg-open' };
|
||||
}
|
||||
return cachedEditor;
|
||||
}
|
||||
@@ -87,7 +87,7 @@ export function createGetDefaultEditorHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Get default editor failed");
|
||||
logError(error, 'Get default editor failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
@@ -103,7 +103,7 @@ export function createOpenInEditorHandler() {
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath required",
|
||||
error: 'worktreePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -125,15 +125,15 @@ export function createOpenInEditorHandler() {
|
||||
let openCommand: string;
|
||||
let fallbackName: string;
|
||||
|
||||
if (platform === "darwin") {
|
||||
if (platform === 'darwin') {
|
||||
openCommand = `open "${worktreePath}"`;
|
||||
fallbackName = "Finder";
|
||||
} else if (platform === "win32") {
|
||||
fallbackName = 'Finder';
|
||||
} else if (platform === 'win32') {
|
||||
openCommand = `explorer "${worktreePath}"`;
|
||||
fallbackName = "Explorer";
|
||||
fallbackName = 'Explorer';
|
||||
} else {
|
||||
openCommand = `xdg-open "${worktreePath}"`;
|
||||
fallbackName = "File Manager";
|
||||
fallbackName = 'File Manager';
|
||||
}
|
||||
|
||||
await execAsync(openCommand);
|
||||
@@ -146,7 +146,7 @@ export function createOpenInEditorHandler() {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Open in editor failed");
|
||||
logError(error, 'Open in editor failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
* POST /pr-info endpoint - Get PR info and comments for a branch
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import type { Request, Response } from 'express';
|
||||
import {
|
||||
getErrorMessage,
|
||||
logError,
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
execEnv,
|
||||
isValidBranchName,
|
||||
isGhCliAvailable,
|
||||
} from "../common.js";
|
||||
} from '../common.js';
|
||||
|
||||
export interface PRComment {
|
||||
id: number;
|
||||
@@ -44,7 +44,7 @@ export function createPRInfoHandler() {
|
||||
if (!worktreePath || !branchName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath and branchName required",
|
||||
error: 'worktreePath and branchName required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -53,7 +53,7 @@ export function createPRInfoHandler() {
|
||||
if (!isValidBranchName(branchName)) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "Invalid branch name contains unsafe characters",
|
||||
error: 'Invalid branch name contains unsafe characters',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -67,7 +67,7 @@ export function createPRInfoHandler() {
|
||||
result: {
|
||||
hasPR: false,
|
||||
ghCliAvailable: false,
|
||||
error: "gh CLI not available",
|
||||
error: 'gh CLI not available',
|
||||
},
|
||||
});
|
||||
return;
|
||||
@@ -79,7 +79,7 @@ export function createPRInfoHandler() {
|
||||
let originRepo: string | null = null;
|
||||
|
||||
try {
|
||||
const { stdout: remotes } = await execAsync("git remote -v", {
|
||||
const { stdout: remotes } = await execAsync('git remote -v', {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
@@ -87,21 +87,15 @@ export function createPRInfoHandler() {
|
||||
const lines = remotes.split(/\r?\n/);
|
||||
for (const line of lines) {
|
||||
let match =
|
||||
line.match(
|
||||
/^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/
|
||||
) ||
|
||||
line.match(
|
||||
/^(\w+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/
|
||||
) ||
|
||||
line.match(
|
||||
/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/
|
||||
);
|
||||
line.match(/^(\w+)\s+.*[:/]([^/]+)\/([^/\s]+?)(?:\.git)?\s+\(fetch\)/) ||
|
||||
line.match(/^(\w+)\s+git@[^:]+:([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/) ||
|
||||
line.match(/^(\w+)\s+https?:\/\/[^/]+\/([^/]+)\/([^\s]+?)(?:\.git)?\s+\(fetch\)/);
|
||||
|
||||
if (match) {
|
||||
const [, remoteName, owner, repo] = match;
|
||||
if (remoteName === "upstream") {
|
||||
if (remoteName === 'upstream') {
|
||||
upstreamRepo = `${owner}/${repo}`;
|
||||
} else if (remoteName === "origin") {
|
||||
} else if (remoteName === 'origin') {
|
||||
originOwner = owner;
|
||||
originRepo = repo;
|
||||
}
|
||||
@@ -113,16 +107,11 @@ export function createPRInfoHandler() {
|
||||
|
||||
if (!originOwner || !originRepo) {
|
||||
try {
|
||||
const { stdout: originUrl } = await execAsync(
|
||||
"git config --get remote.origin.url",
|
||||
{
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
}
|
||||
);
|
||||
const match = originUrl
|
||||
.trim()
|
||||
.match(/[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/);
|
||||
const { stdout: originUrl } = await execAsync('git config --get remote.origin.url', {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
const match = originUrl.trim().match(/[:/]([^/]+)\/([^/\s]+?)(?:\.git)?$/);
|
||||
if (match) {
|
||||
if (!originOwner) {
|
||||
originOwner = match[1];
|
||||
@@ -137,21 +126,18 @@ export function createPRInfoHandler() {
|
||||
}
|
||||
|
||||
const targetRepo =
|
||||
upstreamRepo || (originOwner && originRepo
|
||||
? `${originOwner}/${originRepo}`
|
||||
: null);
|
||||
const repoFlag = targetRepo ? ` --repo "${targetRepo}"` : "";
|
||||
const headRef =
|
||||
upstreamRepo && originOwner ? `${originOwner}:${branchName}` : branchName;
|
||||
upstreamRepo || (originOwner && originRepo ? `${originOwner}/${originRepo}` : null);
|
||||
const repoFlag = targetRepo ? ` --repo "${targetRepo}"` : '';
|
||||
const headRef = upstreamRepo && originOwner ? `${originOwner}:${branchName}` : branchName;
|
||||
|
||||
// Get PR info for the branch using gh CLI
|
||||
try {
|
||||
// First, find the PR associated with this branch
|
||||
const listCmd = `gh pr list${repoFlag} --head "${headRef}" --json number,title,url,state,author,body --limit 1`;
|
||||
const { stdout: prListOutput } = await execAsync(
|
||||
listCmd,
|
||||
{ cwd: worktreePath, env: execEnv }
|
||||
);
|
||||
const { stdout: prListOutput } = await execAsync(listCmd, {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
|
||||
const prList = JSON.parse(prListOutput);
|
||||
|
||||
@@ -173,25 +159,22 @@ export function createPRInfoHandler() {
|
||||
let comments: PRComment[] = [];
|
||||
try {
|
||||
const viewCmd = `gh pr view ${prNumber}${repoFlag} --json comments`;
|
||||
const { stdout: commentsOutput } = await execAsync(
|
||||
viewCmd,
|
||||
{ cwd: worktreePath, env: execEnv }
|
||||
);
|
||||
const { stdout: commentsOutput } = await execAsync(viewCmd, {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
const commentsData = JSON.parse(commentsOutput);
|
||||
comments = (commentsData.comments || []).map((c: {
|
||||
id: number;
|
||||
author: { login: string };
|
||||
body: string;
|
||||
createdAt: string;
|
||||
}) => ({
|
||||
id: c.id,
|
||||
author: c.author?.login || "unknown",
|
||||
body: c.body,
|
||||
createdAt: c.createdAt,
|
||||
isReviewComment: false,
|
||||
}));
|
||||
comments = (commentsData.comments || []).map(
|
||||
(c: { id: number; author: { login: string }; body: string; createdAt: string }) => ({
|
||||
id: c.id,
|
||||
author: c.author?.login || 'unknown',
|
||||
body: c.body,
|
||||
createdAt: c.createdAt,
|
||||
isReviewComment: false,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("[PRInfo] Failed to fetch PR comments:", error);
|
||||
console.warn('[PRInfo] Failed to fetch PR comments:', error);
|
||||
}
|
||||
|
||||
// Get review comments (inline code comments)
|
||||
@@ -201,33 +184,35 @@ export function createPRInfoHandler() {
|
||||
try {
|
||||
const reviewsEndpoint = `repos/${targetRepo}/pulls/${prNumber}/comments`;
|
||||
const reviewsCmd = `gh api ${reviewsEndpoint}`;
|
||||
const { stdout: reviewsOutput } = await execAsync(
|
||||
reviewsCmd,
|
||||
{ cwd: worktreePath, env: execEnv }
|
||||
);
|
||||
const { stdout: reviewsOutput } = await execAsync(reviewsCmd, {
|
||||
cwd: worktreePath,
|
||||
env: execEnv,
|
||||
});
|
||||
const reviewsData = JSON.parse(reviewsOutput);
|
||||
reviewComments = reviewsData.map((c: {
|
||||
id: number;
|
||||
user: { login: string };
|
||||
body: string;
|
||||
path: string;
|
||||
line?: number;
|
||||
original_line?: number;
|
||||
created_at: string;
|
||||
}) => ({
|
||||
id: c.id,
|
||||
author: c.user?.login || "unknown",
|
||||
body: c.body,
|
||||
path: c.path,
|
||||
line: c.line || c.original_line,
|
||||
createdAt: c.created_at,
|
||||
isReviewComment: true,
|
||||
}));
|
||||
reviewComments = reviewsData.map(
|
||||
(c: {
|
||||
id: number;
|
||||
user: { login: string };
|
||||
body: string;
|
||||
path: string;
|
||||
line?: number;
|
||||
original_line?: number;
|
||||
created_at: string;
|
||||
}) => ({
|
||||
id: c.id,
|
||||
author: c.user?.login || 'unknown',
|
||||
body: c.body,
|
||||
path: c.path,
|
||||
line: c.line || c.original_line,
|
||||
createdAt: c.created_at,
|
||||
isReviewComment: true,
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
console.warn("[PRInfo] Failed to fetch review comments:", error);
|
||||
console.warn('[PRInfo] Failed to fetch review comments:', error);
|
||||
}
|
||||
} else {
|
||||
console.warn("[PRInfo] Cannot fetch review comments: repository info not available");
|
||||
console.warn('[PRInfo] Cannot fetch review comments: repository info not available');
|
||||
}
|
||||
|
||||
const prInfo: PRInfo = {
|
||||
@@ -235,8 +220,8 @@ export function createPRInfoHandler() {
|
||||
title: pr.title,
|
||||
url: pr.url,
|
||||
state: pr.state,
|
||||
author: pr.author?.login || "unknown",
|
||||
body: pr.body || "",
|
||||
author: pr.author?.login || 'unknown',
|
||||
body: pr.body || '',
|
||||
comments,
|
||||
reviewComments,
|
||||
};
|
||||
@@ -251,7 +236,7 @@ export function createPRInfoHandler() {
|
||||
});
|
||||
} catch (error) {
|
||||
// gh CLI failed - might not be authenticated or no remote
|
||||
logError(error, "Failed to get PR info");
|
||||
logError(error, 'Failed to get PR info');
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
@@ -262,7 +247,7 @@ export function createPRInfoHandler() {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "PR info handler failed");
|
||||
logError(error, 'PR info handler failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* POST /pull endpoint - Pull latest changes for a worktree/branch
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -19,23 +19,22 @@ export function createPullHandler() {
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath required",
|
||||
error: 'worktreePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current branch name
|
||||
const { stdout: branchOutput } = await execAsync(
|
||||
"git rev-parse --abbrev-ref HEAD",
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const branchName = branchOutput.trim();
|
||||
|
||||
// Fetch latest from remote
|
||||
await execAsync("git fetch origin", { cwd: worktreePath });
|
||||
await execAsync('git fetch origin', { cwd: worktreePath });
|
||||
|
||||
// Check if there are local changes that would be overwritten
|
||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
||||
const { stdout: status } = await execAsync('git status --porcelain', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const hasLocalChanges = status.trim().length > 0;
|
||||
@@ -43,35 +42,34 @@ export function createPullHandler() {
|
||||
if (hasLocalChanges) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "You have local changes. Please commit them before pulling.",
|
||||
error: 'You have local changes. Please commit them before pulling.',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Pull latest changes
|
||||
try {
|
||||
const { stdout: pullOutput } = await execAsync(
|
||||
`git pull origin ${branchName}`,
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
const { stdout: pullOutput } = await execAsync(`git pull origin ${branchName}`, {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
|
||||
// Check if we pulled any changes
|
||||
const alreadyUpToDate = pullOutput.includes("Already up to date");
|
||||
const alreadyUpToDate = pullOutput.includes('Already up to date');
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
result: {
|
||||
branch: branchName,
|
||||
pulled: !alreadyUpToDate,
|
||||
message: alreadyUpToDate ? "Already up to date" : "Pulled latest changes",
|
||||
message: alreadyUpToDate ? 'Already up to date' : 'Pulled latest changes',
|
||||
},
|
||||
});
|
||||
} catch (pullError: unknown) {
|
||||
const err = pullError as { stderr?: string; message?: string };
|
||||
const errorMsg = err.stderr || err.message || "Pull failed";
|
||||
const errorMsg = err.stderr || err.message || 'Pull failed';
|
||||
|
||||
// Check for common errors
|
||||
if (errorMsg.includes("no tracking information")) {
|
||||
if (errorMsg.includes('no tracking information')) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Branch '${branchName}' has no upstream branch. Push it first or set upstream with: git branch --set-upstream-to=origin/${branchName}`,
|
||||
@@ -85,7 +83,7 @@ export function createPullHandler() {
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Pull failed");
|
||||
logError(error, 'Pull failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
* POST /push endpoint - Push a worktree branch to remote
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -20,20 +20,19 @@ export function createPushHandler() {
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath required",
|
||||
error: 'worktreePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get branch name
|
||||
const { stdout: branchOutput } = await execAsync(
|
||||
"git rev-parse --abbrev-ref HEAD",
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
const { stdout: branchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const branchName = branchOutput.trim();
|
||||
|
||||
// Push the branch
|
||||
const forceFlag = force ? "--force" : "";
|
||||
const forceFlag = force ? '--force' : '';
|
||||
try {
|
||||
await execAsync(`git push -u origin ${branchName} ${forceFlag}`, {
|
||||
cwd: worktreePath,
|
||||
@@ -54,7 +53,7 @@ export function createPushHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Push worktree failed");
|
||||
logError(error, 'Push worktree failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,9 +6,9 @@
|
||||
* affecting the main dev server.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getDevServerService } from "../../../services/dev-server-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getDevServerService } from '../../../services/dev-server-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createStartDevHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -21,7 +21,7 @@ export function createStartDevHandler() {
|
||||
if (!projectPath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "projectPath is required",
|
||||
error: 'projectPath is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ export function createStartDevHandler() {
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath is required",
|
||||
error: 'worktreePath is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -50,11 +50,11 @@ export function createStartDevHandler() {
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: result.error || "Failed to start dev server",
|
||||
error: result.error || 'Failed to start dev server',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Start dev server failed");
|
||||
logError(error, 'Start dev server failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,9 +5,9 @@
|
||||
* freeing up the ports for reuse.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { getDevServerService } from "../../../services/dev-server-service.js";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { getDevServerService } from '../../../services/dev-server-service.js';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
export function createStopDevHandler() {
|
||||
return async (req: Request, res: Response): Promise<void> => {
|
||||
@@ -19,7 +19,7 @@ export function createStopDevHandler() {
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath is required",
|
||||
error: 'worktreePath is required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -38,11 +38,11 @@ export function createStopDevHandler() {
|
||||
} else {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: result.error || "Failed to stop dev server",
|
||||
error: result.error || 'Failed to stop dev server',
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
logError(error, "Stop dev server failed");
|
||||
logError(error, 'Stop dev server failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,10 +6,10 @@
|
||||
* the user should commit first.
|
||||
*/
|
||||
|
||||
import type { Request, Response } from "express";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getErrorMessage, logError } from "../common.js";
|
||||
import type { Request, Response } from 'express';
|
||||
import { exec } from 'child_process';
|
||||
import { promisify } from 'util';
|
||||
import { getErrorMessage, logError } from '../common.js';
|
||||
|
||||
const execAsync = promisify(exec);
|
||||
|
||||
@@ -19,13 +19,16 @@ const execAsync = promisify(exec);
|
||||
*/
|
||||
async function hasUncommittedChanges(cwd: string): Promise<boolean> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git status --porcelain", { cwd });
|
||||
const lines = stdout.trim().split("\n").filter((line) => {
|
||||
if (!line.trim()) return false;
|
||||
// Exclude .worktrees/ directory (created by automaker)
|
||||
if (line.includes(".worktrees/") || line.endsWith(".worktrees")) return false;
|
||||
return true;
|
||||
});
|
||||
const { stdout } = await execAsync('git status --porcelain', { cwd });
|
||||
const lines = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => {
|
||||
if (!line.trim()) return false;
|
||||
// Exclude .worktrees/ directory (created by automaker)
|
||||
if (line.includes('.worktrees/') || line.endsWith('.worktrees')) return false;
|
||||
return true;
|
||||
});
|
||||
return lines.length > 0;
|
||||
} catch {
|
||||
return false;
|
||||
@@ -38,18 +41,21 @@ async function hasUncommittedChanges(cwd: string): Promise<boolean> {
|
||||
*/
|
||||
async function getChangesSummary(cwd: string): Promise<string> {
|
||||
try {
|
||||
const { stdout } = await execAsync("git status --short", { cwd });
|
||||
const lines = stdout.trim().split("\n").filter((line) => {
|
||||
if (!line.trim()) return false;
|
||||
// Exclude .worktrees/ directory
|
||||
if (line.includes(".worktrees/") || line.endsWith(".worktrees")) return false;
|
||||
return true;
|
||||
});
|
||||
if (lines.length === 0) return "";
|
||||
if (lines.length <= 5) return lines.join(", ");
|
||||
return `${lines.slice(0, 5).join(", ")} and ${lines.length - 5} more files`;
|
||||
const { stdout } = await execAsync('git status --short', { cwd });
|
||||
const lines = stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter((line) => {
|
||||
if (!line.trim()) return false;
|
||||
// Exclude .worktrees/ directory
|
||||
if (line.includes('.worktrees/') || line.endsWith('.worktrees')) return false;
|
||||
return true;
|
||||
});
|
||||
if (lines.length === 0) return '';
|
||||
if (lines.length <= 5) return lines.join(', ');
|
||||
return `${lines.slice(0, 5).join(', ')} and ${lines.length - 5} more files`;
|
||||
} catch {
|
||||
return "unknown changes";
|
||||
return 'unknown changes';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +70,7 @@ export function createSwitchBranchHandler() {
|
||||
if (!worktreePath) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "worktreePath required",
|
||||
error: 'worktreePath required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -72,16 +78,15 @@ export function createSwitchBranchHandler() {
|
||||
if (!branchName) {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: "branchName required",
|
||||
error: 'branchName required',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Get current branch
|
||||
const { stdout: currentBranchOutput } = await execAsync(
|
||||
"git rev-parse --abbrev-ref HEAD",
|
||||
{ cwd: worktreePath }
|
||||
);
|
||||
const { stdout: currentBranchOutput } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||
cwd: worktreePath,
|
||||
});
|
||||
const previousBranch = currentBranchOutput.trim();
|
||||
|
||||
if (previousBranch === branchName) {
|
||||
@@ -115,7 +120,7 @@ export function createSwitchBranchHandler() {
|
||||
res.status(400).json({
|
||||
success: false,
|
||||
error: `Cannot switch branches: you have uncommitted changes (${summary}). Please commit your changes first.`,
|
||||
code: "UNCOMMITTED_CHANGES",
|
||||
code: 'UNCOMMITTED_CHANGES',
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -132,7 +137,7 @@ export function createSwitchBranchHandler() {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
logError(error, "Switch branch failed");
|
||||
logError(error, 'Switch branch failed');
|
||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,11 +5,11 @@
|
||||
* Supports cross-platform shell detection including WSL.
|
||||
*/
|
||||
|
||||
import * as pty from "node-pty";
|
||||
import { EventEmitter } from "events";
|
||||
import * as os from "os";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import * as pty from 'node-pty';
|
||||
import { EventEmitter } from 'events';
|
||||
import * as os from 'os';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
// Maximum scrollback buffer size (characters)
|
||||
const MAX_SCROLLBACK_SIZE = 50000; // ~50KB per terminal
|
||||
@@ -21,7 +21,7 @@ export const MAX_MAX_SESSIONS = 1000;
|
||||
// Maximum number of concurrent terminal sessions
|
||||
// Can be overridden via TERMINAL_MAX_SESSIONS environment variable
|
||||
// Default set to 1000 - effectively unlimited for most use cases
|
||||
let maxSessions = parseInt(process.env.TERMINAL_MAX_SESSIONS || "1000", 10);
|
||||
let maxSessions = parseInt(process.env.TERMINAL_MAX_SESSIONS || '1000', 10);
|
||||
|
||||
// Throttle output to prevent overwhelming WebSocket under heavy load
|
||||
// Using 4ms for responsive input feedback while still preventing flood
|
||||
@@ -65,20 +65,20 @@ export class TerminalService extends EventEmitter {
|
||||
const platform = os.platform();
|
||||
|
||||
// Check if running in WSL
|
||||
if (platform === "linux" && this.isWSL()) {
|
||||
if (platform === 'linux' && this.isWSL()) {
|
||||
// In WSL, prefer the user's configured shell or bash
|
||||
const userShell = process.env.SHELL || "/bin/bash";
|
||||
const userShell = process.env.SHELL || '/bin/bash';
|
||||
if (fs.existsSync(userShell)) {
|
||||
return { shell: userShell, args: ["--login"] };
|
||||
return { shell: userShell, args: ['--login'] };
|
||||
}
|
||||
return { shell: "/bin/bash", args: ["--login"] };
|
||||
return { shell: '/bin/bash', args: ['--login'] };
|
||||
}
|
||||
|
||||
switch (platform) {
|
||||
case "win32": {
|
||||
case 'win32': {
|
||||
// Windows: prefer PowerShell, fall back to cmd
|
||||
const pwsh = "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe";
|
||||
const pwshCore = "C:\\Program Files\\PowerShell\\7\\pwsh.exe";
|
||||
const pwsh = 'C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe';
|
||||
const pwshCore = 'C:\\Program Files\\PowerShell\\7\\pwsh.exe';
|
||||
|
||||
if (fs.existsSync(pwshCore)) {
|
||||
return { shell: pwshCore, args: [] };
|
||||
@@ -86,32 +86,32 @@ export class TerminalService extends EventEmitter {
|
||||
if (fs.existsSync(pwsh)) {
|
||||
return { shell: pwsh, args: [] };
|
||||
}
|
||||
return { shell: "cmd.exe", args: [] };
|
||||
return { shell: 'cmd.exe', args: [] };
|
||||
}
|
||||
|
||||
case "darwin": {
|
||||
case 'darwin': {
|
||||
// macOS: prefer user's shell, then zsh, then bash
|
||||
const userShell = process.env.SHELL;
|
||||
if (userShell && fs.existsSync(userShell)) {
|
||||
return { shell: userShell, args: ["--login"] };
|
||||
return { shell: userShell, args: ['--login'] };
|
||||
}
|
||||
if (fs.existsSync("/bin/zsh")) {
|
||||
return { shell: "/bin/zsh", args: ["--login"] };
|
||||
if (fs.existsSync('/bin/zsh')) {
|
||||
return { shell: '/bin/zsh', args: ['--login'] };
|
||||
}
|
||||
return { shell: "/bin/bash", args: ["--login"] };
|
||||
return { shell: '/bin/bash', args: ['--login'] };
|
||||
}
|
||||
|
||||
case "linux":
|
||||
case 'linux':
|
||||
default: {
|
||||
// Linux: prefer user's shell, then bash, then sh
|
||||
const userShell = process.env.SHELL;
|
||||
if (userShell && fs.existsSync(userShell)) {
|
||||
return { shell: userShell, args: ["--login"] };
|
||||
return { shell: userShell, args: ['--login'] };
|
||||
}
|
||||
if (fs.existsSync("/bin/bash")) {
|
||||
return { shell: "/bin/bash", args: ["--login"] };
|
||||
if (fs.existsSync('/bin/bash')) {
|
||||
return { shell: '/bin/bash', args: ['--login'] };
|
||||
}
|
||||
return { shell: "/bin/sh", args: [] };
|
||||
return { shell: '/bin/sh', args: [] };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -122,9 +122,9 @@ export class TerminalService extends EventEmitter {
|
||||
isWSL(): boolean {
|
||||
try {
|
||||
// Check /proc/version for Microsoft/WSL indicators
|
||||
if (fs.existsSync("/proc/version")) {
|
||||
const version = fs.readFileSync("/proc/version", "utf-8").toLowerCase();
|
||||
return version.includes("microsoft") || version.includes("wsl");
|
||||
if (fs.existsSync('/proc/version')) {
|
||||
const version = fs.readFileSync('/proc/version', 'utf-8').toLowerCase();
|
||||
return version.includes('microsoft') || version.includes('wsl');
|
||||
}
|
||||
// Check for WSL environment variable
|
||||
if (process.env.WSL_DISTRO_NAME || process.env.WSLENV) {
|
||||
@@ -170,19 +170,19 @@ export class TerminalService extends EventEmitter {
|
||||
let cwd = requestedCwd.trim();
|
||||
|
||||
// Reject paths with null bytes (could bypass path checks)
|
||||
if (cwd.includes("\0")) {
|
||||
console.warn(`[Terminal] Rejecting path with null byte: ${cwd.replace(/\0/g, "\\0")}`);
|
||||
if (cwd.includes('\0')) {
|
||||
console.warn(`[Terminal] Rejecting path with null byte: ${cwd.replace(/\0/g, '\\0')}`);
|
||||
return homeDir;
|
||||
}
|
||||
|
||||
// Fix double slashes at start (but not for Windows UNC paths)
|
||||
if (cwd.startsWith("//") && !cwd.startsWith("//wsl")) {
|
||||
if (cwd.startsWith('//') && !cwd.startsWith('//wsl')) {
|
||||
cwd = cwd.slice(1);
|
||||
}
|
||||
|
||||
// Normalize the path to resolve . and .. segments
|
||||
// Skip normalization for WSL UNC paths as path.resolve would break them
|
||||
if (!cwd.startsWith("//wsl")) {
|
||||
if (!cwd.startsWith('//wsl')) {
|
||||
cwd = path.resolve(cwd);
|
||||
}
|
||||
|
||||
@@ -247,19 +247,19 @@ export class TerminalService extends EventEmitter {
|
||||
// These settings ensure consistent terminal behavior across platforms
|
||||
const env: Record<string, string> = {
|
||||
...process.env,
|
||||
TERM: "xterm-256color",
|
||||
COLORTERM: "truecolor",
|
||||
TERM_PROGRAM: "automaker-terminal",
|
||||
TERM: 'xterm-256color',
|
||||
COLORTERM: 'truecolor',
|
||||
TERM_PROGRAM: 'automaker-terminal',
|
||||
// Ensure proper locale for character handling
|
||||
LANG: process.env.LANG || "en_US.UTF-8",
|
||||
LC_ALL: process.env.LC_ALL || process.env.LANG || "en_US.UTF-8",
|
||||
LANG: process.env.LANG || 'en_US.UTF-8',
|
||||
LC_ALL: process.env.LC_ALL || process.env.LANG || 'en_US.UTF-8',
|
||||
...options.env,
|
||||
};
|
||||
|
||||
console.log(`[Terminal] Creating session ${id} with shell: ${shell} in ${cwd}`);
|
||||
|
||||
const ptyProcess = pty.spawn(shell, shellArgs, {
|
||||
name: "xterm-256color",
|
||||
name: 'xterm-256color',
|
||||
cols: options.cols || 80,
|
||||
rows: options.rows || 24,
|
||||
cwd,
|
||||
@@ -272,8 +272,8 @@ export class TerminalService extends EventEmitter {
|
||||
cwd,
|
||||
createdAt: new Date(),
|
||||
shell,
|
||||
scrollbackBuffer: "",
|
||||
outputBuffer: "",
|
||||
scrollbackBuffer: '',
|
||||
outputBuffer: '',
|
||||
flushTimeout: null,
|
||||
resizeInProgress: false,
|
||||
resizeDebounceTimeout: null,
|
||||
@@ -293,12 +293,12 @@ export class TerminalService extends EventEmitter {
|
||||
// Schedule another flush for remaining data
|
||||
session.flushTimeout = setTimeout(flushOutput, OUTPUT_THROTTLE_MS);
|
||||
} else {
|
||||
session.outputBuffer = "";
|
||||
session.outputBuffer = '';
|
||||
session.flushTimeout = null;
|
||||
}
|
||||
|
||||
this.dataCallbacks.forEach((cb) => cb(id, dataToSend));
|
||||
this.emit("data", id, dataToSend);
|
||||
this.emit('data', id, dataToSend);
|
||||
};
|
||||
|
||||
// Forward data events with throttling
|
||||
@@ -331,7 +331,7 @@ export class TerminalService extends EventEmitter {
|
||||
console.log(`[Terminal] Session ${id} exited with code ${exitCode}`);
|
||||
this.sessions.delete(id);
|
||||
this.exitCallbacks.forEach((cb) => cb(id, exitCode));
|
||||
this.emit("exit", id, exitCode);
|
||||
this.emit('exit', id, exitCode);
|
||||
});
|
||||
|
||||
console.log(`[Terminal] Session ${id} created successfully`);
|
||||
@@ -414,7 +414,7 @@ export class TerminalService extends EventEmitter {
|
||||
|
||||
// First try graceful SIGTERM to allow process cleanup
|
||||
console.log(`[Terminal] Session ${sessionId} sending SIGTERM`);
|
||||
session.pty.kill("SIGTERM");
|
||||
session.pty.kill('SIGTERM');
|
||||
|
||||
// Schedule SIGKILL fallback if process doesn't exit gracefully
|
||||
// The onExit handler will remove session from map when it actually exits
|
||||
@@ -422,7 +422,7 @@ export class TerminalService extends EventEmitter {
|
||||
if (this.sessions.has(sessionId)) {
|
||||
console.log(`[Terminal] Session ${sessionId} still alive after SIGTERM, sending SIGKILL`);
|
||||
try {
|
||||
session.pty.kill("SIGKILL");
|
||||
session.pty.kill('SIGKILL');
|
||||
} catch {
|
||||
// Process may have already exited
|
||||
}
|
||||
@@ -467,7 +467,7 @@ export class TerminalService extends EventEmitter {
|
||||
|
||||
// Clear any pending output that hasn't been flushed yet
|
||||
// This data is already in scrollbackBuffer
|
||||
session.outputBuffer = "";
|
||||
session.outputBuffer = '';
|
||||
if (session.flushTimeout) {
|
||||
clearTimeout(session.flushTimeout);
|
||||
session.flushTimeout = null;
|
||||
|
||||
Reference in New Issue
Block a user