mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 09:33:07 +00:00
Merge pull request #206 from AutoMaker-Org/refactoring-themes
refactor: replace fs with secureFs for improved file handling
This commit is contained in:
@@ -9,45 +9,59 @@
|
|||||||
* - Chat: Full tool access for interactive coding
|
* - Chat: Full tool access for interactive coding
|
||||||
*
|
*
|
||||||
* Uses model-resolver for consistent model handling across the application.
|
* Uses model-resolver for consistent model handling across the application.
|
||||||
|
*
|
||||||
|
* SECURITY: All factory functions validate the working directory (cwd) against
|
||||||
|
* ALLOWED_ROOT_DIRECTORY before returning options. This provides a centralized
|
||||||
|
* security check that applies to ALL AI model invocations, regardless of provider.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Options } from "@anthropic-ai/claude-agent-sdk";
|
import type { Options } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import { resolveModelString } from "@automaker/model-resolver";
|
import path from 'path';
|
||||||
import { DEFAULT_MODELS, CLAUDE_MODEL_MAP } from "@automaker/types";
|
import { resolveModelString } from '@automaker/model-resolver';
|
||||||
|
import { DEFAULT_MODELS, CLAUDE_MODEL_MAP } from '@automaker/types';
|
||||||
|
import { isPathAllowed, PathNotAllowedError, getAllowedRootDirectory } from '@automaker/platform';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate that a working directory is allowed by ALLOWED_ROOT_DIRECTORY.
|
||||||
|
* This is the centralized security check for ALL AI model invocations.
|
||||||
|
*
|
||||||
|
* @param cwd - The working directory to validate
|
||||||
|
* @throws PathNotAllowedError if the directory is not within ALLOWED_ROOT_DIRECTORY
|
||||||
|
*
|
||||||
|
* This function is called by all create*Options() factory functions to ensure
|
||||||
|
* that AI models can only operate within allowed directories. This applies to:
|
||||||
|
* - All current models (Claude, future models)
|
||||||
|
* - All invocation types (chat, auto-mode, spec generation, etc.)
|
||||||
|
*/
|
||||||
|
export function validateWorkingDirectory(cwd: string): void {
|
||||||
|
const resolvedCwd = path.resolve(cwd);
|
||||||
|
|
||||||
|
if (!isPathAllowed(resolvedCwd)) {
|
||||||
|
const allowedRoot = getAllowedRootDirectory();
|
||||||
|
throw new PathNotAllowedError(
|
||||||
|
`Working directory "${cwd}" (resolved: ${resolvedCwd}) is not allowed. ` +
|
||||||
|
(allowedRoot
|
||||||
|
? `Must be within ALLOWED_ROOT_DIRECTORY: ${allowedRoot}`
|
||||||
|
: 'ALLOWED_ROOT_DIRECTORY is configured but path is not within allowed directories.')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Tool presets for different use cases
|
* Tool presets for different use cases
|
||||||
*/
|
*/
|
||||||
export const TOOL_PRESETS = {
|
export const TOOL_PRESETS = {
|
||||||
/** Read-only tools for analysis */
|
/** Read-only tools for analysis */
|
||||||
readOnly: ["Read", "Glob", "Grep"] as const,
|
readOnly: ['Read', 'Glob', 'Grep'] as const,
|
||||||
|
|
||||||
/** Tools for spec generation that needs to read the codebase */
|
/** Tools for spec generation that needs to read the codebase */
|
||||||
specGeneration: ["Read", "Glob", "Grep"] as const,
|
specGeneration: ['Read', 'Glob', 'Grep'] as const,
|
||||||
|
|
||||||
/** Full tool access for feature implementation */
|
/** Full tool access for feature implementation */
|
||||||
fullAccess: [
|
fullAccess: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'] as const,
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
] as const,
|
|
||||||
|
|
||||||
/** Tools for chat/interactive mode */
|
/** Tools for chat/interactive mode */
|
||||||
chat: [
|
chat: ['Read', 'Write', 'Edit', 'Glob', 'Grep', 'Bash', 'WebSearch', 'WebFetch'] as const,
|
||||||
"Read",
|
|
||||||
"Write",
|
|
||||||
"Edit",
|
|
||||||
"Glob",
|
|
||||||
"Grep",
|
|
||||||
"Bash",
|
|
||||||
"WebSearch",
|
|
||||||
"WebFetch",
|
|
||||||
] as const,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -78,7 +92,7 @@ export const MAX_TURNS = {
|
|||||||
* - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations
|
* - AUTOMAKER_MODEL_DEFAULT: Fallback model for all operations
|
||||||
*/
|
*/
|
||||||
export function getModelForUseCase(
|
export function getModelForUseCase(
|
||||||
useCase: "spec" | "features" | "suggestions" | "chat" | "auto" | "default",
|
useCase: 'spec' | 'features' | 'suggestions' | 'chat' | 'auto' | 'default',
|
||||||
explicitModel?: string
|
explicitModel?: string
|
||||||
): string {
|
): string {
|
||||||
// Explicit model takes precedence
|
// Explicit model takes precedence
|
||||||
@@ -102,12 +116,12 @@ export function getModelForUseCase(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const defaultModels: Record<string, string> = {
|
const defaultModels: Record<string, string> = {
|
||||||
spec: CLAUDE_MODEL_MAP["haiku"], // used to generate app specs
|
spec: CLAUDE_MODEL_MAP['haiku'], // used to generate app specs
|
||||||
features: CLAUDE_MODEL_MAP["haiku"], // used to generate features from app specs
|
features: CLAUDE_MODEL_MAP['haiku'], // used to generate features from app specs
|
||||||
suggestions: CLAUDE_MODEL_MAP["haiku"], // used for suggestions
|
suggestions: CLAUDE_MODEL_MAP['haiku'], // used for suggestions
|
||||||
chat: CLAUDE_MODEL_MAP["haiku"], // used for chat
|
chat: CLAUDE_MODEL_MAP['haiku'], // used for chat
|
||||||
auto: CLAUDE_MODEL_MAP["opus"], // used to implement kanban cards
|
auto: CLAUDE_MODEL_MAP['opus'], // used to implement kanban cards
|
||||||
default: CLAUDE_MODEL_MAP["opus"],
|
default: CLAUDE_MODEL_MAP['opus'],
|
||||||
};
|
};
|
||||||
|
|
||||||
return resolveModelString(defaultModels[useCase] || DEFAULT_MODELS.claude);
|
return resolveModelString(defaultModels[useCase] || DEFAULT_MODELS.claude);
|
||||||
@@ -118,7 +132,7 @@ export function getModelForUseCase(
|
|||||||
*/
|
*/
|
||||||
function getBaseOptions(): Partial<Options> {
|
function getBaseOptions(): Partial<Options> {
|
||||||
return {
|
return {
|
||||||
permissionMode: "acceptEdits",
|
permissionMode: 'acceptEdits',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +157,7 @@ export interface CreateSdkOptionsConfig {
|
|||||||
|
|
||||||
/** Optional output format for structured outputs */
|
/** Optional output format for structured outputs */
|
||||||
outputFormat?: {
|
outputFormat?: {
|
||||||
type: "json_schema";
|
type: 'json_schema';
|
||||||
schema: Record<string, unknown>;
|
schema: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -156,16 +170,17 @@ export interface CreateSdkOptionsConfig {
|
|||||||
* - Extended turns for thorough exploration
|
* - Extended turns for thorough exploration
|
||||||
* - Opus model by default (can be overridden)
|
* - Opus model by default (can be overridden)
|
||||||
*/
|
*/
|
||||||
export function createSpecGenerationOptions(
|
export function createSpecGenerationOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
config: CreateSdkOptionsConfig
|
// Validate working directory before creating options
|
||||||
): Options {
|
validateWorkingDirectory(config.cwd);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getBaseOptions(),
|
...getBaseOptions(),
|
||||||
// Override permissionMode - spec generation only needs read-only tools
|
// Override permissionMode - spec generation only needs read-only tools
|
||||||
// Using "acceptEdits" can cause Claude to write files to unexpected locations
|
// Using "acceptEdits" can cause Claude to write files to unexpected locations
|
||||||
// See: https://github.com/AutoMaker-Org/automaker/issues/149
|
// See: https://github.com/AutoMaker-Org/automaker/issues/149
|
||||||
permissionMode: "default",
|
permissionMode: 'default',
|
||||||
model: getModelForUseCase("spec", config.model),
|
model: getModelForUseCase('spec', config.model),
|
||||||
maxTurns: MAX_TURNS.maximum,
|
maxTurns: MAX_TURNS.maximum,
|
||||||
cwd: config.cwd,
|
cwd: config.cwd,
|
||||||
allowedTools: [...TOOL_PRESETS.specGeneration],
|
allowedTools: [...TOOL_PRESETS.specGeneration],
|
||||||
@@ -183,14 +198,15 @@ export function createSpecGenerationOptions(
|
|||||||
* - Quick turns since it's mostly JSON generation
|
* - Quick turns since it's mostly JSON generation
|
||||||
* - Sonnet model by default for speed
|
* - Sonnet model by default for speed
|
||||||
*/
|
*/
|
||||||
export function createFeatureGenerationOptions(
|
export function createFeatureGenerationOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
config: CreateSdkOptionsConfig
|
// Validate working directory before creating options
|
||||||
): Options {
|
validateWorkingDirectory(config.cwd);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getBaseOptions(),
|
...getBaseOptions(),
|
||||||
// Override permissionMode - feature generation only needs read-only tools
|
// Override permissionMode - feature generation only needs read-only tools
|
||||||
permissionMode: "default",
|
permissionMode: 'default',
|
||||||
model: getModelForUseCase("features", config.model),
|
model: getModelForUseCase('features', config.model),
|
||||||
maxTurns: MAX_TURNS.quick,
|
maxTurns: MAX_TURNS.quick,
|
||||||
cwd: config.cwd,
|
cwd: config.cwd,
|
||||||
allowedTools: [...TOOL_PRESETS.readOnly],
|
allowedTools: [...TOOL_PRESETS.readOnly],
|
||||||
@@ -207,12 +223,13 @@ export function createFeatureGenerationOptions(
|
|||||||
* - Standard turns to allow thorough codebase exploration and structured output generation
|
* - Standard turns to allow thorough codebase exploration and structured output generation
|
||||||
* - Opus model by default for thorough analysis
|
* - Opus model by default for thorough analysis
|
||||||
*/
|
*/
|
||||||
export function createSuggestionsOptions(
|
export function createSuggestionsOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
config: CreateSdkOptionsConfig
|
// Validate working directory before creating options
|
||||||
): Options {
|
validateWorkingDirectory(config.cwd);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getBaseOptions(),
|
...getBaseOptions(),
|
||||||
model: getModelForUseCase("suggestions", config.model),
|
model: getModelForUseCase('suggestions', config.model),
|
||||||
maxTurns: MAX_TURNS.extended,
|
maxTurns: MAX_TURNS.extended,
|
||||||
cwd: config.cwd,
|
cwd: config.cwd,
|
||||||
allowedTools: [...TOOL_PRESETS.readOnly],
|
allowedTools: [...TOOL_PRESETS.readOnly],
|
||||||
@@ -232,12 +249,15 @@ export function createSuggestionsOptions(
|
|||||||
* - Sandbox enabled for bash safety
|
* - Sandbox enabled for bash safety
|
||||||
*/
|
*/
|
||||||
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
|
export function createChatOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
|
// Validate working directory before creating options
|
||||||
|
validateWorkingDirectory(config.cwd);
|
||||||
|
|
||||||
// Model priority: explicit model > session model > chat default
|
// Model priority: explicit model > session model > chat default
|
||||||
const effectiveModel = config.model || config.sessionModel;
|
const effectiveModel = config.model || config.sessionModel;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getBaseOptions(),
|
...getBaseOptions(),
|
||||||
model: getModelForUseCase("chat", effectiveModel),
|
model: getModelForUseCase('chat', effectiveModel),
|
||||||
maxTurns: MAX_TURNS.standard,
|
maxTurns: MAX_TURNS.standard,
|
||||||
cwd: config.cwd,
|
cwd: config.cwd,
|
||||||
allowedTools: [...TOOL_PRESETS.chat],
|
allowedTools: [...TOOL_PRESETS.chat],
|
||||||
@@ -260,9 +280,12 @@ export function createChatOptions(config: CreateSdkOptionsConfig): Options {
|
|||||||
* - Sandbox enabled for bash safety
|
* - Sandbox enabled for bash safety
|
||||||
*/
|
*/
|
||||||
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
|
export function createAutoModeOptions(config: CreateSdkOptionsConfig): Options {
|
||||||
|
// Validate working directory before creating options
|
||||||
|
validateWorkingDirectory(config.cwd);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getBaseOptions(),
|
...getBaseOptions(),
|
||||||
model: getModelForUseCase("auto", config.model),
|
model: getModelForUseCase('auto', config.model),
|
||||||
maxTurns: MAX_TURNS.maximum,
|
maxTurns: MAX_TURNS.maximum,
|
||||||
cwd: config.cwd,
|
cwd: config.cwd,
|
||||||
allowedTools: [...TOOL_PRESETS.fullAccess],
|
allowedTools: [...TOOL_PRESETS.fullAccess],
|
||||||
@@ -287,14 +310,15 @@ export function createCustomOptions(
|
|||||||
sandbox?: { enabled: boolean; autoAllowBashIfSandboxed?: boolean };
|
sandbox?: { enabled: boolean; autoAllowBashIfSandboxed?: boolean };
|
||||||
}
|
}
|
||||||
): Options {
|
): Options {
|
||||||
|
// Validate working directory before creating options
|
||||||
|
validateWorkingDirectory(config.cwd);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...getBaseOptions(),
|
...getBaseOptions(),
|
||||||
model: getModelForUseCase("default", config.model),
|
model: getModelForUseCase('default', config.model),
|
||||||
maxTurns: config.maxTurns ?? MAX_TURNS.maximum,
|
maxTurns: config.maxTurns ?? MAX_TURNS.maximum,
|
||||||
cwd: config.cwd,
|
cwd: config.cwd,
|
||||||
allowedTools: config.allowedTools
|
allowedTools: config.allowedTools ? [...config.allowedTools] : [...TOOL_PRESETS.readOnly],
|
||||||
? [...config.allowedTools]
|
|
||||||
: [...TOOL_PRESETS.readOnly],
|
|
||||||
...(config.sandbox && { sandbox: config.sandbox }),
|
...(config.sandbox && { sandbox: config.sandbox }),
|
||||||
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
...(config.systemPrompt && { systemPrompt: config.systemPrompt }),
|
||||||
...(config.abortController && { abortController: config.abortController }),
|
...(config.abortController && { abortController: config.abortController }),
|
||||||
|
|||||||
@@ -3,8 +3,8 @@
|
|||||||
* Stores worktree-specific data in .automaker/worktrees/:branch/worktree.json
|
* Stores worktree-specific data in .automaker/worktrees/:branch/worktree.json
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import * as fs from "fs/promises";
|
import * as secureFs from './secure-fs.js';
|
||||||
import * as path from "path";
|
import * as path from 'path';
|
||||||
|
|
||||||
/** Maximum length for sanitized branch names in filesystem paths */
|
/** Maximum length for sanitized branch names in filesystem paths */
|
||||||
const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200;
|
const MAX_SANITIZED_BRANCH_PATH_LENGTH = 200;
|
||||||
@@ -32,11 +32,11 @@ function sanitizeBranchName(branch: string): string {
|
|||||||
// - Windows invalid chars: : * ? " < > |
|
// - Windows invalid chars: : * ? " < > |
|
||||||
// - Other potentially problematic chars
|
// - Other potentially problematic chars
|
||||||
let safeBranch = branch
|
let safeBranch = branch
|
||||||
.replace(/[/\\:*?"<>|]/g, "-") // Replace invalid chars with dash
|
.replace(/[/\\:*?"<>|]/g, '-') // Replace invalid chars with dash
|
||||||
.replace(/\s+/g, "_") // Replace spaces with underscores
|
.replace(/\s+/g, '_') // Replace spaces with underscores
|
||||||
.replace(/\.+$/g, "") // Remove trailing dots (Windows issue)
|
.replace(/\.+$/g, '') // Remove trailing dots (Windows issue)
|
||||||
.replace(/-+/g, "-") // Collapse multiple dashes
|
.replace(/-+/g, '-') // Collapse multiple dashes
|
||||||
.replace(/^-|-$/g, ""); // Remove leading/trailing dashes
|
.replace(/^-|-$/g, ''); // Remove leading/trailing dashes
|
||||||
|
|
||||||
// Truncate to safe length (leave room for path components)
|
// Truncate to safe length (leave room for path components)
|
||||||
safeBranch = safeBranch.substring(0, MAX_SANITIZED_BRANCH_PATH_LENGTH);
|
safeBranch = safeBranch.substring(0, MAX_SANITIZED_BRANCH_PATH_LENGTH);
|
||||||
@@ -44,7 +44,7 @@ function sanitizeBranchName(branch: string): string {
|
|||||||
// Handle Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
|
// Handle Windows reserved names (CON, PRN, AUX, NUL, COM1-9, LPT1-9)
|
||||||
const windowsReserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
|
const windowsReserved = /^(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])$/i;
|
||||||
if (windowsReserved.test(safeBranch) || safeBranch.length === 0) {
|
if (windowsReserved.test(safeBranch) || safeBranch.length === 0) {
|
||||||
safeBranch = `_${safeBranch || "branch"}`;
|
safeBranch = `_${safeBranch || 'branch'}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return safeBranch;
|
return safeBranch;
|
||||||
@@ -55,14 +55,14 @@ function sanitizeBranchName(branch: string): string {
|
|||||||
*/
|
*/
|
||||||
function getWorktreeMetadataDir(projectPath: string, branch: string): string {
|
function getWorktreeMetadataDir(projectPath: string, branch: string): string {
|
||||||
const safeBranch = sanitizeBranchName(branch);
|
const safeBranch = sanitizeBranchName(branch);
|
||||||
return path.join(projectPath, ".automaker", "worktrees", safeBranch);
|
return path.join(projectPath, '.automaker', 'worktrees', safeBranch);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the path to the worktree metadata file
|
* Get the path to the worktree metadata file
|
||||||
*/
|
*/
|
||||||
function getWorktreeMetadataPath(projectPath: string, branch: string): string {
|
function getWorktreeMetadataPath(projectPath: string, branch: string): string {
|
||||||
return path.join(getWorktreeMetadataDir(projectPath, branch), "worktree.json");
|
return path.join(getWorktreeMetadataDir(projectPath, branch), 'worktree.json');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,7 +74,7 @@ export async function readWorktreeMetadata(
|
|||||||
): Promise<WorktreeMetadata | null> {
|
): Promise<WorktreeMetadata | null> {
|
||||||
try {
|
try {
|
||||||
const metadataPath = getWorktreeMetadataPath(projectPath, branch);
|
const metadataPath = getWorktreeMetadataPath(projectPath, branch);
|
||||||
const content = await fs.readFile(metadataPath, "utf-8");
|
const content = (await secureFs.readFile(metadataPath, 'utf-8')) as string;
|
||||||
return JSON.parse(content) as WorktreeMetadata;
|
return JSON.parse(content) as WorktreeMetadata;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// File doesn't exist or can't be read
|
// File doesn't exist or can't be read
|
||||||
@@ -94,10 +94,10 @@ export async function writeWorktreeMetadata(
|
|||||||
const metadataPath = getWorktreeMetadataPath(projectPath, branch);
|
const metadataPath = getWorktreeMetadataPath(projectPath, branch);
|
||||||
|
|
||||||
// Ensure directory exists
|
// Ensure directory exists
|
||||||
await fs.mkdir(metadataDir, { recursive: true });
|
await secureFs.mkdir(metadataDir, { recursive: true });
|
||||||
|
|
||||||
// Write metadata
|
// Write metadata
|
||||||
await fs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), "utf-8");
|
await secureFs.writeFile(metadataPath, JSON.stringify(metadata, null, 2), 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -143,16 +143,16 @@ export async function readAllWorktreeMetadata(
|
|||||||
projectPath: string
|
projectPath: string
|
||||||
): Promise<Map<string, WorktreeMetadata>> {
|
): Promise<Map<string, WorktreeMetadata>> {
|
||||||
const result = new Map<string, WorktreeMetadata>();
|
const result = new Map<string, WorktreeMetadata>();
|
||||||
const worktreesDir = path.join(projectPath, ".automaker", "worktrees");
|
const worktreesDir = path.join(projectPath, '.automaker', 'worktrees');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const dirs = await fs.readdir(worktreesDir, { withFileTypes: true });
|
const dirs = await secureFs.readdir(worktreesDir, { withFileTypes: true });
|
||||||
|
|
||||||
for (const dir of dirs) {
|
for (const dir of dirs) {
|
||||||
if (dir.isDirectory()) {
|
if (dir.isDirectory()) {
|
||||||
const metadataPath = path.join(worktreesDir, dir.name, "worktree.json");
|
const metadataPath = path.join(worktreesDir, dir.name, 'worktree.json');
|
||||||
try {
|
try {
|
||||||
const content = await fs.readFile(metadataPath, "utf-8");
|
const content = (await secureFs.readFile(metadataPath, 'utf-8')) as string;
|
||||||
const metadata = JSON.parse(content) as WorktreeMetadata;
|
const metadata = JSON.parse(content) as WorktreeMetadata;
|
||||||
result.set(metadata.branch, metadata);
|
result.set(metadata.branch, metadata);
|
||||||
} catch {
|
} catch {
|
||||||
@@ -170,13 +170,10 @@ export async function readAllWorktreeMetadata(
|
|||||||
/**
|
/**
|
||||||
* Delete worktree metadata for a branch
|
* Delete worktree metadata for a branch
|
||||||
*/
|
*/
|
||||||
export async function deleteWorktreeMetadata(
|
export async function deleteWorktreeMetadata(projectPath: string, branch: string): Promise<void> {
|
||||||
projectPath: string,
|
|
||||||
branch: string
|
|
||||||
): Promise<void> {
|
|
||||||
const metadataDir = getWorktreeMetadataDir(projectPath, branch);
|
const metadataDir = getWorktreeMetadataDir(projectPath, branch);
|
||||||
try {
|
try {
|
||||||
await fs.rm(metadataDir, { recursive: true, force: true });
|
await secureFs.rm(metadataDir, { recursive: true, force: true });
|
||||||
} catch {
|
} catch {
|
||||||
// Ignore errors if directory doesn't exist
|
// Ignore errors if directory doesn't exist
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
* Generate features from existing app_spec.txt
|
* Generate features from existing app_spec.txt
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../lib/secure-fs.js';
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { createFeatureGenerationOptions } from "../../lib/sdk-options.js";
|
import { createFeatureGenerationOptions } from '../../lib/sdk-options.js';
|
||||||
import { logAuthStatus } from "./common.js";
|
import { logAuthStatus } from './common.js';
|
||||||
import { parseAndCreateFeatures } from "./parse-and-create-features.js";
|
import { parseAndCreateFeatures } from './parse-and-create-features.js';
|
||||||
import { getAppSpecPath } from "@automaker/platform";
|
import { getAppSpecPath } from '@automaker/platform';
|
||||||
|
|
||||||
const logger = createLogger("SpecRegeneration");
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
const DEFAULT_MAX_FEATURES = 50;
|
const DEFAULT_MAX_FEATURES = 50;
|
||||||
|
|
||||||
@@ -22,28 +22,26 @@ export async function generateFeaturesFromSpec(
|
|||||||
maxFeatures?: number
|
maxFeatures?: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const featureCount = maxFeatures ?? DEFAULT_MAX_FEATURES;
|
const featureCount = maxFeatures ?? DEFAULT_MAX_FEATURES;
|
||||||
logger.debug("========== generateFeaturesFromSpec() started ==========");
|
logger.debug('========== generateFeaturesFromSpec() started ==========');
|
||||||
logger.debug("projectPath:", projectPath);
|
logger.debug('projectPath:', projectPath);
|
||||||
logger.debug("maxFeatures:", featureCount);
|
logger.debug('maxFeatures:', featureCount);
|
||||||
|
|
||||||
// Read existing spec from .automaker directory
|
// Read existing spec from .automaker directory
|
||||||
const specPath = getAppSpecPath(projectPath);
|
const specPath = getAppSpecPath(projectPath);
|
||||||
let spec: string;
|
let spec: string;
|
||||||
|
|
||||||
logger.debug("Reading spec from:", specPath);
|
logger.debug('Reading spec from:', specPath);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
spec = await fs.readFile(specPath, "utf-8");
|
spec = (await secureFs.readFile(specPath, 'utf-8')) as string;
|
||||||
logger.info(`Spec loaded successfully (${spec.length} chars)`);
|
logger.info(`Spec loaded successfully (${spec.length} chars)`);
|
||||||
logger.info(`Spec preview (first 500 chars): ${spec.substring(0, 500)}`);
|
logger.info(`Spec preview (first 500 chars): ${spec.substring(0, 500)}`);
|
||||||
logger.info(
|
logger.info(`Spec preview (last 500 chars): ${spec.substring(spec.length - 500)}`);
|
||||||
`Spec preview (last 500 chars): ${spec.substring(spec.length - 500)}`
|
|
||||||
);
|
|
||||||
} catch (readError) {
|
} catch (readError) {
|
||||||
logger.error("❌ Failed to read spec file:", readError);
|
logger.error('❌ Failed to read spec file:', readError);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_error",
|
type: 'spec_regeneration_error',
|
||||||
error: "No project spec found. Generate spec first.",
|
error: 'No project spec found. Generate spec first.',
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -82,16 +80,14 @@ Generate ${featureCount} features that build on each other logically.
|
|||||||
|
|
||||||
IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`;
|
IMPORTANT: Do not ask for clarification. The specification is provided above. Generate the JSON immediately.`;
|
||||||
|
|
||||||
logger.info("========== PROMPT BEING SENT ==========");
|
logger.info('========== PROMPT BEING SENT ==========');
|
||||||
logger.info(`Prompt length: ${prompt.length} chars`);
|
logger.info(`Prompt length: ${prompt.length} chars`);
|
||||||
logger.info(
|
logger.info(`Prompt preview (first 1000 chars):\n${prompt.substring(0, 1000)}`);
|
||||||
`Prompt preview (first 1000 chars):\n${prompt.substring(0, 1000)}`
|
logger.info('========== END PROMPT PREVIEW ==========');
|
||||||
);
|
|
||||||
logger.info("========== END PROMPT PREVIEW ==========");
|
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_progress",
|
type: 'spec_regeneration_progress',
|
||||||
content: "Analyzing spec and generating features...\n",
|
content: 'Analyzing spec and generating features...\n',
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -100,73 +96,67 @@ IMPORTANT: Do not ask for clarification. The specification is provided above. Ge
|
|||||||
abortController,
|
abortController,
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
|
||||||
logger.info("Calling Claude Agent SDK query() for features...");
|
logger.info('Calling Claude Agent SDK query() for features...');
|
||||||
|
|
||||||
logAuthStatus("Right before SDK query() for features");
|
logAuthStatus('Right before SDK query() for features');
|
||||||
|
|
||||||
let stream;
|
let stream;
|
||||||
try {
|
try {
|
||||||
stream = query({ prompt, options });
|
stream = query({ prompt, options });
|
||||||
logger.debug("query() returned stream successfully");
|
logger.debug('query() returned stream successfully');
|
||||||
} catch (queryError) {
|
} catch (queryError) {
|
||||||
logger.error("❌ query() threw an exception:");
|
logger.error('❌ query() threw an exception:');
|
||||||
logger.error("Error:", queryError);
|
logger.error('Error:', queryError);
|
||||||
throw queryError;
|
throw queryError;
|
||||||
}
|
}
|
||||||
|
|
||||||
let responseText = "";
|
let responseText = '';
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
|
|
||||||
logger.debug("Starting to iterate over feature stream...");
|
logger.debug('Starting to iterate over feature stream...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
logger.debug(
|
logger.debug(
|
||||||
`Feature stream message #${messageCount}:`,
|
`Feature stream message #${messageCount}:`,
|
||||||
JSON.stringify(
|
JSON.stringify({ type: msg.type, subtype: (msg as any).subtype }, null, 2)
|
||||||
{ type: msg.type, subtype: (msg as any).subtype },
|
|
||||||
null,
|
|
||||||
2
|
|
||||||
)
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (msg.type === "assistant" && msg.message.content) {
|
if (msg.type === 'assistant' && msg.message.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text") {
|
if (block.type === 'text') {
|
||||||
responseText += block.text;
|
responseText += block.text;
|
||||||
logger.debug(
|
logger.debug(`Feature text block received (${block.text.length} chars)`);
|
||||||
`Feature text block received (${block.text.length} chars)`
|
events.emit('spec-regeneration:event', {
|
||||||
);
|
type: 'spec_regeneration_progress',
|
||||||
events.emit("spec-regeneration:event", {
|
|
||||||
type: "spec_regeneration_progress",
|
|
||||||
content: block.text,
|
content: block.text,
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === "result" && (msg as any).subtype === "success") {
|
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
|
||||||
logger.debug("Received success result for features");
|
logger.debug('Received success result for features');
|
||||||
responseText = (msg as any).result || responseText;
|
responseText = (msg as any).result || responseText;
|
||||||
} else if ((msg as { type: string }).type === "error") {
|
} else if ((msg as { type: string }).type === 'error') {
|
||||||
logger.error("❌ Received error message from feature stream:");
|
logger.error('❌ Received error message from feature stream:');
|
||||||
logger.error("Error message:", JSON.stringify(msg, null, 2));
|
logger.error('Error message:', JSON.stringify(msg, null, 2));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
logger.error("❌ Error while iterating feature stream:");
|
logger.error('❌ Error while iterating feature stream:');
|
||||||
logger.error("Stream error:", streamError);
|
logger.error('Stream error:', streamError);
|
||||||
throw streamError;
|
throw streamError;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
|
logger.info(`Feature stream complete. Total messages: ${messageCount}`);
|
||||||
logger.info(`Feature response length: ${responseText.length} chars`);
|
logger.info(`Feature response length: ${responseText.length} chars`);
|
||||||
logger.info("========== FULL RESPONSE TEXT ==========");
|
logger.info('========== FULL RESPONSE TEXT ==========');
|
||||||
logger.info(responseText);
|
logger.info(responseText);
|
||||||
logger.info("========== END RESPONSE TEXT ==========");
|
logger.info('========== END RESPONSE TEXT ==========');
|
||||||
|
|
||||||
await parseAndCreateFeatures(projectPath, responseText, events);
|
await parseAndCreateFeatures(projectPath, responseText, events);
|
||||||
|
|
||||||
logger.debug("========== generateFeaturesFromSpec() completed ==========");
|
logger.debug('========== generateFeaturesFromSpec() completed ==========');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,23 +2,23 @@
|
|||||||
* Generate app_spec.txt from project overview
|
* Generate app_spec.txt from project overview
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { query } from "@anthropic-ai/claude-agent-sdk";
|
import { query } from '@anthropic-ai/claude-agent-sdk';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../lib/secure-fs.js';
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import {
|
import {
|
||||||
specOutputSchema,
|
specOutputSchema,
|
||||||
specToXml,
|
specToXml,
|
||||||
getStructuredSpecPromptInstruction,
|
getStructuredSpecPromptInstruction,
|
||||||
type SpecOutput,
|
type SpecOutput,
|
||||||
} from "../../lib/app-spec-format.js";
|
} from '../../lib/app-spec-format.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { createSpecGenerationOptions } from "../../lib/sdk-options.js";
|
import { createSpecGenerationOptions } from '../../lib/sdk-options.js';
|
||||||
import { logAuthStatus } from "./common.js";
|
import { logAuthStatus } from './common.js';
|
||||||
import { generateFeaturesFromSpec } from "./generate-features-from-spec.js";
|
import { generateFeaturesFromSpec } from './generate-features-from-spec.js';
|
||||||
import { ensureAutomakerDir, getAppSpecPath } from "@automaker/platform";
|
import { ensureAutomakerDir, getAppSpecPath } from '@automaker/platform';
|
||||||
|
|
||||||
const logger = createLogger("SpecRegeneration");
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
export async function generateSpec(
|
export async function generateSpec(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
@@ -29,17 +29,17 @@ export async function generateSpec(
|
|||||||
analyzeProject?: boolean,
|
analyzeProject?: boolean,
|
||||||
maxFeatures?: number
|
maxFeatures?: number
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.info("========== generateSpec() started ==========");
|
logger.info('========== generateSpec() started ==========');
|
||||||
logger.info("projectPath:", projectPath);
|
logger.info('projectPath:', projectPath);
|
||||||
logger.info("projectOverview length:", `${projectOverview.length} chars`);
|
logger.info('projectOverview length:', `${projectOverview.length} chars`);
|
||||||
logger.info("projectOverview preview:", projectOverview.substring(0, 300));
|
logger.info('projectOverview preview:', projectOverview.substring(0, 300));
|
||||||
logger.info("generateFeatures:", generateFeatures);
|
logger.info('generateFeatures:', generateFeatures);
|
||||||
logger.info("analyzeProject:", analyzeProject);
|
logger.info('analyzeProject:', analyzeProject);
|
||||||
logger.info("maxFeatures:", maxFeatures);
|
logger.info('maxFeatures:', maxFeatures);
|
||||||
|
|
||||||
// Build the prompt based on whether we should analyze the project
|
// Build the prompt based on whether we should analyze the project
|
||||||
let analysisInstructions = "";
|
let analysisInstructions = '';
|
||||||
let techStackDefaults = "";
|
let techStackDefaults = '';
|
||||||
|
|
||||||
if (analyzeProject !== false) {
|
if (analyzeProject !== false) {
|
||||||
// Default to true - analyze the project
|
// Default to true - analyze the project
|
||||||
@@ -73,114 +73,110 @@ ${analysisInstructions}
|
|||||||
|
|
||||||
${getStructuredSpecPromptInstruction()}`;
|
${getStructuredSpecPromptInstruction()}`;
|
||||||
|
|
||||||
logger.info("========== PROMPT BEING SENT ==========");
|
logger.info('========== PROMPT BEING SENT ==========');
|
||||||
logger.info(`Prompt length: ${prompt.length} chars`);
|
logger.info(`Prompt length: ${prompt.length} chars`);
|
||||||
logger.info(`Prompt preview (first 500 chars):\n${prompt.substring(0, 500)}`);
|
logger.info(`Prompt preview (first 500 chars):\n${prompt.substring(0, 500)}`);
|
||||||
logger.info("========== END PROMPT PREVIEW ==========");
|
logger.info('========== END PROMPT PREVIEW ==========');
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_progress",
|
type: 'spec_progress',
|
||||||
content: "Starting spec generation...\n",
|
content: 'Starting spec generation...\n',
|
||||||
});
|
});
|
||||||
|
|
||||||
const options = createSpecGenerationOptions({
|
const options = createSpecGenerationOptions({
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
abortController,
|
abortController,
|
||||||
outputFormat: {
|
outputFormat: {
|
||||||
type: "json_schema",
|
type: 'json_schema',
|
||||||
schema: specOutputSchema,
|
schema: specOutputSchema,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
logger.debug("SDK Options:", JSON.stringify(options, null, 2));
|
logger.debug('SDK Options:', JSON.stringify(options, null, 2));
|
||||||
logger.info("Calling Claude Agent SDK query()...");
|
logger.info('Calling Claude Agent SDK query()...');
|
||||||
|
|
||||||
// Log auth status right before the SDK call
|
// Log auth status right before the SDK call
|
||||||
logAuthStatus("Right before SDK query()");
|
logAuthStatus('Right before SDK query()');
|
||||||
|
|
||||||
let stream;
|
let stream;
|
||||||
try {
|
try {
|
||||||
stream = query({ prompt, options });
|
stream = query({ prompt, options });
|
||||||
logger.debug("query() returned stream successfully");
|
logger.debug('query() returned stream successfully');
|
||||||
} catch (queryError) {
|
} catch (queryError) {
|
||||||
logger.error("❌ query() threw an exception:");
|
logger.error('❌ query() threw an exception:');
|
||||||
logger.error("Error:", queryError);
|
logger.error('Error:', queryError);
|
||||||
throw queryError;
|
throw queryError;
|
||||||
}
|
}
|
||||||
|
|
||||||
let responseText = "";
|
let responseText = '';
|
||||||
let messageCount = 0;
|
let messageCount = 0;
|
||||||
let structuredOutput: SpecOutput | null = null;
|
let structuredOutput: SpecOutput | null = null;
|
||||||
|
|
||||||
logger.info("Starting to iterate over stream...");
|
logger.info('Starting to iterate over stream...');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
messageCount++;
|
messageCount++;
|
||||||
logger.info(
|
logger.info(
|
||||||
`Stream message #${messageCount}: type=${msg.type}, subtype=${
|
`Stream message #${messageCount}: type=${msg.type}, subtype=${(msg as any).subtype}`
|
||||||
(msg as any).subtype
|
|
||||||
}`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
if (msg.type === "assistant") {
|
if (msg.type === 'assistant') {
|
||||||
const msgAny = msg as any;
|
const msgAny = msg as any;
|
||||||
if (msgAny.message?.content) {
|
if (msgAny.message?.content) {
|
||||||
for (const block of msgAny.message.content) {
|
for (const block of msgAny.message.content) {
|
||||||
if (block.type === "text") {
|
if (block.type === 'text') {
|
||||||
responseText += block.text;
|
responseText += block.text;
|
||||||
logger.info(
|
logger.info(
|
||||||
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
`Text block received (${block.text.length} chars), total now: ${responseText.length} chars`
|
||||||
);
|
);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_progress",
|
type: 'spec_regeneration_progress',
|
||||||
content: block.text,
|
content: block.text,
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
} else if (block.type === "tool_use") {
|
} else if (block.type === 'tool_use') {
|
||||||
logger.info("Tool use:", block.name);
|
logger.info('Tool use:', block.name);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_tool",
|
type: 'spec_tool',
|
||||||
tool: block.name,
|
tool: block.name,
|
||||||
input: block.input,
|
input: block.input,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === "result" && (msg as any).subtype === "success") {
|
} else if (msg.type === 'result' && (msg as any).subtype === 'success') {
|
||||||
logger.info("Received success result");
|
logger.info('Received success result');
|
||||||
// Check for structured output - this is the reliable way to get spec data
|
// Check for structured output - this is the reliable way to get spec data
|
||||||
const resultMsg = msg as any;
|
const resultMsg = msg as any;
|
||||||
if (resultMsg.structured_output) {
|
if (resultMsg.structured_output) {
|
||||||
structuredOutput = resultMsg.structured_output as SpecOutput;
|
structuredOutput = resultMsg.structured_output as SpecOutput;
|
||||||
logger.info("✅ Received structured output");
|
logger.info('✅ Received structured output');
|
||||||
logger.debug("Structured output:", JSON.stringify(structuredOutput, null, 2));
|
logger.debug('Structured output:', JSON.stringify(structuredOutput, null, 2));
|
||||||
} else {
|
} else {
|
||||||
logger.warn("⚠️ No structured output in result, will fall back to text parsing");
|
logger.warn('⚠️ No structured output in result, will fall back to text parsing');
|
||||||
}
|
}
|
||||||
} else if (msg.type === "result") {
|
} else if (msg.type === 'result') {
|
||||||
// Handle error result types
|
// Handle error result types
|
||||||
const subtype = (msg as any).subtype;
|
const subtype = (msg as any).subtype;
|
||||||
logger.info(`Result message: subtype=${subtype}`);
|
logger.info(`Result message: subtype=${subtype}`);
|
||||||
if (subtype === "error_max_turns") {
|
if (subtype === 'error_max_turns') {
|
||||||
logger.error("❌ Hit max turns limit!");
|
logger.error('❌ Hit max turns limit!');
|
||||||
} else if (subtype === "error_max_structured_output_retries") {
|
} else if (subtype === 'error_max_structured_output_retries') {
|
||||||
logger.error("❌ Failed to produce valid structured output after retries");
|
logger.error('❌ Failed to produce valid structured output after retries');
|
||||||
throw new Error("Could not produce valid spec output");
|
throw new Error('Could not produce valid spec output');
|
||||||
}
|
}
|
||||||
} else if ((msg as { type: string }).type === "error") {
|
} else if ((msg as { type: string }).type === 'error') {
|
||||||
logger.error("❌ Received error message from stream:");
|
logger.error('❌ Received error message from stream:');
|
||||||
logger.error("Error message:", JSON.stringify(msg, null, 2));
|
logger.error('Error message:', JSON.stringify(msg, null, 2));
|
||||||
} else if (msg.type === "user") {
|
} else if (msg.type === 'user') {
|
||||||
// Log user messages (tool results)
|
// Log user messages (tool results)
|
||||||
logger.info(
|
logger.info(`User message (tool result): ${JSON.stringify(msg).substring(0, 500)}`);
|
||||||
`User message (tool result): ${JSON.stringify(msg).substring(0, 500)}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (streamError) {
|
} catch (streamError) {
|
||||||
logger.error("❌ Error while iterating stream:");
|
logger.error('❌ Error while iterating stream:');
|
||||||
logger.error("Stream error:", streamError);
|
logger.error('Stream error:', streamError);
|
||||||
throw streamError;
|
throw streamError;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,40 +188,42 @@ ${getStructuredSpecPromptInstruction()}`;
|
|||||||
|
|
||||||
if (structuredOutput) {
|
if (structuredOutput) {
|
||||||
// Use structured output - convert JSON to XML
|
// Use structured output - convert JSON to XML
|
||||||
logger.info("✅ Using structured output for XML generation");
|
logger.info('✅ Using structured output for XML generation');
|
||||||
xmlContent = specToXml(structuredOutput);
|
xmlContent = specToXml(structuredOutput);
|
||||||
logger.info(`Generated XML from structured output: ${xmlContent.length} chars`);
|
logger.info(`Generated XML from structured output: ${xmlContent.length} chars`);
|
||||||
} else {
|
} else {
|
||||||
// Fallback: Extract XML content from response text
|
// Fallback: Extract XML content from response text
|
||||||
// Claude might include conversational text before/after
|
// Claude might include conversational text before/after
|
||||||
// See: https://github.com/AutoMaker-Org/automaker/issues/149
|
// See: https://github.com/AutoMaker-Org/automaker/issues/149
|
||||||
logger.warn("⚠️ No structured output, falling back to text parsing");
|
logger.warn('⚠️ No structured output, falling back to text parsing');
|
||||||
logger.info("========== FINAL RESPONSE TEXT ==========");
|
logger.info('========== FINAL RESPONSE TEXT ==========');
|
||||||
logger.info(responseText || "(empty)");
|
logger.info(responseText || '(empty)');
|
||||||
logger.info("========== END RESPONSE TEXT ==========");
|
logger.info('========== END RESPONSE TEXT ==========');
|
||||||
|
|
||||||
if (!responseText || responseText.trim().length === 0) {
|
if (!responseText || responseText.trim().length === 0) {
|
||||||
throw new Error("No response text and no structured output - cannot generate spec");
|
throw new Error('No response text and no structured output - cannot generate spec');
|
||||||
}
|
}
|
||||||
|
|
||||||
const xmlStart = responseText.indexOf("<project_specification>");
|
const xmlStart = responseText.indexOf('<project_specification>');
|
||||||
const xmlEnd = responseText.lastIndexOf("</project_specification>");
|
const xmlEnd = responseText.lastIndexOf('</project_specification>');
|
||||||
|
|
||||||
if (xmlStart !== -1 && xmlEnd !== -1) {
|
if (xmlStart !== -1 && xmlEnd !== -1) {
|
||||||
// Extract just the XML content, discarding any conversational text before/after
|
// Extract just the XML content, discarding any conversational text before/after
|
||||||
xmlContent = responseText.substring(xmlStart, xmlEnd + "</project_specification>".length);
|
xmlContent = responseText.substring(xmlStart, xmlEnd + '</project_specification>'.length);
|
||||||
logger.info(`Extracted XML content: ${xmlContent.length} chars (from position ${xmlStart})`);
|
logger.info(`Extracted XML content: ${xmlContent.length} chars (from position ${xmlStart})`);
|
||||||
} else {
|
} else {
|
||||||
// No valid XML structure found in the response text
|
// No valid XML structure found in the response text
|
||||||
// This happens when structured output was expected but not received, and the agent
|
// This happens when structured output was expected but not received, and the agent
|
||||||
// output conversational text instead of XML (e.g., "The project directory appears to be empty...")
|
// output conversational text instead of XML (e.g., "The project directory appears to be empty...")
|
||||||
// We should NOT save this conversational text as it's not a valid spec
|
// We should NOT save this conversational text as it's not a valid spec
|
||||||
logger.error("❌ Response does not contain valid <project_specification> XML structure");
|
logger.error('❌ Response does not contain valid <project_specification> XML structure');
|
||||||
logger.error("This typically happens when structured output failed and the agent produced conversational text instead of XML");
|
logger.error(
|
||||||
|
'This typically happens when structured output failed and the agent produced conversational text instead of XML'
|
||||||
|
);
|
||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to generate spec: No valid XML structure found in response. " +
|
'Failed to generate spec: No valid XML structure found in response. ' +
|
||||||
"The response contained conversational text but no <project_specification> tags. " +
|
'The response contained conversational text but no <project_specification> tags. ' +
|
||||||
"Please try again."
|
'Please try again.'
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -234,60 +232,55 @@ ${getStructuredSpecPromptInstruction()}`;
|
|||||||
await ensureAutomakerDir(projectPath);
|
await ensureAutomakerDir(projectPath);
|
||||||
const specPath = getAppSpecPath(projectPath);
|
const specPath = getAppSpecPath(projectPath);
|
||||||
|
|
||||||
logger.info("Saving spec to:", specPath);
|
logger.info('Saving spec to:', specPath);
|
||||||
logger.info(`Content to save (${xmlContent.length} chars)`);
|
logger.info(`Content to save (${xmlContent.length} chars)`);
|
||||||
|
|
||||||
await fs.writeFile(specPath, xmlContent);
|
await secureFs.writeFile(specPath, xmlContent);
|
||||||
|
|
||||||
// Verify the file was written
|
// Verify the file was written
|
||||||
const savedContent = await fs.readFile(specPath, "utf-8");
|
const savedContent = await secureFs.readFile(specPath, 'utf-8');
|
||||||
logger.info(`Verified saved file: ${savedContent.length} chars`);
|
logger.info(`Verified saved file: ${savedContent.length} chars`);
|
||||||
if (savedContent.length === 0) {
|
if (savedContent.length === 0) {
|
||||||
logger.error("❌ File was saved but is empty!");
|
logger.error('❌ File was saved but is empty!');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("Spec saved successfully");
|
logger.info('Spec saved successfully');
|
||||||
|
|
||||||
// Emit spec completion event
|
// Emit spec completion event
|
||||||
if (generateFeatures) {
|
if (generateFeatures) {
|
||||||
// If features will be generated, emit intermediate completion
|
// If features will be generated, emit intermediate completion
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_progress",
|
type: 'spec_regeneration_progress',
|
||||||
content: "[Phase: spec_complete] Spec created! Generating features...\n",
|
content: '[Phase: spec_complete] Spec created! Generating features...\n',
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
// If no features, emit final completion
|
// If no features, emit final completion
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_complete",
|
type: 'spec_regeneration_complete',
|
||||||
message: "Spec regeneration complete!",
|
message: 'Spec regeneration complete!',
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// If generate features was requested, generate them from the spec
|
// If generate features was requested, generate them from the spec
|
||||||
if (generateFeatures) {
|
if (generateFeatures) {
|
||||||
logger.info("Starting feature generation from spec...");
|
logger.info('Starting feature generation from spec...');
|
||||||
// Create a new abort controller for feature generation
|
// Create a new abort controller for feature generation
|
||||||
const featureAbortController = new AbortController();
|
const featureAbortController = new AbortController();
|
||||||
try {
|
try {
|
||||||
await generateFeaturesFromSpec(
|
await generateFeaturesFromSpec(projectPath, events, featureAbortController, maxFeatures);
|
||||||
projectPath,
|
|
||||||
events,
|
|
||||||
featureAbortController,
|
|
||||||
maxFeatures
|
|
||||||
);
|
|
||||||
// Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures
|
// Final completion will be emitted by generateFeaturesFromSpec -> parseAndCreateFeatures
|
||||||
} catch (featureError) {
|
} catch (featureError) {
|
||||||
logger.error("Feature generation failed:", featureError);
|
logger.error('Feature generation failed:', featureError);
|
||||||
// Don't throw - spec generation succeeded, feature generation is optional
|
// Don't throw - spec generation succeeded, feature generation is optional
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_error",
|
type: 'spec_regeneration_error',
|
||||||
error: (featureError as Error).message || "Feature generation failed",
|
error: (featureError as Error).message || 'Feature generation failed',
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("========== generateSpec() completed ==========");
|
logger.debug('========== generateSpec() completed ==========');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,71 +2,71 @@
|
|||||||
* Parse agent response and create feature files
|
* Parse agent response and create feature files
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../lib/secure-fs.js';
|
||||||
import type { EventEmitter } from "../../lib/events.js";
|
import type { EventEmitter } from '../../lib/events.js';
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { getFeaturesDir } from "@automaker/platform";
|
import { getFeaturesDir } from '@automaker/platform';
|
||||||
|
|
||||||
const logger = createLogger("SpecRegeneration");
|
const logger = createLogger('SpecRegeneration');
|
||||||
|
|
||||||
export async function parseAndCreateFeatures(
|
export async function parseAndCreateFeatures(
|
||||||
projectPath: string,
|
projectPath: string,
|
||||||
content: string,
|
content: string,
|
||||||
events: EventEmitter
|
events: EventEmitter
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
logger.info("========== parseAndCreateFeatures() started ==========");
|
logger.info('========== parseAndCreateFeatures() started ==========');
|
||||||
logger.info(`Content length: ${content.length} chars`);
|
logger.info(`Content length: ${content.length} chars`);
|
||||||
logger.info("========== CONTENT RECEIVED FOR PARSING ==========");
|
logger.info('========== CONTENT RECEIVED FOR PARSING ==========');
|
||||||
logger.info(content);
|
logger.info(content);
|
||||||
logger.info("========== END CONTENT ==========");
|
logger.info('========== END CONTENT ==========');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Extract JSON from response
|
// Extract JSON from response
|
||||||
logger.info("Extracting JSON from response...");
|
logger.info('Extracting JSON from response...');
|
||||||
logger.info(`Looking for pattern: /{[\\s\\S]*"features"[\\s\\S]*}/`);
|
logger.info(`Looking for pattern: /{[\\s\\S]*"features"[\\s\\S]*}/`);
|
||||||
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
const jsonMatch = content.match(/\{[\s\S]*"features"[\s\S]*\}/);
|
||||||
if (!jsonMatch) {
|
if (!jsonMatch) {
|
||||||
logger.error("❌ No valid JSON found in response");
|
logger.error('❌ No valid JSON found in response');
|
||||||
logger.error("Full content received:");
|
logger.error('Full content received:');
|
||||||
logger.error(content);
|
logger.error(content);
|
||||||
throw new Error("No valid JSON found in response");
|
throw new Error('No valid JSON found in response');
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info(`JSON match found (${jsonMatch[0].length} chars)`);
|
logger.info(`JSON match found (${jsonMatch[0].length} chars)`);
|
||||||
logger.info("========== MATCHED JSON ==========");
|
logger.info('========== MATCHED JSON ==========');
|
||||||
logger.info(jsonMatch[0]);
|
logger.info(jsonMatch[0]);
|
||||||
logger.info("========== END MATCHED JSON ==========");
|
logger.info('========== END MATCHED JSON ==========');
|
||||||
|
|
||||||
const parsed = JSON.parse(jsonMatch[0]);
|
const parsed = JSON.parse(jsonMatch[0]);
|
||||||
logger.info(`Parsed ${parsed.features?.length || 0} features`);
|
logger.info(`Parsed ${parsed.features?.length || 0} features`);
|
||||||
logger.info("Parsed features:", JSON.stringify(parsed.features, null, 2));
|
logger.info('Parsed features:', JSON.stringify(parsed.features, null, 2));
|
||||||
|
|
||||||
const featuresDir = getFeaturesDir(projectPath);
|
const featuresDir = getFeaturesDir(projectPath);
|
||||||
await fs.mkdir(featuresDir, { recursive: true });
|
await secureFs.mkdir(featuresDir, { recursive: true });
|
||||||
|
|
||||||
const createdFeatures: Array<{ id: string; title: string }> = [];
|
const createdFeatures: Array<{ id: string; title: string }> = [];
|
||||||
|
|
||||||
for (const feature of parsed.features) {
|
for (const feature of parsed.features) {
|
||||||
logger.debug("Creating feature:", feature.id);
|
logger.debug('Creating feature:', feature.id);
|
||||||
const featureDir = path.join(featuresDir, feature.id);
|
const featureDir = path.join(featuresDir, feature.id);
|
||||||
await fs.mkdir(featureDir, { recursive: true });
|
await secureFs.mkdir(featureDir, { recursive: true });
|
||||||
|
|
||||||
const featureData = {
|
const featureData = {
|
||||||
id: feature.id,
|
id: feature.id,
|
||||||
category: feature.category || "Uncategorized",
|
category: feature.category || 'Uncategorized',
|
||||||
title: feature.title,
|
title: feature.title,
|
||||||
description: feature.description,
|
description: feature.description,
|
||||||
status: "backlog", // Features go to backlog - user must manually start them
|
status: 'backlog', // Features go to backlog - user must manually start them
|
||||||
priority: feature.priority || 2,
|
priority: feature.priority || 2,
|
||||||
complexity: feature.complexity || "moderate",
|
complexity: feature.complexity || 'moderate',
|
||||||
dependencies: feature.dependencies || [],
|
dependencies: feature.dependencies || [],
|
||||||
createdAt: new Date().toISOString(),
|
createdAt: new Date().toISOString(),
|
||||||
updatedAt: new Date().toISOString(),
|
updatedAt: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
|
|
||||||
await fs.writeFile(
|
await secureFs.writeFile(
|
||||||
path.join(featureDir, "feature.json"),
|
path.join(featureDir, 'feature.json'),
|
||||||
JSON.stringify(featureData, null, 2)
|
JSON.stringify(featureData, null, 2)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -75,20 +75,20 @@ export async function parseAndCreateFeatures(
|
|||||||
|
|
||||||
logger.info(`✓ Created ${createdFeatures.length} features successfully`);
|
logger.info(`✓ Created ${createdFeatures.length} features successfully`);
|
||||||
|
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_complete",
|
type: 'spec_regeneration_complete',
|
||||||
message: `Spec regeneration complete! Created ${createdFeatures.length} features.`,
|
message: `Spec regeneration complete! Created ${createdFeatures.length} features.`,
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("❌ parseAndCreateFeatures() failed:");
|
logger.error('❌ parseAndCreateFeatures() failed:');
|
||||||
logger.error("Error:", error);
|
logger.error('Error:', error);
|
||||||
events.emit("spec-regeneration:event", {
|
events.emit('spec-regeneration:event', {
|
||||||
type: "spec_regeneration_error",
|
type: 'spec_regeneration_error',
|
||||||
error: (error as Error).message,
|
error: (error as Error).message,
|
||||||
projectPath: projectPath,
|
projectPath: projectPath,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.debug("========== parseAndCreateFeatures() completed ==========");
|
logger.debug('========== parseAndCreateFeatures() completed ==========');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,16 +2,12 @@
|
|||||||
* POST /browse endpoint - Browse directories for file browser UI
|
* POST /browse endpoint - Browse directories for file browser UI
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import os from "os";
|
import os from 'os';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import {
|
import { getAllowedRootDirectory, PathNotAllowedError } from '@automaker/platform';
|
||||||
getAllowedRootDirectory,
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
isPathAllowed,
|
|
||||||
PathNotAllowedError,
|
|
||||||
} from "@automaker/platform";
|
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
|
||||||
|
|
||||||
export function createBrowseHandler() {
|
export function createBrowseHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -22,24 +18,19 @@ export function createBrowseHandler() {
|
|||||||
const defaultPath = getAllowedRootDirectory() || os.homedir();
|
const defaultPath = getAllowedRootDirectory() || os.homedir();
|
||||||
const targetPath = dirPath ? path.resolve(dirPath) : defaultPath;
|
const targetPath = dirPath ? path.resolve(dirPath) : defaultPath;
|
||||||
|
|
||||||
// Validate that the path is allowed
|
|
||||||
if (!isPathAllowed(targetPath)) {
|
|
||||||
throw new PathNotAllowedError(dirPath || targetPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Detect available drives on Windows
|
// Detect available drives on Windows
|
||||||
const detectDrives = async (): Promise<string[]> => {
|
const detectDrives = async (): Promise<string[]> => {
|
||||||
if (os.platform() !== "win32") {
|
if (os.platform() !== 'win32') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
const drives: string[] = [];
|
const drives: string[] = [];
|
||||||
const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
const letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
|
||||||
|
|
||||||
for (const letter of letters) {
|
for (const letter of letters) {
|
||||||
const drivePath = `${letter}:\\`;
|
const drivePath = `${letter}:\\`;
|
||||||
try {
|
try {
|
||||||
await fs.access(drivePath);
|
await secureFs.access(drivePath);
|
||||||
drives.push(drivePath);
|
drives.push(drivePath);
|
||||||
} catch {
|
} catch {
|
||||||
// Drive doesn't exist, skip it
|
// Drive doesn't exist, skip it
|
||||||
@@ -57,21 +48,19 @@ export function createBrowseHandler() {
|
|||||||
const drives = await detectDrives();
|
const drives = await detectDrives();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const stats = await fs.stat(targetPath);
|
const stats = await secureFs.stat(targetPath);
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'Path is not a directory' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "Path is not a directory" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read directory contents
|
// Read directory contents
|
||||||
const entries = await fs.readdir(targetPath, { withFileTypes: true });
|
const entries = await secureFs.readdir(targetPath, { withFileTypes: true });
|
||||||
|
|
||||||
// Filter for directories only and add parent directory option
|
// Filter for directories only and add parent directory option
|
||||||
const directories = entries
|
const directories = entries
|
||||||
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
path: path.join(targetPath, entry.name),
|
path: path.join(targetPath, entry.name),
|
||||||
@@ -87,10 +76,8 @@ export function createBrowseHandler() {
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle permission errors gracefully - still return path info so user can navigate away
|
// Handle permission errors gracefully - still return path info so user can navigate away
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : 'Failed to read directory';
|
||||||
error instanceof Error ? error.message : "Failed to read directory";
|
const isPermissionError = errorMessage.includes('EPERM') || errorMessage.includes('EACCES');
|
||||||
const isPermissionError =
|
|
||||||
errorMessage.includes("EPERM") || errorMessage.includes("EACCES");
|
|
||||||
|
|
||||||
if (isPermissionError) {
|
if (isPermissionError) {
|
||||||
// Return success with empty directories so user can still navigate to parent
|
// Return success with empty directories so user can still navigate to parent
|
||||||
@@ -101,7 +88,7 @@ export function createBrowseHandler() {
|
|||||||
directories: [],
|
directories: [],
|
||||||
drives,
|
drives,
|
||||||
warning:
|
warning:
|
||||||
"Permission denied - grant Full Disk Access to Terminal in System Preferences > Privacy & Security",
|
'Permission denied - grant Full Disk Access to Terminal in System Preferences > Privacy & Security',
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
@@ -117,7 +104,7 @@ export function createBrowseHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logError(error, "Browse directories failed");
|
logError(error, 'Browse directories failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* POST /delete-board-background endpoint - Delete board background image
|
* POST /delete-board-background endpoint - Delete board background image
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getBoardDir } from "@automaker/platform";
|
import { getBoardDir } from '@automaker/platform';
|
||||||
|
|
||||||
export function createDeleteBoardBackgroundHandler() {
|
export function createDeleteBoardBackgroundHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -16,7 +16,7 @@ export function createDeleteBoardBackgroundHandler() {
|
|||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath is required",
|
error: 'projectPath is required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -26,10 +26,10 @@ export function createDeleteBoardBackgroundHandler() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to remove all background files in the board directory
|
// Try to remove all background files in the board directory
|
||||||
const files = await fs.readdir(boardDir);
|
const files = await secureFs.readdir(boardDir);
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
if (file.startsWith("background")) {
|
if (file.startsWith('background')) {
|
||||||
await fs.unlink(path.join(boardDir, file));
|
await secureFs.unlink(path.join(boardDir, file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@@ -38,7 +38,7 @@ export function createDeleteBoardBackgroundHandler() {
|
|||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Delete board background failed");
|
logError(error, 'Delete board background failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
* POST /delete endpoint - Delete file
|
* POST /delete endpoint - Delete file
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { validatePath, PathNotAllowedError } from "@automaker/platform";
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createDeleteHandler() {
|
export function createDeleteHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -13,12 +13,11 @@ export function createDeleteHandler() {
|
|||||||
const { filePath } = req.body as { filePath: string };
|
const { filePath } = req.body as { filePath: string };
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
res.status(400).json({ success: false, error: "filePath is required" });
|
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = validatePath(filePath);
|
await secureFs.rm(filePath, { recursive: true });
|
||||||
await fs.rm(resolvedPath, { recursive: true });
|
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -28,7 +27,7 @@ export function createDeleteHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logError(error, "Delete file failed");
|
logError(error, 'Delete file failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
* POST /exists endpoint - Check if file/directory exists
|
* POST /exists endpoint - Check if file/directory exists
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import { isPathAllowed, PathNotAllowedError } from "@automaker/platform";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
|
||||||
|
|
||||||
export function createExistsHandler() {
|
export function createExistsHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -14,21 +13,18 @@ export function createExistsHandler() {
|
|||||||
const { filePath } = req.body as { filePath: string };
|
const { filePath } = req.body as { filePath: string };
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
res.status(400).json({ success: false, error: "filePath is required" });
|
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = path.resolve(filePath);
|
|
||||||
|
|
||||||
// Validate that the path is allowed
|
|
||||||
if (!isPathAllowed(resolvedPath)) {
|
|
||||||
throw new PathNotAllowedError(filePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.access(resolvedPath);
|
await secureFs.access(filePath);
|
||||||
res.json({ success: true, exists: true });
|
res.json({ success: true, exists: true });
|
||||||
} catch {
|
} catch (accessError) {
|
||||||
|
// Check if it's a path not allowed error vs file not existing
|
||||||
|
if (accessError instanceof PathNotAllowedError) {
|
||||||
|
throw accessError;
|
||||||
|
}
|
||||||
res.json({ success: true, exists: false });
|
res.json({ success: true, exists: false });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -38,7 +34,7 @@ export function createExistsHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logError(error, "Check exists failed");
|
logError(error, 'Check exists failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,11 @@
|
|||||||
* GET /image endpoint - Serve image files
|
* GET /image endpoint - Serve image files
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createImageHandler() {
|
export function createImageHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -16,7 +17,7 @@ export function createImageHandler() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!imagePath) {
|
if (!imagePath) {
|
||||||
res.status(400).json({ success: false, error: "path is required" });
|
res.status(400).json({ success: false, error: 'path is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,40 +25,41 @@ export function createImageHandler() {
|
|||||||
const fullPath = path.isAbsolute(imagePath)
|
const fullPath = path.isAbsolute(imagePath)
|
||||||
? imagePath
|
? imagePath
|
||||||
: projectPath
|
: projectPath
|
||||||
? path.join(projectPath, imagePath)
|
? path.join(projectPath, imagePath)
|
||||||
: imagePath;
|
: imagePath;
|
||||||
|
|
||||||
// Check if file exists
|
// Check if file exists
|
||||||
try {
|
try {
|
||||||
await fs.access(fullPath);
|
await secureFs.access(fullPath);
|
||||||
} catch {
|
} catch (accessError) {
|
||||||
res.status(404).json({ success: false, error: "Image not found" });
|
if (accessError instanceof PathNotAllowedError) {
|
||||||
|
res.status(403).json({ success: false, error: 'Path not allowed' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
res.status(404).json({ success: false, error: 'Image not found' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the file
|
// Read the file
|
||||||
const buffer = await fs.readFile(fullPath);
|
const buffer = await secureFs.readFile(fullPath);
|
||||||
|
|
||||||
// Determine MIME type from extension
|
// Determine MIME type from extension
|
||||||
const ext = path.extname(fullPath).toLowerCase();
|
const ext = path.extname(fullPath).toLowerCase();
|
||||||
const mimeTypes: Record<string, string> = {
|
const mimeTypes: Record<string, string> = {
|
||||||
".png": "image/png",
|
'.png': 'image/png',
|
||||||
".jpg": "image/jpeg",
|
'.jpg': 'image/jpeg',
|
||||||
".jpeg": "image/jpeg",
|
'.jpeg': 'image/jpeg',
|
||||||
".gif": "image/gif",
|
'.gif': 'image/gif',
|
||||||
".webp": "image/webp",
|
'.webp': 'image/webp',
|
||||||
".svg": "image/svg+xml",
|
'.svg': 'image/svg+xml',
|
||||||
".bmp": "image/bmp",
|
'.bmp': 'image/bmp',
|
||||||
};
|
};
|
||||||
|
|
||||||
res.setHeader(
|
res.setHeader('Content-Type', mimeTypes[ext] || 'application/octet-stream');
|
||||||
"Content-Type",
|
res.setHeader('Cache-Control', 'public, max-age=3600');
|
||||||
mimeTypes[ext] || "application/octet-stream"
|
|
||||||
);
|
|
||||||
res.setHeader("Cache-Control", "public, max-age=3600");
|
|
||||||
res.send(buffer);
|
res.send(buffer);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Serve image failed");
|
logError(error, 'Serve image failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
* Handles symlinks safely to avoid ELOOP errors
|
* Handles symlinks safely to avoid ELOOP errors
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { isPathAllowed, PathNotAllowedError } from "@automaker/platform";
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createMkdirHandler() {
|
export function createMkdirHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -15,20 +15,15 @@ export function createMkdirHandler() {
|
|||||||
const { dirPath } = req.body as { dirPath: string };
|
const { dirPath } = req.body as { dirPath: string };
|
||||||
|
|
||||||
if (!dirPath) {
|
if (!dirPath) {
|
||||||
res.status(400).json({ success: false, error: "dirPath is required" });
|
res.status(400).json({ success: false, error: 'dirPath is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = path.resolve(dirPath);
|
const resolvedPath = path.resolve(dirPath);
|
||||||
|
|
||||||
// Validate that the path is allowed
|
|
||||||
if (!isPathAllowed(resolvedPath)) {
|
|
||||||
throw new PathNotAllowedError(dirPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if path already exists using lstat (doesn't follow symlinks)
|
// Check if path already exists using lstat (doesn't follow symlinks)
|
||||||
try {
|
try {
|
||||||
const stats = await fs.lstat(resolvedPath);
|
const stats = await secureFs.lstat(resolvedPath);
|
||||||
// Path exists - if it's a directory or symlink, consider it success
|
// Path exists - if it's a directory or symlink, consider it success
|
||||||
if (stats.isDirectory() || stats.isSymbolicLink()) {
|
if (stats.isDirectory() || stats.isSymbolicLink()) {
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
@@ -37,19 +32,19 @@ export function createMkdirHandler() {
|
|||||||
// It's a file - can't create directory
|
// It's a file - can't create directory
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Path exists and is not a directory",
|
error: 'Path exists and is not a directory',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch (statError: any) {
|
} catch (statError: any) {
|
||||||
// ENOENT means path doesn't exist - we should create it
|
// ENOENT means path doesn't exist - we should create it
|
||||||
if (statError.code !== "ENOENT") {
|
if (statError.code !== 'ENOENT') {
|
||||||
// Some other error (could be ELOOP in parent path)
|
// Some other error (could be ELOOP in parent path)
|
||||||
throw statError;
|
throw statError;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path doesn't exist, create it
|
// Path doesn't exist, create it
|
||||||
await fs.mkdir(resolvedPath, { recursive: true });
|
await secureFs.mkdir(resolvedPath, { recursive: true });
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
@@ -60,15 +55,15 @@ export function createMkdirHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Handle ELOOP specifically
|
// Handle ELOOP specifically
|
||||||
if (error.code === "ELOOP") {
|
if (error.code === 'ELOOP') {
|
||||||
logError(error, "Create directory failed - symlink loop detected");
|
logError(error, 'Create directory failed - symlink loop detected');
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Cannot create directory: symlink loop detected in path",
|
error: 'Cannot create directory: symlink loop detected in path',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
logError(error, "Create directory failed");
|
logError(error, 'Create directory failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,26 +2,21 @@
|
|||||||
* POST /read endpoint - Read file
|
* POST /read endpoint - Read file
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { validatePath, PathNotAllowedError } from "@automaker/platform";
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
// Optional files that are expected to not exist in new projects
|
// Optional files that are expected to not exist in new projects
|
||||||
// Don't log ENOENT errors for these to reduce noise
|
// Don't log ENOENT errors for these to reduce noise
|
||||||
const OPTIONAL_FILES = ["categories.json", "app_spec.txt"];
|
const OPTIONAL_FILES = ['categories.json', 'app_spec.txt'];
|
||||||
|
|
||||||
function isOptionalFile(filePath: string): boolean {
|
function isOptionalFile(filePath: string): boolean {
|
||||||
return OPTIONAL_FILES.some((optionalFile) => filePath.endsWith(optionalFile));
|
return OPTIONAL_FILES.some((optionalFile) => filePath.endsWith(optionalFile));
|
||||||
}
|
}
|
||||||
|
|
||||||
function isENOENT(error: unknown): boolean {
|
function isENOENT(error: unknown): boolean {
|
||||||
return (
|
return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
|
||||||
error !== null &&
|
|
||||||
typeof error === "object" &&
|
|
||||||
"code" in error &&
|
|
||||||
error.code === "ENOENT"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createReadHandler() {
|
export function createReadHandler() {
|
||||||
@@ -30,12 +25,11 @@ export function createReadHandler() {
|
|||||||
const { filePath } = req.body as { filePath: string };
|
const { filePath } = req.body as { filePath: string };
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
res.status(400).json({ success: false, error: "filePath is required" });
|
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = validatePath(filePath);
|
const content = await secureFs.readFile(filePath, 'utf-8');
|
||||||
const content = await fs.readFile(resolvedPath, "utf-8");
|
|
||||||
|
|
||||||
res.json({ success: true, content });
|
res.json({ success: true, content });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -46,9 +40,9 @@ export function createReadHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Don't log ENOENT errors for optional files (expected to be missing in new projects)
|
// Don't log ENOENT errors for optional files (expected to be missing in new projects)
|
||||||
const shouldLog = !(isENOENT(error) && isOptionalFile(req.body?.filePath || ""));
|
const shouldLog = !(isENOENT(error) && isOptionalFile(req.body?.filePath || ''));
|
||||||
if (shouldLog) {
|
if (shouldLog) {
|
||||||
logError(error, "Read file failed");
|
logError(error, 'Read file failed');
|
||||||
}
|
}
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
* POST /readdir endpoint - Read directory
|
* POST /readdir endpoint - Read directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { validatePath, PathNotAllowedError } from "@automaker/platform";
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createReaddirHandler() {
|
export function createReaddirHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -13,12 +13,11 @@ export function createReaddirHandler() {
|
|||||||
const { dirPath } = req.body as { dirPath: string };
|
const { dirPath } = req.body as { dirPath: string };
|
||||||
|
|
||||||
if (!dirPath) {
|
if (!dirPath) {
|
||||||
res.status(400).json({ success: false, error: "dirPath is required" });
|
res.status(400).json({ success: false, error: 'dirPath is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = validatePath(dirPath);
|
const entries = await secureFs.readdir(dirPath, { withFileTypes: true });
|
||||||
const entries = await fs.readdir(resolvedPath, { withFileTypes: true });
|
|
||||||
|
|
||||||
const result = entries.map((entry) => ({
|
const result = entries.map((entry) => ({
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
@@ -34,7 +33,7 @@ export function createReaddirHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logError(error, "Read directory failed");
|
logError(error, 'Read directory failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
* POST /resolve-directory endpoint - Resolve directory path from directory name
|
* POST /resolve-directory endpoint - Resolve directory path from directory name
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createResolveDirectoryHandler() {
|
export function createResolveDirectoryHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -17,9 +17,7 @@ export function createResolveDirectoryHandler() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!directoryName) {
|
if (!directoryName) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'directoryName is required' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "directoryName is required" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -27,7 +25,7 @@ export function createResolveDirectoryHandler() {
|
|||||||
if (path.isAbsolute(directoryName) || directoryName.includes(path.sep)) {
|
if (path.isAbsolute(directoryName) || directoryName.includes(path.sep)) {
|
||||||
try {
|
try {
|
||||||
const resolvedPath = path.resolve(directoryName);
|
const resolvedPath = path.resolve(directoryName);
|
||||||
const stats = await fs.stat(resolvedPath);
|
const stats = await secureFs.stat(resolvedPath);
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -43,17 +41,11 @@ export function createResolveDirectoryHandler() {
|
|||||||
// Search for directory in common locations
|
// Search for directory in common locations
|
||||||
const searchPaths: string[] = [
|
const searchPaths: string[] = [
|
||||||
process.cwd(), // Current working directory
|
process.cwd(), // Current working directory
|
||||||
process.env.HOME || process.env.USERPROFILE || "", // User home
|
process.env.HOME || process.env.USERPROFILE || '', // User home
|
||||||
path.join(
|
path.join(process.env.HOME || process.env.USERPROFILE || '', 'Documents'),
|
||||||
process.env.HOME || process.env.USERPROFILE || "",
|
path.join(process.env.HOME || process.env.USERPROFILE || '', 'Desktop'),
|
||||||
"Documents"
|
|
||||||
),
|
|
||||||
path.join(process.env.HOME || process.env.USERPROFILE || "", "Desktop"),
|
|
||||||
// Common project locations
|
// Common project locations
|
||||||
path.join(
|
path.join(process.env.HOME || process.env.USERPROFILE || '', 'Projects'),
|
||||||
process.env.HOME || process.env.USERPROFILE || "",
|
|
||||||
"Projects"
|
|
||||||
),
|
|
||||||
].filter(Boolean);
|
].filter(Boolean);
|
||||||
|
|
||||||
// Also check parent of current working directory
|
// Also check parent of current working directory
|
||||||
@@ -70,7 +62,7 @@ export function createResolveDirectoryHandler() {
|
|||||||
for (const searchPath of searchPaths) {
|
for (const searchPath of searchPaths) {
|
||||||
try {
|
try {
|
||||||
const candidatePath = path.join(searchPath, directoryName);
|
const candidatePath = path.join(searchPath, directoryName);
|
||||||
const stats = await fs.stat(candidatePath);
|
const stats = await secureFs.stat(candidatePath);
|
||||||
|
|
||||||
if (stats.isDirectory()) {
|
if (stats.isDirectory()) {
|
||||||
// Verify it matches by checking for sample files
|
// Verify it matches by checking for sample files
|
||||||
@@ -78,15 +70,15 @@ export function createResolveDirectoryHandler() {
|
|||||||
let matches = 0;
|
let matches = 0;
|
||||||
for (const sampleFile of sampleFiles.slice(0, 5)) {
|
for (const sampleFile of sampleFiles.slice(0, 5)) {
|
||||||
// Remove directory name prefix from sample file path
|
// Remove directory name prefix from sample file path
|
||||||
const relativeFile = sampleFile.startsWith(directoryName + "/")
|
const relativeFile = sampleFile.startsWith(directoryName + '/')
|
||||||
? sampleFile.substring(directoryName.length + 1)
|
? sampleFile.substring(directoryName.length + 1)
|
||||||
: sampleFile.split("/").slice(1).join("/") ||
|
: sampleFile.split('/').slice(1).join('/') ||
|
||||||
sampleFile.split("/").pop() ||
|
sampleFile.split('/').pop() ||
|
||||||
sampleFile;
|
sampleFile;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const filePath = path.join(candidatePath, relativeFile);
|
const filePath = path.join(candidatePath, relativeFile);
|
||||||
await fs.access(filePath);
|
await secureFs.access(filePath);
|
||||||
matches++;
|
matches++;
|
||||||
} catch {
|
} catch {
|
||||||
// File doesn't exist, continue checking
|
// File doesn't exist, continue checking
|
||||||
@@ -118,7 +110,7 @@ export function createResolveDirectoryHandler() {
|
|||||||
error: `Directory "${directoryName}" not found in common locations. Please ensure the directory exists.`,
|
error: `Directory "${directoryName}" not found in common locations. Please ensure the directory exists.`,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Resolve directory failed");
|
logError(error, 'Resolve directory failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* POST /save-board-background endpoint - Save board background image
|
* POST /save-board-background endpoint - Save board background image
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getBoardDir } from "@automaker/platform";
|
import { getBoardDir } from '@automaker/platform';
|
||||||
|
|
||||||
export function createSaveBoardBackgroundHandler() {
|
export function createSaveBoardBackgroundHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -21,31 +21,31 @@ export function createSaveBoardBackgroundHandler() {
|
|||||||
if (!data || !filename || !projectPath) {
|
if (!data || !filename || !projectPath) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "data, filename, and projectPath are required",
|
error: 'data, filename, and projectPath are required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get board directory
|
// Get board directory
|
||||||
const boardDir = getBoardDir(projectPath);
|
const boardDir = getBoardDir(projectPath);
|
||||||
await fs.mkdir(boardDir, { recursive: true });
|
await secureFs.mkdir(boardDir, { recursive: true });
|
||||||
|
|
||||||
// Decode base64 data (remove data URL prefix if present)
|
// Decode base64 data (remove data URL prefix if present)
|
||||||
const base64Data = data.replace(/^data:image\/\w+;base64,/, "");
|
const base64Data = data.replace(/^data:image\/\w+;base64,/, '');
|
||||||
const buffer = Buffer.from(base64Data, "base64");
|
const buffer = Buffer.from(base64Data, 'base64');
|
||||||
|
|
||||||
// Use a fixed filename for the board background (overwrite previous)
|
// Use a fixed filename for the board background (overwrite previous)
|
||||||
const ext = path.extname(filename) || ".png";
|
const ext = path.extname(filename) || '.png';
|
||||||
const uniqueFilename = `background${ext}`;
|
const uniqueFilename = `background${ext}`;
|
||||||
const filePath = path.join(boardDir, uniqueFilename);
|
const filePath = path.join(boardDir, uniqueFilename);
|
||||||
|
|
||||||
// Write file
|
// Write file
|
||||||
await fs.writeFile(filePath, buffer);
|
await secureFs.writeFile(filePath, buffer);
|
||||||
|
|
||||||
// Return the absolute path
|
// Return the absolute path
|
||||||
res.json({ success: true, path: filePath });
|
res.json({ success: true, path: filePath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Save board background failed");
|
logError(error, 'Save board background failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* POST /save-image endpoint - Save image to .automaker images directory
|
* POST /save-image endpoint - Save image to .automaker images directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getImagesDir } from "@automaker/platform";
|
import { getImagesDir } from '@automaker/platform';
|
||||||
|
|
||||||
export function createSaveImageHandler() {
|
export function createSaveImageHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -21,33 +21,33 @@ export function createSaveImageHandler() {
|
|||||||
if (!data || !filename || !projectPath) {
|
if (!data || !filename || !projectPath) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "data, filename, and projectPath are required",
|
error: 'data, filename, and projectPath are required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get images directory
|
// Get images directory
|
||||||
const imagesDir = getImagesDir(projectPath);
|
const imagesDir = getImagesDir(projectPath);
|
||||||
await fs.mkdir(imagesDir, { recursive: true });
|
await secureFs.mkdir(imagesDir, { recursive: true });
|
||||||
|
|
||||||
// Decode base64 data (remove data URL prefix if present)
|
// Decode base64 data (remove data URL prefix if present)
|
||||||
const base64Data = data.replace(/^data:image\/\w+;base64,/, "");
|
const base64Data = data.replace(/^data:image\/\w+;base64,/, '');
|
||||||
const buffer = Buffer.from(base64Data, "base64");
|
const buffer = Buffer.from(base64Data, 'base64');
|
||||||
|
|
||||||
// Generate unique filename with timestamp
|
// Generate unique filename with timestamp
|
||||||
const timestamp = Date.now();
|
const timestamp = Date.now();
|
||||||
const ext = path.extname(filename) || ".png";
|
const ext = path.extname(filename) || '.png';
|
||||||
const baseName = path.basename(filename, ext);
|
const baseName = path.basename(filename, ext);
|
||||||
const uniqueFilename = `${baseName}-${timestamp}${ext}`;
|
const uniqueFilename = `${baseName}-${timestamp}${ext}`;
|
||||||
const filePath = path.join(imagesDir, uniqueFilename);
|
const filePath = path.join(imagesDir, uniqueFilename);
|
||||||
|
|
||||||
// Write file
|
// Write file
|
||||||
await fs.writeFile(filePath, buffer);
|
await secureFs.writeFile(filePath, buffer);
|
||||||
|
|
||||||
// Return the absolute path
|
// Return the absolute path
|
||||||
res.json({ success: true, path: filePath });
|
res.json({ success: true, path: filePath });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Save image failed");
|
logError(error, 'Save image failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
* POST /stat endpoint - Get file stats
|
* POST /stat endpoint - Get file stats
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { validatePath, PathNotAllowedError } from "@automaker/platform";
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createStatHandler() {
|
export function createStatHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -13,12 +13,11 @@ export function createStatHandler() {
|
|||||||
const { filePath } = req.body as { filePath: string };
|
const { filePath } = req.body as { filePath: string };
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
res.status(400).json({ success: false, error: "filePath is required" });
|
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = validatePath(filePath);
|
const stats = await secureFs.stat(filePath);
|
||||||
const stats = await fs.stat(resolvedPath);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
@@ -36,7 +35,7 @@ export function createStatHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logError(error, "Get file stats failed");
|
logError(error, 'Get file stats failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* POST /validate-path endpoint - Validate and add path to allowed list
|
* POST /validate-path endpoint - Validate and add path to allowed list
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { isPathAllowed } from "@automaker/platform";
|
import { isPathAllowed } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createValidatePathHandler() {
|
export function createValidatePathHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -14,7 +14,7 @@ export function createValidatePathHandler() {
|
|||||||
const { filePath } = req.body as { filePath: string };
|
const { filePath } = req.body as { filePath: string };
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
res.status(400).json({ success: false, error: "filePath is required" });
|
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -22,12 +22,10 @@ export function createValidatePathHandler() {
|
|||||||
|
|
||||||
// Check if path exists
|
// Check if path exists
|
||||||
try {
|
try {
|
||||||
const stats = await fs.stat(resolvedPath);
|
const stats = await secureFs.stat(resolvedPath);
|
||||||
|
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
res
|
res.status(400).json({ success: false, error: 'Path is not a directory' });
|
||||||
.status(400)
|
|
||||||
.json({ success: false, error: "Path is not a directory" });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,10 +35,10 @@ export function createValidatePathHandler() {
|
|||||||
isAllowed: isPathAllowed(resolvedPath),
|
isAllowed: isPathAllowed(resolvedPath),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
res.status(400).json({ success: false, error: "Path does not exist" });
|
res.status(400).json({ success: false, error: 'Path does not exist' });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Validate path failed");
|
logError(error, 'Validate path failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* POST /write endpoint - Write file
|
* POST /write endpoint - Write file
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { validatePath, PathNotAllowedError } from "@automaker/platform";
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import { mkdirSafe } from "@automaker/utils";
|
import { mkdirSafe } from '@automaker/utils';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createWriteHandler() {
|
export function createWriteHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -18,15 +18,13 @@ export function createWriteHandler() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!filePath) {
|
if (!filePath) {
|
||||||
res.status(400).json({ success: false, error: "filePath is required" });
|
res.status(400).json({ success: false, error: 'filePath is required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const resolvedPath = validatePath(filePath);
|
|
||||||
|
|
||||||
// Ensure parent directory exists (symlink-safe)
|
// Ensure parent directory exists (symlink-safe)
|
||||||
await mkdirSafe(path.dirname(resolvedPath));
|
await mkdirSafe(path.dirname(path.resolve(filePath)));
|
||||||
await fs.writeFile(resolvedPath, content, "utf-8");
|
await secureFs.writeFile(filePath, content, 'utf-8');
|
||||||
|
|
||||||
res.json({ success: true });
|
res.json({ success: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -36,7 +34,7 @@ export function createWriteHandler() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
logError(error, "Write file failed");
|
logError(error, 'Write file failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* POST /clone endpoint - Clone a GitHub template to a new project directory
|
* POST /clone endpoint - Clone a GitHub template to a new project directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { spawn } from "child_process";
|
import { spawn } from 'child_process';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { isPathAllowed } from "@automaker/platform";
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
import { logger, getErrorMessage, logError } from "../common.js";
|
import { logger, getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createCloneHandler() {
|
export function createCloneHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -22,7 +22,7 @@ export function createCloneHandler() {
|
|||||||
if (!repoUrl || !projectName || !parentDir) {
|
if (!repoUrl || !projectName || !parentDir) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "repoUrl, projectName, and parentDir are required",
|
error: 'repoUrl, projectName, and parentDir are required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -36,17 +36,15 @@ export function createCloneHandler() {
|
|||||||
if (!githubUrlPattern.test(repoUrl)) {
|
if (!githubUrlPattern.test(repoUrl)) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid GitHub repository URL",
|
error: 'Invalid GitHub repository URL',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize project name (allow alphanumeric, dash, underscore)
|
// Sanitize project name (allow alphanumeric, dash, underscore)
|
||||||
const sanitizedName = projectName.replace(/[^a-zA-Z0-9-_]/g, "-");
|
const sanitizedName = projectName.replace(/[^a-zA-Z0-9-_]/g, '-');
|
||||||
if (sanitizedName !== projectName) {
|
if (sanitizedName !== projectName) {
|
||||||
logger.info(
|
logger.info(`[Templates] Sanitized project name: ${projectName} -> ${sanitizedName}`);
|
||||||
`[Templates] Sanitized project name: ${projectName} -> ${sanitizedName}`
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build full project path
|
// Build full project path
|
||||||
@@ -55,41 +53,30 @@ export function createCloneHandler() {
|
|||||||
const resolvedParent = path.resolve(parentDir);
|
const resolvedParent = path.resolve(parentDir);
|
||||||
const resolvedProject = path.resolve(projectPath);
|
const resolvedProject = path.resolve(projectPath);
|
||||||
const relativePath = path.relative(resolvedParent, resolvedProject);
|
const relativePath = path.relative(resolvedParent, resolvedProject);
|
||||||
if (relativePath.startsWith("..") || path.isAbsolute(relativePath)) {
|
if (relativePath.startsWith('..') || path.isAbsolute(relativePath)) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Invalid project name; potential path traversal attempt.",
|
error: 'Invalid project name; potential path traversal attempt.',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate that parent directory is within allowed root directory
|
// Check if directory already exists (secureFs.access also validates path is allowed)
|
||||||
if (!isPathAllowed(resolvedParent)) {
|
|
||||||
res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
error: `Parent directory not allowed: ${parentDir}. Must be within ALLOWED_ROOT_DIRECTORY.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate that project path will be within allowed root directory
|
|
||||||
if (!isPathAllowed(resolvedProject)) {
|
|
||||||
res.status(403).json({
|
|
||||||
success: false,
|
|
||||||
error: `Project path not allowed: ${projectPath}. Must be within ALLOWED_ROOT_DIRECTORY.`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if directory already exists
|
|
||||||
try {
|
try {
|
||||||
await fs.access(projectPath);
|
await secureFs.access(projectPath);
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Directory "${sanitizedName}" already exists in ${parentDir}`,
|
error: `Directory "${sanitizedName}" already exists in ${parentDir}`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch {
|
} catch (accessError) {
|
||||||
|
if (accessError instanceof PathNotAllowedError) {
|
||||||
|
res.status(403).json({
|
||||||
|
success: false,
|
||||||
|
error: `Project path not allowed: ${projectPath}. Must be within ALLOWED_ROOT_DIRECTORY.`,
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
// Directory doesn't exist, which is what we want
|
// Directory doesn't exist, which is what we want
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,35 +84,33 @@ export function createCloneHandler() {
|
|||||||
try {
|
try {
|
||||||
// Check if parentDir is a root path (Windows: C:\, D:\, etc. or Unix: /)
|
// Check if parentDir is a root path (Windows: C:\, D:\, etc. or Unix: /)
|
||||||
const isWindowsRoot = /^[A-Za-z]:\\?$/.test(parentDir);
|
const isWindowsRoot = /^[A-Za-z]:\\?$/.test(parentDir);
|
||||||
const isUnixRoot = parentDir === "/" || parentDir === "";
|
const isUnixRoot = parentDir === '/' || parentDir === '';
|
||||||
const isRoot = isWindowsRoot || isUnixRoot;
|
const isRoot = isWindowsRoot || isUnixRoot;
|
||||||
|
|
||||||
if (isRoot) {
|
if (isRoot) {
|
||||||
// Root paths always exist, just verify access
|
// Root paths always exist, just verify access
|
||||||
logger.info(`[Templates] Using root path: ${parentDir}`);
|
logger.info(`[Templates] Using root path: ${parentDir}`);
|
||||||
await fs.access(parentDir);
|
await secureFs.access(parentDir);
|
||||||
} else {
|
} else {
|
||||||
// Check if parent directory exists
|
// Check if parent directory exists
|
||||||
const parentExists = await fs
|
let parentExists = false;
|
||||||
.access(parentDir)
|
try {
|
||||||
.then(() => true)
|
await secureFs.access(parentDir);
|
||||||
.catch(() => false);
|
parentExists = true;
|
||||||
|
} catch {
|
||||||
|
parentExists = false;
|
||||||
|
}
|
||||||
|
|
||||||
if (!parentExists) {
|
if (!parentExists) {
|
||||||
logger.info(`[Templates] Creating parent directory: ${parentDir}`);
|
logger.info(`[Templates] Creating parent directory: ${parentDir}`);
|
||||||
await fs.mkdir(parentDir, { recursive: true });
|
await secureFs.mkdir(parentDir, { recursive: true });
|
||||||
} else {
|
} else {
|
||||||
logger.info(`[Templates] Parent directory exists: ${parentDir}`);
|
logger.info(`[Templates] Parent directory exists: ${parentDir}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage =
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
error instanceof Error ? error.message : String(error);
|
logger.error('[Templates] Failed to access parent directory:', parentDir, error);
|
||||||
logger.error(
|
|
||||||
"[Templates] Failed to access parent directory:",
|
|
||||||
parentDir,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to access parent directory: ${errorMessage}`,
|
error: `Failed to access parent directory: ${errorMessage}`,
|
||||||
@@ -140,17 +125,17 @@ export function createCloneHandler() {
|
|||||||
success: boolean;
|
success: boolean;
|
||||||
error?: string;
|
error?: string;
|
||||||
}>((resolve) => {
|
}>((resolve) => {
|
||||||
const gitProcess = spawn("git", ["clone", repoUrl, projectPath], {
|
const gitProcess = spawn('git', ['clone', repoUrl, projectPath], {
|
||||||
cwd: parentDir,
|
cwd: parentDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
let stderr = "";
|
let stderr = '';
|
||||||
|
|
||||||
gitProcess.stderr.on("data", (data) => {
|
gitProcess.stderr.on('data', (data) => {
|
||||||
stderr += data.toString();
|
stderr += data.toString();
|
||||||
});
|
});
|
||||||
|
|
||||||
gitProcess.on("close", (code) => {
|
gitProcess.on('close', (code) => {
|
||||||
if (code === 0) {
|
if (code === 0) {
|
||||||
resolve({ success: true });
|
resolve({ success: true });
|
||||||
} else {
|
} else {
|
||||||
@@ -161,7 +146,7 @@ export function createCloneHandler() {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
gitProcess.on("error", (error) => {
|
gitProcess.on('error', (error) => {
|
||||||
resolve({
|
resolve({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Failed to spawn git: ${error.message}`,
|
error: `Failed to spawn git: ${error.message}`,
|
||||||
@@ -172,34 +157,34 @@ export function createCloneHandler() {
|
|||||||
if (!cloneResult.success) {
|
if (!cloneResult.success) {
|
||||||
res.status(500).json({
|
res.status(500).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: cloneResult.error || "Failed to clone repository",
|
error: cloneResult.error || 'Failed to clone repository',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove .git directory to start fresh
|
// Remove .git directory to start fresh
|
||||||
try {
|
try {
|
||||||
const gitDir = path.join(projectPath, ".git");
|
const gitDir = path.join(projectPath, '.git');
|
||||||
await fs.rm(gitDir, { recursive: true, force: true });
|
await secureFs.rm(gitDir, { recursive: true, force: true });
|
||||||
logger.info("[Templates] Removed .git directory");
|
logger.info('[Templates] Removed .git directory');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.warn("[Templates] Could not remove .git directory:", error);
|
logger.warn('[Templates] Could not remove .git directory:', error);
|
||||||
// Continue anyway - not critical
|
// Continue anyway - not critical
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize a fresh git repository
|
// Initialize a fresh git repository
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
const gitInit = spawn("git", ["init"], {
|
const gitInit = spawn('git', ['init'], {
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
gitInit.on("close", () => {
|
gitInit.on('close', () => {
|
||||||
logger.info("[Templates] Initialized fresh git repository");
|
logger.info('[Templates] Initialized fresh git repository');
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
gitInit.on("error", () => {
|
gitInit.on('error', () => {
|
||||||
logger.warn("[Templates] Could not initialize git");
|
logger.warn('[Templates] Could not initialize git');
|
||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -212,7 +197,7 @@ export function createCloneHandler() {
|
|||||||
projectName: sanitizedName,
|
projectName: sanitizedName,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Clone template failed");
|
logError(error, 'Clone template failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,14 +2,11 @@
|
|||||||
* GET /config endpoint - Get workspace configuration status
|
* GET /config endpoint - Get workspace configuration status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import {
|
import { getAllowedRootDirectory, getDataDirectory } from '@automaker/platform';
|
||||||
getAllowedRootDirectory,
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
getDataDirectory,
|
|
||||||
} from "@automaker/platform";
|
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
|
||||||
|
|
||||||
export function createConfigHandler() {
|
export function createConfigHandler() {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
@@ -30,12 +27,12 @@ export function createConfigHandler() {
|
|||||||
// Check if the directory exists
|
// Check if the directory exists
|
||||||
try {
|
try {
|
||||||
const resolvedWorkspaceDir = path.resolve(allowedRootDirectory);
|
const resolvedWorkspaceDir = path.resolve(allowedRootDirectory);
|
||||||
const stats = await fs.stat(resolvedWorkspaceDir);
|
const stats = await secureFs.stat(resolvedWorkspaceDir);
|
||||||
if (!stats.isDirectory()) {
|
if (!stats.isDirectory()) {
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
configured: false,
|
configured: false,
|
||||||
error: "ALLOWED_ROOT_DIRECTORY is not a valid directory",
|
error: 'ALLOWED_ROOT_DIRECTORY is not a valid directory',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -50,11 +47,11 @@ export function createConfigHandler() {
|
|||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
configured: false,
|
configured: false,
|
||||||
error: "ALLOWED_ROOT_DIRECTORY path does not exist",
|
error: 'ALLOWED_ROOT_DIRECTORY path does not exist',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get workspace config failed");
|
logError(error, 'Get workspace config failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* GET /directories endpoint - List directories in workspace
|
* GET /directories endpoint - List directories in workspace
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { getAllowedRootDirectory } from "@automaker/platform";
|
import { getAllowedRootDirectory } from '@automaker/platform';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
export function createDirectoriesHandler() {
|
export function createDirectoriesHandler() {
|
||||||
return async (_req: Request, res: Response): Promise<void> => {
|
return async (_req: Request, res: Response): Promise<void> => {
|
||||||
@@ -16,7 +16,7 @@ export function createDirectoriesHandler() {
|
|||||||
if (!allowedRootDirectory) {
|
if (!allowedRootDirectory) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "ALLOWED_ROOT_DIRECTORY is not configured",
|
error: 'ALLOWED_ROOT_DIRECTORY is not configured',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -25,23 +25,23 @@ export function createDirectoriesHandler() {
|
|||||||
|
|
||||||
// Check if directory exists
|
// Check if directory exists
|
||||||
try {
|
try {
|
||||||
await fs.stat(resolvedWorkspaceDir);
|
await secureFs.stat(resolvedWorkspaceDir);
|
||||||
} catch {
|
} catch {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Workspace directory path does not exist",
|
error: 'Workspace directory path does not exist',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read directory contents
|
// Read directory contents
|
||||||
const entries = await fs.readdir(resolvedWorkspaceDir, {
|
const entries = await secureFs.readdir(resolvedWorkspaceDir, {
|
||||||
withFileTypes: true,
|
withFileTypes: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Filter to directories only and map to result format
|
// Filter to directories only and map to result format
|
||||||
const directories = entries
|
const directories = entries
|
||||||
.filter((entry) => entry.isDirectory() && !entry.name.startsWith("."))
|
.filter((entry) => entry.isDirectory() && !entry.name.startsWith('.'))
|
||||||
.map((entry) => ({
|
.map((entry) => ({
|
||||||
name: entry.name,
|
name: entry.name,
|
||||||
path: path.join(resolvedWorkspaceDir, entry.name),
|
path: path.join(resolvedWorkspaceDir, entry.name),
|
||||||
@@ -53,7 +53,7 @@ export function createDirectoriesHandler() {
|
|||||||
directories,
|
directories,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "List workspace directories failed");
|
logError(error, 'List workspace directories failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,18 +2,14 @@
|
|||||||
* Common utilities for worktree routes
|
* Common utilities for worktree routes
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from "@automaker/utils";
|
import { createLogger } from '@automaker/utils';
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
|
||||||
import {
|
import { FeatureLoader } from '../../services/feature-loader.js';
|
||||||
getErrorMessage as getErrorMessageShared,
|
|
||||||
createLogError,
|
|
||||||
} from "../common.js";
|
|
||||||
import { FeatureLoader } from "../../services/feature-loader.js";
|
|
||||||
|
|
||||||
const logger = createLogger("Worktree");
|
const logger = createLogger('Worktree');
|
||||||
export const execAsync = promisify(exec);
|
export const execAsync = promisify(exec);
|
||||||
const featureLoader = new FeatureLoader();
|
const featureLoader = new FeatureLoader();
|
||||||
|
|
||||||
@@ -28,10 +24,10 @@ export const MAX_BRANCH_NAME_LENGTH = 250;
|
|||||||
// Extended PATH configuration for Electron apps
|
// Extended PATH configuration for Electron apps
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
const pathSeparator = process.platform === "win32" ? ";" : ":";
|
const pathSeparator = process.platform === 'win32' ? ';' : ':';
|
||||||
const additionalPaths: string[] = [];
|
const additionalPaths: string[] = [];
|
||||||
|
|
||||||
if (process.platform === "win32") {
|
if (process.platform === 'win32') {
|
||||||
// Windows paths
|
// Windows paths
|
||||||
if (process.env.LOCALAPPDATA) {
|
if (process.env.LOCALAPPDATA) {
|
||||||
additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`);
|
additionalPaths.push(`${process.env.LOCALAPPDATA}\\Programs\\Git\\cmd`);
|
||||||
@@ -39,23 +35,22 @@ if (process.platform === "win32") {
|
|||||||
if (process.env.PROGRAMFILES) {
|
if (process.env.PROGRAMFILES) {
|
||||||
additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`);
|
additionalPaths.push(`${process.env.PROGRAMFILES}\\Git\\cmd`);
|
||||||
}
|
}
|
||||||
if (process.env["ProgramFiles(x86)"]) {
|
if (process.env['ProgramFiles(x86)']) {
|
||||||
additionalPaths.push(`${process.env["ProgramFiles(x86)"]}\\Git\\cmd`);
|
additionalPaths.push(`${process.env['ProgramFiles(x86)']}\\Git\\cmd`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Unix/Mac paths
|
// Unix/Mac paths
|
||||||
additionalPaths.push(
|
additionalPaths.push(
|
||||||
"/opt/homebrew/bin", // Homebrew on Apple Silicon
|
'/opt/homebrew/bin', // Homebrew on Apple Silicon
|
||||||
"/usr/local/bin", // Homebrew on Intel Mac, common Linux location
|
'/usr/local/bin', // Homebrew on Intel Mac, common Linux location
|
||||||
"/home/linuxbrew/.linuxbrew/bin", // Linuxbrew
|
'/home/linuxbrew/.linuxbrew/bin', // Linuxbrew
|
||||||
`${process.env.HOME}/.local/bin`, // pipx, other user installs
|
`${process.env.HOME}/.local/bin` // pipx, other user installs
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const extendedPath = [
|
const extendedPath = [process.env.PATH, ...additionalPaths.filter(Boolean)]
|
||||||
process.env.PATH,
|
.filter(Boolean)
|
||||||
...additionalPaths.filter(Boolean),
|
.join(pathSeparator);
|
||||||
].filter(Boolean).join(pathSeparator);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Environment variables with extended PATH for executing shell commands.
|
* Environment variables with extended PATH for executing shell commands.
|
||||||
@@ -85,9 +80,7 @@ export function isValidBranchName(name: string): boolean {
|
|||||||
*/
|
*/
|
||||||
export async function isGhCliAvailable(): Promise<boolean> {
|
export async function isGhCliAvailable(): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
const checkCommand = process.platform === "win32"
|
const checkCommand = process.platform === 'win32' ? 'where gh' : 'command -v gh';
|
||||||
? "where gh"
|
|
||||||
: "command -v gh";
|
|
||||||
await execAsync(checkCommand, { env: execEnv });
|
await execAsync(checkCommand, { env: execEnv });
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -95,8 +88,7 @@ export async function isGhCliAvailable(): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AUTOMAKER_INITIAL_COMMIT_MESSAGE =
|
export const AUTOMAKER_INITIAL_COMMIT_MESSAGE = 'chore: automaker initial commit';
|
||||||
"chore: automaker initial commit";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize path separators to forward slashes for cross-platform consistency.
|
* Normalize path separators to forward slashes for cross-platform consistency.
|
||||||
@@ -104,7 +96,7 @@ export const AUTOMAKER_INITIAL_COMMIT_MESSAGE =
|
|||||||
* from git commands (which may use forward slashes).
|
* from git commands (which may use forward slashes).
|
||||||
*/
|
*/
|
||||||
export function normalizePath(p: string): string {
|
export function normalizePath(p: string): string {
|
||||||
return p.replace(/\\/g, "/");
|
return p.replace(/\\/g, '/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -112,7 +104,7 @@ export function normalizePath(p: string): string {
|
|||||||
*/
|
*/
|
||||||
export async function isGitRepo(repoPath: string): Promise<boolean> {
|
export async function isGitRepo(repoPath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await execAsync("git rev-parse --is-inside-work-tree", { cwd: repoPath });
|
await execAsync('git rev-parse --is-inside-work-tree', { cwd: repoPath });
|
||||||
return true;
|
return true;
|
||||||
} catch {
|
} catch {
|
||||||
return false;
|
return false;
|
||||||
@@ -124,30 +116,21 @@ export async function isGitRepo(repoPath: string): Promise<boolean> {
|
|||||||
* These are expected in test environments with mock paths
|
* These are expected in test environments with mock paths
|
||||||
*/
|
*/
|
||||||
export function isENOENT(error: unknown): boolean {
|
export function isENOENT(error: unknown): boolean {
|
||||||
return (
|
return error !== null && typeof error === 'object' && 'code' in error && error.code === 'ENOENT';
|
||||||
error !== null &&
|
|
||||||
typeof error === "object" &&
|
|
||||||
"code" in error &&
|
|
||||||
error.code === "ENOENT"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a path is a mock/test path that doesn't exist
|
* Check if a path is a mock/test path that doesn't exist
|
||||||
*/
|
*/
|
||||||
export function isMockPath(worktreePath: string): boolean {
|
export function isMockPath(worktreePath: string): boolean {
|
||||||
return worktreePath.startsWith("/mock/") || worktreePath.includes("/mock/");
|
return worktreePath.startsWith('/mock/') || worktreePath.includes('/mock/');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Conditionally log worktree errors - suppress ENOENT for mock paths
|
* Conditionally log worktree errors - suppress ENOENT for mock paths
|
||||||
* to reduce noise in test output
|
* to reduce noise in test output
|
||||||
*/
|
*/
|
||||||
export function logWorktreeError(
|
export function logWorktreeError(error: unknown, message: string, worktreePath?: string): void {
|
||||||
error: unknown,
|
|
||||||
message: string,
|
|
||||||
worktreePath?: string
|
|
||||||
): void {
|
|
||||||
// Don't log ENOENT errors for mock paths (expected in tests)
|
// Don't log ENOENT errors for mock paths (expected in tests)
|
||||||
if (isENOENT(error) && worktreePath && isMockPath(worktreePath)) {
|
if (isENOENT(error) && worktreePath && isMockPath(worktreePath)) {
|
||||||
return;
|
return;
|
||||||
@@ -165,17 +148,14 @@ export const logError = createLogError(logger);
|
|||||||
*/
|
*/
|
||||||
export async function ensureInitialCommit(repoPath: string): Promise<boolean> {
|
export async function ensureInitialCommit(repoPath: string): Promise<boolean> {
|
||||||
try {
|
try {
|
||||||
await execAsync("git rev-parse --verify HEAD", { cwd: repoPath });
|
await execAsync('git rev-parse --verify HEAD', { cwd: repoPath });
|
||||||
return false;
|
return false;
|
||||||
} catch {
|
} catch {
|
||||||
try {
|
try {
|
||||||
await execAsync(
|
await execAsync(`git commit --allow-empty -m "${AUTOMAKER_INITIAL_COMMIT_MESSAGE}"`, {
|
||||||
`git commit --allow-empty -m "${AUTOMAKER_INITIAL_COMMIT_MESSAGE}"`,
|
cwd: repoPath,
|
||||||
{ cwd: repoPath }
|
});
|
||||||
);
|
logger.info(`[Worktree] Created initial empty commit to enable worktrees in ${repoPath}`);
|
||||||
logger.info(
|
|
||||||
`[Worktree] Created initial empty commit to enable worktrees in ${repoPath}`
|
|
||||||
);
|
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const reason = getErrorMessageShared(error);
|
const reason = getErrorMessageShared(error);
|
||||||
|
|||||||
@@ -5,12 +5,9 @@
|
|||||||
* can switch between branches even after worktrees are removed.
|
* can switch between branches even after worktrees are removed.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFile, writeFile } from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import {
|
import { getBranchTrackingPath, ensureAutomakerDir } from '@automaker/platform';
|
||||||
getBranchTrackingPath,
|
|
||||||
ensureAutomakerDir,
|
|
||||||
} from "@automaker/platform";
|
|
||||||
|
|
||||||
export interface TrackedBranch {
|
export interface TrackedBranch {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -25,19 +22,17 @@ interface BranchTrackingData {
|
|||||||
/**
|
/**
|
||||||
* Read tracked branches from file
|
* Read tracked branches from file
|
||||||
*/
|
*/
|
||||||
export async function getTrackedBranches(
|
export async function getTrackedBranches(projectPath: string): Promise<TrackedBranch[]> {
|
||||||
projectPath: string
|
|
||||||
): Promise<TrackedBranch[]> {
|
|
||||||
try {
|
try {
|
||||||
const filePath = getBranchTrackingPath(projectPath);
|
const filePath = getBranchTrackingPath(projectPath);
|
||||||
const content = await readFile(filePath, "utf-8");
|
const content = (await secureFs.readFile(filePath, 'utf-8')) as string;
|
||||||
const data: BranchTrackingData = JSON.parse(content);
|
const data: BranchTrackingData = JSON.parse(content);
|
||||||
return data.branches || [];
|
return data.branches || [];
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
if (error.code === "ENOENT") {
|
if (error.code === 'ENOENT') {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
console.warn("[branch-tracking] Failed to read tracked branches:", error);
|
console.warn('[branch-tracking] Failed to read tracked branches:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -45,23 +40,17 @@ export async function getTrackedBranches(
|
|||||||
/**
|
/**
|
||||||
* Save tracked branches to file
|
* Save tracked branches to file
|
||||||
*/
|
*/
|
||||||
async function saveTrackedBranches(
|
async function saveTrackedBranches(projectPath: string, branches: TrackedBranch[]): Promise<void> {
|
||||||
projectPath: string,
|
|
||||||
branches: TrackedBranch[]
|
|
||||||
): Promise<void> {
|
|
||||||
const automakerDir = await ensureAutomakerDir(projectPath);
|
const automakerDir = await ensureAutomakerDir(projectPath);
|
||||||
const filePath = path.join(automakerDir, "active-branches.json");
|
const filePath = path.join(automakerDir, 'active-branches.json');
|
||||||
const data: BranchTrackingData = { branches };
|
const data: BranchTrackingData = { branches };
|
||||||
await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
|
await secureFs.writeFile(filePath, JSON.stringify(data, null, 2), 'utf-8');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add a branch to tracking
|
* Add a branch to tracking
|
||||||
*/
|
*/
|
||||||
export async function trackBranch(
|
export async function trackBranch(projectPath: string, branchName: string): Promise<void> {
|
||||||
projectPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<void> {
|
|
||||||
const branches = await getTrackedBranches(projectPath);
|
const branches = await getTrackedBranches(projectPath);
|
||||||
|
|
||||||
// Check if already tracked
|
// Check if already tracked
|
||||||
@@ -82,10 +71,7 @@ export async function trackBranch(
|
|||||||
/**
|
/**
|
||||||
* Remove a branch from tracking
|
* Remove a branch from tracking
|
||||||
*/
|
*/
|
||||||
export async function untrackBranch(
|
export async function untrackBranch(projectPath: string, branchName: string): Promise<void> {
|
||||||
projectPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<void> {
|
|
||||||
const branches = await getTrackedBranches(projectPath);
|
const branches = await getTrackedBranches(projectPath);
|
||||||
const filtered = branches.filter((b) => b.name !== branchName);
|
const filtered = branches.filter((b) => b.name !== branchName);
|
||||||
|
|
||||||
@@ -114,10 +100,7 @@ export async function updateBranchActivation(
|
|||||||
/**
|
/**
|
||||||
* Check if a branch is tracked
|
* Check if a branch is tracked
|
||||||
*/
|
*/
|
||||||
export async function isBranchTracked(
|
export async function isBranchTracked(projectPath: string, branchName: string): Promise<boolean> {
|
||||||
projectPath: string,
|
|
||||||
branchName: string
|
|
||||||
): Promise<boolean> {
|
|
||||||
const branches = await getTrackedBranches(projectPath);
|
const branches = await getTrackedBranches(projectPath);
|
||||||
return branches.some((b) => b.name === branchName);
|
return branches.some((b) => b.name === branchName);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,19 +7,19 @@
|
|||||||
* 3. Only creates a new worktree if none exists for the branch
|
* 3. Only creates a new worktree if none exists for the branch
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { mkdir } from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import {
|
import {
|
||||||
isGitRepo,
|
isGitRepo,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
logError,
|
logError,
|
||||||
normalizePath,
|
normalizePath,
|
||||||
ensureInitialCommit,
|
ensureInitialCommit,
|
||||||
} from "../common.js";
|
} from '../common.js';
|
||||||
import { trackBranch } from "./branch-tracking.js";
|
import { trackBranch } from './branch-tracking.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -31,20 +31,20 @@ async function findExistingWorktreeForBranch(
|
|||||||
branchName: string
|
branchName: string
|
||||||
): Promise<{ path: string; branch: string } | null> {
|
): Promise<{ path: string; branch: string } | null> {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execAsync("git worktree list --porcelain", {
|
const { stdout } = await execAsync('git worktree list --porcelain', {
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const lines = stdout.split("\n");
|
const lines = stdout.split('\n');
|
||||||
let currentPath: string | null = null;
|
let currentPath: string | null = null;
|
||||||
let currentBranch: string | null = null;
|
let currentBranch: string | null = null;
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith("worktree ")) {
|
if (line.startsWith('worktree ')) {
|
||||||
currentPath = line.slice(9);
|
currentPath = line.slice(9);
|
||||||
} else if (line.startsWith("branch ")) {
|
} else if (line.startsWith('branch ')) {
|
||||||
currentBranch = line.slice(7).replace("refs/heads/", "");
|
currentBranch = line.slice(7).replace('refs/heads/', '');
|
||||||
} else if (line === "" && currentPath && currentBranch) {
|
} else if (line === '' && currentPath && currentBranch) {
|
||||||
// End of a worktree entry
|
// End of a worktree entry
|
||||||
if (currentBranch === branchName) {
|
if (currentBranch === branchName) {
|
||||||
// Resolve to absolute path - git may return relative paths
|
// Resolve to absolute path - git may return relative paths
|
||||||
@@ -86,7 +86,7 @@ export function createCreateHandler() {
|
|||||||
if (!projectPath || !branchName) {
|
if (!projectPath || !branchName) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath and branchName required",
|
error: 'projectPath and branchName required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -94,7 +94,7 @@ export function createCreateHandler() {
|
|||||||
if (!(await isGitRepo(projectPath))) {
|
if (!(await isGitRepo(projectPath))) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "Not a git repository",
|
error: 'Not a git repository',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,9 @@ export function createCreateHandler() {
|
|||||||
if (existingWorktree) {
|
if (existingWorktree) {
|
||||||
// Worktree already exists, return it as success (not an error)
|
// Worktree already exists, return it as success (not an error)
|
||||||
// This handles manually created worktrees or worktrees from previous runs
|
// This handles manually created worktrees or worktrees from previous runs
|
||||||
console.log(`[Worktree] Found existing worktree for branch "${branchName}" at: ${existingWorktree.path}`);
|
console.log(
|
||||||
|
`[Worktree] Found existing worktree for branch "${branchName}" at: ${existingWorktree.path}`
|
||||||
|
);
|
||||||
|
|
||||||
// Track the branch so it persists in the UI
|
// Track the branch so it persists in the UI
|
||||||
await trackBranch(projectPath, branchName);
|
await trackBranch(projectPath, branchName);
|
||||||
@@ -124,12 +126,12 @@ export function createCreateHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Sanitize branch name for directory usage
|
// Sanitize branch name for directory usage
|
||||||
const sanitizedName = branchName.replace(/[^a-zA-Z0-9_-]/g, "-");
|
const sanitizedName = branchName.replace(/[^a-zA-Z0-9_-]/g, '-');
|
||||||
const worktreesDir = path.join(projectPath, ".worktrees");
|
const worktreesDir = path.join(projectPath, '.worktrees');
|
||||||
const worktreePath = path.join(worktreesDir, sanitizedName);
|
const worktreePath = path.join(worktreesDir, sanitizedName);
|
||||||
|
|
||||||
// Create worktrees directory if it doesn't exist
|
// Create worktrees directory if it doesn't exist
|
||||||
await mkdir(worktreesDir, { recursive: true });
|
await secureFs.mkdir(worktreesDir, { recursive: true });
|
||||||
|
|
||||||
// Check if branch exists
|
// Check if branch exists
|
||||||
let branchExists = false;
|
let branchExists = false;
|
||||||
@@ -149,7 +151,7 @@ export function createCreateHandler() {
|
|||||||
createCmd = `git worktree add "${worktreePath}" ${branchName}`;
|
createCmd = `git worktree add "${worktreePath}" ${branchName}`;
|
||||||
} else {
|
} else {
|
||||||
// Create new branch from base or HEAD
|
// Create new branch from base or HEAD
|
||||||
const base = baseBranch || "HEAD";
|
const base = baseBranch || 'HEAD';
|
||||||
createCmd = `git worktree add -b ${branchName} "${worktreePath}" ${base}`;
|
createCmd = `git worktree add -b ${branchName} "${worktreePath}" ${base}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,7 +176,7 @@ export function createCreateHandler() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Create worktree failed");
|
logError(error, 'Create worktree failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
* POST /diffs endpoint - Get diffs for a worktree
|
* POST /diffs endpoint - Get diffs for a worktree
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { getGitRepositoryDiffs } from "../../common.js";
|
import { getGitRepositoryDiffs } from '../../common.js';
|
||||||
|
|
||||||
export function createDiffsHandler() {
|
export function createDiffsHandler() {
|
||||||
return async (req: Request, res: Response): Promise<void> => {
|
return async (req: Request, res: Response): Promise<void> => {
|
||||||
@@ -17,21 +17,19 @@ export function createDiffsHandler() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res
|
res.status(400).json({
|
||||||
.status(400)
|
success: false,
|
||||||
.json({
|
error: 'projectPath and featureId required',
|
||||||
success: false,
|
});
|
||||||
error: "projectPath and featureId required",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Git worktrees are stored in project directory
|
// Git worktrees are stored in project directory
|
||||||
const worktreePath = path.join(projectPath, ".worktrees", featureId);
|
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Check if worktree exists
|
// Check if worktree exists
|
||||||
await fs.access(worktreePath);
|
await secureFs.access(worktreePath);
|
||||||
|
|
||||||
// Get diffs from worktree
|
// Get diffs from worktree
|
||||||
const result = await getGitRepositoryDiffs(worktreePath);
|
const result = await getGitRepositoryDiffs(worktreePath);
|
||||||
@@ -43,7 +41,7 @@ export function createDiffsHandler() {
|
|||||||
});
|
});
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
// Worktree doesn't exist - fallback to main project path
|
// Worktree doesn't exist - fallback to main project path
|
||||||
logError(innerError, "Worktree access failed, falling back to main project");
|
logError(innerError, 'Worktree access failed, falling back to main project');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await getGitRepositoryDiffs(projectPath);
|
const result = await getGitRepositoryDiffs(projectPath);
|
||||||
@@ -54,12 +52,12 @@ export function createDiffsHandler() {
|
|||||||
hasChanges: result.hasChanges,
|
hasChanges: result.hasChanges,
|
||||||
});
|
});
|
||||||
} catch (fallbackError) {
|
} catch (fallbackError) {
|
||||||
logError(fallbackError, "Fallback to main project also failed");
|
logError(fallbackError, 'Fallback to main project also failed');
|
||||||
res.json({ success: true, diff: "", files: [], hasChanges: false });
|
res.json({ success: true, diff: '', files: [], hasChanges: false });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get worktree diffs failed");
|
logError(error, 'Get worktree diffs failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
* POST /file-diff endpoint - Get diff for a specific file
|
* POST /file-diff endpoint - Get diff for a specific file
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
import { generateSyntheticDiffForNewFile } from "../../common.js";
|
import { generateSyntheticDiffForNewFile } from '../../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -24,24 +24,23 @@ export function createFileDiffHandler() {
|
|||||||
if (!projectPath || !featureId || !filePath) {
|
if (!projectPath || !featureId || !filePath) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath, featureId, and filePath required",
|
error: 'projectPath, featureId, and filePath required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Git worktrees are stored in project directory
|
// Git worktrees are stored in project directory
|
||||||
const worktreePath = path.join(projectPath, ".worktrees", featureId);
|
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.access(worktreePath);
|
await secureFs.access(worktreePath);
|
||||||
|
|
||||||
// First check if the file is untracked
|
// First check if the file is untracked
|
||||||
const { stdout: status } = await execAsync(
|
const { stdout: status } = await execAsync(`git status --porcelain -- "${filePath}"`, {
|
||||||
`git status --porcelain -- "${filePath}"`,
|
cwd: worktreePath,
|
||||||
{ cwd: worktreePath }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
const isUntracked = status.trim().startsWith("??");
|
const isUntracked = status.trim().startsWith('??');
|
||||||
|
|
||||||
let diff: string;
|
let diff: string;
|
||||||
if (isUntracked) {
|
if (isUntracked) {
|
||||||
@@ -49,23 +48,20 @@ export function createFileDiffHandler() {
|
|||||||
diff = await generateSyntheticDiffForNewFile(worktreePath, filePath);
|
diff = await generateSyntheticDiffForNewFile(worktreePath, filePath);
|
||||||
} else {
|
} else {
|
||||||
// Use regular git diff for tracked files
|
// Use regular git diff for tracked files
|
||||||
const result = await execAsync(
|
const result = await execAsync(`git diff HEAD -- "${filePath}"`, {
|
||||||
`git diff HEAD -- "${filePath}"`,
|
cwd: worktreePath,
|
||||||
{
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
cwd: worktreePath,
|
});
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
diff = result.stdout;
|
diff = result.stdout;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({ success: true, diff, filePath });
|
res.json({ success: true, diff, filePath });
|
||||||
} catch (innerError) {
|
} catch (innerError) {
|
||||||
logError(innerError, "Worktree file diff failed");
|
logError(innerError, 'Worktree file diff failed');
|
||||||
res.json({ success: true, diff: "", filePath });
|
res.json({ success: true, diff: '', filePath });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get worktree file diff failed");
|
logError(error, 'Get worktree file diff failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* POST /info endpoint - Get worktree info
|
* POST /info endpoint - Get worktree info
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { getErrorMessage, logError, normalizePath } from "../common.js";
|
import { getErrorMessage, logError, normalizePath } from '../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -20,20 +20,18 @@ export function createInfoHandler() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res
|
res.status(400).json({
|
||||||
.status(400)
|
success: false,
|
||||||
.json({
|
error: 'projectPath and featureId required',
|
||||||
success: false,
|
});
|
||||||
error: "projectPath and featureId required",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if worktree exists (git worktrees are stored in project directory)
|
// Check if worktree exists (git worktrees are stored in project directory)
|
||||||
const worktreePath = path.join(projectPath, ".worktrees", featureId);
|
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||||
try {
|
try {
|
||||||
await fs.access(worktreePath);
|
await secureFs.access(worktreePath);
|
||||||
const { stdout } = await execAsync("git rev-parse --abbrev-ref HEAD", {
|
const { stdout } = await execAsync('git rev-parse --abbrev-ref HEAD', {
|
||||||
cwd: worktreePath,
|
cwd: worktreePath,
|
||||||
});
|
});
|
||||||
res.json({
|
res.json({
|
||||||
@@ -45,7 +43,7 @@ export function createInfoHandler() {
|
|||||||
res.json({ success: true, worktreePath: null, branchName: null });
|
res.json({ success: true, worktreePath: null, branchName: null });
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get worktree info failed");
|
logError(error, 'Get worktree info failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* POST /init-git endpoint - Initialize a git repository in a directory
|
* POST /init-git endpoint - Initialize a git repository in a directory
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import { existsSync } from "fs";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { join } from "path";
|
import { join } from 'path';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -21,39 +21,42 @@ export function createInitGitHandler() {
|
|||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
res.status(400).json({
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath required",
|
error: 'projectPath required',
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if .git already exists
|
// Check if .git already exists
|
||||||
const gitDirPath = join(projectPath, ".git");
|
const gitDirPath = join(projectPath, '.git');
|
||||||
if (existsSync(gitDirPath)) {
|
try {
|
||||||
|
await secureFs.access(gitDirPath);
|
||||||
|
// .git exists
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
result: {
|
result: {
|
||||||
initialized: false,
|
initialized: false,
|
||||||
message: "Git repository already exists",
|
message: 'Git repository already exists',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
} catch {
|
||||||
|
// .git doesn't exist, continue with initialization
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize git and create an initial empty commit
|
// Initialize git and create an initial empty commit
|
||||||
await execAsync(
|
await execAsync(`git init && git commit --allow-empty -m "Initial commit"`, {
|
||||||
`git init && git commit --allow-empty -m "Initial commit"`,
|
cwd: projectPath,
|
||||||
{ cwd: projectPath }
|
});
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
result: {
|
result: {
|
||||||
initialized: true,
|
initialized: true,
|
||||||
message: "Git repository initialized with initial commit",
|
message: 'Git repository initialized with initial commit',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Init git failed");
|
logError(error, 'Init git failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,13 +5,13 @@
|
|||||||
* Does NOT include tracked branches - only real worktrees with separate directories.
|
* Does NOT include tracked branches - only real worktrees with separate directories.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import { existsSync } from "fs";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { isGitRepo } from "@automaker/git-utils";
|
import { isGitRepo } from '@automaker/git-utils';
|
||||||
import { getErrorMessage, logError, normalizePath } from "../common.js";
|
import { getErrorMessage, logError, normalizePath } from '../common.js';
|
||||||
import { readAllWorktreeMetadata, type WorktreePRInfo } from "../../../lib/worktree-metadata.js";
|
import { readAllWorktreeMetadata, type WorktreePRInfo } from '../../../lib/worktree-metadata.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -28,10 +28,10 @@ interface WorktreeInfo {
|
|||||||
|
|
||||||
async function getCurrentBranch(cwd: string): Promise<string> {
|
async function getCurrentBranch(cwd: string): Promise<string> {
|
||||||
try {
|
try {
|
||||||
const { stdout } = await execAsync("git branch --show-current", { cwd });
|
const { stdout } = await execAsync('git branch --show-current', { cwd });
|
||||||
return stdout.trim();
|
return stdout.trim();
|
||||||
} catch {
|
} catch {
|
||||||
return "";
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ export function createListHandler() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath) {
|
if (!projectPath) {
|
||||||
res.status(400).json({ success: false, error: "projectPath required" });
|
res.status(400).json({ success: false, error: 'projectPath required' });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -57,28 +57,35 @@ export function createListHandler() {
|
|||||||
const currentBranch = await getCurrentBranch(projectPath);
|
const currentBranch = await getCurrentBranch(projectPath);
|
||||||
|
|
||||||
// Get actual worktrees from git
|
// Get actual worktrees from git
|
||||||
const { stdout } = await execAsync("git worktree list --porcelain", {
|
const { stdout } = await execAsync('git worktree list --porcelain', {
|
||||||
cwd: projectPath,
|
cwd: projectPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
const worktrees: WorktreeInfo[] = [];
|
const worktrees: WorktreeInfo[] = [];
|
||||||
const removedWorktrees: Array<{ path: string; branch: string }> = [];
|
const removedWorktrees: Array<{ path: string; branch: string }> = [];
|
||||||
const lines = stdout.split("\n");
|
const lines = stdout.split('\n');
|
||||||
let current: { path?: string; branch?: string } = {};
|
let current: { path?: string; branch?: string } = {};
|
||||||
let isFirst = true;
|
let isFirst = true;
|
||||||
|
|
||||||
// First pass: detect removed worktrees
|
// First pass: detect removed worktrees
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
if (line.startsWith("worktree ")) {
|
if (line.startsWith('worktree ')) {
|
||||||
current.path = normalizePath(line.slice(9));
|
current.path = normalizePath(line.slice(9));
|
||||||
} else if (line.startsWith("branch ")) {
|
} else if (line.startsWith('branch ')) {
|
||||||
current.branch = line.slice(7).replace("refs/heads/", "");
|
current.branch = line.slice(7).replace('refs/heads/', '');
|
||||||
} else if (line === "") {
|
} else if (line === '') {
|
||||||
if (current.path && current.branch) {
|
if (current.path && current.branch) {
|
||||||
const isMainWorktree = isFirst;
|
const isMainWorktree = isFirst;
|
||||||
// Check if the worktree directory actually exists
|
// Check if the worktree directory actually exists
|
||||||
// Skip checking/pruning the main worktree (projectPath itself)
|
// Skip checking/pruning the main worktree (projectPath itself)
|
||||||
if (!isMainWorktree && !existsSync(current.path)) {
|
let worktreeExists = false;
|
||||||
|
try {
|
||||||
|
await secureFs.access(current.path);
|
||||||
|
worktreeExists = true;
|
||||||
|
} catch {
|
||||||
|
worktreeExists = false;
|
||||||
|
}
|
||||||
|
if (!isMainWorktree && !worktreeExists) {
|
||||||
// Worktree directory doesn't exist - it was manually deleted
|
// Worktree directory doesn't exist - it was manually deleted
|
||||||
removedWorktrees.push({
|
removedWorktrees.push({
|
||||||
path: current.path,
|
path: current.path,
|
||||||
@@ -103,7 +110,7 @@ export function createListHandler() {
|
|||||||
// Prune removed worktrees from git (only if any were detected)
|
// Prune removed worktrees from git (only if any were detected)
|
||||||
if (removedWorktrees.length > 0) {
|
if (removedWorktrees.length > 0) {
|
||||||
try {
|
try {
|
||||||
await execAsync("git worktree prune", { cwd: projectPath });
|
await execAsync('git worktree prune', { cwd: projectPath });
|
||||||
} catch {
|
} catch {
|
||||||
// Prune failed, but we'll still report the removed worktrees
|
// Prune failed, but we'll still report the removed worktrees
|
||||||
}
|
}
|
||||||
@@ -116,13 +123,12 @@ export function createListHandler() {
|
|||||||
if (includeDetails) {
|
if (includeDetails) {
|
||||||
for (const worktree of worktrees) {
|
for (const worktree of worktrees) {
|
||||||
try {
|
try {
|
||||||
const { stdout: statusOutput } = await execAsync(
|
const { stdout: statusOutput } = await execAsync('git status --porcelain', {
|
||||||
"git status --porcelain",
|
cwd: worktree.path,
|
||||||
{ cwd: worktree.path }
|
});
|
||||||
);
|
|
||||||
const changedFiles = statusOutput
|
const changedFiles = statusOutput
|
||||||
.trim()
|
.trim()
|
||||||
.split("\n")
|
.split('\n')
|
||||||
.filter((line) => line.trim());
|
.filter((line) => line.trim());
|
||||||
worktree.hasChanges = changedFiles.length > 0;
|
worktree.hasChanges = changedFiles.length > 0;
|
||||||
worktree.changedFilesCount = changedFiles.length;
|
worktree.changedFilesCount = changedFiles.length;
|
||||||
@@ -141,13 +147,13 @@ export function createListHandler() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
worktrees,
|
worktrees,
|
||||||
removedWorktrees: removedWorktrees.length > 0 ? removedWorktrees : undefined,
|
removedWorktrees: removedWorktrees.length > 0 ? removedWorktrees : undefined,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "List worktrees failed");
|
logError(error, 'List worktrees failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -2,12 +2,12 @@
|
|||||||
* POST /status endpoint - Get worktree status
|
* POST /status endpoint - Get worktree status
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Request, Response } from "express";
|
import type { Request, Response } from 'express';
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import fs from "fs/promises";
|
import * as secureFs from '../../../lib/secure-fs.js';
|
||||||
import { getErrorMessage, logError } from "../common.js";
|
import { getErrorMessage, logError } from '../common.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
|
|
||||||
@@ -20,53 +20,50 @@ export function createStatusHandler() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (!projectPath || !featureId) {
|
if (!projectPath || !featureId) {
|
||||||
res
|
res.status(400).json({
|
||||||
.status(400)
|
success: false,
|
||||||
.json({
|
error: 'projectPath and featureId required',
|
||||||
success: false,
|
});
|
||||||
error: "projectPath and featureId required",
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Git worktrees are stored in project directory
|
// Git worktrees are stored in project directory
|
||||||
const worktreePath = path.join(projectPath, ".worktrees", featureId);
|
const worktreePath = path.join(projectPath, '.worktrees', featureId);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await fs.access(worktreePath);
|
await secureFs.access(worktreePath);
|
||||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
const { stdout: status } = await execAsync('git status --porcelain', {
|
||||||
cwd: worktreePath,
|
cwd: worktreePath,
|
||||||
});
|
});
|
||||||
const files = status
|
const files = status
|
||||||
.split("\n")
|
.split('\n')
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((line) => line.slice(3));
|
.map((line) => line.slice(3));
|
||||||
const { stdout: diffStat } = await execAsync("git diff --stat", {
|
const { stdout: diffStat } = await execAsync('git diff --stat', {
|
||||||
|
cwd: worktreePath,
|
||||||
|
});
|
||||||
|
const { stdout: logOutput } = await execAsync('git log --oneline -5 --format="%h %s"', {
|
||||||
cwd: worktreePath,
|
cwd: worktreePath,
|
||||||
});
|
});
|
||||||
const { stdout: logOutput } = await execAsync(
|
|
||||||
'git log --oneline -5 --format="%h %s"',
|
|
||||||
{ cwd: worktreePath }
|
|
||||||
);
|
|
||||||
|
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
modifiedFiles: files.length,
|
modifiedFiles: files.length,
|
||||||
files,
|
files,
|
||||||
diffStat: diffStat.trim(),
|
diffStat: diffStat.trim(),
|
||||||
recentCommits: logOutput.trim().split("\n").filter(Boolean),
|
recentCommits: logOutput.trim().split('\n').filter(Boolean),
|
||||||
});
|
});
|
||||||
} catch {
|
} catch {
|
||||||
res.json({
|
res.json({
|
||||||
success: true,
|
success: true,
|
||||||
modifiedFiles: 0,
|
modifiedFiles: 0,
|
||||||
files: [],
|
files: [],
|
||||||
diffStat: "",
|
diffStat: '',
|
||||||
recentCommits: [],
|
recentCommits: [],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error, "Get worktree status failed");
|
logError(error, 'Get worktree status failed');
|
||||||
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
res.status(500).json({ success: false, error: getErrorMessage(error) });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3,22 +3,18 @@
|
|||||||
* Manages conversation sessions and streams responses via WebSocket
|
* Manages conversation sessions and streams responses via WebSocket
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import * as secureFs from "../lib/secure-fs.js";
|
import * as secureFs from '../lib/secure-fs.js';
|
||||||
import type { EventEmitter } from "../lib/events.js";
|
import type { EventEmitter } from '../lib/events.js';
|
||||||
import type { ExecuteOptions } from "@automaker/types";
|
import type { ExecuteOptions } from '@automaker/types';
|
||||||
import {
|
import { readImageAsBase64, buildPromptWithImages, isAbortError } from '@automaker/utils';
|
||||||
readImageAsBase64,
|
import { ProviderFactory } from '../providers/provider-factory.js';
|
||||||
buildPromptWithImages,
|
import { createChatOptions, validateWorkingDirectory } from '../lib/sdk-options.js';
|
||||||
isAbortError,
|
import { PathNotAllowedError } from '@automaker/platform';
|
||||||
} from "@automaker/utils";
|
|
||||||
import { ProviderFactory } from "../providers/provider-factory.js";
|
|
||||||
import { createChatOptions } from "../lib/sdk-options.js";
|
|
||||||
import { isPathAllowed, PathNotAllowedError } from "@automaker/platform";
|
|
||||||
|
|
||||||
interface Message {
|
interface Message {
|
||||||
id: string;
|
id: string;
|
||||||
role: "user" | "assistant";
|
role: 'user' | 'assistant';
|
||||||
content: string;
|
content: string;
|
||||||
images?: Array<{
|
images?: Array<{
|
||||||
data: string;
|
data: string;
|
||||||
@@ -58,8 +54,8 @@ export class AgentService {
|
|||||||
private events: EventEmitter;
|
private events: EventEmitter;
|
||||||
|
|
||||||
constructor(dataDir: string, events: EventEmitter) {
|
constructor(dataDir: string, events: EventEmitter) {
|
||||||
this.stateDir = path.join(dataDir, "agent-sessions");
|
this.stateDir = path.join(dataDir, 'agent-sessions');
|
||||||
this.metadataFile = path.join(dataDir, "sessions-metadata.json");
|
this.metadataFile = path.join(dataDir, 'sessions-metadata.json');
|
||||||
this.events = events;
|
this.events = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,12 +82,8 @@ export class AgentService {
|
|||||||
const effectiveWorkingDirectory = workingDirectory || process.cwd();
|
const effectiveWorkingDirectory = workingDirectory || process.cwd();
|
||||||
const resolvedWorkingDirectory = path.resolve(effectiveWorkingDirectory);
|
const resolvedWorkingDirectory = path.resolve(effectiveWorkingDirectory);
|
||||||
|
|
||||||
// Validate that the working directory is allowed
|
// Validate that the working directory is allowed using centralized validation
|
||||||
if (!isPathAllowed(resolvedWorkingDirectory)) {
|
validateWorkingDirectory(resolvedWorkingDirectory);
|
||||||
throw new Error(
|
|
||||||
`Working directory ${effectiveWorkingDirectory} is not allowed`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.sessions.set(sessionId, {
|
this.sessions.set(sessionId, {
|
||||||
messages,
|
messages,
|
||||||
@@ -132,7 +124,7 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (session.isRunning) {
|
if (session.isRunning) {
|
||||||
throw new Error("Agent is already processing a message");
|
throw new Error('Agent is already processing a message');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update session model if provided
|
// Update session model if provided
|
||||||
@@ -142,7 +134,7 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read images and convert to base64
|
// Read images and convert to base64
|
||||||
const images: Message["images"] = [];
|
const images: Message['images'] = [];
|
||||||
if (imagePaths && imagePaths.length > 0) {
|
if (imagePaths && imagePaths.length > 0) {
|
||||||
for (const imagePath of imagePaths) {
|
for (const imagePath of imagePaths) {
|
||||||
try {
|
try {
|
||||||
@@ -153,10 +145,7 @@ export class AgentService {
|
|||||||
filename: imageData.filename,
|
filename: imageData.filename,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(
|
console.error(`[AgentService] Failed to load image ${imagePath}:`, error);
|
||||||
`[AgentService] Failed to load image ${imagePath}:`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -164,7 +153,7 @@ export class AgentService {
|
|||||||
// Add user message
|
// Add user message
|
||||||
const userMessage: Message = {
|
const userMessage: Message = {
|
||||||
id: this.generateId(),
|
id: this.generateId(),
|
||||||
role: "user",
|
role: 'user',
|
||||||
content: message,
|
content: message,
|
||||||
images: images.length > 0 ? images : undefined,
|
images: images.length > 0 ? images : undefined,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
@@ -182,7 +171,7 @@ export class AgentService {
|
|||||||
|
|
||||||
// Emit user message event
|
// Emit user message event
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: "message",
|
type: 'message',
|
||||||
message: userMessage,
|
message: userMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -212,15 +201,14 @@ export class AgentService {
|
|||||||
|
|
||||||
// Build options for provider
|
// Build options for provider
|
||||||
const options: ExecuteOptions = {
|
const options: ExecuteOptions = {
|
||||||
prompt: "", // Will be set below based on images
|
prompt: '', // Will be set below based on images
|
||||||
model: effectiveModel,
|
model: effectiveModel,
|
||||||
cwd: workingDirectory || session.workingDirectory,
|
cwd: workingDirectory || session.workingDirectory,
|
||||||
systemPrompt: this.getSystemPrompt(),
|
systemPrompt: this.getSystemPrompt(),
|
||||||
maxTurns: maxTurns,
|
maxTurns: maxTurns,
|
||||||
allowedTools: allowedTools,
|
allowedTools: allowedTools,
|
||||||
abortController: session.abortController!,
|
abortController: session.abortController!,
|
||||||
conversationHistory:
|
conversationHistory: conversationHistory.length > 0 ? conversationHistory : undefined,
|
||||||
conversationHistory.length > 0 ? conversationHistory : undefined,
|
|
||||||
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
|
sdkSessionId: session.sdkSessionId, // Pass SDK session ID for resuming
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -239,30 +227,28 @@ export class AgentService {
|
|||||||
const stream = provider.executeQuery(options);
|
const stream = provider.executeQuery(options);
|
||||||
|
|
||||||
let currentAssistantMessage: Message | null = null;
|
let currentAssistantMessage: Message | null = null;
|
||||||
let responseText = "";
|
let responseText = '';
|
||||||
const toolUses: Array<{ name: string; input: unknown }> = [];
|
const toolUses: Array<{ name: string; input: unknown }> = [];
|
||||||
|
|
||||||
for await (const msg of stream) {
|
for await (const msg of stream) {
|
||||||
// Capture SDK session ID from any message and persist it
|
// Capture SDK session ID from any message and persist it
|
||||||
if (msg.session_id && !session.sdkSessionId) {
|
if (msg.session_id && !session.sdkSessionId) {
|
||||||
session.sdkSessionId = msg.session_id;
|
session.sdkSessionId = msg.session_id;
|
||||||
console.log(
|
console.log(`[AgentService] Captured SDK session ID: ${msg.session_id}`);
|
||||||
`[AgentService] Captured SDK session ID: ${msg.session_id}`
|
|
||||||
);
|
|
||||||
// Persist the SDK session ID to ensure conversation continuity across server restarts
|
// Persist the SDK session ID to ensure conversation continuity across server restarts
|
||||||
await this.updateSession(sessionId, { sdkSessionId: msg.session_id });
|
await this.updateSession(sessionId, { sdkSessionId: msg.session_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (msg.type === "assistant") {
|
if (msg.type === 'assistant') {
|
||||||
if (msg.message?.content) {
|
if (msg.message?.content) {
|
||||||
for (const block of msg.message.content) {
|
for (const block of msg.message.content) {
|
||||||
if (block.type === "text") {
|
if (block.type === 'text') {
|
||||||
responseText += block.text;
|
responseText += block.text;
|
||||||
|
|
||||||
if (!currentAssistantMessage) {
|
if (!currentAssistantMessage) {
|
||||||
currentAssistantMessage = {
|
currentAssistantMessage = {
|
||||||
id: this.generateId(),
|
id: this.generateId(),
|
||||||
role: "assistant",
|
role: 'assistant',
|
||||||
content: responseText,
|
content: responseText,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
};
|
};
|
||||||
@@ -272,27 +258,27 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: "stream",
|
type: 'stream',
|
||||||
messageId: currentAssistantMessage.id,
|
messageId: currentAssistantMessage.id,
|
||||||
content: responseText,
|
content: responseText,
|
||||||
isComplete: false,
|
isComplete: false,
|
||||||
});
|
});
|
||||||
} else if (block.type === "tool_use") {
|
} else if (block.type === 'tool_use') {
|
||||||
const toolUse = {
|
const toolUse = {
|
||||||
name: block.name || "unknown",
|
name: block.name || 'unknown',
|
||||||
input: block.input,
|
input: block.input,
|
||||||
};
|
};
|
||||||
toolUses.push(toolUse);
|
toolUses.push(toolUse);
|
||||||
|
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: "tool_use",
|
type: 'tool_use',
|
||||||
tool: toolUse,
|
tool: toolUse,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (msg.type === "result") {
|
} else if (msg.type === 'result') {
|
||||||
if (msg.subtype === "success" && msg.result) {
|
if (msg.subtype === 'success' && msg.result) {
|
||||||
if (currentAssistantMessage) {
|
if (currentAssistantMessage) {
|
||||||
currentAssistantMessage.content = msg.result;
|
currentAssistantMessage.content = msg.result;
|
||||||
responseText = msg.result;
|
responseText = msg.result;
|
||||||
@@ -300,7 +286,7 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: "complete",
|
type: 'complete',
|
||||||
messageId: currentAssistantMessage?.id,
|
messageId: currentAssistantMessage?.id,
|
||||||
content: responseText,
|
content: responseText,
|
||||||
toolUses,
|
toolUses,
|
||||||
@@ -324,14 +310,14 @@ export class AgentService {
|
|||||||
return { success: false, aborted: true };
|
return { success: false, aborted: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
console.error("[AgentService] Error:", error);
|
console.error('[AgentService] Error:', error);
|
||||||
|
|
||||||
session.isRunning = false;
|
session.isRunning = false;
|
||||||
session.abortController = null;
|
session.abortController = null;
|
||||||
|
|
||||||
const errorMessage: Message = {
|
const errorMessage: Message = {
|
||||||
id: this.generateId(),
|
id: this.generateId(),
|
||||||
role: "assistant",
|
role: 'assistant',
|
||||||
content: `Error: ${(error as Error).message}`,
|
content: `Error: ${(error as Error).message}`,
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
isError: true,
|
isError: true,
|
||||||
@@ -341,7 +327,7 @@ export class AgentService {
|
|||||||
await this.saveSession(sessionId, session.messages);
|
await this.saveSession(sessionId, session.messages);
|
||||||
|
|
||||||
this.emitAgentEvent(sessionId, {
|
this.emitAgentEvent(sessionId, {
|
||||||
type: "error",
|
type: 'error',
|
||||||
error: (error as Error).message,
|
error: (error as Error).message,
|
||||||
message: errorMessage,
|
message: errorMessage,
|
||||||
});
|
});
|
||||||
@@ -356,7 +342,7 @@ export class AgentService {
|
|||||||
getHistory(sessionId: string) {
|
getHistory(sessionId: string) {
|
||||||
const session = this.sessions.get(sessionId);
|
const session = this.sessions.get(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return { success: false, error: "Session not found" };
|
return { success: false, error: 'Session not found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -372,7 +358,7 @@ export class AgentService {
|
|||||||
async stopExecution(sessionId: string) {
|
async stopExecution(sessionId: string) {
|
||||||
const session = this.sessions.get(sessionId);
|
const session = this.sessions.get(sessionId);
|
||||||
if (!session) {
|
if (!session) {
|
||||||
return { success: false, error: "Session not found" };
|
return { success: false, error: 'Session not found' };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (session.abortController) {
|
if (session.abortController) {
|
||||||
@@ -404,7 +390,7 @@ export class AgentService {
|
|||||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = (await secureFs.readFile(sessionFile, "utf-8")) as string;
|
const data = (await secureFs.readFile(sessionFile, 'utf-8')) as string;
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
} catch {
|
} catch {
|
||||||
return [];
|
return [];
|
||||||
@@ -415,23 +401,16 @@ export class AgentService {
|
|||||||
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
const sessionFile = path.join(this.stateDir, `${sessionId}.json`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await secureFs.writeFile(
|
await secureFs.writeFile(sessionFile, JSON.stringify(messages, null, 2), 'utf-8');
|
||||||
sessionFile,
|
|
||||||
JSON.stringify(messages, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
await this.updateSessionTimestamp(sessionId);
|
await this.updateSessionTimestamp(sessionId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("[AgentService] Failed to save session:", error);
|
console.error('[AgentService] Failed to save session:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadMetadata(): Promise<Record<string, SessionMetadata>> {
|
async loadMetadata(): Promise<Record<string, SessionMetadata>> {
|
||||||
try {
|
try {
|
||||||
const data = (await secureFs.readFile(
|
const data = (await secureFs.readFile(this.metadataFile, 'utf-8')) as string;
|
||||||
this.metadataFile,
|
|
||||||
"utf-8"
|
|
||||||
)) as string;
|
|
||||||
return JSON.parse(data);
|
return JSON.parse(data);
|
||||||
} catch {
|
} catch {
|
||||||
return {};
|
return {};
|
||||||
@@ -439,11 +418,7 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async saveMetadata(metadata: Record<string, SessionMetadata>): Promise<void> {
|
async saveMetadata(metadata: Record<string, SessionMetadata>): Promise<void> {
|
||||||
await secureFs.writeFile(
|
await secureFs.writeFile(this.metadataFile, JSON.stringify(metadata, null, 2), 'utf-8');
|
||||||
this.metadataFile,
|
|
||||||
JSON.stringify(metadata, null, 2),
|
|
||||||
"utf-8"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async updateSessionTimestamp(sessionId: string): Promise<void> {
|
async updateSessionTimestamp(sessionId: string): Promise<void> {
|
||||||
@@ -463,8 +438,7 @@ export class AgentService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return sessions.sort(
|
return sessions.sort(
|
||||||
(a, b) =>
|
(a, b) => new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
||||||
new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime()
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,21 +452,15 @@ export class AgentService {
|
|||||||
const metadata = await this.loadMetadata();
|
const metadata = await this.loadMetadata();
|
||||||
|
|
||||||
// Determine the effective working directory
|
// Determine the effective working directory
|
||||||
const effectiveWorkingDirectory =
|
const effectiveWorkingDirectory = workingDirectory || projectPath || process.cwd();
|
||||||
workingDirectory || projectPath || process.cwd();
|
|
||||||
const resolvedWorkingDirectory = path.resolve(effectiveWorkingDirectory);
|
const resolvedWorkingDirectory = path.resolve(effectiveWorkingDirectory);
|
||||||
|
|
||||||
// Validate that the working directory is allowed
|
// Validate that the working directory is allowed using centralized validation
|
||||||
if (!isPathAllowed(resolvedWorkingDirectory)) {
|
validateWorkingDirectory(resolvedWorkingDirectory);
|
||||||
throw new PathNotAllowedError(effectiveWorkingDirectory);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate that projectPath is allowed if provided
|
// Validate that projectPath is allowed if provided
|
||||||
if (projectPath) {
|
if (projectPath) {
|
||||||
const resolvedProjectPath = path.resolve(projectPath);
|
validateWorkingDirectory(projectPath);
|
||||||
if (!isPathAllowed(resolvedProjectPath)) {
|
|
||||||
throw new PathNotAllowedError(projectPath);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const session: SessionMetadata = {
|
const session: SessionMetadata = {
|
||||||
@@ -569,11 +537,8 @@ export class AgentService {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitAgentEvent(
|
private emitAgentEvent(sessionId: string, data: Record<string, unknown>): void {
|
||||||
sessionId: string,
|
this.events.emit('agent:stream', { sessionId, ...data });
|
||||||
data: Record<string, unknown>
|
|
||||||
): void {
|
|
||||||
this.events.emit("agent:stream", { sessionId, ...data });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getSystemPrompt(): string {
|
private getSystemPrompt(): string {
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -7,10 +7,10 @@
|
|||||||
* Developers should configure their projects to use the PORT environment variable.
|
* Developers should configure their projects to use the PORT environment variable.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { spawn, execSync, type ChildProcess } from "child_process";
|
import { spawn, execSync, type ChildProcess } from 'child_process';
|
||||||
import { existsSync } from "fs";
|
import * as secureFs from '../lib/secure-fs.js';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import net from "net";
|
import net from 'net';
|
||||||
|
|
||||||
export interface DevServerInfo {
|
export interface DevServerInfo {
|
||||||
worktreePath: string;
|
worktreePath: string;
|
||||||
@@ -40,12 +40,12 @@ class DevServerService {
|
|||||||
// Then check if the system has it in use
|
// Then check if the system has it in use
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const server = net.createServer();
|
const server = net.createServer();
|
||||||
server.once("error", () => resolve(false));
|
server.once('error', () => resolve(false));
|
||||||
server.once("listening", () => {
|
server.once('listening', () => {
|
||||||
server.close();
|
server.close();
|
||||||
resolve(true);
|
resolve(true);
|
||||||
});
|
});
|
||||||
server.listen(port, "127.0.0.1");
|
server.listen(port, '127.0.0.1');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,21 +54,21 @@ class DevServerService {
|
|||||||
*/
|
*/
|
||||||
private killProcessOnPort(port: number): void {
|
private killProcessOnPort(port: number): void {
|
||||||
try {
|
try {
|
||||||
if (process.platform === "win32") {
|
if (process.platform === 'win32') {
|
||||||
// Windows: find and kill process on port
|
// Windows: find and kill process on port
|
||||||
const result = execSync(`netstat -ano | findstr :${port}`, { encoding: "utf-8" });
|
const result = execSync(`netstat -ano | findstr :${port}`, { encoding: 'utf-8' });
|
||||||
const lines = result.trim().split("\n");
|
const lines = result.trim().split('\n');
|
||||||
const pids = new Set<string>();
|
const pids = new Set<string>();
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
const parts = line.trim().split(/\s+/);
|
const parts = line.trim().split(/\s+/);
|
||||||
const pid = parts[parts.length - 1];
|
const pid = parts[parts.length - 1];
|
||||||
if (pid && pid !== "0") {
|
if (pid && pid !== '0') {
|
||||||
pids.add(pid);
|
pids.add(pid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (const pid of pids) {
|
for (const pid of pids) {
|
||||||
try {
|
try {
|
||||||
execSync(`taskkill /F /PID ${pid}`, { stdio: "ignore" });
|
execSync(`taskkill /F /PID ${pid}`, { stdio: 'ignore' });
|
||||||
console.log(`[DevServerService] Killed process ${pid} on port ${port}`);
|
console.log(`[DevServerService] Killed process ${pid} on port ${port}`);
|
||||||
} catch {
|
} catch {
|
||||||
// Process may have already exited
|
// Process may have already exited
|
||||||
@@ -77,11 +77,11 @@ class DevServerService {
|
|||||||
} else {
|
} else {
|
||||||
// macOS/Linux: use lsof to find and kill process
|
// macOS/Linux: use lsof to find and kill process
|
||||||
try {
|
try {
|
||||||
const result = execSync(`lsof -ti:${port}`, { encoding: "utf-8" });
|
const result = execSync(`lsof -ti:${port}`, { encoding: 'utf-8' });
|
||||||
const pids = result.trim().split("\n").filter(Boolean);
|
const pids = result.trim().split('\n').filter(Boolean);
|
||||||
for (const pid of pids) {
|
for (const pid of pids) {
|
||||||
try {
|
try {
|
||||||
execSync(`kill -9 ${pid}`, { stdio: "ignore" });
|
execSync(`kill -9 ${pid}`, { stdio: 'ignore' });
|
||||||
console.log(`[DevServerService] Killed process ${pid} on port ${port}`);
|
console.log(`[DevServerService] Killed process ${pid} on port ${port}`);
|
||||||
} catch {
|
} catch {
|
||||||
// Process may have already exited
|
// Process may have already exited
|
||||||
@@ -127,37 +127,47 @@ class DevServerService {
|
|||||||
throw new Error(`No available ports found between ${BASE_PORT} and ${MAX_PORT}`);
|
throw new Error(`No available ports found between ${BASE_PORT} and ${MAX_PORT}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to check if a file exists using secureFs
|
||||||
|
*/
|
||||||
|
private async fileExists(filePath: string): Promise<boolean> {
|
||||||
|
try {
|
||||||
|
await secureFs.access(filePath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detect the package manager used in a directory
|
* Detect the package manager used in a directory
|
||||||
*/
|
*/
|
||||||
private detectPackageManager(
|
private async detectPackageManager(dir: string): Promise<'npm' | 'yarn' | 'pnpm' | 'bun' | null> {
|
||||||
dir: string
|
if (await this.fileExists(path.join(dir, 'bun.lockb'))) return 'bun';
|
||||||
): "npm" | "yarn" | "pnpm" | "bun" | null {
|
if (await this.fileExists(path.join(dir, 'pnpm-lock.yaml'))) return 'pnpm';
|
||||||
if (existsSync(path.join(dir, "bun.lockb"))) return "bun";
|
if (await this.fileExists(path.join(dir, 'yarn.lock'))) return 'yarn';
|
||||||
if (existsSync(path.join(dir, "pnpm-lock.yaml"))) return "pnpm";
|
if (await this.fileExists(path.join(dir, 'package-lock.json'))) return 'npm';
|
||||||
if (existsSync(path.join(dir, "yarn.lock"))) return "yarn";
|
if (await this.fileExists(path.join(dir, 'package.json'))) return 'npm'; // Default
|
||||||
if (existsSync(path.join(dir, "package-lock.json"))) return "npm";
|
|
||||||
if (existsSync(path.join(dir, "package.json"))) return "npm"; // Default
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the dev script command for a directory
|
* Get the dev script command for a directory
|
||||||
*/
|
*/
|
||||||
private getDevCommand(dir: string): { cmd: string; args: string[] } | null {
|
private async getDevCommand(dir: string): Promise<{ cmd: string; args: string[] } | null> {
|
||||||
const pm = this.detectPackageManager(dir);
|
const pm = await this.detectPackageManager(dir);
|
||||||
if (!pm) return null;
|
if (!pm) return null;
|
||||||
|
|
||||||
switch (pm) {
|
switch (pm) {
|
||||||
case "bun":
|
case 'bun':
|
||||||
return { cmd: "bun", args: ["run", "dev"] };
|
return { cmd: 'bun', args: ['run', 'dev'] };
|
||||||
case "pnpm":
|
case 'pnpm':
|
||||||
return { cmd: "pnpm", args: ["run", "dev"] };
|
return { cmd: 'pnpm', args: ['run', 'dev'] };
|
||||||
case "yarn":
|
case 'yarn':
|
||||||
return { cmd: "yarn", args: ["dev"] };
|
return { cmd: 'yarn', args: ['dev'] };
|
||||||
case "npm":
|
case 'npm':
|
||||||
default:
|
default:
|
||||||
return { cmd: "npm", args: ["run", "dev"] };
|
return { cmd: 'npm', args: ['run', 'dev'] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,7 +202,7 @@ class DevServerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Verify the worktree exists
|
// Verify the worktree exists
|
||||||
if (!existsSync(worktreePath)) {
|
if (!(await this.fileExists(worktreePath))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `Worktree path does not exist: ${worktreePath}`,
|
error: `Worktree path does not exist: ${worktreePath}`,
|
||||||
@@ -200,8 +210,8 @@ class DevServerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check for package.json
|
// Check for package.json
|
||||||
const packageJsonPath = path.join(worktreePath, "package.json");
|
const packageJsonPath = path.join(worktreePath, 'package.json');
|
||||||
if (!existsSync(packageJsonPath)) {
|
if (!(await this.fileExists(packageJsonPath))) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: `No package.json found in: ${worktreePath}`,
|
error: `No package.json found in: ${worktreePath}`,
|
||||||
@@ -209,7 +219,7 @@ class DevServerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get dev command
|
// Get dev command
|
||||||
const devCommand = this.getDevCommand(worktreePath);
|
const devCommand = await this.getDevCommand(worktreePath);
|
||||||
if (!devCommand) {
|
if (!devCommand) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
@@ -224,7 +234,7 @@ class DevServerService {
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
return {
|
return {
|
||||||
success: false,
|
success: false,
|
||||||
error: error instanceof Error ? error.message : "Port allocation failed",
|
error: error instanceof Error ? error.message : 'Port allocation failed',
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,14 +251,10 @@ class DevServerService {
|
|||||||
// Small delay to ensure related ports are freed
|
// Small delay to ensure related ports are freed
|
||||||
await new Promise((resolve) => setTimeout(resolve, 100));
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
||||||
|
|
||||||
|
console.log(`[DevServerService] Starting dev server on port ${port}`);
|
||||||
|
console.log(`[DevServerService] Working directory (cwd): ${worktreePath}`);
|
||||||
console.log(
|
console.log(
|
||||||
`[DevServerService] Starting dev server on port ${port}`
|
`[DevServerService] Command: ${devCommand.cmd} ${devCommand.args.join(' ')} with PORT=${port}`
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[DevServerService] Working directory (cwd): ${worktreePath}`
|
|
||||||
);
|
|
||||||
console.log(
|
|
||||||
`[DevServerService] Command: ${devCommand.cmd} ${devCommand.args.join(" ")} with PORT=${port}`
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// Spawn the dev process with PORT environment variable
|
// Spawn the dev process with PORT environment variable
|
||||||
@@ -260,7 +266,7 @@ class DevServerService {
|
|||||||
const devProcess = spawn(devCommand.cmd, devCommand.args, {
|
const devProcess = spawn(devCommand.cmd, devCommand.args, {
|
||||||
cwd: worktreePath,
|
cwd: worktreePath,
|
||||||
env,
|
env,
|
||||||
stdio: ["ignore", "pipe", "pipe"],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
detached: false,
|
detached: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -269,29 +275,27 @@ class DevServerService {
|
|||||||
|
|
||||||
// Log output for debugging
|
// Log output for debugging
|
||||||
if (devProcess.stdout) {
|
if (devProcess.stdout) {
|
||||||
devProcess.stdout.on("data", (data: Buffer) => {
|
devProcess.stdout.on('data', (data: Buffer) => {
|
||||||
console.log(`[DevServer:${port}] ${data.toString().trim()}`);
|
console.log(`[DevServer:${port}] ${data.toString().trim()}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (devProcess.stderr) {
|
if (devProcess.stderr) {
|
||||||
devProcess.stderr.on("data", (data: Buffer) => {
|
devProcess.stderr.on('data', (data: Buffer) => {
|
||||||
const msg = data.toString().trim();
|
const msg = data.toString().trim();
|
||||||
console.error(`[DevServer:${port}] ${msg}`);
|
console.error(`[DevServer:${port}] ${msg}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
devProcess.on("error", (error) => {
|
devProcess.on('error', (error) => {
|
||||||
console.error(`[DevServerService] Process error:`, error);
|
console.error(`[DevServerService] Process error:`, error);
|
||||||
status.error = error.message;
|
status.error = error.message;
|
||||||
this.allocatedPorts.delete(port);
|
this.allocatedPorts.delete(port);
|
||||||
this.runningServers.delete(worktreePath);
|
this.runningServers.delete(worktreePath);
|
||||||
});
|
});
|
||||||
|
|
||||||
devProcess.on("exit", (code) => {
|
devProcess.on('exit', (code) => {
|
||||||
console.log(
|
console.log(`[DevServerService] Process for ${worktreePath} exited with code ${code}`);
|
||||||
`[DevServerService] Process for ${worktreePath} exited with code ${code}`
|
|
||||||
);
|
|
||||||
status.exited = true;
|
status.exited = true;
|
||||||
this.allocatedPorts.delete(port);
|
this.allocatedPorts.delete(port);
|
||||||
this.runningServers.delete(worktreePath);
|
this.runningServers.delete(worktreePath);
|
||||||
@@ -348,7 +352,9 @@ class DevServerService {
|
|||||||
// If we don't have a record of this server, it may have crashed/exited on its own
|
// If we don't have a record of this server, it may have crashed/exited on its own
|
||||||
// Return success so the frontend can clear its state
|
// Return success so the frontend can clear its state
|
||||||
if (!server) {
|
if (!server) {
|
||||||
console.log(`[DevServerService] No server record for ${worktreePath}, may have already stopped`);
|
console.log(
|
||||||
|
`[DevServerService] No server record for ${worktreePath}, may have already stopped`
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
result: {
|
result: {
|
||||||
@@ -362,7 +368,7 @@ class DevServerService {
|
|||||||
|
|
||||||
// Kill the process
|
// Kill the process
|
||||||
if (server.process && !server.process.killed) {
|
if (server.process && !server.process.killed) {
|
||||||
server.process.kill("SIGTERM");
|
server.process.kill('SIGTERM');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Free the port
|
// Free the port
|
||||||
@@ -447,13 +453,13 @@ export function getDevServerService(): DevServerService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Cleanup on process exit
|
// Cleanup on process exit
|
||||||
process.on("SIGTERM", async () => {
|
process.on('SIGTERM', async () => {
|
||||||
if (devServerServiceInstance) {
|
if (devServerServiceInstance) {
|
||||||
await devServerServiceInstance.stopAll();
|
await devServerServiceInstance.stopAll();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
process.on("SIGINT", async () => {
|
process.on('SIGINT', async () => {
|
||||||
if (devServerServiceInstance) {
|
if (devServerServiceInstance) {
|
||||||
await devServerServiceInstance.stopAll();
|
await devServerServiceInstance.stopAll();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,37 +1,33 @@
|
|||||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||||
import { EventEmitter } from "events";
|
import { EventEmitter } from 'events';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import os from "os";
|
import os from 'os';
|
||||||
import fs from "fs/promises";
|
import fs from 'fs/promises';
|
||||||
|
|
||||||
// Mock child_process
|
// Mock child_process
|
||||||
vi.mock("child_process", () => ({
|
vi.mock('child_process', () => ({
|
||||||
spawn: vi.fn(),
|
spawn: vi.fn(),
|
||||||
execSync: vi.fn(),
|
execSync: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock fs existsSync
|
// Mock secure-fs
|
||||||
vi.mock("fs", async (importOriginal) => {
|
vi.mock('@/lib/secure-fs.js', () => ({
|
||||||
const actual = await importOriginal<typeof import("fs")>();
|
access: vi.fn(),
|
||||||
return {
|
}));
|
||||||
...actual,
|
|
||||||
existsSync: vi.fn(),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock net
|
// Mock net
|
||||||
vi.mock("net", () => ({
|
vi.mock('net', () => ({
|
||||||
default: {
|
default: {
|
||||||
createServer: vi.fn(),
|
createServer: vi.fn(),
|
||||||
},
|
},
|
||||||
createServer: vi.fn(),
|
createServer: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
import { spawn, execSync } from "child_process";
|
import { spawn, execSync } from 'child_process';
|
||||||
import { existsSync } from "fs";
|
import * as secureFs from '@/lib/secure-fs.js';
|
||||||
import net from "net";
|
import net from 'net';
|
||||||
|
|
||||||
describe("dev-server-service.ts", () => {
|
describe('dev-server-service.ts', () => {
|
||||||
let testDir: string;
|
let testDir: string;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -41,20 +37,20 @@ describe("dev-server-service.ts", () => {
|
|||||||
testDir = path.join(os.tmpdir(), `dev-server-test-${Date.now()}`);
|
testDir = path.join(os.tmpdir(), `dev-server-test-${Date.now()}`);
|
||||||
await fs.mkdir(testDir, { recursive: true });
|
await fs.mkdir(testDir, { recursive: true });
|
||||||
|
|
||||||
// Default mock for existsSync - return true
|
// Default mock for secureFs.access - return resolved (file exists)
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
// Default mock for net.createServer - port available
|
// Default mock for net.createServer - port available
|
||||||
const mockServer = new EventEmitter() as any;
|
const mockServer = new EventEmitter() as any;
|
||||||
mockServer.listen = vi.fn().mockImplementation((port: number, host: string) => {
|
mockServer.listen = vi.fn().mockImplementation((port: number, host: string) => {
|
||||||
process.nextTick(() => mockServer.emit("listening"));
|
process.nextTick(() => mockServer.emit('listening'));
|
||||||
});
|
});
|
||||||
mockServer.close = vi.fn();
|
mockServer.close = vi.fn();
|
||||||
vi.mocked(net.createServer).mockReturnValue(mockServer);
|
vi.mocked(net.createServer).mockReturnValue(mockServer);
|
||||||
|
|
||||||
// Default mock for execSync - no process on port
|
// Default mock for execSync - no process on port
|
||||||
vi.mocked(execSync).mockImplementation(() => {
|
vi.mocked(execSync).mockImplementation(() => {
|
||||||
throw new Error("No process found");
|
throw new Error('No process found');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -66,11 +62,9 @@ describe("dev-server-service.ts", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getDevServerService", () => {
|
describe('getDevServerService', () => {
|
||||||
it("should return a singleton instance", async () => {
|
it('should return a singleton instance', async () => {
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
|
|
||||||
const instance1 = getDevServerService();
|
const instance1 = getDevServerService();
|
||||||
const instance2 = getDevServerService();
|
const instance2 = getDevServerService();
|
||||||
@@ -79,148 +73,125 @@ describe("dev-server-service.ts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("startDevServer", () => {
|
describe('startDevServer', () => {
|
||||||
it("should return error if worktree path does not exist", async () => {
|
it('should return error if worktree path does not exist', async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(false);
|
vi.mocked(secureFs.access).mockRejectedValueOnce(new Error('File not found'));
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
const result = await service.startDevServer(
|
const result = await service.startDevServer('/project', '/nonexistent/worktree');
|
||||||
"/project",
|
|
||||||
"/nonexistent/worktree"
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.error).toContain("does not exist");
|
expect(result.error).toContain('does not exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return error if no package.json found", async () => {
|
it('should return error if no package.json found', async () => {
|
||||||
vi.mocked(existsSync).mockImplementation((p: any) => {
|
vi.mocked(secureFs.access).mockImplementation(async (p: any) => {
|
||||||
if (p.includes("package.json")) return false;
|
if (typeof p === 'string' && p.includes('package.json')) {
|
||||||
return true;
|
throw new Error('File not found');
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
const result = await service.startDevServer(testDir, testDir);
|
const result = await service.startDevServer(testDir, testDir);
|
||||||
|
|
||||||
expect(result.success).toBe(false);
|
expect(result.success).toBe(false);
|
||||||
expect(result.error).toContain("No package.json found");
|
expect(result.error).toContain('No package.json found');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect npm as package manager with package-lock.json", async () => {
|
it('should detect npm as package manager with package-lock.json', async () => {
|
||||||
vi.mocked(existsSync).mockImplementation((p: any) => {
|
vi.mocked(secureFs.access).mockImplementation(async (p: any) => {
|
||||||
if (p.includes("bun.lockb")) return false;
|
const pathStr = typeof p === 'string' ? p : '';
|
||||||
if (p.includes("pnpm-lock.yaml")) return false;
|
if (pathStr.includes('bun.lockb')) throw new Error('Not found');
|
||||||
if (p.includes("yarn.lock")) return false;
|
if (pathStr.includes('pnpm-lock.yaml')) throw new Error('Not found');
|
||||||
if (p.includes("package-lock.json")) return true;
|
if (pathStr.includes('yarn.lock')) throw new Error('Not found');
|
||||||
if (p.includes("package.json")) return true;
|
if (pathStr.includes('package-lock.json')) return undefined;
|
||||||
return true;
|
if (pathStr.includes('package.json')) return undefined;
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
await service.startDevServer(testDir, testDir);
|
await service.startDevServer(testDir, testDir);
|
||||||
|
|
||||||
expect(spawn).toHaveBeenCalledWith(
|
expect(spawn).toHaveBeenCalledWith('npm', ['run', 'dev'], expect.any(Object));
|
||||||
"npm",
|
|
||||||
["run", "dev"],
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect yarn as package manager with yarn.lock", async () => {
|
it('should detect yarn as package manager with yarn.lock', async () => {
|
||||||
vi.mocked(existsSync).mockImplementation((p: any) => {
|
vi.mocked(secureFs.access).mockImplementation(async (p: any) => {
|
||||||
if (p.includes("bun.lockb")) return false;
|
const pathStr = typeof p === 'string' ? p : '';
|
||||||
if (p.includes("pnpm-lock.yaml")) return false;
|
if (pathStr.includes('bun.lockb')) throw new Error('Not found');
|
||||||
if (p.includes("yarn.lock")) return true;
|
if (pathStr.includes('pnpm-lock.yaml')) throw new Error('Not found');
|
||||||
if (p.includes("package.json")) return true;
|
if (pathStr.includes('yarn.lock')) return undefined;
|
||||||
return true;
|
if (pathStr.includes('package.json')) return undefined;
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
await service.startDevServer(testDir, testDir);
|
await service.startDevServer(testDir, testDir);
|
||||||
|
|
||||||
expect(spawn).toHaveBeenCalledWith("yarn", ["dev"], expect.any(Object));
|
expect(spawn).toHaveBeenCalledWith('yarn', ['dev'], expect.any(Object));
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect pnpm as package manager with pnpm-lock.yaml", async () => {
|
it('should detect pnpm as package manager with pnpm-lock.yaml', async () => {
|
||||||
vi.mocked(existsSync).mockImplementation((p: any) => {
|
vi.mocked(secureFs.access).mockImplementation(async (p: any) => {
|
||||||
if (p.includes("bun.lockb")) return false;
|
const pathStr = typeof p === 'string' ? p : '';
|
||||||
if (p.includes("pnpm-lock.yaml")) return true;
|
if (pathStr.includes('bun.lockb')) throw new Error('Not found');
|
||||||
if (p.includes("package.json")) return true;
|
if (pathStr.includes('pnpm-lock.yaml')) return undefined;
|
||||||
return true;
|
if (pathStr.includes('package.json')) return undefined;
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
await service.startDevServer(testDir, testDir);
|
await service.startDevServer(testDir, testDir);
|
||||||
|
|
||||||
expect(spawn).toHaveBeenCalledWith(
|
expect(spawn).toHaveBeenCalledWith('pnpm', ['run', 'dev'], expect.any(Object));
|
||||||
"pnpm",
|
|
||||||
["run", "dev"],
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should detect bun as package manager with bun.lockb", async () => {
|
it('should detect bun as package manager with bun.lockb', async () => {
|
||||||
vi.mocked(existsSync).mockImplementation((p: any) => {
|
vi.mocked(secureFs.access).mockImplementation(async (p: any) => {
|
||||||
if (p.includes("bun.lockb")) return true;
|
const pathStr = typeof p === 'string' ? p : '';
|
||||||
if (p.includes("package.json")) return true;
|
if (pathStr.includes('bun.lockb')) return undefined;
|
||||||
return true;
|
if (pathStr.includes('package.json')) return undefined;
|
||||||
|
return undefined;
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
await service.startDevServer(testDir, testDir);
|
await service.startDevServer(testDir, testDir);
|
||||||
|
|
||||||
expect(spawn).toHaveBeenCalledWith(
|
expect(spawn).toHaveBeenCalledWith('bun', ['run', 'dev'], expect.any(Object));
|
||||||
"bun",
|
|
||||||
["run", "dev"],
|
|
||||||
expect.any(Object)
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return existing server info if already running", async () => {
|
it('should return existing server info if already running', async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
// Start first server
|
// Start first server
|
||||||
@@ -230,18 +201,16 @@ describe("dev-server-service.ts", () => {
|
|||||||
// Try to start again - should return existing
|
// Try to start again - should return existing
|
||||||
const result2 = await service.startDevServer(testDir, testDir);
|
const result2 = await service.startDevServer(testDir, testDir);
|
||||||
expect(result2.success).toBe(true);
|
expect(result2.success).toBe(true);
|
||||||
expect(result2.result?.message).toContain("already running");
|
expect(result2.result?.message).toContain('already running');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should start dev server successfully", async () => {
|
it('should start dev server successfully', async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
const result = await service.startDevServer(testDir, testDir);
|
const result = await service.startDevServer(testDir, testDir);
|
||||||
@@ -249,32 +218,28 @@ describe("dev-server-service.ts", () => {
|
|||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.result).toBeDefined();
|
expect(result.result).toBeDefined();
|
||||||
expect(result.result?.port).toBeGreaterThanOrEqual(3001);
|
expect(result.result?.port).toBeGreaterThanOrEqual(3001);
|
||||||
expect(result.result?.url).toContain("http://localhost:");
|
expect(result.result?.url).toContain('http://localhost:');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("stopDevServer", () => {
|
describe('stopDevServer', () => {
|
||||||
it("should return success if server not found", async () => {
|
it('should return success if server not found', async () => {
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
const result = await service.stopDevServer("/nonexistent/path");
|
const result = await service.stopDevServer('/nonexistent/path');
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(result.result?.message).toContain("already stopped");
|
expect(result.result?.message).toContain('already stopped');
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should stop a running server", async () => {
|
it('should stop a running server', async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
// Start server
|
// Start server
|
||||||
@@ -284,15 +249,13 @@ describe("dev-server-service.ts", () => {
|
|||||||
const result = await service.stopDevServer(testDir);
|
const result = await service.stopDevServer(testDir);
|
||||||
|
|
||||||
expect(result.success).toBe(true);
|
expect(result.success).toBe(true);
|
||||||
expect(mockProcess.kill).toHaveBeenCalledWith("SIGTERM");
|
expect(mockProcess.kill).toHaveBeenCalledWith('SIGTERM');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("listDevServers", () => {
|
describe('listDevServers', () => {
|
||||||
it("should return empty list when no servers running", async () => {
|
it('should return empty list when no servers running', async () => {
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
const result = service.listDevServers();
|
const result = service.listDevServers();
|
||||||
@@ -301,15 +264,13 @@ describe("dev-server-service.ts", () => {
|
|||||||
expect(result.result.servers).toEqual([]);
|
expect(result.result.servers).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should list running servers", async () => {
|
it('should list running servers', async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
await service.startDevServer(testDir, testDir);
|
await service.startDevServer(testDir, testDir);
|
||||||
@@ -322,25 +283,21 @@ describe("dev-server-service.ts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("isRunning", () => {
|
describe('isRunning', () => {
|
||||||
it("should return false for non-running server", async () => {
|
it('should return false for non-running server', async () => {
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
expect(service.isRunning("/some/path")).toBe(false);
|
expect(service.isRunning('/some/path')).toBe(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return true for running server", async () => {
|
it('should return true for running server', async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
await service.startDevServer(testDir, testDir);
|
await service.startDevServer(testDir, testDir);
|
||||||
@@ -349,25 +306,21 @@ describe("dev-server-service.ts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getServerInfo", () => {
|
describe('getServerInfo', () => {
|
||||||
it("should return undefined for non-running server", async () => {
|
it('should return undefined for non-running server', async () => {
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
expect(service.getServerInfo("/some/path")).toBeUndefined();
|
expect(service.getServerInfo('/some/path')).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should return info for running server", async () => {
|
it('should return info for running server', async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
await service.startDevServer(testDir, testDir);
|
await service.startDevServer(testDir, testDir);
|
||||||
@@ -379,16 +332,14 @@ describe("dev-server-service.ts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getAllocatedPorts", () => {
|
describe('getAllocatedPorts', () => {
|
||||||
it("should return allocated ports", async () => {
|
it('should return allocated ports', async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
await service.startDevServer(testDir, testDir);
|
await service.startDevServer(testDir, testDir);
|
||||||
@@ -399,16 +350,14 @@ describe("dev-server-service.ts", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("stopAll", () => {
|
describe('stopAll', () => {
|
||||||
it("should stop all running servers", async () => {
|
it('should stop all running servers', async () => {
|
||||||
vi.mocked(existsSync).mockReturnValue(true);
|
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
const mockProcess = createMockProcess();
|
const mockProcess = createMockProcess();
|
||||||
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
vi.mocked(spawn).mockReturnValue(mockProcess as any);
|
||||||
|
|
||||||
const { getDevServerService } = await import(
|
const { getDevServerService } = await import('@/services/dev-server-service.js');
|
||||||
"@/services/dev-server-service.js"
|
|
||||||
);
|
|
||||||
const service = getDevServerService();
|
const service = getDevServerService();
|
||||||
|
|
||||||
await service.startDevServer(testDir, testDir);
|
await service.startDevServer(testDir, testDir);
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,9 @@
|
|||||||
import { Label } from "@/components/ui/label";
|
import { useState } from 'react';
|
||||||
import { Palette } from "lucide-react";
|
import { Label } from '@/components/ui/label';
|
||||||
import { themeOptions } from "@/config/theme-options";
|
import { Palette, Moon, Sun } from 'lucide-react';
|
||||||
import { cn } from "@/lib/utils";
|
import { darkThemes, lightThemes } from '@/config/theme-options';
|
||||||
import type { Theme, Project } from "../shared/types";
|
import { cn } from '@/lib/utils';
|
||||||
|
import type { Theme, Project } from '../shared/types';
|
||||||
|
|
||||||
interface AppearanceSectionProps {
|
interface AppearanceSectionProps {
|
||||||
effectiveTheme: Theme;
|
effectiveTheme: Theme;
|
||||||
@@ -15,13 +16,17 @@ export function AppearanceSection({
|
|||||||
currentProject,
|
currentProject,
|
||||||
onThemeChange,
|
onThemeChange,
|
||||||
}: AppearanceSectionProps) {
|
}: AppearanceSectionProps) {
|
||||||
|
const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
|
||||||
|
|
||||||
|
const themesToShow = activeTab === 'dark' ? darkThemes : lightThemes;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
"rounded-2xl overflow-hidden",
|
'rounded-2xl overflow-hidden',
|
||||||
"border border-border/50",
|
'border border-border/50',
|
||||||
"bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl",
|
'bg-gradient-to-br from-card/90 via-card/70 to-card/80 backdrop-blur-xl',
|
||||||
"shadow-sm shadow-black/5"
|
'shadow-sm shadow-black/5'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
<div className="p-6 border-b border-border/50 bg-gradient-to-r from-transparent via-accent/5 to-transparent">
|
||||||
@@ -37,43 +42,69 @@ export function AppearanceSection({
|
|||||||
</div>
|
</div>
|
||||||
<div className="p-6 space-y-4">
|
<div className="p-6 space-y-4">
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<Label className="text-foreground font-medium">
|
<div className="flex items-center justify-between">
|
||||||
Theme{" "}
|
<Label className="text-foreground font-medium">
|
||||||
<span className="text-muted-foreground font-normal">
|
Theme{' '}
|
||||||
{currentProject ? `(for ${currentProject.name})` : "(Global)"}
|
<span className="text-muted-foreground font-normal">
|
||||||
</span>
|
{currentProject ? `(for ${currentProject.name})` : '(Global)'}
|
||||||
</Label>
|
</span>
|
||||||
|
</Label>
|
||||||
|
{/* Dark/Light Tabs */}
|
||||||
|
<div className="flex gap-1 p-1 rounded-lg bg-accent/30">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('dark')}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200',
|
||||||
|
activeTab === 'dark'
|
||||||
|
? 'bg-brand-500 text-white shadow-sm'
|
||||||
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Moon className="w-3.5 h-3.5" />
|
||||||
|
Dark
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('light')}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-1.5 px-3 py-1.5 rounded-md text-sm font-medium transition-all duration-200',
|
||||||
|
activeTab === 'light'
|
||||||
|
? 'bg-brand-500 text-white shadow-sm'
|
||||||
|
: 'text-muted-foreground hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Sun className="w-3.5 h-3.5" />
|
||||||
|
Light
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
|
||||||
{themeOptions.map(({ value, label, Icon, testId }) => {
|
{themesToShow.map(({ value, label, Icon, testId, color }) => {
|
||||||
const isActive = effectiveTheme === value;
|
const isActive = effectiveTheme === value;
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
key={value}
|
key={value}
|
||||||
onClick={() => onThemeChange(value)}
|
onClick={() => onThemeChange(value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"group flex items-center justify-center gap-2.5 px-4 py-3.5 rounded-xl",
|
'group flex items-center justify-center gap-2.5 px-4 py-3.5 rounded-xl',
|
||||||
"text-sm font-medium transition-all duration-200 ease-out",
|
'text-sm font-medium transition-all duration-200 ease-out',
|
||||||
isActive
|
isActive
|
||||||
? [
|
? [
|
||||||
"bg-gradient-to-br from-brand-500/15 to-brand-600/10",
|
'bg-gradient-to-br from-brand-500/15 to-brand-600/10',
|
||||||
"border-2 border-brand-500/40",
|
'border-2 border-brand-500/40',
|
||||||
"text-foreground",
|
'text-foreground',
|
||||||
"shadow-md shadow-brand-500/10",
|
'shadow-md shadow-brand-500/10',
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
"bg-accent/30 hover:bg-accent/50",
|
'bg-accent/30 hover:bg-accent/50',
|
||||||
"border border-border/50 hover:border-border",
|
'border border-border/50 hover:border-border',
|
||||||
"text-muted-foreground hover:text-foreground",
|
'text-muted-foreground hover:text-foreground',
|
||||||
"hover:shadow-sm",
|
'hover:shadow-sm',
|
||||||
],
|
],
|
||||||
"hover:scale-[1.02] active:scale-[0.98]"
|
'hover:scale-[1.02] active:scale-[0.98]'
|
||||||
)}
|
)}
|
||||||
data-testid={testId}
|
data-testid={testId}
|
||||||
>
|
>
|
||||||
<Icon className={cn(
|
<Icon className="w-4 h-4 transition-all duration-200" style={{ color }} />
|
||||||
"w-4 h-4 transition-all duration-200",
|
|
||||||
isActive ? "text-brand-500" : "group-hover:text-brand-400"
|
|
||||||
)} />
|
|
||||||
<span>{label}</span>
|
<span>{label}</span>
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
// Shared TypeScript types for settings view components
|
// Shared TypeScript types for settings view components
|
||||||
|
// Theme type is now imported from the central theme-options config
|
||||||
|
export { type Theme } from '@/config/theme-options';
|
||||||
|
|
||||||
export interface CliStatus {
|
export interface CliStatus {
|
||||||
success: boolean;
|
success: boolean;
|
||||||
@@ -17,31 +19,13 @@ export interface CliStatus {
|
|||||||
error?: string;
|
error?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Theme =
|
export type KanbanDetailLevel = 'minimal' | 'standard' | 'detailed';
|
||||||
| "dark"
|
|
||||||
| "light"
|
|
||||||
| "retro"
|
|
||||||
| "dracula"
|
|
||||||
| "nord"
|
|
||||||
| "monokai"
|
|
||||||
| "tokyonight"
|
|
||||||
| "solarized"
|
|
||||||
| "gruvbox"
|
|
||||||
| "catppuccin"
|
|
||||||
| "onedark"
|
|
||||||
| "synthwave"
|
|
||||||
| "red"
|
|
||||||
| "cream"
|
|
||||||
| "sunset"
|
|
||||||
| "gray";
|
|
||||||
|
|
||||||
export type KanbanDetailLevel = "minimal" | "standard" | "detailed";
|
|
||||||
|
|
||||||
export interface Project {
|
export interface Project {
|
||||||
id: string;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
theme?: Theme;
|
theme?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ApiKeys {
|
export interface ApiKeys {
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
import { useState } from 'react';
|
||||||
import { ArrowRight, ArrowLeft, Check } from "lucide-react";
|
import { Button } from '@/components/ui/button';
|
||||||
import { themeOptions } from "@/config/theme-options";
|
import { ArrowRight, ArrowLeft, Check, Moon, Sun } from 'lucide-react';
|
||||||
import { useAppStore } from "@/store/app-store";
|
import { darkThemes, lightThemes } from '@/config/theme-options';
|
||||||
import { cn } from "@/lib/utils";
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { cn } from '@/lib/utils';
|
||||||
|
|
||||||
interface ThemeStepProps {
|
interface ThemeStepProps {
|
||||||
onNext: () => void;
|
onNext: () => void;
|
||||||
@@ -11,6 +12,7 @@ interface ThemeStepProps {
|
|||||||
|
|
||||||
export function ThemeStep({ onNext, onBack }: ThemeStepProps) {
|
export function ThemeStep({ onNext, onBack }: ThemeStepProps) {
|
||||||
const { theme, setTheme, setPreviewTheme } = useAppStore();
|
const { theme, setTheme, setPreviewTheme } = useAppStore();
|
||||||
|
const [activeTab, setActiveTab] = useState<'dark' | 'light'>('dark');
|
||||||
|
|
||||||
const handleThemeHover = (themeValue: string) => {
|
const handleThemeHover = (themeValue: string) => {
|
||||||
setPreviewTheme(themeValue as typeof theme);
|
setPreviewTheme(themeValue as typeof theme);
|
||||||
@@ -25,19 +27,47 @@ export function ThemeStep({ onNext, onBack }: ThemeStepProps) {
|
|||||||
setPreviewTheme(null);
|
setPreviewTheme(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const themesToShow = activeTab === 'dark' ? darkThemes : lightThemes;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h2 className="text-3xl font-bold text-foreground mb-3">
|
<h2 className="text-3xl font-bold text-foreground mb-3">Choose Your Theme</h2>
|
||||||
Choose Your Theme
|
|
||||||
</h2>
|
|
||||||
<p className="text-muted-foreground max-w-md mx-auto">
|
<p className="text-muted-foreground max-w-md mx-auto">
|
||||||
Pick a theme that suits your style. Hover to preview, click to select.
|
Pick a theme that suits your style. Hover to preview, click to select.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Dark/Light Tabs */}
|
||||||
|
<div className="flex justify-center gap-2">
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('dark')}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-6 py-2.5 rounded-lg font-medium transition-all duration-200',
|
||||||
|
activeTab === 'dark'
|
||||||
|
? 'bg-brand-500 text-white shadow-lg shadow-brand-500/25'
|
||||||
|
: 'bg-accent/50 text-muted-foreground hover:bg-accent hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Moon className="w-4 h-4" />
|
||||||
|
Dark Themes
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setActiveTab('light')}
|
||||||
|
className={cn(
|
||||||
|
'flex items-center gap-2 px-6 py-2.5 rounded-lg font-medium transition-all duration-200',
|
||||||
|
activeTab === 'light'
|
||||||
|
? 'bg-brand-500 text-white shadow-lg shadow-brand-500/25'
|
||||||
|
: 'bg-accent/50 text-muted-foreground hover:bg-accent hover:text-foreground'
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Sun className="w-4 h-4" />
|
||||||
|
Light Themes
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 gap-3">
|
||||||
{themeOptions.map((option) => {
|
{themesToShow.map((option) => {
|
||||||
const Icon = option.Icon;
|
const Icon = option.Icon;
|
||||||
const isSelected = theme === option.value;
|
const isSelected = theme === option.value;
|
||||||
|
|
||||||
@@ -49,11 +79,11 @@ export function ThemeStep({ onNext, onBack }: ThemeStepProps) {
|
|||||||
onMouseLeave={handleThemeLeave}
|
onMouseLeave={handleThemeLeave}
|
||||||
onClick={() => handleThemeClick(option.value)}
|
onClick={() => handleThemeClick(option.value)}
|
||||||
className={cn(
|
className={cn(
|
||||||
"relative flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all duration-200",
|
'relative flex flex-col items-center gap-2 p-4 rounded-lg border-2 transition-all duration-200',
|
||||||
"hover:scale-105 hover:shadow-lg",
|
'hover:scale-105 hover:shadow-lg',
|
||||||
isSelected
|
isSelected
|
||||||
? "border-brand-500 bg-brand-500/10"
|
? 'border-brand-500 bg-brand-500/10'
|
||||||
: "border-border hover:border-brand-400 bg-card"
|
: 'border-border hover:border-brand-400 bg-card'
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isSelected && (
|
{isSelected && (
|
||||||
@@ -61,10 +91,8 @@ export function ThemeStep({ onNext, onBack }: ThemeStepProps) {
|
|||||||
<Check className="w-4 h-4 text-brand-500" />
|
<Check className="w-4 h-4 text-brand-500" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Icon className="w-6 h-6 text-foreground" />
|
<Icon className="w-6 h-6" style={{ color: option.color }} />
|
||||||
<span className="text-sm font-medium text-foreground">
|
<span className="text-sm font-medium text-foreground">{option.label}</span>
|
||||||
{option.label}
|
|
||||||
</span>
|
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -2,115 +2,342 @@ import {
|
|||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
Atom,
|
Atom,
|
||||||
Cat,
|
Cat,
|
||||||
|
Cherry,
|
||||||
CloudSun,
|
CloudSun,
|
||||||
Coffee,
|
Coffee,
|
||||||
Eclipse,
|
Eclipse,
|
||||||
|
Feather,
|
||||||
Flame,
|
Flame,
|
||||||
|
Flower2,
|
||||||
Ghost,
|
Ghost,
|
||||||
|
Github,
|
||||||
Heart,
|
Heart,
|
||||||
|
Leaf,
|
||||||
Moon,
|
Moon,
|
||||||
|
Palmtree,
|
||||||
Radio,
|
Radio,
|
||||||
|
Scroll,
|
||||||
Snowflake,
|
Snowflake,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Square,
|
Square,
|
||||||
Sun,
|
Sun,
|
||||||
|
Sunrise,
|
||||||
Terminal,
|
Terminal,
|
||||||
Trees,
|
Trees,
|
||||||
} from "lucide-react";
|
Waves,
|
||||||
import { Theme } from "@/components/views/settings-view/shared/types";
|
Wind,
|
||||||
|
} from 'lucide-react';
|
||||||
|
|
||||||
|
// Theme value type - all available themes
|
||||||
|
export type Theme =
|
||||||
|
// Dark themes (16)
|
||||||
|
| 'dark'
|
||||||
|
| 'retro'
|
||||||
|
| 'dracula'
|
||||||
|
| 'nord'
|
||||||
|
| 'monokai'
|
||||||
|
| 'tokyonight'
|
||||||
|
| 'solarized'
|
||||||
|
| 'gruvbox'
|
||||||
|
| 'catppuccin'
|
||||||
|
| 'onedark'
|
||||||
|
| 'synthwave'
|
||||||
|
| 'red'
|
||||||
|
| 'sunset'
|
||||||
|
| 'gray'
|
||||||
|
| 'forest'
|
||||||
|
| 'ocean'
|
||||||
|
// Light themes (16)
|
||||||
|
| 'light'
|
||||||
|
| 'cream'
|
||||||
|
| 'solarizedlight'
|
||||||
|
| 'github'
|
||||||
|
| 'paper'
|
||||||
|
| 'rose'
|
||||||
|
| 'mint'
|
||||||
|
| 'lavender'
|
||||||
|
| 'sand'
|
||||||
|
| 'sky'
|
||||||
|
| 'peach'
|
||||||
|
| 'snow'
|
||||||
|
| 'sepia'
|
||||||
|
| 'gruvboxlight'
|
||||||
|
| 'nordlight'
|
||||||
|
| 'blossom';
|
||||||
|
|
||||||
export interface ThemeOption {
|
export interface ThemeOption {
|
||||||
value: Theme;
|
value: Theme;
|
||||||
label: string;
|
label: string;
|
||||||
Icon: LucideIcon;
|
Icon: LucideIcon;
|
||||||
testId: string;
|
testId: string;
|
||||||
|
isDark: boolean;
|
||||||
|
color: string; // Primary/brand color for icon display
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// All theme options with dark/light categorization
|
||||||
export const themeOptions: ReadonlyArray<ThemeOption> = [
|
export const themeOptions: ReadonlyArray<ThemeOption> = [
|
||||||
{ value: "dark", label: "Dark", Icon: Moon, testId: "dark-mode-button" },
|
// Dark themes (16)
|
||||||
{ value: "light", label: "Light", Icon: Sun, testId: "light-mode-button" },
|
|
||||||
{
|
{
|
||||||
value: "retro",
|
value: 'dark',
|
||||||
label: "Retro",
|
label: 'Dark',
|
||||||
|
Icon: Moon,
|
||||||
|
testId: 'dark-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#3b82f6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'retro',
|
||||||
|
label: 'Retro',
|
||||||
Icon: Terminal,
|
Icon: Terminal,
|
||||||
testId: "retro-mode-button",
|
testId: 'retro-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#22c55e',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "dracula",
|
value: 'dracula',
|
||||||
label: "Dracula",
|
label: 'Dracula',
|
||||||
Icon: Ghost,
|
Icon: Ghost,
|
||||||
testId: "dracula-mode-button",
|
testId: 'dracula-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#bd93f9',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "nord",
|
value: 'nord',
|
||||||
label: "Nord",
|
label: 'Nord',
|
||||||
Icon: Snowflake,
|
Icon: Snowflake,
|
||||||
testId: "nord-mode-button",
|
testId: 'nord-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#88c0d0',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "monokai",
|
value: 'monokai',
|
||||||
label: "Monokai",
|
label: 'Monokai',
|
||||||
Icon: Flame,
|
Icon: Flame,
|
||||||
testId: "monokai-mode-button",
|
testId: 'monokai-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#f92672',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "tokyonight",
|
value: 'tokyonight',
|
||||||
label: "Tokyo Night",
|
label: 'Tokyo Night',
|
||||||
Icon: Sparkles,
|
Icon: Sparkles,
|
||||||
testId: "tokyonight-mode-button",
|
testId: 'tokyonight-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#bb9af7',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "solarized",
|
value: 'solarized',
|
||||||
label: "Solarized",
|
label: 'Solarized Dark',
|
||||||
Icon: Eclipse,
|
Icon: Eclipse,
|
||||||
testId: "solarized-mode-button",
|
testId: 'solarized-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#268bd2',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "gruvbox",
|
value: 'gruvbox',
|
||||||
label: "Gruvbox",
|
label: 'Gruvbox',
|
||||||
Icon: Trees,
|
Icon: Trees,
|
||||||
testId: "gruvbox-mode-button",
|
testId: 'gruvbox-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#fe8019',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "catppuccin",
|
value: 'catppuccin',
|
||||||
label: "Catppuccin",
|
label: 'Catppuccin',
|
||||||
Icon: Cat,
|
Icon: Cat,
|
||||||
testId: "catppuccin-mode-button",
|
testId: 'catppuccin-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#cba6f7',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "onedark",
|
value: 'onedark',
|
||||||
label: "One Dark",
|
label: 'One Dark',
|
||||||
Icon: Atom,
|
Icon: Atom,
|
||||||
testId: "onedark-mode-button",
|
testId: 'onedark-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#61afef',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "synthwave",
|
value: 'synthwave',
|
||||||
label: "Synthwave",
|
label: 'Synthwave',
|
||||||
Icon: Radio,
|
Icon: Radio,
|
||||||
testId: "synthwave-mode-button",
|
testId: 'synthwave-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#ff7edb',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "red",
|
value: 'red',
|
||||||
label: "Red",
|
label: 'Red',
|
||||||
Icon: Heart,
|
Icon: Heart,
|
||||||
testId: "red-mode-button",
|
testId: 'red-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#ef4444',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "cream",
|
value: 'sunset',
|
||||||
label: "Cream",
|
label: 'Sunset',
|
||||||
Icon: Coffee,
|
|
||||||
testId: "cream-mode-button",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
value: "sunset",
|
|
||||||
label: "Sunset",
|
|
||||||
Icon: CloudSun,
|
Icon: CloudSun,
|
||||||
testId: "sunset-mode-button",
|
testId: 'sunset-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#f97316',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
value: "gray",
|
value: 'gray',
|
||||||
label: "Gray",
|
label: 'Gray',
|
||||||
Icon: Square,
|
Icon: Square,
|
||||||
testId: "gray-mode-button",
|
testId: 'gray-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#6b7280',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'forest',
|
||||||
|
label: 'Forest',
|
||||||
|
Icon: Leaf,
|
||||||
|
testId: 'forest-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#22c55e',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'ocean',
|
||||||
|
label: 'Ocean',
|
||||||
|
Icon: Waves,
|
||||||
|
testId: 'ocean-mode-button',
|
||||||
|
isDark: true,
|
||||||
|
color: '#06b6d4',
|
||||||
|
},
|
||||||
|
// Light themes (16)
|
||||||
|
{
|
||||||
|
value: 'light',
|
||||||
|
label: 'Light',
|
||||||
|
Icon: Sun,
|
||||||
|
testId: 'light-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#3b82f6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'cream',
|
||||||
|
label: 'Cream',
|
||||||
|
Icon: Coffee,
|
||||||
|
testId: 'cream-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#b45309',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'solarizedlight',
|
||||||
|
label: 'Solarized Light',
|
||||||
|
Icon: Sunrise,
|
||||||
|
testId: 'solarizedlight-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#268bd2',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'github',
|
||||||
|
label: 'GitHub',
|
||||||
|
Icon: Github,
|
||||||
|
testId: 'github-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#0969da',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'paper',
|
||||||
|
label: 'Paper',
|
||||||
|
Icon: Scroll,
|
||||||
|
testId: 'paper-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#374151',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'rose',
|
||||||
|
label: 'Rose',
|
||||||
|
Icon: Flower2,
|
||||||
|
testId: 'rose-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#e11d48',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'mint',
|
||||||
|
label: 'Mint',
|
||||||
|
Icon: Wind,
|
||||||
|
testId: 'mint-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#0d9488',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'lavender',
|
||||||
|
label: 'Lavender',
|
||||||
|
Icon: Feather,
|
||||||
|
testId: 'lavender-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#8b5cf6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'sand',
|
||||||
|
label: 'Sand',
|
||||||
|
Icon: Palmtree,
|
||||||
|
testId: 'sand-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#d97706',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'sky',
|
||||||
|
label: 'Sky',
|
||||||
|
Icon: Sun,
|
||||||
|
testId: 'sky-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#0284c7',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'peach',
|
||||||
|
label: 'Peach',
|
||||||
|
Icon: Cherry,
|
||||||
|
testId: 'peach-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#ea580c',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'snow',
|
||||||
|
label: 'Snow',
|
||||||
|
Icon: Snowflake,
|
||||||
|
testId: 'snow-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#3b82f6',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'sepia',
|
||||||
|
label: 'Sepia',
|
||||||
|
Icon: Coffee,
|
||||||
|
testId: 'sepia-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#92400e',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'gruvboxlight',
|
||||||
|
label: 'Gruvbox Light',
|
||||||
|
Icon: Trees,
|
||||||
|
testId: 'gruvboxlight-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#d65d0e',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'nordlight',
|
||||||
|
label: 'Nord Light',
|
||||||
|
Icon: Snowflake,
|
||||||
|
testId: 'nordlight-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#5e81ac',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 'blossom',
|
||||||
|
label: 'Blossom',
|
||||||
|
Icon: Cherry,
|
||||||
|
testId: 'blossom-mode-button',
|
||||||
|
isDark: false,
|
||||||
|
color: '#ec4899',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
// Helper: Get only dark themes
|
||||||
|
export const darkThemes = themeOptions.filter((t) => t.isDark);
|
||||||
|
|
||||||
|
// Helper: Get only light themes
|
||||||
|
export const lightThemes = themeOptions.filter((t) => !t.isDark);
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import { createRootRoute, Outlet, useLocation, useNavigate } from "@tanstack/react-router";
|
import { createRootRoute, Outlet, useLocation, useNavigate } from '@tanstack/react-router';
|
||||||
import { useEffect, useState, useCallback } from "react";
|
import { useEffect, useState, useCallback, useDeferredValue } from 'react';
|
||||||
import { Sidebar } from "@/components/layout/sidebar";
|
import { Sidebar } from '@/components/layout/sidebar';
|
||||||
import { FileBrowserProvider, useFileBrowser, setGlobalFileBrowser } from "@/contexts/file-browser-context";
|
import {
|
||||||
import { useAppStore } from "@/store/app-store";
|
FileBrowserProvider,
|
||||||
import { useSetupStore } from "@/store/setup-store";
|
useFileBrowser,
|
||||||
import { getElectronAPI } from "@/lib/electron";
|
setGlobalFileBrowser,
|
||||||
import { Toaster } from "sonner";
|
} from '@/contexts/file-browser-context';
|
||||||
import { ThemeOption, themeOptions } from "@/config/theme-options";
|
import { useAppStore } from '@/store/app-store';
|
||||||
|
import { useSetupStore } from '@/store/setup-store';
|
||||||
|
import { getElectronAPI } from '@/lib/electron';
|
||||||
|
import { Toaster } from 'sonner';
|
||||||
|
import { ThemeOption, themeOptions } from '@/config/theme-options';
|
||||||
|
|
||||||
function RootLayoutContent() {
|
function RootLayoutContent() {
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const {
|
const { setIpcConnected, theme, currentProject, previewTheme, getEffectiveTheme } = useAppStore();
|
||||||
setIpcConnected,
|
|
||||||
theme,
|
|
||||||
currentProject,
|
|
||||||
previewTheme,
|
|
||||||
getEffectiveTheme,
|
|
||||||
} = useAppStore();
|
|
||||||
const { setupComplete } = useSetupStore();
|
const { setupComplete } = useSetupStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const [isMounted, setIsMounted] = useState(false);
|
const [isMounted, setIsMounted] = useState(false);
|
||||||
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
|
const [streamerPanelOpen, setStreamerPanelOpen] = useState(false);
|
||||||
const [setupHydrated, setSetupHydrated] = useState(() =>
|
const [setupHydrated, setSetupHydrated] = useState(
|
||||||
useSetupStore.persist?.hasHydrated?.() ?? false
|
() => useSetupStore.persist?.hasHydrated?.() ?? false
|
||||||
);
|
);
|
||||||
const { openFileBrowser } = useFileBrowser();
|
const { openFileBrowser } = useFileBrowser();
|
||||||
|
|
||||||
@@ -31,14 +29,14 @@ function RootLayoutContent() {
|
|||||||
const activeElement = document.activeElement;
|
const activeElement = document.activeElement;
|
||||||
if (activeElement) {
|
if (activeElement) {
|
||||||
const tagName = activeElement.tagName.toLowerCase();
|
const tagName = activeElement.tagName.toLowerCase();
|
||||||
if (tagName === "input" || tagName === "textarea" || tagName === "select") {
|
if (tagName === 'input' || tagName === 'textarea' || tagName === 'select') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (activeElement.getAttribute("contenteditable") === "true") {
|
if (activeElement.getAttribute('contenteditable') === 'true') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const role = activeElement.getAttribute("role");
|
const role = activeElement.getAttribute('role');
|
||||||
if (role === "textbox" || role === "searchbox" || role === "combobox") {
|
if (role === 'textbox' || role === 'searchbox' || role === 'combobox') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -47,20 +45,22 @@ function RootLayoutContent() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (event.key === "\\") {
|
if (event.key === '\\') {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
setStreamerPanelOpen((prev) => !prev);
|
setStreamerPanelOpen((prev) => !prev);
|
||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener("keydown", handleStreamerPanelShortcut);
|
window.addEventListener('keydown', handleStreamerPanelShortcut);
|
||||||
return () => {
|
return () => {
|
||||||
window.removeEventListener("keydown", handleStreamerPanelShortcut);
|
window.removeEventListener('keydown', handleStreamerPanelShortcut);
|
||||||
};
|
};
|
||||||
}, [handleStreamerPanelShortcut]);
|
}, [handleStreamerPanelShortcut]);
|
||||||
|
|
||||||
const effectiveTheme = getEffectiveTheme();
|
const effectiveTheme = getEffectiveTheme();
|
||||||
|
// Defer the theme value to keep UI responsive during rapid hover changes
|
||||||
|
const deferredTheme = useDeferredValue(effectiveTheme);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMounted(true);
|
setIsMounted(true);
|
||||||
@@ -78,7 +78,7 @@ function RootLayoutContent() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
if (typeof unsubscribe === "function") {
|
if (typeof unsubscribe === 'function') {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -88,10 +88,10 @@ function RootLayoutContent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!setupHydrated) return;
|
if (!setupHydrated) return;
|
||||||
|
|
||||||
if (!setupComplete && location.pathname !== "/setup") {
|
if (!setupComplete && location.pathname !== '/setup') {
|
||||||
navigate({ to: "/setup" });
|
navigate({ to: '/setup' });
|
||||||
} else if (setupComplete && location.pathname === "/setup") {
|
} else if (setupComplete && location.pathname === '/setup') {
|
||||||
navigate({ to: "/" });
|
navigate({ to: '/' });
|
||||||
}
|
}
|
||||||
}, [setupComplete, setupHydrated, location.pathname, navigate]);
|
}, [setupComplete, setupHydrated, location.pathname, navigate]);
|
||||||
|
|
||||||
@@ -105,9 +105,9 @@ function RootLayoutContent() {
|
|||||||
try {
|
try {
|
||||||
const api = getElectronAPI();
|
const api = getElectronAPI();
|
||||||
const result = await api.ping();
|
const result = await api.ping();
|
||||||
setIpcConnected(result === "pong");
|
setIpcConnected(result === 'pong');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("IPC connection failed:", error);
|
console.error('IPC connection failed:', error);
|
||||||
setIpcConnected(false);
|
setIpcConnected(false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -117,34 +117,34 @@ function RootLayoutContent() {
|
|||||||
|
|
||||||
// Restore to board view if a project was previously open
|
// Restore to board view if a project was previously open
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMounted && currentProject && location.pathname === "/") {
|
if (isMounted && currentProject && location.pathname === '/') {
|
||||||
navigate({ to: "/board" });
|
navigate({ to: '/board' });
|
||||||
}
|
}
|
||||||
}, [isMounted, currentProject, location.pathname, navigate]);
|
}, [isMounted, currentProject, location.pathname, navigate]);
|
||||||
|
|
||||||
// Apply theme class to document
|
// Apply theme class to document - use deferred value to avoid blocking UI
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const root = document.documentElement;
|
const root = document.documentElement;
|
||||||
// Remove all theme classes dynamically from themeOptions
|
// Remove all theme classes dynamically from themeOptions
|
||||||
const themeClasses = themeOptions
|
const themeClasses = themeOptions
|
||||||
.map((option) => option.value)
|
.map((option) => option.value)
|
||||||
.filter((theme) => theme !== "system" as ThemeOption['value']);
|
.filter((theme) => theme !== ('system' as ThemeOption['value']));
|
||||||
root.classList.remove(...themeClasses);
|
root.classList.remove(...themeClasses);
|
||||||
|
|
||||||
if (effectiveTheme === "dark") {
|
if (deferredTheme === 'dark') {
|
||||||
root.classList.add("dark");
|
root.classList.add('dark');
|
||||||
} else if (effectiveTheme === "system") {
|
} else if (deferredTheme === 'system') {
|
||||||
const isDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
|
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||||
root.classList.add(isDark ? "dark" : "light");
|
root.classList.add(isDark ? 'dark' : 'light');
|
||||||
} else if (effectiveTheme && effectiveTheme !== "light") {
|
} else if (deferredTheme && deferredTheme !== 'light') {
|
||||||
root.classList.add(effectiveTheme);
|
root.classList.add(deferredTheme);
|
||||||
} else {
|
} else {
|
||||||
root.classList.add("light");
|
root.classList.add('light');
|
||||||
}
|
}
|
||||||
}, [effectiveTheme, previewTheme, currentProject, theme]);
|
}, [deferredTheme]);
|
||||||
|
|
||||||
// Setup view is full-screen without sidebar
|
// Setup view is full-screen without sidebar
|
||||||
const isSetupRoute = location.pathname === "/setup";
|
const isSetupRoute = location.pathname === '/setup';
|
||||||
|
|
||||||
if (isSetupRoute) {
|
if (isSetupRoute) {
|
||||||
return (
|
return (
|
||||||
@@ -159,7 +159,7 @@ function RootLayoutContent() {
|
|||||||
<Sidebar />
|
<Sidebar />
|
||||||
<div
|
<div
|
||||||
className="flex-1 flex flex-col overflow-hidden transition-all duration-300"
|
className="flex-1 flex flex-col overflow-hidden transition-all duration-300"
|
||||||
style={{ marginRight: streamerPanelOpen ? "250px" : "0" }}
|
style={{ marginRight: streamerPanelOpen ? '250px' : '0' }}
|
||||||
>
|
>
|
||||||
<Outlet />
|
<Outlet />
|
||||||
</div>
|
</div>
|
||||||
@@ -167,7 +167,7 @@ function RootLayoutContent() {
|
|||||||
{/* Hidden streamer panel - opens with "\" key, pushes content */}
|
{/* Hidden streamer panel - opens with "\" key, pushes content */}
|
||||||
<div
|
<div
|
||||||
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
|
className={`fixed top-0 right-0 h-full w-[250px] bg-background border-l border-border transition-transform duration-300 ${
|
||||||
streamerPanelOpen ? "translate-x-0" : "translate-x-full"
|
streamerPanelOpen ? 'translate-x-0' : 'translate-x-full'
|
||||||
}`}
|
}`}
|
||||||
/>
|
/>
|
||||||
<Toaster richColors position="bottom-right" />
|
<Toaster richColors position="bottom-right" />
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import 'tailwindcss';
|
||||||
@import "tw-animate-css";
|
@import 'tw-animate-css';
|
||||||
|
|
||||||
|
/* Dark themes */
|
||||||
@custom-variant dark (&:is(.dark *));
|
@custom-variant dark (&:is(.dark *));
|
||||||
@custom-variant retro (&:is(.retro *));
|
@custom-variant retro (&:is(.retro *));
|
||||||
@custom-variant dracula (&:is(.dracula *));
|
@custom-variant dracula (&:is(.dracula *));
|
||||||
@@ -13,9 +14,27 @@
|
|||||||
@custom-variant onedark (&:is(.onedark *));
|
@custom-variant onedark (&:is(.onedark *));
|
||||||
@custom-variant synthwave (&:is(.synthwave *));
|
@custom-variant synthwave (&:is(.synthwave *));
|
||||||
@custom-variant red (&:is(.red *));
|
@custom-variant red (&:is(.red *));
|
||||||
@custom-variant cream (&:is(.cream *));
|
|
||||||
@custom-variant sunset (&:is(.sunset *));
|
@custom-variant sunset (&:is(.sunset *));
|
||||||
@custom-variant gray (&:is(.gray *));
|
@custom-variant gray (&:is(.gray *));
|
||||||
|
@custom-variant forest (&:is(.forest *));
|
||||||
|
@custom-variant ocean (&:is(.ocean *));
|
||||||
|
|
||||||
|
/* Light themes */
|
||||||
|
@custom-variant cream (&:is(.cream *));
|
||||||
|
@custom-variant solarizedlight (&:is(.solarizedlight *));
|
||||||
|
@custom-variant github (&:is(.github *));
|
||||||
|
@custom-variant paper (&:is(.paper *));
|
||||||
|
@custom-variant rose (&:is(.rose *));
|
||||||
|
@custom-variant mint (&:is(.mint *));
|
||||||
|
@custom-variant lavender (&:is(.lavender *));
|
||||||
|
@custom-variant sand (&:is(.sand *));
|
||||||
|
@custom-variant sky (&:is(.sky *));
|
||||||
|
@custom-variant peach (&:is(.peach *));
|
||||||
|
@custom-variant snow (&:is(.snow *));
|
||||||
|
@custom-variant sepia (&:is(.sepia *));
|
||||||
|
@custom-variant gruvboxlight (&:is(.gruvboxlight *));
|
||||||
|
@custom-variant nordlight (&:is(.nordlight *));
|
||||||
|
@custom-variant blossom (&:is(.blossom *));
|
||||||
|
|
||||||
@theme inline {
|
@theme inline {
|
||||||
--color-background: var(--background);
|
--color-background: var(--background);
|
||||||
@@ -201,7 +220,7 @@
|
|||||||
/* Card and popover backgrounds */
|
/* Card and popover backgrounds */
|
||||||
--card: oklch(0.14 0 0);
|
--card: oklch(0.14 0 0);
|
||||||
--card-foreground: oklch(1 0 0);
|
--card-foreground: oklch(1 0 0);
|
||||||
--popover: oklch(0.10 0 0);
|
--popover: oklch(0.1 0 0);
|
||||||
--popover-foreground: oklch(1 0 0);
|
--popover-foreground: oklch(1 0 0);
|
||||||
|
|
||||||
/* Brand colors - purple/violet theme */
|
/* Brand colors - purple/violet theme */
|
||||||
@@ -336,7 +355,6 @@
|
|||||||
--running-indicator-text: oklch(0.6 0.22 265);
|
--running-indicator-text: oklch(0.6 0.22 265);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@layer base {
|
@layer base {
|
||||||
* {
|
* {
|
||||||
@apply border-border outline-ring/50;
|
@apply border-border outline-ring/50;
|
||||||
@@ -351,19 +369,19 @@
|
|||||||
|
|
||||||
/* Ensure all clickable elements show pointer cursor */
|
/* Ensure all clickable elements show pointer cursor */
|
||||||
button:not(:disabled),
|
button:not(:disabled),
|
||||||
[role="button"]:not([aria-disabled="true"]),
|
[role='button']:not([aria-disabled='true']),
|
||||||
a[href],
|
a[href],
|
||||||
input[type="button"]:not(:disabled),
|
input[type='button']:not(:disabled),
|
||||||
input[type="submit"]:not(:disabled),
|
input[type='submit']:not(:disabled),
|
||||||
input[type="reset"]:not(:disabled),
|
input[type='reset']:not(:disabled),
|
||||||
select:not(:disabled),
|
select:not(:disabled),
|
||||||
[tabindex]:not([tabindex="-1"]):not(:disabled) {
|
[tabindex]:not([tabindex='-1']):not(:disabled) {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Disabled elements should show not-allowed cursor */
|
/* Disabled elements should show not-allowed cursor */
|
||||||
button:disabled,
|
button:disabled,
|
||||||
[role="button"][aria-disabled="true"],
|
[role='button'][aria-disabled='true'],
|
||||||
input:disabled,
|
input:disabled,
|
||||||
select:disabled,
|
select:disabled,
|
||||||
textarea:disabled {
|
textarea:disabled {
|
||||||
@@ -372,12 +390,44 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* Custom scrollbar for dark themes */
|
/* Custom scrollbar for dark themes */
|
||||||
:is(.dark, .retro, .dracula, .nord, .monokai, .tokyonight, .solarized, .gruvbox, .catppuccin, .onedark, .synthwave, .red, .sunset, .gray) ::-webkit-scrollbar {
|
:is(
|
||||||
|
.dark,
|
||||||
|
.retro,
|
||||||
|
.dracula,
|
||||||
|
.nord,
|
||||||
|
.monokai,
|
||||||
|
.tokyonight,
|
||||||
|
.solarized,
|
||||||
|
.gruvbox,
|
||||||
|
.catppuccin,
|
||||||
|
.onedark,
|
||||||
|
.synthwave,
|
||||||
|
.red,
|
||||||
|
.sunset,
|
||||||
|
.gray
|
||||||
|
)
|
||||||
|
::-webkit-scrollbar {
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
:is(.dark, .retro, .dracula, .nord, .monokai, .tokyonight, .solarized, .gruvbox, .catppuccin, .onedark, .synthwave, .red, .sunset, .gray) ::-webkit-scrollbar-track {
|
:is(
|
||||||
|
.dark,
|
||||||
|
.retro,
|
||||||
|
.dracula,
|
||||||
|
.nord,
|
||||||
|
.monokai,
|
||||||
|
.tokyonight,
|
||||||
|
.solarized,
|
||||||
|
.gruvbox,
|
||||||
|
.catppuccin,
|
||||||
|
.onedark,
|
||||||
|
.synthwave,
|
||||||
|
.red,
|
||||||
|
.sunset,
|
||||||
|
.gray
|
||||||
|
)
|
||||||
|
::-webkit-scrollbar-track {
|
||||||
background: var(--muted);
|
background: var(--muted);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -392,11 +442,11 @@
|
|||||||
|
|
||||||
/* Retro Scrollbar override */
|
/* Retro Scrollbar override */
|
||||||
.retro ::-webkit-scrollbar-thumb {
|
.retro ::-webkit-scrollbar-thumb {
|
||||||
background: var(--primary);
|
background: var(--primary);
|
||||||
border-radius: 0;
|
border-radius: 0;
|
||||||
}
|
}
|
||||||
.retro ::-webkit-scrollbar-track {
|
.retro ::-webkit-scrollbar-track {
|
||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Red theme scrollbar */
|
/* Red theme scrollbar */
|
||||||
@@ -518,8 +568,6 @@
|
|||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Hover state utilities */
|
/* Hover state utilities */
|
||||||
.hover-glass {
|
.hover-glass {
|
||||||
@@ -543,8 +591,6 @@
|
|||||||
background: var(--background);
|
background: var(--background);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Action button utilities */
|
/* Action button utilities */
|
||||||
.bg-action-view {
|
.bg-action-view {
|
||||||
background-color: var(--action-view);
|
background-color: var(--action-view);
|
||||||
@@ -588,8 +634,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.shadow-running-indicator\/50 {
|
.shadow-running-indicator\/50 {
|
||||||
box-shadow: 0 10px 15px -3px color-mix(in oklch, var(--running-indicator), transparent 50%),
|
box-shadow:
|
||||||
0 4px 6px -4px color-mix(in oklch, var(--running-indicator), transparent 50%);
|
0 10px 15px -3px color-mix(in oklch, var(--running-indicator), transparent 50%),
|
||||||
|
0 4px 6px -4px color-mix(in oklch, var(--running-indicator), transparent 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.text-running-indicator {
|
.text-running-indicator {
|
||||||
@@ -632,132 +679,78 @@
|
|||||||
|
|
||||||
/* Retro Overrides for Utilities */
|
/* Retro Overrides for Utilities */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.retro * {
|
.retro * {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Animated Outline Button Styles */
|
/* Animated Outline Button Styles */
|
||||||
.animated-outline-gradient {
|
.animated-outline-gradient {
|
||||||
/* Default gradient - purple to blue */
|
/* Default gradient - purple to blue */
|
||||||
background: conic-gradient(from 90deg at 50% 50%, #a855f7 0%, #3b82f6 50%, #a855f7 100%);
|
background: conic-gradient(from 90deg at 50% 50%, #a855f7 0%, #3b82f6 50%, #a855f7 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Light mode - deeper purple to blue gradient for better visibility */
|
/* Light mode - deeper purple to blue gradient for better visibility */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Dark mode - purple to blue gradient */
|
/* Dark mode - purple to blue gradient */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Retro mode - unique scanline + neon effect */
|
/* Retro mode - unique scanline + neon effect */
|
||||||
|
|
||||||
@keyframes retro-glow {
|
@keyframes retro-glow {
|
||||||
from {
|
from {
|
||||||
filter: brightness(1) drop-shadow(0 0 2px #00ff41);
|
filter: brightness(1) drop-shadow(0 0 2px #00ff41);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
filter: brightness(1.2) drop-shadow(0 0 8px #00ff41);
|
filter: brightness(1.2) drop-shadow(0 0 8px #00ff41);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Dracula animated-outline - purple/pink */
|
/* Dracula animated-outline - purple/pink */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Nord animated-outline - frost blue */
|
/* Nord animated-outline - frost blue */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Monokai animated-outline - pink/yellow */
|
/* Monokai animated-outline - pink/yellow */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Tokyo Night animated-outline - blue/magenta */
|
/* Tokyo Night animated-outline - blue/magenta */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Solarized animated-outline - blue/cyan */
|
/* Solarized animated-outline - blue/cyan */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Gruvbox animated-outline - yellow/orange */
|
/* Gruvbox animated-outline - yellow/orange */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Catppuccin animated-outline - mauve/pink */
|
/* Catppuccin animated-outline - mauve/pink */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* One Dark animated-outline - blue/magenta */
|
/* One Dark animated-outline - blue/magenta */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Synthwave animated-outline - hot pink/cyan with glow */
|
/* Synthwave animated-outline - hot pink/cyan with glow */
|
||||||
|
|
||||||
@keyframes synthwave-glow {
|
@keyframes synthwave-glow {
|
||||||
from {
|
from {
|
||||||
filter: brightness(1) drop-shadow(0 0 3px #f97e72);
|
filter: brightness(1) drop-shadow(0 0 3px #f97e72);
|
||||||
}
|
}
|
||||||
to {
|
to {
|
||||||
filter: brightness(1.3) drop-shadow(0 0 10px #ff7edb);
|
filter: brightness(1.3) drop-shadow(0 0 10px #ff7edb);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Slider Theme Styles */
|
/* Slider Theme Styles */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Dracula slider */
|
/* Dracula slider */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Nord slider */
|
/* Nord slider */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Monokai slider */
|
/* Monokai slider */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Tokyo Night slider */
|
/* Tokyo Night slider */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Solarized slider */
|
/* Solarized slider */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Gruvbox slider */
|
/* Gruvbox slider */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Catppuccin slider */
|
/* Catppuccin slider */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* One Dark slider */
|
/* One Dark slider */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Synthwave slider */
|
/* Synthwave slider */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Line clamp utilities for text overflow prevention */
|
/* Line clamp utilities for text overflow prevention */
|
||||||
.line-clamp-2 {
|
.line-clamp-2 {
|
||||||
display: -webkit-box;
|
display: -webkit-box;
|
||||||
@@ -807,136 +800,28 @@
|
|||||||
|
|
||||||
/* Light theme - professional and readable */
|
/* Light theme - professional and readable */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Dark theme - high contrast */
|
/* Dark theme - high contrast */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Retro theme - neon green on black */
|
/* Retro theme - neon green on black */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Dracula theme */
|
/* Dracula theme */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Nord theme */
|
/* Nord theme */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Monokai theme */
|
/* Monokai theme */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Tokyo Night theme */
|
/* Tokyo Night theme */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Solarized theme */
|
/* Solarized theme */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Gruvbox theme */
|
/* Gruvbox theme */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Catppuccin theme */
|
/* Catppuccin theme */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* One Dark theme */
|
/* One Dark theme */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Synthwave theme */
|
/* Synthwave theme */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* XML Editor container styles */
|
/* XML Editor container styles */
|
||||||
.xml-editor {
|
.xml-editor {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|||||||
@@ -2,21 +2,39 @@
|
|||||||
* Bundles all individual theme styles so the build pipeline
|
* Bundles all individual theme styles so the build pipeline
|
||||||
* doesn't tree-shake their CSS when imported dynamically.
|
* doesn't tree-shake their CSS when imported dynamically.
|
||||||
*/
|
*/
|
||||||
import "./themes/dark.css";
|
|
||||||
import "./themes/light.css";
|
|
||||||
import "./themes/retro.css";
|
|
||||||
import "./themes/dracula.css";
|
|
||||||
import "./themes/nord.css";
|
|
||||||
import "./themes/monokai.css";
|
|
||||||
import "./themes/tokyonight.css";
|
|
||||||
import "./themes/solarized.css";
|
|
||||||
import "./themes/gruvbox.css";
|
|
||||||
import "./themes/catppuccin.css";
|
|
||||||
import "./themes/onedark.css";
|
|
||||||
import "./themes/synthwave.css";
|
|
||||||
import "./themes/red.css";
|
|
||||||
import "./themes/cream.css";
|
|
||||||
import "./themes/sunset.css";
|
|
||||||
import "./themes/gray.css";
|
|
||||||
|
|
||||||
|
// Dark themes (16)
|
||||||
|
import './themes/dark.css';
|
||||||
|
import './themes/retro.css';
|
||||||
|
import './themes/dracula.css';
|
||||||
|
import './themes/nord.css';
|
||||||
|
import './themes/monokai.css';
|
||||||
|
import './themes/tokyonight.css';
|
||||||
|
import './themes/solarized.css';
|
||||||
|
import './themes/gruvbox.css';
|
||||||
|
import './themes/catppuccin.css';
|
||||||
|
import './themes/onedark.css';
|
||||||
|
import './themes/synthwave.css';
|
||||||
|
import './themes/red.css';
|
||||||
|
import './themes/sunset.css';
|
||||||
|
import './themes/gray.css';
|
||||||
|
import './themes/forest.css';
|
||||||
|
import './themes/ocean.css';
|
||||||
|
|
||||||
|
// Light themes (16)
|
||||||
|
import './themes/light.css';
|
||||||
|
import './themes/cream.css';
|
||||||
|
import './themes/solarizedlight.css';
|
||||||
|
import './themes/github.css';
|
||||||
|
import './themes/paper.css';
|
||||||
|
import './themes/rose.css';
|
||||||
|
import './themes/mint.css';
|
||||||
|
import './themes/lavender.css';
|
||||||
|
import './themes/sand.css';
|
||||||
|
import './themes/sky.css';
|
||||||
|
import './themes/peach.css';
|
||||||
|
import './themes/snow.css';
|
||||||
|
import './themes/sepia.css';
|
||||||
|
import './themes/gruvboxlight.css';
|
||||||
|
import './themes/nordlight.css';
|
||||||
|
import './themes/blossom.css';
|
||||||
|
|||||||
92
apps/ui/src/styles/themes/blossom.css
Normal file
92
apps/ui/src/styles/themes/blossom.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Blossom Theme - Cherry blossom pink */
|
||||||
|
|
||||||
|
.blossom {
|
||||||
|
--background: oklch(0.98 0.02 330);
|
||||||
|
--background-50: oklch(0.98 0.02 330 / 0.5);
|
||||||
|
--background-80: oklch(0.98 0.02 330 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.25 0.04 340);
|
||||||
|
--foreground-secondary: oklch(0.45 0.03 340);
|
||||||
|
--foreground-muted: oklch(0.55 0.025 340);
|
||||||
|
|
||||||
|
--card: oklch(0.99 0.015 330);
|
||||||
|
--card-foreground: oklch(0.25 0.04 340);
|
||||||
|
--popover: oklch(0.99 0.015 330);
|
||||||
|
--popover-foreground: oklch(0.25 0.04 340);
|
||||||
|
|
||||||
|
--primary: oklch(0.65 0.15 340);
|
||||||
|
--primary-foreground: oklch(0.99 0.015 330);
|
||||||
|
--brand-400: oklch(0.7 0.13 340);
|
||||||
|
--brand-500: oklch(0.65 0.15 340);
|
||||||
|
--brand-600: oklch(0.6 0.17 340);
|
||||||
|
|
||||||
|
--secondary: oklch(0.95 0.025 330);
|
||||||
|
--secondary-foreground: oklch(0.25 0.04 340);
|
||||||
|
--muted: oklch(0.94 0.028 330);
|
||||||
|
--muted-foreground: oklch(0.5 0.03 340);
|
||||||
|
--accent: oklch(0.92 0.032 330);
|
||||||
|
--accent-foreground: oklch(0.25 0.04 340);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 15);
|
||||||
|
--border: oklch(0.9 0.025 330);
|
||||||
|
--border-glass: oklch(0.65 0.15 340 / 0.15);
|
||||||
|
--input: oklch(0.98 0.018 330);
|
||||||
|
--ring: oklch(0.65 0.15 340);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.65 0.15 340);
|
||||||
|
--chart-2: oklch(0.6 0.15 300);
|
||||||
|
--chart-3: oklch(0.55 0.15 15);
|
||||||
|
--chart-4: oklch(0.6 0.12 350);
|
||||||
|
--chart-5: oklch(0.55 0.1 280);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.97 0.022 330);
|
||||||
|
--sidebar-foreground: oklch(0.25 0.04 340);
|
||||||
|
--sidebar-primary: oklch(0.65 0.15 340);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0.015 330);
|
||||||
|
--sidebar-accent: oklch(0.94 0.028 330);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.04 340);
|
||||||
|
--sidebar-border: oklch(0.9 0.025 330);
|
||||||
|
--sidebar-ring: oklch(0.65 0.15 340);
|
||||||
|
|
||||||
|
--action-view: oklch(0.65 0.15 340);
|
||||||
|
--action-view-hover: oklch(0.6 0.17 340);
|
||||||
|
--action-followup: oklch(0.6 0.15 300);
|
||||||
|
--action-followup-hover: oklch(0.55 0.17 300);
|
||||||
|
--action-commit: oklch(0.55 0.15 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.17 145);
|
||||||
|
--action-verify: oklch(0.55 0.15 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.17 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.65 0.15 340);
|
||||||
|
--running-indicator-text: oklch(0.6 0.17 340);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.15 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.65 0.15 70);
|
||||||
|
--status-warning-bg: oklch(0.65 0.15 70 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.2 15);
|
||||||
|
--status-error-bg: oklch(0.55 0.2 15 / 0.15);
|
||||||
|
--status-info: oklch(0.55 0.15 230);
|
||||||
|
--status-info-bg: oklch(0.55 0.15 230 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.025 340);
|
||||||
|
--status-in-progress: oklch(0.65 0.15 70);
|
||||||
|
--status-waiting: oklch(0.6 0.12 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blossom .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.98 0.02 330),
|
||||||
|
oklch(0.97 0.025 330),
|
||||||
|
oklch(0.98 0.02 330)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blossom .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #ec4899 0%, #f472b6 50%, #ec4899 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.blossom .animated-outline-inner {
|
||||||
|
background: oklch(0.99 0.015 330) !important;
|
||||||
|
color: #ec4899 !important;
|
||||||
|
}
|
||||||
98
apps/ui/src/styles/themes/forest.css
Normal file
98
apps/ui/src/styles/themes/forest.css
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/* Forest Theme - Deep green dark theme */
|
||||||
|
|
||||||
|
.forest {
|
||||||
|
--background: oklch(0.12 0.02 150);
|
||||||
|
--background-50: oklch(0.12 0.02 150 / 0.5);
|
||||||
|
--background-80: oklch(0.12 0.02 150 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.95 0.02 150);
|
||||||
|
--foreground-secondary: oklch(0.7 0.03 150);
|
||||||
|
--foreground-muted: oklch(0.6 0.03 150);
|
||||||
|
|
||||||
|
--card: oklch(0.16 0.025 150);
|
||||||
|
--card-foreground: oklch(0.95 0.02 150);
|
||||||
|
--popover: oklch(0.14 0.022 150);
|
||||||
|
--popover-foreground: oklch(0.95 0.02 150);
|
||||||
|
|
||||||
|
--primary: oklch(0.6 0.18 145);
|
||||||
|
--primary-foreground: oklch(0.12 0.02 150);
|
||||||
|
--brand-400: oklch(0.65 0.16 145);
|
||||||
|
--brand-500: oklch(0.6 0.18 145);
|
||||||
|
--brand-600: oklch(0.55 0.2 145);
|
||||||
|
|
||||||
|
--secondary: oklch(1 0 0 / 0.05);
|
||||||
|
--secondary-foreground: oklch(0.95 0.02 150);
|
||||||
|
--muted: oklch(0.2 0.03 150);
|
||||||
|
--muted-foreground: oklch(0.65 0.03 150);
|
||||||
|
--accent: oklch(1 0 0 / 0.08);
|
||||||
|
--accent-foreground: oklch(0.95 0.02 150);
|
||||||
|
|
||||||
|
--destructive: oklch(0.6 0.22 25);
|
||||||
|
--border: oklch(0.25 0.04 150);
|
||||||
|
--border-glass: oklch(1 0 0 / 0.1);
|
||||||
|
--input: oklch(0.14 0.022 150);
|
||||||
|
--ring: oklch(0.6 0.18 145);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.6 0.18 145);
|
||||||
|
--chart-2: oklch(0.65 0.15 100);
|
||||||
|
--chart-3: oklch(0.7 0.12 180);
|
||||||
|
--chart-4: oklch(0.6 0.2 200);
|
||||||
|
--chart-5: oklch(0.55 0.15 50);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.1 0.02 150 / 0.8);
|
||||||
|
--sidebar-foreground: oklch(0.95 0.02 150);
|
||||||
|
--sidebar-primary: oklch(0.6 0.18 145);
|
||||||
|
--sidebar-primary-foreground: oklch(0.12 0.02 150);
|
||||||
|
--sidebar-accent: oklch(1 0 0 / 0.05);
|
||||||
|
--sidebar-accent-foreground: oklch(0.95 0.02 150);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 0.1);
|
||||||
|
--sidebar-ring: oklch(0.6 0.18 145);
|
||||||
|
|
||||||
|
--action-view: oklch(0.6 0.18 145);
|
||||||
|
--action-view-hover: oklch(0.55 0.2 145);
|
||||||
|
--action-followup: oklch(0.6 0.15 180);
|
||||||
|
--action-followup-hover: oklch(0.55 0.17 180);
|
||||||
|
--action-commit: oklch(0.6 0.18 145);
|
||||||
|
--action-commit-hover: oklch(0.55 0.2 145);
|
||||||
|
--action-verify: oklch(0.6 0.18 145);
|
||||||
|
--action-verify-hover: oklch(0.55 0.2 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.6 0.18 145);
|
||||||
|
--running-indicator-text: oklch(0.65 0.16 145);
|
||||||
|
|
||||||
|
--status-success: oklch(0.65 0.18 145);
|
||||||
|
--status-success-bg: oklch(0.65 0.18 145 / 0.2);
|
||||||
|
--status-warning: oklch(0.7 0.15 80);
|
||||||
|
--status-warning-bg: oklch(0.7 0.15 80 / 0.2);
|
||||||
|
--status-error: oklch(0.6 0.22 25);
|
||||||
|
--status-error-bg: oklch(0.6 0.22 25 / 0.2);
|
||||||
|
--status-info: oklch(0.6 0.15 180);
|
||||||
|
--status-info-bg: oklch(0.6 0.15 180 / 0.2);
|
||||||
|
--status-backlog: oklch(0.55 0.03 150);
|
||||||
|
--status-in-progress: oklch(0.7 0.15 80);
|
||||||
|
--status-waiting: oklch(0.65 0.12 60);
|
||||||
|
|
||||||
|
--shadow-xs: 0 1px 2px rgba(0, 20, 10, 0.3);
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0, 20, 10, 0.4), 0 1px 2px rgba(0, 20, 10, 0.3);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 20, 10, 0.4), 0 2px 4px -1px rgba(0, 20, 10, 0.3);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 20, 10, 0.4), 0 4px 6px -2px rgba(0, 20, 10, 0.2);
|
||||||
|
--shadow-xl: 0 20px 25px -5px rgba(0, 20, 10, 0.5), 0 10px 10px -5px rgba(0, 20, 10, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.forest .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.12 0.02 150),
|
||||||
|
oklch(0.15 0.025 150),
|
||||||
|
oklch(0.12 0.02 150)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.forest .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #22c55e 0%, #16a34a 50%, #22c55e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.forest .animated-outline-inner {
|
||||||
|
background: oklch(0.16 0.025 150) !important;
|
||||||
|
color: #86efac !important;
|
||||||
|
}
|
||||||
87
apps/ui/src/styles/themes/github.css
Normal file
87
apps/ui/src/styles/themes/github.css
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
/* GitHub Light Theme */
|
||||||
|
|
||||||
|
.github {
|
||||||
|
--background: oklch(0.99 0 0);
|
||||||
|
--background-50: oklch(0.99 0 0 / 0.5);
|
||||||
|
--background-80: oklch(0.99 0 0 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.2 0 0);
|
||||||
|
--foreground-secondary: oklch(0.4 0 0);
|
||||||
|
--foreground-muted: oklch(0.55 0 0);
|
||||||
|
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.2 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.2 0 0);
|
||||||
|
|
||||||
|
--primary: oklch(0.5 0.15 250);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--brand-400: oklch(0.55 0.13 250);
|
||||||
|
--brand-500: oklch(0.5 0.15 250);
|
||||||
|
--brand-600: oklch(0.45 0.17 250);
|
||||||
|
|
||||||
|
--secondary: oklch(0.96 0 0);
|
||||||
|
--secondary-foreground: oklch(0.2 0 0);
|
||||||
|
--muted: oklch(0.96 0 0);
|
||||||
|
--muted-foreground: oklch(0.45 0 0);
|
||||||
|
--accent: oklch(0.94 0 0);
|
||||||
|
--accent-foreground: oklch(0.2 0 0);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.22 25);
|
||||||
|
--border: oklch(0.88 0 0);
|
||||||
|
--border-glass: oklch(0.2 0 0 / 0.1);
|
||||||
|
--input: oklch(0.98 0 0);
|
||||||
|
--ring: oklch(0.5 0.15 250);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.5 0.15 250);
|
||||||
|
--chart-2: oklch(0.55 0.18 145);
|
||||||
|
--chart-3: oklch(0.6 0.15 45);
|
||||||
|
--chart-4: oklch(0.55 0.15 300);
|
||||||
|
--chart-5: oklch(0.55 0.18 25);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.98 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.2 0 0);
|
||||||
|
--sidebar-primary: oklch(0.5 0.15 250);
|
||||||
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-accent: oklch(0.94 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.2 0 0);
|
||||||
|
--sidebar-border: oklch(0.88 0 0);
|
||||||
|
--sidebar-ring: oklch(0.5 0.15 250);
|
||||||
|
|
||||||
|
--action-view: oklch(0.5 0.15 250);
|
||||||
|
--action-view-hover: oklch(0.45 0.17 250);
|
||||||
|
--action-followup: oklch(0.55 0.15 230);
|
||||||
|
--action-followup-hover: oklch(0.5 0.17 230);
|
||||||
|
--action-commit: oklch(0.55 0.18 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.2 145);
|
||||||
|
--action-verify: oklch(0.55 0.18 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.2 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.5 0.15 250);
|
||||||
|
--running-indicator-text: oklch(0.45 0.17 250);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.18 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.18 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.65 0.15 80);
|
||||||
|
--status-warning-bg: oklch(0.65 0.15 80 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.22 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.22 25 / 0.15);
|
||||||
|
--status-info: oklch(0.5 0.15 250);
|
||||||
|
--status-info-bg: oklch(0.5 0.15 250 / 0.15);
|
||||||
|
--status-backlog: oklch(0.55 0 0);
|
||||||
|
--status-in-progress: oklch(0.65 0.15 80);
|
||||||
|
--status-waiting: oklch(0.6 0.12 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github .content-bg {
|
||||||
|
background: linear-gradient(135deg, oklch(0.99 0 0), oklch(0.98 0 0), oklch(0.99 0 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.github .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #0969da 0%, #1f883d 50%, #0969da 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.github .animated-outline-inner {
|
||||||
|
background: oklch(1 0 0) !important;
|
||||||
|
color: #0969da !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/gruvboxlight.css
Normal file
92
apps/ui/src/styles/themes/gruvboxlight.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Gruvbox Light Theme */
|
||||||
|
|
||||||
|
.gruvboxlight {
|
||||||
|
--background: oklch(0.96 0.02 85);
|
||||||
|
--background-50: oklch(0.96 0.02 85 / 0.5);
|
||||||
|
--background-80: oklch(0.96 0.02 85 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.25 0.04 65);
|
||||||
|
--foreground-secondary: oklch(0.4 0.04 65);
|
||||||
|
--foreground-muted: oklch(0.55 0.03 65);
|
||||||
|
|
||||||
|
--card: oklch(0.98 0.015 85);
|
||||||
|
--card-foreground: oklch(0.25 0.04 65);
|
||||||
|
--popover: oklch(0.97 0.018 85);
|
||||||
|
--popover-foreground: oklch(0.25 0.04 65);
|
||||||
|
|
||||||
|
--primary: oklch(0.55 0.15 70);
|
||||||
|
--primary-foreground: oklch(0.98 0.015 85);
|
||||||
|
--brand-400: oklch(0.6 0.13 70);
|
||||||
|
--brand-500: oklch(0.55 0.15 70);
|
||||||
|
--brand-600: oklch(0.5 0.17 70);
|
||||||
|
|
||||||
|
--secondary: oklch(0.93 0.025 85);
|
||||||
|
--secondary-foreground: oklch(0.25 0.04 65);
|
||||||
|
--muted: oklch(0.92 0.028 85);
|
||||||
|
--muted-foreground: oklch(0.5 0.03 65);
|
||||||
|
--accent: oklch(0.9 0.032 85);
|
||||||
|
--accent-foreground: oklch(0.25 0.04 65);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 25);
|
||||||
|
--border: oklch(0.87 0.03 85);
|
||||||
|
--border-glass: oklch(0.55 0.15 70 / 0.15);
|
||||||
|
--input: oklch(0.97 0.018 85);
|
||||||
|
--ring: oklch(0.55 0.15 70);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.55 0.15 70);
|
||||||
|
--chart-2: oklch(0.55 0.18 25);
|
||||||
|
--chart-3: oklch(0.5 0.15 145);
|
||||||
|
--chart-4: oklch(0.55 0.15 230);
|
||||||
|
--chart-5: oklch(0.55 0.15 300);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.95 0.022 85);
|
||||||
|
--sidebar-foreground: oklch(0.25 0.04 65);
|
||||||
|
--sidebar-primary: oklch(0.55 0.15 70);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0.015 85);
|
||||||
|
--sidebar-accent: oklch(0.92 0.028 85);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.04 65);
|
||||||
|
--sidebar-border: oklch(0.87 0.03 85);
|
||||||
|
--sidebar-ring: oklch(0.55 0.15 70);
|
||||||
|
|
||||||
|
--action-view: oklch(0.55 0.15 70);
|
||||||
|
--action-view-hover: oklch(0.5 0.17 70);
|
||||||
|
--action-followup: oklch(0.55 0.15 230);
|
||||||
|
--action-followup-hover: oklch(0.5 0.17 230);
|
||||||
|
--action-commit: oklch(0.5 0.15 145);
|
||||||
|
--action-commit-hover: oklch(0.45 0.17 145);
|
||||||
|
--action-verify: oklch(0.5 0.15 145);
|
||||||
|
--action-verify-hover: oklch(0.45 0.17 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.55 0.15 70);
|
||||||
|
--running-indicator-text: oklch(0.5 0.17 70);
|
||||||
|
|
||||||
|
--status-success: oklch(0.5 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.5 0.15 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.6 0.15 70);
|
||||||
|
--status-warning-bg: oklch(0.6 0.15 70 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.2 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.2 25 / 0.15);
|
||||||
|
--status-info: oklch(0.55 0.15 230);
|
||||||
|
--status-info-bg: oklch(0.55 0.15 230 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.03 65);
|
||||||
|
--status-in-progress: oklch(0.6 0.15 70);
|
||||||
|
--status-waiting: oklch(0.58 0.12 55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gruvboxlight .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.96 0.02 85),
|
||||||
|
oklch(0.95 0.025 85),
|
||||||
|
oklch(0.96 0.02 85)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gruvboxlight .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #d79921 0%, #b57614 50%, #d79921 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.gruvboxlight .animated-outline-inner {
|
||||||
|
background: oklch(0.98 0.015 85) !important;
|
||||||
|
color: #b57614 !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/lavender.css
Normal file
92
apps/ui/src/styles/themes/lavender.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Lavender Theme - Soft purple/lavender */
|
||||||
|
|
||||||
|
.lavender {
|
||||||
|
--background: oklch(0.97 0.02 285);
|
||||||
|
--background-50: oklch(0.97 0.02 285 / 0.5);
|
||||||
|
--background-80: oklch(0.97 0.02 285 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.25 0.04 285);
|
||||||
|
--foreground-secondary: oklch(0.45 0.04 285);
|
||||||
|
--foreground-muted: oklch(0.55 0.03 285);
|
||||||
|
|
||||||
|
--card: oklch(0.98 0.015 285);
|
||||||
|
--card-foreground: oklch(0.25 0.04 285);
|
||||||
|
--popover: oklch(0.98 0.015 285);
|
||||||
|
--popover-foreground: oklch(0.25 0.04 285);
|
||||||
|
|
||||||
|
--primary: oklch(0.55 0.18 280);
|
||||||
|
--primary-foreground: oklch(0.98 0.015 285);
|
||||||
|
--brand-400: oklch(0.6 0.16 280);
|
||||||
|
--brand-500: oklch(0.55 0.18 280);
|
||||||
|
--brand-600: oklch(0.5 0.2 280);
|
||||||
|
|
||||||
|
--secondary: oklch(0.94 0.025 285);
|
||||||
|
--secondary-foreground: oklch(0.25 0.04 285);
|
||||||
|
--muted: oklch(0.93 0.028 285);
|
||||||
|
--muted-foreground: oklch(0.5 0.04 285);
|
||||||
|
--accent: oklch(0.91 0.032 285);
|
||||||
|
--accent-foreground: oklch(0.25 0.04 285);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 25);
|
||||||
|
--border: oklch(0.88 0.025 285);
|
||||||
|
--border-glass: oklch(0.55 0.18 280 / 0.15);
|
||||||
|
--input: oklch(0.97 0.018 285);
|
||||||
|
--ring: oklch(0.55 0.18 280);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.55 0.18 280);
|
||||||
|
--chart-2: oklch(0.6 0.15 320);
|
||||||
|
--chart-3: oklch(0.55 0.15 250);
|
||||||
|
--chart-4: oklch(0.6 0.12 200);
|
||||||
|
--chart-5: oklch(0.55 0.15 350);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.96 0.022 285);
|
||||||
|
--sidebar-foreground: oklch(0.25 0.04 285);
|
||||||
|
--sidebar-primary: oklch(0.55 0.18 280);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0.015 285);
|
||||||
|
--sidebar-accent: oklch(0.93 0.028 285);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.04 285);
|
||||||
|
--sidebar-border: oklch(0.88 0.025 285);
|
||||||
|
--sidebar-ring: oklch(0.55 0.18 280);
|
||||||
|
|
||||||
|
--action-view: oklch(0.55 0.18 280);
|
||||||
|
--action-view-hover: oklch(0.5 0.2 280);
|
||||||
|
--action-followup: oklch(0.55 0.15 250);
|
||||||
|
--action-followup-hover: oklch(0.5 0.17 250);
|
||||||
|
--action-commit: oklch(0.55 0.15 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.17 145);
|
||||||
|
--action-verify: oklch(0.55 0.15 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.17 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.55 0.18 280);
|
||||||
|
--running-indicator-text: oklch(0.5 0.2 280);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.15 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.65 0.15 70);
|
||||||
|
--status-warning-bg: oklch(0.65 0.15 70 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.2 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.2 25 / 0.15);
|
||||||
|
--status-info: oklch(0.55 0.15 250);
|
||||||
|
--status-info-bg: oklch(0.55 0.15 250 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.03 285);
|
||||||
|
--status-in-progress: oklch(0.65 0.15 70);
|
||||||
|
--status-waiting: oklch(0.6 0.12 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lavender .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.97 0.02 285),
|
||||||
|
oklch(0.96 0.025 285),
|
||||||
|
oklch(0.97 0.02 285)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lavender .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #a855f7 0%, #8b5cf6 50%, #a855f7 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.lavender .animated-outline-inner {
|
||||||
|
background: oklch(0.98 0.015 285) !important;
|
||||||
|
color: #a855f7 !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/mint.css
Normal file
92
apps/ui/src/styles/themes/mint.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Mint Theme - Fresh mint green */
|
||||||
|
|
||||||
|
.mint {
|
||||||
|
--background: oklch(0.98 0.015 160);
|
||||||
|
--background-50: oklch(0.98 0.015 160 / 0.5);
|
||||||
|
--background-80: oklch(0.98 0.015 160 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.2 0.03 160);
|
||||||
|
--foreground-secondary: oklch(0.4 0.03 160);
|
||||||
|
--foreground-muted: oklch(0.55 0.02 160);
|
||||||
|
|
||||||
|
--card: oklch(0.99 0.01 160);
|
||||||
|
--card-foreground: oklch(0.2 0.03 160);
|
||||||
|
--popover: oklch(0.99 0.01 160);
|
||||||
|
--popover-foreground: oklch(0.2 0.03 160);
|
||||||
|
|
||||||
|
--primary: oklch(0.55 0.15 165);
|
||||||
|
--primary-foreground: oklch(0.99 0.01 160);
|
||||||
|
--brand-400: oklch(0.6 0.13 165);
|
||||||
|
--brand-500: oklch(0.55 0.15 165);
|
||||||
|
--brand-600: oklch(0.5 0.17 165);
|
||||||
|
|
||||||
|
--secondary: oklch(0.95 0.018 160);
|
||||||
|
--secondary-foreground: oklch(0.2 0.03 160);
|
||||||
|
--muted: oklch(0.94 0.02 160);
|
||||||
|
--muted-foreground: oklch(0.5 0.03 160);
|
||||||
|
--accent: oklch(0.92 0.025 160);
|
||||||
|
--accent-foreground: oklch(0.2 0.03 160);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 25);
|
||||||
|
--border: oklch(0.88 0.02 160);
|
||||||
|
--border-glass: oklch(0.55 0.15 165 / 0.15);
|
||||||
|
--input: oklch(0.98 0.012 160);
|
||||||
|
--ring: oklch(0.55 0.15 165);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.55 0.15 165);
|
||||||
|
--chart-2: oklch(0.6 0.12 200);
|
||||||
|
--chart-3: oklch(0.55 0.15 140);
|
||||||
|
--chart-4: oklch(0.6 0.12 180);
|
||||||
|
--chart-5: oklch(0.55 0.1 230);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.97 0.015 160);
|
||||||
|
--sidebar-foreground: oklch(0.2 0.03 160);
|
||||||
|
--sidebar-primary: oklch(0.55 0.15 165);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0.01 160);
|
||||||
|
--sidebar-accent: oklch(0.94 0.02 160);
|
||||||
|
--sidebar-accent-foreground: oklch(0.2 0.03 160);
|
||||||
|
--sidebar-border: oklch(0.88 0.02 160);
|
||||||
|
--sidebar-ring: oklch(0.55 0.15 165);
|
||||||
|
|
||||||
|
--action-view: oklch(0.55 0.15 165);
|
||||||
|
--action-view-hover: oklch(0.5 0.17 165);
|
||||||
|
--action-followup: oklch(0.55 0.12 200);
|
||||||
|
--action-followup-hover: oklch(0.5 0.14 200);
|
||||||
|
--action-commit: oklch(0.55 0.15 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.17 145);
|
||||||
|
--action-verify: oklch(0.55 0.15 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.17 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.55 0.15 165);
|
||||||
|
--running-indicator-text: oklch(0.5 0.17 165);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.15 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.65 0.15 70);
|
||||||
|
--status-warning-bg: oklch(0.65 0.15 70 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.2 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.2 25 / 0.15);
|
||||||
|
--status-info: oklch(0.55 0.12 200);
|
||||||
|
--status-info-bg: oklch(0.55 0.12 200 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.02 160);
|
||||||
|
--status-in-progress: oklch(0.65 0.15 70);
|
||||||
|
--status-waiting: oklch(0.6 0.12 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mint .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.98 0.015 160),
|
||||||
|
oklch(0.97 0.018 160),
|
||||||
|
oklch(0.98 0.015 160)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mint .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #10b981 0%, #14b8a6 50%, #10b981 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mint .animated-outline-inner {
|
||||||
|
background: oklch(0.99 0.01 160) !important;
|
||||||
|
color: #10b981 !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/nordlight.css
Normal file
92
apps/ui/src/styles/themes/nordlight.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Nord Light Theme */
|
||||||
|
|
||||||
|
.nordlight {
|
||||||
|
--background: oklch(0.97 0.01 220);
|
||||||
|
--background-50: oklch(0.97 0.01 220 / 0.5);
|
||||||
|
--background-80: oklch(0.97 0.01 220 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.25 0.03 220);
|
||||||
|
--foreground-secondary: oklch(0.45 0.025 220);
|
||||||
|
--foreground-muted: oklch(0.55 0.02 220);
|
||||||
|
|
||||||
|
--card: oklch(0.99 0.008 220);
|
||||||
|
--card-foreground: oklch(0.25 0.03 220);
|
||||||
|
--popover: oklch(0.98 0.01 220);
|
||||||
|
--popover-foreground: oklch(0.25 0.03 220);
|
||||||
|
|
||||||
|
--primary: oklch(0.55 0.12 225);
|
||||||
|
--primary-foreground: oklch(0.99 0.008 220);
|
||||||
|
--brand-400: oklch(0.6 0.1 225);
|
||||||
|
--brand-500: oklch(0.55 0.12 225);
|
||||||
|
--brand-600: oklch(0.5 0.14 225);
|
||||||
|
|
||||||
|
--secondary: oklch(0.94 0.012 220);
|
||||||
|
--secondary-foreground: oklch(0.25 0.03 220);
|
||||||
|
--muted: oklch(0.93 0.015 220);
|
||||||
|
--muted-foreground: oklch(0.5 0.025 220);
|
||||||
|
--accent: oklch(0.91 0.018 220);
|
||||||
|
--accent-foreground: oklch(0.25 0.03 220);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.18 25);
|
||||||
|
--border: oklch(0.88 0.015 220);
|
||||||
|
--border-glass: oklch(0.55 0.12 225 / 0.12);
|
||||||
|
--input: oklch(0.98 0.01 220);
|
||||||
|
--ring: oklch(0.55 0.12 225);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.55 0.12 225);
|
||||||
|
--chart-2: oklch(0.55 0.15 25);
|
||||||
|
--chart-3: oklch(0.55 0.12 145);
|
||||||
|
--chart-4: oklch(0.55 0.12 300);
|
||||||
|
--chart-5: oklch(0.6 0.12 80);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.96 0.01 220);
|
||||||
|
--sidebar-foreground: oklch(0.25 0.03 220);
|
||||||
|
--sidebar-primary: oklch(0.55 0.12 225);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0.008 220);
|
||||||
|
--sidebar-accent: oklch(0.93 0.015 220);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.03 220);
|
||||||
|
--sidebar-border: oklch(0.88 0.015 220);
|
||||||
|
--sidebar-ring: oklch(0.55 0.12 225);
|
||||||
|
|
||||||
|
--action-view: oklch(0.55 0.12 225);
|
||||||
|
--action-view-hover: oklch(0.5 0.14 225);
|
||||||
|
--action-followup: oklch(0.55 0.12 200);
|
||||||
|
--action-followup-hover: oklch(0.5 0.14 200);
|
||||||
|
--action-commit: oklch(0.55 0.12 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.14 145);
|
||||||
|
--action-verify: oklch(0.55 0.12 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.14 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.55 0.12 225);
|
||||||
|
--running-indicator-text: oklch(0.5 0.14 225);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.12 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.12 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.65 0.12 80);
|
||||||
|
--status-warning-bg: oklch(0.65 0.12 80 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.18 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.18 25 / 0.15);
|
||||||
|
--status-info: oklch(0.55 0.12 225);
|
||||||
|
--status-info-bg: oklch(0.55 0.12 225 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.02 220);
|
||||||
|
--status-in-progress: oklch(0.65 0.12 80);
|
||||||
|
--status-waiting: oklch(0.6 0.1 55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nordlight .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.97 0.01 220),
|
||||||
|
oklch(0.96 0.012 220),
|
||||||
|
oklch(0.97 0.01 220)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nordlight .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #5e81ac 0%, #81a1c1 50%, #5e81ac 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.nordlight .animated-outline-inner {
|
||||||
|
background: oklch(0.99 0.008 220) !important;
|
||||||
|
color: #5e81ac !important;
|
||||||
|
}
|
||||||
98
apps/ui/src/styles/themes/ocean.css
Normal file
98
apps/ui/src/styles/themes/ocean.css
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/* Ocean Theme - Deep blue dark theme */
|
||||||
|
|
||||||
|
.ocean {
|
||||||
|
--background: oklch(0.12 0.03 230);
|
||||||
|
--background-50: oklch(0.12 0.03 230 / 0.5);
|
||||||
|
--background-80: oklch(0.12 0.03 230 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.95 0.02 230);
|
||||||
|
--foreground-secondary: oklch(0.7 0.03 230);
|
||||||
|
--foreground-muted: oklch(0.6 0.03 230);
|
||||||
|
|
||||||
|
--card: oklch(0.16 0.035 230);
|
||||||
|
--card-foreground: oklch(0.95 0.02 230);
|
||||||
|
--popover: oklch(0.14 0.032 230);
|
||||||
|
--popover-foreground: oklch(0.95 0.02 230);
|
||||||
|
|
||||||
|
--primary: oklch(0.6 0.18 230);
|
||||||
|
--primary-foreground: oklch(0.12 0.03 230);
|
||||||
|
--brand-400: oklch(0.65 0.16 230);
|
||||||
|
--brand-500: oklch(0.6 0.18 230);
|
||||||
|
--brand-600: oklch(0.55 0.2 235);
|
||||||
|
|
||||||
|
--secondary: oklch(1 0 0 / 0.05);
|
||||||
|
--secondary-foreground: oklch(0.95 0.02 230);
|
||||||
|
--muted: oklch(0.2 0.04 230);
|
||||||
|
--muted-foreground: oklch(0.65 0.03 230);
|
||||||
|
--accent: oklch(1 0 0 / 0.08);
|
||||||
|
--accent-foreground: oklch(0.95 0.02 230);
|
||||||
|
|
||||||
|
--destructive: oklch(0.6 0.22 25);
|
||||||
|
--border: oklch(0.25 0.05 230);
|
||||||
|
--border-glass: oklch(1 0 0 / 0.1);
|
||||||
|
--input: oklch(0.14 0.032 230);
|
||||||
|
--ring: oklch(0.6 0.18 230);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.6 0.18 230);
|
||||||
|
--chart-2: oklch(0.6 0.15 200);
|
||||||
|
--chart-3: oklch(0.65 0.15 180);
|
||||||
|
--chart-4: oklch(0.6 0.2 260);
|
||||||
|
--chart-5: oklch(0.55 0.15 300);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.1 0.03 230 / 0.8);
|
||||||
|
--sidebar-foreground: oklch(0.95 0.02 230);
|
||||||
|
--sidebar-primary: oklch(0.6 0.18 230);
|
||||||
|
--sidebar-primary-foreground: oklch(0.12 0.03 230);
|
||||||
|
--sidebar-accent: oklch(1 0 0 / 0.05);
|
||||||
|
--sidebar-accent-foreground: oklch(0.95 0.02 230);
|
||||||
|
--sidebar-border: oklch(1 0 0 / 0.1);
|
||||||
|
--sidebar-ring: oklch(0.6 0.18 230);
|
||||||
|
|
||||||
|
--action-view: oklch(0.6 0.18 230);
|
||||||
|
--action-view-hover: oklch(0.55 0.2 235);
|
||||||
|
--action-followup: oklch(0.6 0.15 200);
|
||||||
|
--action-followup-hover: oklch(0.55 0.17 200);
|
||||||
|
--action-commit: oklch(0.6 0.15 180);
|
||||||
|
--action-commit-hover: oklch(0.55 0.17 180);
|
||||||
|
--action-verify: oklch(0.6 0.15 180);
|
||||||
|
--action-verify-hover: oklch(0.55 0.17 180);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.6 0.18 230);
|
||||||
|
--running-indicator-text: oklch(0.65 0.16 230);
|
||||||
|
|
||||||
|
--status-success: oklch(0.6 0.15 180);
|
||||||
|
--status-success-bg: oklch(0.6 0.15 180 / 0.2);
|
||||||
|
--status-warning: oklch(0.7 0.15 80);
|
||||||
|
--status-warning-bg: oklch(0.7 0.15 80 / 0.2);
|
||||||
|
--status-error: oklch(0.6 0.22 25);
|
||||||
|
--status-error-bg: oklch(0.6 0.22 25 / 0.2);
|
||||||
|
--status-info: oklch(0.6 0.18 230);
|
||||||
|
--status-info-bg: oklch(0.6 0.18 230 / 0.2);
|
||||||
|
--status-backlog: oklch(0.55 0.03 230);
|
||||||
|
--status-in-progress: oklch(0.7 0.15 80);
|
||||||
|
--status-waiting: oklch(0.65 0.12 60);
|
||||||
|
|
||||||
|
--shadow-xs: 0 1px 2px rgba(0, 10, 30, 0.3);
|
||||||
|
--shadow-sm: 0 1px 3px rgba(0, 10, 30, 0.4), 0 1px 2px rgba(0, 10, 30, 0.3);
|
||||||
|
--shadow-md: 0 4px 6px -1px rgba(0, 10, 30, 0.4), 0 2px 4px -1px rgba(0, 10, 30, 0.3);
|
||||||
|
--shadow-lg: 0 10px 15px -3px rgba(0, 10, 30, 0.4), 0 4px 6px -2px rgba(0, 10, 30, 0.2);
|
||||||
|
--shadow-xl: 0 20px 25px -5px rgba(0, 10, 30, 0.5), 0 10px 10px -5px rgba(0, 10, 30, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ocean .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.12 0.03 230),
|
||||||
|
oklch(0.15 0.035 230),
|
||||||
|
oklch(0.12 0.03 230)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ocean .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #3b82f6 0%, #0ea5e9 50%, #3b82f6 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ocean .animated-outline-inner {
|
||||||
|
background: oklch(0.16 0.035 230) !important;
|
||||||
|
color: #93c5fd !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/paper.css
Normal file
92
apps/ui/src/styles/themes/paper.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Paper Theme - Clean minimal white */
|
||||||
|
|
||||||
|
.paper {
|
||||||
|
--background: oklch(0.99 0 0);
|
||||||
|
--background-50: oklch(0.99 0 0 / 0.5);
|
||||||
|
--background-80: oklch(0.99 0 0 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.15 0 0);
|
||||||
|
--foreground-secondary: oklch(0.35 0 0);
|
||||||
|
--foreground-muted: oklch(0.5 0 0);
|
||||||
|
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.15 0 0);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.15 0 0);
|
||||||
|
|
||||||
|
--primary: oklch(0.3 0 0);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--brand-400: oklch(0.35 0 0);
|
||||||
|
--brand-500: oklch(0.3 0 0);
|
||||||
|
--brand-600: oklch(0.25 0 0);
|
||||||
|
|
||||||
|
--secondary: oklch(0.97 0 0);
|
||||||
|
--secondary-foreground: oklch(0.15 0 0);
|
||||||
|
--muted: oklch(0.96 0 0);
|
||||||
|
--muted-foreground: oklch(0.45 0 0);
|
||||||
|
--accent: oklch(0.95 0 0);
|
||||||
|
--accent-foreground: oklch(0.15 0 0);
|
||||||
|
|
||||||
|
--destructive: oklch(0.5 0.2 25);
|
||||||
|
--border: oklch(0.9 0 0);
|
||||||
|
--border-glass: oklch(0.15 0 0 / 0.08);
|
||||||
|
--input: oklch(0.98 0 0);
|
||||||
|
--ring: oklch(0.3 0 0);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.3 0 0);
|
||||||
|
--chart-2: oklch(0.5 0 0);
|
||||||
|
--chart-3: oklch(0.4 0 0);
|
||||||
|
--chart-4: oklch(0.6 0 0);
|
||||||
|
--chart-5: oklch(0.35 0 0);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.98 0 0);
|
||||||
|
--sidebar-foreground: oklch(0.15 0 0);
|
||||||
|
--sidebar-primary: oklch(0.3 0 0);
|
||||||
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-accent: oklch(0.95 0 0);
|
||||||
|
--sidebar-accent-foreground: oklch(0.15 0 0);
|
||||||
|
--sidebar-border: oklch(0.9 0 0);
|
||||||
|
--sidebar-ring: oklch(0.3 0 0);
|
||||||
|
|
||||||
|
--action-view: oklch(0.3 0 0);
|
||||||
|
--action-view-hover: oklch(0.25 0 0);
|
||||||
|
--action-followup: oklch(0.4 0 0);
|
||||||
|
--action-followup-hover: oklch(0.35 0 0);
|
||||||
|
--action-commit: oklch(0.35 0.1 145);
|
||||||
|
--action-commit-hover: oklch(0.3 0.12 145);
|
||||||
|
--action-verify: oklch(0.35 0.1 145);
|
||||||
|
--action-verify-hover: oklch(0.3 0.12 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.3 0 0);
|
||||||
|
--running-indicator-text: oklch(0.25 0 0);
|
||||||
|
|
||||||
|
--status-success: oklch(0.4 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.4 0.15 145 / 0.12);
|
||||||
|
--status-warning: oklch(0.55 0.15 70);
|
||||||
|
--status-warning-bg: oklch(0.55 0.15 70 / 0.12);
|
||||||
|
--status-error: oklch(0.5 0.2 25);
|
||||||
|
--status-error-bg: oklch(0.5 0.2 25 / 0.12);
|
||||||
|
--status-info: oklch(0.45 0.12 230);
|
||||||
|
--status-info-bg: oklch(0.45 0.12 230 / 0.12);
|
||||||
|
--status-backlog: oklch(0.55 0 0);
|
||||||
|
--status-in-progress: oklch(0.55 0.15 70);
|
||||||
|
--status-waiting: oklch(0.5 0.1 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper .content-bg {
|
||||||
|
background: linear-gradient(135deg, oklch(0.99 0 0), oklch(0.98 0 0), oklch(0.99 0 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper .animated-outline-gradient {
|
||||||
|
background: conic-gradient(
|
||||||
|
from 90deg at 50% 50%,
|
||||||
|
oklch(0.3 0 0) 0%,
|
||||||
|
oklch(0.5 0 0) 50%,
|
||||||
|
oklch(0.3 0 0) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.paper .animated-outline-inner {
|
||||||
|
background: oklch(1 0 0) !important;
|
||||||
|
color: oklch(0.2 0 0) !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/peach.css
Normal file
92
apps/ui/src/styles/themes/peach.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Peach Theme - Soft peach/coral tones */
|
||||||
|
|
||||||
|
.peach {
|
||||||
|
--background: oklch(0.98 0.02 50);
|
||||||
|
--background-50: oklch(0.98 0.02 50 / 0.5);
|
||||||
|
--background-80: oklch(0.98 0.02 50 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.25 0.04 40);
|
||||||
|
--foreground-secondary: oklch(0.45 0.03 40);
|
||||||
|
--foreground-muted: oklch(0.55 0.03 40);
|
||||||
|
|
||||||
|
--card: oklch(0.99 0.015 50);
|
||||||
|
--card-foreground: oklch(0.25 0.04 40);
|
||||||
|
--popover: oklch(0.99 0.015 50);
|
||||||
|
--popover-foreground: oklch(0.25 0.04 40);
|
||||||
|
|
||||||
|
--primary: oklch(0.65 0.15 40);
|
||||||
|
--primary-foreground: oklch(0.99 0.015 50);
|
||||||
|
--brand-400: oklch(0.7 0.13 40);
|
||||||
|
--brand-500: oklch(0.65 0.15 40);
|
||||||
|
--brand-600: oklch(0.6 0.17 40);
|
||||||
|
|
||||||
|
--secondary: oklch(0.95 0.025 50);
|
||||||
|
--secondary-foreground: oklch(0.25 0.04 40);
|
||||||
|
--muted: oklch(0.94 0.028 50);
|
||||||
|
--muted-foreground: oklch(0.5 0.03 40);
|
||||||
|
--accent: oklch(0.92 0.032 50);
|
||||||
|
--accent-foreground: oklch(0.25 0.04 40);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 20);
|
||||||
|
--border: oklch(0.9 0.025 50);
|
||||||
|
--border-glass: oklch(0.65 0.15 40 / 0.15);
|
||||||
|
--input: oklch(0.98 0.018 50);
|
||||||
|
--ring: oklch(0.65 0.15 40);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.65 0.15 40);
|
||||||
|
--chart-2: oklch(0.6 0.15 25);
|
||||||
|
--chart-3: oklch(0.6 0.12 70);
|
||||||
|
--chart-4: oklch(0.55 0.12 350);
|
||||||
|
--chart-5: oklch(0.55 0.1 100);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.97 0.022 50);
|
||||||
|
--sidebar-foreground: oklch(0.25 0.04 40);
|
||||||
|
--sidebar-primary: oklch(0.65 0.15 40);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0.015 50);
|
||||||
|
--sidebar-accent: oklch(0.94 0.028 50);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.04 40);
|
||||||
|
--sidebar-border: oklch(0.9 0.025 50);
|
||||||
|
--sidebar-ring: oklch(0.65 0.15 40);
|
||||||
|
|
||||||
|
--action-view: oklch(0.65 0.15 40);
|
||||||
|
--action-view-hover: oklch(0.6 0.17 40);
|
||||||
|
--action-followup: oklch(0.6 0.15 25);
|
||||||
|
--action-followup-hover: oklch(0.55 0.17 25);
|
||||||
|
--action-commit: oklch(0.55 0.15 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.17 145);
|
||||||
|
--action-verify: oklch(0.55 0.15 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.17 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.65 0.15 40);
|
||||||
|
--running-indicator-text: oklch(0.6 0.17 40);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.15 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.65 0.15 70);
|
||||||
|
--status-warning-bg: oklch(0.65 0.15 70 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.2 20);
|
||||||
|
--status-error-bg: oklch(0.55 0.2 20 / 0.15);
|
||||||
|
--status-info: oklch(0.55 0.12 230);
|
||||||
|
--status-info-bg: oklch(0.55 0.12 230 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.03 40);
|
||||||
|
--status-in-progress: oklch(0.65 0.15 70);
|
||||||
|
--status-waiting: oklch(0.6 0.12 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.peach .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.98 0.02 50),
|
||||||
|
oklch(0.97 0.025 50),
|
||||||
|
oklch(0.98 0.02 50)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.peach .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #fb923c 0%, #f97316 50%, #fb923c 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.peach .animated-outline-inner {
|
||||||
|
background: oklch(0.99 0.015 50) !important;
|
||||||
|
color: #f97316 !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/rose.css
Normal file
92
apps/ui/src/styles/themes/rose.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Rose Theme - Soft pink/rose tones */
|
||||||
|
|
||||||
|
.rose {
|
||||||
|
--background: oklch(0.98 0.01 350);
|
||||||
|
--background-50: oklch(0.98 0.01 350 / 0.5);
|
||||||
|
--background-80: oklch(0.98 0.01 350 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.25 0.03 350);
|
||||||
|
--foreground-secondary: oklch(0.45 0.03 350);
|
||||||
|
--foreground-muted: oklch(0.55 0.02 350);
|
||||||
|
|
||||||
|
--card: oklch(0.99 0.008 350);
|
||||||
|
--card-foreground: oklch(0.25 0.03 350);
|
||||||
|
--popover: oklch(0.99 0.008 350);
|
||||||
|
--popover-foreground: oklch(0.25 0.03 350);
|
||||||
|
|
||||||
|
--primary: oklch(0.6 0.18 350);
|
||||||
|
--primary-foreground: oklch(0.99 0.008 350);
|
||||||
|
--brand-400: oklch(0.65 0.16 350);
|
||||||
|
--brand-500: oklch(0.6 0.18 350);
|
||||||
|
--brand-600: oklch(0.55 0.2 350);
|
||||||
|
|
||||||
|
--secondary: oklch(0.95 0.015 350);
|
||||||
|
--secondary-foreground: oklch(0.25 0.03 350);
|
||||||
|
--muted: oklch(0.94 0.02 350);
|
||||||
|
--muted-foreground: oklch(0.5 0.03 350);
|
||||||
|
--accent: oklch(0.92 0.025 350);
|
||||||
|
--accent-foreground: oklch(0.25 0.03 350);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 15);
|
||||||
|
--border: oklch(0.9 0.02 350);
|
||||||
|
--border-glass: oklch(0.6 0.18 350 / 0.15);
|
||||||
|
--input: oklch(0.98 0.01 350);
|
||||||
|
--ring: oklch(0.6 0.18 350);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.6 0.18 350);
|
||||||
|
--chart-2: oklch(0.65 0.15 320);
|
||||||
|
--chart-3: oklch(0.55 0.15 15);
|
||||||
|
--chart-4: oklch(0.6 0.15 280);
|
||||||
|
--chart-5: oklch(0.55 0.12 200);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.97 0.012 350);
|
||||||
|
--sidebar-foreground: oklch(0.25 0.03 350);
|
||||||
|
--sidebar-primary: oklch(0.6 0.18 350);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0.008 350);
|
||||||
|
--sidebar-accent: oklch(0.94 0.02 350);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.03 350);
|
||||||
|
--sidebar-border: oklch(0.9 0.02 350);
|
||||||
|
--sidebar-ring: oklch(0.6 0.18 350);
|
||||||
|
|
||||||
|
--action-view: oklch(0.6 0.18 350);
|
||||||
|
--action-view-hover: oklch(0.55 0.2 350);
|
||||||
|
--action-followup: oklch(0.6 0.15 320);
|
||||||
|
--action-followup-hover: oklch(0.55 0.17 320);
|
||||||
|
--action-commit: oklch(0.55 0.15 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.17 145);
|
||||||
|
--action-verify: oklch(0.55 0.15 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.17 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.6 0.18 350);
|
||||||
|
--running-indicator-text: oklch(0.55 0.2 350);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.15 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.65 0.15 70);
|
||||||
|
--status-warning-bg: oklch(0.65 0.15 70 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.2 15);
|
||||||
|
--status-error-bg: oklch(0.55 0.2 15 / 0.15);
|
||||||
|
--status-info: oklch(0.55 0.15 230);
|
||||||
|
--status-info-bg: oklch(0.55 0.15 230 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.02 350);
|
||||||
|
--status-in-progress: oklch(0.65 0.15 70);
|
||||||
|
--status-waiting: oklch(0.6 0.12 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rose .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.98 0.01 350),
|
||||||
|
oklch(0.97 0.015 350),
|
||||||
|
oklch(0.98 0.01 350)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rose .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #f43f5e 0%, #ec4899 50%, #f43f5e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.rose .animated-outline-inner {
|
||||||
|
background: oklch(0.99 0.008 350) !important;
|
||||||
|
color: #f43f5e !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/sand.css
Normal file
92
apps/ui/src/styles/themes/sand.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Sand Theme - Sandy beige tones */
|
||||||
|
|
||||||
|
.sand {
|
||||||
|
--background: oklch(0.96 0.02 80);
|
||||||
|
--background-50: oklch(0.96 0.02 80 / 0.5);
|
||||||
|
--background-80: oklch(0.96 0.02 80 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.25 0.03 70);
|
||||||
|
--foreground-secondary: oklch(0.45 0.03 70);
|
||||||
|
--foreground-muted: oklch(0.55 0.02 70);
|
||||||
|
|
||||||
|
--card: oklch(0.98 0.015 80);
|
||||||
|
--card-foreground: oklch(0.25 0.03 70);
|
||||||
|
--popover: oklch(0.97 0.015 80);
|
||||||
|
--popover-foreground: oklch(0.25 0.03 70);
|
||||||
|
|
||||||
|
--primary: oklch(0.55 0.12 60);
|
||||||
|
--primary-foreground: oklch(0.98 0.015 80);
|
||||||
|
--brand-400: oklch(0.6 0.1 60);
|
||||||
|
--brand-500: oklch(0.55 0.12 60);
|
||||||
|
--brand-600: oklch(0.5 0.14 60);
|
||||||
|
|
||||||
|
--secondary: oklch(0.93 0.025 80);
|
||||||
|
--secondary-foreground: oklch(0.25 0.03 70);
|
||||||
|
--muted: oklch(0.92 0.028 80);
|
||||||
|
--muted-foreground: oklch(0.5 0.03 70);
|
||||||
|
--accent: oklch(0.9 0.03 80);
|
||||||
|
--accent-foreground: oklch(0.25 0.03 70);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 25);
|
||||||
|
--border: oklch(0.87 0.03 80);
|
||||||
|
--border-glass: oklch(0.55 0.12 60 / 0.15);
|
||||||
|
--input: oklch(0.97 0.018 80);
|
||||||
|
--ring: oklch(0.55 0.12 60);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.55 0.12 60);
|
||||||
|
--chart-2: oklch(0.6 0.1 40);
|
||||||
|
--chart-3: oklch(0.55 0.1 100);
|
||||||
|
--chart-4: oklch(0.5 0.08 30);
|
||||||
|
--chart-5: oklch(0.55 0.08 120);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.95 0.022 80);
|
||||||
|
--sidebar-foreground: oklch(0.25 0.03 70);
|
||||||
|
--sidebar-primary: oklch(0.55 0.12 60);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0.015 80);
|
||||||
|
--sidebar-accent: oklch(0.92 0.028 80);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.03 70);
|
||||||
|
--sidebar-border: oklch(0.87 0.03 80);
|
||||||
|
--sidebar-ring: oklch(0.55 0.12 60);
|
||||||
|
|
||||||
|
--action-view: oklch(0.55 0.12 60);
|
||||||
|
--action-view-hover: oklch(0.5 0.14 60);
|
||||||
|
--action-followup: oklch(0.55 0.1 40);
|
||||||
|
--action-followup-hover: oklch(0.5 0.12 40);
|
||||||
|
--action-commit: oklch(0.55 0.12 130);
|
||||||
|
--action-commit-hover: oklch(0.5 0.14 130);
|
||||||
|
--action-verify: oklch(0.55 0.12 130);
|
||||||
|
--action-verify-hover: oklch(0.5 0.14 130);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.55 0.12 60);
|
||||||
|
--running-indicator-text: oklch(0.5 0.14 60);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.12 130);
|
||||||
|
--status-success-bg: oklch(0.55 0.12 130 / 0.15);
|
||||||
|
--status-warning: oklch(0.6 0.12 70);
|
||||||
|
--status-warning-bg: oklch(0.6 0.12 70 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.18 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.18 25 / 0.15);
|
||||||
|
--status-info: oklch(0.5 0.12 230);
|
||||||
|
--status-info-bg: oklch(0.5 0.12 230 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.03 70);
|
||||||
|
--status-in-progress: oklch(0.6 0.12 70);
|
||||||
|
--status-waiting: oklch(0.58 0.1 55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sand .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.96 0.02 80),
|
||||||
|
oklch(0.95 0.025 80),
|
||||||
|
oklch(0.96 0.02 80)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sand .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #d97706 0%, #b45309 50%, #d97706 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sand .animated-outline-inner {
|
||||||
|
background: oklch(0.98 0.015 80) !important;
|
||||||
|
color: #d97706 !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/sepia.css
Normal file
92
apps/ui/src/styles/themes/sepia.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Sepia Theme - Warm sepia/parchment tones */
|
||||||
|
|
||||||
|
.sepia {
|
||||||
|
--background: oklch(0.95 0.025 70);
|
||||||
|
--background-50: oklch(0.95 0.025 70 / 0.5);
|
||||||
|
--background-80: oklch(0.95 0.025 70 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.25 0.04 50);
|
||||||
|
--foreground-secondary: oklch(0.45 0.03 50);
|
||||||
|
--foreground-muted: oklch(0.55 0.03 50);
|
||||||
|
|
||||||
|
--card: oklch(0.97 0.02 70);
|
||||||
|
--card-foreground: oklch(0.25 0.04 50);
|
||||||
|
--popover: oklch(0.96 0.022 70);
|
||||||
|
--popover-foreground: oklch(0.25 0.04 50);
|
||||||
|
|
||||||
|
--primary: oklch(0.5 0.12 50);
|
||||||
|
--primary-foreground: oklch(0.97 0.02 70);
|
||||||
|
--brand-400: oklch(0.55 0.1 50);
|
||||||
|
--brand-500: oklch(0.5 0.12 50);
|
||||||
|
--brand-600: oklch(0.45 0.14 50);
|
||||||
|
|
||||||
|
--secondary: oklch(0.92 0.028 70);
|
||||||
|
--secondary-foreground: oklch(0.25 0.04 50);
|
||||||
|
--muted: oklch(0.91 0.03 70);
|
||||||
|
--muted-foreground: oklch(0.5 0.03 50);
|
||||||
|
--accent: oklch(0.89 0.035 70);
|
||||||
|
--accent-foreground: oklch(0.25 0.04 50);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.18 25);
|
||||||
|
--border: oklch(0.86 0.035 70);
|
||||||
|
--border-glass: oklch(0.5 0.12 50 / 0.15);
|
||||||
|
--input: oklch(0.96 0.022 70);
|
||||||
|
--ring: oklch(0.5 0.12 50);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.5 0.12 50);
|
||||||
|
--chart-2: oklch(0.55 0.1 35);
|
||||||
|
--chart-3: oklch(0.5 0.08 80);
|
||||||
|
--chart-4: oklch(0.45 0.1 30);
|
||||||
|
--chart-5: oklch(0.5 0.06 100);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.94 0.026 70);
|
||||||
|
--sidebar-foreground: oklch(0.25 0.04 50);
|
||||||
|
--sidebar-primary: oklch(0.5 0.12 50);
|
||||||
|
--sidebar-primary-foreground: oklch(0.97 0.02 70);
|
||||||
|
--sidebar-accent: oklch(0.91 0.03 70);
|
||||||
|
--sidebar-accent-foreground: oklch(0.25 0.04 50);
|
||||||
|
--sidebar-border: oklch(0.86 0.035 70);
|
||||||
|
--sidebar-ring: oklch(0.5 0.12 50);
|
||||||
|
|
||||||
|
--action-view: oklch(0.5 0.12 50);
|
||||||
|
--action-view-hover: oklch(0.45 0.14 50);
|
||||||
|
--action-followup: oklch(0.5 0.1 35);
|
||||||
|
--action-followup-hover: oklch(0.45 0.12 35);
|
||||||
|
--action-commit: oklch(0.5 0.12 130);
|
||||||
|
--action-commit-hover: oklch(0.45 0.14 130);
|
||||||
|
--action-verify: oklch(0.5 0.12 130);
|
||||||
|
--action-verify-hover: oklch(0.45 0.14 130);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.5 0.12 50);
|
||||||
|
--running-indicator-text: oklch(0.45 0.14 50);
|
||||||
|
|
||||||
|
--status-success: oklch(0.5 0.12 130);
|
||||||
|
--status-success-bg: oklch(0.5 0.12 130 / 0.15);
|
||||||
|
--status-warning: oklch(0.58 0.12 70);
|
||||||
|
--status-warning-bg: oklch(0.58 0.12 70 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.18 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.18 25 / 0.15);
|
||||||
|
--status-info: oklch(0.5 0.1 230);
|
||||||
|
--status-info-bg: oklch(0.5 0.1 230 / 0.15);
|
||||||
|
--status-backlog: oklch(0.58 0.03 50);
|
||||||
|
--status-in-progress: oklch(0.58 0.12 70);
|
||||||
|
--status-waiting: oklch(0.55 0.1 55);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sepia .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.95 0.025 70),
|
||||||
|
oklch(0.94 0.028 70),
|
||||||
|
oklch(0.95 0.025 70)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sepia .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #92400e 0%, #a16207 50%, #92400e 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sepia .animated-outline-inner {
|
||||||
|
background: oklch(0.97 0.02 70) !important;
|
||||||
|
color: #92400e !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/sky.css
Normal file
92
apps/ui/src/styles/themes/sky.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Sky Theme - Light sky blue */
|
||||||
|
|
||||||
|
.sky {
|
||||||
|
--background: oklch(0.98 0.015 220);
|
||||||
|
--background-50: oklch(0.98 0.015 220 / 0.5);
|
||||||
|
--background-80: oklch(0.98 0.015 220 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.2 0.03 220);
|
||||||
|
--foreground-secondary: oklch(0.4 0.03 220);
|
||||||
|
--foreground-muted: oklch(0.55 0.02 220);
|
||||||
|
|
||||||
|
--card: oklch(0.99 0.01 220);
|
||||||
|
--card-foreground: oklch(0.2 0.03 220);
|
||||||
|
--popover: oklch(0.99 0.01 220);
|
||||||
|
--popover-foreground: oklch(0.2 0.03 220);
|
||||||
|
|
||||||
|
--primary: oklch(0.55 0.15 225);
|
||||||
|
--primary-foreground: oklch(0.99 0.01 220);
|
||||||
|
--brand-400: oklch(0.6 0.13 225);
|
||||||
|
--brand-500: oklch(0.55 0.15 225);
|
||||||
|
--brand-600: oklch(0.5 0.17 225);
|
||||||
|
|
||||||
|
--secondary: oklch(0.95 0.018 220);
|
||||||
|
--secondary-foreground: oklch(0.2 0.03 220);
|
||||||
|
--muted: oklch(0.94 0.02 220);
|
||||||
|
--muted-foreground: oklch(0.5 0.03 220);
|
||||||
|
--accent: oklch(0.92 0.025 220);
|
||||||
|
--accent-foreground: oklch(0.2 0.03 220);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 25);
|
||||||
|
--border: oklch(0.88 0.02 220);
|
||||||
|
--border-glass: oklch(0.55 0.15 225 / 0.15);
|
||||||
|
--input: oklch(0.98 0.012 220);
|
||||||
|
--ring: oklch(0.55 0.15 225);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.55 0.15 225);
|
||||||
|
--chart-2: oklch(0.6 0.12 200);
|
||||||
|
--chart-3: oklch(0.55 0.15 250);
|
||||||
|
--chart-4: oklch(0.6 0.12 180);
|
||||||
|
--chart-5: oklch(0.55 0.1 270);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.97 0.015 220);
|
||||||
|
--sidebar-foreground: oklch(0.2 0.03 220);
|
||||||
|
--sidebar-primary: oklch(0.55 0.15 225);
|
||||||
|
--sidebar-primary-foreground: oklch(0.99 0.01 220);
|
||||||
|
--sidebar-accent: oklch(0.94 0.02 220);
|
||||||
|
--sidebar-accent-foreground: oklch(0.2 0.03 220);
|
||||||
|
--sidebar-border: oklch(0.88 0.02 220);
|
||||||
|
--sidebar-ring: oklch(0.55 0.15 225);
|
||||||
|
|
||||||
|
--action-view: oklch(0.55 0.15 225);
|
||||||
|
--action-view-hover: oklch(0.5 0.17 225);
|
||||||
|
--action-followup: oklch(0.55 0.12 200);
|
||||||
|
--action-followup-hover: oklch(0.5 0.14 200);
|
||||||
|
--action-commit: oklch(0.55 0.15 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.17 145);
|
||||||
|
--action-verify: oklch(0.55 0.15 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.17 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.55 0.15 225);
|
||||||
|
--running-indicator-text: oklch(0.5 0.17 225);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.15 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.65 0.15 70);
|
||||||
|
--status-warning-bg: oklch(0.65 0.15 70 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.2 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.2 25 / 0.15);
|
||||||
|
--status-info: oklch(0.55 0.15 225);
|
||||||
|
--status-info-bg: oklch(0.55 0.15 225 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.02 220);
|
||||||
|
--status-in-progress: oklch(0.65 0.15 70);
|
||||||
|
--status-waiting: oklch(0.6 0.12 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sky .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.98 0.015 220),
|
||||||
|
oklch(0.97 0.018 220),
|
||||||
|
oklch(0.98 0.015 220)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sky .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #0ea5e9 0%, #38bdf8 50%, #0ea5e9 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sky .animated-outline-inner {
|
||||||
|
background: oklch(0.99 0.01 220) !important;
|
||||||
|
color: #0ea5e9 !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/snow.css
Normal file
92
apps/ui/src/styles/themes/snow.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Snow Theme - Clean white with cool blue tints */
|
||||||
|
|
||||||
|
.snow {
|
||||||
|
--background: oklch(0.99 0.005 240);
|
||||||
|
--background-50: oklch(0.99 0.005 240 / 0.5);
|
||||||
|
--background-80: oklch(0.99 0.005 240 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.2 0.02 240);
|
||||||
|
--foreground-secondary: oklch(0.4 0.02 240);
|
||||||
|
--foreground-muted: oklch(0.55 0.015 240);
|
||||||
|
|
||||||
|
--card: oklch(1 0 0);
|
||||||
|
--card-foreground: oklch(0.2 0.02 240);
|
||||||
|
--popover: oklch(1 0 0);
|
||||||
|
--popover-foreground: oklch(0.2 0.02 240);
|
||||||
|
|
||||||
|
--primary: oklch(0.5 0.15 240);
|
||||||
|
--primary-foreground: oklch(1 0 0);
|
||||||
|
--brand-400: oklch(0.55 0.13 240);
|
||||||
|
--brand-500: oklch(0.5 0.15 240);
|
||||||
|
--brand-600: oklch(0.45 0.17 240);
|
||||||
|
|
||||||
|
--secondary: oklch(0.97 0.008 240);
|
||||||
|
--secondary-foreground: oklch(0.2 0.02 240);
|
||||||
|
--muted: oklch(0.96 0.01 240);
|
||||||
|
--muted-foreground: oklch(0.5 0.015 240);
|
||||||
|
--accent: oklch(0.95 0.012 240);
|
||||||
|
--accent-foreground: oklch(0.2 0.02 240);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 25);
|
||||||
|
--border: oklch(0.92 0.008 240);
|
||||||
|
--border-glass: oklch(0.5 0.15 240 / 0.1);
|
||||||
|
--input: oklch(0.98 0.005 240);
|
||||||
|
--ring: oklch(0.5 0.15 240);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.5 0.15 240);
|
||||||
|
--chart-2: oklch(0.55 0.12 200);
|
||||||
|
--chart-3: oklch(0.5 0.15 280);
|
||||||
|
--chart-4: oklch(0.55 0.12 180);
|
||||||
|
--chart-5: oklch(0.5 0.1 260);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.98 0.006 240);
|
||||||
|
--sidebar-foreground: oklch(0.2 0.02 240);
|
||||||
|
--sidebar-primary: oklch(0.5 0.15 240);
|
||||||
|
--sidebar-primary-foreground: oklch(1 0 0);
|
||||||
|
--sidebar-accent: oklch(0.96 0.01 240);
|
||||||
|
--sidebar-accent-foreground: oklch(0.2 0.02 240);
|
||||||
|
--sidebar-border: oklch(0.92 0.008 240);
|
||||||
|
--sidebar-ring: oklch(0.5 0.15 240);
|
||||||
|
|
||||||
|
--action-view: oklch(0.5 0.15 240);
|
||||||
|
--action-view-hover: oklch(0.45 0.17 240);
|
||||||
|
--action-followup: oklch(0.5 0.12 200);
|
||||||
|
--action-followup-hover: oklch(0.45 0.14 200);
|
||||||
|
--action-commit: oklch(0.55 0.15 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.17 145);
|
||||||
|
--action-verify: oklch(0.55 0.15 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.17 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.5 0.15 240);
|
||||||
|
--running-indicator-text: oklch(0.45 0.17 240);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.15 145 / 0.12);
|
||||||
|
--status-warning: oklch(0.65 0.15 70);
|
||||||
|
--status-warning-bg: oklch(0.65 0.15 70 / 0.12);
|
||||||
|
--status-error: oklch(0.55 0.2 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.2 25 / 0.12);
|
||||||
|
--status-info: oklch(0.5 0.15 240);
|
||||||
|
--status-info-bg: oklch(0.5 0.15 240 / 0.12);
|
||||||
|
--status-backlog: oklch(0.6 0.015 240);
|
||||||
|
--status-in-progress: oklch(0.65 0.15 70);
|
||||||
|
--status-waiting: oklch(0.6 0.12 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snow .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.99 0.005 240),
|
||||||
|
oklch(0.98 0.008 240),
|
||||||
|
oklch(0.99 0.005 240)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snow .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #6366f1 0%, #8b5cf6 50%, #6366f1 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.snow .animated-outline-inner {
|
||||||
|
background: oklch(1 0 0) !important;
|
||||||
|
color: #6366f1 !important;
|
||||||
|
}
|
||||||
92
apps/ui/src/styles/themes/solarizedlight.css
Normal file
92
apps/ui/src/styles/themes/solarizedlight.css
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
/* Solarized Light Theme */
|
||||||
|
|
||||||
|
.solarizedlight {
|
||||||
|
--background: oklch(0.98 0.01 90);
|
||||||
|
--background-50: oklch(0.98 0.01 90 / 0.5);
|
||||||
|
--background-80: oklch(0.98 0.01 90 / 0.8);
|
||||||
|
|
||||||
|
--foreground: oklch(0.35 0.05 200);
|
||||||
|
--foreground-secondary: oklch(0.5 0.05 200);
|
||||||
|
--foreground-muted: oklch(0.6 0.04 200);
|
||||||
|
|
||||||
|
--card: oklch(0.95 0.01 90);
|
||||||
|
--card-foreground: oklch(0.35 0.05 200);
|
||||||
|
--popover: oklch(0.96 0.01 90);
|
||||||
|
--popover-foreground: oklch(0.35 0.05 200);
|
||||||
|
|
||||||
|
--primary: oklch(0.55 0.15 230);
|
||||||
|
--primary-foreground: oklch(0.98 0.01 90);
|
||||||
|
--brand-400: oklch(0.6 0.13 230);
|
||||||
|
--brand-500: oklch(0.55 0.15 230);
|
||||||
|
--brand-600: oklch(0.5 0.17 230);
|
||||||
|
|
||||||
|
--secondary: oklch(0.92 0.01 90);
|
||||||
|
--secondary-foreground: oklch(0.35 0.05 200);
|
||||||
|
--muted: oklch(0.93 0.01 90);
|
||||||
|
--muted-foreground: oklch(0.5 0.04 200);
|
||||||
|
--accent: oklch(0.9 0.02 90);
|
||||||
|
--accent-foreground: oklch(0.35 0.05 200);
|
||||||
|
|
||||||
|
--destructive: oklch(0.55 0.2 25);
|
||||||
|
--border: oklch(0.88 0.015 90);
|
||||||
|
--border-glass: oklch(0.35 0.05 200 / 0.1);
|
||||||
|
--input: oklch(0.97 0.008 90);
|
||||||
|
--ring: oklch(0.55 0.15 230);
|
||||||
|
|
||||||
|
--chart-1: oklch(0.55 0.15 230);
|
||||||
|
--chart-2: oklch(0.6 0.18 25);
|
||||||
|
--chart-3: oklch(0.55 0.15 300);
|
||||||
|
--chart-4: oklch(0.6 0.15 180);
|
||||||
|
--chart-5: oklch(0.65 0.12 80);
|
||||||
|
|
||||||
|
--sidebar: oklch(0.96 0.01 90);
|
||||||
|
--sidebar-foreground: oklch(0.35 0.05 200);
|
||||||
|
--sidebar-primary: oklch(0.55 0.15 230);
|
||||||
|
--sidebar-primary-foreground: oklch(0.98 0.01 90);
|
||||||
|
--sidebar-accent: oklch(0.92 0.01 90);
|
||||||
|
--sidebar-accent-foreground: oklch(0.35 0.05 200);
|
||||||
|
--sidebar-border: oklch(0.88 0.015 90);
|
||||||
|
--sidebar-ring: oklch(0.55 0.15 230);
|
||||||
|
|
||||||
|
--action-view: oklch(0.55 0.15 230);
|
||||||
|
--action-view-hover: oklch(0.5 0.17 230);
|
||||||
|
--action-followup: oklch(0.6 0.18 25);
|
||||||
|
--action-followup-hover: oklch(0.55 0.2 25);
|
||||||
|
--action-commit: oklch(0.55 0.15 145);
|
||||||
|
--action-commit-hover: oklch(0.5 0.17 145);
|
||||||
|
--action-verify: oklch(0.55 0.15 145);
|
||||||
|
--action-verify-hover: oklch(0.5 0.17 145);
|
||||||
|
|
||||||
|
--running-indicator: oklch(0.55 0.15 230);
|
||||||
|
--running-indicator-text: oklch(0.5 0.17 230);
|
||||||
|
|
||||||
|
--status-success: oklch(0.55 0.15 145);
|
||||||
|
--status-success-bg: oklch(0.55 0.15 145 / 0.15);
|
||||||
|
--status-warning: oklch(0.65 0.15 80);
|
||||||
|
--status-warning-bg: oklch(0.65 0.15 80 / 0.15);
|
||||||
|
--status-error: oklch(0.55 0.2 25);
|
||||||
|
--status-error-bg: oklch(0.55 0.2 25 / 0.15);
|
||||||
|
--status-info: oklch(0.55 0.15 230);
|
||||||
|
--status-info-bg: oklch(0.55 0.15 230 / 0.15);
|
||||||
|
--status-backlog: oklch(0.6 0.04 200);
|
||||||
|
--status-in-progress: oklch(0.65 0.15 80);
|
||||||
|
--status-waiting: oklch(0.6 0.12 50);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solarizedlight .content-bg {
|
||||||
|
background: linear-gradient(
|
||||||
|
135deg,
|
||||||
|
oklch(0.98 0.01 90),
|
||||||
|
oklch(0.96 0.012 90),
|
||||||
|
oklch(0.98 0.01 90)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solarizedlight .animated-outline-gradient {
|
||||||
|
background: conic-gradient(from 90deg at 50% 50%, #268bd2 0%, #2aa198 50%, #268bd2 100%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.solarizedlight .animated-outline-inner {
|
||||||
|
background: oklch(0.98 0.01 90) !important;
|
||||||
|
color: #268bd2 !important;
|
||||||
|
}
|
||||||
@@ -3,15 +3,15 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { createLogger } from '@automaker/utils';
|
import { createLogger } from '@automaker/utils';
|
||||||
import fs from "fs/promises";
|
import { secureFs } from '@automaker/platform';
|
||||||
import path from "path";
|
import path from 'path';
|
||||||
import { exec } from "child_process";
|
import { exec } from 'child_process';
|
||||||
import { promisify } from "util";
|
import { promisify } from 'util';
|
||||||
import { BINARY_EXTENSIONS, type FileStatus } from './types.js';
|
import { BINARY_EXTENSIONS, type FileStatus } from './types.js';
|
||||||
import { isGitRepo, parseGitStatus } from './status.js';
|
import { isGitRepo, parseGitStatus } from './status.js';
|
||||||
|
|
||||||
const execAsync = promisify(exec);
|
const execAsync = promisify(exec);
|
||||||
const logger = createLogger("GitUtils");
|
const logger = createLogger('GitUtils');
|
||||||
|
|
||||||
// Max file size for generating synthetic diffs (1MB)
|
// Max file size for generating synthetic diffs (1MB)
|
||||||
const MAX_SYNTHETIC_DIFF_SIZE = 1024 * 1024;
|
const MAX_SYNTHETIC_DIFF_SIZE = 1024 * 1024;
|
||||||
@@ -45,7 +45,7 @@ Binary file ${relativePath} added
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get file stats to check size
|
// Get file stats to check size
|
||||||
const stats = await fs.stat(fullPath);
|
const stats = await secureFs.stat(fullPath);
|
||||||
if (stats.size > MAX_SYNTHETIC_DIFF_SIZE) {
|
if (stats.size > MAX_SYNTHETIC_DIFF_SIZE) {
|
||||||
const sizeKB = Math.round(stats.size / 1024);
|
const sizeKB = Math.round(stats.size / 1024);
|
||||||
return `diff --git a/${relativePath} b/${relativePath}
|
return `diff --git a/${relativePath} b/${relativePath}
|
||||||
@@ -59,18 +59,18 @@ index 0000000..0000000
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Read file content
|
// Read file content
|
||||||
const content = await fs.readFile(fullPath, "utf-8");
|
const content = (await secureFs.readFile(fullPath, 'utf-8')) as string;
|
||||||
const hasTrailingNewline = content.endsWith("\n");
|
const hasTrailingNewline = content.endsWith('\n');
|
||||||
const lines = content.split("\n");
|
const lines = content.split('\n');
|
||||||
|
|
||||||
// Remove trailing empty line if the file ends with newline
|
// Remove trailing empty line if the file ends with newline
|
||||||
if (lines.length > 0 && lines.at(-1) === "") {
|
if (lines.length > 0 && lines.at(-1) === '') {
|
||||||
lines.pop();
|
lines.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate diff format
|
// Generate diff format
|
||||||
const lineCount = lines.length;
|
const lineCount = lines.length;
|
||||||
const addedLines = lines.map(line => `+${line}`).join("\n");
|
const addedLines = lines.map((line) => `+${line}`).join('\n');
|
||||||
|
|
||||||
let diff = `diff --git a/${relativePath} b/${relativePath}
|
let diff = `diff --git a/${relativePath} b/${relativePath}
|
||||||
new file mode 100644
|
new file mode 100644
|
||||||
@@ -82,10 +82,10 @@ ${addedLines}`;
|
|||||||
|
|
||||||
// Add "No newline at end of file" indicator if needed
|
// Add "No newline at end of file" indicator if needed
|
||||||
if (!hasTrailingNewline && content.length > 0) {
|
if (!hasTrailingNewline && content.length > 0) {
|
||||||
diff += "\n\\ No newline at end of file";
|
diff += '\n\\ No newline at end of file';
|
||||||
}
|
}
|
||||||
|
|
||||||
return diff + "\n";
|
return diff + '\n';
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Log the error for debugging
|
// Log the error for debugging
|
||||||
logger.error(`Failed to generate synthetic diff for ${fullPath}:`, error);
|
logger.error(`Failed to generate synthetic diff for ${fullPath}:`, error);
|
||||||
@@ -110,7 +110,7 @@ export async function appendUntrackedFileDiffs(
|
|||||||
files: Array<{ status: string; path: string }>
|
files: Array<{ status: string; path: string }>
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Find untracked files (status "?")
|
// Find untracked files (status "?")
|
||||||
const untrackedFiles = files.filter(f => f.status === "?");
|
const untrackedFiles = files.filter((f) => f.status === '?');
|
||||||
|
|
||||||
if (untrackedFiles.length === 0) {
|
if (untrackedFiles.length === 0) {
|
||||||
return existingDiff;
|
return existingDiff;
|
||||||
@@ -118,11 +118,11 @@ export async function appendUntrackedFileDiffs(
|
|||||||
|
|
||||||
// Generate synthetic diffs for each untracked file
|
// Generate synthetic diffs for each untracked file
|
||||||
const syntheticDiffs = await Promise.all(
|
const syntheticDiffs = await Promise.all(
|
||||||
untrackedFiles.map(f => generateSyntheticDiffForNewFile(basePath, f.path))
|
untrackedFiles.map((f) => generateSyntheticDiffForNewFile(basePath, f.path))
|
||||||
);
|
);
|
||||||
|
|
||||||
// Combine existing diff with synthetic diffs
|
// Combine existing diff with synthetic diffs
|
||||||
const combinedDiff = existingDiff + syntheticDiffs.join("");
|
const combinedDiff = existingDiff + syntheticDiffs.join('');
|
||||||
|
|
||||||
return combinedDiff;
|
return combinedDiff;
|
||||||
}
|
}
|
||||||
@@ -133,25 +133,39 @@ export async function appendUntrackedFileDiffs(
|
|||||||
*/
|
*/
|
||||||
export async function listAllFilesInDirectory(
|
export async function listAllFilesInDirectory(
|
||||||
basePath: string,
|
basePath: string,
|
||||||
relativePath: string = ""
|
relativePath: string = ''
|
||||||
): Promise<string[]> {
|
): Promise<string[]> {
|
||||||
const files: string[] = [];
|
const files: string[] = [];
|
||||||
const fullPath = path.join(basePath, relativePath);
|
const fullPath = path.join(basePath, relativePath);
|
||||||
|
|
||||||
// Directories to skip
|
// Directories to skip
|
||||||
const skipDirs = new Set([
|
const skipDirs = new Set([
|
||||||
"node_modules", ".git", ".automaker", "dist", "build",
|
'node_modules',
|
||||||
".next", ".nuxt", "__pycache__", ".cache", "coverage",
|
'.git',
|
||||||
".venv", "venv", "target", "vendor", ".gradle",
|
'.automaker',
|
||||||
"out", "tmp", ".tmp"
|
'dist',
|
||||||
|
'build',
|
||||||
|
'.next',
|
||||||
|
'.nuxt',
|
||||||
|
'__pycache__',
|
||||||
|
'.cache',
|
||||||
|
'coverage',
|
||||||
|
'.venv',
|
||||||
|
'venv',
|
||||||
|
'target',
|
||||||
|
'vendor',
|
||||||
|
'.gradle',
|
||||||
|
'out',
|
||||||
|
'tmp',
|
||||||
|
'.tmp',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const entries = await fs.readdir(fullPath, { withFileTypes: true });
|
const entries = await secureFs.readdir(fullPath, { withFileTypes: true });
|
||||||
|
|
||||||
for (const entry of entries) {
|
for (const entry of entries) {
|
||||||
// Skip hidden files/folders (except we want to allow some)
|
// Skip hidden files/folders (except we want to allow some)
|
||||||
if (entry.name.startsWith(".") && entry.name !== ".env") {
|
if (entry.name.startsWith('.') && entry.name !== '.env') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,19 +197,19 @@ export async function generateDiffsForNonGitDirectory(
|
|||||||
): Promise<{ diff: string; files: FileStatus[] }> {
|
): Promise<{ diff: string; files: FileStatus[] }> {
|
||||||
const allFiles = await listAllFilesInDirectory(basePath);
|
const allFiles = await listAllFilesInDirectory(basePath);
|
||||||
|
|
||||||
const files: FileStatus[] = allFiles.map(filePath => ({
|
const files: FileStatus[] = allFiles.map((filePath) => ({
|
||||||
status: "?",
|
status: '?',
|
||||||
path: filePath,
|
path: filePath,
|
||||||
statusText: "New",
|
statusText: 'New',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Generate synthetic diffs for all files
|
// Generate synthetic diffs for all files
|
||||||
const syntheticDiffs = await Promise.all(
|
const syntheticDiffs = await Promise.all(
|
||||||
files.map(f => generateSyntheticDiffForNewFile(basePath, f.path))
|
files.map((f) => generateSyntheticDiffForNewFile(basePath, f.path))
|
||||||
);
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
diff: syntheticDiffs.join(""),
|
diff: syntheticDiffs.join(''),
|
||||||
files,
|
files,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -221,11 +235,11 @@ export async function getGitRepositoryDiffs(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get git diff and status
|
// Get git diff and status
|
||||||
const { stdout: diff } = await execAsync("git diff HEAD", {
|
const { stdout: diff } = await execAsync('git diff HEAD', {
|
||||||
cwd: repoPath,
|
cwd: repoPath,
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
maxBuffer: 10 * 1024 * 1024,
|
||||||
});
|
});
|
||||||
const { stdout: status } = await execAsync("git status --porcelain", {
|
const { stdout: status } = await execAsync('git status --porcelain', {
|
||||||
cwd: repoPath,
|
cwd: repoPath,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -14,42 +14,61 @@ export type { AgentModel };
|
|||||||
/**
|
/**
|
||||||
* ThemeMode - Available color themes for the UI
|
* ThemeMode - Available color themes for the UI
|
||||||
*
|
*
|
||||||
* Includes system theme and multiple color schemes:
|
* Includes system theme and multiple color schemes organized by dark/light:
|
||||||
* - System: Respects OS dark/light mode preference
|
* - System: Respects OS dark/light mode preference
|
||||||
* - Light/Dark: Basic light and dark variants
|
* - Dark themes (16): dark, retro, dracula, nord, monokai, tokyonight, solarized,
|
||||||
* - Color Schemes: Retro, Dracula, Nord, Monokai, Tokyo Night, Solarized, Gruvbox,
|
* gruvbox, catppuccin, onedark, synthwave, red, sunset, gray, forest, ocean
|
||||||
* Catppuccin, OneDark, Synthwave, Red, Cream, Sunset, Gray
|
* - Light themes (16): light, cream, solarizedlight, github, paper, rose, mint,
|
||||||
|
* lavender, sand, sky, peach, snow, sepia, gruvboxlight, nordlight, blossom
|
||||||
*/
|
*/
|
||||||
export type ThemeMode =
|
export type ThemeMode =
|
||||||
| "light"
|
| 'system'
|
||||||
| "dark"
|
// Dark themes (16)
|
||||||
| "system"
|
| 'dark'
|
||||||
| "retro"
|
| 'retro'
|
||||||
| "dracula"
|
| 'dracula'
|
||||||
| "nord"
|
| 'nord'
|
||||||
| "monokai"
|
| 'monokai'
|
||||||
| "tokyonight"
|
| 'tokyonight'
|
||||||
| "solarized"
|
| 'solarized'
|
||||||
| "gruvbox"
|
| 'gruvbox'
|
||||||
| "catppuccin"
|
| 'catppuccin'
|
||||||
| "onedark"
|
| 'onedark'
|
||||||
| "synthwave"
|
| 'synthwave'
|
||||||
| "red"
|
| 'red'
|
||||||
| "cream"
|
| 'sunset'
|
||||||
| "sunset"
|
| 'gray'
|
||||||
| "gray";
|
| 'forest'
|
||||||
|
| 'ocean'
|
||||||
|
// Light themes (16)
|
||||||
|
| 'light'
|
||||||
|
| 'cream'
|
||||||
|
| 'solarizedlight'
|
||||||
|
| 'github'
|
||||||
|
| 'paper'
|
||||||
|
| 'rose'
|
||||||
|
| 'mint'
|
||||||
|
| 'lavender'
|
||||||
|
| 'sand'
|
||||||
|
| 'sky'
|
||||||
|
| 'peach'
|
||||||
|
| 'snow'
|
||||||
|
| 'sepia'
|
||||||
|
| 'gruvboxlight'
|
||||||
|
| 'nordlight'
|
||||||
|
| 'blossom';
|
||||||
|
|
||||||
/** KanbanCardDetailLevel - Controls how much information is displayed on kanban cards */
|
/** KanbanCardDetailLevel - Controls how much information is displayed on kanban cards */
|
||||||
export type KanbanCardDetailLevel = "minimal" | "standard" | "detailed";
|
export type KanbanCardDetailLevel = 'minimal' | 'standard' | 'detailed';
|
||||||
|
|
||||||
/** PlanningMode - Planning levels for feature generation workflows */
|
/** PlanningMode - Planning levels for feature generation workflows */
|
||||||
export type PlanningMode = "skip" | "lite" | "spec" | "full";
|
export type PlanningMode = 'skip' | 'lite' | 'spec' | 'full';
|
||||||
|
|
||||||
/** ThinkingLevel - Extended thinking levels for Claude models (reasoning intensity) */
|
/** ThinkingLevel - Extended thinking levels for Claude models (reasoning intensity) */
|
||||||
export type ThinkingLevel = "none" | "low" | "medium" | "high" | "ultrathink";
|
export type ThinkingLevel = 'none' | 'low' | 'medium' | 'high' | 'ultrathink';
|
||||||
|
|
||||||
/** ModelProvider - AI model provider for credentials and API key management */
|
/** ModelProvider - AI model provider for credentials and API key management */
|
||||||
export type ModelProvider = "claude";
|
export type ModelProvider = 'claude';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* KeyboardShortcuts - User-configurable keyboard bindings for common actions
|
* KeyboardShortcuts - User-configurable keyboard bindings for common actions
|
||||||
@@ -356,45 +375,45 @@ export interface ProjectSettings {
|
|||||||
|
|
||||||
/** Default keyboard shortcut bindings */
|
/** Default keyboard shortcut bindings */
|
||||||
export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
|
export const DEFAULT_KEYBOARD_SHORTCUTS: KeyboardShortcuts = {
|
||||||
board: "K",
|
board: 'K',
|
||||||
agent: "A",
|
agent: 'A',
|
||||||
spec: "D",
|
spec: 'D',
|
||||||
context: "C",
|
context: 'C',
|
||||||
settings: "S",
|
settings: 'S',
|
||||||
profiles: "M",
|
profiles: 'M',
|
||||||
terminal: "T",
|
terminal: 'T',
|
||||||
toggleSidebar: "`",
|
toggleSidebar: '`',
|
||||||
addFeature: "N",
|
addFeature: 'N',
|
||||||
addContextFile: "N",
|
addContextFile: 'N',
|
||||||
startNext: "G",
|
startNext: 'G',
|
||||||
newSession: "N",
|
newSession: 'N',
|
||||||
openProject: "O",
|
openProject: 'O',
|
||||||
projectPicker: "P",
|
projectPicker: 'P',
|
||||||
cyclePrevProject: "Q",
|
cyclePrevProject: 'Q',
|
||||||
cycleNextProject: "E",
|
cycleNextProject: 'E',
|
||||||
addProfile: "N",
|
addProfile: 'N',
|
||||||
splitTerminalRight: "Alt+D",
|
splitTerminalRight: 'Alt+D',
|
||||||
splitTerminalDown: "Alt+S",
|
splitTerminalDown: 'Alt+S',
|
||||||
closeTerminal: "Alt+W",
|
closeTerminal: 'Alt+W',
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Default global settings used when no settings file exists */
|
/** Default global settings used when no settings file exists */
|
||||||
export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
||||||
version: 1,
|
version: 1,
|
||||||
theme: "dark",
|
theme: 'dark',
|
||||||
sidebarOpen: true,
|
sidebarOpen: true,
|
||||||
chatHistoryOpen: false,
|
chatHistoryOpen: false,
|
||||||
kanbanCardDetailLevel: "standard",
|
kanbanCardDetailLevel: 'standard',
|
||||||
maxConcurrency: 3,
|
maxConcurrency: 3,
|
||||||
defaultSkipTests: true,
|
defaultSkipTests: true,
|
||||||
enableDependencyBlocking: true,
|
enableDependencyBlocking: true,
|
||||||
useWorktrees: false,
|
useWorktrees: false,
|
||||||
showProfilesOnly: false,
|
showProfilesOnly: false,
|
||||||
defaultPlanningMode: "skip",
|
defaultPlanningMode: 'skip',
|
||||||
defaultRequirePlanApproval: false,
|
defaultRequirePlanApproval: false,
|
||||||
defaultAIProfileId: null,
|
defaultAIProfileId: null,
|
||||||
muteDoneSound: false,
|
muteDoneSound: false,
|
||||||
enhancementModel: "sonnet",
|
enhancementModel: 'sonnet',
|
||||||
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,
|
keyboardShortcuts: DEFAULT_KEYBOARD_SHORTCUTS,
|
||||||
aiProfiles: [],
|
aiProfiles: [],
|
||||||
projects: [],
|
projects: [],
|
||||||
@@ -411,9 +430,9 @@ export const DEFAULT_GLOBAL_SETTINGS: GlobalSettings = {
|
|||||||
export const DEFAULT_CREDENTIALS: Credentials = {
|
export const DEFAULT_CREDENTIALS: Credentials = {
|
||||||
version: 1,
|
version: 1,
|
||||||
apiKeys: {
|
apiKeys: {
|
||||||
anthropic: "",
|
anthropic: '',
|
||||||
google: "",
|
google: '',
|
||||||
openai: "",
|
openai: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user