Merge main into massive-terminal-upgrade

Resolves merge conflicts:
- apps/server/src/routes/terminal/common.ts: Keep randomBytes import, use @automaker/utils for createLogger
- apps/ui/eslint.config.mjs: Use main's explicit globals list with XMLHttpRequest and MediaQueryListEvent additions
- apps/ui/src/components/views/terminal-view.tsx: Keep our terminal improvements (killAllSessions, beforeunload, better error handling)
- apps/ui/src/config/terminal-themes.ts: Keep our search highlight colors for all themes
- apps/ui/src/store/app-store.ts: Keep our terminal settings persistence improvements (merge function)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
SuperComboGamer
2025-12-21 20:27:44 -05:00
393 changed files with 32473 additions and 17974 deletions

View File

@@ -0,0 +1,26 @@
/**
* Common utilities for settings routes
*
* Provides logger and error handling utilities shared across all settings endpoints.
* Re-exports error handling helpers from the parent routes module.
*/
import { createLogger } from '@automaker/utils';
import { getErrorMessage as getErrorMessageShared, createLogError } from '../common.js';
/** Logger instance for settings-related operations */
export const logger = createLogger('Settings');
/**
* Extract user-friendly error message from error objects
*
* Re-exported from parent routes common module for consistency.
*/
export { getErrorMessageShared as getErrorMessage };
/**
* Log error with automatic logger binding
*
* Convenience function for logging errors with the Settings logger.
*/
export const logError = createLogError(logger);

View File

@@ -0,0 +1,76 @@
/**
* Settings routes - HTTP API for persistent file-based settings
*
* Provides endpoints for:
* - Status checking (migration readiness)
* - Global settings CRUD
* - Credentials management
* - Project-specific settings
* - localStorage to file migration
*
* All endpoints use handler factories that receive the SettingsService instance.
* Mounted at /api/settings in the main server.
*/
import { Router } from 'express';
import type { SettingsService } from '../../services/settings-service.js';
import { validatePathParams } from '../../middleware/validate-paths.js';
import { createGetGlobalHandler } from './routes/get-global.js';
import { createUpdateGlobalHandler } from './routes/update-global.js';
import { createGetCredentialsHandler } from './routes/get-credentials.js';
import { createUpdateCredentialsHandler } from './routes/update-credentials.js';
import { createGetProjectHandler } from './routes/get-project.js';
import { createUpdateProjectHandler } from './routes/update-project.js';
import { createMigrateHandler } from './routes/migrate.js';
import { createStatusHandler } from './routes/status.js';
/**
* Create settings router with all endpoints
*
* Registers handlers for all settings-related HTTP endpoints.
* Each handler is created with the provided SettingsService instance.
*
* Endpoints:
* - GET /status - Check migration status and data availability
* - GET /global - Get global settings
* - PUT /global - Update global settings
* - GET /credentials - Get masked credentials (safe for UI)
* - PUT /credentials - Update API keys
* - POST /project - Get project settings (requires projectPath in body)
* - PUT /project - Update project settings
* - POST /migrate - Migrate settings from localStorage
*
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express Router configured with all settings endpoints
*/
export function createSettingsRoutes(settingsService: SettingsService): Router {
const router = Router();
// Status endpoint (check if migration needed)
router.get('/status', createStatusHandler(settingsService));
// Global settings
router.get('/global', createGetGlobalHandler(settingsService));
router.put('/global', createUpdateGlobalHandler(settingsService));
// Credentials (separate for security)
router.get('/credentials', createGetCredentialsHandler(settingsService));
router.put('/credentials', createUpdateCredentialsHandler(settingsService));
// Project settings
router.post(
'/project',
validatePathParams('projectPath'),
createGetProjectHandler(settingsService)
);
router.put(
'/project',
validatePathParams('projectPath'),
createUpdateProjectHandler(settingsService)
);
// Migration from localStorage
router.post('/migrate', createMigrateHandler(settingsService));
return router;
}

View File

@@ -0,0 +1,35 @@
/**
* GET /api/settings/credentials - Get API key status (masked for security)
*
* Returns masked credentials showing which providers have keys configured.
* Each provider shows: `{ configured: boolean, masked: string }`
* Masked shows first 4 and last 4 characters for verification.
*
* Response: `{ "success": true, "credentials": { anthropic } }`
*/
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for GET /api/settings/credentials
*
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
export function createGetCredentialsHandler(settingsService: SettingsService) {
return async (_req: Request, res: Response): Promise<void> => {
try {
const credentials = await settingsService.getMaskedCredentials();
res.json({
success: true,
credentials,
});
} catch (error) {
logError(error, 'Get credentials failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,34 @@
/**
* GET /api/settings/global - Retrieve global user settings
*
* Returns the complete GlobalSettings object with all user preferences,
* keyboard shortcuts, AI profiles, and project history.
*
* Response: `{ "success": true, "settings": GlobalSettings }`
*/
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for GET /api/settings/global
*
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
export function createGetGlobalHandler(settingsService: SettingsService) {
return async (_req: Request, res: Response): Promise<void> => {
try {
const settings = await settingsService.getGlobalSettings();
res.json({
success: true,
settings,
});
} catch (error) {
logError(error, 'Get global settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,45 @@
/**
* POST /api/settings/project - Get project-specific settings
*
* Retrieves settings overrides for a specific project. Uses POST because
* projectPath may contain special characters that don't work well in URLs.
*
* Request body: `{ projectPath: string }`
* Response: `{ "success": true, "settings": ProjectSettings }`
*/
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for POST /api/settings/project
*
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
export function createGetProjectHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath } = req.body as { projectPath?: string };
if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({
success: false,
error: 'projectPath is required',
});
return;
}
const settings = await settingsService.getProjectSettings(projectPath);
res.json({
success: true,
settings,
});
} catch (error) {
logError(error, 'Get project settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,86 @@
/**
* POST /api/settings/migrate - Migrate settings from localStorage to file storage
*
* Called during onboarding when UI detects localStorage data but no settings files.
* Extracts settings from various localStorage keys and writes to new file structure.
* Collects errors but continues on partial failures (graceful degradation).
*
* Request body:
* ```json
* {
* "data": {
* "automaker-storage"?: string,
* "automaker-setup"?: string,
* "worktree-panel-collapsed"?: string,
* "file-browser-recent-folders"?: string,
* "automaker:lastProjectDir"?: string
* }
* }
* ```
*
* Response:
* ```json
* {
* "success": boolean,
* "migratedGlobalSettings": boolean,
* "migratedCredentials": boolean,
* "migratedProjectCount": number,
* "errors": string[]
* }
* ```
*/
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError, logger } from '../common.js';
/**
* Create handler factory for POST /api/settings/migrate
*
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
export function createMigrateHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { data } = req.body as {
data?: {
'automaker-storage'?: string;
'automaker-setup'?: string;
'worktree-panel-collapsed'?: string;
'file-browser-recent-folders'?: string;
'automaker:lastProjectDir'?: string;
};
};
if (!data || typeof data !== 'object') {
res.status(400).json({
success: false,
error: 'data object is required containing localStorage data',
});
return;
}
logger.info('Starting settings migration from localStorage');
const result = await settingsService.migrateFromLocalStorage(data);
if (result.success) {
logger.info(`Migration successful: ${result.migratedProjectCount} projects migrated`);
} else {
logger.warn(`Migration completed with errors: ${result.errors.join(', ')}`);
}
res.json({
success: result.success,
migratedGlobalSettings: result.migratedGlobalSettings,
migratedCredentials: result.migratedCredentials,
migratedProjectCount: result.migratedProjectCount,
errors: result.errors,
});
} catch (error) {
logError(error, 'Migration failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,47 @@
/**
* GET /api/settings/status - Get settings migration and availability status
*
* Checks which settings files exist to determine if migration from localStorage
* is needed. Used by UI during onboarding to decide whether to show migration flow.
*
* Response:
* ```json
* {
* "success": true,
* "hasGlobalSettings": boolean,
* "hasCredentials": boolean,
* "dataDir": string,
* "needsMigration": boolean
* }
* ```
*/
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for GET /api/settings/status
*
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
export function createStatusHandler(settingsService: SettingsService) {
return async (_req: Request, res: Response): Promise<void> => {
try {
const hasGlobalSettings = await settingsService.hasGlobalSettings();
const hasCredentials = await settingsService.hasCredentials();
res.json({
success: true,
hasGlobalSettings,
hasCredentials,
dataDir: settingsService.getDataDir(),
needsMigration: !hasGlobalSettings,
});
} catch (error) {
logError(error, 'Get settings status failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,49 @@
/**
* PUT /api/settings/credentials - Update API credentials
*
* Updates API keys for Anthropic. Partial updates supported.
* Returns masked credentials for verification without exposing full keys.
*
* Request body: `Partial<Credentials>` (usually just apiKeys)
* Response: `{ "success": true, "credentials": { anthropic } }`
*/
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import type { Credentials } from '../../../types/settings.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for PUT /api/settings/credentials
*
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
export function createUpdateCredentialsHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const updates = req.body as Partial<Credentials>;
if (!updates || typeof updates !== 'object') {
res.status(400).json({
success: false,
error: 'Invalid request body - expected credentials object',
});
return;
}
await settingsService.updateCredentials(updates);
// Return masked credentials for confirmation
const masked = await settingsService.getMaskedCredentials();
res.json({
success: true,
credentials: masked,
});
} catch (error) {
logError(error, 'Update credentials failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,46 @@
/**
* PUT /api/settings/global - Update global user settings
*
* Accepts partial GlobalSettings update. Fields provided are merged into
* existing settings (not replaced). Returns updated settings.
*
* Request body: `Partial<GlobalSettings>`
* Response: `{ "success": true, "settings": GlobalSettings }`
*/
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import type { GlobalSettings } from '../../../types/settings.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for PUT /api/settings/global
*
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
export function createUpdateGlobalHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const updates = req.body as Partial<GlobalSettings>;
if (!updates || typeof updates !== 'object') {
res.status(400).json({
success: false,
error: 'Invalid request body - expected settings object',
});
return;
}
const settings = await settingsService.updateGlobalSettings(updates);
res.json({
success: true,
settings,
});
} catch (error) {
logError(error, 'Update global settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}

View File

@@ -0,0 +1,57 @@
/**
* PUT /api/settings/project - Update project-specific settings
*
* Updates settings for a specific project. Partial updates supported.
* Project settings override global settings when present.
*
* Request body: `{ projectPath: string, updates: Partial<ProjectSettings> }`
* Response: `{ "success": true, "settings": ProjectSettings }`
*/
import type { Request, Response } from 'express';
import type { SettingsService } from '../../../services/settings-service.js';
import type { ProjectSettings } from '../../../types/settings.js';
import { getErrorMessage, logError } from '../common.js';
/**
* Create handler factory for PUT /api/settings/project
*
* @param settingsService - Instance of SettingsService for file I/O
* @returns Express request handler
*/
export function createUpdateProjectHandler(settingsService: SettingsService) {
return async (req: Request, res: Response): Promise<void> => {
try {
const { projectPath, updates } = req.body as {
projectPath?: string;
updates?: Partial<ProjectSettings>;
};
if (!projectPath || typeof projectPath !== 'string') {
res.status(400).json({
success: false,
error: 'projectPath is required',
});
return;
}
if (!updates || typeof updates !== 'object') {
res.status(400).json({
success: false,
error: 'updates object is required',
});
return;
}
const settings = await settingsService.updateProjectSettings(projectPath, updates);
res.json({
success: true,
settings,
});
} catch (error) {
logError(error, 'Update project settings failed');
res.status(500).json({ success: false, error: getErrorMessage(error) });
}
};
}