mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-03-18 22:33:08 +00:00
* feat(terminal): Add core infrastructure for custom terminal configurations - Add TerminalConfig types to settings schema (global & project-specific) - Create RC generator with hex-to-xterm-256 color mapping - Create RC file manager for .automaker/terminal/ directory - Add terminal theme color data (40 themes) to platform package - Integrate terminal config injection into TerminalService - Support bash, zsh, and sh with proper env var injection (BASH_ENV, ZDOTDIR, ENV) - Add onThemeChange hook for theme synchronization Part of custom terminal configurations feature implementation. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(terminal): Wire terminal service with settings service - Pass SettingsService to TerminalService constructor - Initialize terminal service with settings service dependency - Enable terminal config injection to work with actual settings This completes Steps 1-4 of the terminal configuration plan: - RC Generator (color mapping, prompt formats) - RC File Manager (file I/O, atomic writes) - Settings Schema (GlobalSettings + ProjectSettings) - Terminal Service Integration (env var injection) Next steps: Settings UI and theme change hooks. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * feat(terminal): Add Settings UI and theme change synchronization Complete Steps 5 & 6 of terminal configuration implementation: Settings UI Components: - Add PromptPreview component with live theme-aware rendering - Add TerminalConfigSection with comprehensive controls: * Enable/disable toggle with confirmation dialog * Custom prompt toggle * Prompt format selector (4 formats) * Git branch/status toggles * Custom aliases textarea * Custom env vars key-value editor with validation * Info box explaining behavior - Integrate into existing TerminalSection Theme Change Hook: - Add theme detection in update-global settings route - Regenerate RC files for all projects when theme changes - Skip projects with terminal config disabled - Error handling with per-project logging - Inject terminal service with settings service dependency This completes the full terminal configuration feature: ✓ RC Generator (color mapping, prompts) ✓ RC File Manager (file I/O, versioning) ✓ Settings Schema (types, defaults) ✓ Terminal Service Integration (env vars, PTY spawn) ✓ Settings UI (comprehensive controls, preview) ✓ Theme Synchronization (automatic RC regeneration) New terminals will use custom prompts matching app theme. Existing terminals unaffected. User RC files preserved. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(terminal): Add error handling and explicit field mapping for terminal config - Add try-catch block to handleToggleEnabled - Explicitly set all required terminalConfig fields - Add console logging for debugging - Show error toast if update fails - Include rcFileVersion: 1 in config object This should fix the issue where the toggle doesn't enable after clicking OK in the confirmation dialog. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(terminal): Use React Query mutation hook for settings updates The issue was that `updateGlobalSettings` doesn't exist in the app store. The correct pattern is to use the `useUpdateGlobalSettings` hook from use-settings-mutations.ts, which is a React Query mutation. Changes: - Import useUpdateGlobalSettings from mutations hook - Use mutation.mutate() instead of direct function call - Add proper onSuccess/onError callbacks - Remove async/await pattern (React Query handles this) This fixes the toggle not enabling after clicking OK in the confirmation dialog. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * fix(terminal): Use React Query hook for globalSettings instead of store The root cause: Component was reading globalSettings from the app store, which doesn't update reactively when the mutation completes. Solution: Use useGlobalSettings() React Query hook which: - Automatically refetches when the mutation invalidates the cache - Triggers re-render with updated data - Makes the toggle reflect the new state Now the flow is: 1. User clicks toggle → confirmation dialog 2. Click OK → mutation.mutate() called 3. Mutation succeeds → invalidates queryKeys.settings.global() 4. Query refetches → component re-renders with new globalSettings 5. Toggle shows enabled state ✓ Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * debug(terminal): Add detailed logging for terminal config application Add logging to track: - When terminal config check happens - CWD being used - Global and project enabled states - Effective enabled state This will help diagnose why RC files aren't being generated when opening terminals in Automaker. Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> * Fix terminal rc updates and bash rcfile loading * feat(terminal): add banner on shell start * feat(terminal): colorize banner per theme * chore(terminal): bump rc version for banner colors * feat(terminal): match banner colors to launcher * feat(terminal): add prompt customization controls * feat: integrate oh-my-posh prompt themes * fix: resolve oh-my-posh theme path * fix: correct oh-my-posh config invocation * docs: add terminal theme screenshot * fix: address review feedback and stabilize e2e test * ui: split terminal config into separate card * fix: enable cross-platform Warp terminal detection - Remove macOS-only platform restriction for Warp - Add Linux CLI alias 'warp-terminal' (primary on Linux) - Add CLI launch handler using --cwd flag - Fixes issue where Warp was not detected on Linux systems Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.5 <noreply@anthropic.com>
309 lines
8.9 KiB
TypeScript
309 lines
8.9 KiB
TypeScript
/**
|
|
* RC File Manager - Manage shell configuration files in .automaker/terminal/
|
|
*
|
|
* This module handles file I/O operations for generating and managing shell RC files,
|
|
* including version checking and regeneration logic.
|
|
*/
|
|
|
|
import * as fs from 'node:fs/promises';
|
|
import * as path from 'node:path';
|
|
import { createHash } from 'node:crypto';
|
|
import type { ThemeMode } from '@automaker/types';
|
|
import {
|
|
generateBashrc,
|
|
generateZshrc,
|
|
generateCommonFunctions,
|
|
generateThemeColors,
|
|
type TerminalConfig,
|
|
type TerminalTheme,
|
|
} from './rc-generator.js';
|
|
|
|
/**
|
|
* Current RC file format version
|
|
*/
|
|
export const RC_FILE_VERSION = 11;
|
|
|
|
const RC_SIGNATURE_FILENAME = 'config.sha256';
|
|
|
|
/**
|
|
* Get the terminal directory path
|
|
*/
|
|
export function getTerminalDir(projectPath: string): string {
|
|
return path.join(projectPath, '.automaker', 'terminal');
|
|
}
|
|
|
|
/**
|
|
* Get the themes directory path
|
|
*/
|
|
export function getThemesDir(projectPath: string): string {
|
|
return path.join(getTerminalDir(projectPath), 'themes');
|
|
}
|
|
|
|
/**
|
|
* Get RC file path for specific shell
|
|
*/
|
|
export function getRcFilePath(projectPath: string, shell: 'bash' | 'zsh' | 'sh'): string {
|
|
const terminalDir = getTerminalDir(projectPath);
|
|
switch (shell) {
|
|
case 'bash':
|
|
return path.join(terminalDir, 'bashrc.sh');
|
|
case 'zsh':
|
|
return path.join(terminalDir, '.zshrc'); // Zsh looks for .zshrc in ZDOTDIR
|
|
case 'sh':
|
|
return path.join(terminalDir, 'common.sh');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ensure terminal directory exists
|
|
*/
|
|
export async function ensureTerminalDir(projectPath: string): Promise<void> {
|
|
const terminalDir = getTerminalDir(projectPath);
|
|
const themesDir = getThemesDir(projectPath);
|
|
|
|
await fs.mkdir(terminalDir, { recursive: true, mode: 0o755 });
|
|
await fs.mkdir(themesDir, { recursive: true, mode: 0o755 });
|
|
}
|
|
|
|
/**
|
|
* Write RC file with atomic write (write to temp, then rename)
|
|
*/
|
|
async function atomicWriteFile(
|
|
filePath: string,
|
|
content: string,
|
|
mode: number = 0o644
|
|
): Promise<void> {
|
|
const tempPath = `${filePath}.tmp`;
|
|
await fs.writeFile(tempPath, content, { encoding: 'utf8', mode });
|
|
await fs.rename(tempPath, filePath);
|
|
}
|
|
|
|
function sortObjectKeys(value: unknown): unknown {
|
|
if (Array.isArray(value)) {
|
|
return value.map((item) => sortObjectKeys(item));
|
|
}
|
|
|
|
if (value && typeof value === 'object') {
|
|
const sortedEntries = Object.entries(value as Record<string, unknown>)
|
|
.filter(([, entryValue]) => entryValue !== undefined)
|
|
.sort(([left], [right]) => left.localeCompare(right));
|
|
|
|
const sortedObject: Record<string, unknown> = {};
|
|
for (const [key, entryValue] of sortedEntries) {
|
|
sortedObject[key] = sortObjectKeys(entryValue);
|
|
}
|
|
return sortedObject;
|
|
}
|
|
|
|
return value;
|
|
}
|
|
|
|
function buildConfigSignature(theme: ThemeMode, config: TerminalConfig): string {
|
|
const payload = { theme, config: sortObjectKeys(config) };
|
|
const serializedPayload = JSON.stringify(payload);
|
|
return createHash('sha256').update(serializedPayload).digest('hex');
|
|
}
|
|
|
|
async function readSignatureFile(projectPath: string): Promise<string | null> {
|
|
const signaturePath = path.join(getTerminalDir(projectPath), RC_SIGNATURE_FILENAME);
|
|
try {
|
|
const signature = await fs.readFile(signaturePath, 'utf8');
|
|
return signature.trim() || null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
async function writeSignatureFile(projectPath: string, signature: string): Promise<void> {
|
|
const signaturePath = path.join(getTerminalDir(projectPath), RC_SIGNATURE_FILENAME);
|
|
await atomicWriteFile(signaturePath, `${signature}\n`, 0o644);
|
|
}
|
|
|
|
/**
|
|
* Check current RC file version
|
|
*/
|
|
export async function checkRcFileVersion(projectPath: string): Promise<number | null> {
|
|
const versionPath = path.join(getTerminalDir(projectPath), 'version.txt');
|
|
try {
|
|
const content = await fs.readFile(versionPath, 'utf8');
|
|
const version = parseInt(content.trim(), 10);
|
|
return isNaN(version) ? null : version;
|
|
} catch (error) {
|
|
return null; // File doesn't exist or can't be read
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write version file
|
|
*/
|
|
async function writeVersionFile(projectPath: string, version: number): Promise<void> {
|
|
const versionPath = path.join(getTerminalDir(projectPath), 'version.txt');
|
|
await atomicWriteFile(versionPath, `${version}\n`, 0o644);
|
|
}
|
|
|
|
/**
|
|
* Check if RC files need regeneration
|
|
*/
|
|
export async function needsRegeneration(
|
|
projectPath: string,
|
|
theme: ThemeMode,
|
|
config: TerminalConfig
|
|
): Promise<boolean> {
|
|
const currentVersion = await checkRcFileVersion(projectPath);
|
|
|
|
// Regenerate if version doesn't match or files don't exist
|
|
if (currentVersion !== RC_FILE_VERSION) {
|
|
return true;
|
|
}
|
|
|
|
const expectedSignature = buildConfigSignature(theme, config);
|
|
const existingSignature = await readSignatureFile(projectPath);
|
|
if (!existingSignature || existingSignature !== expectedSignature) {
|
|
return true;
|
|
}
|
|
|
|
// Check if critical files exist
|
|
const bashrcPath = getRcFilePath(projectPath, 'bash');
|
|
const zshrcPath = getRcFilePath(projectPath, 'zsh');
|
|
const commonPath = path.join(getTerminalDir(projectPath), 'common.sh');
|
|
const themeFilePath = path.join(getThemesDir(projectPath), `${theme}.sh`);
|
|
|
|
try {
|
|
await Promise.all([
|
|
fs.access(bashrcPath),
|
|
fs.access(zshrcPath),
|
|
fs.access(commonPath),
|
|
fs.access(themeFilePath),
|
|
]);
|
|
return false; // All files exist
|
|
} catch {
|
|
return true; // Some files are missing
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Write all theme color files (all 40 themes)
|
|
*/
|
|
export async function writeAllThemeFiles(
|
|
projectPath: string,
|
|
terminalThemes: Record<ThemeMode, TerminalTheme>
|
|
): Promise<void> {
|
|
const themesDir = getThemesDir(projectPath);
|
|
await fs.mkdir(themesDir, { recursive: true, mode: 0o755 });
|
|
|
|
const themeEntries = Object.entries(terminalThemes);
|
|
await Promise.all(
|
|
themeEntries.map(async ([themeName, theme]) => {
|
|
const themeFilePath = path.join(themesDir, `${themeName}.sh`);
|
|
const content = generateThemeColors(theme);
|
|
await atomicWriteFile(themeFilePath, content, 0o644);
|
|
})
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Write a single theme color file
|
|
*/
|
|
export async function writeThemeFile(
|
|
projectPath: string,
|
|
theme: ThemeMode,
|
|
themeColors: TerminalTheme
|
|
): Promise<void> {
|
|
const themesDir = getThemesDir(projectPath);
|
|
await fs.mkdir(themesDir, { recursive: true, mode: 0o755 });
|
|
|
|
const themeFilePath = path.join(themesDir, `${theme}.sh`);
|
|
const content = generateThemeColors(themeColors);
|
|
await atomicWriteFile(themeFilePath, content, 0o644);
|
|
}
|
|
|
|
/**
|
|
* Write all RC files
|
|
*/
|
|
export async function writeRcFiles(
|
|
projectPath: string,
|
|
theme: ThemeMode,
|
|
config: TerminalConfig,
|
|
themeColors: TerminalTheme,
|
|
allThemes: Record<ThemeMode, TerminalTheme>
|
|
): Promise<void> {
|
|
await ensureTerminalDir(projectPath);
|
|
|
|
// Write common functions file
|
|
const commonPath = path.join(getTerminalDir(projectPath), 'common.sh');
|
|
const commonContent = generateCommonFunctions(config);
|
|
await atomicWriteFile(commonPath, commonContent, 0o644);
|
|
|
|
// Write bashrc
|
|
const bashrcPath = getRcFilePath(projectPath, 'bash');
|
|
const bashrcContent = generateBashrc(themeColors, config);
|
|
await atomicWriteFile(bashrcPath, bashrcContent, 0o644);
|
|
|
|
// Write zshrc
|
|
const zshrcPath = getRcFilePath(projectPath, 'zsh');
|
|
const zshrcContent = generateZshrc(themeColors, config);
|
|
await atomicWriteFile(zshrcPath, zshrcContent, 0o644);
|
|
|
|
// Write all theme files (40 themes)
|
|
await writeAllThemeFiles(projectPath, allThemes);
|
|
|
|
// Write version file
|
|
await writeVersionFile(projectPath, RC_FILE_VERSION);
|
|
|
|
// Write config signature for change detection
|
|
const signature = buildConfigSignature(theme, config);
|
|
await writeSignatureFile(projectPath, signature);
|
|
}
|
|
|
|
/**
|
|
* Ensure RC files are up to date
|
|
*/
|
|
export async function ensureRcFilesUpToDate(
|
|
projectPath: string,
|
|
theme: ThemeMode,
|
|
config: TerminalConfig,
|
|
themeColors: TerminalTheme,
|
|
allThemes: Record<ThemeMode, TerminalTheme>
|
|
): Promise<void> {
|
|
const needsRegen = await needsRegeneration(projectPath, theme, config);
|
|
if (needsRegen) {
|
|
await writeRcFiles(projectPath, theme, config, themeColors, allThemes);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete terminal directory (for disable flow)
|
|
*/
|
|
export async function deleteTerminalDir(projectPath: string): Promise<void> {
|
|
const terminalDir = getTerminalDir(projectPath);
|
|
try {
|
|
await fs.rm(terminalDir, { recursive: true, force: true });
|
|
} catch (error) {
|
|
// Ignore errors if directory doesn't exist
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Create user-custom.sh placeholder if it doesn't exist
|
|
*/
|
|
export async function ensureUserCustomFile(projectPath: string): Promise<void> {
|
|
const userCustomPath = path.join(getTerminalDir(projectPath), 'user-custom.sh');
|
|
try {
|
|
await fs.access(userCustomPath);
|
|
} catch {
|
|
// File doesn't exist, create it
|
|
const content = `#!/bin/sh
|
|
# Automaker User Customizations
|
|
# Add your custom shell configuration here
|
|
# This file will not be overwritten by Automaker
|
|
|
|
# Example: Add custom aliases
|
|
# alias myalias='command'
|
|
|
|
# Example: Add custom environment variables
|
|
# export MY_VAR="value"
|
|
`;
|
|
await atomicWriteFile(userCustomPath, content, 0o644);
|
|
}
|
|
}
|