mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-05 09:33:07 +00:00
refactor: replace fs with secureFs for improved file handling
This commit updates various modules to utilize the secure file system operations from the secureFs module instead of the native fs module. Key changes include: - Replaced fs imports with secureFs in multiple route handlers and services to enhance security and consistency in file operations. - Added centralized validation for working directories in the sdk-options module to ensure all AI model invocations are secure. These changes aim to improve the security and maintainability of file handling across the application.
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -29,35 +30,36 @@ export function createImageHandler() {
|
|||||||
|
|
||||||
// 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)) {
|
try {
|
||||||
res.status(403).json({
|
await secureFs.access(projectPath);
|
||||||
|
res.status(400).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Parent directory not allowed: ${parentDir}. Must be within ALLOWED_ROOT_DIRECTORY.`,
|
error: `Directory "${sanitizedName}" already exists in ${parentDir}`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
} catch (accessError) {
|
||||||
|
if (accessError instanceof PathNotAllowedError) {
|
||||||
// Validate that project path will be within allowed root directory
|
|
||||||
if (!isPathAllowed(resolvedProject)) {
|
|
||||||
res.status(403).json({
|
res.status(403).json({
|
||||||
success: false,
|
success: false,
|
||||||
error: `Project path not allowed: ${projectPath}. Must be within ALLOWED_ROOT_DIRECTORY.`,
|
error: `Project path not allowed: ${projectPath}. Must be within ALLOWED_ROOT_DIRECTORY.`,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if directory already exists
|
|
||||||
try {
|
|
||||||
await fs.access(projectPath);
|
|
||||||
res.status(400).json({
|
|
||||||
success: false,
|
|
||||||
error: `Directory "${sanitizedName}" already exists in ${parentDir}`,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} catch {
|
|
||||||
// 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)
|
|
||||||
.json({
|
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath and featureId required",
|
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,
|
cwd: worktreePath,
|
||||||
maxBuffer: 10 * 1024 * 1024,
|
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)
|
|
||||||
.json({
|
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath and featureId required",
|
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;
|
||||||
@@ -147,7 +153,7 @@ export function createListHandler() {
|
|||||||
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)
|
|
||||||
.json({
|
|
||||||
success: false,
|
success: false,
|
||||||
error: "projectPath and featureId required",
|
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();
|
||||||
}
|
}
|
||||||
|
|||||||
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">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
<Label className="text-foreground font-medium">
|
<Label className="text-foreground font-medium">
|
||||||
Theme{" "}
|
Theme{' '}
|
||||||
<span className="text-muted-foreground font-normal">
|
<span className="text-muted-foreground font-normal">
|
||||||
{currentProject ? `(for ${currentProject.name})` : "(Global)"}
|
{currentProject ? `(for ${currentProject.name})` : '(Global)'}
|
||||||
</span>
|
</span>
|
||||||
</Label>
|
</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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -519,8 +569,6 @@
|
|||||||
-webkit-backdrop-filter: blur(12px);
|
-webkit-backdrop-filter: blur(12px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* Hover state utilities */
|
/* Hover state utilities */
|
||||||
.hover-glass {
|
.hover-glass {
|
||||||
transition: background-color 0.2s ease;
|
transition: background-color 0.2s ease;
|
||||||
@@ -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,7 +634,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.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 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%);
|
0 4px 6px -4px color-mix(in oklch, var(--running-indicator), transparent 50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -632,8 +679,6 @@
|
|||||||
|
|
||||||
/* Retro Overrides for Utilities */
|
/* Retro Overrides for Utilities */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
.retro * {
|
.retro * {
|
||||||
border-radius: 0 !important;
|
border-radius: 0 !important;
|
||||||
}
|
}
|
||||||
@@ -646,12 +691,8 @@
|
|||||||
|
|
||||||
/* 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 {
|
||||||
@@ -663,41 +704,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* 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 {
|
||||||
@@ -709,55 +731,26 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/* 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